diff options
Diffstat (limited to 'node_modules/@jet-app')
466 files changed, 76151 insertions, 0 deletions
diff --git a/node_modules/@jet-app/app-store/tmp/src/api/intents/app-event-page-intent.js b/node_modules/@jet-app/app-store/tmp/src/api/intents/app-event-page-intent.js new file mode 100644 index 0000000..1913f2a --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/api/intents/app-event-page-intent.js @@ -0,0 +1,14 @@ +import { normalizePreviewPlaform } from "../models/preview-platform"; +const APP_EVENT_PAGE_INTENT_KIND = "AppEventPageIntent"; +export function makeAppEventPageIntent(opts) { + const { platform, ...rest } = opts; + return { + ...rest, + $kind: APP_EVENT_PAGE_INTENT_KIND, + platform: normalizePreviewPlaform(platform), + }; +} +export function isAppEventPageIntent(intent) { + return (intent === null || intent === void 0 ? void 0 : intent.$kind) === APP_EVENT_PAGE_INTENT_KIND; +} +//# sourceMappingURL=app-event-page-intent.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/api/intents/arcade-grouping-page-intent.js b/node_modules/@jet-app/app-store/tmp/src/api/intents/arcade-grouping-page-intent.js new file mode 100644 index 0000000..a24df96 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/api/intents/arcade-grouping-page-intent.js @@ -0,0 +1,10 @@ +import { normalizePreviewPlaform } from "../models/preview-platform"; +export function makeArcadeGroupingPageIntent(opts) { + const { platform, ...rest } = opts; + return { + ...rest, + platform: normalizePreviewPlaform(platform), + $kind: "ArcadeGroupingPageIntent", + }; +} +//# sourceMappingURL=arcade-grouping-page-intent.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/api/intents/bundle-page-intent.js b/node_modules/@jet-app/app-store/tmp/src/api/intents/bundle-page-intent.js new file mode 100644 index 0000000..b6bed12 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/api/intents/bundle-page-intent.js @@ -0,0 +1,16 @@ +import { normalizeAdamID } from "../util"; +import { normalizePreviewPlaform } from "../models/preview-platform"; +export function makeBundlePageIntent(opts) { + const { id, platform, lic, ...rest } = opts; + return { + $kind: "BundlePageIntent", + id: normalizeAdamID(id), + platform: normalizePreviewPlaform(platform), + lic, + ...rest, + }; +} +export function isBundlePageIntent(intent) { + return (intent === null || intent === void 0 ? void 0 : intent.$kind) === "BundlePageIntent"; +} +//# sourceMappingURL=bundle-page-intent.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/api/intents/category-tabs-intent.js b/node_modules/@jet-app/app-store/tmp/src/api/intents/category-tabs-intent.js new file mode 100644 index 0000000..0b2ec9e --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/api/intents/category-tabs-intent.js @@ -0,0 +1,8 @@ +import { normalizePreviewPlaform } from "../models/preview-platform"; +export function makeCategoryTabsIntent(opts) { + return { + $kind: "CategoryTabsIntent", + platform: normalizePreviewPlaform(opts.platform), + }; +} +//# sourceMappingURL=category-tabs-intent.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/api/intents/charts-hub-page-intent.js b/node_modules/@jet-app/app-store/tmp/src/api/intents/charts-hub-page-intent.js new file mode 100644 index 0000000..bb964f0 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/api/intents/charts-hub-page-intent.js @@ -0,0 +1,13 @@ +import { normalizePreviewPlaform } from "../models/preview-platform"; +/** + * Create a {@linkcode ChartsHubPageIntent} + */ +export function makeChartsHubPageIntent(opts) { + const { platform, ...rest } = opts; + return { + ...rest, + platform: normalizePreviewPlaform(platform), + $kind: "ChartsHubPageIntent", + }; +} +//# sourceMappingURL=charts-hub-page-intent.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/api/intents/charts-page-intent.js b/node_modules/@jet-app/app-store/tmp/src/api/intents/charts-page-intent.js new file mode 100644 index 0000000..e900b06 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/api/intents/charts-page-intent.js @@ -0,0 +1,13 @@ +import { normalizePreviewPlaform } from "../models/preview-platform"; +const CHARTS_PAGE_INTENT_KIND = "ChartsPageIntent"; +export function makeChartsPageIntent({ platform, ...rest }) { + return { + ...rest, + platform: normalizePreviewPlaform(platform), + $kind: CHARTS_PAGE_INTENT_KIND, + }; +} +export function isChartsPageIntent(intent) { + return (intent === null || intent === void 0 ? void 0 : intent.$kind) === CHARTS_PAGE_INTENT_KIND; +} +//# sourceMappingURL=charts-page-intent.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/api/intents/developer-page-intent.js b/node_modules/@jet-app/app-store/tmp/src/api/intents/developer-page-intent.js new file mode 100644 index 0000000..a9633b6 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/api/intents/developer-page-intent.js @@ -0,0 +1,19 @@ +import { normalizeAdamID } from "../util"; +const DEVELOPER_PAGE_INTENT_KIND = "DeveloperPageIntent"; +/** + * Determines if {@linkcode intent} is a {@linkcode DeveloperPageIntent} + */ +export function isDeveloperPageIntent(intent) { + return intent.$kind === DEVELOPER_PAGE_INTENT_KIND; +} +/** + * Creates a {@linkcode DeveloperPageIntent} + */ +export function makeDeveloperPageIntent({ id, ...rest }) { + return { + ...rest, + id: normalizeAdamID(id), + $kind: DEVELOPER_PAGE_INTENT_KIND, + }; +} +//# sourceMappingURL=developer-page-intent.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/api/intents/editorial/editorial-page-intent.js b/node_modules/@jet-app/app-store/tmp/src/api/intents/editorial/editorial-page-intent.js new file mode 100644 index 0000000..353a168 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/api/intents/editorial/editorial-page-intent.js @@ -0,0 +1,37 @@ +import { normalizePreviewPlaform } from "../../models/preview-platform"; +export function isEditorialPageIntent(intent) { + return (intent === null || intent === void 0 ? void 0 : intent.$kind) === "EditorialPageIntent"; +} +export function isEditorialPageIntentByID(intent) { + return isEditorialPageIntent(intent) && "id" in intent; +} +export function isEditorialPageIntentByName(intent) { + return isEditorialPageIntent(intent) && "name" in intent; +} +/** + * Creates an {@link EditorialPageIntent} that fetches an Editorial Page by "name" + * + * @param options the properties of the created {@link EditorialPageIntentByName} + */ +export function makeEditorialPageIntentByName(options) { + const { platform, ...rest } = options; + return { + ...rest, + $kind: "EditorialPageIntent", + platform: normalizePreviewPlaform(platform), + }; +} +/** + * Creates an {@link EditorialPageIntent} that fetches an Editorial Page by "id" + * + * @param options the properties of the created {@linkcode EditorialPageIntentById} + */ +export function makeEditorialPageIntentByID(options) { + const { platform, ...rest } = options; + return { + $kind: "EditorialPageIntent", + platform: normalizePreviewPlaform(platform), + ...rest, + }; +} +//# sourceMappingURL=editorial-page-intent.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/api/intents/editorial/editorial-shelf-collection-page-intent.js b/node_modules/@jet-app/app-store/tmp/src/api/intents/editorial/editorial-shelf-collection-page-intent.js new file mode 100644 index 0000000..0e9b9af --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/api/intents/editorial/editorial-shelf-collection-page-intent.js @@ -0,0 +1,14 @@ +import { normalizePreviewPlaform } from "../../models/preview-platform"; +const EDITORIAL_SHELF_COLLECTION_PAGE_INTENT_KIND = "EditorialShelfCollectionPageIntent"; +export function isEditorialShelfCollectionPageIntent(intent) { + return intent.$kind === EDITORIAL_SHELF_COLLECTION_PAGE_INTENT_KIND; +} +export function makeEditorialShelfCollectionPageIntent(opts) { + const { platform, ...rest } = opts; + return { + ...rest, + $kind: EDITORIAL_SHELF_COLLECTION_PAGE_INTENT_KIND, + platform: normalizePreviewPlaform(platform), + }; +} +//# sourceMappingURL=editorial-shelf-collection-page-intent.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/api/intents/eula-page-intent.js b/node_modules/@jet-app/app-store/tmp/src/api/intents/eula-page-intent.js new file mode 100644 index 0000000..f040ff9 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/api/intents/eula-page-intent.js @@ -0,0 +1,19 @@ +const EULA_PAGE_INTENT_KIND = "EulaPageIntent"; +/** + * Determines if {@linkcode intent} is a {@linkcode EulaPageIntent} + */ +export function isEulaPageIntent(intent) { + return intent.$kind === EULA_PAGE_INTENT_KIND; +} +/** + * Creates a {@linkcode EulaPageIntent} + */ +export function makeEulaPageIntent({ resourceId, resourceType, ...rest }) { + return { + ...rest, + resourceId, + resourceType, + $kind: EULA_PAGE_INTENT_KIND, + }; +} +//# sourceMappingURL=eula-page-intent.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/api/intents/grouping-page-intent.js b/node_modules/@jet-app/app-store/tmp/src/api/intents/grouping-page-intent.js new file mode 100644 index 0000000..5d381a0 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/api/intents/grouping-page-intent.js @@ -0,0 +1,33 @@ +import { normalizePreviewPlaform } from "../models/preview-platform"; +import { normalizeAdamID } from "../util"; +export function isGroupingPageIntent(intent) { + return intent.$kind === "GroupingPageIntent"; +} +/** + * Creates a {@link GroupingPageIntent} "by name" + * + * @param options the properties of the created {@link GroupingPageIntent} + */ +export function makeGroupingPageIntentByName(options) { + const { platform, ...rest } = options; + return { + ...rest, + $kind: "GroupingPageIntent", + platform: normalizePreviewPlaform(platform), + }; +} +/** + * Creates a {@link GroupingPageIntent} "by ID" + * + * @param options the properties of the created {@link GroupingPageIntent} + */ +export function makeGroupingPageIntentByID(options) { + const { platform, id, ...rest } = options; + return { + ...rest, + $kind: "GroupingPageIntent", + id: normalizeAdamID(id), + platform: normalizePreviewPlaform(platform), + }; +} +//# sourceMappingURL=grouping-page-intent.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/api/intents/product-page-intent.js b/node_modules/@jet-app/app-store/tmp/src/api/intents/product-page-intent.js new file mode 100644 index 0000000..d8100ea --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/api/intents/product-page-intent.js @@ -0,0 +1,17 @@ +import { normalizeAdamID } from "../util"; +import { normalizePreviewPlaform } from "../models/preview-platform"; +export function makeProductPageIntent(opts) { + const { id, platform, ppid, lic, ...rest } = opts; + return { + ...rest, + $kind: "ProductPageIntent", + id: normalizeAdamID(id), + platform: normalizePreviewPlaform(platform), + ppid, + lic, + }; +} +export function isProductPageIntent(intent) { + return (intent === null || intent === void 0 ? void 0 : intent.$kind) === "ProductPageIntent"; +} +//# sourceMappingURL=product-page-intent.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/api/intents/room-page-intent.js b/node_modules/@jet-app/app-store/tmp/src/api/intents/room-page-intent.js new file mode 100644 index 0000000..f5ee4bf --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/api/intents/room-page-intent.js @@ -0,0 +1,16 @@ +import { normalizePreviewPlaform } from "../models/preview-platform"; +import { normalizeAdamID } from "../util"; +const ROOM_PAGE_INTENT_KIND = "RoomPageIntent"; +export function makeRoomPageIntent(opts) { + const { platform, id, ...rest } = opts; + return { + ...rest, + platform: normalizePreviewPlaform(platform), + id: normalizeAdamID(id), + $kind: ROOM_PAGE_INTENT_KIND, + }; +} +export function isRoomPageIntent(intent) { + return (intent === null || intent === void 0 ? void 0 : intent.$kind) === ROOM_PAGE_INTENT_KIND; +} +//# sourceMappingURL=room-page-intent.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/api/intents/routable-arcade-see-all-page-intent.js b/node_modules/@jet-app/app-store/tmp/src/api/intents/routable-arcade-see-all-page-intent.js new file mode 100644 index 0000000..0a09491 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/api/intents/routable-arcade-see-all-page-intent.js @@ -0,0 +1,11 @@ +import { normalizePreviewPlaform } from "../models/preview-platform"; +const ROUTABLE_ARCADE_SEE_ALL_PAGE_INTENT_KIND = "RoutableArcadeSeeAllPageIntent"; +export function makeRoutableArcadeSeeAllPageIntent(opts) { + const { platform, ...rest } = opts; + return { + ...rest, + platform: normalizePreviewPlaform(platform), + $kind: "RoutableArcadeSeeAllPageIntent", + }; +} +//# sourceMappingURL=routable-arcade-see-all-page-intent.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/api/intents/routable-article-page-intent.js b/node_modules/@jet-app/app-store/tmp/src/api/intents/routable-article-page-intent.js new file mode 100644 index 0000000..1f41b32 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/api/intents/routable-article-page-intent.js @@ -0,0 +1,13 @@ +import { normalizePreviewPlaform } from "../models/preview-platform"; +import { normalizeAdamID } from "../util"; +const ROUTABLE_ARTICLE_PAGE_INTENT_KIND = "RoutableArticlePageIntent"; +export function makeRoutableArticlePageIntent(opts) { + const { id, platform, ...rest } = opts; + return { + ...rest, + id: normalizeAdamID(id), + platform: normalizePreviewPlaform(platform), + $kind: ROUTABLE_ARTICLE_PAGE_INTENT_KIND, + }; +} +//# sourceMappingURL=routable-article-page-intent.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/api/intents/routable-intent.js b/node_modules/@jet-app/app-store/tmp/src/api/intents/routable-intent.js new file mode 100644 index 0000000..3769762 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/api/intents/routable-intent.js @@ -0,0 +1,4 @@ +export function isRoutableIntent(intent) { + return "storefront" in intent && "language" in intent; +} +//# sourceMappingURL=routable-intent.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/api/intents/routable-today-page-intent.js b/node_modules/@jet-app/app-store/tmp/src/api/intents/routable-today-page-intent.js new file mode 100644 index 0000000..0a5c109 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/api/intents/routable-today-page-intent.js @@ -0,0 +1,10 @@ +import { normalizePreviewPlaform } from "../models/preview-platform"; +export function makeRoutableTodayPageIntent(opts) { + const { platform, ...rest } = opts; + return { + ...rest, + platform: normalizePreviewPlaform(platform), + $kind: "RoutableTodayPageIntent", + }; +} +//# sourceMappingURL=routable-today-page-intent.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/api/intents/search-results-page-intent.js b/node_modules/@jet-app/app-store/tmp/src/api/intents/search-results-page-intent.js new file mode 100644 index 0000000..94edcf7 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/api/intents/search-results-page-intent.js @@ -0,0 +1,19 @@ +import { normalizePreviewPlaform } from "../models/preview-platform"; +export function makeSearchResultsPageIntent(opts) { + const { platform, ...rest } = opts; + return { + ...rest, + platform: normalizePreviewPlaform(platform), + $kind: "SearchResultsPageIntent", + }; +} +export function makeSearchResultsPageIntentFromURLParams(opts) { + return makeSearchResultsPageIntent({ + ...opts, + origin: "userText", + }); +} +export function isSearchResultsPageIntent(intent) { + return intent.$kind === "SearchResultsPageIntent"; +} +//# sourceMappingURL=search-results-page-intent.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/api/intents/search/search-landing-page-intent.js b/node_modules/@jet-app/app-store/tmp/src/api/intents/search/search-landing-page-intent.js new file mode 100644 index 0000000..74b6374 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/api/intents/search/search-landing-page-intent.js @@ -0,0 +1,14 @@ +import { normalizePreviewPlaform } from "../../models/preview-platform"; +const SEARCH_LANDING_PAGE_INTENT_KIND = "SearchLandingPageIntent"; +export function makeSearchLandingPageIntent(opts) { + const { platform, ...rest } = opts; + return { + ...rest, + platform: normalizePreviewPlaform(platform), + $kind: SEARCH_LANDING_PAGE_INTENT_KIND, + }; +} +export function isSearchLandingPageIntent(intent) { + return (intent === null || intent === void 0 ? void 0 : intent.$kind) === SEARCH_LANDING_PAGE_INTENT_KIND; +} +//# sourceMappingURL=search-landing-page-intent.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/api/intents/see-all-page-intent.js b/node_modules/@jet-app/app-store/tmp/src/api/intents/see-all-page-intent.js new file mode 100644 index 0000000..2fb8203 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/api/intents/see-all-page-intent.js @@ -0,0 +1,13 @@ +import { normalizePreviewPlaform } from "../models/preview-platform"; +import { normalizeAdamID } from "../util"; +export const SEE_ALL_TYPES = ["reviews", "customers-also-bought-apps", "developer-other-apps"]; +export function makeSeeAllPageIntent(opts) { + return { + ...opts, + "$kind": "SeeAllPageIntent", + "id": normalizeAdamID(opts.id), + "platform": normalizePreviewPlaform(opts.platform), + "see-all": opts["see-all"], + }; +} +//# sourceMappingURL=see-all-page-intent.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/api/models/accessibility.js b/node_modules/@jet-app/app-store/tmp/src/api/models/accessibility.js new file mode 100644 index 0000000..5af649e --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/api/models/accessibility.js @@ -0,0 +1,37 @@ +import * as models from "./index"; +/** + * @public + * A model that represents an accessibility feature of a product. + */ +export class AccessibilityFeature extends models.Model { + constructor(title, description, artwork) { + super(); + this.title = title; + this.description = description; + this.artwork = artwork; + } +} +/** + * @public + * A model that represents a set of accessibility features of a product. + */ +export class AccessibilityFeatures extends models.ViewModel { + constructor(title, artwork, features) { + super(); + this.title = title; + this.artwork = artwork; + this.features = features; + } +} +/** + * @public + * A model that represents a paragraph of linkable text, displayed in the accessibility section. + */ +export class AccessibilityParagraph extends models.ViewModel { + constructor(text, actions) { + super(); + this.text = text; + this.actions = actions; + } +} +//# sourceMappingURL=accessibility.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/api/models/actions/actions.js b/node_modules/@jet-app/app-store/tmp/src/api/models/actions/actions.js new file mode 100644 index 0000000..9316414 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/api/models/actions/actions.js @@ -0,0 +1,525 @@ +/** + * Created by km on 2/13/17. + */ +import { isSome } from "@jet/environment/types/optional"; +import * as base from "../base"; +import { Action } from "./base-action"; +import { ActionMetrics } from "../metrics/metrics"; +/** @public */ +export class TabChangeAction extends Action { + constructor(navigationTab) { + super("TabChangeAction"); + this.navigationTab = navigationTab; + this.actions = []; + this.popToRoot = false; + } +} +/** @public */ +export class TabBadgeAction extends Action { + constructor(navigationTab, text) { + super("TabBadgeAction"); + this.navigationTab = navigationTab; + this.text = text; + } +} +/** @public */ +export class ExternalUrlAction extends Action { + constructor(url, isSensitive = true, actionMetrics = new ActionMetrics()) { + super("ExternalUrlAction", actionMetrics); + this.url = url; + this.isSensitive = isSensitive; + } +} +/** @public */ +export class CompoundAction extends Action { + constructor(actions) { + super("CompoundAction"); + const sanitizedActions = []; + for (const action of actions) { + if (isSome(action.actionMetrics)) { + this.actionMetrics.addManyMetricsData(action.actionMetrics.data); + Object.assign(this.actionMetrics.custom, action.actionMetrics.custom); + action.actionMetrics.clearAll(); + } + sanitizedActions.push(action); + } + this.actions = sanitizedActions; + } +} +/** @public */ +export class OfferAction extends Action { + constructor(title, adamId, purchaseConfiguration, parentAdamId) { + super("OfferAction"); + this.title = title; + this.adamId = adamId; + this.parentAdamId = parentAdamId; + this.purchaseConfiguration = purchaseConfiguration; + if (purchaseConfiguration) { + this.bundleId = purchaseConfiguration.bundleId; + this.lineItem = purchaseConfiguration.lineItem; + } + this.includeBetaApps = false; + } +} +/** @public */ +export class OfferConfirmationAction extends Action { + constructor(buyAction, confirmationInitiationAction) { + super("OfferConfirmationAction"); + this.buyAction = buyAction; + this.confirmationInitiationAction = confirmationInitiationAction; + } +} +/** @public */ +export class OfferAlertAction extends Action { + constructor() { + super("OfferAlertAction"); + this.title = null; + this.message = null; + this.footerMessage = null; + this.isCancelable = true; + this.shouldCheckForAvailableDiskSpace = false; + this.checkRestrictionsForContentRating = null; + this.remoteControllerRequirement = "NO_BADGE"; + this.spatialControllerRequirement = null; + this.shouldCheckForGameController = false; + this.shouldIncludeActiveAccountInFooterMessage = false; + this.shouldPromptForConfirmation = false; + this.completionAction = null; + } +} +/** @public */ +export class CancelPreorderAction extends Action { + constructor(preorderAdamId, isArcade) { + super("CancelPreorderAction"); + this.preorderAdamId = preorderAdamId; + this.isArcade = isArcade; + } +} +/** @public */ +export class InAppPurchaseAction extends Action { + constructor(productIdentifier, appAdamId, appBundleId, installRequiredAction, minimumShortVersionSupportingInAppPurchaseFlow) { + super("InAppPurchaseAction"); + this.productIdentifier = productIdentifier; + this.appAdamId = appAdamId; + this.appBundleId = appBundleId; + this.installRequiredAction = installRequiredAction; + this.minimumShortVersionSupportingInAppPurchaseFlow = minimumShortVersionSupportingInAppPurchaseFlow; + } +} +/** @public */ +export class OfferStateAction extends Action { + constructor(adamId, defaultAction) { + super("OfferStateAction"); + this.title = defaultAction.title; + this.adamId = adamId; + this.defaultAction = defaultAction; + this.includeBetaApps = false; + } +} +/** @public */ +export class OpenAppAction extends Action { + constructor(adamId, destination = "app") { + super("OpenAppAction"); + this.adamId = adamId; + this.destination = destination; + } +} +/** @public */ +export class OpenGamesUIAction extends Action { + constructor(target) { + super("OpenGamesUIAction"); + this.target = target; + } +} +/** @public + * Purchase action that contains multiple offers for a set of Arcade games. + * It is used for "Get all" button in Arcade Download/Starter Pack feature + * to make a single purchase via App Store Daemon. + * The action implementation doesn't execute its `offerActions` + * but uses them as data objects, extracting required parameters. + * */ +export class ArcadePackOfferAction extends Action { + constructor(offerActions) { + super("ArcadePackOfferAction"); + this.offerActions = offerActions; + } +} +/** @public + * An action representing the "selection" of an app, from the offer button. + * The intention is to communicate this app was selected to another process, not actually + * purchase/download/etc. + * Currently used for the browser selection flow. + */ +export class SelectAppAction extends Action { + constructor(adamId) { + super("SelectAppAction"); + this.adamId = adamId; + } +} +/** @public */ +export class HttpAction extends Action { + constructor(url) { + super("HttpAction"); + this.url = url; + this.method = "GET"; + this.headers = {}; + this.body = null; + this.isStoreRequest = false; + this.needsAuthentication = false; + this.needsMediaToken = false; + this.retryCount = 0; + this.disableCache = false; + this.successAction = null; + this.failureAction = null; + } +} +/** @public */ +export class HttpTemplateParameter { + constructor(key, target, inputType, title) { + this.key = key; + this.target = target; + this.inputType = inputType; + this.title = title; + this.isRequired = true; + this.maximumLength = null; + } +} +/** @public */ +export class HttpTemplateAction extends Action { + constructor(url) { + super("HttpTemplateAction"); + this.message = null; + this.url = url; + this.method = "GET"; + this.headers = {}; + this.body = null; + this.bodyDictionary = {}; + this.parameters = []; + this.isStoreRequest = false; + this.needsAuthentication = false; + this.needsMediaToken = false; + this.retryCount = 0; + this.disableCache = false; + this.successAction = null; + this.failureAction = null; + } +} +/** @public */ +export class RateAction extends HttpTemplateAction { +} +export class ShowSettingsAction extends Action { + constructor() { + super("ShowSettingsAction"); + } +} +/** @public */ +export class WriteReviewAction extends Action { + constructor(adamId, url) { + super("WriteReviewAction"); + this.adamId = adamId; + this.url = url; + } +} +/** @public */ +export class PageTabChangeAction extends Action { + constructor(selectedTabId, title, actionClass) { + super(actionClass !== null && actionClass !== void 0 ? actionClass : "PageTabChangeAction"); + this.selectedTabId = selectedTabId; + this.title = title; + } +} +/** @public */ +export class SearchPageSegmentChangeAction extends PageTabChangeAction { + constructor(selectedTabId, selectedTabTitle, switchToOtherSegmentText) { + super(selectedTabId, "SearchPageSegmentChangeAction"); + this.switchToOtherSegmentText = switchToOtherSegmentText; + this.title = selectedTabTitle; + } +} +/** @public */ +export class GameCenterDashboardAction extends Action { + constructor() { + super("GameCenterDashboardAction"); + } +} +/** @public */ +export class SheetAction extends Action { + constructor(actions) { + super("SheetAction"); + this.actions = actions; + this.isCancelable = false; + this.cancelTitle = null; + this.message = null; + this.style = "default"; + this.isCustom = false; + this.destructiveActionIndex = null; + } +} +/** @public */ +export class AlertAction extends Action { + constructor(style) { + super("AlertAction"); + this.style = style; + this.title = null; + this.message = null; + this.isCancelable = false; + this.cancelTitle = null; + this.cancelAction = null; + this.buttonActions = []; + this.buttonTitles = []; + this.destructiveActionIndex = null; + this.imageName = null; + } +} +/** @public */ +export class ScrollingAlertAction extends Action { + constructor(title, message) { + super("ScrollingAlertAction"); + this.title = title; + this.message = message; + } +} +/** + * @public + * Describes a unique activity as used in a Share Sheet. + */ +export class ShareSheetActivity extends base.ViewModel { + constructor(activityType, action) { + super(); + this.activityType = activityType; + this.action = action; + } +} +/** @public */ +export class ShareSheetAction extends Action { + constructor(data, activities, style = "expanded") { + super("ShareSheetAction"); + this.data = data; + this.activities = activities; + this.shareSheetStyle = style; + } +} +/** @public */ +export class ReportConcernAction extends Action { + constructor(reasons) { + super("ReportConcernAction"); + this.reasons = reasons; + } +} +/** @public */ +export class ReviewSummaryReportConcernAction extends Action { + constructor(concerns, title, explanation, sendAction) { + super("ReviewSummaryReportConcernAction"); + this.concerns = concerns; + this.title = title; + this.explanation = explanation; + this.sendAction = sendAction; + } +} +/** @public */ +export class ProductPageScrollAction extends Action { + constructor(section, clicksOnScroll) { + super("ProductPageScrollAction"); + this.section = section; + this.clicksOnScroll = clicksOnScroll; + } +} +/** @public */ +export class ArcadeAction extends Action { + constructor(productIdentifier, appAdamId, subscriptionToken, postSubscribeAction) { + super("ArcadeAction"); + this.productIdentifier = productIdentifier; + this.appAdamId = appAdamId; + this.postSubscribeAction = postSubscribeAction; + this.subscriptionToken = subscriptionToken; + } +} +export class FamilyCircleAction extends Action { + constructor(clientName, eventType, additionalParameters) { + super("FamilyCircleAction"); + this.clientName = clientName; + this.eventType = eventType; + this.additionalParameters = additionalParameters; + } +} +/** @public */ +export class BlankAction extends Action { + constructor() { + super("BlankAction"); + } +} +/** @public */ +export class SearchAdAction extends Action { + constructor(action) { + super("SearchAdAction"); + this.action = action; + } +} +/** @public */ +export class RateLimitedAction extends Action { + constructor(actionKey, primaryAction) { + super("RateLimitedAction"); + this.actionKey = actionKey; + this.primaryAction = primaryAction; + } +} +/** @public */ +export class ArcadeSubscriptionStateAction extends Action { + constructor(notSubscribedAction, purchasingAction, subscribedAction, unknownAction) { + super("ArcadeSubscriptionStateAction"); + this.notSubscribedAction = notSubscribedAction; + this.purchasingAction = purchasingAction; + this.subscribedAction = subscribedAction; + this.unknownAction = unknownAction; + } +} +/** + * @public + * An action for changing the page segment in the Arcade See All Games page. + */ +export class ArcadeSeeAllGamesPageSegmentChangeAction extends Action { + constructor(facet, selectedOption) { + super("ArcadeSeeAllGamesPageSegmentChangeAction"); + this.facet = facet; + this.selectedOption = selectedOption; + } +} +/** @public */ +export class GameCenterPlayerProfileAction extends Action { + constructor(playerId) { + super("GameCenterPlayerProfileAction"); + this.playerId = playerId; + } +} +/** @public */ +export class GameCenterAchievementsAction extends Action { + constructor(bundleId) { + super("GameCenterAchievementsAction"); + this.bundleId = bundleId; + } +} +/** @public */ +export class LegacyGameCenterInvitePlayerAction extends Action { + constructor(contactId) { + super("GameCenterInvitePlayerAction"); + this.contactId = contactId; + } +} +/** @public */ +export class GameCenterInvitePlayerAction extends Action { + constructor(invitationType) { + super("GameCenterInvitePlayerAction"); + this.invitationType = invitationType; + } +} +/** @public */ +export class GameCenterDenylistPlayerAction extends Action { + constructor(contactId) { + super("GameCenterDenylistPlayerAction"); + this.contactId = contactId; + } +} +/** @public */ +export class PageFacetsChangeAction extends Action { + constructor(filterParameter) { + super("PageFacetsChangeAction"); + this.filterParameter = filterParameter; + } +} +/** @public */ +export class EngagementToggleAction extends Action { + constructor(identifier, value) { + super("EngagementToggleAction"); + this.identifier = identifier; + this.value = value; + } +} +/** @public */ +export class ShelfBasedPageScrollAction extends Action { + constructor(shelfId, notPurchasedShelfId, purchasedShelfId, adamId, index, clicksOnScroll) { + super("ShelfBasedPageScrollAction"); + this.shelfId = shelfId; + this.notPurchasedShelfId = notPurchasedShelfId; + this.purchasedShelfId = purchasedShelfId; + this.adamId = adamId; + this.index = index; + this.clicksOnScroll = clicksOnScroll; + } +} +/** @public */ +export class InvalidateAllWidgetsAction extends Action { + constructor() { + super("InvalidateAllWidgetsAction"); + } +} +/** @public */ +export class AppLaunchTrampolineAction extends Action { + constructor(bundleId, payloadUrl, fallbackAction) { + super("AppLaunchTrampolineAction"); + this.bundleId = bundleId; + this.payloadUrl = payloadUrl; + this.fallbackAction = fallbackAction; + } +} +/** @public */ +export class CreateCalendarEventAction extends Action { + constructor(startDate, endDate, isAllDay, name, location, notes, url, notAuthorizedAction, availability) { + super("CreateCalendarEventAction"); + this.startDate = startDate; + this.endDate = endDate; + this.isAllDay = isAllDay; + this.name = name; + this.location = location; + this.notes = notes; + this.url = url; + this.notAuthorizedAction = notAuthorizedAction; + this.availability = availability; + } +} +/** @public + * + */ +export class CopyTextAction extends Action { + constructor(text) { + super("CopyTextAction"); + this.text = text; + } +} +/** @public + * + */ +export class ClearAppUsageDataAction extends Action { + constructor() { + super("ClearAppUsageDataAction"); + } +} +/** @public + * An action describing a request to delete all recent searches from on-device storage. + */ +export class ClearSearchHistoryAction extends Action { + constructor() { + super("ClearSearchHistoryAction"); + } +} +/** + * @public + * An action describing an ad interaction, that passes data on to native ad instrumentation + * via Ad Platforms frameworks. + */ +export class AdInteractionAction extends Action { + constructor(adActionMetrics) { + super("AdInteractionAction"); + this.adActionMetrics = adActionMetrics; + } +} +/** + * @public + * An action describes a begin of crossfire referral flow, that will pass the `ReferrerData` on to native to mark the beginning of crossfire flow. + */ +export class CrossfireReferralAction extends Action { + constructor(referrerData) { + super("CrossfireReferralAction"); + this.referrerData = referrerData; + } +} +//# sourceMappingURL=actions.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/api/models/actions/base-action.js b/node_modules/@jet-app/app-store/tmp/src/api/models/actions/base-action.js new file mode 100644 index 0000000..fa6da41 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/api/models/actions/base-action.js @@ -0,0 +1,15 @@ +import * as base from "../base"; +import { ActionMetrics } from "../metrics/metrics"; +/** @public */ +export class Action extends base.ViewModel { + constructor(actionClass, actionMetrics = new ActionMetrics()) { + super(); + this.title = null; + this.artwork = null; + this.presentationStyle = []; + this.actionClass = actionClass; + this.$kind = actionClass; + this.actionMetrics = actionMetrics; + } +} +//# sourceMappingURL=base-action.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/api/models/actions/flow-action.js b/node_modules/@jet-app/app-store/tmp/src/api/models/actions/flow-action.js new file mode 100644 index 0000000..2317142 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/api/models/actions/flow-action.js @@ -0,0 +1,26 @@ +import { Action } from "./base-action"; +/** @public */ +export class FlowBackAction extends Action { + constructor(dismissal) { + super("FlowBackAction"); + this.dismissal = dismissal; + } +} +/** @public */ +export class FlowAction extends Action { + constructor(flowPage, pageUrl) { + super("FlowAction"); + this.$kind = "flowAction"; + this.page = flowPage; + this.pageUrl = pageUrl; + this.pageData = null; + this.referrerData = undefined; + this.presentationContext = "infer"; + this.animationBehavior = "infer"; + this.origin = "inapp"; + } +} +export function isFlowAction(action) { + return action.$kind === "flowAction"; +} +//# sourceMappingURL=flow-action.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/api/models/ad-incidents.js b/node_modules/@jet-app/app-store/tmp/src/api/models/ad-incidents.js new file mode 100644 index 0000000..d62caa5 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/api/models/ad-incidents.js @@ -0,0 +1,26 @@ +/** + * Models for Ad Incidents that may occur while building model. + */ +import { Model } from "./base"; +/** + * @public + * Base Incident Type + */ +export class AdIncident extends Model { + constructor(incidentType) { + super(); + this.incidentType = incidentType; + } +} +/** + * @public + * Type of Ad Incident where Ad was discarded due to some reason + */ +export class DiscardAdIncident extends AdIncident { + constructor(instanceId, reason) { + super("discard"); + this.instanceId = instanceId; + this.reason = reason; + } +} +//# sourceMappingURL=ad-incidents.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/api/models/annotation.js b/node_modules/@jet-app/app-store/tmp/src/api/models/annotation.js new file mode 100644 index 0000000..92f41ba --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/api/models/annotation.js @@ -0,0 +1,50 @@ +import * as base from "./base"; +/** + * The `Annotation` model describes a category of information about an app, + * such as supported languages, app size, content ratings, in-app-purchase + * info, etc. + * + * `Annotations` always have a title and summary, and most also have an `items` + * array of `AnnotationItem`s with more fine-grained details. + * + * @public + */ +export class Annotation extends base.Model { + constructor(title, items, summary, linkAction) { + super(); + this.title = title; + this.summary = summary; + this.items = items; + this.items_V3 = []; + this.shouldAlwaysPresentExpanded = false; + this.linkAction = linkAction; + } +} +/** + * A single item used to show more details in an `Annotation`. This could be an + * In-App Purchase, supported language list, a link to an editorial item, etc. + * @public + */ +export class AnnotationItem extends base.Model { + constructor(text, options = {}) { + super(); + this.text = text; + this.heading = options.heading; + this.headingArtworks = options.headingArtworks; + this.listText = options.listText; + this.textPairs = options.textPairs; + } +} +/** + * Used on tvOS to visually group related `Annotation` objects into a column. + * @public + */ +export class AnnotationGroup extends base.Model { + constructor(title, annotations, forceExpanded) { + super(); + this.title = title; + this.annotations = annotations; + this.forceExpanded = forceExpanded; + } +} +//# sourceMappingURL=annotation.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/api/models/app-promotions.js b/node_modules/@jet-app/app-store/tmp/src/api/models/app-promotions.js new file mode 100644 index 0000000..cf0c242 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/api/models/app-promotions.js @@ -0,0 +1,186 @@ +import * as base from "./base"; +import * as models from "./index"; +import * as metrics from "./metrics/metrics"; +/** @public */ +export class AppEventFormattedDate extends models.Model { + constructor(showLiveIndicator, displayFromDate, displayText, countdownToDate, countdownStringKey) { + super(); + this.showLiveIndicator = showLiveIndicator; + this.displayFromDate = displayFromDate; + this.displayText = displayText; + this.countdownToDate = countdownToDate; + this.countdownStringKey = countdownStringKey; + } +} +/** @public */ +export var AppEventBadgeKind; +(function (AppEventBadgeKind) { + AppEventBadgeKind["available"] = "available"; + AppEventBadgeKind["happening"] = "happening"; + AppEventBadgeKind["live"] = "live"; +})(AppEventBadgeKind || (AppEventBadgeKind = {})); +export var AppPromotionType; +(function (AppPromotionType) { + AppPromotionType["AppEvent"] = "appEvent"; + AppPromotionType["ContingentOffer"] = "contingentOffer"; + AppPromotionType["OfferItem"] = "offerItem"; +})(AppPromotionType || (AppPromotionType = {})); +/** @public */ +export class AppPromotion extends base.ViewModel { + constructor(promotionType) { + super(); + this.promotionType = promotionType; + } +} +/** @public */ +export class AppEvent extends AppPromotion { + constructor(appEventId, moduleArtwork, moduleVideo, title, subtitle, detail, startDate, endDate, appEventBadgeKind, kind, requirements, lockup, hideLockupWhenNotInstalled, formattedDates, mediaOverlayStyle, includeBorderInDarkMode) { + super(AppPromotionType.AppEvent); + this.appEventId = appEventId; + this.moduleArtwork = moduleArtwork; + this.moduleVideo = moduleVideo; + this.title = title; + this.subtitle = subtitle; + this.detail = detail; + this.startDate = startDate; + this.endDate = endDate; + this.kind = kind; + this.appEventBadgeKind = appEventBadgeKind; + this.requirements = requirements; + this.lockup = lockup; + this.hideLockupWhenNotInstalled = hideLockupWhenNotInstalled; + this.formattedDates = formattedDates; + this.mediaOverlayStyle = mediaOverlayStyle; + this.includeBorderInDarkMode = includeBorderInDarkMode; + this.clickAction = null; + } +} +/** @public */ +export class AppEventDetailPage extends models.Model { + constructor(appEvent, artwork, video, shareAction, mediaOverlayStyle, includeBorderInDarkMode) { + super(); + this.appEvent = appEvent; + this.artwork = artwork; + this.video = video; + this.shareAction = shareAction; + this.mediaOverlayStyle = mediaOverlayStyle; + this.includeBorderInDarkMode = includeBorderInDarkMode; + this.pageMetrics = new metrics.PageMetrics(); + this.pageRenderMetrics = {}; + } +} +/** + * Determines if a {@linkcode shelves.Shelf} is actually a {@linkcode AppEventDetailShelf} + */ +export function isAppEventDetailShelf(shelf) { + return shelf.contentType === "appEventDetail"; +} +/** + * `Shelf` definition that wraps a single {@linkcode AppEventDetailPage} + * + * This is used to power the "web" client UI for an `app-event` detail page + * + * @public + */ +export class AppEventDetailShelf extends models.Shelf { + constructor(detailPage) { + super("appEventDetail", null, [detailPage]); + } +} +/** @public */ +export class AppEventNotificationConfig extends models.Model { + constructor(appEventId, title, detail, artworkUrl, displayTime, scheduledAction, notAuthorizedAction, failureAction, destinationUrl, scheduleClickEvent, cancelScheduleClickEvent) { + super(); + this.appEventId = appEventId; + this.title = title; + this.detail = detail; + this.artworkUrl = artworkUrl; + this.displayTime = displayTime; + this.scheduledAction = scheduledAction; + this.notAuthorizedAction = notAuthorizedAction; + this.failureAction = failureAction; + this.destinationUrl = destinationUrl; + this.scheduleClickEvent = scheduleClickEvent; + this.cancelScheduleClickEvent = cancelScheduleClickEvent; + } +} +/** @public */ +export class ContingentOffer extends AppPromotion { + constructor(backgroundArtwork, mediaOverlayStyle, isStreamlinedBuy, learnMoreTitle, titleFormatted, subtitle, description, label, badge, additionalInfo, trunkAppIcon, offerLockup) { + super(AppPromotionType.ContingentOffer); + this.backgroundArtwork = backgroundArtwork; + this.isStreamlinedBuy = isStreamlinedBuy; + this.mediaOverlayStyle = mediaOverlayStyle; + this.titleFormatted = titleFormatted; + this.subtitle = subtitle; + this.additionalInfo = additionalInfo; + this.description = description; + this.label = label; + this.badge = badge; + this.offerLockup = offerLockup; + this.trunkAppIcon = trunkAppIcon; + this.learnMoreTitle = learnMoreTitle; + this.clickAction = null; + } +} +/** @public */ +export class AppPromotionDetailPage extends base.ViewModel { + constructor(promotionType) { + super(); + this.promotionType = promotionType; + } +} +/** @public */ +export class ContingentOfferDetailPage extends AppPromotionDetailPage { + constructor(contingentOffer, artwork, mediaOverlayStyle) { + super(AppPromotionType.ContingentOffer); + this.contingentOffer = contingentOffer; + this.artwork = artwork; + this.mediaOverlayStyle = mediaOverlayStyle; + this.pageMetrics = new metrics.PageMetrics(); + this.pageRenderMetrics = {}; + this.learnMoreActionMetrics = new metrics.ActionMetrics(); + this.backButtonActionMetrics = new metrics.ActionMetrics(); + this.closeButtonActionMetrics = new metrics.ActionMetrics(); + } +} +/** @public + * + * + */ +export class OfferItem extends AppPromotion { + constructor(moduleArtwork, moduleVideo, mediaOverlayStyle, isStreamlinedBuy, titleFormatted, expiryDateFormatted, subtitle, description, badge, endDate, iapArtwork, offerLockup) { + super(AppPromotionType.OfferItem); + this.moduleArtwork = moduleArtwork; + this.moduleVideo = moduleVideo; + this.isStreamlinedBuy = isStreamlinedBuy; + this.mediaOverlayStyle = mediaOverlayStyle; + this.titleFormatted = titleFormatted; + this.description = description; + this.badge = badge; + this.offerLockup = offerLockup; + this.subtitle = subtitle; + this.endDate = endDate; + this.expiryDateFormatted = expiryDateFormatted; + this.iapArtwork = iapArtwork; + this.clickAction = null; + } +} +/** @public */ +export class OfferItemDetailPage extends AppPromotionDetailPage { + constructor(offerItem, artwork, video, mediaOverlayStyle, includeBorderInDarkMode, learnMoreTitle, additionalInfo) { + super(AppPromotionType.OfferItem); + this.offerItem = offerItem; + this.artwork = artwork; + this.learnMoreTitle = learnMoreTitle; + this.video = video; + this.mediaOverlayStyle = mediaOverlayStyle; + this.pageMetrics = new metrics.PageMetrics(); + this.additionalInfo = additionalInfo; + this.pageRenderMetrics = {}; + this.learnMoreActionMetrics = new metrics.ActionMetrics(); + this.backButtonActionMetrics = new metrics.ActionMetrics(); + this.closeButtonActionMetrics = new metrics.ActionMetrics(); + } +} +//# sourceMappingURL=app-promotions.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/api/models/arcade-upsell.js b/node_modules/@jet-app/app-store/tmp/src/api/models/arcade-upsell.js new file mode 100644 index 0000000..8d8eac3 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/api/models/arcade-upsell.js @@ -0,0 +1,18 @@ +/** + * Created by Jellenbogen on 11/19/19. + */ +import * as base from "./base"; +/** @public */ +export class UpsellBreakout extends base.ViewModel { + constructor(details, offerDisplayProperties, displayProperties, offerButtonAction, buttonCallToAction, artwork, video) { + super(); + this.details = details; + this.displayProperties = displayProperties; + this.offerButtonAction = offerButtonAction; + this.buttonCallToAction = buttonCallToAction; + this.offerDisplayProperties = offerDisplayProperties; + this.artwork = artwork; + this.video = video; + } +} +//# sourceMappingURL=arcade-upsell.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/api/models/arcade.js b/node_modules/@jet-app/app-store/tmp/src/api/models/arcade.js new file mode 100644 index 0000000..a016e3b --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/api/models/arcade.js @@ -0,0 +1,240 @@ +import * as models from "./index"; +import * as metrics from "./metrics/metrics"; +export function marketingItemContextFromString(context) { + var _a, _b; + if (preprocessor.GAMES_TARGET) { + switch (context) { + case "generic": + return "gameCenterGeneric"; + case "groupingLockup": + return "gameCenterLockup"; + case "arcadeTabHeader": + return "gameCenterEditorialPageHeader"; + case "productPage": + return "gameCenterProductPage"; + case "arcadeComingSoon": + return "gameCenterComingSoon"; + case "gameCenterEditorialPage": // TBD + return "gameCenterEditorialPage"; + default: + return (_a = context) !== null && _a !== void 0 ? _a : "gameCenterGeneric"; + } + } + switch (context) { + case "askToBuy": + return "arcadeAskToBuy"; + case "generic": + return "arcadeGeneric"; + case "groupingLockup": + return "arcadeGroupingLockup"; + case "launchRepair": + return "arcadeLaunchRepair"; + case "productPage": + return "arcadeProductPage"; + case "topShelfATV": + return "arcadeTopShelfATV"; + case "topShelfATVClickThrough": + return "arcadeTopShelfATVClickThrough"; + case "editorialItem": + return "arcadeTodayCard"; + case "editorialItemCanvas": + return "arcadeStoryCanvas"; + case "arcadeComingSoon": + return "arcadeComingSoon"; + case "arcadeTabHeader": + return "arcadeTabHeader"; + case "arcadeTabNavBar": + return "arcadeTabNavBar"; + default: + return (_b = context) !== null && _b !== void 0 ? _b : "arcadeGeneric"; + } +} +export function isContextualUpsellContext(context) { + if (preprocessor.GAMES_TARGET) { + switch (context) { + case "gameCenterLockup": + case "gameCenterProductPage": + return true; + default: + return false; + } + } + switch (context) { + case "arcadeGroupingLockup": + case "arcadeProductPage": + case "productPage": + case "groupingLockup": + return true; + default: + return false; + } +} +/** @public */ +export class ArcadeSubscribePage extends models.Model { + constructor(details, dismissButtonTitle, offerButtonAction, offerDisplayProperties) { + super(); + this.details = details; + this.dismissButtonTitle = dismissButtonTitle; + this.offerButtonAction = offerButtonAction; + this.offerDisplayProperties = offerDisplayProperties; + this.pageMetrics = new metrics.PageMetrics(); + this.pageRenderMetrics = {}; + } +} +/** @public */ +export class ArcadeWelcomeItem extends models.Model { + constructor(headline, body, artwork) { + super(); + this.headline = headline; + this.body = body; + this.artwork = artwork; + } +} +/** @public */ +export class ArcadeWelcomeContent extends models.Model { + constructor(title, subtitle, items, continueAction, familyAction) { + super(); + this.title = title; + this.subtitle = subtitle; + this.items = items; + this.continueAction = continueAction; + this.familyAction = familyAction; + } +} +/** @public */ +export class ArcadeWelcomePage extends models.Model { + constructor(individualContent, familyMemberContent) { + super(); + this.individualContent = individualContent; + this.familyMemberContent = familyMemberContent; + this.pageMetrics = new metrics.PageMetrics(); + this.pageRenderMetrics = {}; + } +} +/** @public */ +export class MarketingItemRequestInfo extends models.Model { + constructor(serviceType, placement, metricsTopic, seed, clientOptions) { + super(); + this.serviceType = serviceType; + this.placement = placement; + this.seed = seed; + this.clientOptions = clientOptions; + this.metricsOverlay = { topic: metricsTopic }; + } +} +/** @public */ +export class DynamicUIRequestInfo extends models.Model { + constructor(metricsTopic, clientOptions) { + super(); + this.clientOptions = clientOptions; + this.metricsOverlay = { topic: metricsTopic }; + } +} +/** @public */ +export class UpsellGridContent extends models.Model { + constructor(primaryIcon, icons) { + super(); + this.primaryIcon = primaryIcon; + this.icons = icons; + } +} +/** @public */ +export class AppStoreEngagementTask extends models.Model { + constructor(action) { + super(); + this.action = action; + } +} +/** @public + * A model for game category button on Arcade download pack screen. + */ +export class ArcadeDownloadPackCategory extends models.ViewModel { + constructor(id, title, artwork, gradientStartColor, gradientEndColor) { + super(); + this.id = id; + this.title = title; + this.artwork = artwork; + this.gradientStartColor = gradientStartColor; + this.gradientEndColor = gradientEndColor; + this.selectActionMetrics = new models.ActionMetrics(); + this.deselectActionMetrics = new models.ActionMetrics(); + } +} +/** @public + * Initial Arcade download pack screen with game categories to select by user. + */ +export class ArcadeDownloadPackCategoriesPage extends models.Model { + constructor(title, categories, maxNumberOfCategoriesToChoose, maxNumberOfCategoriesToChooseTemplate, numberOfChosenCategoriesTemplate, primaryAction, dismissAction) { + super(); + this.title = title; + this.categories = categories; + this.maxNumberOfCategoriesToChoose = maxNumberOfCategoriesToChoose; + this.maxNumberOfCategoriesToChooseTemplate = maxNumberOfCategoriesToChooseTemplate; + this.numberOfChosenCategoriesTemplate = numberOfChosenCategoriesTemplate; + this.primaryAction = primaryAction; + this.dismissAction = dismissAction; + this.pageMetrics = new metrics.PageMetrics(); + this.pageRenderMetrics = {}; + } +} +/** @public + * A lockup model for Arcade download suggestions pack screen with linked Arcade category. + */ +export class ArcadeDownloadPackSuggestion extends models.Model { + constructor(lockup, categoryId) { + super(); + this.lockup = lockup; + this.categoryId = categoryId; + } +} +/** @public + * Follow up Arcade download pack screen with the list of suggested games. + */ +export class ArcadeDownloadPackSuggestionsPage extends models.Model { + constructor(title, suggestions, getAllAction, getAllButtonStyle, primaryAction, primaryActionKind, primaryActionSecondaryTitle) { + super(); + this.title = title; + this.suggestions = suggestions; + this.getAllAction = getAllAction; + this.getAllButtonStyle = getAllButtonStyle; + this.primaryAction = primaryAction; + this.primaryActionKind = primaryActionKind; + this.primaryActionSecondaryTitle = primaryActionSecondaryTitle; + this.pageMetrics = new metrics.PageMetrics(); + this.pageRenderMetrics = {}; + } +} +/** + * Matches untyped string with `ArcadeOnboardingSubscriptionStatus` typed value. + * It is used to convert opaque `string` value that is passed across JS bridge from native code. + * @param value `string` value with the same content. + */ +export function arcadeOnboardingSubscriptionStatusFromString(value) { + switch (value) { + case "new": + return "new"; + case "existing": + return "existing"; + default: + return "unknown"; + } +} +/** @public + * Purchase params to use in`ASDPurchaseManager.purchaseBatch` call. + */ +export class BatchPurchaseParams { + constructor(items, commonBuyParams) { + this.items = items; + this.commonBuyParams = commonBuyParams; + } +} +/** @public + * Decorated purchase for a product with a separate buyParams map. + */ +export class BatchPurchaseItem { + constructor(purchase, buyParams) { + this.purchase = purchase; + this.buyParams = buyParams; + } +} +//# sourceMappingURL=arcade.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/api/models/article-page.js b/node_modules/@jet-app/app-store/tmp/src/api/models/article-page.js new file mode 100644 index 0000000..9d5d6c0 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/api/models/article-page.js @@ -0,0 +1,18 @@ +/** + * Created by km on 2/13/17. + */ +import * as models from "./base"; +import * as metrics from "./metrics/metrics"; +/** @public */ +export class ArticlePage extends models.Model { + constructor(card, shelfModels, shareAction) { + super(); + this.card = card; + this.shelves = shelfModels; + this.shareAction = shareAction; + this.isIncomplete = false; + this.pageMetrics = new metrics.PageMetrics(); + this.pageRenderMetrics = {}; + } +} +//# sourceMappingURL=article-page.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/api/models/base.js b/node_modules/@jet-app/app-store/tmp/src/api/models/base.js new file mode 100644 index 0000000..8281f6a --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/api/models/base.js @@ -0,0 +1,596 @@ +/** + * Created by km on 2/13/17. + */ +// region Core +/** @public */ +export class Model { + constructor() { + this.$incidents = undefined; + } + /** + * Indicates whether this model is valid. Subclasses should override if there are specific + * properties that are required for proper functioning + * @returns {boolean} Whether the model is valid. Defaults to true. + */ + isValid() { + return true; + } +} +/** + * @public + * @todo Make this implement JetEngine ViewModel once we migrate ImpressionMetrics + */ +export class ViewModel extends Model { + constructor(impressionMetrics = null) { + super(); + this.impressionMetrics = impressionMetrics; + } +} +/** @public */ +export class PurchaseConfiguration extends Model { + constructor(buyParams, vendor, appName, bundleId, appPlatforms, isPreorder, excludeAttribution, metricsPlatformDisplayStyle, lineItem, isRedownload, preflightPackageUrl, isArcadeApp, isHalva, supportsVisionOSCompatibleIOSBinary, inAppEventId, extRefApp2, extRefUrl2, additionalHeaders, appCapabilities, isDefaultBrowser, remoteDownloadIdentifiers, hasMacIPAPackage, contentRating) { + super(); + this.buyParams = buyParams; + this.vendor = vendor; + this.appName = appName; + this.bundleId = bundleId; + this.appPlatforms = appPlatforms; + this.isPreorder = isPreorder === null || isPreorder === undefined ? false : isPreorder; + this.excludeAttribution = + excludeAttribution === null || excludeAttribution === undefined ? true : excludeAttribution; + this.metricsPlatformDisplayStyle = metricsPlatformDisplayStyle; + this.isRedownload = isRedownload === null || isRedownload === undefined ? false : isRedownload; + this.lineItem = lineItem; + this.preflightPackageUrl = preflightPackageUrl === undefined ? null : preflightPackageUrl; + this.isArcadeApp = isArcadeApp; + this.isHalva = isHalva; + this.supportsVisionOSCompatibleIOSBinary = supportsVisionOSCompatibleIOSBinary; + this.inAppEventId = inAppEventId; + this.extRefApp2 = extRefApp2; + this.extRefUrl2 = extRefUrl2; + this.additionalHeaders = additionalHeaders; + this.appCapabilities = appCapabilities; + this.isDefaultBrowser = isDefaultBrowser; + this.remoteDownloadIdentifiers = remoteDownloadIdentifiers; + this.hasMacIPAPackage = hasMacIPAPackage; + this.contentRating = contentRating; + } +} +/** @public */ +export class OfferDisplayProperties extends Model { + constructor(offerType, adamId, bundleId, style, parentAdamId, environment, offerTint, titles, titleSymbolNames, subtitles, hasInAppPurchases, hasExternalPurchases, isDeletableSystemApp, isFree, isPreorder, offerLabelStyle, hasDiscount, contentRating, subscriptionFamilyId, useAdsLocale, priceFormatted, isStreamlinedBuy, appCapabilities, isRedownloadDisallowed = false, isOpenBundleAllowed = false) { + super(); + this.offerType = offerType; + this.adamId = adamId; + this.bundleId = bundleId; + this.parentAdamId = parentAdamId; + this.style = style === null || style === undefined ? "infer" : style; + this.environment = environment === null || environment === undefined ? "light" : environment; + this.offerTint = offerTint === null || offerTint === undefined ? { type: "blue" } : offerTint; + this.titles = titles === null || titles === undefined ? {} : titles; + this.titleSymbolNames = titleSymbolNames === null || titleSymbolNames === undefined ? {} : titleSymbolNames; + this.subtitles = subtitles === null || subtitles === undefined ? {} : subtitles; + this.hasInAppPurchases = hasInAppPurchases; + this.hasExternalPurchases = hasExternalPurchases; + this.isDeletableSystemApp = isDeletableSystemApp; + this.isFree = isFree; + this.isPreorder = isPreorder; + this.offerLabelStyle = offerLabelStyle === null || offerLabelStyle === undefined ? "none" : offerLabelStyle; + this.hasDiscount = hasDiscount; + this.contentRating = contentRating; + this.subscriptionFamilyId = subscriptionFamilyId; + this.useAdsLocale = useAdsLocale; + this.priceFormatted = priceFormatted; + this.isStreamlinedBuy = isStreamlinedBuy; + this.appCapabilities = appCapabilities; + this.isRedownloadDisallowed = isRedownloadDisallowed; + this.isOpenBundleAllowed = isOpenBundleAllowed; + } + /** + * Create new offer display properties changing appearance. + * @param {boolean} overrideDisabledStyle When false, an offer style of 'disabled' is maintained. When true, the style is overridden regardless of being disabled to begin with. + * @param {OfferStyle} style The new style to apply. + * @param {OfferEnvironment} environment The new environment to apply. + * @param {OfferTint} offerTint + * @return {string} A new instance of OfferDisplayProperties with the desired modifications. + */ + newOfferDisplayPropertiesChangingAppearance(overrideDisabledStyle, style, environment, offerTint) { + return new OfferDisplayProperties(this.offerType, this.adamId, this.bundleId, style === null || style === undefined || (!overrideDisabledStyle && this.style === "disabled") + ? this.style + : style, this.parentAdamId, environment === null || environment === undefined ? this.environment : environment, offerTint === null || offerTint === undefined ? this.offerTint : offerTint, this.titles, this.titleSymbolNames, this.subtitles, this.hasInAppPurchases, this.hasExternalPurchases, this.isDeletableSystemApp, this.isFree, this.isPreorder, this.offerLabelStyle, this.hasDiscount, this.contentRating, this.subscriptionFamilyId, this.useAdsLocale, this.priceFormatted, this.isStreamlinedBuy, this.appCapabilities, this.isRedownloadDisallowed, this.isOpenBundleAllowed); + } +} +/** + * @public + * Information needed to personalize offerActions and how they are displayed. + */ +export class PersonalizedOfferContext extends Model { + constructor(personalizedOfferType, offerAction, offerDisplayProperties) { + super(); + this.offerAction = offerAction; + this.offerDisplayProperties = offerDisplayProperties; + this.personalizedOfferType = personalizedOfferType; + } +} +export var ArtworkContentMode; +(function (ArtworkContentMode) { + ArtworkContentMode[ArtworkContentMode["scaleToFill"] = 0] = "scaleToFill"; + ArtworkContentMode[ArtworkContentMode["scaleAspectFit"] = 1] = "scaleAspectFit"; + ArtworkContentMode[ArtworkContentMode["scaleAspectFill"] = 2] = "scaleAspectFill"; + ArtworkContentMode[ArtworkContentMode["redraw"] = 3] = "redraw"; + ArtworkContentMode[ArtworkContentMode["center"] = 4] = "center"; + ArtworkContentMode[ArtworkContentMode["top"] = 5] = "top"; + ArtworkContentMode[ArtworkContentMode["bottom"] = 6] = "bottom"; + ArtworkContentMode[ArtworkContentMode["left"] = 7] = "left"; + ArtworkContentMode[ArtworkContentMode["right"] = 8] = "right"; + ArtworkContentMode[ArtworkContentMode["topLeft"] = 9] = "topLeft"; + ArtworkContentMode[ArtworkContentMode["topRight"] = 10] = "topRight"; + ArtworkContentMode[ArtworkContentMode["bottomLeft"] = 11] = "bottomLeft"; + ArtworkContentMode[ArtworkContentMode["bottomRight"] = 12] = "bottomRight"; +})(ArtworkContentMode || (ArtworkContentMode = {})); +/** @public */ +export class ArtworkVariant extends Model { + constructor(format, quality, supportsWideGamut) { + super(); + this.format = format; + this.quality = quality; + this.supportsWideGamut = supportsWideGamut; + } + isValid() { + return this.format && this.supportsWideGamut !== undefined; + } +} +const systemImages = [ + "app.3.stack.3d", + "app.3.stack.3d.fill", + "applewatch", + "appstore", + "hammer", + "hammer.fill", + "house", + "ipad.gen2.landscape", + "iphone.gen2", + "joystickcontroller", + "joystickcontroller.fill", + "macbook.gen2", + "magnifyingglass", + "paintbrush", + "paintbrush.fill", + "paperplane", + "paperplane.fill", + "person.crop.square", + "rocket", + "rocket.fill", + "safari", + "square.grid.2x2", + "square.grid.2x2.fill", + "star", + "star.fill", + "text.rectangle.page", + "text.rectangle.page.fill", + "tv", + "visionpro", +]; +/** + * @public + * Type guard to narrow and ensure a string is a valid `SystemImage`. + */ +export function isSystemImage(systemImage) { + return typeof systemImage === "string" && systemImages.includes(systemImage); +} +/** @public */ +export class Artwork extends Model { + constructor(template, width, height, variants, backgroundColor, textColor) { + super(); + this.checksum = null; + this.backgroundColor = null; + this.textColor = null; // Represents a valid text color that can be used alongside this artwork + this.style = null; + this.crop = "bb"; + this.contentMode = null; + this.imageScale = null; + this.template = template; + this.width = width; + this.height = height; + this.variants = variants; + this.backgroundColor = backgroundColor; + this.textColor = textColor; + } + isPortrait() { + return this.height >= this.width; + } + isLandscape() { + return !this.isPortrait(); + } + isValid() { + return this.template !== "" && this.width > 0 && this.height > 0 && this.variants.length > 0; + } +} +/** @public */ +export class Screenshots extends Model { + constructor(artwork, mediaPlatform) { + super(); + this.artwork = artwork; + this.mediaPlatform = mediaPlatform; + } +} +/** @public */ +export class MediaPlatform extends Model { + constructor(appPlatform, mediaType, systemImageName, supplementaryAppPlatforms, deviceCornerRadiusFactor, deviceBorderThickness, outerDeviceCornerRadiusFactor) { + super(); + this.appPlatform = appPlatform; + this.supplementaryAppPlatforms = supplementaryAppPlatforms || []; + this.deviceCornerRadiusFactor = deviceCornerRadiusFactor; + this.mediaType = mediaType; + this.systemImageName = systemImageName; + this.deviceBorderThickness = deviceBorderThickness; + this.outerDeviceCornerRadiusFactor = outerDeviceCornerRadiusFactor; + } + isEqualTo(mediaPlatform) { + this.supplementaryAppPlatforms.sort(); + mediaPlatform.supplementaryAppPlatforms.sort(); + const supplementaryPlatformsAreEqual = this.supplementaryAppPlatforms.join(",") === mediaPlatform.supplementaryAppPlatforms.join(","); + return (mediaPlatform.appPlatform === this.appPlatform && + mediaPlatform.mediaType === this.mediaType && + mediaPlatform.systemImageName === this.systemImageName && + supplementaryPlatformsAreEqual && + mediaPlatform.deviceCornerRadiusFactor === this.deviceCornerRadiusFactor); + } +} +/** + * @public + * Generic size class used to describe an size in points + */ +export class Size { + constructor(width, height) { + this.width = width; + this.height = height; + } + static fromNativeSize(nativeSize) { + return new Size(nativeSize.width, nativeSize.height); + } + isEqualTo(size) { + return size.width === this.width && size.height === this.height; + } +} +// endregion +// region Share +/** @public */ +export class ShareSheetNotesMetadata extends Model { + constructor(itemName, url, developer, category, fileSize, mediaType) { + super(); + this.itemName = itemName; + this.url = url; + this.developer = developer; + this.category = category; + this.fileSize = fileSize; + this.mediaType = mediaType; + } +} +/** @public */ +export class ShareSheetArticleMetadata extends Model { + constructor(id, text, subtitle, artwork) { + super(); + this.context = "article"; + this.id = id; + this.text = text; + this.subtitle = subtitle; + this.artwork = artwork; + } +} +/** @public */ +export class ShareSheetAppEventMetadata extends Model { + constructor(text, subtitle, artwork) { + super(); + this.context = "appEvent"; + this.text = text; + this.subtitle = subtitle; + this.artwork = artwork; + } +} +/** @public */ +export class ShareSheetProductMetadata extends Model { + constructor(adamId, storeFrontIdentifier, name, platform, icon, screenshots, videos, isMessagesOnlyApp, subtitle, genreName, messagesAppIcon, notesMetadata) { + super(); + this.context = "product"; + this.adamId = adamId; + this.storeFrontIdentifier = storeFrontIdentifier; + this.name = name; + this.platform = platform; + this.icon = icon; + this.screenshots = screenshots; + this.videos = videos; + this.isMessagesOnlyApp = isMessagesOnlyApp; + this.subtitle = subtitle; + this.genreName = genreName; + this.messagesAppIcon = messagesAppIcon; + this.notesMetadata = notesMetadata; + } +} +/** @public */ +export class ShareSheetGenericMetadata extends Model { + constructor(text, subtitle, artwork) { + super(); + this.context = "generic"; + this.text = text; + this.subtitle = subtitle; + this.artwork = artwork; + } +} +/** @public */ +export class ShareSheetData extends Model { + constructor(metadata, url, shortUrl) { + super(); + this.metadata = metadata; + this.url = url; + this.shortUrl = shortUrl; + } +} +// endregion +// region ReportConcern +/** @public */ +export class ReportConcernReason extends Model { + constructor(reasonId, name, uppercaseName) { + super(); + this.reasonId = reasonId; + this.name = name; + this.uppercaseName = uppercaseName; + } +} +/** @public */ +export class StyledText extends Model { + constructor(rawText, rawTextType = "text/plain") { + super(); + this.rawText = rawText; + this.rawTextType = rawTextType; + } +} +/** @public */ +export class Paragraph extends ViewModel { + constructor(text, mediaType, style) { + super(); + this.text = text; + this.mediaType = mediaType || "text/plain"; + this.style = style || "standard"; + this.alignment = "localized"; + this.isCollapsed = false; + this.suppressVerticalMargins = false; + this.wantsCollapsedNewlines = true; + } +} +// endregion +// region Flow Preview +/** @public */ +export class FlowPreviewActionsConfiguration extends Model { + constructor(actionss, offerDisplayProperties = null, offerActionIndex = null) { + super(); + this.actions = actionss; + this.offerDisplayProperties = offerDisplayProperties; + this.offerActionIndex = offerActionIndex; + } +} +// region Arcade Footer +/** @public */ +export class ArcadeFooter extends ViewModel { +} +// endregion +/** @public + * Arcade download (starter) pack shelf data model. + * Currently used only for iPhone (see `ArcadeDownloadPackShelfController`). + */ +export class ArcadeDownloadPackCard extends ViewModel { +} +/** @public */ +export class TitleEffect extends Model { + constructor(type) { + super(); + this.isFallbackStyle = false; + this.type = type; + } +} +// endregion +// region Game Center +/** @public */ +export class GameCenterPlayerGameAchievementSummary extends ViewModel { + constructor(bundleId, completedAchievements, totalAchievements, completedText, action, achievements) { + super(); + this.bundleId = bundleId; + this.completedAchievements = completedAchievements; + this.totalAchievements = totalAchievements; + this.completedText = completedText; + this.action = action; + this.achievements = achievements; + } +} +/** @public */ +export class GameCenterPlayer extends ViewModel { + constructor(playerId, alias, displayName, size, artwork, action) { + super(); + this.playerId = playerId; + this.alias = alias; + this.displayName = displayName; + this.artwork = artwork; + this.action = action; + this.size = size; + } +} +/** @public */ +export class GameCenterActivityFeedCard extends ViewModel { + constructor(id, avatarImageURL, avatarActionURL, body, supplementaryViewImageURL, supplementaryViewActionURL, adamID) { + super(); + this.id = id; + this.avatarImageURL = avatarImageURL; + this.avatarActionURL = avatarActionURL; + this.body = body; + this.supplementaryViewImageURL = supplementaryViewImageURL; + this.supplementaryViewActionURL = supplementaryViewActionURL; + this.adamID = adamID; + } +} +/** @public */ +export class SmallContactCard extends ViewModel { + constructor(id, title, subtitle, buttonText, contactId, buttonAction, removeButtonAction, shouldShowMessagesBadge) { + super(); + this.id = id; + this.title = title; + this.subtitle = subtitle; + this.buttonText = buttonText; + this.contactId = contactId; + this.buttonAction = buttonAction; + this.removeButtonAction = removeButtonAction; + this.shouldShowMessagesBadge = shouldShowMessagesBadge; + } +} +/** @public */ +export class GameCenterAchievementStatus extends Model { + constructor(type) { + super(); + this.type = type; + } +} +/** @public */ +export class GameCenterAchievement extends Model { + constructor(id, title, subtitle, status) { + super(); + this.id = id; + this.title = title; + this.subtitle = subtitle; + this.status = status; + } +} +/** @public */ +export class GameCenterGameplayHistory extends Model { + constructor(adamId, platformId, isArcade, records) { + super(); + this.adamId = adamId; + this.platformId = platformId; + this.isArcade = isArcade; + this.records = records; + } +} +/** @public */ +export class GameCenterGameplayHistoryRecord extends Model { + constructor(playerId, timestamp) { + super(); + this.playerId = playerId; + this.timestamp = timestamp; + } +} +/** @public */ +export class Video extends Model { + constructor(videoUrl, preview, videoConfiguration) { + super(); + this.videoUrl = videoUrl; + this.preview = preview; + this.allowsAutoPlay = videoConfiguration.allowsAutoPlay; + this.looping = videoConfiguration.looping; + this.canPlayFullScreen = videoConfiguration.canPlayFullScreen; + this.playbackControls = { ...videoConfiguration.playbackControls }; + this.autoPlayPlaybackControls = { ...videoConfiguration.autoPlayPlaybackControls }; + this.templateMediaEvent = null; + } +} +export class CombinedFileSize { + constructor(fileSizeByDevice, maxDownloadSizeInBytes, maxInstallSizeInBytes, maxEssentialInstallSizeInBytes) { + this.fileSizeByDevice = fileSizeByDevice; + this.maxDownloadSizeInBytes = maxDownloadSizeInBytes; + this.maxInstallSizeInBytes = maxInstallSizeInBytes; + this.maxEssentialInstallSizeInBytes = maxEssentialInstallSizeInBytes; + } +} +/** @public */ +export class PageHeader extends ViewModel { + constructor(badge, title, subtitle) { + super(); + this.badge = badge; + this.title = title; + this.subtitle = subtitle; + } +} +export class MediaPageHeader extends PageHeader { + constructor(badge, title, subtitle, artwork, video, collectionIcons, useGeneratedBackgroundGradient, backgroundColor, style, backgroundStyle) { + super(badge, title, subtitle); + this.artwork = artwork; + this.video = video; + this.collectionIcons = collectionIcons; + this.useGeneratedBackgroundGradient = useGeneratedBackgroundGradient; + this.backgroundColor = backgroundColor; + this.style = style; + this.backgroundStyle = backgroundStyle; + } +} +export const EdgeInsetsZero = { + top: 0.0, + left: 0.0, + bottom: 0.0, + right: 0.0, +}; +/** + * The `Priority` is used when sorting the metrics to match for a given + * today card, so if we're looking for a layout for a given priority we + * will match that first + */ +export var TodayCardArtworkSizedLayoutMetricsPriority; +(function (TodayCardArtworkSizedLayoutMetricsPriority) { + TodayCardArtworkSizedLayoutMetricsPriority["Mini"] = "mini"; + TodayCardArtworkSizedLayoutMetricsPriority["ExtraWide"] = "extraWide"; + TodayCardArtworkSizedLayoutMetricsPriority["None"] = "none"; +})(TodayCardArtworkSizedLayoutMetricsPriority || (TodayCardArtworkSizedLayoutMetricsPriority = {})); +export class ChartOrCategorySafeAreaLocation { +} +export var ChartOrCategorySafeAreaRelativeLocation; +(function (ChartOrCategorySafeAreaRelativeLocation) { + ChartOrCategorySafeAreaRelativeLocation["TopRight"] = "topRight"; + ChartOrCategorySafeAreaRelativeLocation["TopLeft"] = "topLeft"; + ChartOrCategorySafeAreaRelativeLocation["MiddleLeft"] = "middleLeft"; +})(ChartOrCategorySafeAreaRelativeLocation || (ChartOrCategorySafeAreaRelativeLocation = {})); +export var ChartOrCategorySafeAreaSizeConstraint; +(function (ChartOrCategorySafeAreaSizeConstraint) { + ChartOrCategorySafeAreaSizeConstraint["SquareByWidth"] = "squareByWidth"; + ChartOrCategorySafeAreaSizeConstraint["SquareByHeight"] = "squareByHeight"; + ChartOrCategorySafeAreaSizeConstraint["FreeForm"] = "freeForm"; +})(ChartOrCategorySafeAreaSizeConstraint || (ChartOrCategorySafeAreaSizeConstraint = {})); +export class ChartOrCategorySafeAreaLocationDistance { +} +export class ChartOrCategorySafeArea { + constructor(width, height, asRatioOfSize, location, constraint) { + this.width = width; + this.height = height; + this.asRatioOfSize = asRatioOfSize !== null && asRatioOfSize !== void 0 ? asRatioOfSize : false; + this.location = location; + this.constraint = constraint; + } +} +/// The default tile safe area displays the artwork in a square in the top trailing corner of the cell. +ChartOrCategorySafeArea.defaultTileArtworkSafeArea = { + width: 0.4, + height: 0.4, + asRatioOfSize: true, + constraint: ChartOrCategorySafeAreaSizeConstraint.SquareByWidth, + location: { + relativeLocation: ChartOrCategorySafeAreaRelativeLocation.TopRight, + }, +}; +/// The default pill safe area displays the artwork on the middle leading side of the cell. +ChartOrCategorySafeArea.defaultPillArtworkSafeArea = { + width: 0.28, + height: 1.0, + asRatioOfSize: true, + constraint: ChartOrCategorySafeAreaSizeConstraint.FreeForm, + location: { + relativeLocation: ChartOrCategorySafeAreaRelativeLocation.MiddleLeft, + }, +}; +/// The default tile safe area increases the width available to render the text for the cell. +ChartOrCategorySafeArea.defaultTileTextSafeArea = { + width: 0.15, + height: 0.0, + asRatioOfSize: true, +}; +/// The default pill safe area increases the width available to render the text for the cell. +ChartOrCategorySafeArea.defaultPillTextSafeArea = { + width: 0.28, + height: 0.0, + asRatioOfSize: true, +}; +//# sourceMappingURL=base.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/api/models/categories.js b/node_modules/@jet-app/app-store/tmp/src/api/models/categories.js new file mode 100644 index 0000000..2435540 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/api/models/categories.js @@ -0,0 +1,20 @@ +import * as models from "./index"; +/** @public */ +export class Category extends models.Model { + constructor(name, genreId, artwork, ageBandId, children) { + super(); + this.name = name; + this.genreId = genreId; + this.artwork = artwork; + this.ageBandId = ageBandId; + this.children = children; + } +} +/** @public */ +export class CategoryList extends models.Model { + constructor(categories) { + super(); + this.categories = categories; + } +} +//# sourceMappingURL=categories.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/api/models/dynamic-generic-page.js b/node_modules/@jet-app/app-store/tmp/src/api/models/dynamic-generic-page.js new file mode 100644 index 0000000..6037275 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/api/models/dynamic-generic-page.js @@ -0,0 +1,15 @@ +import * as base from "./base"; +import * as metrics from "./metrics/metrics"; +/** @public */ +export class DynamicGenericPage extends base.Model { + constructor() { + super(); + this.shelfOrderings = {}; + this.shelfMapping = {}; + this.presentationOptions = []; + this.isIncomplete = false; + this.pageMetrics = new metrics.PageMetrics(); + this.pageRenderMetrics = {}; + } +} +//# sourceMappingURL=dynamic-generic-page.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/api/models/generic-page.js b/node_modules/@jet-app/app-store/tmp/src/api/models/generic-page.js new file mode 100644 index 0000000..1ee16fb --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/api/models/generic-page.js @@ -0,0 +1,47 @@ +/** + * Created by km on 2/13/17. + */ +import * as base from "./base"; +import * as metrics from "./metrics/metrics"; +/** @public */ +export class GenericPage extends base.Model { + constructor(shelfModels) { + super(); + this.shelves = shelfModels; + this.title = null; + this.presentationOptions = []; + this.isIncomplete = false; + this.pageMetrics = new metrics.PageMetrics(); + } +} +/** @public */ +export class InAppPurchaseInstallPage extends base.Model { + constructor() { + super(); + this.pageMetrics = new metrics.PageMetrics(); + this.pageRenderMetrics = {}; + } +} +/** + * @public + * Screen shown in search tab when search bar is focused but empty. + */ +export class SearchFocusPage extends GenericPage { +} +/** + * @public + * Initial screen shown in search tab when search bar is unfocused. + */ +export class SearchLandingPage extends GenericPage { +} +/** @public */ +export class ArcadePage extends GenericPage { +} +/** @public */ +export class ArcadeSeeAllGamesPage extends GenericPage { + constructor(shelfModels) { + super(shelfModels); + this.pageSegments = []; + } +} +//# sourceMappingURL=generic-page.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/api/models/hero-carousel.js b/node_modules/@jet-app/app-store/tmp/src/api/models/hero-carousel.js new file mode 100644 index 0000000..85b9514 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/api/models/hero-carousel.js @@ -0,0 +1,37 @@ +import { isSome } from "@jet/environment/types/optional"; +import * as models from "./index"; +/** @public */ +export class HeroCarouselItemOverlay extends models.ViewModel { + isValid() { + const hasButtonRequirements = this.callToActionText !== undefined && + this.callToActionText !== null && + this.clickAction !== undefined && + this.clickAction !== null; + const hasTitle = this.titleText !== undefined && this.titleText !== null; + const hasLockupRequirements = this.lockup !== undefined && this.lockup !== null; + const hasCollectionIcons = this.collectionIcons !== undefined && this.collectionIcons !== null; + return hasTitle && (hasLockupRequirements || hasCollectionIcons || hasButtonRequirements); + } +} +/** @public */ +export class HeroCarouselItem extends models.ViewModel { + isValid() { + const hasCollectionIcons = isSome(this.collectionIcons) && this.collectionIcons.length > 0; + const hasValidArtwork = isSome(this.artwork) && this.artwork.isValid(); + const hasValidVideo = isSome(this.video) && this.video.isValid(); + const hasMedia = hasValidArtwork || hasValidVideo || hasCollectionIcons; + const hasOverlay = isSome(this.overlay) && this.overlay.isValid(); + return hasMedia && hasOverlay; + } +} +/** @public */ +export class HeroCarousel extends models.ViewModel { + constructor() { + super(...arguments); + /// The different items to display in the carousel + this.items = []; + /// The different items to display in the carousel, in RTL + this.rtlItems = []; + } +} +//# sourceMappingURL=hero-carousel.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/api/models/metrics/advert-action-metrics.js b/node_modules/@jet-app/app-store/tmp/src/api/models/metrics/advert-action-metrics.js new file mode 100644 index 0000000..4560458 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/api/models/metrics/advert-action-metrics.js @@ -0,0 +1,21 @@ +/** + * Models for Advert Actions + */ +import * as models from "../base"; +/** + * @public + * Object that provides metrics data for actions pertaining to advert's native instrumentation. + */ +export class AdvertActionMetrics extends models.Model { + constructor(instanceId, adamId, bundleId, advertType, invocation, purchaseType, reportingDestination) { + super(); + this.instanceId = instanceId; + this.adamId = adamId; + this.bundleId = bundleId; + this.advertType = advertType; + this.invocation = invocation; + this.purchaseType = purchaseType; + this.reportingDestination = reportingDestination; + } +} +//# sourceMappingURL=advert-action-metrics.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/api/models/metrics/metrics.js b/node_modules/@jet-app/app-store/tmp/src/api/models/metrics/metrics.js new file mode 100644 index 0000000..23e26bd --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/api/models/metrics/metrics.js @@ -0,0 +1,101 @@ +/** + * Created by joel on 2/13/17. + */ +import * as JetMetrics from "@jet/environment/types/metrics"; +import * as models from "../base"; +// TS only allows extending **type information** in `declare module`s. +// These members **must** be initialized. +// TODO: The root cause for this workaround is because the Jet type `PageInvocationPoint` is not a `const enum`. +// Consider changing the Jet type to a `const enum`, and the compiler will automatically inline these values. +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const extendedPageInvocationPoint = JetMetrics.PageInvocationPoint; +extendedPageInvocationPoint["search"] = "search"; +extendedPageInvocationPoint["timer"] = "timer"; +extendedPageInvocationPoint["never"] = "never"; +extendedPageInvocationPoint["pageChange"] = "pageChange"; +/** @public */ +export class AppStoreMetricsData { + constructor(fields, includingFields, excludingFields, topic, shouldFlush = false) { + this.fields = fields; + this.includingFields = includingFields; + this.excludingFields = excludingFields; + this.topic = topic; + this.shouldFlush = shouldFlush; + } +} +/** @public */ +export class LintedMetricsEvent extends models.Model { + constructor(fields) { + super(); + this.fields = fields; + } +} +/** @public */ +export class ActionMetrics extends models.Model { + constructor(events) { + super(); + this.data = events || []; + this.custom = {}; + } + addMetricsData(data) { + this.data.push(data); + } + addManyMetricsData(dataArray) { + for (const data of dataArray) { + this.addMetricsData(data); + } + } + clearAll() { + this.data.length = 0; + } +} +/** @public */ +export class PageMetrics extends models.Model { + constructor() { + super(); + this.instructions = []; + this.custom = {}; + } + addInstruction(instruction) { + this.instructions.push(instruction); + } + addManyInstructions(instructions) { + for (const instruction of instructions) { + this.addInstruction(instruction); + } + } + addData(data, invocationPoints) { + const event = { + data, + invocationPoints, + }; + this.instructions.push(event); + } + addManyData(dataArray, invocationPoints) { + for (const data of dataArray) { + this.addData(data, invocationPoints); + } + } +} +// TODO: This needs to be migrated to the JetEngine version of ImpressionMetrics +// The primary challenge is ID is required in the JetEngine representation, which will require +// some refactoring both in JS and native to get right +/** @public */ +export class ImpressionMetrics { + constructor(fields, id, custom) { + this.fields = fields; + this.id = id; + this.custom = custom; + } +} +/** @public */ +export class FastImpressionMetrics extends ImpressionMetrics { + constructor(metrics, isFast) { + super(metrics.fields, metrics.id, metrics.custom || {}); + if (this.custom !== undefined) { + this.custom["isFast"] = isFast; + } + this.isFast = isFast; + } +} +//# sourceMappingURL=metrics.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/api/models/page-facets.js b/node_modules/@jet-app/app-store/tmp/src/api/models/page-facets.js new file mode 100644 index 0000000..a534c8e --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/api/models/page-facets.js @@ -0,0 +1,88 @@ +import { isSome } from "@jet/environment/types/optional"; +import * as models from "./base"; +/** @public */ +export class PageFacetOption extends models.Model { + constructor(title, value, metricsValue = null, systemImageName = undefined) { + super(); + this.title = title; + this.value = value; + this.systemImageName = systemImageName; + this.metricsValue = metricsValue; + } +} +PageFacetOption.defaultValue = "pageFacetsDefaultValue"; +PageFacetOption.trueValue = new PageFacetOption("true", "false"); +PageFacetOption.falseValue = new PageFacetOption("false", "false"); +/** @public */ +export class PageFacetsFacet extends models.Model { + constructor(id, parameterName, title, displayType, options = [], defaultOptions = null, metricsParameterName = null, clickAction = null, displayOptionsInline = false, showsSelectedOptions = false, isHiddenFromMenu = false) { + super(); + this.id = id; + this.parameterName = parameterName; + this.title = title; + this.displayType = displayType; + this.defaultOptions = defaultOptions; + this.options = options; + this.metricsParameterName = metricsParameterName; + this.clickAction = clickAction; + this.displayOptionsInline = displayOptionsInline; + this.showsSelectedOptions = showsSelectedOptions; + this.isHiddenFromMenu = isHiddenFromMenu; + } +} +/** @public */ +export class PageFacetsGroup extends models.Model { + constructor(facets = [], title = null) { + super(); + this.title = title; + this.facets = facets; + } +} +/** @public */ +export class PageFacets extends models.Model { + constructor(facetGroups, allowsResetButton, resetButtonTitle) { + super(); + this.facetGroups = facetGroups; + this.allowsResetButton = allowsResetButton; + this.resetButtonTitle = resetButtonTitle; + } + static isDefinedNonNullNonEmpty(object) { + return isSome(object) && Object.keys(object).length !== 0; + } +} +/** + * Facet out nil values / join all values together returning the string used for a query parameter value + * returns null if resulting string would be empty or selectedOptions is null + * @param selectedOptions + */ +PageFacets.requestValuesForSelectedFacetOptions = function (selectedOptions) { + if (PageFacets.isDefinedNonNullNonEmpty(selectedOptions)) { + const facetOptionValue = selectedOptions + .filter((option) => { + return PageFacets.isDefinedNonNullNonEmpty(option.value); + }) + .map((option) => { + return option.value; + }) + .join(","); + const allFacetOptionValues = facetOptionValue.split("&"); + const primaryValue = allFacetOptionValues[0]; + const additionalQueryValues = allFacetOptionValues.splice(1); + const additionalKeyValuePairs = {}; + for (const additionalQueryValue of additionalQueryValues) { + const keyValue = additionalQueryValue.split("="); + if (keyValue.length !== 2) { + continue; + } + additionalKeyValuePairs[keyValue[0]] = keyValue[1]; + } + return { + value: primaryValue, + additionalKeyValuePairs: additionalKeyValuePairs, + }; + } + else { + return null; + } +}; +//# sourceMappingURL=page-facets.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/api/models/page-refresh-policy.js b/node_modules/@jet-app/app-store/tmp/src/api/models/page-refresh-policy.js new file mode 100644 index 0000000..96a0732 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/api/models/page-refresh-policy.js @@ -0,0 +1,17 @@ +/** + * Model for Page Refresh Policy + */ +import { Model } from "./base"; +/** @public */ +export class PageRefreshPolicy extends Model { + constructor(strategy, updateDelayInterval, timeSinceOnScreenInterval, timeToLiveEndDate, refreshWhileVisible = false, refreshForDeviceDrivenContentChange = false) { + super(); + this.strategy = strategy; + this.updateDelayInterval = updateDelayInterval; + this.timeSinceOnScreenInterval = timeSinceOnScreenInterval; + this.timeToLiveEndDate = timeToLiveEndDate; + this.refreshWhileVisible = refreshWhileVisible; + this.refreshForDeviceDrivenContentChange = refreshForDeviceDrivenContentChange; + } +} +//# sourceMappingURL=page-refresh-policy.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/api/models/preview-platform.js b/node_modules/@jet-app/app-store/tmp/src/api/models/preview-platform.js new file mode 100644 index 0000000..387e689 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/api/models/preview-platform.js @@ -0,0 +1,25 @@ +import { isNothing } from "@jet/environment/types/optional"; +/** + * All {@linkcode PreviewPlatform}, defined in their order of precidence + */ +export const allPreviewPlatforms = ["iphone", "ipad", "mac", "vision", "watch", "tv"]; +/** + * Determines if {@linkcode input} is a {@linkcode PreviewPlatform} + */ +export function isPreviewPlatform(input) { + return allPreviewPlatforms.includes(input); +} +/** + * Normalize some {@linkcode input} into a {@linkcode PreviewPlatform}, if possible + */ +export function normalizePreviewPlaform(input) { + if (isNothing(input)) { + return undefined; + } + const normalized = input.toLocaleLowerCase(); + if (isPreviewPlatform(normalized)) { + return normalized; + } + return undefined; +} +//# sourceMappingURL=preview-platform.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/api/models/privacy.js b/node_modules/@jet-app/app-store/tmp/src/api/models/privacy.js new file mode 100644 index 0000000..f866cc5 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/api/models/privacy.js @@ -0,0 +1,75 @@ +import * as models from "./index"; +/** @public */ +export class PrivacyHeader extends models.Model { + constructor(bodyText, isDetailHeader, privacyTypes, bodyActions, supplementaryItems, seeDetailsAction) { + super(); + this.bodyText = bodyText; + this.isDetailHeader = isDetailHeader; + this.privacyTypes = privacyTypes; + this.bodyActions = bodyActions; + this.supplementaryItems = supplementaryItems; + this.seeDetailsAction = seeDetailsAction; + } +} +/** @public */ +export class PrivacyHeaderSupplementaryItem extends models.Model { + constructor(bodyText, action) { + super(); + this.bodyText = bodyText; + this.action = action; + } +} +/** @public */ +export class PrivacyFooter extends models.Model { + constructor(bodyText, actions, privacyTypesCount) { + super(); + this.bodyText = bodyText; + this.actions = actions; + this.privacyTypesCount = privacyTypesCount; + } +} +/** @public */ +export class PrivacyType extends models.ViewModel { + constructor(identifier, title, detail, artwork, style, purposes, categories, clickAction) { + super(); + this.identifier = identifier; + this.title = title; + this.detail = detail; + this.artwork = artwork; + this.style = style; + this.purposes = purposes; + this.categories = categories; + this.clickAction = clickAction; + this.wantsScrollFocus = false; + } +} +/** @public */ +export class PrivacyPurpose extends models.Model { + constructor(identifier, title, categories) { + super(); + this.identifier = identifier; + this.title = title; + this.categories = categories; + } +} +/** @public */ +export class PrivacyCategory extends models.Model { + constructor(identifier, title, artwork, style, dataTypes = []) { + super(); + this.identifier = identifier; + this.title = title; + this.artwork = artwork; + this.style = style; + this.dataTypes = dataTypes; + this.prefersSmallArtwork = false; + } +} +/** @public */ +export class PrivacyDefinition extends models.Model { + constructor(title, definition) { + super(); + this.title = title; + this.definition = definition; + } +} +//# sourceMappingURL=privacy.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/api/models/product-media-gallery-page.js b/node_modules/@jet-app/app-store/tmp/src/api/models/product-media-gallery-page.js new file mode 100644 index 0000000..a736532 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/api/models/product-media-gallery-page.js @@ -0,0 +1,12 @@ +import * as base from "./base"; +import * as metrics from "./metrics/metrics"; +/** @public */ +export class ProductMediaGalleryPage extends base.Model { + constructor(productMedia) { + super(); + this.productMedia = productMedia; + this.pageMetrics = new metrics.PageMetrics(); + this.pageRenderMetrics = {}; + } +} +//# sourceMappingURL=product-media-gallery-page.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/api/models/product-page-shelf-ids.js b/node_modules/@jet-app/app-store/tmp/src/api/models/product-page-shelf-ids.js new file mode 100644 index 0000000..63a4cbf --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/api/models/product-page-shelf-ids.js @@ -0,0 +1,45 @@ +/** + * Created by jellenbogen 4/12/21. + */ +// This is the set of ProductPageShelfIds that were previously containerized, this is needed +// for backwards compatibility converting shelfBased to non-shelfBased +export const legacyProductPageNonShelfSections = new Set([ + "header", + "topLockup", + "screenshots", +]); +// This is the list of productPageShelfIds that are able to be used on non-shelfBasaed product +// pages +export const legacyProductPageKnownShelfIds = new Set([ + "accessibilityDeveloperLink", + "accessibilityFeatures", + "accessibilityHeader", + "achievements", + "actionLinks", + "appEvents", + "bundleChildren", + "bundleParents", + "capabilities", + "description", + "editorialQuote", + "featuredIn", + "friendsPlaying", + "inAppPurchases", + "information", + "informationRibbon", + "notPurchasedLinks", + "purchasedLinks", + "moreByDeveloper", + "mostRecentVersion", + "preorderDisclaimer", + "privacyFooter", + "privacyHeader", + "privacyTypes", + "reviews", + "similarItems", + "subscriptions", + "textCards", + "textLinksShelf", + "videos", +]); +//# sourceMappingURL=product-page-shelf-ids.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/api/models/product.js b/node_modules/@jet-app/app-store/tmp/src/api/models/product.js new file mode 100644 index 0000000..a5764f3 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/api/models/product.js @@ -0,0 +1,58 @@ +/** + * Created by km on 2/13/17. + */ +import * as base from "./base"; +import * as metrics from "./metrics/metrics"; +import * as shelves from "./shelves"; +/** @public */ +export class Badge extends base.Model { + constructor(type, key, content, style, heading, caption, longCaption, accessibilityTitle, accessibilityCaption, leadingValue = "infer") { + super(); + this.type = type; + this.key = key; + this.content = content; + this.heading = heading; + this.caption = caption; + this.longCaption = longCaption; + this.leadingValue = leadingValue; + this.accessibilityTitle = accessibilityTitle; + this.accessibilityCaption = accessibilityCaption; + this.style = style; + this.isMonochrome = true; + } +} +/** @public */ +export class TextCard extends base.Model { + constructor(headingType, title, subtitle) { + super(); + this.titleStyle = headingType; + this.title = title; + this.subtitle = subtitle; + this.isExpanded = false; + } +} +/** @public */ +export class ProductPageSection extends base.Model { + constructor(type, shelfId) { + super(); + this.type = type; + this.shelfId = shelfId; + } +} +/** @public */ +export class ProductPageExpandedOfferDetails extends base.Model { + constructor(title, subtitle) { + super(); + this.title = title; + this.subtitle = subtitle; + } +} +/** @public */ +export class ProductPage extends shelves.Lockup { + constructor() { + super(); + this.pageMetrics = new metrics.PageMetrics(); + this.pageRenderMetrics = {}; + } +} +//# sourceMappingURL=product.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/api/models/reviews.js b/node_modules/@jet-app/app-store/tmp/src/api/models/reviews.js new file mode 100644 index 0000000..e71a139 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/api/models/reviews.js @@ -0,0 +1,115 @@ +/** + * Created by phare on 3/28/17. + */ +import * as base from "./base"; +import * as page from "./generic-page"; +/* tslint:disable:no-unsafe-any */ +/** @public */ +export class ReviewsPageSortOption extends base.Model { + constructor(id, title, selectedActionTitle, url) { + super(); + this.id = id; + this.title = title; + this.selectedActionTitle = selectedActionTitle; + this.url = url; + } +} +/** @public */ +export class ReviewsPage extends page.GenericPage { + constructor() { + super([]); + } +} +/** @public */ +export class Ratings extends base.Model { +} +/** @public */ +export class Review extends base.ViewModel { +} +/** @public */ +export class Response extends base.Model { +} +/** @public */ +export class TapToRate extends base.Model { + constructor() { + super(...arguments); + this.componentType = "tapToRate"; + } +} +/** @public */ +export class ProductReviewActions extends base.Model { + constructor() { + super(...arguments); + this.componentType = "productReviewActions"; + } +} +/** @public */ +export class EditorsChoice extends base.Model { + constructor(notes) { + super(); + this.showsBadge = false; + this.title = null; + this.notes = notes; + this.isCollapsed = true; + } +} +/** @public */ +export class ReviewsContainer extends base.Model { +} +/** @public */ +export class ProductReviewAction extends base.Model { +} +/** @public */ +export class ProductWriteAReview extends base.Model { +} +/** @public */ +export class ProductStarRatings extends Ratings { + constructor() { + super(...arguments); + this.componentType = "starRatings"; + } +} +/** @public */ +export class ProductStarRatingsHistogram extends Ratings { + constructor() { + super(...arguments); + this.componentType = "starRatingsHistogram"; + } +} +/** @public */ +export class ProductNoRatings extends Ratings { + constructor() { + super(...arguments); + this.componentType = "noRatings"; + } +} +/** @public */ +export class ProductRatingsAndReviewsMessage extends base.Model { + constructor(messageText) { + super(); + this.componentType = "message"; + this.messageText = messageText; + } +} +/** @public */ +export class ReviewSummary extends base.ViewModel { + constructor(body, bodyNoTitle, subtitle, subtitleArtwork, subtitleArtworkAlignment, bodyMediaType, flowPreviewActionsConfiguration) { + super(); + this.body = body; + this.bodyNoTitle = bodyNoTitle; + this.subtitle = subtitle; + this.subtitleArtwork = subtitleArtwork; + this.subtitleArtworkAlignment = subtitleArtworkAlignment; + this.bodyMediaType = bodyMediaType; + this.flowPreviewActionsConfiguration = flowPreviewActionsConfiguration; + } +} +/** @public */ +export class ProductReview extends base.ViewModel { + constructor() { + super(...arguments); + this.componentType = "productReview"; + } +} +/* tslint:enable:no-unsafe-any */ +//# sourceMappingURL=reviews.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/api/models/search/guided-search-actions.js b/node_modules/@jet-app/app-store/tmp/src/api/models/search/guided-search-actions.js new file mode 100644 index 0000000..047f60f --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/api/models/search/guided-search-actions.js @@ -0,0 +1,31 @@ +/** + * Actions for Guided Search Feature + */ +import * as actions from "../actions"; +/** + * @public + * An action that toggles a specified guided search token on and off. + * + * @note + * The same instance of this object must handle both selection and deselection + * as it can fire multiple times without JS refresh + */ +export class GuidedSearchTokenToggleAction extends actions.Action { + constructor(targetToken, searchOrigin) { + super("GuidedSearchTokenToggleAction"); + this.targetToken = targetToken; + this.searchOrigin = searchOrigin; + } +} +/** + * @public + * An action that changes the selected search entity (replacing existing, if any) + */ +export class SearchEntityChangeAction extends actions.Action { + constructor(entity, searchOrigin) { + super("SearchEntityChangeAction"); + this.entity = entity; + this.searchOrigin = searchOrigin; + } +} +//# sourceMappingURL=guided-search-actions.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/api/models/search/guided-search.js b/node_modules/@jet-app/app-store/tmp/src/api/models/search/guided-search.js new file mode 100644 index 0000000..cda8c16 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/api/models/search/guided-search.js @@ -0,0 +1,48 @@ +import * as models from "../base"; +/** + * @public + * Returns the system image for the given search entity. + */ +export function searchEntitySystemImage(entity) { + switch (entity) { + case "developer": + return "person.crop.square"; + case "story": + return "appstore"; + case "watch": + return "applewatch"; + case "arcade": + return "joystickcontroller.fill"; + default: + return undefined; + } +} +/** + * @public + * Model representing selectable displayed token for Guided Search + */ +export class GuidedSearchToken extends models.ViewModel { + constructor(value, isSelected, leadingIcon, displayName, clickAction) { + super(); + this.value = value; + this.isSelected = isSelected; + this.leadingIcon = leadingIcon; + this.displayName = displayName; + this.clickAction = clickAction; + } +} +/** + * @public + * Object storing mapping of: + * Search Term + [Set of Guided Search Tokens] = Optimization Term. + * I.e. how the search term and token swill be comined for final query term search results are searched for. + */ +export class GuidedSearchQuery extends models.Model { + constructor(searchTerm, selectedTokens, optimizationTerm) { + super(); + this.searchTerm = searchTerm; + this.selectedTokens = selectedTokens; + this.optimizationTerm = optimizationTerm; + } +} +//# sourceMappingURL=guided-search.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/api/models/search/search-categories.js b/node_modules/@jet-app/app-store/tmp/src/api/models/search/search-categories.js new file mode 100644 index 0000000..1d0b588 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/api/models/search/search-categories.js @@ -0,0 +1,96 @@ +import * as dynamicGenericPage from "../dynamic-generic-page"; +import * as models from "../base"; +export class SearchChartsAndCategoriesPage extends dynamicGenericPage.DynamicGenericPage { +} +/** + * @public + * Model representing a piece of UI for a chart or category on the search landing page. + */ +export class SearchChartOrCategory extends models.ViewModel { + constructor(title, artwork, collectionIcons, backgroundColor, badge, action, density, artworkSafeArea, textSafeArea) { + super(); + this.title = title; + this.artwork = artwork; + this.collectionIcons = collectionIcons; + this.backgroundColor = backgroundColor; + this.badge = badge; + this.action = action; + this.density = density; + this.artworkSafeArea = artworkSafeArea; + this.textSafeArea = textSafeArea; + } +} +/** + * @public + * Model representing a piece of UI for a medium ad lockup with custom creative artwork on the search landing page. + */ +export class MediumAdLockupWithAlignedRegionBackground extends models.ViewModel { + constructor(lockup, alignedRegionArtwork) { + super(); + this.lockup = lockup; + this.alignedRegionArtwork = alignedRegionArtwork; + } +} +/** + * @public + * Model representing a piece of UI for a medium ad lockup with screenshots on the search landing page. + */ +export class MediumAdLockupWithScreenshotsBackground extends models.ViewModel { + constructor(lockup, screenshots, isAnimated, secondaryTextColor, backgroundColor, riverSpeed) { + super(); + this.lockup = lockup; + this.screenshots = screenshots; + this.isAnimated = isAnimated; + this.secondaryTextColor = secondaryTextColor; + this.backgroundColor = backgroundColor; + this.riverSpeed = riverSpeed; + } +} +/** + * @public + * Model representing a piece of UI for a condensed ad lockup on the search landing page. + */ +export class CondensedAdLockupWithIconBackground extends models.ViewModel { + constructor(lockup, backgroundArtwork) { + super(); + this.lockup = lockup; + this.backgroundArtwork = backgroundArtwork; + } +} +export class SearchShelfAttributes { + constructor(id, title, displayStyle, displayCount, hasSeeAll, seeAllLink, searchLandingItemDisplayStyle = null, searchShelfKind) { + this.id = id; + this.title = title; + this.searchLandingItemDisplayStyle = searchLandingItemDisplayStyle; + this.displayStyle = displayStyle; + this.displayCount = displayCount; + this.hasSeeAll = hasSeeAll; + this.seeAllLink = seeAllLink; + this.searchShelfKind = searchShelfKind; + } +} +export var SearchLandingPageContentKind; +(function (SearchLandingPageContentKind) { + SearchLandingPageContentKind["Suggestion"] = "Query"; + SearchLandingPageContentKind["CategoriesAndCharts"] = "EditorialLink"; + SearchLandingPageContentKind["Apps"] = "AppsLockup"; + SearchLandingPageContentKind["EditorialCollection"] = "EditorialCollection"; +})(SearchLandingPageContentKind || (SearchLandingPageContentKind = {})); +export var SearchPageKind; +(function (SearchPageKind) { + SearchPageKind[SearchPageKind["Default"] = 0] = "Default"; + SearchPageKind[SearchPageKind["CategoriesAndCharts"] = 1] = "CategoriesAndCharts"; +})(SearchPageKind || (SearchPageKind = {})); +export var GenericSearchPageShelfDisplayStyleDensity; +(function (GenericSearchPageShelfDisplayStyleDensity) { + GenericSearchPageShelfDisplayStyleDensity["Density1"] = "density_1"; + GenericSearchPageShelfDisplayStyleDensity["Density2"] = "density_2"; + GenericSearchPageShelfDisplayStyleDensity["Density3"] = "density_3"; +})(GenericSearchPageShelfDisplayStyleDensity || (GenericSearchPageShelfDisplayStyleDensity = {})); +export var SearchLandingPageShelfItemIconKind; +(function (SearchLandingPageShelfItemIconKind) { + SearchLandingPageShelfItemIconKind["Chiclet"] = "chiclet"; + SearchLandingPageShelfItemIconKind["App"] = "app"; + SearchLandingPageShelfItemIconKind["Symbol"] = "symbol"; +})(SearchLandingPageShelfItemIconKind || (SearchLandingPageShelfItemIconKind = {})); +//# sourceMappingURL=search-categories.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/api/models/search/search.js b/node_modules/@jet-app/app-store/tmp/src/api/models/search/search.js new file mode 100644 index 0000000..4876f03 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/api/models/search/search.js @@ -0,0 +1,283 @@ +/** + * Created by km on 2/13/17. + */ +import * as actions from "../actions"; +import * as base from "../base"; +import * as metrics from "../metrics/metrics"; +/** @public */ +export class BaseSearchPage extends base.Model { + constructor() { + super(); + this.pageMetrics = new metrics.PageMetrics(); + this.pageRenderMetrics = {}; + } +} +/** @public */ +export class SearchResults extends BaseSearchPage { +} +/** + * @public + * Message to show user when search results need additional context. + * Currently supported by: + * - iOS, for NLS Safety UI + */ +export class SearchResultsContextCard extends base.ViewModel { + constructor(message, action) { + super(); + this.message = message; + this.action = action; + } +} +/** @public */ +export class SearchResultsPage extends BaseSearchPage { + constructor(shelfModels = []) { + super(); + /// The shelves of the results page. + this.shelves = []; + this.shelves = shelfModels; + } +} +export class SegmentedSearchResultsPage { + constructor() { + this.selectedSegmentId = null; + this.segments = []; + } +} +export var SegmentedSearchResultsPageSegmentType; +(function (SegmentedSearchResultsPageSegmentType) { + SegmentedSearchResultsPageSegmentType["visionOS"] = "xros"; + SegmentedSearchResultsPageSegmentType["iOS"] = "ios"; +})(SegmentedSearchResultsPageSegmentType || (SegmentedSearchResultsPageSegmentType = {})); +export class SegmentedSearchResultsPageSegment { +} +/** @public */ +export class SearchFacetSet extends base.Model { + constructor(type, values) { + super(); + this.type = type; + this.values = values; + } +} +/** @public */ +export class SearchFacetValue extends base.Model { + constructor(name, value, selectedValue = null) { + super(); + this.name = name; + this.value = value; + this.isSelected = value === selectedValue; + } +} +/** @public */ +export class SearchAdOpportunity extends base.Model { + constructor(instanceId, searchLifecycleEventPayloads, searchAd) { + super(); + this.instanceId = instanceId; + this.eventPayloads = searchLifecycleEventPayloads; + this.searchAd = searchAd; + } + /** + * Sets the template type on the event payloads for metrics tracking + * @param templateType The native ad template type + */ + setTemplateType(templateType) { + this.eventPayloads.placed.iAdTemplateType = templateType; + } + /** + * Sets the duplicate position on the event payloads for metrics tracking. This is only expected for search results + * @param position The position of the duplicate organic app lockup that appeared in the shelf's contents + */ + setDuplicatePosition(position) { + this.eventPayloads.placed.duplicatePosition = position; + } + /** + * Sets the missed reason code on the event payloads for metrics tracking + * @param reasonCode The reason that the opportunity was not filled + */ + setMissedOpportunityReason(reasonCode) { + this.eventPayloads.placed.missedOpportunityReason = reasonCode; + } +} +/** @public */ +export class SearchAd extends base.Model { + constructor(instanceId, iAdObject, searchAdLifecycleEventPayloads, impressionId, transparencyAction) { + super(); + this.instanceId = instanceId; + this.iAd = iAdObject; + this.eventPayloads = searchAdLifecycleEventPayloads; + this.impressionId = impressionId; + this.transparencyAction = transparencyAction; + } + /** + * Sets the template type on the event payloads for metrics tracking + * @param templateType The native ad template type + */ + setTemplateType(templateType) { + this.eventPayloads.placed.iAdTemplateType = templateType; + } + /** + * Sets the duplicate position on the event payloads for metrics tracking. This is only expected for search results + * @param position The position of the duplicate organic app lockup that appeared in the shelf's contents + */ + setDuplicatePosition(position) { + this.eventPayloads.placed.duplicatePosition = position; + } +} +/** @public */ +export class AdTransparencyAction extends actions.Action { + constructor(adTransparencyData) { + super("AdTransparencyAction"); + this.adTransparencyData = adTransparencyData; + } +} +/** @public */ +export class SearchAction extends actions.Action { + constructor(title, term, url, origin, entity, source, presentationStyle, referrerData) { + super("SearchAction"); + /** + * Whether or not Search should opt-in to spell-checking, which will: + * - Correct high-confidence misspellings server-side. + * - Suggest queries for low-confidence misspellings + * + * This defaults to `false`. Currently `true` for user-typed term and hints terms. + */ + this.spellCheckEnabled = false; + this.title = title; + this.term = term; + this.url = url; + this.origin = origin; + this.entity = entity; + this.source = source; + this.presentationStyle = presentationStyle !== null && presentationStyle !== void 0 ? presentationStyle : ["textFollowsTintColor"]; + this.referrerData = referrerData; + } +} +// endregion +// region Search Hints +/** @public */ +export class SearchHintSet extends base.Model { + constructor(hints, ghostHintTerm) { + super(); + this.hints = hints; + this.ghostHintTerm = ghostHintTerm; + this.pageMetrics = new metrics.PageMetrics(); + this.pageRenderMetrics = {}; + } +} +// endregion +// region Trending Searches +/** @public */ +export class TrendingSearches extends base.Model { + constructor(title, searches) { + super(); + this.title = title; + this.searches = searches; + } +} +// endregion +// region Search Message +/** + * @public + * Message associated with a set of Search Results. + * Currently supported by: + * - iOS, for Spell Correction feature. + */ +export class SearchResultsMessage extends base.Model { + constructor(primaryText, secondaryText, messageClickAction = null) { + super(); + this.primaryText = primaryText; + this.secondaryText = secondaryText; + this.messageClickAction = messageClickAction; + } +} +/** @public */ +export class SearchResult extends base.ViewModel { + constructor(resultType) { + super(); + this.resultType = resultType; + this.clickAction = null; + } +} +/** + * @public + * Search result for a single app + */ +export class AppSearchResult extends SearchResult { + constructor(lockup) { + super("content"); + this.lockup = lockup; + } +} +/** @public */ +export class InAppPurchaseSearchResult extends SearchResult { + constructor(lockup) { + super("inAppPurchase"); + this.lockup = lockup; + } +} +/** @public */ +export class BundleSearchResult extends SearchResult { + constructor(lockup) { + super("bundle"); + this.lockup = lockup; + } +} +/** @public */ +export class AdvertsSearchResult extends SearchResult { + constructor() { + super("advert"); + this.lockups = []; + this.displaysScreenshots = true; + this.itemBackground = "ad"; + } +} +/** @public */ +export class AppEventSearchResult extends SearchResult { + constructor() { + super("appEvent"); + } +} +/** @public */ +export class EditorialSearchResult extends SearchResult { + constructor(title) { + super("editorial"); + this.title = title; + } +} +/** + * @public + * Collection of lockups with shared heading and title. + * May include an action to show additional details (e.g. FlowAction to Article) + */ +export class SearchLockupCollection extends SearchResult { + constructor(heading, title, items, detailAction, headingArtwork) { + super("lockupCollection"); + this.heading = heading; + this.headingArtwork = headingArtwork; + this.title = title; + this.items = items; + this.detailAction = detailAction; + } +} +/** + * @public + * Guided search suggestions shown mid-scroll alongside other search results. + */ +export class GuidedSearchResult extends SearchResult { + constructor(title, tokens) { + super("guidedSearch"); + this.title = title; + this.tokens = tokens; + } +} +/** + * @public + * A model for the search results learn more notice. + */ +export class SearchResultsLearnMoreNotice extends base.ViewModel { + constructor(linkableText) { + super(); + this.linkableText = linkableText; + } +} +// endregion +//# sourceMappingURL=search.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/api/models/shelf-based-product.js b/node_modules/@jet-app/app-store/tmp/src/api/models/shelf-based-product.js new file mode 100644 index 0000000..7291687 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/api/models/shelf-based-product.js @@ -0,0 +1,25 @@ +/** + * Created by km on 2/13/17. + */ +import * as dynamicGenericPage from "./dynamic-generic-page"; +export const shelfBasedProductPageOrderingIdPurcahsed = "purchasedOrdering"; +export const shelfBasedProductPageOrderingIdNotPurcahsed = "notPurchasedOrdering"; +export const shelfBasedProductPageOrderingIdPurcahsedExpandedMedia = "purchasedOrdering_ExpandedMedia"; +export const shelfBasedProductPageOrderingIdNotPurcahsedExpandedMedia = "notPurchasedOrdering_ExpandedMedia"; +export const shelfBasedProductPageOrderingIdPurcahsedCompact = "purchasedOrdering_Compact"; +export const shelfBasedProductPageOrderingIdNotPurcahsedCompact = "notPurchasedOrdering_Compact"; +export const shelfBasedProductPageOrderingIdPurcahsedExpandedMediaCompact = "purchasedOrdering_ExpandedMedia_Compact"; +export const shelfBasedProductPageOrderingIdNotPurcahsedExpandedMediaCompact = "notPurchasedOrdering_ExpandedMedia_Compact"; +export const shelfBasedProductPageOrderingIdDownloading = "downloadingOrdering"; +export const shelfBasedProductPageOrderingIdDownloadingExpandedMedia = "downloadingOrdering_ExpandedMedia"; +export const shelfBasedProductPageOrderingIdDownloadingCompact = "downloadingOrdering_Compact"; +export const shelfBasedProductPageOrderingIdDownloadingExpandedMediaCompact = "downloadingOrdering_ExpandedMedia_Compact"; +/** @public */ +export class ShelfBasedProductPage extends dynamicGenericPage.DynamicGenericPage { + constructor() { + super(...arguments); + // Reviews + this.appPlatforms = []; + } +} +//# sourceMappingURL=shelf-based-product.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/api/models/shelves.js b/node_modules/@jet-app/app-store/tmp/src/api/models/shelves.js new file mode 100644 index 0000000..5a68c4c --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/api/models/shelves.js @@ -0,0 +1,709 @@ +/** + * Created by km on 2/13/17. + */ +import { isSome } from "@jet/environment/types/optional"; +import * as base from "./base"; +/** @public */ +export class InformationContainer extends base.Model { + constructor(annotationGroups) { + super(); + this.annotationGroups = annotationGroups; + } +} +/** @public */ +export class ImpressionableArtwork extends base.ViewModel { + constructor(art) { + super(); + this.art = art; + } +} +/** @public */ +export class AppShowcase extends base.Model { + constructor(type, lockup) { + super(); + this.type = type; + this.lockup = lockup; + } +} +/** @public */ +export class Banner extends base.Model { + constructor(message, focusedMessage, action, fullProductAction, leadingArtwork, leadingArtworkTintColor, includeBackgroundBorder, kind, hideCriteria) { + super(); + this.message = message; + this.focusedMessage = focusedMessage; + this.action = action; + this.fullProductAction = fullProductAction; + this.leadingArtwork = leadingArtwork; + this.leadingArtworkTintColor = leadingArtworkTintColor; + this.includeBackgroundBorder = includeBackgroundBorder; + this.kind = kind; + this.hideCriteria = hideCriteria; + } +} +/** + * @public + * Defines which banner to show for various app states so the client can update dynamically. + */ +export class AppStateBanner extends base.Model { + constructor(unknownBanner, buyBanner, downloadBanner, updateBanner, openBanner) { + super(); + this.unknownBanner = unknownBanner; + this.buyBanner = buyBanner; + this.downloadBanner = downloadBanner; + this.updateBanner = updateBanner; + this.openBanner = openBanner; + } +} +/** @public */ +export class InAppPurchaseShowcase extends base.Model { + constructor(lockup) { + super(); + this.lockup = lockup; + } +} +// endregion +/** @public */ +export class ProductMediaItem extends base.Model { +} +/** @public */ +export class ProductMedia extends base.Model { + constructor(items, mediaPlatform, allPlatforms, platformDescription, allPlatformsDescription, allPlatformsDescriptionPlacement) { + super(); + this.items = items; + this.mediaPlatform = mediaPlatform; + this.allPlatforms = allPlatforms; + this.platformDescription = platformDescription; + this.allPlatformsDescription = allPlatformsDescription; + this.allPlatformsDescriptionPlacement = allPlatformsDescriptionPlacement; + } +} +/** @public */ +export class ProductCapability extends base.Model { + constructor(type, title, caption, captionTrailingArtwork, linkAction, artwork, artworkTintColor) { + super(); + this.type = type; + this.title = title; + this.caption = caption; + this.captionTrailingArtwork = captionTrailingArtwork; + this.linkAction = linkAction; + this.artwork = artwork; + this.artworkTintColor = artworkTintColor; + } +} +/** @public */ +export class ProductDescription extends base.Model { + constructor(paragraph, developerLinks, tags, developerAction) { + super(); + this.paragraph = paragraph; + this.developerAction = developerAction; + this.tags = tags; + this.developerLinks = developerLinks; + } +} +/** @public */ +export class Footnote extends base.Model { + constructor(text) { + super(); + this.text = text; + this.presentationStyle = []; + this.clickAction = null; + } +} +/** @public */ +export class LockupContextMenuData extends base.Model { +} +/** @public */ +export class Lockup extends base.ViewModel { + isValid() { + if (!this.title) { + return false; + } + if (!this.icon || !this.icon.isValid()) { + return false; + } + return super.isValid(); + } +} +/** @public */ +export class ScreenshotsLockup extends Lockup { + constructor() { + super(); + this.screenshots = []; + /// Force a 4 screenshot portrait display style to ensure alignment with landscape screenshots + this.screenshotsDisplayStyle = "four-screenshots"; + } + isValid() { + const hasScreenshots = this.screenshots && this.screenshots.length; + if (!hasScreenshots) { + return false; + } + return super.isValid(); + } +} +/** @public */ +export class MixedMediaLockup extends Lockup { + constructor() { + super(); + this.screenshots = []; + this.trailers = []; + this.overrideLockupPosition = null; + this.screenshotsDisplayStyle = "control"; + this.metadataRibbonItems = []; + this.showMetadataInformationInLockup = false; + this.alignedRegionArtwork = null; + this.alignedRegionVideo = null; + } +} +export class UpdatesLockup extends Lockup { + constructor() { + super(); + this.whatsNew = undefined; + this.version = undefined; + this.size = undefined; + this.externalVersionId = undefined; + this.releaseDate = undefined; + this.installDate = undefined; + } +} +/** + * @public + * The metadata ribbon item used to contain any content of a metadata ribbon view. + * Keys will be present based on which metadata ribbon item type is being presented. + * This is using specific keys rather than creating them on the fly because the key + * names get mangled if they're not defined. + */ +export class MetadataRibbonItem extends base.ViewModel { + constructor(viewType) { + super(); + this.viewType = viewType; + this.moduleType = null; + this.labelText = null; + this.borderedText = null; + this.highlightedText = null; + this.starRating = null; + this.secondaryViewPlacement = "leading"; + this.artwork = null; + this.maxCharacterCount = null; + this.truncationLegibilityCharacterCountThreshold = null; + this.allowsTruncation = null; + } +} +/** @public */ +export class TrailersLockup extends Lockup { + isValid() { + const hasTrailers = this.trailers && this.trailers.isValid(); + if (!hasTrailers) { + return false; + } + if (!this.editorialTagline || this.editorialTagline.length === 0) { + return false; + } + return super.isValid(); + } +} +/** @public */ +export class Trailers extends base.Model { + constructor(videos, mediaPlatform) { + super(); + if (videos) { + this.videos = videos; + } + if (mediaPlatform) { + this.mediaPlatform = mediaPlatform; + } + } + isValid() { + return this.videos && this.videos.length > 0 && super.isValid(); + } +} +/** @public */ +export class InAppPurchaseLockup extends Lockup { +} +/** + * @public + * This is a lockup that represents the Arcade *service* (in contrast to lockups for Arcade *apps*) + * It is used in: + * - Today Card Overlay (iOS) + * - Article Persistent Lockup (iOS, macOS) + * - Maybe more + * + * This object does *not* subclass `Lockup`, as we cannot populate the app-specific properties. Decision was made to use a separate codepath altogether for delivering this + * fake lockup to views we otherwise display standard `Lockup`, as opposed to trying to reconcile the use-cases at this time. + * + * This class deliberately maintains two button actions to fire for subscribed and unsubscribed state, respectively, instead of a single hypothetical `ArcadeStateAction` + * that switches behavior based on subscription state. This is since the subscription state can switch between model apply time (what is visually represented in UI), + * and when it is run in action runner. + */ +export class ArcadeLockup extends base.ViewModel { +} +export class ImageLockup extends base.ViewModel { + constructor(artwork, lockup, caption, title, isDark = false) { + super(); + this.isDark = false; + this.artwork = artwork; + this.lockup = lockup; + this.caption = caption; + this.title = title; + this.isDark = isDark; + } + isValid() { + return this.lockup.isValid() && this.artwork.isValid(); + } +} +/** @public */ +export class TitledParagraph extends base.Model { + constructor(text, style, mediaType) { + super(); + this.text = text; + this.style = style; + this.mediaType = mediaType; + this.wantsCollapsedNewlines = true; + } +} +/** @public */ +export class EditorialCard extends base.ViewModel { + constructor() { + super(); + this.adamId = null; + this.caption = null; + this.title = null; + this.subtitle = null; + this.artwork = null; + this.shelfBackground = null; + this.clickAction = null; + this.decorations = []; + this.flowPreviewActionsConfiguration = null; + this.appEventFormattedDates = null; + this.mediaOverlayStyle = null; + } + isValid() { + if (!this.clickAction) { + return false; + } + if (!this.artwork || !this.artwork.isValid()) { + return false; + } + if (!this.caption || !this.title) { + return false; + } + return super.isValid(); + } +} +/** + * @public + * Used for: + * - Arcade Continue Playing. + */ +export class VideoCard extends base.ViewModel { + constructor() { + super(); + this.flowPreviewActionsConfiguration = null; + } +} +/** + * @public + * Used within a `RibbonBar` which is a horizontal scrolling bar that contains multiple `RibbonBarItem`s. + * E.g. a `RibbonBarItem` that defines an Arcade category within an Arcade category ribbon bar. + * + * Each bar item should have a title and a click action alongside with an optional artwork. + * Empty artworks are potentially handled by a fallback icon by client side. + */ +export class RibbonBarItem extends base.ViewModel { + constructor(title, clickAction) { + super(); + this.title = title; + this.clickAction = clickAction; + this.artwork = null; + this.accessibilityLabel = null; + } +} +/** @public */ +export class Brick extends base.ViewModel { + constructor() { + super(); + this.artworks = null; + this.accessibilityLabel = null; + this.shortEditorialDescription = null; + this.clickAction = null; + this.personalizationStyle = "none"; + this.shelfBackground = null; + this.flowPreviewActionsConfiguration = null; + this.editorialDisplayOptions = {}; + this.artworkSafeArea = null; + this.textSafeArea = null; + } + isValid() { + var _a, _b, _c, _d; + const hasValidArtwork = (_b = (_a = this.artworks) === null || _a === void 0 ? void 0 : _a.every((artwork, index) => { + return isSome(artwork) && artwork.isValid(); + })) !== null && _b !== void 0 ? _b : false; + const hasCollectionIcons = ((_d = (_c = this.collectionIcons) === null || _c === void 0 ? void 0 : _c.length) !== null && _d !== void 0 ? _d : 0) > 0; + const hasBackgroundColor = isSome(this.backgroundColor); + return (isSome(this.clickAction) && (hasValidArtwork || hasCollectionIcons || hasBackgroundColor) && super.isValid()); + } +} +/** @public */ +export class EditorialLink extends base.ViewModel { + constructor(descriptionText, summaryText, clickAction, linkPresentationEnabled = true) { + super(); + this.descriptionText = descriptionText; + this.summaryText = summaryText; + this.clickAction = clickAction; + this.linkPresentationEnabled = linkPresentationEnabled; + } +} +/** @public */ +export class SearchLink extends base.ViewModel { + constructor(title, clickAction, artwork, imageName) { + super(); + this.title = title; + this.clickAction = clickAction; + this.imageName = imageName; + this.artwork = artwork; + } +} +/** @public */ +export class LinkableText extends base.Model { + constructor(styledText, linkedSubstrings) { + super(); + this.styledText = styledText; + this.linkedSubstrings = linkedSubstrings; + } +} +/** @public */ +export class ProductPageLink extends base.Model { + constructor(text, clickAction, systemImageName, adamIdForPurchaseHistoryFilter) { + super(); + this.text = text; + this.clickAction = clickAction; + this.systemImageName = systemImageName; + this.adamIdForPurchaseHistoryFilter = adamIdForPurchaseHistoryFilter; + } +} +/** @public */ +export class PreorderDisclaimer extends base.Model { + constructor(disclaimer) { + super(); + this.disclaimer = disclaimer; + } +} +/** @public */ +export class TitledButtonStack extends base.Model { + constructor(buttons) { + super(); + this.buttons = buttons; + } +} +/** @public */ +export class TitledButton extends base.Model { + constructor(title, action) { + super(); + this.title = title; + this.action = action; + } +} +/** @public */ +export class FramedArtwork extends base.ViewModel { + constructor(artwork, isFullWidth, captionMediaType = "text/plain", caption = null, ordinal = null, hasRoundedCorners = null) { + super(); + this.artwork = artwork; + this.isFullWidth = isFullWidth; + this.hasRoundedCorners = typeof hasRoundedCorners === "boolean" ? hasRoundedCorners : !this.isFullWidth; + this.ordinal = ordinal; + this.caption = caption; + this.captionMediaType = captionMediaType; + } +} +/** @public */ +export class FramedVideo extends base.ViewModel { + constructor(video, isFullWidth, captionMediaType = "text/plain", caption = null, ordinal = null, hasRoundedCorners = null) { + super(); + this.video = video; + this.isFullWidth = isFullWidth; + this.hasRoundedCorners = typeof hasRoundedCorners === "boolean" ? hasRoundedCorners : !this.isFullWidth; + this.ordinal = ordinal; + this.caption = caption; + this.captionMediaType = captionMediaType; + } +} +/** @public */ +export class RoundedButton extends base.Model { + constructor(type, title, hasDivider, action) { + super(); + this.type = type; + this.title = title; + this.hasDivider = hasDivider; + this.action = action; + } +} +/** @public */ +export class Quote extends base.ViewModel { + constructor(text, credit, artwork, isFullWidthArtwork) { + super(); + this.text = text; + this.credit = credit; + this.artwork = artwork; + this.isFullWidthArtwork = isFullWidthArtwork || false; + } +} +/** @public */ +export class EditorialQuote extends base.ViewModel { + constructor(text, attribution) { + super(); + this.text = text; + this.attribution = attribution; + } +} +/** @public */ +export class HorizontalRule extends base.Model { + constructor(style, ruleColor, isFullWidth) { + super(); + this.style = style; + this.color = ruleColor; + this.isFullWidth = isFullWidth; + } +} +/** + * @public + * A horizontal scrolling bar that contains multiple `RibbonBarItem`s. + * + * E.g. category ribbon bar displaying different Arcade categories in a horizontal scrolling bar. + */ +export class RibbonBar extends base.ViewModel { + constructor(items) { + super(); + this.items = items; + } +} +/** @public */ +export class InformationRibbon extends base.ViewModel { + constructor(badges, hasTopSeparator, hasBottomSeparator, separatorsAreFullWidth, alignment) { + super(); + this.badges = badges; + this.hasTopSeparator = hasTopSeparator; + this.hasBottomSeparator = hasBottomSeparator; + this.separatorsAreFullWidth = separatorsAreFullWidth; + this.alignment = alignment; + } +} +// region Client Control Button +/** + * @public + * A client control button is an button appearing within Article pages. + * This is currently used so MAS editorial articles to link into OS Updates in preferences. + */ +export class ClientControlButton extends base.ViewModel { + constructor(title, buttonAction) { + super(); + this.title = title; + this.buttonAction = buttonAction; + } +} +/** @public */ +export class BreakoutDetails extends base.Model { + constructor(title, description, badgeType, callToActionButtonAction, backgroundStyle, textAlignment) { + super(); + this.badgeType = badgeType; + this.badge = badgeType.title; + this.title = title; + this.description = description; + this.callToActionButtonAction = callToActionButtonAction; + this.backgroundStyle = backgroundStyle; + this.textAlignment = textAlignment; + } +} +/** @public */ +export class LargeHeroBreakout extends base.ViewModel { + constructor(details, detailsDisplayProperties, heading, artwork, video, collectionIcons, backgroundColor) { + super(); + this.details = details; + this.detailsDisplayProperties = detailsDisplayProperties; + this.heading = heading; + this.artwork = artwork; + this.video = video; + this.collectionIcons = collectionIcons; + this.backgroundColor = backgroundColor; + this.editorialDisplayOptions = {}; + } +} +/** @public */ +export class SmallBreakout extends base.ViewModel { + constructor(details, iconArtwork, backgroundColor) { + super(); + this.details = details; + this.iconArtwork = iconArtwork; + this.backgroundColor = backgroundColor; + } +} +/** + * The `Placeholder` is a dummy model used so a shelf with content type Placeholder can be populated with items representing the amount of placeholders to show + */ +export class Placeholder extends base.ViewModel { +} +// region ArcadeShowcase +/** + * @public + * The `ArcadeShowcase` is a model that is similar to the upsell breakout on the grouping pages, but appears inline in article pages. + * This module is specialized for Arcade service, since we don't have other subscription services currently. + * On some platforms, it may display some icon artwork. + */ +export class ArcadeShowcase extends base.ViewModel { + /** + * Initialize arcade showcase with required values + * @param unsubscribedAction Action to run in non-subscribed state. + * @param subscribedAction Action to run in subscribed state. + */ + constructor(unsubscribedAction, subscribedAction) { + super(); + this.unsubscribedAction = unsubscribedAction; + this.subscribedAction = subscribedAction; + } +} +/** @public */ +export class GameCenterReengagement extends base.ViewModel { + constructor(badgeGlyph, badge, title, subtitle, achievement, lockup, backgroundColor, backgroundArtwork, heroAction) { + super(); + this.badgeGlyph = badgeGlyph; + this.badge = badge; + this.title = title; + this.subtitle = subtitle; + this.achievement = achievement; + this.backgroundColor = backgroundColor; + this.backgroundArtwork = backgroundArtwork; + this.lockup = lockup; + this.heroAction = heroAction; + } +} +export class UnifiedMessage extends base.ViewModel { + constructor(placement, context, deliveryMethod) { + super(); + this.placement = placement; + this.context = context; + this.deliveryMethod = deliveryMethod !== null && deliveryMethod !== void 0 ? deliveryMethod : "pushAndPull"; + } +} +export const genericShelfBatchGroupBase = "shelfBatchGroup_"; +export var IncompleteShelfFetchStrategy; +(function (IncompleteShelfFetchStrategy) { + /** + * Indicates the shelf should be fetched as soon as the page is loaded, + * this is the behavior we've always had, where all incomplete shelves are + * fetched after the page loads + */ + IncompleteShelfFetchStrategy["OnPageLoad"] = "onPageLoad"; + /** + * Indicates the shelf should be fetched when it is about to be scrolled into view + */ + IncompleteShelfFetchStrategy["OnShelfWillAppear"] = "onShelfWillAppear"; +})(IncompleteShelfFetchStrategy || (IncompleteShelfFetchStrategy = {})); +// Describes the snapping behaviour for horizontally scrolling shelves. +export var ShelfHorizontalScrollTargetBehavior; +(function (ShelfHorizontalScrollTargetBehavior) { + // The default behaviour, where scrolling will snap to the leading edge of the view. + ShelfHorizontalScrollTargetBehavior["ViewAligned"] = "viewAligned"; + // Scrolling will snap so that views are center aligned in their container. Primarily + // used for large components that take up most of the container. + ShelfHorizontalScrollTargetBehavior["CenterAligned"] = "centerAligned"; +})(ShelfHorizontalScrollTargetBehavior || (ShelfHorizontalScrollTargetBehavior = {})); +/** @public */ +export class Shelf extends base.ViewModel { + constructor(contentType, marker = null, items = null) { + super(); + // - Item Properties + this.contentType = contentType; + this.marker = marker; + this.items = items || []; + this.header = undefined; + // - Incomplete Shelf Properties + this.url = null; + this.mergeWhenFetched = false; + this.fetchStrategy = IncompleteShelfFetchStrategy.OnPageLoad; + this.batchGroup = null; + // - Overflow Properties + this.seeAllAction = null; + // - Footer Properties + this.footerTitle = null; + this.footerAction = null; + // - Presentation Properties + this.eyebrow = null; + this.eyebrowArtwork = null; + this.footerStyle = null; + this.title = null; + this.titleArtwork = null; + this.subtitle = null; + this.isHorizontal = false; + this.isHidden = false; + this.rowsPerColumn = null; + this.background = { type: "none" }; + this.contentsMetadata = { type: "none" }; + // - Personalization & Filtering Properties + this.isPersonalized = false; + this.shouldFilterApps = false; + } +} +/** @public */ +export var ShelfBackgroundGradientLocation; +(function (ShelfBackgroundGradientLocation) { + ShelfBackgroundGradientLocation["TopLeading"] = "topLeading"; + ShelfBackgroundGradientLocation["Top"] = "top"; + ShelfBackgroundGradientLocation["TopTrailing"] = "topTrailing"; + ShelfBackgroundGradientLocation["Trailing"] = "trailing"; + ShelfBackgroundGradientLocation["BottomTrailing"] = "bottomTrailing"; + ShelfBackgroundGradientLocation["Bottom"] = "bottom"; + ShelfBackgroundGradientLocation["BottomLeading"] = "bottomLeading"; + ShelfBackgroundGradientLocation["Leading"] = "leading"; +})(ShelfBackgroundGradientLocation || (ShelfBackgroundGradientLocation = {})); +export var GroupDisplayStyle; +(function (GroupDisplayStyle) { + GroupDisplayStyle["Grid"] = "grid"; + GroupDisplayStyle["Hero"] = "hero"; + GroupDisplayStyle["Standard"] = "standard"; +})(GroupDisplayStyle || (GroupDisplayStyle = {})); +/// The different artwork types we can display in the header +export var ShelfHeaderArtworkType; +(function (ShelfHeaderArtworkType) { + /// The image displayed is a content icon + ShelfHeaderArtworkType["Icon"] = "icon"; + /// The image displayed is a category image + ShelfHeaderArtworkType["Category"] = "category"; +})(ShelfHeaderArtworkType || (ShelfHeaderArtworkType = {})); +/** @public */ +export class EditorialStoryCard extends base.ViewModel { + constructor(title, artwork, video, heading, badge, description, clickAction) { + super(); + this.title = title; + this.artwork = artwork; + this.video = video; + this.heading = heading; + this.badge = badge; + this.description = description; + this.clickAction = clickAction; + this.shelfBackground = null; + this.collectionIcons = null; + this.editorialDisplayOptions = {}; + } +} +/** @public */ +export class PosterLockup extends Lockup { + isValid() { + const hasPosterArtwork = this.posterArtwork; + const hasPosterVideo = this.posterVideo; + // We require at least one type of poster media. + if (!(hasPosterArtwork || hasPosterVideo)) { + return false; + } + return super.isValid(); + } +} +/** @public */ +export class PageTab extends base.Model { +} +/** @public */ +export class PageTabs extends base.Model { + constructor() { + super(); + /// The id is not from a payload and should be unique to the container + this.id = random.nextUUID(); + } +} +//# sourceMappingURL=shelves.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/api/models/today-page.js b/node_modules/@jet-app/app-store/tmp/src/api/models/today-page.js new file mode 100644 index 0000000..58017fa --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/api/models/today-page.js @@ -0,0 +1,300 @@ +import * as base from "./base"; +import * as metrics from "./metrics/metrics"; +export var AdPlacementBehavior; +(function (AdPlacementBehavior) { + AdPlacementBehavior["insertIntoShelf"] = "insertIntoShelf"; + AdPlacementBehavior["replaceOrganic"] = "replaceOrganic"; + AdPlacementBehavior["dropAd"] = "dropAd"; +})(AdPlacementBehavior || (AdPlacementBehavior = {})); +/** @public */ +export class TodayCardMedia extends base.ViewModel { + constructor(kind) { + super(); + this.kind = kind; + } + /** + * @returns The best background color for the media. This is used to + * determine the color the section background gradients. If undefined, the + * default section background color will be used. + */ + bestBackgroundColor() { + return undefined; + } +} +/** @public */ +export class TodayCardMediaWithArtwork extends TodayCardMedia { + constructor(kind, artworks, videos, artworkLayoutsWithMetrics) { + super(kind); + this.artworks = artworks; + this.videos = videos; + this.artworkLayoutsWithMetrics = artworkLayoutsWithMetrics; + } + bestBackgroundColor() { + var _a, _b, _c; + return (_b = (_a = this.videos[0]) === null || _a === void 0 ? void 0 : _a.preview.backgroundColor) !== null && _b !== void 0 ? _b : (_c = this.artworks[0]) === null || _c === void 0 ? void 0 : _c.backgroundColor; + } +} +/** @public */ +export class TodayCardMediaHero extends TodayCardMediaWithArtwork { + constructor(artworks, videos) { + super("hero", artworks, videos, []); + } + isValid() { + const isArtworkValid = this.artworks.every((artwork) => artwork.isValid()) && this.artworks.length > 0; + const isVideoValid = this.videos.every((video) => video.isValid()) && this.videos.length > 0; + return (isArtworkValid || isVideoValid) && super.isValid(); + } +} +/** @public */ +export class TodayCardMediaArtwork extends TodayCardMediaWithArtwork { + constructor(artworks, videos, artworkLayoutsWithMetrics, titleBackingGradient) { + super("artwork", artworks, videos, artworkLayoutsWithMetrics); + this.titleBackingGradient = titleBackingGradient; + } +} +/** @public */ +export class TodayCardMediaAppIcon extends TodayCardMedia { + constructor(icon) { + super("appIcon"); + this.icon = icon; + } + bestBackgroundColor() { + return this.icon.backgroundColor; + } +} +/** @public */ +export class TodayCardMediaBrandedSingleApp extends TodayCardMediaWithArtwork { + constructor(icon, artworks, videos, artworkLayoutsWithMetrics, titleBackingGradient) { + super("brandedSingleApp", artworks, videos, artworkLayoutsWithMetrics); + this.icon = icon; + this.titleBackingGradient = titleBackingGradient; + } + bestBackgroundColor() { + var _a; + return (_a = super.bestBackgroundColor()) !== null && _a !== void 0 ? _a : this.icon.backgroundColor; + } +} +/** @public */ +export class TodayCardMediaList extends TodayCardMediaWithArtwork { + constructor(lockups, artworks, videos, artworkLayoutsWithMetrics, marketingText, isMediaDark) { + super("list", artworks, videos, artworkLayoutsWithMetrics); + this.lockups = lockups; + this.marketingText = marketingText; + this.isMediaDark = isMediaDark; + } +} +/** @public */ +export class TodayCardMediaMultiApp extends TodayCardMedia { + constructor(lockups, additionalText) { + super("multiApp"); + this.lockups = lockups; + this.additionalText = additionalText; + } +} +/** @public */ +export class TodayCardInAppPurchase extends TodayCardMedia { + constructor(lockup) { + super("inAppPurchase"); + this.lockup = lockup; + } + bestBackgroundColor() { + return this.lockup.icon.backgroundColor; + } +} +/** @public */ +export class TodayCardMediaRiver extends TodayCardMedia { + constructor(lockups) { + super("river"); + this.lockups = lockups; + this.lockupImpressionLimit = 10; + } +} +/** @public */ +export class TodayCardMediaGrid extends TodayCardMedia { + constructor(lockups, artworkGridType) { + super("grid"); + this.lockups = lockups; + this.artworkGridType = artworkGridType; + this.lockupImpressionLimit = 3; + } +} +/** @public */ +export class TodayCardMediaVideo extends TodayCardMediaWithArtwork { + constructor(videos, artworkLayoutsWithMetrics, description) { + super("video", [], videos, artworkLayoutsWithMetrics); + this.description = description; + } +} +/** @public */ +export class TodayCardMediaAppEvent extends TodayCardMediaWithArtwork { + constructor(formattedDates, startDate, tintColor, artworks, videos, artworkLayoutsWithMetrics, blurStyle) { + super("appEvent", artworks, videos, artworkLayoutsWithMetrics); + this.formattedDates = formattedDates; + this.startDate = startDate; + this.tintColor = tintColor; + this.blurStyle = blurStyle; + } +} +/** @public */ +export class TodayCardMediaMediumLockupWithAlignedRegion extends TodayCardMedia { + constructor(mediumAdLockupWithAlignedRegionBackground) { + super("mediumLockupWithAlignedRegion"); + this.mediumAdLockupWithAlignedRegionBackground = mediumAdLockupWithAlignedRegionBackground; + } +} +/** @public */ +export class TodayCardMediaMediumLockupWithScreenshots extends TodayCardMedia { + constructor(mediumAdLockupWithScreenshotsBackground) { + super("mediumLockupWithScreenshots"); + this.mediumAdLockupWithScreenshotsBackground = mediumAdLockupWithScreenshotsBackground; + } +} +/** + * @public + * Media for a today card with a single lockup. + */ export class TodayCardMediaSingleLockup extends TodayCardMedia { + constructor(condensedAdLockupWithIconBackground) { + super("singleLockup"); + this.condensedAdLockupWithIconBackground = condensedAdLockupWithIconBackground; + } + /** + * @returns The best background color for the media. This is used to + * determine the color the section background gradients. If undefined, the + * default section background color will be used. + */ + bestBackgroundColor() { + var _a; + return (_a = this.condensedAdLockupWithIconBackground) === null || _a === void 0 ? void 0 : _a.backgroundArtwork.backgroundColor; + } +} +/** @public */ +export class TodayCardOverlay extends base.Model { + constructor(kind) { + super(); + this.kind = kind; + } +} +/** @public */ +export class TodayCardLockupOverlay extends TodayCardOverlay { + constructor(lockup) { + super("lockup"); + this.lockup = lockup; + this.displaysIcon = true; + } +} +/** + * @public + * Variant of `TodayCardLockupOverlay` with additional functionality, + * like optionally showing a transient `TodayCardParagraphOverlay` style overlay + */ +export class TodayCardMarketingLockupOverlay extends TodayCardOverlay { + constructor(lockup, paragraph, hideBackground, artworkBackgroundColor) { + super("marketingLockup"); + this.lockup = lockup; + this.paragraph = paragraph; + this.hideBackground = hideBackground; + this.artworkBackgroundColor = artworkBackgroundColor; + } +} +/** @public */ +export class TodayCardLockupListOverlay extends TodayCardOverlay { + constructor(lockups) { + super("lockupList"); + this.lockups = lockups; + } +} +/** @public */ +export class TodayCardParagraphOverlay extends TodayCardOverlay { + constructor(paragraph, style) { + super("paragraph"); + this.paragraph = paragraph; + this.style = style; + } +} +/** @public */ +export class TodayCardActionOverlay extends TodayCardOverlay { + constructor(action) { + super("action"); + this.action = action; + } +} +/** @public */ +export class TodayCardThreeLineOverlay extends TodayCardOverlay { + constructor(heading, title, description) { + super("threeLine"); + this.heading = heading; + this.title = title; + this.description = description; + } +} +/** + * @public + * Overlay for Acquisition Editorial Items that promote the Arcade Service. + */ +export class TodayCardArcadeLockupOverlay extends TodayCardOverlay { + constructor(arcadeLockup) { + super("arcadeLockup"); + this.arcadeLockup = arcadeLockup; + } +} +/** + * @public + * Overlay for App Event lockups. + */ +export class TodayCardAppEventLockupOverlay extends TodayCardOverlay { + constructor(lockup) { + super("appEventLockup"); + this.lockup = lockup; + } +} +// endregion +// region Cards +/** @public */ +export class TodayCard extends base.ViewModel { + constructor() { + super(); + this.heading = undefined; + this.title = undefined; + this.shortTitle = undefined; + this.titleArtwork = undefined; + this.media = undefined; + this.heroMedia = undefined; + this.overlay = undefined; + this.style = undefined; + this.clickAction = undefined; + this.backgroundColor = undefined; + this.inlineDescription = undefined; + this.collapsedHeading = undefined; + this.editorialDisplayOptions = undefined; + this.supportsMediaMirroring = undefined; + } +} +/** @public */ +export class InlineTodayCards extends base.ViewModel { + constructor(cards) { + super(); + this.cards = cards; + } +} +// endregion +/** @public */ +export class TodayPage extends base.Model { + constructor(shelfModels, onboardingCardIds, title, tabTitle, titleDetail, shortTitleDetail) { + super(); + this.shelves = shelfModels; + this.title = title; + this.titleDetail = titleDetail; + this.shortTitleDetail = shortTitleDetail; + this.tabTitle = tabTitle; + this.nextPage = null; + if (onboardingCardIds) { + this.onboardingCardIds = onboardingCardIds; + } + else { + this.onboardingCardIds = []; + } + this.pageMetrics = new metrics.PageMetrics(); + this.pageRenderMetrics = {}; + } +} +//# sourceMappingURL=today-page.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/api/models/top-charts.js b/node_modules/@jet-app/app-store/tmp/src/api/models/top-charts.js new file mode 100644 index 0000000..52f229f --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/api/models/top-charts.js @@ -0,0 +1,63 @@ +/** + * Created by km on 2/13/17. + */ +import * as models from "./index"; +import * as metrics from "./metrics/metrics"; +/** @public */ +export class TopChartSegment extends models.Model { + constructor(shortName, longName, chart, shelfModels) { + super(); + this.shortName = shortName; + this.longName = longName; + this.chart = chart; + this.shelves = shelfModels; + this.nextPage = null; + this.pageMetrics = new metrics.PageMetrics(); + this.pageRenderMetrics = {}; + } +} +/** @public */ +export class TopChartCategory extends models.Category { + constructor(category, url, children) { + super(category.name, category.genreId, category.artwork, category.ageBandId, children); + this.shortName = this.name; + this.longName = this.name; + this.url = url; + } +} +/** + * Shared "Top Charts" view-model + * + * @see {@linkcode TopChartsPage} for when "Top Charts" should be rendered as a page + * @see {@linkcode ChartsHubChart} for when a "chart" exists on a page with other "charts" + */ +export class TopChartsPage extends models.Model { + constructor(genreId, ageBandId, title, segments, categoriesButtonTitle, categories) { + super(); + this.genreId = genreId; + this.ageBandId = ageBandId; + this.title = title; + this.segments = segments; + this.categoriesButtonTitle = categoriesButtonTitle; + this.categories = categories; + this.initialSegmentIndex = 0; + } +} +/** + * A single chart within a {@linkcode ChartsHubPage} + */ +export class ChartsHubChart extends TopChartsPage { +} +/** + * View-model for the "Charts Hub" page + * + * This is used by the "web" client to power the charts overview, which includes + * multiple charts in the same view + */ +export class ChartsHubPage extends models.Model { + constructor(charts) { + super(); + this.charts = charts; + } +} +//# sourceMappingURL=top-charts.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/api/models/uber.js b/node_modules/@jet-app/app-store/tmp/src/api/models/uber.js new file mode 100644 index 0000000..1fd7d78 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/api/models/uber.js @@ -0,0 +1,19 @@ +import * as base from "./base"; +/** @public */ +export class Uber extends base.Model { + constructor(style) { + super(); + this.style = style; + } + isValidUber(isTV, isVision) { + function isDefinedNonNullNonEmpty(object) { + return object !== undefined && object !== null && Object.keys(object).length !== 0; + } + return (isDefinedNonNullNonEmpty(this.artwork) || + isDefinedNonNullNonEmpty(this.compactArtwork) || + isDefinedNonNullNonEmpty(this.video) || + isDefinedNonNullNonEmpty(this.compactVideo) || + ((isTV || isVision) && isDefinedNonNullNonEmpty(this.iconArtwork))); + } +} +//# sourceMappingURL=uber.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/api/models/web-renderable-page.js b/node_modules/@jet-app/app-store/tmp/src/api/models/web-renderable-page.js new file mode 100644 index 0000000..67be95f --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/api/models/web-renderable-page.js @@ -0,0 +1,4 @@ +export function injectSEOData(page, seoData) { + page.seoData = seoData; +} +//# sourceMappingURL=web-renderable-page.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/api/typings/constants.js b/node_modules/@jet-app/app-store/tmp/src/api/typings/constants.js new file mode 100644 index 0000000..7c04c8c --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/api/typings/constants.js @@ -0,0 +1,21 @@ +import { makeMetatype } from "@jet/environment/util/metatype"; +export const storeMetrics = makeMetatype("app-store:storeMetrics"); +export const amsEngagement = makeMetatype("app-store:amsEngagement"); +export const device = makeMetatype("app-store:device"); +export const user = makeMetatype("app-store:user"); +export const player = makeMetatype("games:player"); +export const metricsIdentifiers = makeMetatype("app-store:metricsIdentifiers"); +export const arcade = makeMetatype("app-store:arcade"); +export const gameCenter = makeMetatype("app-store:gameCenter"); +export const resilientDeepLinks = makeMetatype("app-store:resilientDeepLinks"); +export const ads = makeMetatype("app-store:ads"); +export const onDeviceRecommendationsManager = makeMetatype("app-store:onDeviceRecommendationsManager"); +export const onDeviceSearchHistoryManager = makeMetatype("app-store:onDeviceSearchHistoryManager"); +export const featureFlags = makeMetatype("app-store:featureFlags"); +export const mediaToken = makeMetatype("app-store:mediaTokenService"); +export const adsLocalizer = makeMetatype("app-store:adsLocalizer"); +export const appDistribution = makeMetatype("app-store:appDistribution"); +export const timeoutManager = makeMetatype("app-store:timeoutManager"); +export const treatmentStore = makeMetatype("app-store:treatmentStore"); +export const userDefaults = makeMetatype("app-store:userDefaults"); +//# sourceMappingURL=constants.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/api/util.js b/node_modules/@jet-app/app-store/tmp/src/api/util.js new file mode 100644 index 0000000..a6d199c --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/api/util.js @@ -0,0 +1,16 @@ +/** + * Ensures that Adam IDs are always represented with the same format + * + * "ID" segments in a URL typically include an `id` prefix that should + * not be present when using the value as an identifier for a resource + * in the Media APi. + * + * This function strips the prefix if present + */ +export function normalizeAdamID(raw) { + if (raw.startsWith("id")) { + return raw.substring(2); + } + return raw; +} +//# sourceMappingURL=util.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/accessibility/accessibility-common.js b/node_modules/@jet-app/app-store/tmp/src/common/accessibility/accessibility-common.js new file mode 100644 index 0000000..eed6d29 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/accessibility/accessibility-common.js @@ -0,0 +1,70 @@ +import { isNothing, isSome } from "@jet/environment"; +import { unreachable } from "../../foundation/util/errors"; +import { isNotEmpty } from "../../foundation/util/array-util"; +let suppressedAccessibilityApps = null; +/** + * Intiailizes `suppressedAccessibilityApps` with whatever is in the bag for `suppressedAccessibilityAppIds`, so that it only needs to be mapped to a Set once. + */ +function initialize(objectGraph) { + if (suppressedAccessibilityApps !== null) { + return; + } + suppressedAccessibilityApps = new Set(); + for (const appId of objectGraph.bag.suppressedAccessibilityAppIds) { + suppressedAccessibilityApps.add(appId); + } +} +/** + * Returns whether the accessibility support feature is enabled. + */ +export function isProductAccessibilityLabelsEnabled(objectGraph) { + return (objectGraph.featureFlags.isEnabled("product_accessibility_support_2025A") && + objectGraph.bag.enableAppAccessibilityLabels); +} +/** + * Returns whether we should suppress the accessibility information for a given adamId. + */ +export function shouldSuppressAccessibilityLabelsForAdamId(objectGraph, adamId) { + initialize(objectGraph); + return isSome(suppressedAccessibilityApps) && isNotEmpty(adamId) && suppressedAccessibilityApps.has(adamId); +} +/** + * Returns whether we should suppress the accessibility information for a given bundleId. + */ +export function shouldSuppressAccessibilityLabelsForBundleId(objectGraph, bundleId) { + initialize(objectGraph); + // We always want to suppress accessibility information for macOS installers, as accessibility labels don't apply. + if (bundleId.startsWith("com.apple.InstallAssistant.")) { + return true; + } + return isSome(suppressedAccessibilityApps) && isNotEmpty(bundleId) && suppressedAccessibilityApps.has(bundleId); +} +/** + * Returns the device family we want to display the accessibility labels for, based on `AppPlatform` for the first set + * of screenshots we display on the product page. + */ +export function deviceFamilyForAccessibilityLabels(platform) { + if (isNothing(platform)) { + return null; + } + switch (platform) { + case "phone": + return "iphone"; + case "pad": + return "ipad"; + case "mac": + return "mac"; + case "tv": + return "tvos"; + case "vision": + return "realityDevice"; + case "watch": + return "watch"; + case "messages": + // If we are surfacing Messages metadata, we want to hide the labels. + return null; + default: + unreachable(platform); + } +} +//# sourceMappingURL=accessibility-common.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/account/account-links-regex-parser.js b/node_modules/@jet-app/app-store/tmp/src/common/account/account-links-regex-parser.js new file mode 100644 index 0000000..b6d2ad2 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/account/account-links-regex-parser.js @@ -0,0 +1,35 @@ +import { DynamicUIRequestInfo, FlowAction } from "../../api/models"; +/** + * Run a given URL through all the different web view regexes we have in the bag and return a + * flow action with the correct page type for this url. This is essentially what AMSURLParser + * is doing, however that does not work for App Store the way we route URLs. + * + * @param objectGraph The App Store object graph. + * @param urlString The string to create the appropriate FlowAction for. + * @returns A FlowAction appropriate for the provided URL. + */ +export function flowActionForAccountURL(objectGraph, urlString) { + // Dynamic UI + const dynamicUIRegexPatterns = objectGraph.bag.dynamicUIRegexStrings; + for (const pattern of dynamicUIRegexPatterns) { + const regexPattern = pattern.replace(/\//g, "\\/"); + const dynamicUIPattern = new RegExp(regexPattern); + if (dynamicUIPattern.test(urlString)) { + const action = new FlowAction("dynamicUI", urlString); + action.pageData = new DynamicUIRequestInfo(objectGraph.bag.metricsTopic); + return action; + } + } + // Web UI regex check + const webViewRegexPatterns = objectGraph.bag.webViewRegexStrings; + for (const pattern of webViewRegexPatterns) { + const regexPattern = pattern.replace(/\//g, "\\/"); + const webViewPattern = new RegExp(regexPattern); + if (webViewPattern.test(urlString)) { + return new FlowAction("webView", urlString); + } + } + /// Last of all try to allow the finance view controller to handle this url + return new FlowAction("finance", urlString); +} +//# sourceMappingURL=account-links-regex-parser.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/ads/ad-common.js b/node_modules/@jet-app/app-store/tmp/src/common/ads/ad-common.js new file mode 100644 index 0000000..6216dba --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/ads/ad-common.js @@ -0,0 +1,433 @@ +/** + * Common utilities for dealing with Ads. + */ +import { isNothing, isSome } from "@jet/environment"; +import { Lockup, TodayCard, TodayCardMediaMediumLockupWithAlignedRegion, TodayCardMediaMediumLockupWithScreenshots, TodayCardMediaSingleLockup, } from "../../api/models"; +import { isDefinedNonNull, isDefinedNonNullNonEmpty, isNull, isNullOrEmpty, } from "../../foundation/json-parsing/server-data"; +import { attributeAsString } from "../../foundation/media/attributes"; +import { IAdSearchInformation } from "../metrics/helpers/models"; +import { platformSupportsAdverts } from "../search/search-ads"; +import { shouldTodayAdBeCondensed } from "../../foundation/experimentation/today-ad-experiments"; +import { getTemplateTypeForMediumAdFromLockupWithCustomCreative, getTemplateTypeForMediumAdFromLockupWithScreenshots, searchAdMissedOpportunityFromId, } from "../lockups/ad-lockups"; +import { contentAttributeAsString } from "../content/attributes"; +import { currentLocation, currentPosition } from "../metrics/helpers/location"; +// region placement logic +/** + * A helper function to check if the given AdPlacement is enabled. + * @param objectGraph The object graph. + * @param adPlacementKey The ad placement to check. Matches values sent in the bag. + * @returns A boolean indicating if the provided AdPlacement is enabled. + */ +export function isAdPlacementEnabled(objectGraph, placementType) { + if (!platformSupportsAdverts(objectGraph)) { + return false; + } + switch (placementType) { + case "searchLanding": + // In transitioning from the legacy bag key `isSearchLandingAdsEnabled` to the new key `enabledAdPlacements`, + // we will enable SLP ads if either of the keys indicates ads are enabled for SLP. + const slpPlacementBagValue = adPlacementBagValueForAdPlacementType(placementType); + if (isSome(slpPlacementBagValue)) { + return (objectGraph.bag.isSearchLandingAdsEnabled || + objectGraph.bag.enabledAdPlacements.includes(slpPlacementBagValue)); + } + else { + return objectGraph.bag.isSearchLandingAdsEnabled; + } + case "searchResults": + return true; // Legacy + case "today": + const todayBagValue = adPlacementBagValueForAdPlacementType(placementType); + if (isNull(todayBagValue)) { + return false; + } + return objectGraph.bag.enabledAdPlacements.includes(todayBagValue) && isSome(todayAdStyle(objectGraph)); + case "productPageYMAL": + case "productPageYMALDuringDownload": + const placementBagValue = adPlacementBagValueForAdPlacementType(placementType); + if (isNull(placementBagValue)) { + return false; + } + return objectGraph.bag.enabledAdPlacements.includes(placementBagValue); + default: + return false; + } +} +/** + * Get the style to use for the Today ad. + * A return type of `undefined` indicates the ad style is unsupported on the client. + * @param objectGraph The Object Graph. + * @returns A style, or undefined if it's unsupported. + */ +export function todayAdStyle(objectGraph) { + if (objectGraph.bag.todayAdMediumLockupScreenshotEnabled) { + return "mediumLockup"; + } + if (objectGraph.bag.todayAdCondensedEnabled) { + // Only supported on iPhone. + if (!objectGraph.client.isPhone) { + return undefined; + } + return "singleLockup"; + } + if (shouldTodayAdBeCondensed(objectGraph)) { + return "singleLockup"; + } + else { + return undefined; + } +} +/** + * Get the timeout of the ad fetch for a given placement from the bag values. + * @param objectGraph The object graph. + * @param placementType The ad placement. + * @param isDeltaTimeout Whether the timeout we're looking for is a delta timeout between parallel organic and ad requests completing. + * See `Ads.setTimeoutForCurrentOnDeviceAdFetch` for more details on how this is used. + * @returns The timeout. + */ +export function adFetchTimeoutForPlacement(objectGraph, placementType, isDeltaTimeout) { + var _a, _b, _c, _d, _e; + const timeouts = objectGraph.bag.adPlacementTimeouts; + const defaultTimeout = 0.3; + switch (placementType) { + case "searchResults": + return isDeltaTimeout ? null : (_a = timeouts === null || timeouts === void 0 ? void 0 : timeouts["search-results-in-seconds"]) !== null && _a !== void 0 ? _a : defaultTimeout; + case "searchLanding": + return isDeltaTimeout ? null : (_b = objectGraph.bag.searchLandingAdFetchTimeout) !== null && _b !== void 0 ? _b : defaultTimeout; // Legacy value. + case "today": + // Today uses its timeout value as a delta after the organic request finishes. + return isDeltaTimeout ? (_c = timeouts === null || timeouts === void 0 ? void 0 : timeouts["today-in-seconds"]) !== null && _c !== void 0 ? _c : defaultTimeout : null; + case "productPageYMAL": + return isDeltaTimeout ? null : (_d = timeouts === null || timeouts === void 0 ? void 0 : timeouts["product-page-ymal-in-seconds"]) !== null && _d !== void 0 ? _d : defaultTimeout; + case "productPageYMALDuringDownload": + return isDeltaTimeout ? null : (_e = timeouts === null || timeouts === void 0 ? void 0 : timeouts["product-page-ymal-during-download-in-seconds"]) !== null && _e !== void 0 ? _e : defaultTimeout; + default: + return defaultTimeout; + } +} +/** + * Convert the value describing an ad placement from the App Store representation to the bag representation. + * @param placementType The bag value for different ad placements + * @returns The equivalent bag placement value as a string. + */ +function adPlacementBagValueForAdPlacementType(placementType) { + switch (placementType) { + case "searchResults": + return "search-results"; + case "searchLanding": + return "search-landing"; + case "today": + return "today"; + case "productPageYMAL": + return "product-page-ymal"; + case "productPageYMALDuringDownload": + return "product-page-ymal-during-download"; + default: + return undefined; + } +} +/** + * The minimum number of landscape media items for a Today ad placement. + * The app must satisfy this or `todayAdPlacementMinimumPortraitMedia` to be shown. + */ +export const todayAdPlacementMinimumLandscapeMedia = 5; +/** + * The minimum portrait media for a Today ad placement. + * The app must satisfy this or `todayAdPlacementMinimumLandscapeMedia` to be shown. + */ +export const todayAdPlacementMinimumPortraitMedia = 4; +/** + * Get a count of the media in each orientation specifically for a Today ad placement. + * This is specific to Today because the placement only supports one video. + * @param trailers A set of trailers for an app. + * @param screenshots A set of screenshots for an app. + * @returns A count of media in each orientation, respecting the placement's rules. + */ +export function getMediaOrientationCountsForTodayPlacement(trailers, screenshots) { + var _a, _b, _c, _d; + // Grab the first array of platform screenshots and trailers - the ones the ad will display. + const platformScreenshotsArtwork = (_b = (_a = screenshots[0]) === null || _a === void 0 ? void 0 : _a.artwork) !== null && _b !== void 0 ? _b : []; + const platformTrailersVideos = (_d = (_c = trailers[0]) === null || _c === void 0 ? void 0 : _c.videos) !== null && _d !== void 0 ? _d : []; + // Split media into orientations. + const portraitScreenshots = platformScreenshotsArtwork.filter((artwork) => artwork.isPortrait()); + const landscapeScreenshots = platformScreenshotsArtwork.filter((artwork) => artwork.isLandscape()); + // Take a maximum of one video of each orientation. + const portraitVideos = platformTrailersVideos.filter((video) => video.preview.isPortrait()).slice(0, 1); + const landscapeVideos = platformTrailersVideos.filter((video) => video.preview.isLandscape()).slice(0, 1); + const portraitCount = portraitScreenshots.length + portraitVideos.length; + const landscapeCount = landscapeScreenshots.length + landscapeVideos.length; + return { + landscape: landscapeCount, + portrait: portraitCount, + }; +} +/** + * A convenience method to determine if a position is ad eligible. + * @param placementType The type of placement for an ad. + * @param locationTracker The location tracker being used to build the items. Used to determine the current shelf and position within the shelf. + * @returns A boolean indicating if the current position is considered ad eligible. + */ +export function isAdEligible(placementType, locationTracker) { + var _a; + if (isNothing(locationTracker) || isNothing(placementType)) { + return false; + } + const shelfId = (_a = currentLocation(locationTracker)) === null || _a === void 0 ? void 0 : _a.id; + if (isNothing(shelfId)) { + return false; + } + const eligibleIndex = adEligibleIndexForType(placementType, shelfId); + if (isNothing(eligibleIndex)) { + return false; + } + const index = currentPosition(locationTracker); + return index === eligibleIndex; +} +/** + * Inserts a missed opportunity on the correct slot in the page if an ad isn't already present. This looks for + * types conforming to SearchAdOpportunityProviding and handles all of the construction for the metadata associated + * with a SearchAdOpportunity. + * @param objectGraph The object graph. + * @param shelves Completed shelves that are constructed for the page. + * @param placementType The location where the ad will be shown. + * @param shelfIdentifier The specific shelf identifier we should be evaluating against. This is not inferred to support things like Today Page that can use a shelf-per-item. + * @param pageInformation Metrics information for the page. + */ +export function applySearchAdMissedOpportunityToShelvesIfNeeded(objectGraph, shelves, placementType, shelfIdentifier, pageInformation) { + var _a, _b, _c; + if (!platformSupportsAdverts(objectGraph) || isNull(pageInformation.iAdInfo)) { + return; + } + const adEligibleIndex = adEligibleIndexForType(placementType, shelfIdentifier); + if (isNothing(adEligibleIndex)) { + return; + } + // Opportunities aren't present when we've recorded certain missed opportunities + let missedOpportunityReason = null; + if (typeof pageInformation.iAdInfo.pageFields.iAdMissedOpportunityReason === "string") { + missedOpportunityReason = pageInformation.iAdInfo.pageFields.iAdMissedOpportunityReason; + } + if (isNothing(missedOpportunityReason) || + missedOpportunityReason.length === 0 || + missedOpportunityReason === "EDITORIALTAKEOVER" || + missedOpportunityReason === "SLPLOAD") { + return; + } + const allShelfItems = []; + for (const shelf of shelves) { + // searchResult is handled in a separate non-shelf workflow + const isValidContentType = shelf.contentType === "smallLockup" || shelf.contentType === "todayCard"; + if (!isValidContentType) { + continue; + } + const nextShelfItems = shelf.items; + if (isDefinedNonNull(nextShelfItems) && nextShelfItems.length > 0) { + allShelfItems.push(...nextShelfItems); + } + } + if (allShelfItems.length <= adEligibleIndex) { + return; + } + const adEligibleShelfItem = allShelfItems[adEligibleIndex]; + const isTodayCard = adEligibleShelfItem instanceof TodayCard; + const isSmallLockup = adEligibleShelfItem instanceof Lockup; + const shelfItemMedia = isTodayCard ? adEligibleShelfItem.media : null; + const lockupTypeIsMediumScreenshotFormat = isDefinedNonNull(shelfItemMedia) && shelfItemMedia instanceof TodayCardMediaMediumLockupWithScreenshots; + const lockupTypeIsMediumCreativeFormat = isDefinedNonNull(shelfItemMedia) && shelfItemMedia instanceof TodayCardMediaMediumLockupWithAlignedRegion; + const lockupHasPlacedCondensedTodayAd = isDefinedNonNull(shelfItemMedia) && + shelfItemMedia instanceof TodayCardMediaSingleLockup && + isDefinedNonNull(shelfItemMedia.condensedAdLockupWithIconBackground.lockup.searchAdOpportunity); + const lockupHasPlacedMediumAdScreenshots = lockupTypeIsMediumScreenshotFormat && + isDefinedNonNull(shelfItemMedia.mediumAdLockupWithScreenshotsBackground.lockup.searchAdOpportunity); + const lockupHasPlacedMediumAdCreative = lockupTypeIsMediumCreativeFormat && + isDefinedNonNull(shelfItemMedia.mediumAdLockupWithAlignedRegionBackground.lockup.searchAdOpportunity); + const lockupHasPlacedSmallLockup = isSmallLockup && isDefinedNonNull(adEligibleShelfItem.searchAdOpportunity); + // If we've already processed the ad data and placed it, we don't want to indicate that there's a missed opportunity + if (lockupHasPlacedCondensedTodayAd || + lockupHasPlacedMediumAdScreenshots || + lockupHasPlacedMediumAdCreative || + lockupHasPlacedSmallLockup) { + return; + } + adEligibleShelfItem.searchAdOpportunity = searchAdMissedOpportunityFromId(objectGraph, pageInformation); + (_a = adEligibleShelfItem.searchAdOpportunity) === null || _a === void 0 ? void 0 : _a.setMissedOpportunityReason(missedOpportunityReason !== null && missedOpportunityReason !== void 0 ? missedOpportunityReason : "NOAD"); + if (lockupTypeIsMediumScreenshotFormat) { + (_b = adEligibleShelfItem.searchAdOpportunity) === null || _b === void 0 ? void 0 : _b.setTemplateType(getTemplateTypeForMediumAdFromLockupWithScreenshots(shelfItemMedia.mediumAdLockupWithScreenshotsBackground.screenshots[0])); + } + else if (lockupTypeIsMediumCreativeFormat) { + adEligibleShelfItem.searchAdOpportunity.setTemplateType(getTemplateTypeForMediumAdFromLockupWithCustomCreative()); + } + else { + (_c = adEligibleShelfItem.searchAdOpportunity) === null || _c === void 0 ? void 0 : _c.setTemplateType("APPLOCKUP"); + } +} +/** + * This is a bit of a workaround for `isAdEligible` function in this file. Ideally this is bag-driven, but I need to figure out + * how to make it work. `shelfIdentifier` doesn't seem to work correctly, except for the YMAL ad on PP + * @param placementType The ad placement type to evaluate for ad eligibility + * @param shelfIdentifier The identifier for the shelf to evaluate for ad eligibility + * @returns The equivalent bag placement value as a string. + */ +function adEligibleIndexForType(placementType, shelfIdentifier) { + var _a; + const adEligibleBagRepresentation = { + today: [ + { + shelfIdentifier: "today", + adEligibleIndex: 1, + }, + ], + productPageYMAL: [ + { + shelfIdentifier: "customers-also-bought-apps", + adEligibleIndex: 0, + }, + ], + searchLanding: [ + { + shelfIdentifier: "R8802", + adEligibleIndex: 0, + }, + ], + searchResults: [ + { + shelfIdentifier: "search-results", + adEligibleIndex: 0, + }, + ], + }; + const matchingSlot = ((_a = adEligibleBagRepresentation[placementType]) !== null && _a !== void 0 ? _a : []).find((element) => { + return element.shelfIdentifier === shelfIdentifier; + }); + if (isDefinedNonNullNonEmpty(matchingSlot) && isDefinedNonNull(matchingSlot.adEligibleIndex)) { + return matchingSlot.adEligibleIndex; + } + else { + return undefined; + } +} +// endregion +// region iad data +export function iadInfoFromOnDeviceAdResponse(objectGraph, placementType, adResponse, flattenedTodayFeed = null) { + var _a, _b; + if (!platformSupportsAdverts(objectGraph) || isNull(adResponse)) { + return null; + } + return new IAdSearchInformation(objectGraph, placementType, IAdSearchInformation.createInitialSlotInfos(objectGraph, placementType, (_a = adResponse === null || adResponse === void 0 ? void 0 : adResponse.onDeviceAd) === null || _a === void 0 ? void 0 : _a.positionInfo, flattenedTodayFeed), adResponse.iAdId, adResponse.clientRequestId, undefined, (_b = adResponse.onDeviceAd) === null || _b === void 0 ? void 0 : _b.positionInfo); +} +/** + * Key added to `Data`'s `attributes` field so downstream builders can use this field. + * This is currently generated by JS when we create `SponsoredSearchAdverts` + */ +export const instanceIdAttributeKey = "jet_native_advert_instanceid"; +/** + * Returns the native advert id if one was annotated during `applyNativeAdvertData` + */ +export function advertInstanceIdForData(objectGraph, data) { + return attributeAsString(data, instanceIdAttributeKey); +} +/** + * Decorates `instanceId` that identifies an ad to given `Data` for consumption in builders. + */ +export function decorateAdInstanceIdOnData(objectGraph, data, instanceId) { + if (isDefinedNonNullNonEmpty(data === null || data === void 0 ? void 0 : data.attributes)) { + data.attributes[instanceIdAttributeKey] = instanceId; + } +} +// endregion +// region ad localization +/** + * Whether the available localization data in an ad is valid for the defined `adsOverrideLanguage`, if any. + * If no `adsOverrideLanguage` is set, `true` is returned. + * The logic for a valid set of data differs based on the passed in placement and if an ad display style + * is defined (for SRP only). + * @param objectGraph The Object Graph. + * @param data The app data. + * @param onDeviceAd Optionally, an `OnDeviceAdvert` - this is only used as a fallback for SLP ads, + * where the meta resource object is attached to the original ad request, but not to the subsequent + * hydration request. For this case only, we must use a combination of these two requests to identify + * whether the localization is valid. + * @param adDisplayStyle: Optionally, an ad display style - this is used for SRP ads, where the data used + * in the ad UI depends on which ad display style is being used. + * @returns A boolean indicating if the available localization data is valid. + */ +export function isAdLocalizationValid(objectGraph, data, onDeviceAd, adDisplayStyle) { + var _a, _b, _c, _d, _e; + const adsOverrideLanguage = objectGraph.bag.adsOverrideLanguage; + // No localization requirements if there is no adsOverrideLanguage. + if (isNullOrEmpty(adsOverrideLanguage) || isNullOrEmpty(data)) { + return true; + } + let metaResource = (_a = data.meta) === null || _a === void 0 ? void 0 : _a.resource; + // If the `metaResource in `data` is missing, attempt to fall back to the `onDeviceAd` values + // which are provided for SLP. + if (isNullOrEmpty(metaResource) && isDefinedNonNullNonEmpty(onDeviceAd)) { + metaResource = (_e = (_d = (_c = (_b = onDeviceAd === null || onDeviceAd === void 0 ? void 0 : onDeviceAd.appMetadata) === null || _b === void 0 ? void 0 : _b.data) === null || _c === void 0 ? void 0 : _c[0]) === null || _d === void 0 ? void 0 : _d.meta) === null || _e === void 0 ? void 0 : _e.resource; + } + // If `metaResource` still don't exist, we can't validate localization. + if (isNullOrEmpty(metaResource)) { + // The meta resource attributes object is missing, assume we don't have the correct localizations. + return false; + } + // Title + // Title is always used. Check it's localized. + const titleLocale = attributeAsString(metaResource, "name.locale"); + if (titleLocale !== adsOverrideLanguage) { + return false; + } + // Subtitle + // Subtitle is constructed using either the subtitle field, or falling back to the category. + // If the subtitle text is present, we need to validate the locale. If not, we don't. + const subtitle = contentAttributeAsString(objectGraph, data, "subtitle"); + const subtitleLocale = contentAttributeAsString(objectGraph, metaResource, "subtitle.locale"); + if (isDefinedNonNullNonEmpty(subtitle) && subtitleLocale !== adsOverrideLanguage) { + return false; + } + // SRP advertising text + // SRP ads can optionally provide a 3rd set of text, `advertisingText`. + // It's only used when the `adDisplayStyle` is `TEXT`. + if (adDisplayStyle === "TEXT" /* SearchAdDisplayStyle.TEXT */) { + // The text used can be defined by the ad payload inside the `data`. We need to use that path + // to get both the text and the locale for that text. + const iAdTextKey = attributeAsString(data, "iad.format.text"); + if (isSome(iAdTextKey) && iAdTextKey !== "none") { + let attributePath; + const attributePathLocale = iAdTextKey; + // In the case where the path is "description", we need to use different paths for the text and the locale. + // This is due to a Media API limitation where the locale for "description" is just under that key, rather + // than "description.standard", which is the actual path for the text content. + if (iAdTextKey === "description") { + attributePath = "description.standard"; + } + else { + attributePath = iAdTextKey; + } + const advertisingText = contentAttributeAsString(objectGraph, data, attributePath); + const advertisingTextLocale = contentAttributeAsString(objectGraph, metaResource, attributePathLocale.concat(".locale")); + if (isDefinedNonNullNonEmpty(advertisingText) && advertisingTextLocale !== adsOverrideLanguage) { + return false; + } + } + } + return true; +} +// endregion +// region metrics +/** + * Retrieve the eligible slot positions for a given ad placement type. + * @param objectGraph The Object Graph. + * @param placementType The placement the ad is for. + * @returns An array of eligible slot positions for the provided placement. + */ +export function eligibleSlotPositionsForAdPlacement(objectGraph, placementType) { + if (isNothing(placementType)) { + return undefined; + } + const placementTypeBagValue = adPlacementBagValueForAdPlacementType(placementType); + if (isNothing(placementTypeBagValue)) { + return undefined; + } + return objectGraph.bag.adPlacementEligibleSlotPositions[placementTypeBagValue]; +} +// endregion +//# sourceMappingURL=ad-common.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/ads/ad-incident-recorder.js b/node_modules/@jet-app/app-store/tmp/src/common/ads/ad-incident-recorder.js new file mode 100644 index 0000000..6dac34a --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/ads/ad-incident-recorder.js @@ -0,0 +1,116 @@ +/** + * Ad Incident Recorder manages the Journey and Figaro events when rendering a page. + * This exists so managing journey and figaro metrics is a bit more sane. + */ +"use strict"; +import { isNothing } from "@jet/environment"; +import { DiscardAdIncident } from "../../api/models"; +import { isDefinedNonNull, isNull, isNullOrEmpty } from "../../foundation/json-parsing/server-data"; +import { advertInstanceIdForData } from "./ad-common"; +/** + * Create a new ad incident recorder + */ +export function newRecorder(objectGraph, iAdInfo) { + return { + incidents: [], + iAdInfo: iAdInfo, + }; +} +/** + * Get recorded incidents from recorder + */ +export function recordedIncidents(objectGraph, recorder) { + if (isNull(recorder) || isNullOrEmpty(recorder.incidents)) { + return null; + } + return recorder.incidents; +} +/** + * Record that building a lockup from data failed. + */ +export function recordLockupFromDataFailed(objectGraph, recorder, lockupData) { + var _a, _b; + const instanceId = advertInstanceIdForData(objectGraph, lockupData); + if (isNothing(instanceId)) { + return; + } + const incident = objectGraph.props.enabled("advertSlotReporting") + ? null + : new DiscardAdIncident(instanceId, "advertDataMalformed"); + addIncident(recorder, incident); + (_a = recorder === null || recorder === void 0 ? void 0 : recorder.iAdInfo) === null || _a === void 0 ? void 0 : _a.setMissedOpportunity(objectGraph, "METADATA", (_b = recorder === null || recorder === void 0 ? void 0 : recorder.iAdInfo) === null || _b === void 0 ? void 0 : _b.placementType); +} +/** + * Record events that may occur when we try to fetch an on-device ad + data + */ +export function recordAdResponseEventsIfNeeded(objectGraph, recorder, response) { + var _a, _b, _c, _d, _e, _f, _g, _h, _j; + if (isNullOrEmpty(response === null || response === void 0 ? void 0 : response.failureReason)) { + return; // No failure to report + } + const instanceId = (_a = response === null || response === void 0 ? void 0 : response.onDeviceAd) === null || _a === void 0 ? void 0 : _a.instanceId; + const placementType = (_b = recorder === null || recorder === void 0 ? void 0 : recorder.iAdInfo) === null || _b === void 0 ? void 0 : _b.placementType; + switch (response === null || response === void 0 ? void 0 : response.failureReason) { + case "mapiFetchFail": + if (isDefinedNonNull(instanceId)) { + const incident = objectGraph.props.enabled("advertSlotReporting") + ? null + : new DiscardAdIncident(instanceId, "advertDataMalformed"); + addIncident(recorder, incident); + } + (_c = recorder === null || recorder === void 0 ? void 0 : recorder.iAdInfo) === null || _c === void 0 ? void 0 : _c.setMissedOpportunity(objectGraph, "METADATA", placementType); + break; + case "cppAssetsMissing": + if (isDefinedNonNull(instanceId)) { + const incident = objectGraph.props.enabled("advertSlotReporting") + ? null + : new DiscardAdIncident(instanceId, "cppAssetsMissing"); + addIncident(recorder, incident); + } + (_d = recorder === null || recorder === void 0 ? void 0 : recorder.iAdInfo) === null || _d === void 0 ? void 0 : _d.setMissedOpportunity(objectGraph, "METADATA", placementType); + break; + case "insufficientAssets": + if (isDefinedNonNull(instanceId)) { + const incident = objectGraph.props.enabled("advertSlotReporting") + ? null + : new DiscardAdIncident(instanceId, "insufficientAssets"); + addIncident(recorder, incident); + } + (_e = recorder === null || recorder === void 0 ? void 0 : recorder.iAdInfo) === null || _e === void 0 ? void 0 : _e.setMissedOpportunity(objectGraph, "METADATA", placementType); + break; + case "noAdAvailable": + // no journey metric + (_f = recorder === null || recorder === void 0 ? void 0 : recorder.iAdInfo) === null || _f === void 0 ? void 0 : _f.setMissedOpportunity(objectGraph, "NOAD", placementType); + break; + case "timeout": + // no journey metric + (_g = recorder === null || recorder === void 0 ? void 0 : recorder.iAdInfo) === null || _g === void 0 ? void 0 : _g.setMissedOpportunity(objectGraph, "APSLA", placementType); + break; + case "localizationNotAvailable": + if (isDefinedNonNull(instanceId)) { + const incident = objectGraph.props.enabled("advertSlotReporting") + ? null + : new DiscardAdIncident(instanceId, "localizationNotAvailable"); + addIncident(recorder, incident); + } + (_h = recorder === null || recorder === void 0 ? void 0 : recorder.iAdInfo) === null || _h === void 0 ? void 0 : _h.setMissedOpportunity(objectGraph, "NOLOC", placementType); + break; + case "policyAdDrop": + // no journey metric + (_j = recorder === null || recorder === void 0 ? void 0 : recorder.iAdInfo) === null || _j === void 0 ? void 0 : _j.setMissedOpportunity(objectGraph, "ODP_NOAD", placementType); + break; + default: + break; + } +} +// region Internals +/** + * Add incident on `AdIncidentRecorder` + */ +function addIncident(recorder, incident) { + if (isNull(recorder) || isNull(incident)) { + return; + } + recorder.incidents.push(incident); +} +//# sourceMappingURL=ad-incident-recorder.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/ads/ad-stitcher.js b/node_modules/@jet-app/app-store/tmp/src/common/ads/ad-stitcher.js new file mode 100644 index 0000000..c9fc6b9 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/ads/ad-stitcher.js @@ -0,0 +1,73 @@ +/** + * Handles stitching ads into pages. + * + * At a high level: + * 1. Setup `AdStitcher` w/ content to stitch. + * 2. Pass to builder + * 3. Destructively consume tasks during building. + */ +"use strict"; +import { isNull } from "../../foundation/json-parsing/server-data"; +/** + * Creates a string identifier representing the position information for an ad stitch task. + * @param positionInfo The position information object to create an identifier for. + * @returns The string identifier. + */ +function stringIdentifierForPositionInfo(positionInfo) { + return `${positionInfo.shelfIdentifier}.${positionInfo.slot}`; +} +// region exports +/** + * Create a new ad stitcher + */ +export function newAdStitcher() { + return { + tasks: {}, + }; +} +/// Add task +export function addTask(stitcher, task) { + if (isNull(stitcher)) { + return; + } + const positionIdentifier = stringIdentifierForPositionInfo(task.positionInfo); + stitcher.tasks[positionIdentifier] = task; +} +/** + * Consume a single task, if available, for a given position information. + * @param stitcher The relevant ad stitcher. + * @param positionInfo The position information to check for an available task. + * @returns A task for the position, or null if unavailable. + */ +export function consumeTask(stitcher, positionInfo) { + if (isNull(stitcher)) { + return null; + } + const positionIdentifier = stringIdentifierForPositionInfo(positionInfo); + const task = stitcher.tasks[positionIdentifier]; + if (isNull(task)) { + return null; + } + delete stitcher.tasks[positionIdentifier]; + return task; +} +/** + * Consume all tasks for a given shelf identifier. Useful where ad data is being stitched in prior to view models being built. + * @param stitcher The relevant ad stitcher. + * @param shelfIdentifier The identifier for the shelf to get all available tasks for. + * @returns An array of relevant tasks for the shelf. + */ +export function consumeTasksForShelfIdentifier(stitcher, shelfIdentifier) { + if (isNull(stitcher)) { + return []; + } + const tasksForShelf = []; + Object.entries(stitcher.tasks).forEach(([key, value]) => { + if (key.startsWith(shelfIdentifier)) { + tasksForShelf.push(value); + delete stitcher.tasks[key]; + } + }); + return tasksForShelf; +} +//# sourceMappingURL=ad-stitcher.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/ads/on-device-ad-fetch.js b/node_modules/@jet-app/app-store/tmp/src/common/ads/on-device-ad-fetch.js new file mode 100644 index 0000000..04f3e01 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/ads/on-device-ad-fetch.js @@ -0,0 +1,286 @@ +/** + * Handles fetching Ads for all on-device ad placements + * This is currently SLP, Today, Product Page. + * + * # Ad fetching for on-device placements + * On-Device placements are unpersonalized and rely on CDN caching. To show ads, we: + * 1. Fetch from an on-device cache of ads (managed by PromotedContent framework) for the specific placement + * 2. Fetching ad data from MAPI + * 2. Stitch ad onto page data... + */ +import * as serverData from "../../foundation/json-parsing/server-data"; +import { Request } from "../../foundation/media/data-fetching"; +import { dataFromDataContainer } from "../../foundation/media/data-structure"; +import { fetchData } from "../../foundation/media/network"; +import { buildURLFromRequest } from "../../foundation/media/url-builder"; +import { Parameters } from "../../foundation/network/url-constants"; +import { offerDataFromData } from "../offers/offers"; +import { productVariantDataForData, shouldFetchCustomAttributes } from "../product-page/product-page-variants"; +import { todayTabODPTimeoutUseCase } from "../personalization/on-device-recommendations-today"; +import { setTimeoutForRequestKey } from "../util/timeout-manager-util"; +import { adLogger } from "../search/search-ads"; +import * as adCommon from "./ad-common"; +import { getSelectedCustomCreativeId } from "../search/custom-creative"; +import { isSome } from "@jet/environment"; +// region exports +/** + * Fetch ads for the given placement type leveraging on-device ads cache. + * @param objectGraph the object graph. + * @param placementType the placement type to fetch the ad for. + * @param adamId the adamId of the app for which the product page is being viewed, to provide a relevant ad. Only required for product page placements. + * @returns a promise containing an ad. + */ +export async function fetchAds(objectGraph, placementType, adamId) { + const timeout = adCommon.adFetchTimeoutForPlacement(objectGraph, placementType, false); + const request = new Request(objectGraph); + switch (placementType) { + case "today": + request.usingCustomAttributes(shouldFetchCustomAttributes(objectGraph)); + switch (adCommon.todayAdStyle(objectGraph)) { + case "mediumLockup": + if (preprocessor.CARRY_BUILD || preprocessor.DEBUG_BUILD) { + if (objectGraph.featureFlags.isEnabled("aligned_region_artwork_2025A")) { + request.includingAttributes(["customScreenshotsByTypeForAd", "adCreativeArtwork"]); + } + else { + request.includingAttributes(["customScreenshotsByTypeForAd"]); + } + } + else { + request.includingAttributes(["customScreenshotsByTypeForAd"]); + } + break; + default: + break; + } + break; + case "productPageYMAL": + case "productPageYMALDuringDownload": + request.usingCustomAttributes(shouldFetchCustomAttributes(objectGraph)); + break; + default: + break; + } + /** + * Ad not available content filtering + */ + const adsOverrideLanguage = objectGraph.bag.adsOverrideLanguage; + if (serverData.isDefinedNonNullNonEmpty(adsOverrideLanguage)) { + request.enablingFeature("adsLocaleMetadata").addingQuery("l", adsOverrideLanguage); + } + const requestMetaFields = buildURLFromRequest(objectGraph, request).query; + try { + const onDeviceResponse = await objectGraph.ads.fetchOnDeviceAdPlacement(placementType, timeout, requestMetaFields, adamId); + return await handleAdResponse(objectGraph, onDeviceResponse, placementType); + } + catch { + return null; + } +} +/** + * Handle the response from an on device ad request. + * @param objectGraph The App Store Object Graph. + * @param onDeviceResponse The response from the on device ad fetcher. + * @param placementType The placement this request was for. + * @returns A promise containing an ad. + */ +async function handleAdResponse(objectGraph, onDeviceResponse, placementType) { + var _a, _b, _c, _d, _e, _f; + if (serverData.isNullOrEmpty(onDeviceResponse.clientRequestId)) { + onDeviceResponse.clientRequestId = objectGraph.random.nextUUID(); + adLogger(objectGraph, `clientRequestId was nil. Assigned ${onDeviceResponse.clientRequestId}`); + } + const aggregateResponse = { + clientRequestId: onDeviceResponse.clientRequestId, + iAdId: onDeviceResponse.iAdId, + placementType: (_b = (_a = onDeviceResponse === null || onDeviceResponse === void 0 ? void 0 : onDeviceResponse.ad) === null || _a === void 0 ? void 0 : _a.placementType) !== null && _b !== void 0 ? _b : placementType, + }; + // Failed w/o Ad from device. + if (onDeviceResponse.failureReason) { + aggregateResponse.failureReason = onDeviceResponse.failureReason; + return aggregateResponse; + } + // Set the basic ad info received on the response. + aggregateResponse.onDeviceAd = onDeviceResponse.ad; + // Ad requests should return with at least some basic app metadata. + // Note: On pre-SydneyC builds, this is expected to be null for the SLP placement. + let mediaResponse = (_c = onDeviceResponse.ad) === null || _c === void 0 ? void 0 : _c.appMetadata; + // Get the currently available app data from the data container. + const appData = dataFromDataContainer(objectGraph, mediaResponse); + // We should only attempt to fetch the full app data if the app data provided to us via + // Promoted Content is incomplete. This should only be the case for SLP ads - Chainlink + // placements should arrive with complete app data. + // We check a couple of attributes here as a way to be sure it's hydrated. Sometimes + // a single attribute can be misleading. + if (serverData.isNullOrEmpty((_d = appData === null || appData === void 0 ? void 0 : appData.attributes) === null || _d === void 0 ? void 0 : _d.name) || + serverData.isNullOrEmpty((_e = appData === null || appData === void 0 ? void 0 : appData.attributes) === null || _e === void 0 ? void 0 : _e.platformAttributes) || + serverData.isNullOrEmpty(offerDataFromData(objectGraph, appData))) { + try { + const adRequest = createRequestForOnDeviceAd(objectGraph, onDeviceResponse.ad); + mediaResponse = await fetchData(objectGraph, adRequest); + } + catch (e) { + adLogger(objectGraph, `fetchAds request failed - ${e}`); + aggregateResponse.failureReason = "mapiFetchFail"; + } + } + // The app data should now be complete, set it on the response. + if (serverData.isDefinedNonNullNonEmpty((_f = dataFromDataContainer(objectGraph, mediaResponse)) === null || _f === void 0 ? void 0 : _f.attributes)) { + aggregateResponse.mediaResponse = decorateiAdAttributeFromOnDeviceAdResponse(objectGraph, mediaResponse, onDeviceResponse); + // Check the localization is valid for the ad. + if (!adCommon.isAdLocalizationValid(objectGraph, dataFromDataContainer(objectGraph, mediaResponse), aggregateResponse.onDeviceAd)) { + adLogger(objectGraph, `fetchAds request failed - localization not available`); + aggregateResponse.failureReason = "localizationNotAvailable"; + } + const metadataFailReason = checkAppMetadataIsValidForPlacement(objectGraph, aggregateResponse, placementType); + if (serverData.isDefinedNonNull(metadataFailReason)) { + adLogger(objectGraph, `fetchAds request failed - ${metadataFailReason}`); + aggregateResponse.failureReason = metadataFailReason; + } + } + return aggregateResponse; +} +/** + * Indicates that an organic request kicked off parallel to an ad fetch has completed. + * This gives us an opportunity to enforce a timeout on the ad request for the time beyond the organic request. + * @param objectGraph The object graph. + * @param placementType The placement for which the parallel organic request finished. + */ +export function parallelOrganicRequestDidFinish(objectGraph, placementType) { + const timeout = adCommon.adFetchTimeoutForPlacement(objectGraph, placementType, true); + if (serverData.isNull(timeout)) { + return; + } + objectGraph.ads.setTimeoutForCurrentOnDeviceAdFetch(placementType, timeout); + setTimeoutForRequestKey(objectGraph, timeout, todayTabODPTimeoutUseCase); +} +// endregion +// region internals +/** + * Create an request for on-device adverts + * @param ad The on device ad to fetch MAPI data for. + */ +function createRequestForOnDeviceAd(objectGraph, ad) { + const request = new Request(objectGraph) + .withIdOfType(ad.adamId, "apps") + .usingCustomAttributes(shouldFetchCustomAttributes(objectGraph)) + .includingAttributes(["customScreenshotsByTypeForAd"]); + if (serverData.isDefinedNonNullNonEmpty(ad.cppIds)) { + request.addingQuery(Parameters.productVariantID, ad.cppIds[0]); + } + // If there is an `adsOverrideLanguage`, attach it to this request too. + const adsOverrideLanguage = objectGraph.bag.adsOverrideLanguage; + if (serverData.isDefinedNonNullNonEmpty(adsOverrideLanguage)) { + request.addingQuery("l", adsOverrideLanguage); + } + return request; +} +/** + * Decorate `iad` attribute with contents of `OnDeviceAdvert`. + * + * @param mediaResponse The data from of MAPI request + * @param ad Ad data that was fetched independent of response. + */ +function decorateiAdAttributeFromOnDeviceAdResponse(objectGraph, mediaResponse, adResponse) { + const adData = dataFromDataContainer(objectGraph, mediaResponse); + if (serverData.isNullOrEmpty(adData) || serverData.isNull(adData.attributes)) { + adLogger(objectGraph, "decorateiAdAttributeFromOnDeviceAd cannot decorate for malformed response"); + return null; // The data is incompatible with `iad` decoration. Return `null` to let builder report error. + } + const onDeviceAd = adResponse.ad; + const lineItem = `${onDeviceAd.adamId}|${onDeviceAd.metadata}`; + // Create `IAdAttributes` and stitch onto `mediaResponse` + const iadAttributes = { + clientRequestId: adResponse.clientRequestId, + impressionId: onDeviceAd.impressionId, + metadata: onDeviceAd.metadata, + privacy: onDeviceAd.privacy, + lineItem: lineItem, + }; + const metaContainer = dataFromDataContainer(objectGraph, onDeviceAd.appMetadata); + if (serverData.isDefinedNonNullNonEmpty(adData.meta) && + serverData.isDefinedNonNullNonEmpty(metaContainer) && + serverData.isDefinedNonNullNonEmpty(metaContainer.meta)) { + adData.meta.passthroughAdInfo = metaContainer.meta.passthroughAdInfo; + if (isSome(onDeviceAd === null || onDeviceAd === void 0 ? void 0 : onDeviceAd.alignedRegionDetails)) { + adData.meta.alignedRegionDetails = onDeviceAd === null || onDeviceAd === void 0 ? void 0 : onDeviceAd.alignedRegionDetails[0]; + } + } + switch (onDeviceAd.placementType) { + case "today": + // Enable images for specific placements. + const imagesEnabled = adCommon.todayAdStyle(objectGraph) === "mediumLockup"; + iadAttributes.format = { + images: imagesEnabled, + text: "", + userRating: false, + }; + break; + case "searchLanding": + iadAttributes.format = { + images: true, + text: "", + userRating: false, + }; + break; + default: + break; + } + adData.attributes["iad"] = iadAttributes; + adCommon.decorateAdInstanceIdOnData(objectGraph, adData, onDeviceAd.instanceId); + return mediaResponse; +} +/** + * Checks whether the metadata returned for the app is considered valid for the given placement. + * Some ad placements have special rules to be shown. These rules are validated here. + * @param objectGraph The App Store object graph. + * @param adResponse The ad response. + * @param placementType The placement the given ad data is to be placed in. + * @returns A fail reason if there is one. Otherwise null. + */ +function checkAppMetadataIsValidForPlacement(objectGraph, adResponse, placementType) { + switch (placementType) { + case "today": + return checkAppMetadataIsValidForToday(objectGraph, adResponse); + default: + return null; + } +} +/** + * Checks the metadata returned for the app is considered valid **specifically** for the Today placement. + * + * Today ad placements must: + * - Include a valid cppId in the `meta` field of the data that matches the cppId in the ad result, and + * - Have enough assets to fulfill the template requirement: + * - Portrait: at least 4 assets, one of which can be a video. + * - Landscape: at least 5 assets, one of which can be a video. + * + * @param objectGraph The App Store object graph. + * @param adResponse The ad response. + * @returns A fail reason if there is one. Otherwise null. + */ +function checkAppMetadataIsValidForToday(objectGraph, adResponse) { + var _a, _b, _c; + const adData = dataFromDataContainer(objectGraph, adResponse.mediaResponse); + const productVariantData = productVariantDataForData(objectGraph, adData); + const hasCPP = (_b = (_a = adResponse.onDeviceAd) === null || _a === void 0 ? void 0 : _a.cppIds) === null || _b === void 0 ? void 0 : _b.includes(productVariantData.productPageId); + if (preprocessor.CARRY_BUILD || preprocessor.DEBUG_BUILD) { + const alignedRegionDetails = (_c = adResponse.onDeviceAd) === null || _c === void 0 ? void 0 : _c.alignedRegionDetails; + const creativeID = serverData.asString(alignedRegionDetails === null || alignedRegionDetails === void 0 ? void 0 : alignedRegionDetails[0], "apAssetId"); + const selectedCustomCreativeId = getSelectedCustomCreativeId(adData); + const hasCreative = creativeID === selectedCustomCreativeId; + // First check there is a cppId for the ad that matches the `meta`. + if (!hasCPP && !hasCreative) { + // Then check if there is appMetadata for the custom creative ad. + return "cppAssetsMissing"; + } + } + else { + if (!hasCPP) { + return "cppAssetsMissing"; + } + } + return null; +} +// endregion +//# sourceMappingURL=on-device-ad-fetch.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/ads/on-device-ad-stitch.js b/node_modules/@jet-app/app-store/tmp/src/common/ads/on-device-ad-stitch.js new file mode 100644 index 0000000..4cded1e --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/ads/on-device-ad-stitch.js @@ -0,0 +1,98 @@ +/** + * Performs ad stitching for Search Landing page using `AdStitcher` + */ +"use strict"; +import { isNothing, isSome } from "@jet/environment"; +import { dataFromDataContainer } from "../../foundation/media/data-structure"; +import * as adStitch from "./ad-stitcher"; +// region Constants +export const searchLandingPagePositionInfo = { + shelfIdentifier: "first", + slot: 0, +}; +/// shelf identifier for ad stitcher position info +export const searchLandingPageAdShelfIdentifier = "SLPPage"; +// region Setup +/** + * Get a positionInfo from a locationTracker and index. + * @param locationTracker A location tracker to indicate the current parsing position. + * @param index The index of the current position. + * @returns A constructed `AdStitcherPositionInfo`. + */ +export function todayPositionInfoForLocationTrackerAndIndex(locationTracker, index) { + return { + shelfIdentifier: locationTracker.rootPosition.toString(), + slot: index, + }; +} +/** + * Creates an Ad stitcher for on device adverts *specifically for the product page YMAL shelf* + * This is specific to this position as it hardcodes a shelf identifier. + * @param adResponse Ad response to configure stitcher with. + */ +export function adStitcherForOnDeviceProductPageYMALAdvertData(objectGraph, adResponse) { + var _a; + const rawPositionInfo = (_a = adResponse === null || adResponse === void 0 ? void 0 : adResponse.onDeviceAd) === null || _a === void 0 ? void 0 : _a.positionInfo; + if (isNothing(rawPositionInfo) || isNothing(adResponse)) { + return null; + } + let shelfIdentifier; + switch (adResponse.placementType) { + case "productPageYMAL": + shelfIdentifier = "customers-also-bought-apps"; + break; + case "productPageYMALDuringDownload": + shelfIdentifier = "customers-also-bought-apps-download"; + break; + default: + break; + } + if (isNothing(shelfIdentifier)) { + return null; + } + // The slot as provided by ad platforms is one-based - adjust it so we're working with zero-based numbers. + const adjustedRawSlot = rawPositionInfo.slot - 1; + const positionInfo = { + shelfIdentifier: shelfIdentifier, + slot: adjustedRawSlot, + }; + return adStitcherForOnDeviceAdvertDataAndPositionInfo(objectGraph, adResponse, positionInfo); +} +/** + * Creates an Ad stitcher for on device adverts *specifically for search landing page* + * This is specific to SLP because we have a hardcoded position. + * @param adResponse Ad response to configure stitchcher with. + * @param landingPageResponse Search page response to configure the positionInfo. + */ +export function adStitcherForOnDeviceSLPAdvertData(objectGraph, adResponse, landingPageResponse) { + var _a; + const adMeta = (landingPageResponse === null || landingPageResponse === void 0 ? void 0 : landingPageResponse.meta) || null; + const slot = (_a = adMeta === null || adMeta === void 0 ? void 0 : adMeta.adDisplayStyle) === null || _a === void 0 ? void 0 : _a.slot; + if (isSome(slot)) { + return adStitcherForOnDeviceAdvertDataAndPositionInfo(objectGraph, adResponse, { + shelfIdentifier: searchLandingPageAdShelfIdentifier, + slot: slot, + }); + } + else { + return adStitcherForOnDeviceAdvertDataAndPositionInfo(objectGraph, adResponse, searchLandingPagePositionInfo); + } +} +function adStitcherForOnDeviceAdvertDataAndPositionInfo(objectGraph, adResponse, positionInfo) { + const mediaResponse = adResponse === null || adResponse === void 0 ? void 0 : adResponse.mediaResponse; + if (isNothing(mediaResponse) || isSome(adResponse === null || adResponse === void 0 ? void 0 : adResponse.failureReason)) { + return null; + } + const stitcher = adStitch.newAdStitcher(); + /** + * Stitch ad data to first lockup + */ + const firstAdData = dataFromDataContainer(objectGraph, mediaResponse); + const firstLockupAdTask = { + data: firstAdData, + positionInfo: positionInfo, + }; + adStitch.addTask(stitcher, firstLockupAdTask); + return stitcher; +} +//# sourceMappingURL=on-device-ad-stitch.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/app-events/app-events-common.js b/node_modules/@jet-app/app-store/tmp/src/common/app-events/app-events-common.js new file mode 100644 index 0000000..cf5fd18 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/app-events/app-events-common.js @@ -0,0 +1,83 @@ +import { GenericPage, AppEventDetailShelf, Shelf, Banner } from "../../api/models"; +import { attributeAsString } from "../../foundation/media/attributes"; +import { defaultAdditionalPlatformsForClient, Request } from "../../foundation/media/data-fetching"; +import { dataFromDataContainer } from "../../foundation/media/data-structure"; +import { relationshipData } from "../../foundation/media/relationships"; +import { appEventOrPromotionStartDateFromData } from "../app-promotions/app-event"; +import { appEventDetailPageFromData } from "../app-promotions/app-event-detail"; +import { supportedAppPlatformsFromData } from "../content/content"; +import { newLocationTracker } from "../metrics/helpers/location"; +import { metricsPageInformationFromMediaApiResponse } from "../metrics/helpers/page"; +import { create as createBanner } from "../product-page/banner"; +/** + * Generates a {@linkcode Request} to the `app-events` Media API endpoint + */ +export function makeAppEventPageRequest(objectGraph, intent) { + const mediaApiRequest = new Request(objectGraph) + .withIdOfType(intent.id, "app-events") + .includingAdditionalPlatforms(defaultAdditionalPlatformsForClient(objectGraph)) + .includingRelationships(["app"]); + if (objectGraph.client.isWeb) { + mediaApiRequest.includingScopedAttributes("app-events", ["description", "productArtwork", "productVideo"]); + mediaApiRequest.includingScopedAvailableIn("app-events", ["past"]); + } + return mediaApiRequest; +} +/** + * Builds a `GenericPage` that represents an App Event detail page + * + * This is used by the "web" client to build the view-model for the standalone + * "app event" pages + */ +export function makeShelfBasedAppEventDetailPage(objectGraph, container) { + const data = dataFromDataContainer(objectGraph, container); + if (!data) { + return null; + } + const parentApp = relationshipData(objectGraph, data, "app"); + if (!parentApp) { + return null; + } + const metricsOptions = { + pageInformation: metricsPageInformationFromMediaApiResponse(objectGraph, "EventDetails", data.id, container), + locationTracker: newLocationTracker(), + }; + const appEvent = appEventOrPromotionStartDateFromData(objectGraph, data, parentApp, false, // `hideLockupWhenNotInstalled` + false, // `includeClickAction` + "light", // `offerEnvironment` + "infer", // `offerStyle` + false, // `includeCrossLinkStyles` + metricsOptions, true, // `allowEndedEvents` + true, // `includeLockupClickAction` + null, // `editorialKind` + false, // `isArcadePage` + false); + if (!appEvent || appEvent instanceof Date) { + return null; + } + const appEventDetailPage = appEventDetailPageFromData(objectGraph, data, parentApp, appEvent, metricsOptions, true, // `includeLockupClickAction` + null, // `referrerData` + false); + if (!appEventDetailPage) { + return null; + } + const shelves = []; + // Build the "banner" if necessary + const bannerContext = { + appPlatforms: supportedAppPlatformsFromData(objectGraph, data), + offerButtonShouldBeDisabled: true, + metricsPageInformation: metricsOptions.pageInformation, + metricsLocationTracker: metricsOptions.locationTracker, + webBrowser: false, + }; + const maybeBanner = createBanner(objectGraph, parentApp, bannerContext); + if (maybeBanner instanceof Banner) { + shelves.push(new Shelf("banner", null, [maybeBanner])); + } + const appEventDetailShelf = new AppEventDetailShelf(appEventDetailPage); + shelves.push(appEventDetailShelf); + const page = new GenericPage(shelves); + page.canonicalURL = attributeAsString(data, "url"); + return page; +} +//# sourceMappingURL=app-events-common.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/app-promotions/app-event-detail.js b/node_modules/@jet-app/app-store/tmp/src/common/app-promotions/app-event-detail.js new file mode 100644 index 0000000..ea5b203 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/app-promotions/app-event-detail.js @@ -0,0 +1,131 @@ +import * as models from "../../api/models"; +import * as metricsHelpersLocation from "../metrics/helpers/location"; +import * as metricsHelpersPage from "../metrics/helpers/page"; +import * as serverData from "../../foundation/json-parsing/server-data"; +import * as mediaAttributes from "../../foundation/media/attributes"; +import * as color from "../../foundation/util/color-util"; +import * as objects from "../../foundation/util/objects"; +import { addClickEventToAction } from "../metrics/helpers/clicks"; +import { MetricsReferralContext } from "../metrics/metrics-referral-context"; +import * as sharing from "../sharing"; +import * as appPromotionCommon from "./app-promotions-common"; +import { isNothing } from "@jet/environment"; +import { makeAppEventPageIntent } from "../../api/intents/app-event-page-intent"; +import { getLocale } from "../locale"; +import { getPlatform } from "../preview-platform"; +/** + * Create a flow action for navigating to the app event detail page. + * @param data The data blob + * @param parentAppData The associated parent app data + * @param appEvent The source app event + * @param baseMetricsOptions The base metrics options + * @param animationBehavior The animation behaviour for presenting the modal page + * @param includeLockupClickAction Whether to generate a click action for the lockup + * @param referrerData Referrer data from an incoming deep link + */ +export function appEventDetailPageFlowActionFromData(objectGraph, data, parentAppData, appEvent, baseMetricsOptions, animationBehavior, includeLockupClickAction, referrerData) { + const page = appEventDetailPageFromData(objectGraph, data, parentAppData, appEvent, baseMetricsOptions, includeLockupClickAction, referrerData, false); + const action = new models.FlowAction("appEventDetail"); + action.title = appEvent.title; + action.pageData = page; + action.animationBehavior = animationBehavior; + if (baseMetricsOptions && baseMetricsOptions.pageInformation) { + action.referrerUrl = baseMetricsOptions.pageInformation.pageUrl; + } + if (objectGraph.client.isWeb) { + action.destination = makeAppEventPageIntent({ + ...getLocale(objectGraph), + ...getPlatform(objectGraph), + id: appEvent.appEventId, + }); + action.pageUrl = mediaAttributes.attributeAsString(data, "url"); + } + return action; +} +/** + * Creates an app event detail page + * @param objectGraph The object graph + * @param data The data blob + * @param parentAppData The data blob for the related parent app + * @param appEvent The source app event + * @param baseMetricsOptions The base metrics options to use for the detail page + * @param includeLockupClickAction Whether to generate a click action for the lockup + * @param referrerData The referrer data + * @param isArcadePage Whether or not this is presented on the Arcade page + */ +export function appEventDetailPageFromData(objectGraph, data, parentAppData, appEvent, baseMetricsOptions, includeLockupClickAction, referrerData, isArcadePage) { + var _a, _b; + const artwork = appPromotionCommon.artworkFromData(objectGraph, data, "productArtwork"); + const video = appPromotionCommon.videoFromData(objectGraph, data, "productVideo", true, true); + const copy = objects.shallowCopyOf(appEvent); + const selectedArtwork = (_a = video === null || video === void 0 ? void 0 : video.preview) !== null && _a !== void 0 ? _a : artwork; + let mediaOverlayStyle = "dark"; + let isArtworkDark = true; + let includeBorderInDarkMode = false; + if (serverData.isDefinedNonNull(selectedArtwork)) { + isArtworkDark = color.isDarkColor(selectedArtwork.backgroundColor); + includeBorderInDarkMode = color.isDarkColor(selectedArtwork.backgroundColor, 10); + mediaOverlayStyle = isArtworkDark ? "dark" : "light"; + } + const pageInformation = metricsHelpersPage.pageInformationForAppPromotionDetailPage(objectGraph, models.AppPromotionType.AppEvent, data.id, parentAppData.id, referrerData, (_b = baseMetricsOptions.recoMetricsData) !== null && _b !== void 0 ? _b : null); + const metricsOptions = { + ...baseMetricsOptions, + pageInformation: pageInformation, + locationTracker: metricsHelpersLocation.newLocationTracker(), + targetType: "EventDetails", + }; + const clickOptions = { + ...metricsOptions, + id: parentAppData.id, + inAppEventId: data.id, + relatedSubjectIds: [parentAppData.id], + }; + copy.notificationConfig = appPromotionCommon.notificationConfigFromData(objectGraph, data, appEvent, clickOptions, false); + const shareAction = shareActionFromData(objectGraph, data, appEvent, clickOptions); + const offerEnvironment = isArtworkDark ? "dark" : "light"; + const lockup = appPromotionCommon.lockupFromData(objectGraph, data, parentAppData, copy.title, offerEnvironment, "transparent", false, metricsOptions, includeLockupClickAction, referrerData, isArcadePage, false); + if (serverData.isNull(lockup)) { + return null; + } + copy.lockup = lockup; + const page = new models.AppEventDetailPage(copy, artwork, video, shareAction, mediaOverlayStyle, includeBorderInDarkMode); + metricsHelpersPage.addMetricsEventsToPageWithInformation(objectGraph, page, pageInformation, (fields) => { + if (serverData.isDefinedNonNullNonEmpty(referrerData)) { + MetricsReferralContext.shared.addReferralContextToMetricsFieldsIfNecessary(fields); + } + }); + return page; +} +/** + * Creates an action for sharing an app event + * @param data: The data blob + * @param appEvent The source app event + * @param metricsClickOptions The click options used for the containing context. + */ +function shareActionFromData(objectGraph, data, appEvent, metricsClickOptions) { + var _a, _b; + const url = mediaAttributes.attributeAsString(data, "url"); + if (isNothing(url) || url.length === 0) { + return null; + } + // Prefer the module artwork first, otherwise fall back to the video preview frame, if available. + const artwork = (_a = appEvent.moduleArtwork) !== null && _a !== void 0 ? _a : (_b = appEvent.moduleVideo) === null || _b === void 0 ? void 0 : _b.preview; + let subtitle = objectGraph.loc.string("SHARE_APP_EVENT_SUBTITLE"); + if (subtitle === "SHARE_APP_EVENT_SUBTITLE") { + subtitle = appEvent.subtitle; + } + const shareData = sharing.shareSheetDataForAppEvent(objectGraph, appEvent.title, subtitle, url, undefined, artwork); + if (!serverData.isDefinedNonNull(shareData)) { + return null; + } + const activities = sharing.shareSheetActivitiesForAppEvent(objectGraph, appEvent, url); + const action = new models.ShareSheetAction(shareData, activities); + addClickEventToAction(objectGraph, action, { + ...metricsClickOptions, + targetType: "lockup", + actionType: "share", + idType: "its_id", + }); + return action; +} +//# sourceMappingURL=app-event-detail.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/app-promotions/app-event.js b/node_modules/@jet-app/app-store/tmp/src/common/app-promotions/app-event.js new file mode 100644 index 0000000..94eb7fe --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/app-promotions/app-event.js @@ -0,0 +1,243 @@ +import { isNothing, isSome } from "@jet/environment"; +import * as models from "../../api/models"; +import * as serverData from "../../foundation/json-parsing/server-data"; +import * as mediaAttributes from "../../foundation/media/attributes"; +import * as mediaRelationship from "../../foundation/media/relationships"; +import * as color from "../../foundation/util/color-util"; +import * as dateUtil from "../../foundation/util/date-util"; +import * as appPromotionCommon from "./app-promotions-common"; +/** + * Creates an app event object or a Date, if the event should not yet be shown, from the given data. + * @param data The data blob + * @param parentAppData The related parent app of this app event. If not provided will be derived from `data`. + * @param hideLockupWhenNotInstalled Whether to hide the lockup chin when the app is installed + * @param includeClickAction Whether to generate a click action for the app event + * @param offerEnvironment The preferred environment for the offer + * @param offerStyle The preferred style of the offer + * @param includeCrossLinkTitles Whether the cross link titles will be displayed when the app is installed + * @param allowMissingParentApp Whether to still create the app event if the parent app is missing + * @param baseMetricsOptions The base metrics options + * @param allowEndedEvents Whether events in the past are allowed + * @param includeLockupClickAction Whether to include the click action for the lockup + * @param isArcadePage Whether or not this is presented on the Arcade page + * @param allowUnpublishedAppEventPreviews Whether or not to allow app event previews + * @returns an AppEvent, or a Date if the event's `promotionStartDate` is in the future. + */ +export function appEventOrPromotionStartDateFromData(objectGraph, data, parentAppData, hideLockupWhenNotInstalled, includeClickAction, offerEnvironment, offerStyle, includeCrossLinkTitles, baseMetricsOptions, allowEndedEvents, includeLockupClickAction, editorialKind, isArcadePage, allowUnpublishedAppEventPreviews) { + var _a, _b, _c; + if (data.type !== "app-events") { + return null; + } + const promotionStartDateString = mediaAttributes.attributeAsString(data, "promotionStartDate"); + if (isNothing(promotionStartDateString) || promotionStartDateString.length === 0) { + return null; + } + const promotionStartDate = new Date(promotionStartDateString); + if (isNothing(promotionStartDate)) { + return null; + } + const todayDate = new Date(); + const isFutureDate = promotionStartDate.getTime() > todayDate.getTime(); + if (isFutureDate && !allowUnpublishedAppEventPreviews) { + return promotionStartDate; + } + // Artwork / video + const moduleArtwork = appPromotionCommon.artworkFromData(objectGraph, data, "lockupArtwork"); + const moduleVideo = appPromotionCommon.videoFromData(objectGraph, data, "lockupVideo", false, false); + if (isNothing(moduleArtwork) && serverData.isNullOrEmpty(moduleVideo)) { + return null; + } + const selectedArtwork = (_a = moduleVideo === null || moduleVideo === void 0 ? void 0 : moduleVideo.preview) !== null && _a !== void 0 ? _a : moduleArtwork; + const isArtworkDark = color.isDarkColor(selectedArtwork === null || selectedArtwork === void 0 ? void 0 : selectedArtwork.backgroundColor); + const mediaOverlayStyle = isArtworkDark ? "dark" : "light"; + const includeBorderInDarkMode = color.isDarkColor(selectedArtwork === null || selectedArtwork === void 0 ? void 0 : selectedArtwork.backgroundColor, 10); + // Other required fields + const title = mediaAttributes.attributeAsString(data, "name"); + let kind = mediaAttributes.attributeAsString(data, "kind"); + if (serverData.isDefinedNonNullNonEmpty(editorialKind)) { + kind = editorialKind; + } + const startDateString = mediaAttributes.attributeAsString(data, "startDate"); + if (isNothing(title) || + title.length === 0 || + isNothing(kind) || + kind.length === 0 || + isNothing(startDateString) || + startDateString.length === 0) { + return null; + } + const startDate = new Date(startDateString); + if (isNothing(startDate)) { + return null; + } + // Description + const detail = (_b = mediaAttributes.attributeAsString(data, "description.standard")) !== null && _b !== void 0 ? _b : ""; + // Lockup + const resolvedParentAppData = parentAppData !== null && parentAppData !== void 0 ? parentAppData : mediaRelationship.relationshipData(objectGraph, data, "app"); + let lockup = null; + if (isSome(resolvedParentAppData)) { + lockup = appPromotionCommon.lockupFromData(objectGraph, data, resolvedParentAppData, title, offerEnvironment, offerStyle, includeCrossLinkTitles, baseMetricsOptions, includeLockupClickAction, null, isArcadePage, true); + } + if (isNothing(lockup)) { + objectGraph.console.warn(`Parent app for event ${data.id} is missing, returning null.`); + return null; + } + // Requirements + const requirements = mediaAttributes.attributeAsString(data, "requirement"); + // Supplementary optional fields + const subtitle = mediaAttributes.attributeAsString(data, "subtitle"); + const endDateString = mediaAttributes.attributeAsString(data, "endDate"); + let endDate; + if (isSome(endDateString) && endDateString.length > 0) { + endDate = new Date(endDateString); + } + const hasEventEnded = endDate !== undefined && endDate.getTime() <= todayDate.getTime(); + if (hasEventEnded && !allowEndedEvents) { + return null; + } + // Formatted dates + const badgeKindString = (_c = mediaAttributes.attributeAsString(data, "badgeKind")) !== null && _c !== void 0 ? _c : undefined; + const badgeKind = badgeKindFromString(badgeKindString, startDate, endDate); + const formattedDates = formattedDatesWithKind(objectGraph, badgeKind, startDate, endDate); + const appEvent = new models.AppEvent(data.id, moduleArtwork !== null && moduleArtwork !== void 0 ? moduleArtwork : undefined, moduleVideo !== null && moduleVideo !== void 0 ? moduleVideo : undefined, title, subtitle !== null && subtitle !== void 0 ? subtitle : undefined, detail, startDate, endDate, badgeKind, kind, requirements !== null && requirements !== void 0 ? requirements : undefined, lockup, hideLockupWhenNotInstalled, formattedDates, mediaOverlayStyle, includeBorderInDarkMode); + // Notifications + if (serverData.isDefinedNonNull(resolvedParentAppData)) { + const clickOptions = { + ...baseMetricsOptions, + id: resolvedParentAppData.id, + inAppEventId: data.id, + relatedSubjectIds: [resolvedParentAppData.id], + }; + appEvent.notificationConfig = appPromotionCommon.notificationConfigFromData(objectGraph, data, appEvent, clickOptions, true); + } + // Click action + if (includeClickAction && serverData.isDefinedNonNull(resolvedParentAppData)) { + appEvent.clickAction = appPromotionCommon.detailPageClickActionFromData(objectGraph, data, resolvedParentAppData, appEvent, baseMetricsOptions, includeLockupClickAction); + } + return appEvent; +} +/** + * Determines the badge kind from the given string. + * @param badgeKindString The raw badge kind string + * @param startDate The start date of the app event + * @param endDate The end date of the app event, if any + * @returns An AppEventBadgeKind + */ +export function badgeKindFromString(badgeKindString, startDate, endDate) { + let badgeKind = (badgeKindString !== null && badgeKindString !== void 0 ? badgeKindString : models.AppEventBadgeKind.live); + if (badgeKind === models.AppEventBadgeKind.live && serverData.isDefinedNonNull(endDate)) { + // If the event is longer than 6 hours, override with "happening now" so that + // the blinking red live dot is less prevalent. + const difference = endDate.getTime() - startDate.getTime(); + const sixHoursDifference = 1000 * 60 * 60 * 6; + if (difference > sixHoursDifference) { + badgeKind = models.AppEventBadgeKind.happening; + } + } + return badgeKind; +} +/** + * Generates a list of all the possible date variants for a given app event. The native + * client will look at this list and determine which variant to display, based + * on the current time. The reason for sending down all variants is that the client will + * continue to update the display as time progresses, so it's feasible that the client will + * cross over from date variant to the next (eg. TOMORROW 12:00 PM -> TODAY 12:00 PM). + * @param badgeKind The kind of badge used + * @param startDate The start date of the app event + * @param endDate The end date of the app event, if any + */ +export function formattedDatesWithKind(objectGraph, badgeKind, startDate, endDate) { + const formattedDates = []; + const startMidnight = dateUtil.convertLocalDateToLocalMidnight(startDate); + if (isNothing(startMidnight)) { + // This is only possible if `startDate` is "nothing", which means we can't proceed to create formatted dates anyway. + return []; + } + // Event starts 7+ days from now + // Example: JAN 15 + const introductionDateFormat = objectGraph.loc.string("AppEvents.FormattedDate.SevenDaysOrMore.DateFormat"); + const introductionText = objectGraph.loc.uppercased(objectGraph.loc.formatDate(introductionDateFormat, startDate)); + const introductionFormattedDate = new models.AppEventFormattedDate(false, undefined, introductionText !== null && introductionText !== void 0 ? introductionText : undefined, undefined, undefined); + formattedDates.push(introductionFormattedDate); + // Event starts 2-6 days from now + // Example: FRI 12:00 PM + const sixDaysPrior = new Date(startMidnight); + sixDaysPrior.setDate(sixDaysPrior.getDate() - 6); + const sixDaysPriorDateFormat = objectGraph.loc.string("AppEvents.FormattedDate.SixDaysOrLess.DateFormat"); + const sixDaysPriorText = objectGraph.loc.formatDate(sixDaysPriorDateFormat, startDate); + const sixDaysPriorTextUppercased = objectGraph.loc.uppercased(sixDaysPriorText); + const sixDaysPriorFormattedDate = new models.AppEventFormattedDate(false, sixDaysPrior, sixDaysPriorTextUppercased !== null && sixDaysPriorTextUppercased !== void 0 ? sixDaysPriorTextUppercased : undefined, undefined, undefined); + formattedDates.push(sixDaysPriorFormattedDate); + // Event starts tomorrow + // Example: TOMORROW 12:00 PM + const oneDayPrior = new Date(startMidnight); + oneDayPrior.setDate(oneDayPrior.getDate() - 1); + const tomorrowSentence = objectGraph.loc.string("AppEvents.FormattedDate.Tomorrow"); + const tomorrowText = objectGraph.loc.formatDateInSentence(tomorrowSentence, "j:mm", startDate); + const tomorrowTextUppercased = objectGraph.loc.uppercased(tomorrowText); + const tomorrowFormattedDate = new models.AppEventFormattedDate(false, oneDayPrior, tomorrowTextUppercased !== null && tomorrowTextUppercased !== void 0 ? tomorrowTextUppercased : undefined, undefined, undefined); + formattedDates.push(tomorrowFormattedDate); + // Event starts today + // If the event starts within 1 hour of midnight, we don't need to show this case + // as it will be superceded by the 1 hour countdown + // Example: TODAY 12:00 PM, TONIGHT 7:00 PM + if (startDate.getHours() > 1 || (startDate.getHours() === 1 && startDate.getMinutes() > 0)) { + let todaySentence; + if (startDate.getHours() >= 19) { + todaySentence = objectGraph.loc.string("AppEvents.FormattedDate.Tonight"); + } + else { + todaySentence = objectGraph.loc.string("AppEvents.FormattedDate.Today"); + } + const todayText = objectGraph.loc.formatDateInSentence(todaySentence, "j:mm", startDate); + const todayTextUppercased = objectGraph.loc.uppercased(todayText); + const todayFormattedDate = new models.AppEventFormattedDate(false, startMidnight !== null && startMidnight !== void 0 ? startMidnight : undefined, todayTextUppercased !== null && todayTextUppercased !== void 0 ? todayTextUppercased : undefined, undefined, undefined); + formattedDates.push(todayFormattedDate); + } + // Event is starting in 1 hour or less + // Example: STARTS IN 59 MIN / AVAILABLE IN 59 MIN + const oneHourPrior = new Date(startDate); + oneHourPrior.setHours(oneHourPrior.getHours() - 1); + let countdownStringKey; + switch (badgeKind) { + case models.AppEventBadgeKind.available: + countdownStringKey = "AppEvents.FormattedDate.AvailableIn.MinutesCountdown"; + break; + case models.AppEventBadgeKind.happening: + case models.AppEventBadgeKind.live: + default: + countdownStringKey = "AppEvents.FormattedDate.StartsIn.MinutesCountdown"; + break; + } + const oneHourPriorFormattedDate = new models.AppEventFormattedDate(false, oneHourPrior, undefined, startDate, countdownStringKey); + formattedDates.push(oneHourPriorFormattedDate); + // Event is happening now + // Example: LIVE, HAPPENING NOW, NOW AVAILABLE + let liveText; + let showLiveIndicator; + switch (badgeKind) { + case models.AppEventBadgeKind.available: + liveText = objectGraph.loc.string("AppEvents.FormattedDate.NowAvailable"); + showLiveIndicator = false; + break; + case models.AppEventBadgeKind.happening: + liveText = objectGraph.loc.string("AppEvents.FormattedDate.HappeningNow"); + showLiveIndicator = false; + break; + case models.AppEventBadgeKind.live: + default: + liveText = objectGraph.loc.string("AppEvents.FormattedDate.Live"); + showLiveIndicator = true; + break; + } + const liveFormattedDate = new models.AppEventFormattedDate(showLiveIndicator, startDate, liveText, undefined, undefined); + formattedDates.push(liveFormattedDate); + // Event has ended + // Example: EVENT ENDED + if (endDate !== null) { + const eventEndedFormattedDate = new models.AppEventFormattedDate(false, endDate, objectGraph.loc.string("AppEvents.FormattedDate.EventEnded"), undefined, undefined); + formattedDates.push(eventEndedFormattedDate); + } + return formattedDates; +} +//# sourceMappingURL=app-event.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/app-promotions/app-promotion.js b/node_modules/@jet-app/app-store/tmp/src/common/app-promotions/app-promotion.js new file mode 100644 index 0000000..84afb2e --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/app-promotions/app-promotion.js @@ -0,0 +1,120 @@ +import * as models from "../../api/models"; +import * as appEventDetail from "./app-event-detail"; +import * as contingentOfferDetail from "./contingent-offer-detail"; +import * as offerItemDetail from "./offer-item-detail"; +import * as appEventModel from "./app-event"; +import * as contingentOfferModel from "./contingent-offer"; +import * as offerItemModel from "./offer-item"; +/** + * @param data The MAPI data to determine the promotion type + * @returns The App Promotion type, or null if the type is unknown + */ +export function promotionTypeFromData(data) { + switch (data.type) { + case "app-events": + return models.AppPromotionType.AppEvent; + case "contingent-items": + return models.AppPromotionType.ContingentOffer; + case "offer-items": + return models.AppPromotionType.OfferItem; + default: + return null; + } +} +/** + * @param data The MAPI data to determine the promotion type + * @returns The target type for metrics events for the given app promotion + */ +export function metricsTargetTypeFromData(data) { + switch (data.type) { + case "app-events": + return "eventModule"; + case "contingent-items": + return "module"; + case "offer-items": + return "module"; + default: + return null; + } +} +/** + * @param data The MAPI data to determine the MetricsKind + * @returns The metrics kind for metrics events for the given app promotion + */ +export function metricsKindFromData(data) { + switch (data.type) { + case "app-events": + return "inAppEvent"; + case "contingent-items": + return "contingentPriceOffer"; + case "offer-items": + return "winbackPriceOffer"; + default: + return null; + } +} +/** + * Creates an app promotion (App Event or Contingent Offer) object or a Date, if the event should not yet be shown, from the given data. + * @param data The data blob + * @param parentAppData The related parent app of this app promotion. If not provided will be derived from `data`. + * @param hideLockupWhenNotInstalled Whether to hide the lockup chin when the app is installed + * @param includeClickAction Whether to generate a click action for the app promotion + * @param offerEnvironment The preferred environment for the offer + * @param offerStyle The preferred style of the offer + * @param includeCrossLinkTitles Whether the cross link titles will be displayed when the app is installed + * @param allowMissingParentApp Whether to still create the app event if the parent app is missing + * @param baseMetricsOptions The base metrics options + * @param allowEndedEvents Whether events in the past are allowed + * @param includeLockupClickAction Whether to include the click action for the lockup + * @param isArcadePage Whether or not this is presented on the Arcade page + * @param allowUnpublishedAppEventPreviews Whether or not to allow event previews + * @returns an AppPromotion, or a Date if the event's `promotionStartDate` is in the future. + */ +export function appPromotionOrDateFromData(objectGraph, data, parentAppData, hideLockupWhenNotInstalled, includeClickAction, offerEnvironment, offerStyle, includeCrossLinkTitles, baseMetricsOptions, allowEndedEvents, includeLockupClickAction, editorialKind, isArcadePage, allowUnpublishedAppEventPreviews) { + const promotionType = promotionTypeFromData(data); + const promotionBaseMetricsOptions = { + ...baseMetricsOptions, + targetType: metricsTargetTypeFromData(data), + }; + switch (promotionType) { + case models.AppPromotionType.AppEvent: + return appEventModel.appEventOrPromotionStartDateFromData(objectGraph, data, parentAppData, hideLockupWhenNotInstalled, includeClickAction, offerEnvironment, offerStyle, includeCrossLinkTitles, promotionBaseMetricsOptions, allowEndedEvents, includeLockupClickAction, editorialKind, isArcadePage, allowUnpublishedAppEventPreviews); + case models.AppPromotionType.ContingentOffer: + return contingentOfferModel.contingentOfferFromData(objectGraph, data, parentAppData, offerEnvironment, offerStyle, promotionBaseMetricsOptions, includeLockupClickAction, isArcadePage); + case models.AppPromotionType.OfferItem: + return offerItemModel.offerItemFromData(objectGraph, data, parentAppData, offerEnvironment, offerStyle, promotionBaseMetricsOptions, includeLockupClickAction, isArcadePage); + default: + return null; + } +} +/** + * Create a flow action for navigating to the app promotion detail page. + * @param data The data blob + * @param parentAppData The associated parent app data + * @param appPromotion The source app promotion + * @param baseMetricsOptions The base metrics options + * @param animationBehavior The animation behaviour for presenting the modal page + * @param includeLockupClickAction Whether to generate a click action for the lockup + * @param referrerData Referrer data from an incoming deep link + */ +export function detailPageFlowActionFromData(objectGraph, data, parentAppData, appPromotion, baseMetricsOptions, animationBehavior, includeLockupClickAction, referrerData) { + const promotionType = promotionTypeFromData(data); + const promotionBaseMetricsOptions = { + ...baseMetricsOptions, + targetType: metricsTargetTypeFromData(data), + }; + switch (promotionType) { + case models.AppPromotionType.AppEvent: + const appEvent = appPromotion; + return appEventDetail.appEventDetailPageFlowActionFromData(objectGraph, data, parentAppData, appEvent, promotionBaseMetricsOptions, animationBehavior, includeLockupClickAction, referrerData); + case models.AppPromotionType.ContingentOffer: + const contingentOffer = appPromotion; + return contingentOfferDetail.contingentOfferDetailPageFlowActionFromData(objectGraph, data, parentAppData, contingentOffer, promotionBaseMetricsOptions, animationBehavior, includeLockupClickAction, referrerData); + case models.AppPromotionType.OfferItem: + const offerItem = appPromotion; + return offerItemDetail.offerItemDetailPageFlowActionFromData(objectGraph, data, parentAppData, offerItem, promotionBaseMetricsOptions, animationBehavior, includeLockupClickAction, referrerData); + default: + return null; + } +} +//# sourceMappingURL=app-promotion.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/app-promotions/app-promotions-common.js b/node_modules/@jet-app/app-store/tmp/src/common/app-promotions/app-promotions-common.js new file mode 100644 index 0000000..ffeac86 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/app-promotions/app-promotions-common.js @@ -0,0 +1,385 @@ +import { isNothing, isSome } from "@jet/environment"; +import * as models from "../../api/models"; +import * as serverData from "../../foundation/json-parsing/server-data"; +import * as mediaAttributes from "../../foundation/media/attributes"; +import * as platformAttributes from "../../foundation/media/platform-attributes"; +import * as mediaRelationship from "../../foundation/media/relationships"; +import { Host, Parameters, Protocol } from "../../foundation/network/url-constants"; +import * as urls from "../../foundation/network/urls"; +import * as objects from "../../foundation/util/objects"; +import * as videoDefaults from "../constants/video-constants"; +import * as contentArtwork from "../content/artwork/artwork"; +import * as contentAttributes from "../content/attributes"; +import * as content from "../content/content"; +import * as lockups from "../lockups/lockups"; +import * as metricsBuilder from "../metrics/builder"; +import * as metricsHelpersClicks from "../metrics/helpers/clicks"; +import * as metricsHelpersImpressions from "../metrics/helpers/impressions"; +import * as metricsHelpersLocation from "../metrics/helpers/location"; +import * as metricsHelpersMisc from "../metrics/helpers/misc"; +import * as metricsHelpersModels from "../metrics/helpers/models"; +import * as appPromotionModel from "./app-promotion"; +import { formattedTextFromData } from "./contingent-offer"; +/** + * Convenience function for determining if app events are enabled. + */ +export function appEventsAreEnabled(objectGraph) { + return objectGraph.bag.enableAppEvents && (objectGraph.client.isiOS || objectGraph.client.isWeb); +} +/** + * Convenience function for determining if contingent items are enabled. + */ +export function appContingentItemsAreEnabled(objectGraph) { + const isContingentEnabledInBag = objectGraph.bag.enableContingentOffers; + return isContingentEnabledInBag && objectGraph.client.isiOS; +} +/** + * Convenience function for determining if offer items (Winback) items are enabled. + */ +export function appOfferItemsAreEnabled(objectGraph) { + return objectGraph.bag.enableOfferItems && objectGraph.client.isiOS; +} +/** + * Creates the artwork suitable for an app promotion + * @param data The data blob + * @param artworkKey The key used to derive the artwork from the data blob + */ +export function artworkFromData(objectGraph, data, artworkKey) { + const artworkData = mediaAttributes.attributeAsDictionary(data, artworkKey); + if (isNothing(artworkData)) { + return null; + } + const artwork = content.artworkFromApiArtwork(objectGraph, artworkData, { + useCase: 0 /* content.ArtworkUseCase.Default */, + withJoeColorPlaceholder: true, + cropCode: "sr", + }); + return artwork; +} +/** + * Creates the artwork suitable for an app promotion from the platform attributes + * @param data The data blob + * @param artworkKey The key used to derive the artwork from the data blob + */ +export function artworkFromPlatformData(objectGraph, data, artworkKey) { + const attributePlatform = contentAttributes.bestAttributePlatformFromData(objectGraph, data); + if (isNothing(attributePlatform)) { + return null; + } + const artworkData = platformAttributes.platformAttributeAsDictionary(data, attributePlatform, artworkKey); + if (isNothing(artworkData)) { + return null; + } + const artwork = content.artworkFromApiArtwork(objectGraph, artworkData, { + useCase: 0 /* content.ArtworkUseCase.Default */, + withJoeColorPlaceholder: true, + cropCode: "sr", + }); + return artwork; +} +/** + * Creates the video suitable for an app promotion + * @param objectGraph + * @param data The data blob + * @param videoKey The key used to derive the video from the data blob + * @param canPlayFullScreen Whether the video should support full-screen playback + * @param isFullPage whether this video is being used on the full page + */ +export function videoFromData(objectGraph, data, videoKey, canPlayFullScreen, isFullPage) { + // Preview artwork + const previewArtwork = artworkFromData(objectGraph, data, `${videoKey}.previewFrame`); + if (serverData.isNull(previewArtwork)) { + return null; + } + // Video URL + const videoUrl = mediaAttributes.attributeAsString(data, `${videoKey}.video`); + if (serverData.isNull(videoUrl)) { + return null; + } + const autoplayPlaybackControls = { + muteUnmute: true, + }; + const configuration = { + allowsAutoPlay: true, + looping: true, + canPlayFullScreen: canPlayFullScreen, + playbackControls: isFullPage ? videoDefaults.standardControls(objectGraph) : {}, + autoPlayPlaybackControls: isFullPage ? autoplayPlaybackControls : {}, + }; + const video = new models.Video(videoUrl, previewArtwork, configuration); + video.canPlayFullScreen = canPlayFullScreen; + video.allowsAutoPlay = true; + video.looping = true; + return video; +} +/** + * Creates the lockup for an app event or contingent offer + * @param objectGraph The object graph. + * @param promotionData The data blob + * @param parentAppData The related parent app of this app promotion + * @param title The title of the app promotion + * @param offerEnvironment The preferred environment for the offer + * @param offerStyle The preferred style of the offer + * @param includeCrossLinkTitles Whether the cross link titles will be displayed when the app is installed + * @param baseMetricsOptions The base metrics options for the lockup + * @param includeLockupClickAction Whether to generate a click action for the lockup + * @param referrerData Referrer data from an incoming deep link + * @param isArcadePage Whether or not this is presented on the Arcade page. + * @param includeModuleClickLocation Whether or not this to push the module location to the location tracker. + + */ +export function lockupFromData(objectGraph, promotionData, parentAppData, title, offerEnvironment, offerStyle, includeCrossLinkTitles, baseMetricsOptions, includeLockupClickAction, referrerData, isArcadePage, includeModuleClickLocation) { + var _a, _b, _c; + if (isNothing(promotionData) || isNothing(parentAppData)) { + // if (serverData.isNullOrEmpty(promotionData) || serverData.isNullOrEmpty(parentAppData)) { + return null; + } + const promotionType = appPromotionModel.promotionTypeFromData(promotionData); + // Push a content location, so that the lockup action has both the containing card (eg. event module) + // lockup location included. + const contentMetricsOptions = { + ...baseMetricsOptions, + id: promotionData.id, + relatedSubjectIds: [parentAppData.id], + idType: "its_id", + }; + const lockupMetrics = { + ...baseMetricsOptions, + id: parentAppData.id, + relatedSubjectIds: [parentAppData.id], + targetType: "lockup", + idType: "its_id", + kind: null, + softwareType: null, + title: (_a = mediaAttributes.attributeAsString(parentAppData, "name")) !== null && _a !== void 0 ? _a : "", + excludeAttribution: serverData.isNullOrEmpty(referrerData), + }; + if (promotionType === models.AppPromotionType.AppEvent) { + contentMetricsOptions["inAppEventId"] = promotionData.id; + lockupMetrics["inAppEventId"] = promotionData.id; + } + // If our base metrics options are in fact content metrics options, we want to carry across + // the ID and ID type. This specifically caters for heros / editorial cards. + if (metricsHelpersModels.isContentMetricsOptions(baseMetricsOptions)) { + contentMetricsOptions.id = baseMetricsOptions.id; + contentMetricsOptions.idType = baseMetricsOptions.idType; + } + if (includeModuleClickLocation) { + const locationTitle = promotionType === models.AppPromotionType.ContingentOffer + ? (_b = formattedTextFromData(objectGraph, promotionData)) === null || _b === void 0 ? void 0 : _b.rawTitle + : mediaAttributes.attributeAsString(promotionData, "name"); + metricsHelpersLocation.pushContentLocation(objectGraph, contentMetricsOptions, locationTitle !== null && locationTitle !== void 0 ? locationTitle : ""); + } + const externalDeepLinkUrl = mediaAttributes.attributeAsString(promotionData, "deepLink"); + const lockupOptions = { + metricsOptions: lockupMetrics, + artworkUseCase: 1 /* content.ArtworkUseCase.LockupIconSmall */, + externalDeepLinkUrl: externalDeepLinkUrl !== null && externalDeepLinkUrl !== void 0 ? externalDeepLinkUrl : undefined, + crossLinkSubtitle: includeCrossLinkTitles ? title : undefined, + offerEnvironment: offerEnvironment, + offerStyle: offerStyle, + skipDefaultClickAction: !includeLockupClickAction, + includeBetaApps: true, + referrerData: referrerData !== null && referrerData !== void 0 ? referrerData : undefined, + shouldHideArcadeHeader: objectGraph.featureFlags.isEnabled("hide_arcade_header_on_arcade_tab") && isArcadePage, + parentAppData: parentAppData, + useJoeColorIconPlaceholder: true, + overrideArtworkTextColorKey: "textColor4", + }; + const resolvedData = promotionType === models.AppPromotionType.AppEvent ? parentAppData : promotionData; + const lockup = lockups.lockupFromData(objectGraph, resolvedData, lockupOptions); + if (includeModuleClickLocation) { + metricsHelpersLocation.popLocation(baseMetricsOptions.locationTracker); + } + if (serverData.isNull(lockup)) { + return null; + } + if (includeCrossLinkTitles) { + lockup.crossLinkTitle = (_c = objectGraph.loc.uppercased(lockup.title)) !== null && _c !== void 0 ? _c : undefined; + } + return lockup; +} +export function notificationConfigFromData(objectGraph, data, appEvent, baseMetricsOptions, includeScheduledAction) { + // If the event has already started, we cannot set a notification reminder + if (appEvent.startDate.getTime() <= Date.now()) { + return null; + } + if (isNothing(appEvent.lockup)) { + return null; + } + const title = objectGraph.loc.string("APP_EVENTS_NOTIFICATION_TITLE").replace("{appTitle}", appEvent.lockup.title); + const detail = objectGraph.loc.string("APP_EVENTS_NOTIFICATION_DETAIL").replace("{eventTitle}", appEvent.title); + const displayTime = appEvent.startDate; + const icon = appEvent.lockup.icon; + const artworkUrl = appEvent.lockup.icon.template + .replace("{w}", `${icon.width}`) + .replace("{h}", `${icon.height}`) + .replace("{c}", "wd") // iOS rounded corners + .replace("{f}", "png"); + // Notification scheduled action + let scheduledAction; + if (includeScheduledAction) { + scheduledAction = new models.AlertAction("toast"); + scheduledAction.title = objectGraph.loc.string("APP_EVENTS_NOTIFICATION_TOAST_TITLE"); + scheduledAction.message = objectGraph.loc.string("APP_EVENTS_NOTIFICATION_TOAST_DETAIL"); + scheduledAction.artwork = contentArtwork.createArtworkForResource(objectGraph, "systemimage://bell.fill"); + } + // The below if statement can be removed in Sydro timeframe + // Notifications not authorized action + let notAuthorizedAction; + if (objectGraph.bag.newEventsForODJAreEnabled) { + // When we have ODJs active we send a metrics click event to signal the Alert button was tapped. ODJ picks this up as a signal to + // show a half/full sheet notifications upsell to the user + const notAuthorizedMetricsAction = new models.BlankAction(); + // Schedule click data + const scheduleClickFieldsNotAuthed = metricsHelpersMisc.fieldsFromPageInformation(baseMetricsOptions.pageInformation); + scheduleClickFieldsNotAuthed["actionType"] = "notifyActivateNotificationsDisabled"; + scheduleClickFieldsNotAuthed["location"] = metricsHelpersLocation.createContentLocation(objectGraph, { + ...baseMetricsOptions, + id: data.id, + }, ""); + // We want to actively remove the topic from this click event so it doesn't leave the device and is only consumed by ODJ + scheduleClickFieldsNotAuthed["topic"] = ""; + const scheduleClickDataNotAuthed = metricsBuilder.createMetricsClickData(objectGraph, appEvent.lockup.adamId, "lockup", scheduleClickFieldsNotAuthed); + notAuthorizedMetricsAction.actionMetrics.addMetricsData(scheduleClickDataNotAuthed); + notAuthorizedAction = notAuthorizedMetricsAction; + } + else { + const notAuthorizedAlertAction = new models.AlertAction("default"); + notAuthorizedAlertAction.title = objectGraph.loc.string("APP_EVENTS_NOTIFICATION_NOT_AUTHORIZED_TITLE"); + notAuthorizedAlertAction.message = objectGraph.loc.string("APP_EVENTS_NOTIFICATION_NOT_AUTHORIZED_DETAIL"); + notAuthorizedAlertAction.isCancelable = true; + notAuthorizedAlertAction.buttonTitles = [objectGraph.loc.string("ACTION_SETTINGS")]; + // NOTE: This URL only works on iOS. If this feature is expanded beyond iOS, this code will need to be split per-platform. + notAuthorizedAlertAction.buttonActions = [ + new models.ExternalUrlAction("prefs:root=NOTIFICATIONS_ID&path=com.apple.AppStore", true), + ]; + notAuthorizedAction = notAuthorizedAlertAction; + } + const failureAction = new models.AlertAction("default"); + failureAction.title = objectGraph.loc.string("APP_EVENTS_NOTIFICATION_FAILURE_TITLE"); + failureAction.message = objectGraph.loc.string("APP_EVENTS_NOTIFICATION_FAILURE_DETAIL"); + failureAction.isCancelable = true; + // App launch trampoline URL + const appLaunchTrampolineUrl = new urls.URL() + .set("protocol", Protocol.storeKitUIServiceAppStore) + .param(Parameters.appId, appEvent.lockup.adamId) + .param(Parameters.bundleId, appEvent.lockup.bundleId) + .param(Parameters.appEventId, appEvent.appEventId); + appLaunchTrampolineUrl.host = Host.launchApp; + const externalDeepLinkUrl = mediaAttributes.attributeAsString(data, "deepLink"); + if (isSome(externalDeepLinkUrl) && (externalDeepLinkUrl === null || externalDeepLinkUrl === void 0 ? void 0 : externalDeepLinkUrl.length) > 0) { + appLaunchTrampolineUrl.param(Parameters.appEventDeepLink, encodeURIComponent(externalDeepLinkUrl)); + } + // Schedule click data + const scheduleClickFields = metricsHelpersMisc.fieldsFromPageInformation(baseMetricsOptions.pageInformation); + scheduleClickFields["actionType"] = "notifyActivate"; + scheduleClickFields["location"] = metricsHelpersLocation.createContentLocation(objectGraph, { + ...baseMetricsOptions, + id: data.id, + }, ""); + const scheduleClickData = metricsBuilder.createMetricsClickData(objectGraph, appEvent.lockup.adamId, "lockup", scheduleClickFields); + // Cancel schedule click data + const cancelScheduleClickFields = objects.shallowCopyOf(scheduleClickFields); + cancelScheduleClickFields["actionType"] = "notifyDeactivate"; + const cancelScheduleClickData = metricsBuilder.createMetricsClickData(objectGraph, appEvent.lockup.adamId, "lockup", cancelScheduleClickFields); + return new models.AppEventNotificationConfig(data.id, title, detail, artworkUrl, displayTime, scheduledAction, notAuthorizedAction, failureAction, appLaunchTrampolineUrl.build(), scheduleClickData, cancelScheduleClickData); +} +/** + * Create a click action for navigating to the contingent offer detail page. + * @param data The data blob + * @param parentAppData The associated parent app data + * @param appPromotion The source app promotion + * @param baseMetricsOptions The base metrics options + * @param includeLockupClickAction Whether to generate a click action for the lockup + */ +export function detailPageClickActionFromData(objectGraph, data, parentAppData, appPromotion, baseMetricsOptions, includeLockupClickAction) { + const action = appPromotionModel.detailPageFlowActionFromData(objectGraph, data, parentAppData, appPromotion, baseMetricsOptions, "infer", includeLockupClickAction, null); + if (isNothing(action)) { + return undefined; + } + const clickOptions = { + id: data.id, + actionDetails: { + action: "Open", + contentType: appPromotionModel.metricsTargetTypeFromData(data), + }, + relatedSubjectIds: [parentAppData.id], + ...baseMetricsOptions, + }; + const promotionType = appPromotionModel.promotionTypeFromData(data); + if (promotionType === models.AppPromotionType.AppEvent) { + clickOptions["inAppEventId"] = data.id; + } + metricsHelpersClicks.addClickEventToAction(objectGraph, action, clickOptions); + return action; +} +/** + * Creates the app events or contingent offers objects from the given data + * @param objectGraph The object graph + * @param appPromotionDataItems The array of app event / contingent offer data blobs. + * @param parentAppData The data for the parent app, if any. + * @param hideLockupWhenNotInstalled If true, the lockup will be hidden when the app is not locally installed + * @param includeCrossLinkTitles Whether the cross link titles will be displayed when the app is installed + * @param baseMetricsOptions The base metrics options for the app promotions + * @param allowEndedEvents Whether or not ended events should be returned + * @param includeLockupClickAction Whether to generate a click action for the lockup + * @param isArcadePage Whether or not this is presented on the Arcade page + * @param allowUnpublishedAppEventPreviews Whether or not to allow app event previews + * @returns an DisplayableAppPromotions object including the relevant App Promotions, as well as an optional Date for when the next App Event should be visible. + */ +export function appPromotionsFromData(objectGraph, appPromotionDataItems, parentAppData = null, hideLockupWhenNotInstalled, includeCrossLinkTitles, baseMetricsOptions, allowEndedEvents, includeLockupClickAction, isArcadePage, allowUnpublishedAppEventPreviews) { + var _a; + const appPromotions = []; + let nextAppEventPromotionStartDate; + for (const data of appPromotionDataItems) { + const appPromotionOrDate = appPromotionModel.appPromotionOrDateFromData(objectGraph, data, parentAppData, hideLockupWhenNotInstalled, true, "light", "infer", includeCrossLinkTitles, baseMetricsOptions, allowEndedEvents, includeLockupClickAction, null, isArcadePage, allowUnpublishedAppEventPreviews); + if (serverData.isNull(appPromotionOrDate)) { + continue; + } + if (appPromotionOrDate instanceof Date) { + // Set the next event promotion start date if we don't yet have one, or it's sooner than the current one. + if (isNothing(nextAppEventPromotionStartDate) || + appPromotionOrDate.getTime() < nextAppEventPromotionStartDate.getTime()) { + nextAppEventPromotionStartDate = appPromotionOrDate; + } + continue; + } + const appPromotionItem = appPromotionOrDate; + // Metrics + const impressionOptions = { + ...baseMetricsOptions, + id: data.id, + kind: appPromotionModel.metricsKindFromData(data), + targetType: appPromotionModel.metricsTargetTypeFromData(data), + title: (_a = appPromotionItem.title) !== null && _a !== void 0 ? _a : "", + softwareType: null, + }; + const resolvedParentAppData = parentAppData !== null && parentAppData !== void 0 ? parentAppData : mediaRelationship.relationshipData(objectGraph, data, "app"); + if (serverData.isDefinedNonNull(resolvedParentAppData)) { + impressionOptions.relatedSubjectIds = [resolvedParentAppData.id]; + } + metricsHelpersImpressions.addImpressionFields(objectGraph, appPromotionItem, impressionOptions); + metricsHelpersLocation.nextPosition(impressionOptions.locationTracker); + appPromotions.push(appPromotionItem); + } + return { + appPromotions: appPromotions, + nextAppEventPromotionStartDate: nextAppEventPromotionStartDate, + }; +} +/** + * Replaces keys inside a templated string with their computed values. + * @param templateString A templated string with keys that need to be replaced + * @param templateKeys A map of string keys to replacement strings + * @returns A filled out string with no keys + */ +export function replacingTemplatedKeys(templateString, templateKeys) { + let returnString = templateString !== null && templateString !== void 0 ? templateString : ""; + Object.keys(templateKeys).forEach((element) => { + returnString = returnString.replace(element, templateKeys[element]); + }); + return returnString; +} +// endregion +//# sourceMappingURL=app-promotions-common.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/app-promotions/app-promotions-shelf.js b/node_modules/@jet-app/app-store/tmp/src/common/app-promotions/app-promotions-shelf.js new file mode 100644 index 0000000..4949afa --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/app-promotions/app-promotions-shelf.js @@ -0,0 +1,120 @@ +import { isNothing, isSome } from "@jet/environment"; +import * as models from "../../api/models"; +import * as serverData from "../../foundation/json-parsing/server-data"; +import * as mediaDataStructure from "../../foundation/media/data-structure"; +import * as mediaRelationship from "../../foundation/media/relationships"; +import * as metricsHelpersImpressions from "../metrics/helpers/impressions"; +import * as metricsHelpersLocation from "../metrics/helpers/location"; +import * as refresh from "../refresh/page-refresh-controller"; +import * as appPromotionsCommon from "./app-promotions-common"; +/** + * Builder for the app events shelf, on the product page. + * @param data The container data + * @param shelfMetrics The product page shelf metrics. + * @return The built shelf or null + */ +export function appPromotionsShelfForProductPage(objectGraph, data, shelfMetrics, refreshController) { + var _a; + if (!appPromotionsCommon.appEventsAreEnabled(objectGraph)) { + return null; + } + let eventAndOffersData = mediaRelationship.relationshipViewsCollection(data, "events-and-offers"); + if (isNothing(eventAndOffersData) || eventAndOffersData.length === 0) { + eventAndOffersData = mediaRelationship.relationshipCollection(data, "app-events"); + } + if (isNothing(eventAndOffersData) || eventAndOffersData.length === 0) { + return null; + } + const recoMetricsData = mediaRelationship.relationshipViewsContainer(data, "events-and-offers"); + const metricsOptions = { + pageInformation: shelfMetrics.metricsPageInformation, + locationTracker: shelfMetrics.locationTracker, + recoMetricsData: (_a = mediaDataStructure.metricsFromMediaApiObject(recoMetricsData)) !== null && _a !== void 0 ? _a : undefined, + }; + const hasOffers = eventAndOffersData.some((dataItem) => dataItem.type !== "app-events"); + const titleKey = hasOffers ? "ProductPage.Section.AppEventsAndOffers.Title" : "ProductPage.Section.AppEvents.Title"; + const shelfTitle = objectGraph.loc.string(titleKey); + const shelfId = hasOffers ? "EventsAndOffers" : "Events"; + metricsHelpersLocation.pushContentLocation(objectGraph, { + pageInformation: shelfMetrics.metricsPageInformation, + locationTracker: shelfMetrics.locationTracker, + recoMetricsData: metricsOptions.recoMetricsData, + targetType: "swoosh", + id: shelfId, + idType: "none", + }, shelfTitle); + const displayableAppEvents = appPromotionsCommon.appPromotionsFromData(objectGraph, eventAndOffersData, data, !hasOffers, false, metricsOptions, false, false, false, false); + if (isSome(displayableAppEvents.nextAppEventPromotionStartDate)) { + refresh.addNextPreferredContentRefreshDate(displayableAppEvents.nextAppEventPromotionStartDate, refreshController); + } + const appEvents = displayableAppEvents.appPromotions; + if (appEvents.length === 0) { + metricsHelpersLocation.popLocation(metricsOptions.locationTracker); + return null; + } + const shelf = appEventsShelf(objectGraph, appEvents, shelfTitle); + shelfMetrics.addImpressionsToShelf(objectGraph, shelf, "swoosh", shelfId, "none", metricsOptions.recoMetricsData); + metricsHelpersLocation.popLocation(metricsOptions.locationTracker); + metricsHelpersLocation.nextPosition(metricsOptions.locationTracker); + return shelf; +} +/** + * Builder for the app events shelf, in a today article. + * @param objectGraph + * @param dataItems The array of data blobs + * @param metricsPageInformation The metrics page information. + * @param metricsLocationTracker The metrics location tracker. + * @param context + * @return The built shelf or null + */ +export function appEventsShelfForArticle(objectGraph, dataItems, metricsPageInformation, metricsLocationTracker, context) { + if (!appPromotionsCommon.appEventsAreEnabled(objectGraph)) { + return null; + } + if (serverData.isNullOrEmpty(dataItems)) { + return null; + } + const metricsOptions = { + pageInformation: metricsPageInformation, + locationTracker: metricsLocationTracker, + }; + const displayableAppEvents = appPromotionsCommon.appPromotionsFromData(objectGraph, dataItems, null, false, false, metricsOptions, true, true, false, context.allowUnpublishedAppEventPreviews); + if (isSome(displayableAppEvents.nextAppEventPromotionStartDate) && isSome(context === null || context === void 0 ? void 0 : context.refreshController)) { + refresh.addNextPreferredContentRefreshDate(displayableAppEvents.nextAppEventPromotionStartDate, context === null || context === void 0 ? void 0 : context.refreshController); + } + const appEvents = displayableAppEvents.appPromotions; + if (appEvents.length === 0) { + return null; + } + const shelf = appEventsShelf(objectGraph, appEvents, undefined); + // Items should be centered in the article context. + shelf.isHorizontal = false; + // We don't really need this shelf impression, but without it the contained items + // won't be impressed as it is a horizontal shelf + const shelfMetricsOptions = { + ...metricsOptions, + id: "", + kind: null, + softwareType: null, + targetType: "swoosh", + title: "", + }; + metricsHelpersImpressions.addImpressionFields(objectGraph, shelf, shelfMetricsOptions); + return shelf; +} +/** + * Convenience function for creating a shelf from a list of appEvents + * @param objectGraph + * @param appEvents The array of app events + * @param title The title for the shelf + */ +function appEventsShelf(objectGraph, appEvents, title) { + const shelfType = "appPromotion"; + const shelf = new models.Shelf(shelfType); + shelf.isHorizontal = true; + shelf.title = title; + shelf.items = appEvents; + return shelf; +} +// endregion +//# sourceMappingURL=app-promotions-shelf.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/app-promotions/contingent-offer-detail.js b/node_modules/@jet-app/app-store/tmp/src/common/app-promotions/contingent-offer-detail.js new file mode 100644 index 0000000..64e75db --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/app-promotions/contingent-offer-detail.js @@ -0,0 +1,106 @@ +import * as models from "../../api/models"; +import * as serverData from "../../foundation/json-parsing/server-data"; +import * as color from "../../foundation/util/color-util"; +import * as objects from "../../foundation/util/objects"; +import * as metricsBuilder from "../metrics/builder"; +import * as metricsHelpersLocation from "../metrics/helpers/location"; +import * as metricsHelpersPage from "../metrics/helpers/page"; +import { MetricsReferralContext } from "../metrics/metrics-referral-context"; +import * as appPromotionCommon from "./app-promotions-common"; +/** + * Create a flow action for navigating to the contingent offer detail page. + * @param data The data blob + * @param parentAppData The associated parent app data + * @param contingentOffer The source contingent offer + * @param baseMetricsOptions The base metrics options + * @param animationBehavior The animation behavior for presenting the modal page + * @param includeLockupClickAction Whether to generate a click action for the lockup + * @param referrerData Referrer data from an incoming deep link + */ +export function contingentOfferDetailPageFlowActionFromData(objectGraph, data, parentAppData, contingentOffer, baseMetricsOptions, animationBehavior, includeLockupClickAction, referrerData) { + const page = contingentOfferDetailPageFromData(objectGraph, data, parentAppData, contingentOffer, baseMetricsOptions, includeLockupClickAction, referrerData, false); + const action = new models.FlowAction("contingentOfferDetail"); + action.title = contingentOffer.title; + action.pageData = page; + action.animationBehavior = animationBehavior; + if (baseMetricsOptions && baseMetricsOptions.pageInformation) { + action.referrerUrl = baseMetricsOptions.pageInformation.pageUrl; + } + return action; +} +/** + * Creates an contingent offer detail page + * @param objectGraph The object graph + * @param data The data blob + * @param parentAppData The data blob for the related parent app + * @param contingentOffer The source contingent offer + * @param baseMetricsOptions The base metrics options to use for the detail page + * @param includeLockupClickAction Whether to generate a click action for the lockup + * @param referrerData The referrer data + * @param isArcadePage Whether or not this is presented on the Arcade page + */ +export function contingentOfferDetailPageFromData(objectGraph, data, parentAppData, contingentOffer, baseMetricsOptions, includeLockupClickAction, referrerData, isArcadePage) { + var _a, _b; + const artwork = appPromotionCommon.artworkFromPlatformData(objectGraph, data, "productArtwork"); + const copy = objects.shallowCopyOf(contingentOffer); + let mediaOverlayStyle = "dark"; + let isArtworkDark = true; + if (serverData.isDefinedNonNull(artwork)) { + isArtworkDark = color.isDarkColor(artwork.backgroundColor); + mediaOverlayStyle = isArtworkDark ? "dark" : "light"; + } + const pageInformation = metricsHelpersPage.pageInformationForAppPromotionDetailPage(objectGraph, models.AppPromotionType.ContingentOffer, data.id, parentAppData.id, referrerData, (_a = baseMetricsOptions.recoMetricsData) !== null && _a !== void 0 ? _a : null); + const metricsOptions = { + ...baseMetricsOptions, + pageInformation: pageInformation, + locationTracker: metricsHelpersLocation.newLocationTracker(), + }; + const offerEnvironment = isArtworkDark ? "dark" : "light"; + const lockup = appPromotionCommon.lockupFromData(objectGraph, data, parentAppData, (_b = copy.title) !== null && _b !== void 0 ? _b : undefined, offerEnvironment, "transparent", false, metricsOptions, includeLockupClickAction, referrerData, isArcadePage, false); + if (serverData.isNull(lockup)) { + return null; + } + copy.offerLockup = lockup; + copy.trunkAppIcon = contingentOffer.trunkAppIcon; + const page = new models.ContingentOfferDetailPage(copy, artwork !== null && artwork !== void 0 ? artwork : undefined, mediaOverlayStyle); + page.backButtonActionMetrics.addMetricsData(createButtonActionMetricsForTarget(objectGraph, "back", pageInformation, metricsOptions.locationTracker)); + page.learnMoreActionMetrics.addMetricsData(createButtonActionMetricsForTarget(objectGraph, "LearnMore", pageInformation, metricsOptions.locationTracker, contingentOffer.learnMoreTitle)); + page.closeButtonActionMetrics.addMetricsData(createButtonActionMetricsForTarget(objectGraph, "close", pageInformation, metricsOptions.locationTracker)); + metricsHelpersPage.addMetricsEventsToPageWithInformation(objectGraph, page, pageInformation, (fields) => { + if (serverData.isDefinedNonNullNonEmpty(referrerData)) { + MetricsReferralContext.shared.addReferralContextToMetricsFieldsIfNecessary(fields); + } + }); + return page; +} +function createButtonActionMetricsForTarget(objectGraph, targetId, pageInformation, locationTracker, title) { + let actionType; + switch (targetId) { + case "LearnMore": + actionType = "navigate"; + break; + case "back": + actionType = "back"; + break; + case "close": + actionType = "dismiss"; + break; + default: + break; + } + const eventFields = { + targetType: "button", + actionType, + targetId, + idType: undefined, + location: metricsHelpersLocation.createContentLocation(objectGraph, { + pageInformation: pageInformation, + locationTracker: locationTracker, + targetType: "button", + id: targetId, + }, title !== null && title !== void 0 ? title : targetId), + }; + const event = metricsBuilder.createMetricsClickData(objectGraph, targetId, "button", eventFields); + return event; +} +//# sourceMappingURL=contingent-offer-detail.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/app-promotions/contingent-offer.js b/node_modules/@jet-app/app-store/tmp/src/common/app-promotions/contingent-offer.js new file mode 100644 index 0000000..8cc3cd7 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/app-promotions/contingent-offer.js @@ -0,0 +1,189 @@ +import * as models from "../../api/models"; +import * as serverData from "../../foundation/json-parsing/server-data"; +import * as mediaAttributes from "../../foundation/media/attributes"; +import * as mediaRelationship from "../../foundation/media/relationships"; +import * as appPromotionCommon from "./app-promotions-common"; +import * as offerFormatting from "../offers/offer-formatting"; +import * as offers from "../offers/offers"; +import * as color from "../../foundation/util/color-util"; +import { isNothing, isSome } from "@jet/environment/types/optional"; +import { contentAttributeAsDictionary } from "../content/attributes"; +import * as content from "../content/content"; +/** + * Creates a contingent offer object. + * @param data The data blob + * @param parentAppData The related parent app of this app event. If not provided will be derived from `data`. + * @param offerEnvironment The preferred environment for the offer + * @param offerStyle The preferred style of the offer + * @param baseMetricsOptions The base metrics options + * @param allowEndedEvents Whether events in the past are allowed + * @param includeLockupClickAction Whether to include the click action for the lockup + * @param isArcadePage Whether or not this is presented on the Arcade page + * @returns an ContingentOffer or null + */ +export function contingentOfferFromData(objectGraph, data, parentAppData, offerEnvironment, offerStyle, baseMetricsOptions, includeLockupClickAction, isArcadePage) { + var _a, _b, _c; + if (data.type !== "contingent-items") { + return null; + } + // Artwork + const moduleArtwork = appPromotionCommon.artworkFromPlatformData(objectGraph, data, "lockupArtwork"); + if (serverData.isNull(moduleArtwork)) { + return null; + } + const isArtworkDark = color.isDarkColor(moduleArtwork.backgroundColor); + const mediaOverlayStyle = isArtworkDark ? "dark" : "light"; + // Labels + const badge = mediaAttributes.attributeAsString(data, "badge"); + const subtitle = mediaAttributes.attributeAsString(data, "subtitle"); + const label = mediaAttributes.attributeAsString(data, "label"); + const additionalInfoLabel = objectGraph.loc.string("ContingentOffer.AdditionalInfoButton.Title"); + if (isNothing(badge) || isNothing(label)) { + return null; + } + // The discounted offer + const branch = mediaRelationship.relationshipData(objectGraph, data, "branch"); + if (isNothing(branch) || serverData.isNullOrEmpty(serverData.asDictionary(branch, "meta.contingentItemOffer"))) { + return null; + } + const formattedText = formattedTextFromData(objectGraph, data); + if (isNothing(formattedText) || isNothing(formattedText === null || formattedText === void 0 ? void 0 : formattedText.title)) { + return null; + } + // The Apps + const resolvedBranchAppData = mediaRelationship.relationshipData(objectGraph, data, "branch-app"); + const supportsStreamlinedBuy = mediaAttributes.attributeAsBooleanOrFalse(resolvedBranchAppData, "supportsStreamlinedBuy"); + const offerLockup = appPromotionCommon.lockupFromData(objectGraph, data, resolvedBranchAppData, "", offerEnvironment, offerStyle, false, baseMetricsOptions, includeLockupClickAction, null, isArcadePage, true); + if (isNothing(offerLockup)) { + return null; + } + // Trunk Lockup. The app you need to be subscribed to in order to get the discounted offer + const resolvedTrunkAppData = mediaRelationship.relationshipData(objectGraph, data, "trunk-app"); + let trunkAppIcon; + if (isSome(resolvedTrunkAppData) && isSome(subtitle) && (subtitle === null || subtitle === void 0 ? void 0 : subtitle.length) > 0) { + trunkAppIcon = + (_a = appPromotionCommon.artworkFromPlatformData(objectGraph, resolvedTrunkAppData, "artwork")) !== null && _a !== void 0 ? _a : appPromotionCommon.artworkFromData(objectGraph, resolvedTrunkAppData, "artwork"); + } + // When displaying a first party offer the trunk artwork is in a different location + if (isSome(subtitle) && (subtitle === null || subtitle === void 0 ? void 0 : subtitle.length) > 0) { + trunkAppIcon = (_b = firstPartyTrunkLockupFromData(objectGraph, data)) !== null && _b !== void 0 ? _b : trunkAppIcon; + } + // Offer Strings + const additionalInfo = formattedAdditionalInfoFromData(objectGraph, data, branch); + const contingentOffer = new models.ContingentOffer(moduleArtwork, mediaOverlayStyle, supportsStreamlinedBuy, additionalInfoLabel, formattedText.title, subtitle !== null && subtitle !== void 0 ? subtitle : undefined, (_c = formattedText.description) !== null && _c !== void 0 ? _c : undefined, label, badge, additionalInfo, trunkAppIcon !== null && trunkAppIcon !== void 0 ? trunkAppIcon : undefined, offerLockup); + /// The raw title is used for metrics purposes + contingentOffer.title = formattedText.rawTitle; + // Detail page click action + if (serverData.isDefinedNonNull(resolvedBranchAppData)) { + contingentOffer.clickAction = appPromotionCommon.detailPageClickActionFromData(objectGraph, data, resolvedBranchAppData, contingentOffer, baseMetricsOptions, includeLockupClickAction); + } + return contingentOffer; +} +/** + * Creates the Title and description of the contingent offer out of a templated string + * @param data A data blob for the contingent-item + * @returns Creates a formatted string of the tile including the subscription price and period + */ +export function formattedTextFromData(objectGraph, data) { + var _a, _b, _c; + const branch = mediaRelationship.relationshipData(objectGraph, data, "branch"); + if (isNothing(branch)) { + return undefined; + } + const regularOffer = offers.offerDataFromData(objectGraph, branch); + const discountedOffer = serverData.asDictionary(branch, "meta.contingentItemOffer"); + if (serverData.isNullOrEmpty(regularOffer) || serverData.isNullOrEmpty(discountedOffer)) { + return undefined; + } + // Price Strings + // Adding \u{2060} (Word Joiner) prevents the string from breaking on the slash character + const regularPrice = (_a = formattedPrice(objectGraph, serverData.asString(regularOffer, "recurringSubscriptionPeriod"), serverData.asNumber(regularOffer, "numOfPeriods"), serverData.asString(regularOffer, "priceFormatted"))) === null || _a === void 0 ? void 0 : _a.replace("/", "/\u{2060}"); + const discountedPrice = (_b = formattedPrice(objectGraph, serverData.asString(discountedOffer, "recurringSubscriptionPeriod"), serverData.asNumber(discountedOffer, "numOfPeriods"), serverData.asString(discountedOffer, "priceFormatted"))) === null || _b === void 0 ? void 0 : _b.replace("/", "/\u{2060}"); + if (serverData.isNullOrEmpty(regularPrice) || serverData.isNullOrEmpty(discountedPrice)) { + return null; + } + const title = mediaAttributes.attributeAsString(data, "name"); + const titleKeys = { + "@@discountedPrice@@/@@recurringSubscriptionPeriod@@": discountedPrice, + "@@regularPrice@@/@@recurringSubscriptionPeriod@@": regularPrice, + "@@discountedPricePerRecurringSubscriptionPeriod@@": discountedPrice, + "@@regularPricePerRecurringSubscriptionPeriod@@": regularPrice, + }; + let titleTemplate = title !== null && title !== void 0 ? title : ""; + Object.keys(titleKeys).forEach((element) => { + titleTemplate = titleTemplate.replace(element, titleKeys[element]); + }); + const htmlRegex = /<(?:"[^"]*"['"]*|'[^']*'['"]*|[^'">])+>/g; + const rawTitle = titleTemplate.replace(htmlRegex, ""); + const formattedTitle = new models.Paragraph(titleTemplate, "text/x-apple-as3-nqml", "appPromotionTitle"); + const developerDetailText = (_c = mediaAttributes.attributeAsString(data, "description.standard")) !== null && _c !== void 0 ? _c : ""; + const detailTemplate = objectGraph.loc.string("ContingentOffer.Description.Format"); + const descriptionKeys = { + "@@BranchName@@": mediaAttributes.attributeAsString(branch, "name"), + "@@RegularPrice@@": regularPrice, + "@@DiscountedPrice@@": discountedPrice, + }; + let description = detailTemplate; + Object.keys(descriptionKeys).forEach((element) => { + description = description.replace(element, descriptionKeys[element]); + }); + const formattedDescription = [description, developerDetailText].join(" "); + return { + title: formattedTitle, + rawTitle, + description: formattedDescription, + }; +} +/** + * Creates a readable subscription price + * @param recurringSubscriptionPeriod The subscription recurrence in the server format. (eg P1M) + * @param price A price string in a readable format like $3.99. + * @returns A formatted string eg $3.99/month + */ +function formattedPrice(objectGraph, recurringSubscriptionPeriod, numberOfPeriods, price) { + const subscriptionRecurrence = offerFormatting.subscriptionRecurrenceForServerRecurrence(objectGraph, recurringSubscriptionPeriod, numberOfPeriods); + if (isNothing(subscriptionRecurrence) || isNothing(price)) { + return undefined; + } + return offerFormatting.priceDurationString(objectGraph, subscriptionRecurrence.type, subscriptionRecurrence.periodDuration, price); +} +/** + * Creates the additional info text of the contingent offer out of a templated string + * @param data A data blob + * @param branch A data blob containing the offer also known as the branch + * @param trunk A data blob containing the required subscription app also known as the trunk + * @returns Creates a formatted paragraph containing the contents of the additional info page. + */ +function formattedAdditionalInfoFromData(objectGraph, data, branch) { + const skuNameTitle = "<b>" + mediaAttributes.attributeAsString(branch, "name") + "</b>"; + const skuDescription = mediaAttributes.attributeAsString(branch, "description.standard") + "<br>"; + const termsTitle = "<b>" + objectGraph.loc.string("ContingentOffer.Terms.Title") + "</b>"; + const terms = mediaAttributes.attributeAsString(data, "additionalTerms"); + return new models.Paragraph([skuNameTitle, skuDescription, termsTitle, terms].join("<br>"), "text/x-apple-as3-nqml"); +} +/** + * If the contingent offer has a an associated trunk use that instead of the trunk-app + * @param data The data blob + * @returns Creates a lockup to use as the TrunkLockup + */ +function firstPartyTrunkLockupFromData(objectGraph, data) { + var _a, _b; + const firstPartyTrunkData = serverData.asInterface(data, "meta.associations.trunks"); + if (isNothing(firstPartyTrunkData)) { + return null; + } + const firstPartyApp = firstPartyTrunkData.data[0]; + const shouldUseTrunkArtwork = (_b = (_a = firstPartyApp === null || firstPartyApp === void 0 ? void 0 : firstPartyApp.meta) === null || _a === void 0 ? void 0 : _a["useTrunkArtwork"]) !== null && _b !== void 0 ? _b : false; + if (shouldUseTrunkArtwork) { + const editorialArtwork = contentAttributeAsDictionary(objectGraph, firstPartyApp, "editorialArtwork.brandLogo"); + if (isNothing(editorialArtwork)) { + return null; + } + return content.artworkFromApiArtwork(objectGraph, editorialArtwork, { + useCase: 1 /* content.ArtworkUseCase.LockupIconSmall */, + style: "roundedRect", + }); + } + return null; +} +//# sourceMappingURL=contingent-offer.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/app-promotions/offer-item-detail.js b/node_modules/@jet-app/app-store/tmp/src/common/app-promotions/offer-item-detail.js new file mode 100644 index 0000000..ef400cb --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/app-promotions/offer-item-detail.js @@ -0,0 +1,147 @@ +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 mediaRelationship from "../../foundation/media/relationships"; +import * as mediaAttributes from "../../foundation/media/attributes"; +import * as objects from "../../foundation/util/objects"; +import * as metricsBuilder from "../metrics/builder"; +import * as metricsHelpersLocation from "../metrics/helpers/location"; +import * as metricsHelpersPage from "../metrics/helpers/page"; +import { MetricsReferralContext } from "../metrics/metrics-referral-context"; +import * as appPromotionCommon from "./app-promotions-common"; +/** + * Create a flow action for navigating to the offer detail page. + * @param data The data blob + * @param parentAppData The associated parent app data + * @param offerItem The source offer object + * @param baseMetricsOptions The base metrics options + * @param animationBehavior The animation behavior for presenting the modal page + * @param includeLockupClickAction Whether to generate a click action for the lockup + * @param referrerData Referrer data from an incoming deep link + */ +export function offerItemDetailPageFlowActionFromData(objectGraph, data, parentAppData, offerItem, baseMetricsOptions, animationBehavior, includeLockupClickAction, referrerData) { + const page = offerItemDetailPageFromData(objectGraph, data, parentAppData, offerItem, baseMetricsOptions, includeLockupClickAction, referrerData, false); + const action = new models.FlowAction("offerItemDetail"); + action.title = offerItem.title; + action.pageData = page; + action.animationBehavior = animationBehavior; + if (baseMetricsOptions && baseMetricsOptions.pageInformation) { + action.referrerUrl = baseMetricsOptions.pageInformation.pageUrl; + } + return action; +} +/** + * Creates an offer item detail page + * @param objectGraph The object graph + * @param data The data blob + * @param parentAppData The data blob for the related parent app + * @param offerItem The source offer + * @param baseMetricsOptions The base metrics options to use for the detail page + * @param includeLockupClickAction Whether to generate a click action for the lockup + * @param referrerData The referrer data + * @param isArcadePage Whether or not this is presented on the Arcade page + */ +export function offerItemDetailPageFromData(objectGraph, offerItemData, parentAppData, offerItem, baseMetricsOptions, includeLockupClickAction, referrerData, isArcadePage) { + var _a, _b; + let artwork = appPromotionCommon.artworkFromPlatformData(objectGraph, offerItemData, "productArtwork"); + // rdar://126775681 (Remove force moduleArtwork to be null as MAPI still sends down artwork when it should not) + // The below `if` check should be uncommented when this is removed - it was commented as strict null checking is smart enough to know that `artwork` will always be null. + artwork = null; + const mediaOverlayStyle = "dark"; + const isArtworkDark = true; + const includeBorderInDarkMode = false; + // if (serverData.isDefinedNonNullNonEmpty(artwork)) { + // isArtworkDark = color.isDarkColor(artwork?.backgroundColor); + // mediaOverlayStyle = isArtworkDark ? "dark" : "light"; + // includeBorderInDarkMode = color.isDarkColor(artwork?.backgroundColor, 10); + // } + const copy = objects.shallowCopyOf(offerItem); + const pageInformation = metricsHelpersPage.pageInformationForAppPromotionDetailPage(objectGraph, models.AppPromotionType.OfferItem, offerItemData.id, parentAppData.id, referrerData, (_a = baseMetricsOptions.recoMetricsData) !== null && _a !== void 0 ? _a : null); + const metricsOptions = { + ...baseMetricsOptions, + pageInformation: pageInformation, + locationTracker: metricsHelpersLocation.newLocationTracker(), + }; + const offerEnvironment = isArtworkDark ? "dark" : "light"; + const lockup = appPromotionCommon.lockupFromData(objectGraph, offerItemData, parentAppData, (_b = copy.title) !== null && _b !== void 0 ? _b : undefined, offerEnvironment, "transparent", false, metricsOptions, includeLockupClickAction, referrerData, isArcadePage, false); + if (serverData.isNull(lockup)) { + return null; + } + copy.offerLockup = lockup; + const additionalInfoLabel = objectGraph.loc.string("Winback.AdditionalInfoButton.Title"); + const iapData = mediaRelationship.relationshipData(objectGraph, offerItemData, "salables"); + const redemptionExpirationDate = mediaAttributes.attributeAsString(offerItemData, "redemptionExpirationDate"); + let additionalInfo; + if (isSome(redemptionExpirationDate) && isSome(iapData)) { + const endDate = new Date(redemptionExpirationDate); + additionalInfo = formattedAdditionalInfoFromData(objectGraph, offerItemData, iapData, endDate); + } + const page = new models.OfferItemDetailPage(copy, artwork !== null && artwork !== void 0 ? artwork : undefined, undefined, mediaOverlayStyle, includeBorderInDarkMode, additionalInfoLabel, additionalInfo); + page.backButtonActionMetrics.addMetricsData(createButtonActionMetricsForTarget(objectGraph, "back", pageInformation, metricsOptions.locationTracker)); + page.learnMoreActionMetrics.addMetricsData(createButtonActionMetricsForTarget(objectGraph, "LearnMore", pageInformation, metricsOptions.locationTracker, additionalInfoLabel)); + page.closeButtonActionMetrics.addMetricsData(createButtonActionMetricsForTarget(objectGraph, "close", pageInformation, metricsOptions.locationTracker)); + metricsHelpersPage.addMetricsEventsToPageWithInformation(objectGraph, page, pageInformation, (fields) => { + if (serverData.isDefinedNonNullNonEmpty(referrerData)) { + MetricsReferralContext.shared.addReferralContextToMetricsFieldsIfNecessary(fields); + } + }); + return page; +} +function createButtonActionMetricsForTarget(objectGraph, targetId, pageInformation, locationTracker, title) { + let actionType; + switch (targetId) { + case "LearnMore": + actionType = "navigate"; + break; + case "back": + actionType = "back"; + break; + case "close": + actionType = "dismiss"; + break; + default: + break; + } + const eventFields = { + targetType: "button", + actionType, + targetId, + idType: undefined, + location: metricsHelpersLocation.createContentLocation(objectGraph, { + pageInformation: pageInformation, + locationTracker: locationTracker, + targetType: "button", + id: targetId, + }, title !== null && title !== void 0 ? title : targetId), + }; + const event = metricsBuilder.createMetricsClickData(objectGraph, targetId, "button", eventFields); + return event; +} +/** + * Creates the additional info text of the promotion out of a templated string + * @param data A data blob + * @param iapData A data blob containing the iap offer + * @returns Creates a formatted paragraph containing the contents of the additional info page. + */ +function formattedAdditionalInfoFromData(objectGraph, data, iapData, endDate) { + if (isNothing(iapData)) { + return undefined; + } + const skuName = mediaAttributes.attributeAsString(iapData, "name"); + const skuNameFormatted = isSome(skuName) ? "<b>" + skuName + "</b>" : undefined; + const skuDescription = mediaAttributes.attributeAsString(iapData, "description.standard"); + const skuDescriptionFormatted = isSome(skuDescription) ? skuDescription + "<br>" : undefined; + const termsTitle = "<b>" + objectGraph.loc.string("Promotion.Terms.Title") + "</b>"; + let terms; + const redemptionDateFormat = objectGraph.loc.string("OfferItems.FormattedDate.RedemptionDate.DateFormat"); + if (isSome(skuName) && isSome(endDate)) { + const templateKeyMap = { + "@@redemptionDate@@": objectGraph.loc.formatDate(redemptionDateFormat, endDate), + "@@skuName@@": skuName, + }; + terms = appPromotionCommon.replacingTemplatedKeys(mediaAttributes.attributeAsString(data, "additionalTerms"), templateKeyMap); + } + const combinedStrings = [skuNameFormatted, skuDescriptionFormatted, termsTitle, terms].filter(isSome).join("<br>"); + return new models.Paragraph(combinedStrings, "text/x-apple-as3-nqml"); +} +//# sourceMappingURL=offer-item-detail.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/app-promotions/offer-item.js b/node_modules/@jet-app/app-store/tmp/src/common/app-promotions/offer-item.js new file mode 100644 index 0000000..989ab6d --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/app-promotions/offer-item.js @@ -0,0 +1,206 @@ +import * as models from "../../api/models"; +import * as serverData from "../../foundation/json-parsing/server-data"; +import * as mediaAttributes from "../../foundation/media/attributes"; +import * as mediaRelationship from "../../foundation/media/relationships"; +import * as appPromotionCommon from "./app-promotions-common"; +import * as dateUtil from "../../foundation/util/date-util"; +import { isNothing, isSome } from "@jet/environment/types/optional"; +import * as offerFormatting from "../offers/offer-formatting"; +import * as offers from "../offers/offers"; +import * as content from "../content/content"; +import * as contentArtwork from "../content/artwork/artwork"; +/** + * Creates an offer item object. + * @param offerItemData The data blob + * @param parentAppData The related parent app of this app event. If not provided will be derived from `data`. + * @param offerEnvironment The preferred environment for the offer + * @param offerStyle The preferred style of the offer + * @param baseMetricsOptions The base metrics options + * @param allowEndedEvents Whether events in the past are allowed + * @param includeLockupClickAction Whether to include the click action for the lockup + * @param isArcadePage Whether or not this is presented on the Arcade page + * @returns an Offer Item or null + */ +export function offerItemFromData(objectGraph, offerItemData, parentAppData, offerEnvironment, offerStyle, baseMetricsOptions, includeLockupClickAction, isArcadePage) { + var _a, _b, _c, _d, _e, _f; + if (offerItemData.type !== "offer-items") { + return null; + } + const kind = mediaAttributes.attributeAsString(offerItemData, "kind"); + if (kind !== "resubscription" && kind !== "winback") { + // Only resubscriptions are supported + return null; + } + const isArtworkDark = false; + const mediaOverlayStyle = isArtworkDark ? "dark" : "light"; + // The Offers + const discountedIAP = mediaRelationship.relationshipData(objectGraph, offerItemData, "salables"); + if (isNothing(discountedIAP)) { + return null; + } + const discountedOffer = serverData.asDictionary(discountedIAP, "meta.discountOffer"); + const regularOffer = offers.offerDataFromData(objectGraph, discountedIAP); + if (isNothing(discountedOffer)) { + return null; + } + const resolvedParentApp = (_a = parentAppData !== null && parentAppData !== void 0 ? parentAppData : mediaRelationship.relationshipData(objectGraph, offerItemData, "app")) !== null && _a !== void 0 ? _a : mediaRelationship.relationshipData(objectGraph, discountedIAP, "app"); + const supportsStreamlinedBuy = mediaAttributes.attributeAsBooleanOrFalse(resolvedParentApp, "supportsStreamlinedBuy"); + // Offer Validity + const endDateString = mediaAttributes.attributeAsString(offerItemData, "redemptionExpirationDate"); + if (isNothing(endDateString) || !isOfferValid(endDateString)) { + return null; + } + const endDate = new Date(endDateString); + let formattedExpiryDateShortString = formattedExpiryDate(objectGraph, endDate); + if (isNothing(formattedExpiryDateShortString)) { + formattedExpiryDateShortString = objectGraph.loc.string("OfferItems.Available.Now"); + } + // IAP Artwork + let iapArtwork = content.iconFromData(objectGraph, discountedIAP, { + useCase: 3 /* content.ArtworkUseCase.LockupIconLarge */, + withJoeColorPlaceholder: true, + overrideTextColorKey: "textColor4", + }); + /// A fallback artwork for rare cases where no artwork is found. + /// This should not happen in production + if (serverData.isNullOrEmpty(iapArtwork)) { + const blackColor = { + type: "rgb", + red: 0.0 / 255, + green: 0.0 / 255, + blue: 0.0 / 255, + alpha: 1, + }; + iapArtwork = contentArtwork.createArtworkForResource(objectGraph, "systemimage://questionmark.circle", 200, 200, blackColor); + iapArtwork.style = "iap"; + } + const subscriptionRecurrence = offerFormatting.subscriptionRecurrenceForServerRecurrence(objectGraph, serverData.asString(discountedOffer, "recurringSubscriptionPeriod"), serverData.asNumber(discountedOffer, "numOfPeriods")); + const discountDuration = isSome(subscriptionRecurrence) + ? offerFormatting.durationCountString(objectGraph, subscriptionRecurrence.type, subscriptionRecurrence.periodDuration * subscriptionRecurrence.periodCount) + : undefined; + // Price Strings + // Adding \u{2060} (Word Joiner) prevents the string from breaking on the slash character + const regularPrice = (_b = formattedPrice(objectGraph, serverData.asString(regularOffer, "recurringSubscriptionPeriod"), serverData.asNumber(regularOffer, "numOfPeriods"), serverData.asString(regularOffer, "priceFormatted"))) === null || _b === void 0 ? void 0 : _b.replace("/", "/\u{2060}"); + const discountedPrice = (_c = formattedPrice(objectGraph, serverData.asString(discountedOffer, "recurringSubscriptionPeriod"), serverData.asNumber(discountedOffer, "numOfPeriods"), serverData.asString(discountedOffer, "priceFormatted"))) === null || _c === void 0 ? void 0 : _c.replace("/", "/\u{2060}"); + /// A string map of the templated string in an offer + /// Fallback to the templated string if the string does not exist + const redemptionDateFormat = objectGraph.loc.string("OfferItems.FormattedDate.RedemptionDate.DateFormat"); + const templateKeyMap = { + "@@redemptionDate@@": objectGraph.loc.formatDate(redemptionDateFormat, endDate), + "@@skuName@@": (_d = mediaAttributes.attributeAsString(discountedIAP, "name")) !== null && _d !== void 0 ? _d : "@@skuName@@", + "@@discountedPrice@@": discountedPrice !== null && discountedPrice !== void 0 ? discountedPrice : "@@discountedPrice@@", + "@@regularPricePerDuration@@": regularPrice !== null && regularPrice !== void 0 ? regularPrice : "@@regularPricePerDuration@@", + "@@discountDuration@@": discountDuration !== null && discountDuration !== void 0 ? discountDuration : "@@discountDuration@@", + "@@payUpfrontPrice@@": (_e = serverData.asString(discountedOffer, "priceFormatted")) !== null && _e !== void 0 ? _e : "@@payUpfrontPrice@@", + }; + // Labels + const badge = mediaAttributes.attributeAsString(offerItemData, "badge"); + if (isNothing(badge)) { + return null; + } + const title = appPromotionCommon.replacingTemplatedKeys(mediaAttributes.attributeAsString(offerItemData, "title"), templateKeyMap); + const formattedTitle = new models.Paragraph(title, "text/x-apple-as3-nqml", "appPromotionTitle"); + const subtitle = appPromotionCommon.replacingTemplatedKeys(mediaAttributes.attributeAsString(offerItemData, "subtitle"), templateKeyMap); + const description = appPromotionCommon.replacingTemplatedKeys((_f = mediaAttributes.attributeAsString(offerItemData, "details")) !== null && _f !== void 0 ? _f : "", templateKeyMap); + // Offer Lockup + let offerLockup; + if (serverData.isDefinedNonNull(resolvedParentApp)) { + offerLockup = appPromotionCommon.lockupFromData(objectGraph, offerItemData, resolvedParentApp, "", offerEnvironment, offerStyle, false, baseMetricsOptions, includeLockupClickAction, null, isArcadePage, true); + } + if (isNothing(offerLockup)) { + return null; + } + const offerItem = new models.OfferItem(null, null, mediaOverlayStyle, supportsStreamlinedBuy, formattedTitle, formattedExpiryDateShortString, subtitle, description, badge, endDate, iapArtwork, offerLockup); + offerItem.title = title; + // Detail page click action + if (serverData.isDefinedNonNull(resolvedParentApp)) { + offerItem.clickAction = appPromotionCommon.detailPageClickActionFromData(objectGraph, offerItemData, resolvedParentApp, offerItem, baseMetricsOptions, includeLockupClickAction); + } + return offerItem; +} +/** + * Creates a readable subscription price + * @param recurringSubscriptionPeriod The subscription recurrence in the server format. (eg P1M) + * @param numberOfPeriods The number of periods the subscription recurs for. + * @param price A price string in a readable format like $3.99. + * @returns A formatted string eg $3.99/month + */ +function formattedPrice(objectGraph, recurringSubscriptionPeriod, numberOfPeriods, price) { + const subscriptionRecurrence = offerFormatting.subscriptionRecurrenceForServerRecurrence(objectGraph, recurringSubscriptionPeriod, numberOfPeriods); + if (isNothing(subscriptionRecurrence) || isNothing(price)) { + return null; + } + return offerFormatting.priceDurationString(objectGraph, subscriptionRecurrence === null || subscriptionRecurrence === void 0 ? void 0 : subscriptionRecurrence.type, subscriptionRecurrence === null || subscriptionRecurrence === void 0 ? void 0 : subscriptionRecurrence.periodDuration, price); +} +/** + * Checks the validity of a app promotion based on end date + * @param endDateString A string representation of the end date + * @returns bool of if the offer is valid or not + */ +function isOfferValid(endDateString) { + if (isNothing(endDateString) || serverData.isNullOrEmpty(endDateString)) { + return false; + } + const endDate = new Date(endDateString); + if (serverData.isNull(endDate)) { + return false; + } + const todayDate = new Date(); + const hasEventEnded = endDate.getTime() <= todayDate.getTime(); + if (hasEventEnded) { + return false; + } + return true; +} +/** + * Creates the availability label for a app promotion + * @param startDate The start date + * @param endDate The end date + * @returns string of the format the date should be shown + */ +function formattedExpiryDate(objectGraph, endDate) { + if (isNothing(endDate)) { + return null; + } + const endMidnight = dateUtil.convertLocalDateToLocalMidnight(endDate); + const currentDate = new Date(); + const daysLeft = dateUtil.numberOfDaysBetween(currentDate, endMidnight); + if (isNothing(daysLeft)) { + return null; + } + const isSameDate = dateUtil.areLocalDatesSameDay(currentDate, endDate); + if (daysLeft > 90) { + return objectGraph.loc.string("OfferItems.Available.Now"); + } + // Event ends 5+ days from now + // Example: EXPIRES DEC 31 + if (daysLeft > 5) { + const expiryDateFormat = currentDate.getFullYear() !== endDate.getFullYear() + ? objectGraph.loc.string("OfferItems.FormattedDate.NextYear.DateFormat") + : objectGraph.loc.string("OfferItems.FormattedDate.FiveDaysOrMore.DateFormat"); + const expiryDateString = objectGraph.loc.uppercased(objectGraph.loc.formatDate(expiryDateFormat, endDate)); + if (isNothing(expiryDateString)) { + return null; + } + return objectGraph.loc + .string("OfferItems.FormattedDate.FiveDaysOrMore.Title") + .replace("@@date@@", expiryDateString); + } + // Event ends 2-5 days from now + // Example: EXPIRES IN 3 Days + if (daysLeft > 1) { + const fiveDaysPriorDateFormat = objectGraph.loc + .string("OfferItems.FormattedDate.FiveDaysOrLess.Title") + .replace("@@count@@", objectGraph.loc.formattedCount(daysLeft)); + return fiveDaysPriorDateFormat; + } + // Event ends 1 day from now + // Example: EXPIRES TOMORROW + if (daysLeft === 1 && !isSameDate) { + return objectGraph.loc.string("OfferItems.FormattedDate.Tomorrow.Title"); + } + // Event ends 1 day from now + // Example: EXPIRES TONIGHT + return objectGraph.loc.string("OfferItems.FormattedDate.Today.Title"); +} +//# sourceMappingURL=offer-item.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/arcade/arcade-common.js b/node_modules/@jet-app/app-store/tmp/src/common/arcade/arcade-common.js new file mode 100644 index 0000000..440d350 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/arcade/arcade-common.js @@ -0,0 +1,441 @@ +/** + * Created by ls on 5/15/18. + */ +import { Color, isSome } from "@jet/environment"; +import * as models from "../../api/models/index"; +import * as serverData from "../../foundation/json-parsing/server-data"; +import { isDefinedNonNull } from "../../foundation/json-parsing/server-data"; +import * as mediaFetching from "../../foundation/media/data-fetching"; +import * as mediaNetwork from "../../foundation/media/network"; +import * as mediaRelationships from "../../foundation/media/relationships"; +import { Path, Protocol } from "../../foundation/network/url-constants"; +import * as urls from "../../foundation/network/urls"; +import * as artworkBuilder from "../content/artwork/artwork"; +import * as metricsHelpersClicks from "../metrics/helpers/clicks"; +import * as productPageVariants from "../product-page/product-page-variants"; +import { MetricsIdentifierType } from "../../foundation/metrics/metrics-identifiers-cache"; +import { named } from "../../foundation/util/color-util"; +import { makeRoutableArcadeSeeAllPageIntent } from "../../api/intents/routable-arcade-see-all-page-intent"; +import { getPlatform } from "../preview-platform"; +import { getLocale } from "../locale"; +import { makeArcadeSeeAllCanonicalUrl } from "./arcade-see-all-routing"; +import { shouldUsePrerenderedIconArtwork } from "../content/content"; +// endregion +// region Arcade Navigation Actions +/** + * Creates a flow action for going to the page to see all Arcade games. + * Defaults to sorting by release date (descending) + * @param {ArcadeSeeAllGamesPageSort} sort The order which the games in response will be sorted in. + * @param metricsPageInformation + * @param metricsLocationTracker + * @returns {FlowAction} Flow action to Arcade see all games page. + */ +export function seeAllArcadeGamesPageFlowAction(objectGraph, sort = "releaseDate", metricsPageInformation, metricsLocationTracker, title = undefined, id = undefined, idType = undefined, targetType = "button") { + const seeAllGamesUrl = urls.URL.fromComponents(Protocol.internal, null, `/${Path.arcadeSeeAllGames}`, { + sort: sort, + }); + const flowAction = new models.FlowAction("arcadeSeeAllGames", seeAllGamesUrl.build()); + flowAction.title = title !== null && title !== void 0 ? title : objectGraph.loc.string("Arcade.SeeAllGames.Button.Title"); + if (objectGraph.client.isWeb) { + const destination = makeRoutableArcadeSeeAllPageIntent({ + ...getLocale(objectGraph), + ...getPlatform(objectGraph), + }); + const pageUrl = makeArcadeSeeAllCanonicalUrl(objectGraph, destination); + flowAction.destination = destination; + flowAction.pageUrl = pageUrl; + } + const itemId = id !== null && id !== void 0 ? id : (objectGraph.client.isVision ? "SeeAllGames" : "arcade-see-all-games-button"); + const seeAllClickOptions = { + id: itemId, + idType: idType, + targetType: targetType, + actionType: "navigate", + actionContext: "Arcade", + pageInformation: metricsPageInformation, + locationTracker: metricsLocationTracker, + }; + metricsHelpersClicks.addClickEventToAction(objectGraph, flowAction, seeAllClickOptions); + return flowAction; +} +/** + * Create a flow action for opening the Arcade Subscribe page with optional parameters + * @param context The context in which the Arcade Subscribe page is being opened for, e.g. where the flow action was initiated from. + * @param contextualAppId Optional app ID to associate with flow, if any. This is used for flow into contextual upsell sheet. + * @returns {FlowAction} Flow action to `arcadeSubscribe` page. + */ +export function arcadeSubscribePageFlowAction(objectGraph, context, contextualAppId, purchaseSuccessAction, options) { + var _a, _b, _c, _d; + const upsellRequestInfo = new models.MarketingItemRequestInfo("arcade", context, objectGraph.bag.metricsTopic, contextualAppId); + upsellRequestInfo.purchaseSuccessAction = purchaseSuccessAction; + upsellRequestInfo.carrierLinkSuccessAction = purchaseSuccessAction; + const action = new models.FlowAction("upsellMarketingItem"); + if (isSome((_b = (_a = options === null || options === void 0 ? void 0 : options.pageInformation) === null || _a === void 0 ? void 0 : _a.searchTermContext) === null || _b === void 0 ? void 0 : _b.term)) { + upsellRequestInfo.metricsOverlay["searchTerm"] = (_c = options.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, + ]); + if (isSome(metricsIdentifierFields)) { + upsellRequestInfo.metricsOverlay = { + ...upsellRequestInfo.metricsOverlay, + ...metricsIdentifierFields, + }; + } + action.pageData = upsellRequestInfo; + if (serverData.isDefinedNonNull(options)) { + metricsHelpersClicks.addClickEventToArcadeBuyInitiateAction(objectGraph, action, options); + } + return action; +} +/** + * Action to open the main Arcade page on each platform. + * Depending on the platform, this can be a tab change action or open action to separate Arcade app. + */ +export function openArcadeMainAction(objectGraph, metricsPageInformation, metricsLocationTracker, popToRoot) { + if (objectGraph.client.isTV) { + return openTVArcadeAppAction(objectGraph); + } + else { + const arcadeTabChangeAction = new models.TabChangeAction("arcade"); + if (serverData.isDefinedNonNull(popToRoot)) { + arcadeTabChangeAction.popToRoot = popToRoot; + } + /* + * Presidio / Yukon timeframe workaround for <rdar://problem/53600942> Allow deserialized TabChangeAction to have use `title` property from JS instead of always using `nil` + * We're wrapping a single `TabChangeAction` within `CompoundAction` since `TabChangeAction` deserialization drops the JS provided title. + * + * Tracking removing this workaround in: + * <rdar://problem/53601182> Arcade: Remove workaround for having a tab change action with a title + */ + return new models.CompoundAction([arcadeTabChangeAction]); + } +} +/** + * Creates an action to open Arcade app on tvOS. + */ +export function openTVArcadeAppAction(objectGraph) { + const url = "com.apple.Arcade://"; + return new models.ExternalUrlAction(url); +} +// endregion +/** + * Creates an action to open GamesUI. + */ +export function openGamesUIAction(objectGraph, target = { playNow: {} }) { + return new models.OpenGamesUIAction(target); +} +/** + * Creates Game Center header. + */ +export function makeGameCenterHeader(objectGraph, title = undefined, subtitle = undefined, useTitleArtwork = undefined) { + let eyebrowArtwork; + if (objectGraph.client.isTV) { + eyebrowArtwork = artworkBuilder.createArtworkForResource(objectGraph, "systemimage://gamecenter.fill", 16, 16); + } + else { + eyebrowArtwork = artworkBuilder.createArtworkForResource(objectGraph, "resource://GameCenterEyebrow", 16, 16); + } + const isShelfHeaderEnabled = objectGraph.featureFlags.isEnabled("shelf_header"); + const isGameCenterShelfHeaderEnabled = objectGraph.featureFlags.isEnabled("game_center_shelf_header"); + const isGSEUIEnabled = objectGraph.featureFlags.isGSEUIEnabled("de7bbd8e"); + if (isGSEUIEnabled) { + const configuration = { + eyebrowColor: named("secondaryText"), + includeSeparator: !isShelfHeaderEnabled, + prefersShelfHeader: isGameCenterShelfHeaderEnabled, + }; + const eyebrow = objectGraph.loc.string("GAME_CENTER"); + const shelfHeader = { + eyebrow: eyebrow, + eyebrowArtwork: eyebrowArtwork, + eyebrowArtworkType: models.ShelfHeaderArtworkType.Icon, + title: title, + subtitle: subtitle, + configuration: configuration, + }; + return shelfHeader; + } + else { + const configuration = { + eyebrowColor: isGSEUIEnabled ? named("systemBlue") : undefined, + includeSeparator: !isShelfHeaderEnabled, + prefersShelfHeader: isGameCenterShelfHeaderEnabled, + }; + if (isSome(useTitleArtwork) && useTitleArtwork) { + const shelfHeader = { + title: title, + titleArtwork: eyebrowArtwork, + titleArtworkType: models.ShelfHeaderArtworkType.Icon, + subtitle: subtitle, + configuration: configuration, + }; + return shelfHeader; + } + else { + const eyebrow = objectGraph.loc.uppercased(objectGraph.loc.string("GAME_CENTER")); + const shelfHeader = { + eyebrow: eyebrow, + eyebrowArtwork: eyebrowArtwork, + eyebrowArtworkType: models.ShelfHeaderArtworkType.Icon, + title: title, + subtitle: subtitle, + configuration: configuration, + }; + return shelfHeader; + } + } +} +// region Arcade Catalog MAPI Requests +/** + * Base request for all MAPI requests fetching a set of Arcade games from catalog. + * This request has a server-defined implicit limit value (e.g. 100). + * This request should be bare-bones. If additional attributes are needed, it should be chained to this request. + * + * @returns {mediaFetching.Request} Request object for fetching Arcade apps from MAPI catalog endpoint. + */ +export function arcadeAppsRequest(objectGraph, includeComingSoon = false) { + let request = new mediaFetching.Request(objectGraph).forType("arcade-apps").includingAgeRestrictions(); + if (includeComingSoon) { + request = request.addingQuery("with", "comingSoonApps"); + } + // For visionOS, we need icons for bincompat games. + if (objectGraph.client.isVision) { + request = request.includingAdditionalPlatforms(["iphone", "ipad"]); + request.attributeIncludes.add("compatibilityControllerRequirement"); + } + return request; +} +/** + * Request for fetching a set of arcade apps for displaying a set of Arcade Icons, e.g. for Arcade Grouping Footer, Contextual Upsell Icon Grid, and iOS Arcade Showcase. + * This request is a very special request with MAPI data containing only `artwork` attribute. Data is meant to be used *as-is* for showing a set of icons only. + * + * Requirements: + * - Icon Artwork for apps only. Additional metadata should be pruned if possible. + * + * @param limit Limit of apps. This should be configured to fit the view's needs. + */ +export function arcadeAppsRequestForIcons(objectGraph, limit) { + return arcadeAppsRequest(objectGraph) + .withSparseLimit(limit) + .asPartialResponseLimitedToFields(["artwork"]) + .usingCustomAttributes(productPageVariants.shouldFetchCustomAttributes(objectGraph)); +} +// endregion +// region Arcade Upsell Request +export function arcadeUpsellRequest(objectGraph, context, contextualAppId) { + return arcadeUpsellMarketingItemRequest(objectGraph, context, contextualAppId); +} +export function arcadeUpsellMarketingItemRequest(objectGraph, context, contextualAppId) { + // We always want `context` to be provided, but some callers will provide this value from an messy source, e.g. extracted from URL param. Fall back to `generic` just in case. + if (serverData.isNullOrEmpty(context)) { + context = models.marketingItemContextFromString("generic"); + } + const request = new mediaFetching.Request(objectGraph) + .forType("upsellMarketingItem") + .addingQuery("serviceType", "arcade") + .addingQuery("placement", context) + .includingMetaKeys("marketing-items", ["metrics"]) + .includingRelationships(["contents"]) + .includingAttributes(["marketingArtwork", "marketingVideo"]) + .includingAgeRestrictions(); + // Append app id that is promoted, if any. + if (serverData.isDefinedNonNull(contextualAppId)) { + request.addingQuery("seed", contextualAppId); + } + return request; +} +// endregion +/** + * Grab recently played games. + * @param objectGraph The object graph. + * @param {number} limit The number of recently played games to return. + * @param {boolean} shouldHydrateAppsData Whether the apps data should be fetched from media api. + * @param {number} timeout A timeout in seconds. + * @returns {DataContainer} The media api data container with the recently played games. + */ +export async function getRecentlyPlayedGames(objectGraph, limit = null, shouldHydrateAppsData = false, timeout = null) { + return await new Promise((resolve, reject) => { + const isRecentlyPlayedGamesSupported = objectGraph.host.isiOS || objectGraph.host.isMac || objectGraph.host.isTV; + // Check if the client supports recently played games. + if (!isRecentlyPlayedGamesSupported) { + resolve(null); + return; + } + const getRecentlyPlayGamesPromise = objectGraph.arcade.getRecentlyPlayedGamesWithTimeout(timeout); + // Get recently played games + getRecentlyPlayGamesPromise + .then((adamIds) => { + // Only perform request when there are recently played games. + if (serverData.isNull(adamIds) || adamIds.length === 0) { + resolve(null); + return; + } + // Enforce limit (if any). + if (serverData.isNumber(limit) && adamIds.length > limit) { + adamIds = adamIds.slice(0, limit); + } + if (shouldHydrateAppsData) { + // Fetch data for recently played games. + const attributes = [ + "editorialArtwork", + "editorialVideo", + "description", + "minimumOSVersion", + "minPlayers", + "maxPlayers", + "remoteControllerRequirement", + "requiresGameController", + "supportsSharePlay", + ]; + if (objectGraph.client.isVision) { + attributes.push("compatibilityControllerRequirement"); + } + if (objectGraph.client.isMac) { + attributes.push("hasMacIPAPackage"); + } + if (objectGraph.bag.enableUpdatedAgeRatings) { + attributes.push("ageRating"); + } + if (shouldUsePrerenderedIconArtwork(objectGraph)) { + attributes.push("iconArtwork"); + } + const mediaApiRequest = new mediaFetching.Request(objectGraph) + .withIdsOfType(adamIds, "apps") + .includingAttributes(attributes); + const fetchDataPromise = mediaNetwork.fetchData(objectGraph, mediaApiRequest); + fetchDataPromise.then((dataContainer) => resolve(dataContainer), (reason) => { + objectGraph.console.log(`getRecentlyPlayedGames() failed when calling mediaNetwork.fetchData() with reason: ${reason}`); + resolve(null); + }); + } + else { + // Create an incomplete data container to be fetched later. + const dataContainer = { + data: [], + }; + adamIds.forEach((adamId) => { + dataContainer.data.push({ + id: adamId, + type: "apps", + }); + }); + resolve(dataContainer); + } + }) + .catch((reason) => { + objectGraph.console.log(`getRecentlyPlayedGames() failed with: ${reason}`); + resolve(null); + }); + }); +} +/** + * Convenience function to build a `ArcadeUpsellData` representation from upsell relationship joined to some data. + * @seealso `upsellFromContentsOfUpsellResponse` + * @param objectGraph The object graph + * @param data Data containing `upsell` relationship to build `ArcadeUpsellData` with + */ +export function upsellFromRelationshipOf(objectGraph, data) { + // Data to extract. + let marketingItemData = null; + const upsellDataContainer = objectGraph.client.isVision || preprocessor.GAMES_TARGET + ? mediaRelationships.relationship(data, "contents") + : mediaRelationships.relationship(data, "upsell") || + mediaRelationships.relationship(data, "marketing-items"); + if (serverData.isNullOrEmpty(upsellDataContainer) || serverData.isNullOrEmpty(upsellDataContainer.data)) { + return null; + } + // Create marketing items array from data container. + const marketingItems = upsellDataContainer.data + .map((item) => { + if (item.type === "marketing-items") { + return item; + } + else { + return null; + } + }) + .filter((item) => isDefinedNonNull(item)); + // Return null if there are NOT any marketing items. + if (serverData.isNullOrEmpty(marketingItems)) { + return null; + } + const timeout = objectGraph.bag.marketingItemSelectionTimeout; + // If there is only one marketing item or timeout is zero, set this as the data. + // Otherwise get a single marketing item by calling AMS. + if (marketingItems.length === 1 || timeout === 0) { + marketingItemData = marketingItems[0]; + } + else { + try { + marketingItemData = objectGraph.arcade.getMarketingItemWithTimeout(marketingItems, timeout); + } + catch { + // Default to first item if the call was timed out. + marketingItemData = marketingItems[0]; + } + } + // Return null if marketing item is null. + if (serverData.isNull(marketingItemData)) { + return null; + } + return { + marketingItemData: marketingItemData, + }; +} +/** + * Convenience function to build a `ArcadeUpsellData` representation from contents of a engagement/upsell response. + * This doesn't really belong in + * @seealso `upsellFromRelationshipOf` + * @param arcadeUpsellResponse Response from the engagement/upsell endpoint. + */ +export function upsellFromContentsOfUpsellResponse(objectGraph, arcadeUpsellResponse) { + if (!arcadeUpsellResponse) { + return null; + } + let marketingItemData = null; + const responseDataArray = serverData.asArrayOrEmpty(arcadeUpsellResponse, "results.data"); + if (responseDataArray.length > 0) { + marketingItemData = responseDataArray[0]; + } + /** + * `arcadeUpsellResponse` is expected of form: + * { + * content: { mediaDataStructure.Data } + * } + * matching what is provided via the appropriate `action=<usage>` param from engagement/upsell endpoint. + */ + if (!serverData.isDefinedNonNull(marketingItemData)) { + return null; + } + return { + marketingItemData: marketingItemData, + }; +} +// endregion +// region Arcade Games For You Request +/** + * Request for fetching list of recommended Arcade apps from recommendations API. + * @param objectGraph The App Store object graph. + * @param limit Maximum number of apps to fetch. + * @returns {Request} Request object for fetching recommended Arcade apps from + * personalized recommendations endpoint. + */ +export function arcadeGamesForYouRequest(objectGraph, limit) { + let request = new mediaFetching.Request(objectGraph) + .forType("personal-recommendations") + .addingQuery("sparseLimit[contents]", `${limit}`) + .addingQuery("include[personal-recommendations]", "contents") + .addingQuery("filter[kind]", "arcadeGamesForYou") + .includingAgeRestrictions(); + // For visionOS, we need to include bincompat apps. + if (objectGraph.client.isVision) { + request = request.includingAdditionalPlatforms(["iphone", "ipad"]); + } + return request; +} +// endregion +// The color to use for Arcade content. +export const arcadeColor = Color.fromRGB(1, 90 / 255, 80 / 255); +//# sourceMappingURL=arcade-common.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/arcade/arcade-see-all-games-facets.js b/node_modules/@jet-app/app-store/tmp/src/common/arcade/arcade-see-all-games-facets.js new file mode 100644 index 0000000..2418b5f --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/arcade/arcade-see-all-games-facets.js @@ -0,0 +1,245 @@ +/** + * Build methods for Arcade See All Games Facets. + */ +import * as models from "../../api/models"; +import { isDefinedNonNull, isDefinedNonNullNonEmpty, isNullOrEmpty } from "../../foundation/json-parsing/server-data"; +import { Parameters } from "../../foundation/network/url-constants"; +import * as metricsClickHelpers from "../metrics/helpers/clicks"; +// All Facet Options +export const sortReleaseDateFacetOptionValue = `-releaseDateByDay&sort=name`; +export const sortLastUpdatedFacetOptionValue = `-lastUpdatedByDay&sort=name`; +export const sortNameFacetOptionValue = `alphabet&sort=name`; +export const sortCategoryFacetOptionValue = `category&sort=-releaseDate`; +export const sortFacetOptionParameter = "groupBy"; +export const comingSoonFacetOptionParameter = "with"; +export const comingSoonAppsFacetOptionValue = "comingSoonApps"; +export const comingSoonGroupingFacetOptionValue = "comingSoonGrouping"; +/** + * The facet for filtering bincompat games in visionOS + * By default, not passing this parameter will includes both bincompat and native games. + * To filter native games only, passing `filter[platform]=reality` + */ +export const binCompatGamesFacetOptionParameter = "filter[platform]"; +// The facet option for toggling video preview in lockups. +// This facet is used for toggle `isCompactMode` locally, it don't match with any media api parameter. +export const gamePreviewsFacetOptionParameter = "gamePreviews"; +export const facetOptionsParameterMapping = {}; +let areFacetsInitialized = false; +let sortReleaseDateFacetOption = null; +let sortLastUpdatedFacetOption = null; +let sortNameFacetOption = null; +let sortCategoryFacetOption = null; +let ageRatingDefaultFacetOption = null; +let ageRating4PlusFacetOption = null; +let ageRating9PlusFacetOption = null; +let ageRating12PlusFacetOption = null; +let controllerSupportFacetOption = null; +let multiplayerSupportFacetOption = null; +let comingSoonFacetOption = null; +let includeBinCompatGamesFacetOption = null; +let onlyNativeGamesFacetOption = null; +let gamePreviewsFacetOption = null; +// Mapping of external facet parameters to media api paramter values +const facetParameterMediaApiMapping = {}; +facetParameterMediaApiMapping[Parameters.sort] = sortFacetOptionParameter; +facetParameterMediaApiMapping[Parameters.ageRating] = "filter[ageRating]"; +facetParameterMediaApiMapping[Parameters.controllerSupport] = "filter[supportsGameController]"; +facetParameterMediaApiMapping[Parameters.multiplayerSupport] = "filter[isMultiplayer]"; +facetParameterMediaApiMapping[Parameters.comingSoon] = comingSoonFacetOptionParameter; +facetParameterMediaApiMapping[Parameters.binCompatGames] = binCompatGamesFacetOptionParameter; +function initializeFacets(objectGraph) { + if (areFacetsInitialized) { + return; + } + areFacetsInitialized = true; + sortReleaseDateFacetOption = new models.PageFacetOption(objectGraph.loc.string("PAGE_FACETS_SORTS_RELEASE_DATE"), sortReleaseDateFacetOptionValue, "releaseDate"); + sortLastUpdatedFacetOption = new models.PageFacetOption(objectGraph.loc.string("PAGE_FACETS_SORTS_LAST_UPDATED"), sortLastUpdatedFacetOptionValue, "lastUpdated"); + sortNameFacetOption = new models.PageFacetOption(objectGraph.loc.string("PAGE_FACETS_SORTS_NAME"), sortNameFacetOptionValue, "name"); + sortCategoryFacetOption = new models.PageFacetOption(objectGraph.loc.string("PAGE_FACETS_SORTS_CATEGORY"), sortCategoryFacetOptionValue, "category"); + ageRatingDefaultFacetOption = new models.PageFacetOption(objectGraph.loc.string("PAGE_FACETS_DEFAULT_ALL"), null); + ageRating4PlusFacetOption = new models.PageFacetOption(objectGraph.loc.string("PAGE_FACETS_FILTERS_AGE_VALUE_4PLUS"), "4Plus"); + ageRating9PlusFacetOption = new models.PageFacetOption(objectGraph.loc.string("PAGE_FACETS_FILTERS_AGE_VALUE_9PLUS"), "9Plus"); + ageRating12PlusFacetOption = new models.PageFacetOption(objectGraph.loc.string("PAGE_FACETS_FILTERS_AGE_VALUE_12PLUS"), "12Plus"); + controllerSupportFacetOption = new models.PageFacetOption(objectGraph.loc.string("PAGE_FACETS_FILTERS_CONTROLLER_SUPPORT"), "true", null, "gamecontroller"); + multiplayerSupportFacetOption = new models.PageFacetOption(objectGraph.loc.string("PAGE_FACETS_FILTERS_MULTIPLAYER"), "true", null, "person.2"); + comingSoonFacetOption = new models.PageFacetOption(objectGraph.loc.string("PAGE_FACETS_FILTERS_COMING_SOON"), "true"); + if (objectGraph.client.isVision) { + includeBinCompatGamesFacetOption = new models.PageFacetOption(objectGraph.loc.string("PAGE_FACETS_FILTERS_BIN_COMPAT_GAMES"), null); + onlyNativeGamesFacetOption = new models.PageFacetOption(objectGraph.loc.string("PAGE_FACETS_FILTERS_BIN_COMPAT_GAMES"), "realityDevice"); + gamePreviewsFacetOption = new models.PageFacetOption(objectGraph.loc.string("PAGE_FACETS_FILTERS_GAME_PREVIEWS"), null); + } + facetOptionsParameterMapping[Parameters.sort] = { + releaseDate: [sortReleaseDateFacetOption], + lastUpdated: [sortLastUpdatedFacetOption], + name: [sortNameFacetOption], + category: [sortCategoryFacetOption], + }; + facetOptionsParameterMapping[Parameters.ageRating] = { + "4plus": [ageRating4PlusFacetOption], + "9plus": [ageRating9PlusFacetOption], + "12plus": [ageRating12PlusFacetOption], + }; + facetOptionsParameterMapping[Parameters.controllerSupport] = { + true: [controllerSupportFacetOption], + false: [], + }; + facetOptionsParameterMapping[Parameters.multiplayerSupport] = { + true: [multiplayerSupportFacetOption], + false: [], + }; + facetOptionsParameterMapping[Parameters.comingSoon] = { + true: [comingSoonFacetOption], + false: [], + }; + if (objectGraph.client.isVision) { + facetOptionsParameterMapping[Parameters.binCompatGames] = { + true: [includeBinCompatGamesFacetOption], + false: [onlyNativeGamesFacetOption], + }; + facetOptionsParameterMapping[Parameters.gamePreviews] = { + true: [gamePreviewsFacetOption], + false: [], + }; + } +} +/** + * + * Create Page Facets. Theres platform specific variations here. + */ +export function createArcadeSeeAllGamesFacets(objectGraph) { + initializeFacets(objectGraph); + const isEnabledVisionSAGSegmentedNavFeature = objectGraph.featureFlags.isEnabled("see_all_games_segmented_nav_2024A"); + const ageRatingsFacetTitle = objectGraph.client.isMac + ? objectGraph.loc.string("PAGE_FACETS_AGE_RATINGS_TITLE") + : objectGraph.loc.string("PAGE_FACETS_FILTERS_AGE_RATINGS"); + const ageRatingsFacet = new models.PageFacetsFacet(Parameters.ageRating, "filter[ageRating]", ageRatingsFacetTitle, "singleSelection", [ageRatingDefaultFacetOption, ageRating4PlusFacetOption, ageRating9PlusFacetOption, ageRating12PlusFacetOption], [ageRatingDefaultFacetOption], "age", pageFacetChangeAction(objectGraph, "sort")); + // Sorts + const isEnabledArcadeSeeAllGamesUplift = objectGraph.featureFlags.isEnabled("arcade_see_all_games_menu_uplift"); + const isFeatureFlagEnabled = (objectGraph.client.isiOS && isEnabledArcadeSeeAllGamesUplift) || + (objectGraph.client.isVision && isEnabledVisionSAGSegmentedNavFeature); + const sortsFacet = new models.PageFacetsFacet(Parameters.sort, sortFacetOptionParameter, objectGraph.loc.string("PAGE_FACETS_SORT_BY_TITLE"), "singleSelection", [sortReleaseDateFacetOption, sortLastUpdatedFacetOption, sortNameFacetOption, sortCategoryFacetOption], isFeatureFlagEnabled ? [sortReleaseDateFacetOption] : null, "sort", pageFacetChangeAction(objectGraph, "sort")); + // Controllers + const controllerSupportFacetOptionFacetTitle = objectGraph.client.isMac + ? objectGraph.loc.string("PAGE_FACETS_FILTERS_CONTROLLERS") + : objectGraph.loc.string("PAGE_FACETS_FILTERS_CONTROLLER_SUPPORT"); + const controllerSupportFacetOptionFacet = new models.PageFacetsFacet(Parameters.controllerSupport, "filter[supportsGameController]", controllerSupportFacetOptionFacetTitle, "toggle", [controllerSupportFacetOption], null, "controllerSupport", pageFacetChangeAction(objectGraph, "sort")); + const multiplayerFacetTitle = objectGraph.client.isMac + ? objectGraph.loc.string("PAGE_FACETS_FILTERS_MULTIPLAYER") + : objectGraph.loc.string("PAGE_FACETS_FILTERS_MULTIPLAYER_GAMES"); + const multiplayerSupportFacetOptionFacet = new models.PageFacetsFacet(Parameters.multiplayerSupport, "filter[isMultiplayer]", multiplayerFacetTitle, "toggle", [multiplayerSupportFacetOption], null, "multiplayer", pageFacetChangeAction(objectGraph, "sort")); + const comingSoonFacet = new models.PageFacetsFacet(Parameters.comingSoon, comingSoonFacetOptionParameter, objectGraph.loc.string("PAGE_FACETS_FILTERS_COMING_SOON"), "toggle", [comingSoonFacetOption], [comingSoonFacetOption], "comingSoon", pageFacetChangeAction(objectGraph, "comingSoon")); + comingSoonFacet.isHiddenFromMenu = isFeatureFlagEnabled; + const pageFacets = new models.PageFacets([], false, null); + if (objectGraph.client.isVision) { + sortsFacet.displayOptionsInline = true; + pageFacets.facetGroups.push(new models.PageFacetsGroup([sortsFacet], objectGraph.loc.string("PAGE_FACETS_SORT_BY_TITLE_LOWERCASE"))); + ageRatingsFacet.showsSelectedOptions = true; + pageFacets.facetGroups.push(new models.PageFacetsGroup([controllerSupportFacetOptionFacet, multiplayerSupportFacetOptionFacet, ageRatingsFacet], objectGraph.loc.string("PAGE_FACETS_FILTER_BY_TITLE_LOWERCASE"))); + const binCompatGamesFacet = createBinCompatGamesFacet(objectGraph); + const gamePreviewsFacet = new models.PageFacetsFacet(Parameters.gamePreviews, gamePreviewsFacetOptionParameter, objectGraph.loc.string("PAGE_FACETS_FILTERS_GAME_PREVIEWS"), "toggle", [gamePreviewsFacetOption], [gamePreviewsFacetOption], "gamePreviews", pageFacetChangeAction(objectGraph, "gamePreviews")); + const includeGroupsTitle = objectGraph.client.isVision && isEnabledVisionSAGSegmentedNavFeature + ? objectGraph.loc.string("PAGE_FACETS_SHOW_TITLE_LOWERCASE") + : objectGraph.loc.string("PAGE_FACETS_INCLUDE_TITLE_LOWERCASE"); + pageFacets.facetGroups.push(new models.PageFacetsGroup([binCompatGamesFacet, gamePreviewsFacet, comingSoonFacet], includeGroupsTitle)); + } + else { + switch (objectGraph.client.deviceType) { + case "mac": + pageFacets.facetGroups.push(new models.PageFacetsGroup([ageRatingsFacet])); + if (objectGraph.bag.enableComingSoonToggle) { + pageFacets.facetGroups.push(new models.PageFacetsGroup([comingSoonFacet], objectGraph.loc.string("PAGE_FACETS_INCLUDE_TITLE"))); + } + pageFacets.facetGroups.push(new models.PageFacetsGroup([controllerSupportFacetOptionFacet, multiplayerSupportFacetOptionFacet], objectGraph.loc.string("PAGE_FACETS_SUPPORTS_TITLE"))); + pageFacets.facetGroups.push(new models.PageFacetsGroup([sortsFacet])); + break; + case "tv": + pageFacets.facetGroups.push(new models.PageFacetsGroup([ageRatingsFacet], objectGraph.loc.string("PAGE_FACETS_AGE_RATINGS_TITLE_LOWERCASE"))); + const filterGroupFacets = [ + controllerSupportFacetOptionFacet, + multiplayerSupportFacetOptionFacet, + ]; + if (objectGraph.bag.enableComingSoonToggle) { + filterGroupFacets.push(comingSoonFacet); + } + pageFacets.facetGroups.push(new models.PageFacetsGroup(filterGroupFacets, objectGraph.loc.string("PAGE_FACETS_FILTERS_TITLE"))); + pageFacets.facetGroups.push(new models.PageFacetsGroup([sortsFacet], objectGraph.loc.string("PAGE_FACETS_SORT_BY_TITLE_LOWERCASE"))); + break; + default: + sortsFacet.displayOptionsInline = true; + pageFacets.facetGroups.push(new models.PageFacetsGroup([sortsFacet], objectGraph.loc.string("PAGE_FACETS_SORT_BY_TITLE"))); + ageRatingsFacet.showsSelectedOptions = true; + pageFacets.facetGroups.push(new models.PageFacetsGroup([controllerSupportFacetOptionFacet, multiplayerSupportFacetOptionFacet, ageRatingsFacet], objectGraph.loc.string("PAGE_FACETS_FILTER_BY_TITLE"))); + if (objectGraph.bag.enableComingSoonToggle) { + pageFacets.facetGroups.push(new models.PageFacetsGroup([comingSoonFacet], objectGraph.loc.string("PAGE_FACETS_INCLUDE_TITLE"))); + } + break; + } + } + return pageFacets; +} +function createBinCompatGamesFacet(objectGraph) { + const binCompatGamesFacet = new models.PageFacetsFacet(Parameters.binCompatGames, binCompatGamesFacetOptionParameter, objectGraph.loc.string("PAGE_FACETS_FILTERS_BIN_COMPAT_GAMES"), "toggle", [includeBinCompatGamesFacetOption], [includeBinCompatGamesFacetOption], "binCompatGames", pageFacetChangeAction(objectGraph, "binCompatGames")); + // Hiding this facet from the Filters menu, because this facet will be control by the page segmented picker. + binCompatGamesFacet.isHiddenFromMenu = objectGraph.featureFlags.isEnabled("see_all_games_segmented_nav_2024A"); + return binCompatGamesFacet; +} +export function createDefaultSelectedFacetOptions(objectGraph, urlParameters = {}) { + initializeFacets(objectGraph); + const selectedFacetOptions = { + "filter[ageRating]": [ageRatingDefaultFacetOption], + }; + selectedFacetOptions[sortFacetOptionParameter] = [sortReleaseDateFacetOption]; + selectedFacetOptions[comingSoonFacetOptionParameter] = [comingSoonFacetOption]; + if (objectGraph.client.isVision) { + selectedFacetOptions[binCompatGamesFacetOptionParameter] = [includeBinCompatGamesFacetOption]; + selectedFacetOptions[gamePreviewsFacetOptionParameter] = [gamePreviewsFacetOption]; + } + const availableParameters = [ + Parameters.sort, + Parameters.ageRating, + Parameters.controllerSupport, + Parameters.multiplayerSupport, + Parameters.comingSoon, + ]; + if (objectGraph.client.isVision) { + availableParameters.push(Parameters.binCompatGames); + } + for (const parameter of availableParameters) { + const facetOption = urlParameters[parameter]; + if (isNullOrEmpty(facetOption)) { + continue; + } + const selectedFacetOption = facetOptionsParameterMapping[parameter][facetOption]; + const mediaApiFacetName = facetParameterMediaApiMapping[parameter]; + if (isDefinedNonNull(selectedFacetOption) && isDefinedNonNullNonEmpty(mediaApiFacetName)) { + selectedFacetOptions[`${mediaApiFacetName}`] = selectedFacetOption; + } + } + return selectedFacetOptions; +} +function pageFacetChangeAction(objectGraph, facetParameter) { + const action = new models.BlankAction(); + metricsClickHelpers.addClickEventToPageFacetsChangeAction(objectGraph, action, facetParameter); + return action; +} +export function createPageSegments(objectGraph) { + const isSegmentedNavEnabled = objectGraph.client.isVision && objectGraph.featureFlags.isEnabled("see_all_games_segmented_nav_2024A"); + if (!isSegmentedNavEnabled) { + return []; + } + else { + const binCompatGamesFacet = createBinCompatGamesFacet(objectGraph); + const allGamesPageSegment = { + id: "all_games", + title: objectGraph.loc.string("Arcade.SeeAllGames.PageSegment.AllGames.Title"), + segmentAction: new models.ArcadeSeeAllGamesPageSegmentChangeAction(binCompatGamesFacet, includeBinCompatGamesFacetOption), + }; + const visionGamesOnlyPageSegment = { + id: "vision", + title: objectGraph.loc.string("Arcade.SeeAllGames.PageSegment.AppleVisionGames.Title"), + segmentAction: new models.ArcadeSeeAllGamesPageSegmentChangeAction(binCompatGamesFacet, undefined), + }; + return [allGamesPageSegment, visionGamesOnlyPageSegment]; + } +} +//# sourceMappingURL=arcade-see-all-games-facets.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/arcade/arcade-see-all-request.js b/node_modules/@jet-app/app-store/tmp/src/common/arcade/arcade-see-all-request.js new file mode 100644 index 0000000..6f20944 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/arcade/arcade-see-all-request.js @@ -0,0 +1,78 @@ +import { PageFacets } from "../../api/models"; +import { isDefinedNonNullNonEmpty, isNullOrEmpty } from "../../foundation/json-parsing/server-data"; +import { Parameters } from "../../foundation/network/url-constants"; +import { shouldUsePrerenderedIconArtwork } from "../content/content"; +import { binCompatGamesFacetOptionParameter, comingSoonAppsFacetOptionValue, comingSoonFacetOptionParameter, comingSoonGroupingFacetOptionValue, facetOptionsParameterMapping, sortCategoryFacetOptionValue, sortFacetOptionParameter, sortLastUpdatedFacetOptionValue, sortNameFacetOptionValue, sortReleaseDateFacetOptionValue, } from "./arcade-see-all-games-facets"; +export function defaultRequestAttributes(objectGraph) { + const attributes = [ + "editorialArtwork", + "editorialVideo", + "isAppleWatchSupported", + "requiredCapabilities", + "videoPreviewsByType", + "screenshotsByType", + ]; + if (objectGraph.appleSilicon.isSupportEnabled) { + attributes.push("macRequiredCapabilities"); + } + if (objectGraph.client.isMac) { + attributes.push("hasMacIPAPackage"); + } + if (objectGraph.client.isVision) { + attributes.push("compatibilityControllerRequirement"); + } + if (objectGraph.bag.enableUpdatedAgeRatings) { + attributes.push("ageRating"); + } + if (shouldUsePrerenderedIconArtwork(objectGraph)) { + attributes.push("iconArtwork"); + } + return attributes; +} +const defaultSparseCount = 4; +export function prepareRequestWithSelectedFacets(request, selectedFacetOptions) { + let includeSparseCount = false; + for (const key of Object.keys(selectedFacetOptions)) { + // We need to choose the correct value for the coming soon parameter if coming soon is enabled. + if (key === comingSoonFacetOptionParameter && isDefinedNonNullNonEmpty(selectedFacetOptions[key])) { + const selectedSortValue = selectedFacetOptions[sortFacetOptionParameter]; + if (isDefinedNonNullNonEmpty(selectedSortValue)) { + switch (selectedSortValue[0].value) { + case sortReleaseDateFacetOptionValue: + case sortLastUpdatedFacetOptionValue: + selectedFacetOptions[key][0].value = comingSoonGroupingFacetOptionValue; + break; + case sortNameFacetOptionValue: + selectedFacetOptions[key][0].value = comingSoonAppsFacetOptionValue; + break; + case sortCategoryFacetOptionValue: + selectedFacetOptions[key][0].value = comingSoonAppsFacetOptionValue; + includeSparseCount = true; + break; + default: + break; + } + } + } + // We need to choose the correct value for the bin compat games parameter if it is disabled. + if (key === binCompatGamesFacetOptionParameter && isNullOrEmpty(selectedFacetOptions[key])) { + selectedFacetOptions[key] = facetOptionsParameterMapping[Parameters.binCompatGames].false; + } + } + // We include the sparse count when dealing with the category sort. + if (includeSparseCount) { + request.withSparseCount(defaultSparseCount); + } + for (const key of Object.keys(selectedFacetOptions)) { + const requestValues = PageFacets.requestValuesForSelectedFacetOptions(selectedFacetOptions[key]); + if (isDefinedNonNullNonEmpty(requestValues)) { + if (isDefinedNonNullNonEmpty(requestValues.value)) { + request.addingQuery(key, requestValues.value); + } + for (const additionalKey of Object.keys(requestValues.additionalKeyValuePairs)) { + request.addingQuery(additionalKey, requestValues.additionalKeyValuePairs[additionalKey]); + } + } + } +} +//# sourceMappingURL=arcade-see-all-request.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/arcade/arcade-see-all-routing.js b/node_modules/@jet-app/app-store/tmp/src/common/arcade/arcade-see-all-routing.js new file mode 100644 index 0000000..99b8df7 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/arcade/arcade-see-all-routing.js @@ -0,0 +1,5 @@ +import { makeRoutableArcadeSeeAllPageIntent } from "../../api/intents/routable-arcade-see-all-page-intent"; +import { generateRoutes } from "../util/generate-routes"; +const { routes, makeCanonicalUrl } = generateRoutes(makeRoutableArcadeSeeAllPageIntent, "/{platform}/arcade/see-all"); +export { routes as arcadeSeeAllRoutes, makeCanonicalUrl as makeArcadeSeeAllCanonicalUrl }; +//# sourceMappingURL=arcade-see-all-routing.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/arcade/arcade-upsell.js b/node_modules/@jet-app/app-store/tmp/src/common/arcade/arcade-upsell.js new file mode 100644 index 0000000..5c5d30e --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/arcade/arcade-upsell.js @@ -0,0 +1,257 @@ +// +// arcade-upsell.ts +// AppStoreKit +// +// Created by Jonathan Ellenbogen on 11/19/19. +// Copyright (c) 2016 Apple Inc. All rights reserved. +// +import * as models from "../../api/models"; +import * as serverData from "../../foundation/json-parsing/server-data"; +import * as mediaAttributes from "../../foundation/media/attributes"; +import * as color from "../../foundation/util/color-util"; +import * as heroCommon from "../grouping/hero/hero-common"; +import * as metricsHelpersClicks from "../metrics/helpers/clicks"; +import * as metricsHelpersImpressions from "../metrics/helpers/impressions"; +import * as metricsHelpersLocation from "../metrics/helpers/location"; +import * as metricsHelpersUtil from "../metrics/helpers/util"; +import * as offers from "../offers/offers"; +import * as breakoutsCommon from "./breakouts-common"; +import { ArcadeSubscriptionStateAction } from "../../api/models"; +import { isFeatureEnabledForCurrentUser } from "../util/lottery"; +export function createUpsellBreakout(objectGraph, upsellData, metricsOptions, style = "white") { + if (serverData.isNullOrEmpty(upsellData) || + (serverData.isNullOrEmpty(upsellData.attributes) && !objectGraph.client.isVision)) { + return null; + } + const heroVideoData = heroCommon.heroVideoFromData(objectGraph, upsellData, true); + const heroArtworkData = heroCommon.heroArtworkFromData(objectGraph, upsellData); + const backgroundColor = heroVideoData.backgroundColor || heroArtworkData.backgroundColor; + const detailPosition = breakoutsCommon.detailPositionFromData(objectGraph, upsellData, true, true); + const supportsMaterial = objectGraph.client.isTV; + const wantsBlur = breakoutsCommon.wantsBlur(objectGraph, breakoutsCommon.detailBackgroundStyleFromData(objectGraph, upsellData, supportsMaterial, true, true), true); + const details = createBreakoutDetailsFromData(objectGraph, upsellData, null, supportsMaterial, wantsBlur, "wordmark"); + let buttonCallToAction = null; + if (objectGraph.client.deviceType !== "tv") { + buttonCallToAction = details.description; + details.description = null; + } + const offerTint = offerTintFromMarketingItem(objectGraph, upsellData); + const offerDisplayProperties = new models.OfferDisplayProperties("arcade", objectGraph.bag.arcadeAppAdamId, null, style, null, "arcade", offerTint, null, null, null, null, null, null, null, null, null, null, null, objectGraph.bag.arcadeProductFamilyId); + const displayProperties = { + backgroundColor: backgroundColor, + wantsMaterialDetailBackground: false, + wantsBlur: wantsBlur, + badgeColor: null, + titleColor: null, + descriptionColor: null, + callToActionColor: null, + textAlignment: null, + detailsPosition: detailPosition, + }; + const upsellBreakout = new models.UpsellBreakout(details, offerDisplayProperties, displayProperties, null, buttonCallToAction, heroArtworkData.artwork, heroVideoData.video); + const impressionOptions = metricsHelpersImpressions.impressionOptions(objectGraph, upsellData, upsellBreakout.details.title, { + ...metricsOptions, + targetType: "upsellBreakout", + }); + impressionOptions.displaysArcadeUpsell = true; + metricsHelpersImpressions.addImpressionFields(objectGraph, upsellBreakout, impressionOptions); + // Push the breakout here so that the click action has the breakout in its location + // but we do not want to add it to the overall location tracker, so pop it right after adding it to the button + // action + // <rdar://problem/60883269> Metrics: Arcade: Container values requested in Location field + metricsHelpersLocation.pushContentLocation(objectGraph, impressionOptions, upsellBreakout.details.title); + upsellBreakout.offerButtonAction = arcadeOfferButtonActionFromData(objectGraph, upsellData, models.marketingItemContextFromString("arcadeTabHeader"), metricsOptions); + metricsHelpersLocation.popLocation(metricsOptions.locationTracker); + return upsellBreakout; +} +export function pageTitleEffectFromData(objectGraph, upsellData) { + const heroVideoData = heroCommon.heroVideoFromData(objectGraph, upsellData, true); + const heroArtworkData = heroCommon.heroArtworkFromData(objectGraph, upsellData); + const titleEffectArtwork = heroVideoData.artworkData || heroArtworkData.artworkData; + return breakoutsCommon.titleEffectFromArtwork(objectGraph, titleEffectArtwork, true); +} +export function callToActionLabelFromData(objectGraph, data) { + return breakoutsCommon.callToActionLabelFromData(objectGraph, data); +} +export function artworkDictionaryFromData(objectGraph, data) { + return breakoutsCommon.artworkDictionaryFromData(objectGraph, data); +} +export function artworkDataFromData(objectGraph, data, attributePath) { + const dictionary = artworkDictionaryFromData(objectGraph, data); + return serverData.asDictionary(dictionary, attributePath); +} +export function titleFromData(objectGraph, data) { + return mediaAttributes.attributeAsString(data, "title"); +} +export function descriptionFromData(objectGraph, data) { + return mediaAttributes.attributeAsString(data, "subtitle"); +} +export function createBreakoutDetailsFromData(objectGraph, data, contextualAppDataOrNull, supportsMaterial, wantsBlur, badgeType) { + let badge = null; + switch (badgeType) { + case "wordmark": + badge = { + type: "wordmark", + }; + break; + case "text": + const badgeTitle = mediaAttributes.attributeAsString(data, "badge"); + if ((badgeTitle === null || badgeTitle === void 0 ? void 0 : badgeTitle.length) > 0) { + badge = { + type: "text", + title: badgeTitle, + }; + } + else { + badge = { + type: "none", + }; + } + break; + default: + badge = { + type: "none", + }; + break; + } + let backgroundStyle = breakoutsCommon.detailBackgroundStyleFromData(objectGraph, data, supportsMaterial, true, true); + const detailPosition = breakoutsCommon.detailPositionFromData(objectGraph, data, true, true); + if (wantsBlur) { + backgroundStyle = "dark"; + } + const details = new models.BreakoutDetails(titleFromData(objectGraph, data), descriptionFromData(objectGraph, data), badge, null, backgroundStyle, breakoutsCommon.detailTextAlignmentForDetailPosition(objectGraph, detailPosition, data, true)); + return details; +} +// region Offer Action +export function arcadeOfferButtonActionFromData(objectGraph, upsellData, context, metricsOptions) { + const arcadeUpsellData = { + marketingItemData: upsellData, + }; + // "arcadeTabHeader" context covers both hero and navbar button on the Arcade page + const showStarterPackOnboarding = context === models.marketingItemContextFromString("arcadeTabHeader") && + objectGraph.bag.arcadeDownloadPackPostSubscribeTrigger && + isFeatureEnabledForCurrentUser(objectGraph, objectGraph.bag.arcadeDownloadPackRolloutRate); + return arcadeActionFromUpsellData(objectGraph, arcadeUpsellData, callToActionLabelFromData(objectGraph, upsellData), metricsOptions, showStarterPackOnboarding); +} +export function arcadeActionFromUpsellData(objectGraph, data, title, baseMetricsOptions, showStarterPackOnboarding) { + const marketingItemData = data.marketingItemData; + if (!serverData.isDefinedNonNull(marketingItemData)) { + return null; + } + const offerData = offers.offerDataFromMarketingItem(objectGraph, marketingItemData); + const isLinkAction = serverData.asString(offerData, "kind") === "link"; + const linkUrlString = serverData.asString(offerData, "url"); + if (isLinkAction && linkUrlString) { + const linkAction = arcadeLinkActionFromMarketingItemLinkUrl(objectGraph, linkUrlString); + const linkClickOptions = { + id: objectGraph.bag.arcadeAppAdamId, + actionType: "buy", + actionContext: "Arcade", + contextualAdamId: objectGraph.bag.arcadeAppAdamId, + offerType: "subscribe", + targetType: "button", + mercuryMetricsData: metricsHelpersUtil.marketingItemTopLevelBaseFieldsFromData(objectGraph, marketingItemData), + ...baseMetricsOptions, + }; + linkAction.title = title; + metricsHelpersClicks.addClickEventToAction(objectGraph, linkAction, linkClickOptions); + return linkAction; + } + else { + const arcadeAction = arcadeBuyActionFromMarketingItemData(objectGraph, marketingItemData, title, baseMetricsOptions); + if (showStarterPackOnboarding) { + const downloadPackOnboarding = new models.FlowAction("arcadeDownloadPackCategories"); + const subscriptionStatus = "new"; + downloadPackOnboarding.pageData = subscriptionStatus; + downloadPackOnboarding.presentationContext = "presentModalFormSheet"; + // Passing `undefined` for other than subscribed actions instead of `BlankAction` to record an error. + const subscriptionCheckAction = new ArcadeSubscriptionStateAction(undefined, undefined, downloadPackOnboarding, undefined); + const action = new models.CompoundAction([arcadeAction, subscriptionCheckAction]); + action.title = title; + return action; + } + else { + return arcadeAction; + } + } +} +function arcadeLinkActionFromMarketingItemLinkUrl(objectGraph, urlString) { + /** + * Check against the bag routing regex and dispatch one of the AMS actions. + */ + // Dynamic UI + const dynamicUIRegexPatterns = objectGraph.bag.dynamicUIRegexStrings; + for (const pattern of dynamicUIRegexPatterns) { + const dynamicUIPattern = new RegExp(pattern); + if (dynamicUIPattern.test(urlString)) { + const action = new models.FlowAction("dynamicUI", urlString); + action.pageData = new models.DynamicUIRequestInfo(objectGraph.bag.metricsTopic); + return action; + } + } + // Finance UI regex check + const financeUIRegexPatterns = objectGraph.bag.financeUIRegexStrings; + for (const pattern of financeUIRegexPatterns) { + const financeUIPattern = new RegExp(pattern); + if (financeUIPattern.test(urlString)) { + return new models.FlowAction("finance", urlString); + } + } + // Web UI regex check + const webViewRegexPatterns = objectGraph.bag.webViewRegexStrings; + for (const pattern of webViewRegexPatterns) { + const webViewPattern = new RegExp(pattern); + if (webViewPattern.test(urlString)) { + return new models.FlowAction("webView", urlString); + } + } + return new models.ExternalUrlAction(urlString, false); +} +function arcadeBuyActionFromMarketingItemData(objectGraph, data, title, baseMetricsOptions) { + const offerData = offers.offerDataFromMarketingItem(objectGraph, data); + const offerName = serverData.asString(offerData, "offerName"); + const buyParams = serverData.asString(offerData, "buyParams"); + if (!serverData.isDefinedNonNull(offerName) || !serverData.isDefinedNonNull(buyParams)) { + return null; + } + const offerServiceTypes = serverData.asArrayOrEmpty(offerData, "serviceTypes"); + const isAristotleOffer = offerServiceTypes.length > 1 && objectGraph.bag.aristotleParentAppAdamId; + const parentAdamId = isAristotleOffer ? objectGraph.bag.aristotleParentAppAdamId : objectGraph.bag.arcadeAppAdamId; + const arcadeAction = new models.ArcadeAction(offerName, parentAdamId, { + buyParams: buyParams, + productIdentifier: offerName, + pageInformation: baseMetricsOptions.pageInformation, + }); + const metricsClickOptions = { + id: parentAdamId, + actionType: "buy", + targetType: "button", + subscriptionSKU: offerName, + actionContext: "Arcade", + contextualAdamId: parentAdamId, + actionDetails: { buyParams: buyParams }, + offerType: "subscribe", + mercuryMetricsData: metricsHelpersUtil.marketingItemTopLevelBaseFieldsFromData(objectGraph, data), + ...baseMetricsOptions, + }; + arcadeAction.title = title; + metricsHelpersClicks.addClickEventToAction(objectGraph, arcadeAction, metricsClickOptions); + return arcadeAction; +} +// endregion +// region Helpers +function offerTintFromMarketingItem(objectGraph, data) { + const templateParameters = breakoutsCommon.templateParametersFromData(objectGraph, data); + const fillColor = serverData.asString(templateParameters, "ctaButtonBackgroundColor"); + const textColor = serverData.asString(templateParameters, "ctaButtonTextColor"); + if (!serverData.isDefinedNonNull(fillColor) || !serverData.isDefinedNonNull(textColor)) { + return { type: "blue", fillColor: null, textColor: null }; + } + return { + type: "custom", + fillColor: color.fromHex(fillColor), + textColor: color.fromHex(textColor), + }; +} +// endregion +//# sourceMappingURL=arcade-upsell.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/arcade/breakouts-common.js b/node_modules/@jet-app/app-store/tmp/src/common/arcade/breakouts-common.js new file mode 100644 index 0000000..85d4bb4 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/arcade/breakouts-common.js @@ -0,0 +1,272 @@ +// +// breakouts-common.ts +// AppStoreKit +// +// Created by Jonathan Ellenbogen on 11/19/19. +// Copyright (c) 2016 Apple Inc. All rights reserved. +// +import { isNothing } from "@jet/environment"; +import * as models from "../../api/models"; +import * as serverData from "../../foundation/json-parsing/server-data"; +import * as mediaAttributes from "../../foundation/media/attributes"; +import * as platformAttributes from "../../foundation/media/platform-attributes"; +import * as mediaRelationship from "../../foundation/media/relationships"; +import * as color from "../../foundation/util/color-util"; +import { pageRouter } from "../builders/routing"; +import * as contentAttributes from "../content/attributes"; +import * as heroCommon from "../grouping/hero/hero-common"; +import * as offers from "../offers/offers"; +import * as productPageUtil from "../product-page/product-page-util"; +import { makeProductPageIntent } from "../../api/intents/product-page-intent"; +import { makeRoutableArticlePageIntent } from "../../api/intents/routable-article-page-intent"; +import { getLocale } from "../locale"; +import { makeRoutableArticlePageCanonicalUrl } from "../today/routable-article-page-url-utils"; +import { getPlatform } from "../preview-platform"; +export function detailBackgroundStyleFromData(objectGraph, breakoutData, supportsMaterial = true, isFirstShelf, isUpsell) { + const heroVideoData = heroCommon.heroVideoFromData(objectGraph, breakoutData); + const heroArtworkData = heroCommon.heroArtworkFromData(objectGraph, breakoutData); + const backgroundColor = heroVideoData.backgroundColor || heroArtworkData.backgroundColor; + const detailPosition = detailPositionFromData(objectGraph, breakoutData, isFirstShelf, isUpsell); + const displayBreakoutMaterial = breakoutData.type === "marketing-items" + ? serverData.asBooleanOrFalse(templateParametersFromData(objectGraph, breakoutData), "displayMaterial") + : mediaAttributes.attributeAsBooleanOrFalse(breakoutData, "displayBreakoutMaterial"); + const shouldUseMaterialBackground = displayBreakoutMaterial || (objectGraph.client.isTV && detailPosition === "center"); + if (shouldUseMaterialBackground && supportsMaterial) { + return "material"; + } + else { + return detailBackgroundStyleFromColor(objectGraph, backgroundColor); + } +} +export function detailBackgroundStyleFromColor(objectGraph, backgroundColor) { + if (!backgroundColor) { + return "dark"; + } + return color.isDarkColor(backgroundColor, 50) ? "dark" : "light"; +} +export function detailPositionFromData(objectGraph, data, isFirstShelf, isUpsell) { + // Upsells always position the details in the center. + if (objectGraph.client.isPhone || isUpsell) { + return "center"; + } + if (objectGraph.client.isTV) { + return isFirstShelf ? "center" : "leading"; + } + const breakoutDetailsString = data.type === "marketing-items" + ? serverData.asString(templateParametersFromData(objectGraph, data), "textPosition") + : mediaAttributes.attributeAsString(data, "breakoutTextAlignment"); + if (isNothing(breakoutDetailsString) || breakoutDetailsString.length === 0) { + return objectGraph.client.isMac ? "center" : "leading"; + } + switch (breakoutDetailsString.toLowerCase()) { + case "left": + return "leading"; + case "center": + return "center"; + case "right": + return "trailing"; + default: + return "leading"; + } +} +/** + * The text alignment to use for a particular detail position on large breakouts + * @param position The positioning of the details within the breakout. + */ +export function detailTextAlignmentForDetailPosition(objectGraph, position, breakoutData, isUpsell = false) { + const isTV = objectGraph.client.isTV; + switch (position) { + case "leading": + return isTV ? "center" : "leading"; + case "trailing": + return isTV ? "center" : "leading"; + case "center": + if (isUpsell && !isTV) { + return "center"; + } + else if (isTV) { + return "leading"; + } + else { + return breakoutData.type === "marketing-items" + ? "center" + : textAlignmentFromData(objectGraph, breakoutData); + } + default: + return "leading"; + } +} +function textAlignmentFromData(objectGraph, data) { + var _a; + const breakoutTextAlignmentString = (_a = mediaAttributes.attributeAsString(data, "breakoutTextAlignment")) !== null && _a !== void 0 ? _a : ""; + switch (breakoutTextAlignmentString.toLowerCase()) { + case "left": + return "leading"; + case "center": + return "center"; + case "right": + return "trailing"; + default: + return objectGraph.client.isMac ? "center" : "leading"; + } +} +export function templateParametersFromData(objectGraph, data) { + if (data.type !== "marketing-items") { + return null; + } + return mediaAttributes.attributeAsDictionary(data, "display.templateParameters"); +} +export function artworkDictionaryFromData(objectGraph, data) { + switch (data.type) { + case "editorial-items": + return mediaAttributes.attributeAsDictionary(data, "editorialArtwork"); + case "marketing-items": + return mediaAttributes.attributeAsDictionary(data, "marketingArtwork"); + default: + const attributePlatform = contentAttributes.bestAttributePlatformFromData(objectGraph, data); + return platformAttributes.platformAttributeAsDictionary(data, attributePlatform, "editorialArtwork"); + } +} +export function videoDictionaryFromData(objectGraph, data) { + switch (data.type) { + case "editorial-items": + return mediaAttributes.attributeAsDictionary(data, "editorialVideo"); + case "marketing-items": + return mediaAttributes.attributeAsDictionary(data, "marketingVideo"); + default: + if (preprocessor.GAMES_TARGET) { + return mediaAttributes.attributeAsDictionary(data, "editorialVideo"); + } + else { + const attributePlatform = contentAttributes.bestAttributePlatformFromData(objectGraph, data); + return platformAttributes.platformAttributeAsDictionary(data, attributePlatform, "marketingVideo"); + } + } +} +export function callToActionLabelFromData(objectGraph, data) { + switch (data.type) { + case "marketing-items": + const offerData = offers.offerDataFromMarketingItem(objectGraph, data); + return serverData.asString(offerData, "callToActionLabel"); + default: + return mediaAttributes.attributeAsString(data, "breakoutCallToActionLabel"); + } +} +export function requiresPrimaryContent(objectGraph, data) { + const linkData = mediaAttributes.attributeAsDictionary(data, "link"); + const isLinkAction = serverData.isDefinedNonNullNonEmpty(linkData); + const isStoryAction = mediaAttributes.attributeAsBooleanOrFalse(data, "isCanvasAvailable"); + return !isLinkAction && !isStoryAction; +} +export function wantsBlur(objectGraph, backgroundStyle, isInHeroPosition) { + return backgroundStyle !== "material" && isInHeroPosition; +} +/** + * The action to use when displaying a breakout's call to action button + * @param data The breakout node data from MAPI + */ +export function actionFromData(objectGraph, data) { + const linkData = mediaAttributes.attributeAsDictionary(data, "link"); + const isLinkAction = serverData.isDefinedNonNullNonEmpty(linkData); + const isProductAction = mediaAttributes.attributeAsString(data, "kind") === "App"; + const isStoryAction = mediaAttributes.attributeAsBooleanOrFalse(data, "isCanvasAvailable"); + const primaryContent = mediaRelationship.relationshipData(objectGraph, data, "primary-content"); + if (!isLinkAction && !isStoryAction && !mediaAttributes.hasAttributes(primaryContent)) { + return null; + } + let actionUrl = null; + if (isLinkAction) { + actionUrl = serverData.asString(linkData, "url"); + } + else if (isStoryAction) { + actionUrl = mediaAttributes.attributeAsString(data, "url"); + } + else { + actionUrl = mediaAttributes.attributeAsString(primaryContent, "url"); + } + if (serverData.isNull(actionUrl)) { + return null; + } + let action = null; + if (isLinkAction && serverData.asString(linkData, "target") === "external") { + const externalUrlAction = new models.ExternalUrlAction(actionUrl); + action = externalUrlAction; + } + else if (objectGraph.isAvailable(pageRouter)) { + const flowPage = objectGraph.required(pageRouter).fetchFlowPage(actionUrl); + const flowAction = new models.FlowAction(flowPage); + flowAction.pageUrl = actionUrl; + if (flowPage === "product") { + flowAction.pageData = productPageUtil.createProductPageSidePackFromResponse(objectGraph, primaryContent); + } + action = flowAction; + } + else if (objectGraph.client.isWeb && isStoryAction) { + const routableArticlePageIntent = makeRoutableArticlePageIntent({ + ...getLocale(objectGraph), + ...getPlatform(objectGraph), + id: data.id, + }); + const flowAction = new models.FlowAction("article"); + flowAction.title = mediaAttributes.attributeAsString(data, "breakoutCallToActionLabel"); + flowAction.pageUrl = makeRoutableArticlePageCanonicalUrl(objectGraph, routableArticlePageIntent); + flowAction.destination = routableArticlePageIntent; + action = flowAction; + } + else if (objectGraph.client.isWeb && isProductAction) { + const flowAction = new models.FlowAction("product"); + flowAction.title = mediaAttributes.attributeAsString(data, "breakoutCallToActionLabel"); + flowAction.pageUrl = actionUrl; + flowAction.destination = makeProductPageIntent({ + ...getLocale(objectGraph), + ...getPlatform(objectGraph), + id: primaryContent.id, + }); + action = flowAction; + } + if (action) { + action.title = callToActionLabelFromData(objectGraph, data); + } + return action; +} +export function titleEffectFromArtwork(objectGraph, editorialArtworkData, isUpsell = false) { + if (serverData.isNullOrEmpty(editorialArtworkData)) { + return null; + } + let titleEffect = null; + const white = color.named("white"); + const black = color.named("black"); + const backgroundColor = color.fromHex(serverData.asString(editorialArtworkData, "bgColor")); + const gradientColors = serverData + .asArrayOrEmpty(editorialArtworkData, "textGradient") + .map((hexColor) => { + return color.fromHex(hexColor); + }); + const backgroundStyle = detailBackgroundStyleFromColor(objectGraph, backgroundColor); + const hasGradient = gradientColors.length === 2; + if (hasGradient) { + titleEffect = new models.TitleEffect("horizontalGradient"); + const startColor = gradientColors[0]; + const endColor = gradientColors[1]; + titleEffect.gradientStartColor = color.dynamicWith(startColor, startColor); + titleEffect.gradientEndColor = color.dynamicWith(endColor, endColor); + titleEffect.filter = "plusLight"; + if (!isUpsell) { + titleEffect.accessoryColor = titleEffect.gradientEndColor; + } + } + else { + titleEffect = new models.TitleEffect("color"); + titleEffect.color = backgroundStyle === "dark" ? white : black; + if (!isUpsell) { + titleEffect.accessoryColor = titleEffect.color; + } + titleEffect.isFallbackStyle = true; + } + // The accessory on tvOS should always be white resulting in a dark accessory. + if (objectGraph.client.isTV) { + titleEffect.accessoryColor = white; + } + return titleEffect; +} +//# sourceMappingURL=breakouts-common.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/arcade/render-arcade-see-all-games-page.js b/node_modules/@jet-app/app-store/tmp/src/common/arcade/render-arcade-see-all-games-page.js new file mode 100644 index 0000000..f1d39e0 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/arcade/render-arcade-see-all-games-page.js @@ -0,0 +1,227 @@ +import * as validation from "@jet/environment/json/validation"; +import { Shelf, Trailers, Video, } from "../../api/models"; +import { metricsFromMediaApiObject, } from "../../foundation/media/data-structure"; +import { hasAttributes } from "../../foundation/media/attributes"; +import { isDefinedNonNullNonEmpty, isNullOrEmpty } from "../../foundation/json-parsing/server-data"; +import { Parameters, Path, Protocol } from "../../foundation/network/url-constants"; +import { URL } from "../../foundation/network/urls"; +import * as lockups from "../../common/lockups/lockups"; +import * as metricsHelpersImpressions from "../../common/metrics/helpers/impressions"; +import * as videoDefaults from "../../common/constants/video-constants"; +import * as content from "../../common/content/content"; +import { newLocationTracker, nextPosition, popLocation, pushContentLocation, } from "../metrics/helpers/location"; +export function createArcadeSeeAllGamesPaginationToken(_objectGraph, requestDescriptor, pageInformation) { + const locationTracker = newLocationTracker(); + const paginationUrl = URL.fromComponents(Protocol.internal, null, `/${Path.arcadeSeeAllGames}/${Path.arcadeSeeAllGamesLoadMore}`).build(); + const paginationToken = { + url: paginationUrl, + metricsPageInformation: pageInformation, + metricsLocationTracker: locationTracker, + remainingGroups: [], + lastShelfIndex: 0, + isCompactMode: requestDescriptor.isCompactMode, + }; + return paginationToken; +} +export function createShelves(objectGraph, groups, paginationToken) { + return validation.context("createShelves", () => { + const shelves = []; + if (groups.length === 0) { + return shelves; + } + let shouldStartPagination = false; + for (const group of groups) { + // Start pagination if we hit a group who's first item has not been hydrated + shouldStartPagination = shouldStartPagination || !hasAttributes(group.data[0]); + if (shouldStartPagination) { + paginationToken.remainingGroups.push(group); + } + else { + const shelfToken = { + index: paginationToken.lastShelfIndex, + title: shelfTitleForGroup(objectGraph, group), + contentType: shelfContentTypeForMode(objectGraph, paginationToken.isCompactMode), + shouldFilter: false, + remainingContent: group.data, + groupKind: group.kind, + isCompactMode: paginationToken.isCompactMode, + hasExistingContent: false, + isFirstRender: true, + metricsPageInformation: paginationToken.metricsPageInformation, + metricsLocationTracker: paginationToken.metricsLocationTracker, + }; + const shelf = createShelf(objectGraph, shelfToken); + shelves.push(shelf); + paginationToken.lastShelfIndex++; + } + } + return shelves; + }); +} +const defaultRowsPerColumn = 3; +export function createShelf(objectGraph, shelfToken) { + const shelfItems = []; + const shelf = new Shelf(shelfToken.contentType); + shelf.title = shelfToken.title; + shelf.presentationHints = { showSupplementaryText: false }; + if (objectGraph.client.isVision) { + shelf.isHorizontal = + (shelfToken.groupKind === "comingSoonGrouping" && !shelfToken.isCompactMode) || + shelfToken.groupKind === "category"; + shelf.presentationHints = { + ...shelf.presentationHints, + isSAGComingSoon: true, + }; + } + else { + shelf.isHorizontal = shelfToken.groupKind === "comingSoonGrouping" || shelfToken.groupKind === "category"; + } + const shelfMetricsOptions = { + id: `${shelfToken.index}`, + kind: null, + softwareType: null, + targetType: "swoosh", + title: shelf.title, + pageInformation: shelfToken.metricsPageInformation, + locationTracker: shelfToken.metricsLocationTracker, + idType: "sequential", + }; + /// add impression fields + metricsHelpersImpressions.addImpressionFields(objectGraph, shelf, shelfMetricsOptions); + pushContentLocation(objectGraph, shelfMetricsOptions, shelf.title); + while (isDefinedNonNullNonEmpty(shelfToken.remainingContent) && hasAttributes(shelfToken.remainingContent[0])) { + const lockupData = shelfToken.remainingContent.shift(); + const lockupOptions = { + metricsOptions: { + pageInformation: shelfToken.metricsPageInformation, + locationTracker: shelfToken.metricsLocationTracker, + recoMetricsData: metricsFromMediaApiObject(lockupData), + }, + artworkUseCase: 1 /* content.ArtworkUseCase.LockupIconSmall */, + canDisplayArcadeOfferButton: content.shelfContentTypeCanDisplayArcadeOfferButtons(objectGraph, shelfToken.contentType), + shouldHideArcadeHeader: objectGraph.featureFlags.isEnabled("hide_arcade_header_on_arcade_tab"), + isContainedInPreorderExclusiveShelf: shelfToken.groupKind === "comingSoonGrouping", + }; + const cropCode = objectGraph.client.isVision ? "sr" : null; + const lockup = lockups.mixedMediaLockupFromData(objectGraph, lockupData, lockupOptions, videoDefaults.defaultVideoConfiguration(objectGraph), null, cropCode); + const lockupHasVideos = ensureLockupHasVideos(objectGraph, lockup, lockupData); + if (!lockupHasVideos) { + continue; + } + lockup.editorialTagline = null; + lockup.developerTagline = null; + shelfItems.push(lockup); + nextPosition(shelfToken.metricsLocationTracker); + } + shelf.mergeWhenFetched = shelfToken.hasExistingContent; + shelfToken.hasExistingContent = shelfToken.hasExistingContent || shelfItems.length > 0; + shelf.items = shelfItems; + if (isDefinedNonNullNonEmpty(shelfToken.remainingContent)) { + shelf.url = shelfUrlForToken(shelfToken); + } + else if (objectGraph.client.deviceType !== "tv" && + shelfToken.isCompactMode && + shelf.items.length < defaultRowsPerColumn) { + shelf.rowsPerColumn = shelf.items.length; + } + popLocation(shelfToken.metricsLocationTracker); + return shelf; +} +function shelfContentTypeForMode(objectGraph, isCompactMode) { + if (isCompactMode) { + return "smallLockup"; + } + else if (objectGraph.client.isTV) { + return "mixedMediaLockup"; + } + else { + return "appTrailerLockup"; + } +} +function shelfTitleForGroup(objectGraph, group) { + let shelfTitle; + let dateComponents = null; + let groupDateForTitle = null; + let sentence; + // rdar://104719340 (Arcade See All Games section has a missing name) + // Temporary(?) work around for MAPI sometimes not returning a group.name + if ((!group.name || group.name.length === 0) && group.kind !== "comingSoonGrouping") { + return ""; + } + switch (group.kind) { + case "releaseDateByDay": + dateComponents = group.name.split("-"); + groupDateForTitle = new Date(parseInt(dateComponents[0]), parseInt(dateComponents[1]) - 1, parseInt(dateComponents[2])); + sentence = objectGraph.loc.string("ALL_GAMES_SECTION_TITLE_RELEASE_DATE_SENTENCE"); + shelfTitle = objectGraph.loc.formatDateInSentence(sentence, "MMMM d, y", groupDateForTitle); + break; + case "lastUpdatedByDay": + dateComponents = group.name.split("-"); + groupDateForTitle = new Date(parseInt(dateComponents[0]), parseInt(dateComponents[1]) - 1, parseInt(dateComponents[2])); + sentence = objectGraph.loc.string("ALL_GAMES_SECTION_TITLE_LAST_UPDATED_SENTENCE"); + shelfTitle = objectGraph.loc.formatDateInSentence(sentence, "MMMM d, y", groupDateForTitle); + break; + case "comingSoonGrouping": + shelfTitle = objectGraph.loc.string("Arcade.SeeAllGames.ComingSoonShelf.Title"); + break; + default: + shelfTitle = group.name; + break; + } + return shelfTitle; +} +function shelfUrlForToken(shelfToken) { + if (isNullOrEmpty(shelfToken.remainingContent)) { + return null; + } + return `${Protocol.internal}:/${Path.arcadeSeeAllGames}/${Path.shelf}/?${Parameters.token}=${encodeURIComponent(JSON.stringify(shelfToken))}`; +} +// region Fallback Data +function ensureLockupHasVideos(objectGraph, lockup, lockupData) { + // In visionOS, the bin-compat apps usually have trailer from iPad which using 4:3 video + // But SAG lockup preferred 16:9 video, so even we already have lockup.trailers, we still want to override it + // with the editorial splash video `splashVideo16x9`. + // If there is no editorial splash video found, we fallback to the current `lockup.trailers` or create a dump video. + if (isNullOrEmpty(lockup.trailers) || objectGraph.client.isVision) { + const uberVideo = content.editorialSplashVideoFromData(objectGraph, lockupData); + if (isDefinedNonNullNonEmpty(uberVideo)) { + const uberTrailer = new Trailers(); + uberVideo.playbackControls = videoDefaults.standardControls(objectGraph); + uberVideo.autoPlayPlaybackControls = videoDefaults.autoPlayControls(objectGraph); + uberVideo.canPlayFullScreen = true; + uberTrailer.videos = [uberVideo]; + lockup.trailers = [uberTrailer]; + } + } + if (isNullOrEmpty(lockup.trailers)) { + /** + * <rdar://problem/55328972> See All Games: Bring back screenshots + * In the very rare case that an arcade game has absolutely no videos, + * that means no app trailer, no editorial video in any form. We're going to create a fake + * video that uses the first screenshot as a preview frame. This allows us to + * use the same AppTrailersLockup and get uniform sizing for lockups. + */ + addFakeVideoToMixedMediaLockup(lockup); + } + return isDefinedNonNullNonEmpty(lockup.trailers); +} +const fakeVideoURL = "x-as3-internal:/today/test"; +function addFakeVideoToMixedMediaLockup(lockup) { + const screenshots = lockup.screenshots[0]; + if (isNullOrEmpty(screenshots)) { + return; + } + const firstScreenshot = screenshots.artwork[0]; + const fakeVideo = new Video(fakeVideoURL, firstScreenshot, { + allowsAutoPlay: false, + looping: false, + canPlayFullScreen: false, + autoPlayPlaybackControls: {}, + playbackControls: {}, + }); + const fakeTrailers = new Trailers(); + fakeTrailers.videos = [fakeVideo]; + lockup.trailers = [fakeTrailers]; +} +// endregion +//# sourceMappingURL=render-arcade-see-all-games-page.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/builders/additional-page-requirement-util.js b/node_modules/@jet-app/app-store/tmp/src/common/builders/additional-page-requirement-util.js new file mode 100644 index 0000000..242d28c --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/builders/additional-page-requirement-util.js @@ -0,0 +1,36 @@ +import { isNothing } from "@jet/environment"; +import { allMixed } from "../../foundation/util/promise-util"; +export async function fetchPageWithAdditionalPageRequirements(objectGraph, primaryPageRequirement, additionalRequirements) { + var _a; + // Data Frankensteining Pt 1. - Additional Page Requirements: + // Additional requests that are fired *IN PARALLEL* with page request to fetch additional data needed to render page. + // Why are these not included in a singular page fetch? I don't know. + // This builder API is used to request data that will *definitely* be used, regardless of the content of initial page fetch's response. + const additionalRequirementKeys = Object.keys(additionalRequirements); + const additionalPageRequirementPromises = additionalRequirementKeys.map(async (key) => { + return await additionalRequirements[key]; + }); + const allPageRequirements = await allMixed([primaryPageRequirement, ...additionalPageRequirementPromises]); + const primaryPageDataResult = allPageRequirements[0]; + const primaryPageData = primaryPageDataResult.value; + if (!primaryPageDataResult.success || isNothing(primaryPageData)) { + throw (_a = primaryPageDataResult.error) !== null && _a !== void 0 ? _a : new Error("Unknown primaryPageData error"); + } + const additionalData = {}; + const additionalPageRequirements = allPageRequirements.slice(1); + for (const [index, additionalPageRequirement] of additionalPageRequirements.entries()) { + const additionalRequirementKey = additionalRequirementKeys[index]; + if (additionalPageRequirement.success) { + additionalData[additionalRequirementKey] = additionalPageRequirement.value; + } + else { + additionalData[additionalRequirementKey] = null; + objectGraph.console.log(`Builder - failed to fetch additionalPageRequirement ${additionalRequirementKey}, reason: ${additionalPageRequirement.error}`); + } + } + return { + primaryPageData, + additionalData, + }; +} +//# sourceMappingURL=additional-page-requirement-util.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/builders/pagination.js b/node_modules/@jet-app/app-store/tmp/src/common/builders/pagination.js new file mode 100644 index 0000000..5f11fc7 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/builders/pagination.js @@ -0,0 +1,51 @@ +/** + * Created by km on 10/5/16. + */ +/// The suggested maximum items to load per page. +export const suggestedMaxPerPage = 20; +/** + * Returns a single page of lookup data from a page token. + * @param pageToken The page token to extract the ids from. + * @returns An array of zero or more ids to lookup. + */ +export function getDataForLookup(objectGraph, pageToken) { + if (!pageToken || !pageToken.remainingContent) { + return []; + } + return pageToken.remainingContent.slice(0, pageToken.maxPerPage); +} +/** + * Returns a page token containing the ids to fetch for the next page of data. + * @param pageToken The page token to advance. + * @param count The number of items to get, or all items if not specified + * @returns A new page token. + */ +export function nextPageToken(objectGraph, pageToken, count) { + const nextToken = { ...pageToken }; + if (pageToken && pageToken.remainingContent) { + const nextContent = nextDataRange(objectGraph, pageToken.remainingContent, pageToken.maxPerPage, count); + nextToken.remainingContent = nextContent; + } + return nextToken; +} +/** + * Returns a subset of the content from start to the end of the array + * @param ids The array of IDs from which to get the sub array + * @param start The starting index + * @param count The number of items to get, or all items if not specified + * @returns An array of all ids from start until the end of the array + */ +export function nextDataRange(objectGraph, dataArray, start, count) { + return dataArray.slice(start, count !== null && count !== void 0 ? count : dataArray.length); +} +export function createMediaPageToken(objectGraph, remaining, url, nextHref = null, pageInformation, locationTracker) { + return { + remainingContent: remaining, + maxPerPage: suggestedMaxPerPage, + highestOrdinal: 0, + url: url, + metricsPageInformation: pageInformation, + metricsLocationTracker: locationTracker, + }; +} +//# sourceMappingURL=pagination.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/builders/routing.js b/node_modules/@jet-app/app-store/tmp/src/common/builders/routing.js new file mode 100644 index 0000000..1655d6d --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/builders/routing.js @@ -0,0 +1,321 @@ +/** + * Created by km on 11/17/16. + */ +/* This file DOES NOT target nodejs usage. */ +import { isNothing, isSome } from "@jet/environment"; +import { makeMetatype } from "@jet/environment/util/metatype"; +import * as models from "../../api/models"; +import * as serverData from "../../foundation/json-parsing/server-data"; +import { Parameters } from "../../foundation/network/url-constants"; +import * as routingComponents from "../../foundation/routing/routing-components"; +import * as objects from "../../foundation/util/objects"; +import { allOptional, tryAwait } from "../../foundation/util/promise-util"; +import * as productPageCommon from "../product-page/product-page-common"; +// region Routers +export const pageRouter = makeMetatype("app-store:page-router"); +export class PageRouter { + constructor() { + this.registeredBuilders = new Set(); + this.pageRouter = new routingComponents.UrlRouter(); + this.shelfRouter = new routingComponents.UrlRouter(); + this.paginationRouter = new routingComponents.UrlRouter(); + // endregion + } + /** + * Register a page builder containing routes and related handlers. + * @param objectGraph + * @param {Builder} pageBuilder to register. + * + * @note Although one router can serve the page, shelf, and pagination routes, we maintain + * three instances dedicated to each instead to avoid iterating through each other's routes + * during URL matching. + */ + registerPageBuilder(objectGraph, pageBuilder) { + if (this.registeredBuilders.has(pageBuilder.builderClass)) { + throw new Error(`routing: Registering duplicate builderClass ${pageBuilder.builderClass}`); + } + this.registeredBuilders.add(pageBuilder.builderClass); + this.pageRouter.associate(pageBuilder.pageRoute(objectGraph), pageBuilder); + this.shelfRouter.associate(pageBuilder.shelfRoute(objectGraph), pageBuilder); + this.paginationRouter.associate(pageBuilder.paginationRoute(objectGraph), pageBuilder); + } + /** + * Register a shelf builder containing routes and related handlers. + * @param objectGraph + * @param {Builder} shelfBuilder to register. + * + * @note Although one router can serve the page, shelf, and pagination routes, we maintain + * three instances dedicated to each instead to avoid iterating through each other's routes + * during URL matching. + */ + registerShelfBuilder(objectGraph, shelfBuilder) { + if (this.registeredBuilders.has(shelfBuilder.builderClass)) { + throw new Error(`routing: Registering duplicate builderClass ${shelfBuilder.builderClass}`); + } + this.registeredBuilders.add(shelfBuilder.builderClass); + this.shelfRouter.associate(shelfBuilder.shelfRoute(objectGraph), shelfBuilder); + } + /** + * Register a pagination builder containing routes and related handlers. + * @param objectGraph + * @param {Builder} paginationBuilder to register. + * + * @note Although one router can serve the page, shelf, and pagination routes, we maintain + * three instances dedicated to each instead to avoid iterating through each other's routes + * during URL matching. + */ + registerPaginationBuilder(objectGraph, paginationBuilder) { + if (this.registeredBuilders.has(paginationBuilder.builderClass)) { + throw new Error(`routing: Registering duplicate builderClass ${paginationBuilder.builderClass}`); + } + this.registeredBuilders.add(paginationBuilder.builderClass); + this.paginationRouter.associate(paginationBuilder.paginationRoute(objectGraph), paginationBuilder); + } + // endregion + // region Exported API + /** + * Determine the type of destination for given `url`. + * @param {string} url The url to fetch `flowPage` for. + * @returns {FlowPage} The FlowPage for given `url` + */ + fetchFlowPage(url) { + const routerResult = this.pageRouter.routedObjectForUrl(url); + if (!routerResult.object) { + return `unknown`; + } + const builder = routerResult.object; + // Product URLs can go to `writeReview` via deep links in iOS, so the page type may not necessary be `product` + // So check that they are not routed to the reviews sections + if (builder.builderClass === "ProductBuilder" && + routerResult.parameters[Parameters.action] !== productPageCommon.reviewsAction && + routerResult.parameters[Parameters.action] !== productPageCommon.writeReviewAction) { + return "product"; + } + return builder.pageType(); + } + /** + * Fetch the contents of a page. + * @param objectGraph + * @param url The URL to determine what kind of page to load. + * @param pageType The meta type of the expected page, e.g. `models.ProductPage`. + * This is used to perform a runtime type check to prevent hard to track bugs. + * @returns A promise which will resolve into a page of `pageType`. + */ + async fetchPage(objectGraph, url, pageType) { + return await this.fetchAction(objectGraph, url, null, false).then(async (pageAction) => { + return await new Promise((resolve, reject) => { + if (!pageAction) { + throw new Error(`Promise resolved to null action for: ${url}`); + } + if (pageAction.actionClass === "FlowAction") { + const pageData = pageAction.pageData; + if (!objects.isTypeOf(pageData, pageType)) { + // As we have no data of the correct type, check if we have a redirect + const pageUrl = pageAction.pageUrl; + const isRedirectingToSelf = pageUrl === url; + if (pageUrl && !isRedirectingToSelf) { + // Re-process this fetch with the new URL. + this.fetchPage(objectGraph, pageUrl, pageType) + .then((page) => { + resolve(page); + }) + .catch((error) => { + reject(error); + }); + return; + } + reject(new Error(`pageData is not expected type ${pageType.name}, ${JSON.stringify(pageData)}`)); + return; + } + resolve(pageData); + return; + } + else if (pageAction.actionClass === "TabChangeAction") { + const tabChangeAction = pageAction; + if (tabChangeAction.actions.length === 1 && + tabChangeAction.actions[0].actionClass === "FlowAction") { + const pageData = tabChangeAction.actions[0].pageData; + if (!objects.isTypeOf(pageData, pageType)) { + reject(new Error(`pageData is not expected type ${pageType.name}, ${JSON.stringify(pageData)}`)); + return; + } + resolve(pageData); + return; + } + } + reject(new Error("Action is not a flowAction or a tabChangeAction that contains a single flowAction.")); + }); + }); + } + /** + * Fetch an action for a given url, including page data if there is any to return. + * @param url The URL to determine which actions to take. + * @param referrerData Optional incoming deep link referrer data. + * @param isIncomingURL Whether the fetch is for deep link. + * @param visitedUrls Optional set of URLs already visited in this redirect chain to prevent cycles. + * @returns A promise that will resolve to an Action. + */ + async fetchAction(objectGraph, url, referrerData, isIncomingURL, visitedUrls = new Set()) { + var _a; + const routerResult = this.pageRouter.routedObjectForUrl(url); + if (!routerResult.object) { + // Urls fed into `fetchAction` can redirect to supported routes, thus we attempt to + // chain the redirect and pipe it back into `fetchAction`. + return await this.redirectAndRefetchActionIfPossible(objectGraph, routerResult.normalizedUrl, visitedUrls); + } + const builder = routerResult.object; + return await builder.handlePage(objectGraph, routerResult.normalizedUrl, (_a = routerResult.parameters) !== null && _a !== void 0 ? _a : {}, routerResult.matchedRuleIdentifier, referrerData, isIncomingURL); + } + /** + * Fetch the next page of a page. The returned page will have its `shelves`, `nextPage` properties set. + * @param pageToken The page token to use. + * @returns A promise that will resolve to a page. + */ + async fetchMoreOfPage(objectGraph, pageToken) { + const url = pageToken.url; + if (!url) { + throw new Error("Page token missing required `url` property"); + } + const routerResult = this.paginationRouter.routedObjectForUrl(url); + if (!routerResult.object) { + throw new Error(`fetchMoreOfPage: Unhandled pagination url: ${url}`); + } + const builder = routerResult.object; + return await builder.handlePagination(objectGraph, routerResult.normalizedUrl, routerResult.parameters, routerResult.matchedRuleIdentifier, pageToken); + } + /** + * Fetch the contents of multiple shelves, producing a shelf batch object. + * @param requests A hash of request keys to URL strings. + * @returns A promise that will resolve into multiple shelves. + */ + async fetchShelves(objectGraph, requests) { + // eslint-disable-next-line promise/param-names + const shelfRequestKeys = Object.keys(requests); + const shelfUrls = shelfRequestKeys.map((key) => requests[key]); + // Guaranteed never to throw an error + const shelfForUrl = async (shelfUrl) => { + if (isNothing(shelfUrl)) { + throw new Error(`fetchShelves: Null shelf url`); + } + const routerResult = this.shelfRouter.routedObjectForUrl(shelfUrl); + if (isNothing(routerResult.object)) { + throw new Error(`fetchShelves: Unhandled shelf url: ${shelfUrl}`); + } + const builder = routerResult.object; + const shelf = await builder.handleShelf(objectGraph, routerResult.normalizedUrl, routerResult.parameters, routerResult.matchedRuleIdentifier); + return shelf; + }; + // Map each URL to a promise and make them all optional + const resolvedShelves = await allOptional(shelfUrls.map(shelfForUrl)); + const batch = { + shelves: {}, + errors: {}, + }; + for (const [index, resolvedShelf] of resolvedShelves.entries()) { + const requestKey = shelfRequestKeys[index]; + if (isNothing(requestKey)) { + // This should never happen as we create a shelf request for each key + continue; + } + if (resolvedShelf.success) { + batch.shelves[requestKey] = resolvedShelf.value; + } + else { + // For rejected promises, access the error property + batch.errors[requestKey] = resolvedShelf.error; + } + } + const hasResults = Object.keys(batch.shelves).length > 0; + if (hasResults) { + return batch; + } + else { + const messages = Object.keys(batch.errors).map((key) => batch.errors[key].message); + if (messages.length === 0) { + throw new Error(`Could not load any shelves: ${JSON.stringify(requests)}`); + } + else { + throw new Error(messages.join("\n")); + } + } + } + // endregion + // region Redirect + /** + * Given an `url`, it will fire a GET request and then try to refetch the action for url if + * it was redirected. This is used for urls that are unknown, but redirects to a supported url. + * + * @param objectGraph + * @param {string} url to attempt to redirect with. This is expected to be normalized. + * @param {PromiseResolveFunction<models.Action>} resolve function to resolve promise to an `Action`. + * @param {PromiseRejectFunction} reject function to reject promise to `Error`. + * @param {Set<string>} visitedUrls Set of URLs already visited in this redirect chain to prevent cycles. + */ + async redirectAndRefetchActionIfPossible(objectGraph, url, visitedUrls = new Set()) { + // Check for redirect cycle + const urlString = url.toString(); + if (visitedUrls.has(urlString)) { + throw new Error(`redirectAndRefetchActionIfPossible: Redirect cycle detected for URL: ${urlString}`); + } + // Add current URL to visited set + visitedUrls.add(urlString); + const fetchResult = await tryAwait(objectGraph.network.fetch({ + url: urlString, + method: "GET", + })); + if (!fetchResult.success) { + throw new Error(`redirectAndRefetchActionIfPossible: Failed to fetch page at url: ${url}`); + } + const response = fetchResult.value; + if (this.hasGotoURLInResponse(objectGraph, response)) { + // If the response body contains a goto url fail silently as we will natively try to load the URL. + return new models.BlankAction(); + } + else if (response.status === 200 && response.redirected && response.url) { + // Only route if this was a redirect; otherwise we don't know how to handle this page + return await this.fetchAction(objectGraph, response.url, null, false, visitedUrls); + } + else { + throw new Error(`redirectAndRefetchActionIfPossible: Unhandled page url: ${url}`); + } + } + /** + * Given a `response`, it will check to see if the response body contains a goto URL + * A goto URL is returned by the server in a plist as a way of navigating to a new page + * AMSURLSession which is the native response handler looks for these URLs and tries to load them natively + * @param objectGraph + * @param {FetchResponse} the response body from the server + */ + hasGotoURLInResponse(objectGraph, response) { + if (serverData.isNullOrEmpty(response.body)) { + return false; + } + try { + const responseData = serverData.asJSONValue(objectGraph.plist.parse(response.body)); + const responseActionKind = serverData.asString(responseData, "action.kind"); + const responseActionUrl = serverData.asString(responseData, "action.url"); + if (responseActionKind === "Goto" && isSome(responseActionUrl)) { + return true; + } + return false; + } + catch { + return false; + } + } + // endregion + // region Testing + /** + * Fetch the builder for a given url. + * @param url The URL that the builder is associated with. + * @returns A builder if any was associated, or null if no match was found. + * @note FOR TESTING ONLY. + */ + fetchBuilder(url) { + const routerResult = this.pageRouter.routedObjectForUrl(url) || + this.shelfRouter.routedObjectForUrl(url) || + this.paginationRouter.routedObjectForUrl(url); + return routerResult ? routerResult.object : null; + } +} +//# sourceMappingURL=routing.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/builders/url-mapping-utils.js b/node_modules/@jet-app/app-store/tmp/src/common/builders/url-mapping-utils.js new file mode 100644 index 0000000..5d8e1ac --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/builders/url-mapping-utils.js @@ -0,0 +1,42 @@ +/** + * Adds the required url query params to a media API request for Contingent Items + */ +export function configureContingentItemsForMediaRequest(mediaApiRequest) { + mediaApiRequest.includingScopedRelationships("contingent-items", ["trunk-app", "branch-app", "branch"]); + mediaApiRequest.includingViews(["events-and-offers", "standalone-merchandised-in-apps"]); + mediaApiRequest.includingScopedAttributes("contingent-items", ["name", "subtitle", "additionalTerms"]); + mediaApiRequest.includingMetaKeys("contingent-items:branch", ["contingentItemOffer"]); + mediaApiRequest.includingMetaKeys("contingent-items:association.trunks", ["useTrunkArtwork"]); + mediaApiRequest.includingAssociateKeys("contingent-items", ["trunks"]); +} +/** + * Adds the required url query params to a groupings API request for Contingent Items + */ +export function configureContingentItemsForGroupingRequest(mediaApiRequest) { + mediaApiRequest.includingScopedRelationships("contingent-items", ["trunk-app", "branch-app", "branch"]); + mediaApiRequest.includingScopedAttributes("contingent-items", ["name", "subtitle", "additionalTerms"]); + mediaApiRequest.includingMetaKeys("contingent-items:branch", ["contingentItemOffer"]); + mediaApiRequest.includingMetaKeys("contingent-items:association.trunks", ["useTrunkArtwork"]); + mediaApiRequest.includingAssociateKeys("contingent-items", ["trunks"]); +} +export function configureTagsForMediaRequest(mediaApiRequest) { + mediaApiRequest.enablingFeature("tags"); + mediaApiRequest.includingScopedAttributes("tags", ["editorialArtwork"]); + mediaApiRequest.addingQuery("associate[tags]", "apps"); + mediaApiRequest.includingScopedRelationships("editorial-elements", ["contents", "see-all-contents"]); +} +/** + * Adds the required url query params to a media API request for Winback Offer items + */ +export function configureOfferItemsForMediaRequest(mediaApiRequest) { + mediaApiRequest.includingScopedRelationships("offer-items", ["salables"]); + mediaApiRequest.includingScopedAttributes("offer-items", [ + "title", + "subtitle", + "additionalTerms", + "redemptionExpirationDate", + ]); + mediaApiRequest.includingMetaKeys("offer-items:salables", ["discountOffer"]); + mediaApiRequest.includingKindsKeys("offer-items", ["winback"]); +} +//# sourceMappingURL=url-mapping-utils.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/builders/url-mapping.js b/node_modules/@jet-app/app-store/tmp/src/common/builders/url-mapping.js new file mode 100644 index 0000000..fdaa85b --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/builders/url-mapping.js @@ -0,0 +1,627 @@ +import { isNothing, isSome } from "@jet/environment/types/optional"; +import * as mediaAPI from "@apple-media-services/media-api"; +import * as serverData from "../../foundation/json-parsing/server-data"; +import * as mediaDataFetching from "../../foundation/media/data-fetching"; +import * as urlBuilder from "../../foundation/media/url-builder"; +import { Host, Parameters, Path, previewHosts, ProductPageParameters } from "../../foundation/network/url-constants"; +import { configureMediaRequestForGameRecommendations } from "../../gameservicesui/src/foundation/media-api/requests/game-recommendations"; +import * as client from "../../foundation/wrappers/client"; +import { isAdPlacementEnabled } from "../ads/ad-common"; +import * as appPromotionsCommon from "../app-promotions/app-promotions-common"; +import * as privacySuppression from "../privacy/privacy-suppression"; +import * as productPageVariants from "../product-page/product-page-variants"; +import * as mediaRequestUtils from "./url-mapping-utils"; +import { addDeveloperRequestProperties, makeDeveloperRequest } from "../developer/developer-request"; +import { isReviewSummaryEnabled } from "../product-page/reviews"; +import { isProductAccessibilityLabelsEnabled, shouldSuppressAccessibilityLabelsForAdamId, shouldSuppressAccessibilityLabelsForBundleId, } from "../accessibility/accessibility-common"; +import { buildArticlePageRequest } from "../today/article-request"; +import { makeRoutableArticlePageIntent } from "../../api/intents/routable-article-page-intent"; +import { getLocale } from "../locale"; +import { shouldUsePrerenderedIconArtwork } from "../content/content"; +import { AppEventsAttributes } from "../../gameservicesui/src/foundation/media-api/requests/recommendation-request-types"; +import { MediaApiConfiguration } from "../../gameservicesui/src/foundation/media-api/media-api-configuration"; +const appsResourceAttributes = [ + "description", + "fileSizeByDevice", + "messagesScreenshots", + "minimumOSVersion", + "privacyPolicyUrl", + "promotionalText", + "screenshotsByType", + "supportsFunCamera", + "supportURLForLanguage", + "versionHistory", + "videoPreviewsByType", + "websiteUrl", + "expectedReleaseDateDisplayFormat", + "requirementsByDeviceFamily", + "remoteControllerRequirement", + "backgroundAssetsInfo", + "backgroundAssetsInfoWithOptional", + "supportsSharePlay", + "installSizeByDeviceInBytes", + "miniGamesDeepLink", + "gameDisplayName", +]; +// exporting this will be bad in the long run +// adding for <rdar://problem/39578622> Catagories: Games results in cannot connect to app store. +// should be removed when we stop handling media api urls in the router +export function isMediaUrl(objectGraph, url) { + const host = url.host; + if (serverData.isNull(host)) { + return false; + } + const mediaApiHosts = [ + objectGraph.bag.mediaHost, + objectGraph.bag.mediaEdgeHost(objectGraph), + objectGraph.bag.mediaEdgeSearchHost, + ]; + for (const mediaApiHost of mediaApiHosts) { + if (serverData.isNull(mediaApiHost)) { + continue; + } + if (host.indexOf(mediaApiHost) !== -1) { + return true; + } + } + return false; +} +export function hrefToRoutableUrl(objectGraph, href, additionalQueryParams) { + if (isNothing(href)) { + return undefined; + } + if (preprocessor.GAMES_TARGET) { + const request = new mediaAPI.Request(objectGraph, href); + if (isSome(additionalQueryParams)) { + for (const [key, value] of Object.entries(additionalQueryParams)) { + request.addingQuery(key, value); + } + } + return mediaAPI.buildURLFromRequest(new MediaApiConfiguration(objectGraph), request).toString(); + } + else { + const request = new mediaDataFetching.Request(objectGraph, href).addingQueryValues(additionalQueryParams); + return urlBuilder.buildURLFromRequest(objectGraph, request).toString(); + } +} +// region Grouping +export function mediaApiGroupingURLFromHref(objectGraph, href) { + const attributes = [ + "editorialArtwork", + "editorialVideo", + "isAppleWatchSupported", + "requiredCapabilities", + "minimumOSVersion", + "expectedReleaseDateDisplayFormat", + ]; + if (objectGraph.appleSilicon.isSupportEnabled) { + attributes.push("macRequiredCapabilities"); + } + if (objectGraph.client.isMac) { + attributes.push("hasMacIPAPackage"); + } + if (objectGraph.bag.enableUpdatedAgeRatings) { + attributes.push("ageRating"); + } + if (shouldUsePrerenderedIconArtwork(objectGraph)) { + attributes.push("iconArtwork"); + } + // <rdar://problem/56198088> Catalyst apps have clickable 'get' button on 10.14. + // - minimumOSVersion required to filter catalyst apps from groupings. + const mediaApiRequest = new mediaDataFetching.Request(objectGraph, href) + .includingAttributes(attributes) + .includingAgeRestrictions() + .includingMacOSCompatibleIOSAppsWhenSupported(); + if (appPromotionsCommon.appEventsAreEnabled(objectGraph)) { + mediaApiRequest.enablingFeature("appEvents"); + mediaApiRequest.includingScopedAttributes("app-events", AppEventsAttributes); + mediaApiRequest.includingScopedRelationships("app-events", ["app"]); + mediaApiRequest.includingScopedRelationships("editorial-item-shelves", ["app-events"]); + } + const context = mediaDataFetching.defaultGroupingContextForClient(objectGraph); + if (serverData.isDefinedNonNull(context)) { + mediaApiRequest.addingQuery("contexts", context); + } + return mediaApiRequest; +} +// endregion +// region Editorial Stories +export function mappedMediaApiEditorialItemURL(objectGraph, url, isIncomingURL) { + const pathComponents = url.pathComponents(); + if (pathComponents.indexOf("story") < 0 && + pathComponents.indexOf("editorialItem") < 0 && + pathComponents.indexOf("viewEditorialItem") < 0 && + url.host !== Host.spamBlockingExtensions && + url.host !== Host.sharePlayApps && + url.host !== Host.buddyOnboarding) { + throw new Error(`Unable to map ${url.build()} to a media api url`); + } + let editorialItemId; + if (url.host === Host.spamBlockingExtensions) { + // Populate editorial item ID for spam blocking extensions. + editorialItemId = objectGraph.props.asString("spamBlockingExtensionsEditorialItemID"); + } + else if (url.host === Host.sharePlayApps) { + editorialItemId = objectGraph.bag.sharePlayAppsEditorialItemId; + } + else if (url.host === Host.buddyOnboarding) { + editorialItemId = objectGraph.bag.buddyOnboardingEditorialItemId; + } + else { + // Otherwise extract the ID from the URL. + editorialItemId = extractedIdFromURL(url); + } + if (!serverData.isNumber(editorialItemId)) { + throw new Error(`Unable to map ${url.build()} to a media api url`); + } + if (!serverData.isNumber(editorialItemId)) { + throw new Error(`Unable to map ${url} to a media api url`); + } + const mediaApiRequest = buildArticlePageRequest(objectGraph, makeRoutableArticlePageIntent({ + ...getLocale(objectGraph), + id: editorialItemId, + }), isIncomingURL); + const editorialCardId = url.query[Parameters.editorialCardId]; + if (serverData.isDefinedNonNullNonEmpty(editorialCardId)) { + mediaApiRequest.withFilter("canvas:cardId", editorialCardId); + } + const previewParam = url.query[Parameters.preview]; + if (previewHosts.has(url.host) && (previewParam === null || previewParam === void 0 ? void 0 : previewParam.length) > 0) { + mediaApiRequest.addingQuery("preview", previewParam); + mediaApiRequest.isStorePreviewRequest = true; + } + return mediaApiRequest.attributingTo(url.build()); +} +// endregion +// region Product +const entityIdRegex = /id([0-9]+)\/?$/i; +export function extractedIdFromURL(url) { + // 1. Look at the query parameter for the id + let extractedId = url.query["id"]; + // 2. Consider the last path component /id<id> + const pathName = url.pathname; + if (serverData.isNull(extractedId) && (pathName === null || pathName === void 0 ? void 0 : pathName.length) > 0) { + const match = entityIdRegex.exec(pathName); + if (match && match.length > 1) { + extractedId = match[1]; + } + } + // 3. At this point, if the ID is not found, try looking for /<id> instead, since this is what the last path + // component looks like in QA now. + if (!serverData.isNumber(extractedId)) { + const pathComponents = url.pathComponents(); + if (serverData.isDefinedNonNullNonEmpty(pathComponents)) { + extractedId = pathComponents[pathComponents.length - 1]; + } + } + return extractedId; +} +const productRedirectionTable = { + "915249334": "1462947752", // rdar://49929244 (SAD: Redirect from the old 2nd party Shortcuts product page to the new 1st party page on iOS 13) +}; +export function mappedMediaApiProductURL(objectGraph, url, includeUnlistedApps = false) { + var _a, _b; + let productId = extractedIdFromURL(url); + if (!serverData.isNumber(productId)) { + throw new Error(`Unable to map ${url.build()} to a media api url`); + } + // Redirect to a different product if needed + if (productId in productRedirectionTable) { + productId = productRedirectionTable[productId]; + } + let resourceType = "apps"; + let attributes = appsResourceAttributes; + let relationships = []; + if (preprocessor.GAMES_TARGET) { + relationships = ["alternate-apps"]; + } + else { + relationships = [ + "customers-also-bought-apps", + "reviews", + "app-bundles", + "top-in-apps", + "related-editorial-items", + "alternate-apps", + ]; + } + const pathComponents = (_a = url.pathname) === null || _a === void 0 ? void 0 : _a.split("/"); + const isBundle = isSome(pathComponents) && pathComponents.includes(Path.productBundle); + if (isBundle) { + resourceType = "app-bundles"; + attributes = [ + "screenshotsByType", + "videoPreviewsByType", + "minimumOSVersion", + "requirementsByDeviceFamily", + "remoteControllerRequirement", + ]; + relationships = ["apps", "reviews", "related-editorial-items"]; + } + if (!preprocessor.GAMES_TARGET) { + relationships.push("developer-other-apps"); + } + if (objectGraph.bag.enablePrivacyNutritionLabels && + !privacySuppression.shouldSuppressPrivacyInformationForAdamId(objectGraph, productId)) { + attributes.push("privacy"); + } + if (isProductAccessibilityLabelsEnabled(objectGraph) && + !shouldSuppressAccessibilityLabelsForAdamId(objectGraph, productId)) { + attributes.push("accessibility"); + } + if (objectGraph.appleSilicon.isSupportEnabled) { + attributes.push("macRequiredCapabilities"); + } + if (objectGraph.client.isMac) { + attributes.push("hasMacIPAPackage"); + } + if (objectGraph.bag.enableUpdatedAgeRatings) { + attributes.push("ageRating"); + } + if (shouldUsePrerenderedIconArtwork(objectGraph)) { + attributes.push("iconArtwork"); + } + if (objectGraph.bag.enableLicenses) { + attributes.push("licenses"); + } + // TODO <rdar://problem/49763626> Product Page should not request developer information if supportsArcade is false + // We need to pipe the supportsArcade information from the sidepack, through the lockup, to the productPage action URL + // until then, isArcade will always be null. + // If no arcade affiliation is specified, or the product definitely isArcade, fetch the developer genre. + const isArcade = serverData.asBoolean(url.query[Parameters.isArcade]); + if (isArcade === null || isArcade) { + attributes.push("minPlayers", "maxPlayers", "editorialVideo"); + relationships.push("developer"); + } + if (objectGraph.client.isVision) { + attributes.push("compatibilityControllerRequirement"); + attributes.push("isHighMotion"); + if (objectGraph.featureFlags.isEnabled("spatial_controllers_2025A")) { + attributes.push("spatialControllerRequirement"); + } + } + if (objectGraph.bag.enableSellerInfo) { + attributes.push("sellerInfo"); + } + // Extends the API to include Internet Content Provider Info + if (objectGraph.bag.enableSellerICPAnnotation) { + attributes.push("internetContentProviderInfo"); + } + // Extends the API to include supported Game Center features. + if (objectGraph.bag.gameCenterExtendSupportedFeatures) { + attributes.push("supportedGameCenterFeatures"); + } + // Extends the API to include Review Summary + if (isReviewSummaryEnabled(objectGraph)) { + relationships.push("review-summary"); + } + if (preprocessor.GAMES_TARGET) { + attributes.push("gamesUrl"); + attributes.push("isEligibleForGamesApp"); + } + const mediaApiRequest = new mediaDataFetching.Request(objectGraph) + .withIdOfType(productId, resourceType) + .includingAdditionalPlatforms(mediaDataFetching.defaultAdditionalPlatformsForClient(objectGraph)) + .includingAgeRestrictions() + .includingRelationships(relationships) + .includingAttributes(attributes) + .includingMacOSCompatibleIOSAppsWhenSupported(true) + .usingCustomAttributes(productPageVariants.shouldFetchCustomAttributes(objectGraph)) + .includingViews(["categorizations"]); + if (!preprocessor.GAMES_TARGET) { + mediaApiRequest.addingRelationshipLimit("reviews", mediaDataFetching.defaultProductPageReviewsLimitForClient(objectGraph)); + } + if (preprocessor.GAMES_TARGET) { + mediaApiRequest.includingScopedRelationships("apps", ["activities", "challenges"]); + mediaApiRequest.includingViews(["customers-also-bought-games", "developer-other-games"]); + mediaApiRequest.enablingFeature("unlistedApps"); + if (objectGraph.debugSettings.enableHighlightsFromProductPageResponse) { + configureMediaRequestForGameRecommendations(mediaApiRequest); + } + } + if (((_b = url.query[Parameters.productVariantID]) === null || _b === void 0 ? void 0 : _b.length) > 0) { + mediaApiRequest.addingQuery(Parameters.productVariantID, url.query[Parameters.productVariantID]); + } + // If a `minExternalVersionId` is specified, pass this through to the MAPI request + const minExternalVersionId = url.query[ProductPageParameters.minExternalVersionId]; + if (serverData.isDefinedNonNull(minExternalVersionId)) { + mediaApiRequest.addingQuery(ProductPageParameters.minExternalVersionId, minExternalVersionId); + } + // Disabling for bundles due to rdar://78542145 ([REG] Pre-Prod JS: SkySeed: App bundle product page does not load) + if (appPromotionsCommon.appEventsAreEnabled(objectGraph) && !isBundle) { + if (!appPromotionsCommon.appContingentItemsAreEnabled(objectGraph)) { + mediaApiRequest.includingScopedRelationships("apps", ["app-events"]); + } + mediaApiRequest.includingScopedAttributes("app-events", AppEventsAttributes); + mediaApiRequest.includingScopedAvailableIn("app-events", ["future"]); + } + if (appPromotionsCommon.appContingentItemsAreEnabled(objectGraph)) { + mediaRequestUtils.configureContingentItemsForMediaRequest(mediaApiRequest); + } + if (appPromotionsCommon.appOfferItemsAreEnabled(objectGraph)) { + mediaRequestUtils.configureOfferItemsForMediaRequest(mediaApiRequest); + } + if (isAdPlacementEnabled(objectGraph, "productPageYMAL")) { + mediaApiRequest.enablingFeature("cabAdSupport"); + } + if (includeUnlistedApps) { + mediaApiRequest.enablingFeature("unlistedApps"); + } + if (!appPromotionsCommon.appContingentItemsAreEnabled(objectGraph)) { + mediaApiRequest.includingRelationships(["merchandised-in-apps"]); + } + return mediaApiRequest.attributingTo(url.build()); +} +// endregion +// region Developer +export function mediaApiDeveloperURLFromHref(objectGraph, href) { + const mediaApiRequest = new mediaDataFetching.Request(objectGraph, href); + return addDeveloperRequestProperties(objectGraph, mediaApiRequest); +} +export function mappedMediaApiDeveloperURL(objectGraph, url) { + const developerId = extractedIdFromURL(url); + if (!serverData.isNumber(developerId)) { + throw new Error(`Unable to map ${url.build()} to a media api url`); + } + return makeDeveloperRequest(objectGraph, developerId); +} +// endregion +// region Charts +/** + * Creates a Media API request for one or more charts using provided critera. + * @param objectGraph The App Store object graph. + * @param genreId The genre for the chart request. + * @param charts The list of charts for the request. + * @param ageBandId The age band to include in the request. + * @param clientIdentifier The identifier of the current client. + * @returns A Media API request for one or more charts using provided critera. + */ +export function mediaApiChartRequestForGenre(objectGraph, genreId, charts, ageBandId = null, clientIdentifier = objectGraph.host.clientIdentifier) { + const attributes = ["isAppleWatchSupported", "requiredCapabilities", "minimumOSVersion"]; + if (objectGraph.appleSilicon.isSupportEnabled) { + attributes.push("macRequiredCapabilities"); + } + if (objectGraph.client.isMac) { + attributes.push("hasMacIPAPackage"); + } + const mediaApiRequest = new mediaDataFetching.Request(objectGraph) + .forType("charts") + .addingQuery("genre", genreId) + .includingAttributes(attributes) + .addingQuery("types", "apps") + .includingMacOSCompatibleIOSAppsWhenSupported(true) + .includingAdditionalPlatforms(mediaDataFetching.defaultAdditionalPlatformsForClient(objectGraph)) + .usingCustomAttributes(productPageVariants.shouldFetchCustomAttributes(objectGraph)); + if (serverData.isDefinedNonNullNonEmpty(charts)) { + mediaApiRequest.addingQuery(Parameters.charts, charts); + } + if (clientIdentifier === client.watchIdentifier) { + mediaApiRequest.addingContext("watch"); + } + else if (clientIdentifier === client.messagesIdentifier) { + mediaApiRequest.addingContext("messages"); + } + if (objectGraph.appleSilicon.isSupportEnabled) { + mediaApiRequest.includingAdditionalPlatforms(["iphone", "ipad"]); + } + if (isSome(ageBandId)) { + mediaApiRequest.addingQuery(Parameters.ages, ageBandId); + } + return mediaApiRequest; +} +export function mediaApiChartsRequest(objectGraph, genreId, ageBandId = null) { + let request = new mediaDataFetching.Request(objectGraph) + .forType("charts") + .addingQuery("types", "apps") + .addingQuery("genre", genreId) + .includingMacOSCompatibleIOSAppsWhenSupported(true); + if (ageBandId) { + request = request.addingQuery("ages", ageBandId); + } + if (objectGraph.appleSilicon.isSupportEnabled) { + request = request.includingAdditionalPlatforms(["iphone", "ipad"]); + } + return request; +} +// endregion +// region Lookups +export function lookupURLForProductId(objectGraph, productId, isPurchasesProduct, productVariantID, isBundle, includeUnlistedApps) { + const attributes = appsResourceAttributes; + if (objectGraph.bag.enablePrivacyNutritionLabels && + !privacySuppression.shouldSuppressPrivacyInformationForAdamId(objectGraph, productId)) { + attributes.push("privacy"); + // The "web" client needs the full privacy information in the initial page load; + // unlike other platforms, it does not load the full details on a separate page + if (objectGraph.client.isWeb) { + attributes.push("privacyDetails"); + } + } + if (isProductAccessibilityLabelsEnabled(objectGraph) && + !shouldSuppressAccessibilityLabelsForAdamId(objectGraph, productId)) { + attributes.push("accessibility"); + // The web client requires complete accessibility information on initial page load. + // Unlike other platforms that load full details on separate pages, + // the web client displays this information in a modal overlay. + if (objectGraph.client.isWeb) { + attributes.push("accessibilityDetails"); + } + } + if (objectGraph.appleSilicon.isSupportEnabled) { + attributes.push("macRequiredCapabilities"); + } + if (objectGraph.client.isMac) { + attributes.push("hasMacIPAPackage"); + } + if (objectGraph.client.isVision) { + attributes.push("compatibilityControllerRequirement"); + attributes.push("isHighMotion"); + if (objectGraph.featureFlags.isEnabled("spatial_controllers_2025A")) { + attributes.push("spatialControllerRequirement"); + } + } + if (objectGraph.bag.enableUpdatedAgeRatings) { + attributes.push("ageRating"); + } + if (shouldUsePrerenderedIconArtwork(objectGraph)) { + attributes.push("iconArtwork"); + } + if (objectGraph.bag.enableLicenses) { + attributes.push("licenses"); + } + // Extends the API to include supported Game Center features. + if (objectGraph.bag.gameCenterExtendSupportedFeatures) { + attributes.push("supportedGameCenterFeatures"); + } + const mediaApiRequest = new mediaDataFetching.Request(objectGraph) + .withIdOfType(productId, "apps") + .includingAdditionalPlatforms(mediaDataFetching.defaultAdditionalPlatformsForClient(objectGraph)) + .includingAttributes(attributes) + .includingRelationships([ + "developer", + "customers-also-bought-apps", + "developer-other-apps", + "reviews", + "app-bundles", + "top-in-apps", + "related-editorial-items", + "alternate-apps", + ]) + .addingRelationshipLimit("reviews", mediaDataFetching.defaultProductPageReviewsLimitForClient(objectGraph)) + .includingMacOSCompatibleIOSAppsWhenSupported(true) + .usingCustomAttributes(productPageVariants.shouldFetchCustomAttributes(objectGraph)); + if (objectGraph.client.isWeb) { + // Ensure that the "web" client fully hydrates all PDP shelves + mediaApiRequest + .includingScopedSparseLimit("apps:customers-also-bought-apps", 40) + .includingScopedSparseLimit("apps:developer-other-apps", 40) + .includingScopedSparseLimit("apps:related-editorial-items", 40); + // Ensures web can render the DSA information in the annotation shelf + if (objectGraph.bag.enableSellerInfo) { + attributes.push("sellerInfo"); + } + } + if (isPurchasesProduct) { + mediaApiRequest.addingQuery("availability", "redownload"); + } + if (isSome(productVariantID)) { + mediaApiRequest.addingQuery(Parameters.productVariantID, productVariantID); + } + if (includeUnlistedApps) { + mediaApiRequest.enablingFeature("unlistedApps"); + } + // Disabling for bundles due to rdar://78542145 ([REG] Pre-Prod JS: SkySeed: App bundle product page does not load) + if (appPromotionsCommon.appEventsAreEnabled(objectGraph) && !isBundle) { + if (!appPromotionsCommon.appContingentItemsAreEnabled(objectGraph)) { + mediaApiRequest.includingScopedRelationships("apps", ["app-events"]); + } + mediaApiRequest.includingScopedAttributes("app-events", AppEventsAttributes); + mediaApiRequest.includingScopedAvailableIn("app-events", ["future"]); + } + if (appPromotionsCommon.appContingentItemsAreEnabled(objectGraph)) { + mediaRequestUtils.configureContingentItemsForMediaRequest(mediaApiRequest); + } + if (appPromotionsCommon.appOfferItemsAreEnabled(objectGraph)) { + mediaRequestUtils.configureOfferItemsForMediaRequest(mediaApiRequest); + } + if (!appPromotionsCommon.appContingentItemsAreEnabled(objectGraph)) { + mediaApiRequest.includingRelationships(["merchandised-in-apps"]); + } + // Extends the API to include Review Summary + if (isReviewSummaryEnabled(objectGraph)) { + mediaApiRequest.includingRelationships(["review-summary"]); + } + if (isAdPlacementEnabled(objectGraph, "productPageYMAL")) { + mediaApiRequest.enablingFeature("cabAdSupport"); + } + if (objectGraph.client.isMac) { + attributes.push("hasMacIPAPackage"); + } + return mediaApiRequest; +} +export function lookupURLForBundleId(objectGraph, bundleId, productVariantID, includeUnlistedApps) { + const attributes = appsResourceAttributes; + if (objectGraph.bag.enablePrivacyNutritionLabels && + !privacySuppression.shouldSuppressPrivacyInformationForBundleId(objectGraph, bundleId)) { + attributes.push("privacy"); + } + if (isProductAccessibilityLabelsEnabled(objectGraph) && + !shouldSuppressAccessibilityLabelsForBundleId(objectGraph, bundleId)) { + attributes.push("accessibility"); + } + if (objectGraph.appleSilicon.isSupportEnabled) { + attributes.push("macRequiredCapabilities"); + } + if (objectGraph.client.isMac) { + attributes.push("hasMacIPAPackage"); + } + if (objectGraph.client.isVision) { + attributes.push("compatibilityControllerRequirement"); + attributes.push("isHighMotion"); + if (objectGraph.featureFlags.isEnabled("spatial_controllers_2025A")) { + attributes.push("spatialControllerRequirement"); + } + } + if (objectGraph.bag.enableUpdatedAgeRatings) { + attributes.push("ageRating"); + } + if (shouldUsePrerenderedIconArtwork(objectGraph)) { + attributes.push("iconArtwork"); + } + if (objectGraph.bag.enableLicenses) { + attributes.push("licenses"); + } + const mediaApiRequest = new mediaDataFetching.Request(objectGraph) + .forType("apps") + .withFilter("bundleId", bundleId) + .includingAdditionalPlatforms(mediaDataFetching.defaultAdditionalPlatformsForClient(objectGraph)) + .includingRelationships([ + "developer", + "customers-also-bought-apps", + "developer-other-apps", + "reviews", + "app-bundles", + "top-in-apps", + "related-editorial-items", + "alternate-apps", + ]) + .includingAttributes(attributes) + .addingRelationshipLimit("reviews", mediaDataFetching.defaultProductPageReviewsLimitForClient(objectGraph)) + .includingMacOSCompatibleIOSAppsWhenSupported(true) + .usingCustomAttributes(productPageVariants.shouldFetchCustomAttributes(objectGraph)); + if (isSome(productVariantID)) { + mediaApiRequest.addingQuery(Parameters.productVariantID, productVariantID); + } + if (isAdPlacementEnabled(objectGraph, "productPageYMAL")) { + mediaApiRequest.enablingFeature("cabAdSupport"); + } + if (includeUnlistedApps) { + mediaApiRequest.enablingFeature("unlistedApps"); + } + if (appPromotionsCommon.appContingentItemsAreEnabled(objectGraph)) { + mediaApiRequest.includingViews(["standalone-merchandised-in-apps"]); + } + else { + mediaApiRequest.includingRelationships(["merchandised-in-apps"]); + } + if (objectGraph.client.isMac) { + attributes.push("hasMacIPAPackage"); + } + return mediaApiRequest; +} +export function mediaApiProductSeeAllRequest(objectGraph, productId, relationship) { + const mediaApiRequest = new mediaDataFetching.Request(objectGraph) + .withIdOfType(productId, "apps") + .includingAdditionalPlatforms(mediaDataFetching.defaultAdditionalPlatformsForClient(objectGraph)) + .usingCustomAttributes(productPageVariants.shouldFetchCustomAttributes(objectGraph)) + .includingRelationships([relationship, "apps", "developer"]) + .includingScopedSparseLimit(`apps:${relationship}`, 20); + if (appPromotionsCommon.appContingentItemsAreEnabled(objectGraph)) { + mediaRequestUtils.configureContingentItemsForMediaRequest(mediaApiRequest); + } + if (appPromotionsCommon.appOfferItemsAreEnabled(objectGraph)) { + mediaRequestUtils.configureOfferItemsForMediaRequest(mediaApiRequest); + } + return mediaApiRequest; +} +// endregion +//# sourceMappingURL=url-mapping.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/categories.js b/node_modules/@jet-app/app-store/tmp/src/common/categories.js new file mode 100644 index 0000000..544f932 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/categories.js @@ -0,0 +1,149 @@ +import * as validation from "@jet/environment/json/validation"; +import * as models from "../api/models"; +import * as serverData from "../foundation/json-parsing/server-data"; +import { isNothing } from "@jet/environment"; +import * as mediaDataFetching from "../foundation/media/data-fetching"; +import * as client from "../foundation/wrappers/client"; +import { contentAttributeAsDictionary } from "./content/attributes"; +import * as content from "./content/content"; +/** + * Creates a category list context for child categories of a provided genre, e.g. Games or Apps. + * + * @param rootGenreId The genreId that describes the parent of the category list, regardless of whether the root is included in the list. + * @returns The created category list context, e.g. Apps, Games, or All. + */ +export function createContextFromGenre(rootGenreId) { + if (rootGenreId === 36 /* constants.GenreIds.Apps */.toString()) { + return 1 /* models.CategoryListContext.Apps */; + } + else if (rootGenreId === 6014 /* constants.GenreIds.Games */.toString()) { + return 2 /* models.CategoryListContext.Games */; + } + else { + return 0 /* models.CategoryListContext.All */; + } +} +/** + * Create a category list context for given categories by inspecting the first item, e.g. "All Games", "All Apps". + * + * @param categoryList The category list to create a category list context for. + * @returns The created category list context, e.g. Apps, Games, or All. + */ +export function createContextFromList(categoryList) { + const categories = categoryList.categories; + if (serverData.isDefinedNonNullNonEmpty(categories)) { + const firstCategoryGenreId = categories[0].genreId; + if (firstCategoryGenreId === 6014 /* constants.GenreIds.Games */.toString()) { + return 2 /* models.CategoryListContext.Games */; + } + else if (firstCategoryGenreId === 36 /* constants.GenreIds.Apps */.toString()) { + return 1 /* models.CategoryListContext.Apps */; + } + } + return 0 /* models.CategoryListContext.All */; +} +export function createRequest(objectGraph, genreId, clientIdentifier = objectGraph.host.clientIdentifier, additionalPlatforms = []) { + if (!genreId) { + genreId = objectGraph.client.isMac ? 39 /* constants.GenreIds.MacApps */ : 36 /* constants.GenreIds.Apps */; + } + const categoriesRequest = new mediaDataFetching.Request(objectGraph) + .forType("categories") + .includingAdditionalPlatforms(additionalPlatforms) + .includingMacOSCompatibleIOSAppsWhenSupported(true) + .includingAgeRestrictions() + .addingQuery("genre", `${genreId}`); + if (clientIdentifier === client.watchIdentifier) { + categoriesRequest.addingContext("watch"); + } + else if (clientIdentifier === client.messagesIdentifier) { + categoriesRequest.addingContext("messages"); + } + return categoriesRequest; +} +/** + * Creates a category list model from a server response. + * + * @param objectGraph The dependency graph for the App Store. + * @param response The JSON data returned by the server. + * @param includeRoot Whether or not to include the root category in the subcategory list, e.g. "All". + * @returns The created category list model. + */ +export function categoryListFromApiResponse(objectGraph, response, includeRoot = true) { + return validation.context("categoryListFromApiResponse", () => { + const categoriesData = serverData.asArrayOrEmpty(response, "results.categories"); + const rootCategory = categoryFromApiResponse(objectGraph, serverData.asDictionary(categoriesData, "0"), includeRoot); + if (!rootCategory) { + return null; + } + return new models.CategoryList(rootCategory.children); + }); +} +/** + * Creates a category model from a server response. + * + * @param objectGraph The dependency graph for the App Store. + * @param categoryData The JSON data from the server response for this individual category item. + * @param includeRoot Whether or not to include the root category in the child categories, e.g. "All". + * @returns The created category model. + */ +export function categoryFromApiResponse(objectGraph, categoryData, includeRoot = true) { + return validation.context("categoryFromApiResponse", () => { + if (!categoryData) { + return null; + } + const name = serverData.asString(categoryData, "name"); + const genre = serverData.asString(categoryData, "genre"); + const ages = serverData.asString(categoryData, "ages"); + const artwork = content.artworkFromApiArtwork(objectGraph, serverData.asDictionary(categoryData, "artwork"), { + allowingTransparency: true, + useCase: 20 /* content.ArtworkUseCase.CategoryIcon */, + }); + const children = serverData + .asArrayOrEmpty(categoryData, "children") + .map((childCategory) => { + return categoryFromApiResponse(objectGraph, serverData.asDictionary(childCategory), includeRoot); + }) + .filter((category) => serverData.isDefinedNonNull(category)); + const sortedChildren = sortCategories(objectGraph, children); + const label = serverData.asString(categoryData, "label"); + if (label && includeRoot) { + // Add root to children to show "All" category + sortedChildren.unshift(new models.Category(label, genre, artwork, ages, [])); + } + return new models.Category(name, genre, artwork, ages, sortedChildren); + }); +} +function sortCategories(objectGraph, categories) { + return categories.sort((category1, category2) => { + try { + return category1.name.localeCompare(category2.name, objectGraph.loc.safeIdentifier, { usage: "sort" }); + } + catch (e) { + return 0; + } + }); +} +export function searchAdditionalPlatforms(objectGraph) { + switch (objectGraph.client.deviceType) { + case "pad": + return ["iphone"]; + default: + return []; + } +} +export function categoryArtworkData(objectGraph, data, isMonochrome = false, forceFallbackArtwork = false, allowFallback = true) { + const attributePath = isMonochrome ? "contentIconTrimmedMonochrome" : "contentIconTrimmed"; + const fallbackAttributePath = "brandLogo"; + const editorialArtwork = contentAttributeAsDictionary(objectGraph, data, "editorialArtwork"); + if (forceFallbackArtwork) { + return serverData.asDictionary(editorialArtwork, fallbackAttributePath); + } + else { + let artworkData = serverData.asDictionary(editorialArtwork, attributePath); + if (allowFallback && isNothing(artworkData)) { + artworkData = serverData.asDictionary(editorialArtwork, fallbackAttributePath); + } + return artworkData; + } +} +//# sourceMappingURL=categories.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/charts/charts-hub.js b/node_modules/@jet-app/app-store/tmp/src/common/charts/charts-hub.js new file mode 100644 index 0000000..7c2da09 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/charts/charts-hub.js @@ -0,0 +1,39 @@ +import { ChartsHubChart, ChartsHubPage, FlowAction } from "../../api/models"; +import { makeChartsPageIntent } from "../../api/intents/charts-page-intent"; +import { makeChartsPageURL } from "./charts-page-url"; +import { getLocale } from "../locale"; +import { getPlatform } from "../preview-platform"; +function seeAllActionForChart(objectGraph, chart) { + const selectedChart = chart.segments[chart.initialSegmentIndex].chart; + const seeAllAction = new FlowAction("topCharts"); + const destinationIntent = makeChartsPageIntent({ + ...getLocale(objectGraph), + ...getPlatform(objectGraph), + genreId: chart.genreId, + chart: selectedChart, + }); + seeAllAction.destination = destinationIntent; + seeAllAction.pageUrl = makeChartsPageURL(objectGraph, destinationIntent); + return seeAllAction; +} +/** + * Create a {@linkcode TopChart} from a {@linkcode TopChartsPage} without any of the + * page-level properties defined + */ +function topChartFromPage(objectGraph, page, title) { + const chart = new ChartsHubChart(page.genreId, page.ageBandId, title, page.segments, page.categoriesButtonTitle, page.categories); + chart.initialSegmentIndex = page.initialSegmentIndex; + chart.seeAllAction = seeAllActionForChart(objectGraph, chart); + return chart; +} +/** + * Render a {@linkcode ChartsHubPage} from constituent {@linkcode TopChartsPage}s + */ +export function renderChartsHub(objectGraph, appsPage, gamesPage) { + const appsCharts = topChartFromPage(objectGraph, appsPage, objectGraph.loc.string("TopCharts.Hub.Apps.Title")); + const gamesCharts = topChartFromPage(objectGraph, gamesPage, objectGraph.loc.string("TopCharts.Hub.Games.Title")); + const hubPage = new ChartsHubPage([appsCharts, gamesCharts]); + hubPage.title = objectGraph.loc.string("TopCharts.Hub.Title"); + return hubPage; +} +//# sourceMappingURL=charts-hub.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/charts/charts-page-model.js b/node_modules/@jet-app/app-store/tmp/src/common/charts/charts-page-model.js new file mode 100644 index 0000000..cbbecf3 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/charts/charts-page-model.js @@ -0,0 +1,156 @@ +import * as validation from "@jet/environment/json/validation"; +import { unsafeUnwrapOptional as unwrap } from "@jet/environment/types/optional"; +import { Shelf, TopChartSegment } from "../../api/models"; +import * as serverData from "../../foundation/json-parsing/server-data"; +import { Path, Protocol } from "../../foundation/network/url-constants"; +import { addMetricsEventsToPageWithInformation, metricsPageInformationFromMediaApiResponse, } from "../metrics/helpers/page"; +import { newLocationTracker, nextPosition } from "../metrics/helpers/location"; +import { lockupFromData } from "../lockups/lockups"; +import { shouldFilter } from "../filtering"; +import { createMediaPageToken } from "../builders/pagination"; +/** + * Creates short and long display names for the top chart segment, + * using server data unless client has overrides in `locKeys.json`. + * + * @param objectGraph The dependency graph for the App Store. + * @param segmentData The top charts segment API object. + * @param context The context of the chart, e.g. Apps or Games. + * @returns Short and long display names for the top chart segment. + */ +function namesFromSegmentData(objectGraph, segmentData, context) { + const chart = serverData.asString(segmentData, "chart"); + // For the web client, regardless of whether the context is Apps or Games, we show "Free" or "Paid". + if (objectGraph.client.isWeb) { + return chart === "top-free" /* TopChartType.TopFree */ + ? { + shortName: objectGraph.loc.string("TopCharts.Free.ShortName"), + longName: objectGraph.loc.string("TopCharts.Free.LongName"), + } + : { + shortName: objectGraph.loc.string("TopCharts.Paid.ShortName"), + longName: objectGraph.loc.string("TopCharts.Paid.LongName"), + }; + } + // Use server provided names, unless we are in the Games tab or web client. + let shortName = serverData.asString(segmentData, "shortName"); + let longName = serverData.asString(segmentData, "name"); + if (context !== 2 /* CategoryListContext.Games */) { + return { shortName: unwrap(shortName), longName: unwrap(longName) }; + } + // Override server names with client names, e.g. from "Top Free/Paid iPad Apps" to "Top Free/Paid iPad Games" / "Top Free/Paid Vision Pro Games" as needed. + const isPad = objectGraph.client.isPad; + const isVision = objectGraph.client.isVision; + switch (chart) { + case "top-free" /* TopChartType.TopFree */: + if (isPad) { + shortName = objectGraph.loc.string("TopCharts.iPadGames.Free.ShortName"); // Free + longName = objectGraph.loc.string("TopCharts.iPadGames.Free.LongName"); // Top Free iPad Games + } + else if (isVision) { + shortName = objectGraph.loc.string("TopCharts.VisionGames.Free.ShortName"); // Free + longName = objectGraph.loc.string("TopCharts.VisionGames.Free.LongName"); // Top Free Apple Vision Games + } + else { + shortName = objectGraph.loc.string("TopCharts.Games.Free.ShortName"); // Free Games + longName = objectGraph.loc.string("TopCharts.Games.Free.LongName"); // Top Free Games + } + break; + case "top-paid" /* TopChartType.TopPaid */: + if (isPad) { + shortName = objectGraph.loc.string("TopCharts.iPadGames.Paid.ShortName"); // Paid + longName = objectGraph.loc.string("TopCharts.iPadGames.Paid.LongName"); // Top Paid iPad Games + } + else if (isVision) { + shortName = objectGraph.loc.string("TopCharts.VisionGames.Paid.ShortName"); // Paid + longName = objectGraph.loc.string("TopCharts.VisionGames.Paid.LongName"); // Top Paid Apple Vision Games + } + else { + shortName = objectGraph.loc.string("TopCharts.Games.Paid.ShortName"); // Paid Games + longName = objectGraph.loc.string("TopCharts.Games.Paid.LongName"); // Top Paid Games + } + break; + default: + break; + } + return { shortName: unwrap(shortName), longName: unwrap(longName) }; +} +/** + * Create a lockup from an api chart item. + * @param index The index of the lockup in the chart. + * @param data The mediaAPI chart item. + * @returns A `Lockup` object. + */ +function lockupFromApiChartItem(objectGraph, index, data, metricsPageInformation, locationTracker) { + return validation.context("lockupFromApiChartItem", () => { + return lockupFromData(objectGraph, data, { + ordinal: objectGraph.loc.decimal(index + 1), + metricsOptions: { + pageInformation: metricsPageInformation, + locationTracker: locationTracker, + }, + artworkUseCase: 1 /* ArtworkUseCase.LockupIconSmall */, + }); + }); +} +/** + * Creates a top chart segment model object from a top chart segment API object. + * + * @param objectGraph The dependency graph for the App Store. + * @param segmentData The top chart segment API object. + * @param response The top charts API object containing all segments. + * @param genreId The genre of the chart, e.g. Developer Tools or Board. + * @param context The context of the chart, e.g. Apps or Games. + * @returns A new top chart segment model object. + */ +export function segmentFromData(objectGraph, segmentData, response, genreId, context) { + return validation.context("segmentFromData", () => { + const { shortName, longName } = namesFromSegmentData(objectGraph, segmentData, context); + const chart = serverData.asString(segmentData, "chart"); + const pageDetails = `${chart} ${longName}`; + const pageInformation = metricsPageInformationFromMediaApiResponse(objectGraph, "TopChartsPage", genreId, response, pageDetails); + const locationTracker = newLocationTracker(); + const items = []; + let ordinal = 0; + const missingIds = []; + for (const data of segmentData.data) { + const lockup = lockupFromApiChartItem(objectGraph, ordinal, data, pageInformation, locationTracker); + if (lockup) { + // Filter only if segment has attributes + if (shouldFilter(objectGraph, data, 68606 /* Filter.Charts */)) { + continue; + } + items.push(lockup); + nextPosition(locationTracker); + ordinal++; + } + else { + missingIds.push(data); + } + } + const shelves = []; + /// always generate a shelf else we don't load more items when we have none + const shelf = new Shelf("smallLockup"); + shelf.items = items; + if (objectGraph.featureFlags.isEnabled("shelves_2_0_top_charts") || + objectGraph.client.isiOS || + objectGraph.client.isTV) { + shelf.title = longName; + } + shelves.push(shelf); + const segment = new TopChartSegment(shortName, longName, chart, shelves); + // On Vision, include a URL for pagination - we don't use a specific Top Charts view, so we need the + // `GenericPageMoreIntent` to be able to find this controller for loading new content. + // This can be removed once visionOS adopts `TopChartsPageMoreIntent`. + const paginationUrl = objectGraph.client.isVision + ? `${Protocol.internal}:/TopChartsBuilder/${Path.lookup}` + : undefined; + const token = createMediaPageToken(objectGraph, missingIds, paginationUrl, segmentData.next, pageInformation, locationTracker); + token.metricsPageInformation = pageInformation; + token.metricsLocationTracker = locationTracker; + token.highestOrdinal = ordinal; + segment.nextPage = token; + addMetricsEventsToPageWithInformation(objectGraph, segment, pageInformation); + return segment; + }); +} +//# sourceMappingURL=charts-page-model.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/charts/charts-page-url.js b/node_modules/@jet-app/app-store/tmp/src/common/charts/charts-page-url.js new file mode 100644 index 0000000..2ecc0e2 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/charts/charts-page-url.js @@ -0,0 +1,12 @@ +import { makeChartsPageIntent } from "../../api/intents/charts-page-intent"; +import { makeChartsHubPageIntent } from "../../api/intents/charts-hub-page-intent"; +import { generateRoutes } from "../util/generate-routes"; +/// MARK: Charts Detail Page Routing +const { routes: chartsPageRoutes, makeCanonicalUrl: makeChartsPageURL } = generateRoutes(makeChartsPageIntent, "/{platform}/charts/{genreId}", [], { + optionalQuery: ["chart", "ageBandId"], +}); +export { chartsPageRoutes, makeChartsPageURL }; +/// MARK: Charts Hub Page Routing +const { routes: chartsHubPageRoutes, makeCanonicalUrl: makeChartsHubPageURL } = generateRoutes(makeChartsHubPageIntent, "/{platform}/charts"); +export { chartsHubPageRoutes, makeChartsHubPageURL }; +//# sourceMappingURL=charts-page-url.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/constants/video-constants.js b/node_modules/@jet-app/app-store/tmp/src/common/constants/video-constants.js new file mode 100644 index 0000000..22d3a89 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/constants/video-constants.js @@ -0,0 +1,34 @@ +/** + * Created by jellenbogen on 7/22/19. + */ +export function standardControls(objectGraph) { + return { + prominentPlay: true, + fullScreenToggle: true, + inlinePlayPause: true, + muteUnmute: true, + }; +} +export function autoPlayControls(objectGraph) { + return { + prominentPlay: true, + fullScreenToggle: false, + inlinePlayPause: objectGraph.client.isMac, + muteUnmute: true, + }; +} +export function defaultVideoConfiguration(objectGraph) { + return { + playbackControls: standardControls(objectGraph), + autoPlayPlaybackControls: autoPlayControls(objectGraph), + }; +} +export function noControls(objectGraph) { + return { + prominentPlay: false, + fullScreenToggle: false, + inlinePlayPause: false, + muteUnmute: false, + }; +} +//# sourceMappingURL=video-constants.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/content/age-ratings.js b/node_modules/@jet-app/app-store/tmp/src/common/content/age-ratings.js new file mode 100644 index 0000000..91d95a6 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/content/age-ratings.js @@ -0,0 +1,105 @@ +import { isSome } from "@jet/environment"; +import * as mediaAttributes from "../../foundation/media/attributes"; +import * as serverData from "../../foundation/json-parsing/server-data"; +/** + * Provides the localized name of the product's age rating, e.g. "12+". + * @param objectGraph The App Store object graph. + * @param data The Media API data for an app. + * @param useLegacyFallback Indicates whether the legacy key should be used if the modern one is missing. + * @returns The localized `string` of the product's age rating, or `undefined`. + */ +export function name(objectGraph, data, useLegacyFallback = false) { + const modernValue = mediaAttributes.attributeAsString(data, "ageRating.name"); + if (isSome(modernValue)) { + return modernValue; + } + else if (useLegacyFallback) { + return mediaAttributes.attributeAsString(data, "contentRatingsBySystem.appsApple.name"); + } + else { + return undefined; + } +} +/** + * Provides the product's age rating value, e.g. 300. This value is understood + * by systems such as Managed Configuration and Screen Time to enforce content + * restrictions. + * @param objectGraph The App Store object graph. + * @param data The Media API data for an app. + * @param useLegacyFallback Indicates whether the legacy key should be used if the modern one is missing. + * @returns The `number` value of the product's age rating, or `undefined`. + */ +export function value(objectGraph, data, useLegacyFallback = false) { + const modernValue = mediaAttributes.attributeAsNumber(data, "ageRating.value"); + if (isSome(modernValue)) { + return modernValue; + } + else if (useLegacyFallback) { + return mediaAttributes.attributeAsNumber(data, "contentRatingsBySystem.appsApple.value"); + } + else { + return undefined; + } +} +/** + * Provides the product's age rating description from Media API. This was + * historically generated by the client, so has no legacy fallback. + * @param objectGraph The App Store object graph. + * @param data The Media API data for an app. + * @returns The product's age rating description `string`, or `undefined`. + */ +export function description(objectGraph, data) { + return mediaAttributes.attributeAsString(data, "ageRating.description"); +} +export function hasInAppControls(objectGraph, data) { + const contentLevels = mediaAttributes.attributeAsArrayOrEmpty(data, "ageRating.contentLevels"); + for (const contentLevel of contentLevels) { + if (serverData.asString(contentLevel, "kind") === "IAC") { + return true; + } + } + return false; +} +/** + * Provides the product's developer age guidance URL from Media API. This is an + * optional URL the developer can provide in App Store Connect to give further + * details on their app's content controls. There is no legacy fallback for + * this value. + * @param objectGraph The App Store object graph. + * @param data The Media API data for an app. + * @returns The product's developer age guidance URL `string`, or `undefined`. + */ +export function developerAgeGuidanceURL(objectGraph, data) { + return mediaAttributes.attributeAsString(data, "ageRating.ageGuidanceUrl"); +} +/** + * Returns the name of an image resource in the App Store bundle corresponding + * to the provided MAPI `ageRating` data. + * @param objectGraph The App Store object graph. + * @param data The Media API data for an app. + * @returns The `string` name of an image resource in the bundle, or `undefined`. + */ +export function pictogramResource(objectGraph, data) { + // Values pulled from https://quip-apple.com/0bq3AiLxhzaW + const pictogramResources = new Map([ + // Brazil + ["br.100.official", "br.l.official"], + ["br.100", "br.l"], + ["br.210.official", "br.10.official"], + ["br.210", "br.10"], + ["br.300.official", "br.12.official"], + ["br.300", "br.12"], + ["br.314.official", "br.14.official"], + ["br.314", "br.14"], + ["br.416.official", "br.16.official"], + ["br.416", "br.16"], + ["br.618.official", "br.18.official"], + ["br.618", "br.18"], + ]); + const storefront = objectGraph.locale.activeStorefront; + const contentLevel = mediaAttributes.attributeAsString(data, "ageRating.value"); + const isOfficial = mediaAttributes.attributeAsBooleanOrFalse(data, "ageRating.isOfficial"); + const key = storefront + "." + contentLevel + (isOfficial ? ".official" : ""); + return pictogramResources.get(key); +} +//# sourceMappingURL=age-ratings.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/content/artwork/artwork.js b/node_modules/@jet-app/app-store/tmp/src/common/content/artwork/artwork.js new file mode 100644 index 0000000..ebf7c71 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/content/artwork/artwork.js @@ -0,0 +1,71 @@ +import * as modelsBase from "../../../api/models/base"; +export function createArtworkVariantForClient(objectGraph, allowingTransparency = false, supportsWideGamut = false, useCase = 0 /* content.ArtworkUseCase.Default */) { + const format = artworkFormatForClient(objectGraph, allowingTransparency); + return createArtworkVariantForFormat(objectGraph, format, supportsWideGamut, useCase); +} +export function createArtworkVariantForFormat(objectGraph, format, supportsWideGamut = false, useCase = 0 /* content.ArtworkUseCase.Default */) { + const quality = qualityForClient(objectGraph, format, useCase); + return new modelsBase.ArtworkVariant(format, quality, supportsWideGamut); +} +export function createArtworkForResource(objectGraph, resource, width = 0, height = 0, backgroundColor = null, textColor = null, checksum = null) { + const artwork = new modelsBase.Artwork(resource, width, height, [ + createArtworkVariantForClient(objectGraph, false, false, 0 /* content.ArtworkUseCase.Default */), + ]); + artwork.backgroundColor = backgroundColor; + artwork.textColor = textColor; + artwork.checksum = checksum; + return artwork; +} +/** + * Create a bundle artwork template for a resource. + */ +export function artworkTemplateForBundleImage(resource) { + if (preprocessor.GAMES_TARGET) { + return `bundleimage://${resource}?bundleid=com.apple.GameStoreKit`; + } + else { + return `resource://${resource}`; + } +} +/** + * Create Artwork for a system image. + */ +export function createArtworkForSystemImage(objectGraph, resource) { + return createArtworkForResource(objectGraph, `systemimage://${resource}`); +} +function artworkFormatForClient(objectGraph, allowsTransparency) { + // Determine whether HEIF is supported for the given target. + let supportsHEIF; + switch (objectGraph.host.clientIdentifier) { + case "com.apple.TVAppStore.AppStoreTopShelfExtension": + case "com.apple.Arcade.ArcadeTopShelfExtension": + case "com.apple.AppStore.Widgets": + // Disable HEIF for top shelf + supportsHEIF = false; + break; + default: + supportsHEIF = objectGraph.client.supportsHEIF; + break; + } + // HEIF is the image format, HEIC is the container + const defaultArtworkFormat = supportsHEIF ? "heic" : "jpeg"; + const defaultArtworkFormatAllowingTransparency = supportsHEIF ? "heic" : "png"; + return allowsTransparency ? defaultArtworkFormatAllowingTransparency : defaultArtworkFormat; +} +function qualityForClient(objectGraph, artworkFormat, useCase) { + switch (artworkFormat) { + case "heic": { + if (objectGraph.client.isTV && useCase === 21 /* content.ArtworkUseCase.Uber */) { + return 70; + } + else { + return 60; + } + } + // JPEG, LCR and PNG + default: { + return 70; + } + } +} +//# sourceMappingURL=artwork.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/content/artwork/legacy-artwork.js b/node_modules/@jet-app/app-store/tmp/src/common/content/artwork/legacy-artwork.js new file mode 100644 index 0000000..7607675 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/content/artwork/legacy-artwork.js @@ -0,0 +1,33 @@ +/** + * Created by keithpk on 1/25/17. + */ +import * as serverData from "../../../foundation/json-parsing/server-data"; +import * as artworkBuilder from "./artwork"; +export function closestArtworkMatchingSize(objectGraph, artworkArray, width, height) { + const desiredRatio = width / height; + let foundAspectRatio = 0; + let foundArtwork = null; + for (const artwork of artworkArray) { + const actualWidth = serverData.asNumber(artwork, "width"); + const actualHeight = serverData.asNumber(artwork, "height"); + const actualRatio = actualWidth / actualHeight; + if (actualRatio === desiredRatio || + Math.abs(desiredRatio - actualRatio) <= Math.abs(desiredRatio - foundAspectRatio)) { + if (!foundArtwork || + (actualWidth <= width && actualWidth > foundArtwork.width) || + (foundArtwork.width > width && actualWidth < foundArtwork.width && actualWidth > foundArtwork.width)) { + foundArtwork = artwork; + foundAspectRatio = actualRatio; + } + } + } + if (foundArtwork) { + const artworkUrl = serverData.asString(foundArtwork, "url"); + const foundWidth = serverData.asNumber(foundArtwork, "width"); + const foundHeight = serverData.asNumber(foundArtwork, "height"); + const foundChecksum = serverData.asString(foundArtwork, "checksum"); + return artworkBuilder.createArtworkForResource(objectGraph, artworkUrl, foundWidth, foundHeight, null, null, foundChecksum); + } + return null; +} +//# sourceMappingURL=legacy-artwork.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/content/attributes.js b/node_modules/@jet-app/app-store/tmp/src/common/content/attributes.js new file mode 100644 index 0000000..1cae53a --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/content/attributes.js @@ -0,0 +1,328 @@ +import { isNothing, isSome } from "@jet/environment/types/optional"; +import * as derivedData from "../../foundation/json-parsing/derived-data"; +import * as serverData from "../../foundation/json-parsing/server-data"; +import * as mediaAttributes from "../../foundation/media/attributes"; +import * as mediaPlatformAttributes from "../../foundation/media/platform-attributes"; +import { variantAttributeForKey } from "../product-page/product-page-variants"; +import * as contentDeviceFamily from "./device-family"; +/** + * Retrieve the specified attribute from the data, coercing it to a JSONData dictionary + * + * @param data The data from which to retrieve the attribute. + * @param attributePath The path of the attribute. + * @param attributePlatform The specific platform attribute to get content for. Omit to infer from the data's structure. + * @param defaultValue The object to return if the path search fails. + * @returns The dictionary of data + */ +export function contentAttributeAsDictionary(objectGraph, data, attributePath, attributePlatform, defaultValue) { + if (!attributePlatform) { + attributePlatform = bestAttributePlatformFromData(objectGraph, data); + } + if (isNothing(attributePlatform)) { + return null; + } + let value = mediaPlatformAttributes.platformAttributeAsDictionary(data, attributePlatform, attributePath); + if (!value) { + value = mediaAttributes.attributeAsDictionary(data, attributePath, defaultValue); + } + return value; +} +/** + * Retrieve the specified attribute from the data as an array, coercing to an empty array if the object is not an array. + * + * @param data The data from which to retrieve the attribute. + * @param attributePath The path of the attribute. + * @param attributePlatformOverride An override platform, from which to fetch the attribute. + * @returns {any[]} The attribute value as an array. + */ +export function contentAttributeAsArrayOrEmpty(objectGraph, data, attributePath, attributePlatformOverride = undefined) { + const attributePlatform = attributePlatformOverride !== null && attributePlatformOverride !== void 0 ? attributePlatformOverride : bestAttributePlatformFromData(objectGraph, data); + if (isNothing(attributePlatform)) { + return []; + } + let value = mediaPlatformAttributes.platformAttributeAsArrayOrEmpty(data, attributePlatform, attributePath); + if (serverData.isNullOrEmpty(value)) { + value = mediaAttributes.attributeAsArrayOrEmpty(data, attributePath); + } + return value; +} +/** + * Retrieve the specified attribute from the data as an array. + * + * @param data The data from which to retrieve the attribute. + * @param attributePath The path of the attribute. + * @param attributePlatformOverride An override platform, from which to fetch the attribute. + * @returns {any[]} The attribute value as an array. + */ +export function contentAttributeAsArray(objectGraph, data, attributePath, attributePlatformOverride = undefined) { + const attributePlatform = attributePlatformOverride !== null && attributePlatformOverride !== void 0 ? attributePlatformOverride : bestAttributePlatformFromData(objectGraph, data); + if (isNothing(attributePlatform)) { + return null; + } + let value = mediaPlatformAttributes.platformAttributeAsArray(data, attributePlatform, attributePath); + if (isNothing(value)) { + value = mediaAttributes.attributeAsArray(data, attributePath); + } + return value; +} +/** + * Retrieve the specified attribute from the data as a string. + * + * @param data The data from which to retrieve the attribute. + * @param attributePath The object path for the attribute. + * @param attributePlatformOverride An override platform, from which to fetch the attribute. + * @param policy The validation policy to use when resolving this value. + * @returns {string} The attribute value as a string. + */ +export function contentAttributeAsString(objectGraph, data, attributePath, attributePlatformOverride = undefined, policy = "coercible") { + let value; + const attributePlatform = attributePlatformOverride !== null && attributePlatformOverride !== void 0 ? attributePlatformOverride : bestAttributePlatformFromData(objectGraph, data); + if (isSome(attributePlatform)) { + value = mediaPlatformAttributes.platformAttributeAsString(data, attributePlatform, attributePath, policy); + } + if (!value) { + value = mediaAttributes.attributeAsString(data, attributePath, policy); + } + return value; +} +/** + * Retrieve the specified attribute from the data as a boolean. + * + * @param data The data from which to retrieve the attribute. + * @param attributePath The path of the attribute. + * @param policy The validation policy to use when resolving this value. + * @returns {boolean} The attribute value as a boolean. + */ +export function contentAttributeAsBoolean(objectGraph, data, attributePath, attributePlatform, policy = "coercible") { + if (!attributePlatform) { + attributePlatform = bestAttributePlatformFromData(objectGraph, data); + } + if (isNothing(attributePlatform)) { + return null; + } + let value = mediaPlatformAttributes.platformAttributeAsBoolean(data, attributePlatform, attributePath, policy); + if (serverData.isNull(value)) { + value = mediaAttributes.attributeAsBoolean(data, attributePath, policy); + } + return value; +} +/** + * Retrieve the specified attribute from the data as a boolean, which will be `false` if the attribute does not exist. + * + * @param data The data from which to retrieve the attribute. + * @param attributePath The path of the attribute. + * @param attributePlatform The specific platform attribute to get content for. Omit to infer from the data's structure. + * @returns {boolean} The attribute value as a boolean, coercing to `false` if the value is not present.. + */ +export function contentAttributeAsBooleanOrFalse(objectGraph, data, attributePath, attributePlatform) { + if (!attributePlatform) { + attributePlatform = bestAttributePlatformFromData(objectGraph, data); + } + if (isNothing(attributePlatform)) { + return false; + } + let value = mediaPlatformAttributes.platformAttributeAsBoolean(data, attributePlatform, attributePath); + if (serverData.isNull(value)) { + value = mediaAttributes.attributeAsBooleanOrFalse(data, attributePath); + } + return value; +} +/** + * Retrieve the specified attribute from the data as a number. + * + * @param data The data from which to retrieve the attribute. + * @param attributePath The path of the attribute. + * @param policy The validation policy to use when resolving this value. + * @returns {boolean} The attribute value as a number. + */ +export function contentAttributeAsNumber(objectGraph, data, attributePath, policy = "coercible") { + const attributePlatform = bestAttributePlatformFromData(objectGraph, data); + if (isNothing(attributePlatform)) { + return null; + } + let value = mediaPlatformAttributes.platformAttributeAsNumber(data, attributePlatform, attributePath, policy); + if (serverData.isNull(value)) { + value = mediaAttributes.attributeAsNumber(data, attributePath); + } + return value; +} +/** + * Computes the best attribute platform for a given piece of content + * + * @param {Data} data The media API data representing the content + * @returns {AttributePlatform} + */ +export function bestAttributePlatformFromData(objectGraph, data, clientIdentifierOverride) { + const baseCacheKey = "bestAttributePlatformFromData"; + const cacheKey = isSome(clientIdentifierOverride) ? `${baseCacheKey}.${clientIdentifierOverride}` : baseCacheKey; + return derivedData.value(data, cacheKey, () => { + const isIOSOnly = contentDeviceFamily.dataOnlyHasDeviceFamilies(objectGraph, data, ["iphone", "ipad", "ipod"], true); + const isTvOnly = contentDeviceFamily.dataOnlyHasDeviceFamily(objectGraph, data, "tvos"); + const isMacOnly = contentDeviceFamily.dataOnlyHasDeviceFamily(objectGraph, data, "mac"); + const isWatchOnly = contentDeviceFamily.dataOnlyHasDeviceFamily(objectGraph, data, "watch"); + const isVisionOnly = contentDeviceFamily.dataOnlyHasDeviceFamily(objectGraph, data, "realityDevice"); + // 1. The data is for a single platform only. + let dedicatedPlatform = null; + if (isTvOnly) { + dedicatedPlatform = "appletvos"; + } + else if (isMacOnly) { + dedicatedPlatform = "osx"; + } + else if (isIOSOnly) { + dedicatedPlatform = "ios"; + } + else if (isWatchOnly) { + dedicatedPlatform = "watch"; + } + else if (isVisionOnly) { + dedicatedPlatform = "xros"; + } + if (!serverData.isNull(dedicatedPlatform)) { + return dedicatedPlatform; + } + // 2. Loop through our preferred ordering of platforms and use the first one that has platformAttributes present. + const alternatePlatforms = defaultAttributePlatformOrdering(objectGraph, clientIdentifierOverride); + for (const candidatePlatform of alternatePlatforms) { + if (mediaPlatformAttributes.hasPlatformAttribute(data, candidatePlatform)) { + return candidatePlatform; + } + } + // 3. Catch-All + return defaultAttributePlatform(objectGraph); + }); +} +/** + * Computes the best attribute platform for a given Media API Marketplace + * response. Since Marketplace responses don't contain a top-level + * `deviceFamilies` property, this employs an alternative method from + * `bestAttributePlatformFromData()` to get the attribute platform. + * + * @param objectGraph The App Store object graph + * @param data The Media API Marketplace response to search for an attribute platform. + * @returns The most appropriate attribute platform available for the current client. + */ +export function bestAttributePlatformFromMarketplaceData(objectGraph, data) { + // 1. Iterate through the client's preferred platform ordering until we + // find the first one present in the response. + const preferredAttributePlatforms = defaultAttributePlatformOrdering(objectGraph); + for (const attributePlatform of preferredAttributePlatforms) { + const versionsAttributes = contentAttributeAsDictionary(objectGraph, data, "versionAttributes", attributePlatform); + if (serverData.isDefinedNonNullNonEmpty(versionsAttributes)) { + return attributePlatform; + } + } + // 2. Catch-All + return defaultAttributePlatform(objectGraph); +} +/** + * The default attribute platform for the current client + */ +export function defaultAttributePlatform(objectGraph) { + var _a; + if ((_a = objectGraph.activeIntent) === null || _a === void 0 ? void 0 : _a.attributePlatform) { + return objectGraph.activeIntent.attributePlatform; + } + switch (objectGraph.client.deviceType) { + case "phone": + case "pad": + return "ios"; + case "tv": + return "appletvos"; + case "mac": + return "osx"; + case "watch": + return "watch"; + case "vision": + return "xros"; + default: + return null; + } +} +/** + * The preferred ordering to use given our default platform. + */ +function defaultAttributePlatformOrdering(objectGraph, clientIdentifierOverride) { + const defaultPlatform = defaultAttributePlatform(objectGraph); + if (defaultPlatform === null) { + // If the `"web"` client is active and there is not an "active intent" to + // inform a default platform, fall back to a hard-coded ordering + if (objectGraph.client.isWeb) { + return ["ios", "osx", "xros", "watch", "appletvos"]; + } + else { + return []; + } + } + switch (defaultPlatform) { + case "ios": + if (clientIdentifierOverride === "VisionAppStore" /* ClientIdentifier.VisionAppStore */ || + clientIdentifierOverride === "com.apple.visionproapp" /* ClientIdentifier.VisionCompanion */) { + return ["xros", "ios", "appletvos", "osx"]; + } + else { + return ["ios", "appletvos", "osx", "xros"]; + } + case "osx": + return ["osx", "ios", "appletvos", "xros"]; + case "appletvos": + return ["appletvos", "ios", "osx", "xros"]; + case "watch": + // Per Hiren Kotadia on 2019-2-26, watch platform attributes will always be under ios. + // We're going to promote ios to the head of the search list to speed up Media API + // response parsing. We'll keep watch as #2 in the list so that if this changes in + // the future, it should mostly just work. -km + return ["ios", "watch", "osx", "xros"]; + case "xros": + return ["xros", "ios", "appletvos", "osx"]; + default: + return [defaultPlatform]; + } +} +// region Variant Attributes +/** + * Retrieve the attribute for a specific platform's variant attribute as a dictionary, from custom attributes or standard attributes. + * @param data Data to get attribute for. + * @param productVariantData Variant data to use when finding item. + * @param attributeKey The key to fetch in platform attributes. May be converted to custom attribute key. + * @param attributePlatform The platform to fetch attribute for. Defaults to current platform if unspecified. + */ +export function customAttributeAsDictionary(objectGraph, data, productVariantData, attributeKey, attributePlatform) { + // Use `customAttributes.${customAttributeKey}` for platform if present + const customAttributeKey = mediaAttributes.attributeKeyAsCustomAttributeKey(attributeKey); + if (isNothing(customAttributeKey)) { + return null; + } + const customAttributes = contentAttributeAsDictionary(objectGraph, data, "customAttributes", attributePlatform); + const allowNondefaultTreatmentInNondefaultPage = mediaAttributes.attributeAllowsNonDefaultTreatmentInNonDefaultPage(customAttributeKey); + const customAttribute = variantAttributeForKey(objectGraph, customAttributes, productVariantData, customAttributeKey, allowNondefaultTreatmentInNondefaultPage); + if (serverData.isDefinedNonNullNonEmpty(customAttribute)) { + return serverData.asDictionary(customAttribute); + } + // Otherwise, use `${attributeKey}` for platform. + return contentAttributeAsDictionary(objectGraph, data, attributeKey, attributePlatform); +} +/** + * Retrieve the attribute for a specific platform's variant attribute as a dictionary, from custom attributes or standard attributes. + * @param data Data to get attribute for. + * @param productVariantData Variant data to use when finding item. + * @param attributeKey The key to fetch in platform attributes when custom attributes are not present. + * @param attributePlatform The platform to fetch attribute for. Defaults to current platform if unspecified. + */ +export function customAttributeAsString(objectGraph, data, productVariantData, attributeKey, attributePlatform) { + // Use `customAttributes.${customAttributeKey}` for platform if present + const customAttributeKey = mediaAttributes.attributeKeyAsCustomAttributeKey(attributeKey); + if (isNothing(customAttributeKey)) { + return null; + } + const customAttributes = contentAttributeAsDictionary(objectGraph, data, "customAttributes", attributePlatform); + const allowNondefaultTreatmentInNondefaultPage = mediaAttributes.attributeAllowsNonDefaultTreatmentInNonDefaultPage(customAttributeKey); + const customAttribute = variantAttributeForKey(objectGraph, customAttributes, productVariantData, customAttributeKey, allowNondefaultTreatmentInNondefaultPage); + if (serverData.isDefinedNonNullNonEmpty(customAttribute)) { + return serverData.asString(customAttribute); + } + // Otherwise, use `${attributeKey}` for platform. + return contentAttributeAsString(objectGraph, data, attributeKey); +} +// endregion +//# sourceMappingURL=attributes.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/content/content.js b/node_modules/@jet-app/app-store/tmp/src/common/content/content.js new file mode 100644 index 0000000..72b5ae1 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/content/content.js @@ -0,0 +1,2820 @@ +import * as validation from "@jet/environment/json/validation"; +import { isNothing, isSome, unwrapOptional as unwrap } from "@jet/environment/types/optional"; +import * as models from "../../api/models"; +import * as modelsBase from "../../api/models/base"; +import * as modelsShelves from "../../api/models/shelves"; +import { ads } from "../../api/typings/constants"; +import * as derivedData from "../../foundation/json-parsing/derived-data"; +import * as serverData from "../../foundation/json-parsing/server-data"; +import * as mediaAttributes from "../../foundation/media/attributes"; +import * as mediaDataFetching from "../../foundation/media/data-fetching"; +import * as mediaPlatformAttributes from "../../foundation/media/platform-attributes"; +import * as mediaRelationship from "../../foundation/media/relationships"; +import * as mediaUrlBuilder from "../../foundation/media/url-builder"; +import { Parameters, Path, Protocol } from "../../foundation/network/url-constants"; +import * as color from "../../foundation/util/color-util"; +import * as dateUtil from "../../foundation/util/date-util"; +import { unreachable } from "../../foundation/util/errors"; +import { isDefinedNonNullNonEmpty } from "@apple-media-services/media-api"; +import { editorialCardFromData } from "../../foundation/media/associations"; +import * as client from "../../foundation/wrappers/client"; +import * as videoDefaults from "../constants/video-constants"; +import * as filtering from "../filtering"; +import * as lockups from "../lockups/lockups"; +import * as metricsHelpersImpressions from "../metrics/helpers/impressions"; +import * as metricsHelpersLocation from "../metrics/helpers/location"; +import * as metricsHelpersMedia from "../metrics/helpers/media"; +import * as offers from "../offers/offers"; +import * as productPageVariants from "../product-page/product-page-variants"; +import * as artwork from "./artwork/artwork"; +import * as contentAttributes from "./attributes"; +import * as contentDeviceFamily from "./device-family"; +import * as sad from "./sad"; +import { isFeatureEnabledForCurrentUser } from "../util/lottery"; +class RunnabilityInfo { + constructor() { + this.runsOnIntel = true; + this.runsOnAppleSilicon = true; + this.requiresRosetta = false; + } +} +/** + * Determines a reasonable artwork use case from a given shelf style + * + * @param shelfStyle The shelf style to consider + */ +export function artworkUseCaseFromShelfStyle(objectGraph, shelfStyle) { + switch (shelfStyle) { + case "inAppPurchaseLockup": + case "appShowcase": + case "smallLockup": { + return 1 /* ArtworkUseCase.LockupIconSmall */; + break; + } + case "mediumLockup": { + return 2 /* ArtworkUseCase.LockupIconMedium */; + break; + } + case "largeLockup": { + return 3 /* ArtworkUseCase.LockupIconLarge */; + break; + } + default: { + return 0 /* ArtworkUseCase.Default */; + } + } +} +/** + * Convert an API artwork object into an Artwork model object. + * @param artwork The artwork in API format. + * @returns An `Artwork` object. + */ +export function artworkFromApiArtwork(objectGraph, artworkData, options) { + return validation.context("artworkFromApiArtwork", () => { + var _a, _b, _c; + const allowingTransparency = serverData.isDefinedNonNull(options.allowingTransparency) + ? options.allowingTransparency + : false; + const useJoeColorDefault = objectGraph.client.isVision || objectGraph.client.isWeb; + const withJoeColorPlaceholder = serverData.isDefinedNonNull(options.withJoeColorPlaceholder) + ? options.withJoeColorPlaceholder + : useJoeColorDefault; + const artworkUrl = serverData.asString(artworkData, "url"); + if (serverData.isNull(artworkUrl)) { + return null; + } + // Whether wide gamut is supported + const supportsWideGamut = serverData.asBooleanOrFalse(artworkData, "hasP3"); + // Add base variant + const variants = [ + artwork.createArtworkVariantForClient(objectGraph, allowingTransparency, supportsWideGamut, options.useCase), + ]; + // Add layered image variant + const supportsLayeredImage = serverData.asBooleanOrFalse(artworkData, "supportsLayeredImage"); + if (supportsLayeredImage && (objectGraph.client.isTV || objectGraph.client.isVision)) { + variants.push(artwork.createArtworkVariantForFormat(objectGraph, "lcr", supportsWideGamut, options.useCase)); + } + // Artwork Placeholder Color + // If we indicate the image could be transparent then we don't want a placeholder background + let placeholderBackgroundColor = null; + if (allowingTransparency) { + placeholderBackgroundColor = color.named("clear"); + } + else if (withJoeColorPlaceholder) { + const joeColorHexSet = joeColorHexSetFromData(artworkData); + const placeholderColorHex = (_b = (_a = options.joeColorPlaceholderSelectionLogic) === null || _a === void 0 ? void 0 : _a.call(options, joeColorHexSet)) !== null && _b !== void 0 ? _b : serverData.asString(artworkData, "bgColor"); + const apiBackgroundColor = color.fromHex(placeholderColorHex); + if (!serverData.isNull(apiBackgroundColor)) { + placeholderBackgroundColor = apiBackgroundColor; + } + } + // If we don't want clear, joe color, or joe color fails to parse then fall back to default background + if (serverData.isNull(placeholderBackgroundColor) && !objectGraph.client.isVision) { + placeholderBackgroundColor = color.named("placeholderBackground"); + } + const textColorKey = (_c = options.overrideTextColorKey) !== null && _c !== void 0 ? _c : "textColor1"; + const apiTextColor = color.fromHex(serverData.asString(artworkData, textColorKey)); + const artworkModel = new modelsBase.Artwork(artworkUrl, options.overrideWidth || serverData.asNumber(artworkData, "width"), options.overrideHeight || serverData.asNumber(artworkData, "height"), variants); + artworkModel.backgroundColor = placeholderBackgroundColor; + artworkModel.checksum = serverData.asString(artworkData, "checksum"); + if (serverData.isDefinedNonNull(apiTextColor)) { + artworkModel.textColor = apiTextColor; + } + if (serverData.isDefinedNonNull(options.style)) { + artworkModel.style = options.style; + } + if (serverData.isDefinedNonNull(options.cropCode)) { + artworkModel.crop = options.cropCode; + } + if (serverData.isDefinedNonNull(options.contentMode)) { + artworkModel.contentMode = options.contentMode; + } + return artworkModel; + }); +} +export function impressionableAppIconFromData(objectGraph, data, metricsOptions, artworkOptions) { + return validation.context("impressionableAppIconFromData", () => { + const rawArtwork = iconFromData(objectGraph, data, artworkOptions); + if (!serverData.isDefinedNonNull(rawArtwork)) { + return null; + } + const icon = new models.ImpressionableArtwork(rawArtwork); + const title = mediaAttributes.attributeAsString(data, "name"); + const metricsImpressionOptions = metricsHelpersImpressions.impressionOptions(objectGraph, data, title, metricsOptions); + metricsHelpersImpressions.addImpressionFields(objectGraph, icon, metricsImpressionOptions); + return icon; + }); +} +/** + * Batch method for `impressionableAppIconFromData`. Doesn't push location stack or increment location counter, matching other behavior with other icon grids. + * @param dataCollection Data container array with app data. + * @param metricsOptions Metrics blob containing information about page and location. + * @returns Array of `ImpressionableArtwork` + */ +export function impressionableAppIconsFromDataCollection(objectGraph, dataCollection, metricsOptions, artworkOptions) { + return validation.context("impressionableAppIconFromData", () => { + const icons = []; + if (serverData.isNullOrEmpty(metricsOptions.targetType)) { + metricsOptions.targetType = "artwork"; + } + for (const data of dataCollection) { + const icon = impressionableAppIconFromData(objectGraph, data, metricsOptions, artworkOptions); + if (icon) { + icons.push(icon); + metricsHelpersLocation.nextPosition(metricsOptions.locationTracker); + } + } + return icons; + }); +} +/** + * Defines possible use cases for SearchChartOrCategoryBrick. + */ +export var SearchChartOrCategoryBrickUseCase; +(function (SearchChartOrCategoryBrickUseCase) { + SearchChartOrCategoryBrickUseCase[SearchChartOrCategoryBrickUseCase["seeAllPage"] = 0] = "seeAllPage"; + SearchChartOrCategoryBrickUseCase[SearchChartOrCategoryBrickUseCase["categoryBreakout"] = 1] = "categoryBreakout"; + SearchChartOrCategoryBrickUseCase[SearchChartOrCategoryBrickUseCase["other"] = 2] = "other"; +})(SearchChartOrCategoryBrickUseCase || (SearchChartOrCategoryBrickUseCase = {})); +/** + * Gets all possible artwork that this chart or category can show + * @param objectGraph + * @param data + * @param isForSeeAllPage Whether or not the chart or category is on the see-all page or not; + * this is because the see-all page should always have the `Density1` style + * @param style The style of the chart or category that will be rendered + * @returns All permutations of artowrk that the chart or category can show + */ +export function searchChartOrCategoryArtworkFromData(objectGraph, data, useCase, style) { + const artworkPath = "editorialArtwork.searchCategoryBrick"; + const artworkData = contentAttributes.contentAttributeAsDictionary(objectGraph, data, artworkPath); + if (serverData.isNullOrEmpty(artworkData)) { + return null; + } + let artworkStyle = style; + if (useCase === SearchChartOrCategoryBrickUseCase.seeAllPage) { + artworkStyle = models.GenericSearchPageShelfDisplayStyleDensity.Density1; + } + /// Crops = [LTR crop, RTL crop] + /// ContentModes = [ContentMode for LTR crop, ContentMode for RTL crop] + /// Note: These must be the same length + let crops = []; + let contentModes = []; + switch (artworkStyle) { + /// Tile + case models.GenericSearchPageShelfDisplayStyleDensity.Density1: + const width = useCase === SearchChartOrCategoryBrickUseCase.categoryBreakout ? "1191" : "2350"; + artworkData["width"] = width; + artworkData["height"] = "670"; + crops = ["SCB.ApSCBL01", "SCB.ApSCBL03"]; + contentModes = [modelsBase.ArtworkContentMode.right, modelsBase.ArtworkContentMode.left]; + break; + /// Pill + case models.GenericSearchPageShelfDisplayStyleDensity.Density2: + artworkData["width"] = "2482"; + artworkData["height"] = "670"; + crops = ["SCB.ApSCBS01", "SCB.ApSCBS02"]; + contentModes = [modelsBase.ArtworkContentMode.left, modelsBase.ArtworkContentMode.right]; + break; + /// Round + case models.GenericSearchPageShelfDisplayStyleDensity.Density3: + artworkData["width"] = "670"; + artworkData["height"] = "670"; + crops = ["cc"]; + contentModes = [modelsBase.ArtworkContentMode.scaleAspectFit]; + break; + default: + break; + } + return crops.map((crop, index) => { + return artworkFromApiArtwork(objectGraph, artworkData, { + cropCode: crop, + contentMode: index < contentModes.length ? contentModes[index] : null, + useCase: 0 /* ArtworkUseCase.Default */, + withJoeColorPlaceholder: true, + }); + }); +} +/** + * Create an icon artwork from the provided data. + * @param objectGraph The object graph. + * @param data The data object to pull icon data from. + * @param artworkOptions The options for creating the artwork. + * @param clientIdentifierOverride A client identifier override. + * @param productVariantData The product variant data to use to select the icon. + * @param attributePlatformOverride An override platform, from which to fetch the icon. + * @returns An `Artwork` object representing the icon. + */ +export function iconFromData(objectGraph, data, artworkOptions, clientIdentifierOverride, productVariantData, attributePlatformOverride = undefined) { + return validation.context("iconFromData", () => { + if (!data) { + validation.unexpectedNull("ignoredValue", "data"); + return null; + } + const attributePlatform = attributePlatformOverride !== null && attributePlatformOverride !== void 0 ? attributePlatformOverride : iconAttributePlatform(objectGraph, data, clientIdentifierOverride); + const usePrerenderedIconArtwork = shouldUsePrerenderedIconArtwork(objectGraph); + // The preferred client identifier to use when selecting the artwork. + // This client identifier here ensures that we always prefer pill artwork for messages and circular artwork for watch / vision. + // Unless there's an override specified where another artwork type needs to be used (for example, in developer pages). + const preferredClientIdentifier = clientIdentifierOverride || objectGraph.host.clientIdentifier; + // Watch + const watchIcon = watchIconFromData(objectGraph, data, artworkOptions, preferredClientIdentifier, usePrerenderedIconArtwork, attributePlatform); + if (isSome(watchIcon)) { + return watchIcon; + } + // Messages + const messagesIcon = messagesIconFromData(objectGraph, data, artworkOptions, preferredClientIdentifier, attributePlatform); + if (isSome(messagesIcon)) { + return messagesIcon; + } + // In-App Purchases + const iapIcon = inAppPurchaseIconFromData(objectGraph, data, artworkOptions); + if (isSome(iapIcon)) { + return iapIcon; + } + // Bundles + const bundlesIcon = bundlesIconFromData(objectGraph, data, artworkOptions, usePrerenderedIconArtwork); + if (isSome(bundlesIcon)) { + return bundlesIcon; + } + // Calculate variant data if one wasn't provided from caller. + if (serverData.isNull(productVariantData)) { + productVariantData = productPageVariants.productVariantDataForData(objectGraph, data); + } + const artworkData = contentAttributes.customAttributeAsDictionary(objectGraph, data, productVariantData, "artwork", attributePlatform); + // tvOS + const tvIcon = tvIconFromData(objectGraph, artworkData, artworkOptions, preferredClientIdentifier, attributePlatform); + if (isSome(tvIcon)) { + return tvIcon; + } + // visionOS + const visionIcon = visionIconFromData(objectGraph, artworkData, artworkOptions, preferredClientIdentifier, attributePlatform); + if (isSome(visionIcon)) { + return visionIcon; + } + // macOS & iOS + return macOSOriOSIconFromData(objectGraph, data, artworkData, artworkOptions, usePrerenderedIconArtwork, productVariantData, attributePlatform); + }); +} +/** + * Determines if a client is capable of showing pre-rendered icon artwork, and if the relevant + * feature / bag flags are enabled. + * @param objectGraph Current object graph + * @returns True if we should use prerendered icon artwork. + */ +export function shouldUsePrerenderedIconArtwork(objectGraph) { + const clientSupportsPrerenderedIconArtwork = objectGraph.client.isWatch || objectGraph.client.isiOS || objectGraph.client.isMac || objectGraph.client.isWeb; + const isEnabledForUser = isFeatureEnabledForCurrentUser(objectGraph, objectGraph.bag.iconArtworkRolloutRate); + return (isEnabledForUser && + objectGraph.bag.enableIconArtwork && + objectGraph.client.isIconArtworkCapable && + clientSupportsPrerenderedIconArtwork); +} +function watchIconFromData(objectGraph, data, artworkOptions, clientIdentifier, usePrerenderedIconArtwork, attributePlatform) { + if (clientIdentifier !== client.watchIdentifier && + !contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "isStandaloneForWatchOS") && + !objectGraph.client.isWatch) { + return null; + } + // Attempt to use pre-rendered circular icon artwork first, if applicable + if (usePrerenderedIconArtwork) { + const iconArtworkData = mediaPlatformAttributes.platformAttributeAsDictionary(data, attributePlatform, "circularIconArtwork"); + if (isSome(iconArtworkData)) { + return artworkFromApiArtwork(objectGraph, iconArtworkData, { + ...artworkOptions, + style: "roundPrerendered", + cropCode: "bb", + withJoeColorPlaceholder: true, + }); + } + } + // Fallback to the legacy icon artwork + const artworkData = mediaPlatformAttributes.platformAttributeAsDictionary(data, attributePlatform, "circularArtwork"); + if (isSome(artworkData)) { + const style = usePrerenderedIconArtwork ? "roundPrerendered" : "round"; + const cropCode = usePrerenderedIconArtwork ? "ic" : undefined; + return artworkFromApiArtwork(objectGraph, artworkData, { + ...artworkOptions, + style: style, + cropCode: cropCode, + withJoeColorPlaceholder: true, + }); + } + return null; +} +function messagesIconFromData(objectGraph, data, artworkOptions, clientIdentifier, attributePlatform) { + const isHiddenFromSpringboard = isHiddenFromSpringboardFromData(objectGraph, data); + const hasMessagesExtension = hasMessagesExtensionFromData(objectGraph, data); + const shouldShowMessagesIcon = hasMessagesExtension && (clientIdentifier === client.messagesIdentifier || isHiddenFromSpringboard); + const artworkData = mediaPlatformAttributes.platformAttributeAsDictionary(data, attributePlatform, "ovalArtwork"); + if (shouldShowMessagesIcon && serverData.isDefinedNonNull(artworkData)) { + return artworkFromApiArtwork(objectGraph, artworkData, { + ...artworkOptions, + style: "pill", + }); + } + return null; +} +function inAppPurchaseIconFromData(objectGraph, data, artworkOptions) { + if (data.type !== "in-apps") { + return null; + } + const artworkData = mediaAttributes.attributeAsDictionary(data, "artwork"); + if (isSome(artworkData)) { + return artworkFromApiArtwork(objectGraph, artworkData, { + ...artworkOptions, + style: "iap", + }); + } + return null; +} +function bundlesIconFromData(objectGraph, data, artworkOptions, usePrerenderedIconArtwork) { + if (data.type !== "app-bundles") { + return null; + } + // Attempt to use pre-rendered icon artwork first, if applicable + if (usePrerenderedIconArtwork) { + const iconArtworkData = mediaAttributes.attributeAsDictionary(data, "iconArtwork"); + if (isSome(iconArtworkData)) { + return artworkFromApiArtwork(objectGraph, iconArtworkData, { + ...artworkOptions, + style: "roundedRectPrerendered", + cropCode: "bb", + }); + } + } + // Fallback to the legacy icon artwork + const artworkData = mediaAttributes.attributeAsDictionary(data, "artwork"); + if (isSome(artworkData)) { + const style = usePrerenderedIconArtwork ? "roundedRectPrerendered" : "roundedRect"; + const cropCode = usePrerenderedIconArtwork ? "ia" : undefined; + return artworkFromApiArtwork(objectGraph, artworkData, { + ...artworkOptions, + style: style, + cropCode: cropCode, + allowingTransparency: true, + }); + } + return null; +} +function tvIconFromData(objectGraph, artworkData, artworkOptions, clientIdentifier, attributePlatform) { + if (attributePlatform !== "appletvos" && clientIdentifier !== client.tvIdentifier) { + return null; + } + return artworkFromApiArtwork(objectGraph, artworkData, { + ...artworkOptions, + style: "tvRect", + }); +} +function visionIconFromData(objectGraph, artworkData, artworkOptions, clientIdentifier, attributePlatform) { + if (attributePlatform !== "xros" && clientIdentifier !== "VisionAppStore" /* ClientIdentifier.VisionAppStore */) { + return null; + } + return artworkFromApiArtwork(objectGraph, artworkData, { + ...artworkOptions, + style: "round", + }); +} +function macOSOriOSIconFromData(objectGraph, data, artworkData, artworkOptions, usePrerenderedIconArtwork, productVariantData, attributePlatform) { + const isMac = attributePlatform === "osx"; + const allowTransparency = isMac && !preprocessor.GAMES_TARGET; + // Attempt to use pre-rendered icon artwork first, if applicable + if (usePrerenderedIconArtwork) { + const iconArtworkData = contentAttributes.customAttributeAsDictionary(objectGraph, data, productVariantData, "iconArtwork", attributePlatform); + if (isSome(iconArtworkData)) { + return artworkFromApiArtwork(objectGraph, iconArtworkData, { + ...artworkOptions, + style: "roundedRectPrerendered", + cropCode: "bb", + allowingTransparency: allowTransparency, + }); + } + } + // Fallback to the standard icon artwork + let style; + let cropCode; + if (usePrerenderedIconArtwork) { + style = "roundedRectPrerendered"; + cropCode = isMac ? "ib" : "ia"; + } + else { + style = isMac ? "unadorned" : "roundedRect"; + cropCode = "bb"; + } + return artworkFromApiArtwork(objectGraph, artworkData, { + ...artworkOptions, + style: style, + cropCode: cropCode, + allowingTransparency: allowTransparency, + }); +} +/** + * Determines the best attribute platform to use for the icon. + * @param objectGraph Current object graph + * @param data The product data + * @param clientIdentifierOverride The client identifier override to use, if any + * @returns + */ +export function iconAttributePlatform(objectGraph, data, clientIdentifierOverride) { + switch (clientIdentifierOverride) { + case client.watchIdentifier: + case client.messagesIdentifier: { + return "ios"; + } + case client.tvIdentifier: { + return "appletvos"; + } + case "VisionAppStore" /* ClientIdentifier.VisionAppStore */: { + return "xros"; + } + default: { + return contentAttributes.bestAttributePlatformFromData(objectGraph, data, clientIdentifierOverride); + } + } +} +/** + * Determine the media platform, given the app platform and screenshot type. + * @param appPlatform The app platform specific to this media. + * @param type The response screenshot type, which is applicable for both screenshots and trailers, for this media. + * @param supplementaryAppPlatforms + * @returns {MediaPlatform} The configured media platform object. + * TODO: legacy_export + */ +export function mediaPlatformForTypeAndAppPlatform(objectGraph, appPlatform, type, supplementaryAppPlatforms) { + if (!appPlatform) { + return null; + } + const systemImageName = systemImageNameForAppPlatform(appPlatform); + const deviceCornerRadius = deviceCornerRadiusFactorForMediaType(objectGraph, type); + const deviceBorderThickness = deviceBorderThicknessForMediaType(objectGraph, type); + const outerDeviceCornerRadius = deviceOuterCornerRadiusFactorForMediaType(objectGraph, type); + return new modelsBase.MediaPlatform(appPlatform, type, systemImageName, supplementaryAppPlatforms, deviceCornerRadius, deviceBorderThickness, outerDeviceCornerRadius); +} +/** + * Configures the trailers object from the platform data. + * @param data The platform data to use. + * @param videoConfiguration config to use for the trailers + * @param metricsOptions The metrics options to use. + * @param adamId The adamId for the lockup. + * @param isAd Whether the trailers are for an ad lockup. Defaults to false. + * @param cropCode The crop code to use for the video preview. + * @returns {Trailers} The configured trailers object. + */ +export function trailersFromData(objectGraph, data, videoConfiguration, metricsOptions, adamId, isAd = false, cropCode) { + const platformVideos = platformVideoPreviewFromData(objectGraph, data, videoConfiguration, null, null, isAd, cropCode); + if (!platformVideos) { + return null; + } + const videoPreviews = platformVideos.videos; + const trailerVideos = []; + if (videoPreviews && videoPreviews.length > 0) { + for (const trailerVideo of videoPreviews) { + metricsHelpersMedia.addMetricsEventsToVideo(objectGraph, trailerVideo, { + ...metricsOptions, + id: adamId, + }); + trailerVideos.push(trailerVideo); + } + } + let trailers = null; + if (trailerVideos.length > 0) { + trailers = new modelsShelves.Trailers(); + trailers.videos = trailerVideos; + trailers.mediaPlatform = platformVideos.mediaPlatform; + } + return trailers; +} +/** + * A convenience class for encapsulating a `Video` that is tied to a specific `MediaPlatform`. + */ +class PlatformVideos { + constructor(videos, mediaPlatform) { + this.videos = videos; + this.mediaPlatform = mediaPlatform; + } +} +/** + * Finds the best platform video previews to use for the given parameters. + * @param data The data from which to derive the platform videos. + * @param videoConfiguration A video configuration to use for the videos + * @param includedAppPlatforms If provided, restricts the resulting platform videos to only these platforms + * @param productVariantData + * @param isAd Whether the video preview data is for an ad. Defaults to false. + * @param cropCode The crop code to use for the preview artwork. + * @returns The best available platform videos. + */ +export function platformVideoPreviewFromData(objectGraph, data, videoConfiguration, includedAppPlatforms = null, productVariantData = null, isAd = false, cropCode) { + return validation.context("platformVideoPreviewFromData", () => { + if (serverData.isNull(productVariantData)) { + productVariantData = productPageVariants.productVariantDataForData(objectGraph, data); // create variant data if not provided. + } + const videoPreviewsByTypeData = videoPreviewsByTypeFromData(objectGraph, data, productVariantData, isAd); + const videoPreviewsByType = {}; + if (!videoPreviewsByTypeData) { + return null; + } + let sortedAppPlatforms = sortedAppPlatformsFromData(objectGraph, data, objectGraph.host.clientIdentifier, objectGraph.client.deviceType); + if (serverData.isDefinedNonNull(includedAppPlatforms)) { + // If we have a restricted set of included app platforms, use those platforms + // to build our sortedAppPlatforms array in the proper sort order + const includedSortedAppPlatforms = []; + for (const appPlatform of sortedAppPlatforms) { + if (includedAppPlatforms.includes(appPlatform)) { + includedSortedAppPlatforms.push(appPlatform); + } + } + sortedAppPlatforms = includedSortedAppPlatforms; + } + if (sortedAppPlatforms.length === 0) { + return null; + } + for (const appPlatform of sortedAppPlatforms) { + const types = mediaTypesForAppPlatform(objectGraph, appPlatform, objectGraph.client.screenSize); + for (const type of Object.keys(videoPreviewsByTypeData)) { + const videosDataForType = serverData.asArrayOrEmpty(videoPreviewsByTypeData, type); + const videosForType = []; + for (const video of videosDataForType) { + const previewFrame = serverData.asDictionary(video, "previewFrame"); + if (!previewFrame) { + validation.unexpectedNull("ignoredValue", "object", `videoPreviewsByType.${type}.previewFrame`); + continue; + } + const videoUrl = serverData.asString(video, "video"); + if (!videoUrl) { + validation.unexpectedNull("ignoredValue", "string", `videoPreviewsByType.${type}.video`); + continue; + } + const preview = artwork.createArtworkForResource(objectGraph, serverData.asString(previewFrame, "url"), serverData.asNumber(previewFrame, "width"), serverData.asNumber(previewFrame, "height"), null, null, serverData.asString(previewFrame, "checksum")); + if (serverData.isDefinedNonNull(cropCode)) { + preview.crop = cropCode; + } + videosForType.push(new modelsBase.Video(videoUrl, preview, videoConfiguration)); + } + videoPreviewsByType[type] = videosForType; + } + for (const type of types) { + if (videoPreviewsByType[type]) { + return new PlatformVideos(videoPreviewsByType[type], mediaPlatformForTypeAndAppPlatform(objectGraph, appPlatform, type)); + } + } + } + return null; + }); +} +/** + * Configures the videos from some platform data. + * @param data The store platform data. + * @returns A list of `Video` objects. + */ +export function videoPreviewsFromData(objectGraph, data) { + return validation.context("videoPreviewsFromApiPlatformData", () => { + const platformVideos = platformVideoPreviewFromData(objectGraph, data, videoDefaults.defaultVideoConfiguration(objectGraph)); + if (platformVideos) { + return platformVideos.videos; + } + else { + return []; + } + }); +} +/** + * Determines the `AppPlatform` to use, in order to determine appropriate `MediaType` for media. + * @param {AppPlatform} appPlatform The underlying `AppPlatform`. + * @returns {AppPlatform} The `AppPlatform` that we map to in order to select the appropriate `MediaType`. + */ +function selectionAppPlatformFromAppPlatform(objectGraph, appPlatform) { + if (appPlatform === "messages") { + switch (objectGraph.client.deviceType) { + case "pad": { + return "pad"; + } + default: { + return "phone"; + } + } + } + return appPlatform; +} +/** + * Provide the caller with an ordered array of screenshots for a given context. The first object can be used in search + * lockups, and the array will only contain screenshots for the supported app platforms. + * + * @param data The api product data (containing supported app platforms and screenshots) + * @param useCase + * @param includedAppPlatforms Optionally, a list of app platforms to confine the screenshots to. + * @param clientIdentifierOverride + * @param productVariantData + * @param isAd Whether the screenshots are being gathered for an ad lockup. Defaults to false. + * @returns An ordered array of screenshots for display on a product page + * */ +export function screenshotsFromData(objectGraph, data, useCase, includedAppPlatforms = null, clientIdentifierOverride, productVariantData, isAd = false, cropCode) { + return validation.context("screenshotsFromData", () => { + const screenshots = []; + if (serverData.isNull(productVariantData)) { + productVariantData = productPageVariants.productVariantDataForData(objectGraph, data); // resolve if not resolved by caller. + } + let sortedAppPlatforms = includedAppPlatforms; + if (!sortedAppPlatforms || sortedAppPlatforms.length === 0) { + const preferredClientIdentifier = clientIdentifierOverride || objectGraph.host.clientIdentifier; + let preferredDeviceType = objectGraph.client.deviceType; + if (preferredClientIdentifier === client.watchIdentifier) { + preferredDeviceType = "watch"; + } + if (clientIdentifierOverride === "VisionAppStore" /* ClientIdentifier.VisionAppStore */ || + clientIdentifierOverride === "com.apple.visionproapp" /* ClientIdentifier.VisionCompanion */) { + preferredDeviceType = "vision"; + } + sortedAppPlatforms = sortedAppPlatformsFromData(objectGraph, data, preferredClientIdentifier, preferredDeviceType); + } + for (const appPlatform of sortedAppPlatforms) { + const supplementaryAppPlatforms = []; + let screenshotData; + if (appPlatform === "messages") { + screenshotData = messagesScreenshotsFromData(objectGraph, data, "ios"); + if (supportsFunCameraFromData(objectGraph, data, "ios")) { + supplementaryAppPlatforms.push("faceTime"); + } + } + else if (appPlatform === "tv" && !objectGraph.host.isTV) { + // For tvOS screenshots displayed on other platforms. + screenshotData = screenshotsByTypeFromData(objectGraph, data, productVariantData, isAd, "appletvos"); + } + else if (appPlatform === "vision" && !objectGraph.host.isVision) { + // For visionOS screenshots displayed on other platforms. + screenshotData = screenshotsByTypeFromData(objectGraph, data, productVariantData, isAd, "xros"); + } + else if (appPlatform === "mac" && !objectGraph.host.isMac) { + // For Mac screenshots displayed on other platforms. + screenshotData = screenshotsByTypeFromData(objectGraph, data, productVariantData, isAd, "osx"); + } + else if ((appPlatform === "phone" || appPlatform === "pad" || appPlatform === "watch") && + !objectGraph.host.isiOS && + !objectGraph.host.isWatch) { + // For iPhone / iPad / watch screenshots displayed on other platforms. + screenshotData = screenshotsByTypeFromData(objectGraph, data, productVariantData, isAd, "ios"); + } + else { + screenshotData = screenshotsByTypeFromData(objectGraph, data, productVariantData, isAd); + } + if (!screenshotData) { + continue; + } + const bestScreenshots = bestScreenshotData(objectGraph, screenshotData, appPlatform, useCase, supplementaryAppPlatforms, cropCode); + if (bestScreenshots) { + screenshots.push(bestScreenshots); + } + } + return screenshots; + }); +} +/** + * Creates an array of product media from the given screenshots. If videos are desired + * to be inserted in the same media row, this must be done elsewhere. + * @param objectGraph The object graph. + * @param data Apps resource data. + * @param screenshots The screenshots objects with which to configure the media. + * @return A list of product media objects. + */ +function productMediaFromScreenshots(objectGraph, data, screenshots) { + const allMedia = []; + if (screenshots && screenshots.length > 0) { + const allPlatforms = screenshots.map((platformScreenshots) => { + return platformScreenshots.mediaPlatform; + }); + for (const screenshotsForPlatform of screenshots) { + // Create media items from all the screenshots. + const screenshotMediaItems = []; + for (const screenshotArtwork of screenshotsForPlatform.artwork) { + const screenshotItem = new modelsShelves.ProductMediaItem(); + screenshotItem.screenshot = screenshotArtwork; + screenshotMediaItems.push(screenshotItem); + } + const platform = screenshotsForPlatform.mediaPlatform; + const productMedia = new modelsShelves.ProductMedia(screenshotMediaItems, platform, allPlatforms, descriptionOfMediaPlatform(objectGraph, platform), descriptionOfAllMediaPlatforms(objectGraph, data, allPlatforms), placementOfAllMediaPlatformsDescription(objectGraph, data, allPlatforms)); + allMedia.push(productMedia); + } + } + return allMedia; +} +/** + * Build a set of of `ProductMedia` from apps resource + * @param data Apps resource data + * @param useCase Artwork use case + * @param includedAppPlatforms What platforms are included. + * @param productVariantData A variant to use. This can be populated as an optimization to avoid re-resolving the same variant data, e.g. in a product page. + * @param clientIdentifierOverride + */ +export function productMediaFromData(objectGraph, data, useCase, includedAppPlatforms = null, productVariantData = null, clientIdentifierOverride) { + const screenshots = screenshotsFromData(objectGraph, data, useCase, includedAppPlatforms, clientIdentifierOverride, productVariantData); + return productMediaFromScreenshots(objectGraph, data, screenshots); +} +/** + * Finds the best screenshot data from a response to use for the given parameters. + * @param data The data from which to derive the screenshots. + * @param appPlatform The app platform to which the screenshots belong. + * @param supplementaryAppPlatforms + * @returns The best available screenshots. + */ +function bestScreenshotData(objectGraph, data, appPlatform, useCase, supplementaryAppPlatforms, cropCode) { + const selectionPlatform = selectionAppPlatformFromAppPlatform(objectGraph, appPlatform); + const screenshotTypes = mediaTypesForAppPlatform(objectGraph, selectionPlatform, objectGraph.client.screenSize); + let bestScreenshot = null; + let bestScreenshotType; + for (let i = 0; i < screenshotTypes.length && !serverData.isDefinedNonNullNonEmpty(bestScreenshot); i++) { + bestScreenshot = serverData.asArrayOrEmpty(data, screenshotTypes[i]); + bestScreenshotType = screenshotTypes[i]; + } + if (serverData.isDefinedNonNullNonEmpty(bestScreenshot)) { + const artworks = bestScreenshot.map(function (screenshotArtwork) { + return artworkFromApiArtwork(objectGraph, screenshotArtwork, { + useCase: useCase, + cropCode: cropCode, + }); + }); + const platform = mediaPlatformForTypeAndAppPlatform(objectGraph, appPlatform, bestScreenshotType, supplementaryAppPlatforms); + const screenshots = new modelsBase.Screenshots(artworks, platform); + return screenshots; + } + return null; +} +/** + * Returns a list of sorted app platforms for displaying screenshots. This contains the sorting logic for screenshots. + * + * @param data Server data for the app + * @param clientIdentifier Identifier of the current client. + * @param deviceType Type of the current device. + * @returns A sorted list of AppPlatform values to use when displaying + * */ +export function sortedAppPlatformsFromData(objectGraph, data, clientIdentifier, deviceType) { + return derivedData.value(data, `sortedAppPlatformsFromData.${clientIdentifier}.${deviceType}`, () => { + var _a; + const supportedAppPlatforms = supportedAppPlatformsFromData(objectGraph, data); + const excludedAppPlatforms = []; + let sortedAppPlatforms = []; + const addAppPlatformIfPossible = function (appPlatform, excludePlatform) { + if (sortedAppPlatforms.indexOf(appPlatform) !== -1) { + return; + } + if (excludedAppPlatforms.indexOf(appPlatform) !== -1) { + return; + } + if (supportedAppPlatforms.indexOf(appPlatform) !== -1) { + sortedAppPlatforms.push(appPlatform); + if (excludePlatform) { + excludedAppPlatforms.push(excludePlatform); + } + } + }; + // If there is an `AppPlatform` associated with the active `Intent`, give + // that first priority + if ((_a = objectGraph.activeIntent) === null || _a === void 0 ? void 0 : _a.appPlatform) { + addAppPlatformIfPossible(objectGraph.activeIntent.appPlatform); + } + if (clientIdentifier === "VisionAppStore" /* ClientIdentifier.VisionAppStore */ || + clientIdentifier === "com.apple.visionproapp" /* ClientIdentifier.VisionCompanion */) { + addAppPlatformIfPossible("vision"); + } + // Next, priority is given to the client + switch (clientIdentifier) { + case client.watchIdentifier: { + addAppPlatformIfPossible("watch"); + break; + } + case client.messagesIdentifier: { + addAppPlatformIfPossible("messages"); + break; + } + default: { + break; + } + } + // Next the current device type + switch (deviceType) { + case "phone": { + addAppPlatformIfPossible("phone"); + break; + } + case "pad": { + addAppPlatformIfPossible("pad"); + break; + } + case "tv": { + addAppPlatformIfPossible("tv"); + break; + } + case "watch": { + addAppPlatformIfPossible("watch"); + break; + } + case "mac": { + addAppPlatformIfPossible("mac"); + break; + } + case "vision": { + addAppPlatformIfPossible("vision"); + break; + } + default: { + break; + } + } + // For Apple Silicon and visionOS, prefer iPad platform over iPhone + if (clientIdentifier === "VisionAppStore" /* ClientIdentifier.VisionAppStore */ || + clientIdentifier === "com.apple.visionproapp" /* ClientIdentifier.VisionCompanion */) { + addAppPlatformIfPossible("pad"); + addAppPlatformIfPossible("phone"); + } + if (objectGraph.appleSilicon.isSupportEnabled || objectGraph.client.isVision) { + addAppPlatformIfPossible("pad"); + addAppPlatformIfPossible("phone"); + } + else { + addAppPlatformIfPossible("phone"); + addAppPlatformIfPossible("pad"); + } + addAppPlatformIfPossible("mac"); + addAppPlatformIfPossible("vision"); + addAppPlatformIfPossible("tv"); + addAppPlatformIfPossible("watch"); + addAppPlatformIfPossible("messages"); + sortedAppPlatforms = sortedAppPlatforms.filter(function (appPlatform) { + return excludedAppPlatforms.indexOf(appPlatform) === -1; + }); + return sortedAppPlatforms; + }); +} +/** + * For a given server data, this will return the Game Center features that the app supports + * + * @param data Server data for the app + * @returns An array of supported Game Center features + * */ +export function supportedGameCenterFeaturesFromData(data) { + var _a; + if (isNothing(data)) { + return undefined; + } + return ((_a = derivedData.value(data, "supportedGameCenterFeaturesFromData", () => { + const features = []; + const supportedGameCenterFeatures = serverData.asArrayOrEmpty(data, "attributes.supportedGameCenterFeatures"); + if (supportedGameCenterFeatures.includes("achievements")) { + features.push("achievements"); + } + if (supportedGameCenterFeatures.includes("challenges")) { + features.push("challenges"); + } + if (supportedGameCenterFeatures.includes("leaderboards")) { + features.push("leaderboards"); + } + if (supportedGameCenterFeatures.includes("multiplayer-activities")) { + features.push("multiplayer-activities"); + } + return features; + })) !== null && _a !== void 0 ? _a : undefined); +} +/** + * For a given server data, returns whether the game is eligible for the Games App + * This will default to true since we generally expect apps we view in the Games app to be games. + * It will be unusual that this is evaluated to `false`. + * + * @param data Server data for the app + * @returns A boolean indicating whether game is eligible for display + * */ +export function isEligibleForGamesApp(data) { + var _a; + if (isNothing(data)) { + return true; + } + return (_a = serverData.asBoolean(data, "attributes.isEligibleForGamesApp")) !== null && _a !== void 0 ? _a : true; +} +/** + * For a given server data, this will return the platforms that the app supports + * + * @param data Server data for the app + * @returns An array of supported AppPlatforms + * */ +export function supportedAppPlatformsFromData(objectGraph, data) { + if (!data) { + return null; + } + return derivedData.value(data, "supportedAppPlatformsFromData", () => { + const hasMessagesExtension = hasMessagesExtensionFromData(objectGraph, data, "ios"); + const isHiddenFromSpringboard = isHiddenFromSpringboardFromData(objectGraph, data); + const isAppleWatchSupported = isAppleWatchSupportedFromData(objectGraph, data); + const serverDeviceFamilies = mediaAttributes.attributeAsArrayOrEmpty(data, "deviceFamilies"); + const appPlatforms = []; + for (const serverDeviceFamily of serverDeviceFamilies) { + switch (serverDeviceFamily) { + case "iphone": + if (!isHiddenFromSpringboard) { + appPlatforms.push("phone"); + } + break; + case "ipad": + if (!isHiddenFromSpringboard) { + appPlatforms.push("pad"); + } + break; + case "tvos": + appPlatforms.push("tv"); + break; + case "watch": + appPlatforms.push("watch"); + break; + case "realityDevice": + appPlatforms.push("vision"); + break; + default: + break; + } + } + if (hasMessagesExtension) { + appPlatforms.push("messages"); + } + if (isAppleWatchSupported) { + appPlatforms.push("watch"); + } + if (contentDeviceFamily.dataHasDeviceFamily(objectGraph, data, "mac")) { + appPlatforms.push("mac"); + } + return appPlatforms; + }); +} +/** + * Returns a localized, user-friendly description of all media platforms. This may be a comma delimited + * list of the platforms (including supplementary platforms), or it may be 'Only for ___', depending + * on the context. + * + * The localization keys used by this function are defined natively, and are updated using + * `tools/platform-media-localizations.py`. If the key doesn't exist, then the script needs to + * be updated to add the new combination/order of platforms. + * + * For failed attempts to localize the string, this function will fallback to a default order that is + * guaranteed to exist. + * + * @param objectGraph The object graph. + * @param data Apps resource data. + * @param allPlatforms The list of platforms to describe. + * @returns The friendly description of all platforms. + */ +export function descriptionOfAllMediaPlatforms(objectGraph, data, allPlatforms) { + if (shouldShowOnlyForPlatformDescription(objectGraph, data, allPlatforms)) { + const platform = allPlatforms[0]; + const platformKey = platform.appPlatform.toUpperCase(); + return objectGraph.loc.string(`ONLY_FOR_${platformKey}_APP`); + } + // Flatten all platform partial keys, including their supplementary platforms + let keys = allPlatforms.reduce((partialResult, platform) => partialResult.concat(platformLocalizationKeys(platform)), []); + try { + // Attempt to localize the constructed key + return objectGraph.loc.tryString(`PLATFORMS_${keys.join("_")}`); + } + catch (error) { + // If the key does not exist, a best attempt fallback string will be provided. + const fallbackOrder = ["PHONE", "PAD", "MAC", "VISION", "TV", "WATCH", "MESSAGES", "FACETIME"]; + keys = fallbackOrder.filter((key) => keys.includes(key)); + return objectGraph.loc.string(`PLATFORMS_${keys.join("_")}`); + } +} +/** + * Determines where to place the all platforms description, which is visible when the product media is collapsed, or there is only one platform. + * This is only used by iOS, visionOS & macOS. For tvOS & watchOS, we always put the media description at the bottom. + * + * @param objectGraph The object graph. + * @param data Apps resource data. + * @param allPlatforms The list of platforms to describe. + * @returns Where to place the all platforms description. + */ +export function placementOfAllMediaPlatformsDescription(objectGraph, data, allPlatforms) { + if (shouldShowOnlyForPlatformDescription(objectGraph, data, allPlatforms)) { + return "top"; + } + else { + return "bottom"; + } +} +/** + * Determines whether we want to use the 'Only for ___' text to describe `allPlatforms`. + * + * @param objectGraph The object graph. + * @param data Apps resource data. + * @param allPlatforms The list of platforms to describe. + * @returns Whether we want to use 'Only for ___' text to describe `allPlatforms`. + */ +function shouldShowOnlyForPlatformDescription(objectGraph, data, allPlatforms) { + if (allPlatforms.length === 1) { + const platform = allPlatforms[0]; + const supportsMacOSCompatibleIOSBinary = supportsMacOSCompatibleIOSBinaryFromData(objectGraph, data, objectGraph.appleSilicon.isSupportEnabled); + const supportsVisionOSCompatibleIOSBinary = supportsVisionOSCompatibleIOSBinaryFromData(objectGraph, data); + const runnableAppPlatforms = runnableAppPlatformsForDevice(objectGraph, objectGraph.client.deviceType, supportsMacOSCompatibleIOSBinary, supportsVisionOSCompatibleIOSBinary); + const isRunnableOnCurrentDevice = supportsPlatform(runnableAppPlatforms, platform.appPlatform); + const noSupplementaryPlatforms = platform.supplementaryAppPlatforms.length === 0; + const isForDifferentDevice = platform.appPlatform !== objectGraph.client.deviceType; + if (noSupplementaryPlatforms && isForDifferentDevice && !isRunnableOnCurrentDevice) { + return true; + } + } + return false; +} +/** + * Returns a localized description of a media platform, including any supplementary platforms. + * e.g. 'Mac' or 'iMessage, FaceTime'. + * + * @param objectGraph Object graph, used for localizing the string. + * @param allPlatforms The platform to describe. + * @returns The friendly description of the platform. + */ +export function descriptionOfMediaPlatform(objectGraph, platform) { + const keys = platformLocalizationKeys(platform); + return objectGraph.loc.string(`PLATFORMS_${keys.join("_")}`); +} +/** + * Returns an array of partial loc keys that represent a media platform. + * This consists the media's app platform + any supplementary platforms. + * e.g. ["MAC"] or ["MESSAGES", "FACETIME"] + * + * @param platform The media platform. + * @returns The list of partial loc key that represent the media platform. + */ +function platformLocalizationKeys(platform) { + const appPlatformKey = platform.appPlatform.toUpperCase(); + const supplementaryPlatformKeys = platform.supplementaryAppPlatforms.map((supplementaryPlatform) => supplementaryPlatform.toUpperCase()); + return [appPlatformKey].concat(supplementaryPlatformKeys); +} +/** + * Determines if a given app has a compatible iOS binary for this client. + * + * @param {mediaDataStructure.Data} data The product data to use. + * @param {boolean} doesClientSupportMacOSCompatibleIOSBinary Whether the client supports macOS compatible iOS binaries + * @returns {boolean} True when the app and device are halva. + */ +export function supportsMacOSCompatibleIOSBinaryFromData(objectGraph, data, doesClientSupportMacOSCompatibleIOSBinary) { + let isIOSBinaryMacOSCompatible = mediaAttributes.attributeAsBooleanOrFalse(data, "isIOSBinaryMacOSCompatible"); + // Override for News in Moltres on Mac + if (preprocessor.GAMES_TARGET && data.id === "1066498020" && objectGraph.client.deviceType === "mac") { + isIOSBinaryMacOSCompatible = true; + } + return doesClientSupportMacOSCompatibleIOSBinary && isIOSBinaryMacOSCompatible; +} +/** + * Determines if a given app has a compatible iOS binary for the current client. + * + * @param {mediaDataStructure.Data} data The product data to use. + * @returns {boolean} True when the app and device are visionOS. + */ +export function supportsVisionOSCompatibleIOSBinaryFromData(objectGraph, data) { + return (objectGraph.client.isVision && + mediaPlatformAttributes.platformAttributeAsBooleanOrFalse(data, "ios", "isXROSCompatible")); +} +/** + * Determines if a given app has a compatible iOS binary for arbitrary clients. + * + * @param {mediaDataStructure.Data} data The product data to use. + * @returns {boolean} True when the app can run on visionOS. + */ +export function supportsVisionOSCompatibleIOSBinaryOnAnyClient(data) { + return mediaPlatformAttributes.platformAttributeAsBooleanOrFalse(data, "ios", "isXROSCompatible"); +} +/** + * Determines app binary traits. + * + * @param {mediaDataStructure.Data} data The product data to use. + * @returns {string[]} The app binary traits. + */ +export function appBinaryTraitsFromData(objectGraph, data) { + if (!objectGraph.client.isiOS) { + return undefined; + } + let appBinaryTraits; + if (objectGraph.isAvailable(ads) && + ["debug", "internal"].includes(objectGraph.client.buildType) && + isSome(objectGraph.ads.fetchAppBinaryTraitsOverride)) { + // use client override for debugging internal builds + appBinaryTraits = objectGraph.ads.fetchAppBinaryTraitsOverride(); + } + if (isNothing(appBinaryTraits)) { + // parse from server response + appBinaryTraits = mediaPlatformAttributes.platformAttributeAsArrayOrEmpty(data, "ios", "appBinaryTraits"); + } + return appBinaryTraits; +} +/** + * Determines whether the product has external browser engine. + * @param objectGraph Current object graph + * @param data The product data + * @returns True if the product has external browser engine + */ +export function hasExternalBrowserForData(objectGraph, data) { + var _a; + const appBinaryTraits = appBinaryTraitsFromData(objectGraph, data); + const externalBrowserTraits = new Set(["uses-non-webkit-browser-engine", "is-custom-browser-engine-app"]); + return (_a = appBinaryTraits === null || appBinaryTraits === void 0 ? void 0 : appBinaryTraits.some((trait) => externalBrowserTraits.has(trait))) !== null && _a !== void 0 ? _a : false; +} +/** + * Determines minimum os version + * + * @param {mediaDataStructure.Data} data The product data to use. + * @param {boolean} isClientHalva Whether the client is halva. + * @returns {string} The minimum OS version. + */ +export function minimumOSVersionFromData(objectGraph, data, isClientHalva) { + const supportsMacOSCompatibleIOSBinary = supportsMacOSCompatibleIOSBinaryFromData(objectGraph, data, isClientHalva); + const supportsVisionOSCompatibleIOSBinary = supportsVisionOSCompatibleIOSBinaryFromData(objectGraph, data); + if (supportsMacOSCompatibleIOSBinary) { + const minimumOSVersion = contentAttributes.contentAttributeAsString(objectGraph, data, "minimumMacOSVersion"); + if (serverData.isDefinedNonNullNonEmpty(minimumOSVersion)) { + return minimumOSVersion; + } + } + else if (supportsVisionOSCompatibleIOSBinary) { + const minimumOSVersion = contentAttributes.contentAttributeAsString(objectGraph, data, "minimumXROSVersion"); + if (serverData.isDefinedNonNullNonEmpty(minimumOSVersion)) { + return minimumOSVersion; + } + } + const attributePlatform = contentAttributes.bestAttributePlatformFromData(objectGraph, data); + return mediaPlatformAttributes.platformAttributeAsString(data, attributePlatform, "minimumOSVersion"); +} +/** + * Determines required capabilities for device. + * + * @param {mediaDataStructure.Data} data The product data to use. + * @param {boolean} isClientHalva Whether the client is halva. + * @returns {string} The device capabilities to use. + */ +export function requiredCapabilitiesFromData(objectGraph, data, isClientHalva) { + const supportsMacOSCompatibleIOSBinary = supportsMacOSCompatibleIOSBinaryFromData(objectGraph, data, isClientHalva); + const supportsVisionOSCompatibleIOSBinary = supportsVisionOSCompatibleIOSBinaryFromData(objectGraph, data); + if (supportsMacOSCompatibleIOSBinary) { + return contentAttributes.contentAttributeAsString(objectGraph, data, "macRequiredCapabilities"); + } + else if (supportsVisionOSCompatibleIOSBinary) { + return contentAttributes.contentAttributeAsString(objectGraph, data, "requiredCapabilitiesForRealityDevice"); + } + else { + return contentAttributes.contentAttributeAsString(objectGraph, data, "requiredCapabilities"); + } +} +/** + * Returns the app platforms you can buy for on the given device. + * + * @param objectGraph The current object graph + * @param data The data for the app in question + * @param device The device type to check + * @param supportsMacOSCompatibleIOSBinary Whether device and app supports macOS compatible iOS binary + * @param supportsVisionOSCompatibleIOSBinary Whether device and app supports visionOS compatible iOS binary + * @returns An array of supported app platforms + */ +function buyableAppPlatformsForDevice(objectGraph, data, device, supportsMacOSCompatibleIOSBinary, supportsVisionOSCompatibleIOSBinary) { + let systemApps; + switch (device) { + case "phone": + systemApps = sad.systemApps(objectGraph); + if (isSome(data) && systemApps.isSystemAppFromData(data)) { + return ["phone", "watch", "messages"]; + } + else { + return ["phone", "watch", "messages", "tv", "vision"]; + } + case "pad": + systemApps = sad.systemApps(objectGraph); + if (isSome(data) && systemApps.isSystemAppFromData(data)) { + return ["phone", "pad", "messages"]; + } + else { + return ["phone", "pad", "messages", "tv", "vision"]; + } + case "tv": + return ["tv"]; + case "watch": + return ["watch"]; + case "mac": + if (supportsMacOSCompatibleIOSBinary) { + return ["mac", "phone", "pad"]; + } + else { + return ["mac"]; + } + case "vision": + if (supportsVisionOSCompatibleIOSBinary) { + return ["vision", "phone", "pad"]; + } + else { + return ["vision"]; + } + default: + return []; + } +} +/** + * Returns the app platforms you can preorder on for the given device. + * + * @param {DeviceType} device The device type to check + * @param {boolean} supportsMacOSCompatibleIOSBinary Whether device and app are support macOS compatible iOS binary + * @param {boolean} supportsVisionOSCompatibleIOSBinary Whether device and app supports visionOS compatible iOS binary + * @returns {models.AppPlatform[]} An array of supported app platforms + */ +function preorderableAppPlatformsForDevice(objectGraph, device, supportsMacOSCompatibleIOSBinary, supportsVisionOSCompatibleIOSBinary) { + switch (device) { + case "phone": + return ["phone", "watch", "messages"]; + case "pad": + return ["phone", "pad", "messages"]; + case "tv": + return ["tv"]; + case "watch": + return ["watch"]; + case "mac": + if (supportsMacOSCompatibleIOSBinary) { + return ["mac", "phone", "pad"]; + } + else { + return ["mac"]; + } + case "vision": + if (supportsVisionOSCompatibleIOSBinary) { + return ["vision", "phone", "pad"]; + } + else { + return ["vision"]; + } + default: + return []; + } +} +/** + * Returns the app platforms you can run on the given device. + * + * @param {DeviceType} device The device type to check + * @param {boolean} supportsMacOSCompatibleIOSBinary Whether device and app are support macOS compatible iOS binary + * @param {boolean} supportsVisionOSCompatibleIOSBinary Whether device and app supports visionOS compatible iOS binary + * @returns {models.AppPlatform[]} An array of supported app platforms + */ +export function runnableAppPlatformsForDevice(objectGraph, device, supportsMacOSCompatibleIOSBinary, supportsVisionOSCompatibleIOSBinary) { + switch (device) { + case "phone": + return ["phone", "messages"]; + case "pad": + return ["phone", "pad", "messages"]; + case "tv": + return ["tv"]; + case "watch": + return ["watch"]; + case "mac": + if (supportsMacOSCompatibleIOSBinary) { + return ["mac", "phone", "pad"]; + } + else { + return ["mac"]; + } + case "vision": + if (supportsVisionOSCompatibleIOSBinary) { + return ["vision", "phone", "pad"]; + } + else { + return ["vision"]; + } + default: + return []; + } +} +/** + * Determines if a given piece of content supports the provided app platform + * + * @param {models.AppPlatform[]} appPlatforms The app platforms supported by the content + * @param {AppPlatform} platform The platform to check + * @returns {boolean} True if the platform is supported, false if not + */ +export function supportsPlatform(appPlatforms, platform) { + return appPlatforms.indexOf(platform) !== -1; +} +/** + * Determines if a given piece of content is buyable on the provided device. + * + * @param {models.AppPlatform[]} appPlatforms The app platforms supported by the content + * @param {DeviceType} device The device type to check + * @param {boolean} supportsMacOSCompatibleIOSBinary Whether app and device support macOS compatible iOS binary. + * @param {boolean} supportsVisionOSCompatibleIOSBinary Whether device and app supports visionOS compatible iOS binary + * @param {boolean} isMacOSAppBuyableOnDevice Whether a macOS app is buyable on this device (this enables additional criteria for Apple Silicon). + * @returns {boolean} True if any of the app platforms are buyable on the given device, false if not + */ +export function buyableOnDevice(objectGraph, data, appPlatforms, device, supportsMacOSCompatibleIOSBinary, supportsVisionOSCompatibleIOSBinary, isMacOSAppBuyableOnDevice = true) { + const platforms = buyableAppPlatformsForDevice(objectGraph, data, device, supportsMacOSCompatibleIOSBinary, supportsVisionOSCompatibleIOSBinary); + // Do any of the platforms supported by the device match any of the content's app platforms? + if (!platforms.some((platform) => supportsPlatform(appPlatforms, platform))) { + return false; + } + if (objectGraph.client.isMac && platforms.includes("mac")) { + return isMacOSAppBuyableOnDevice; + } + return true; +} +/** + * Determines macOS runnability info for apps and bundles on macOS. + */ +function macOSRunnabilityInfoFromData(objectGraph, data) { + var _a; + const runnabilityInfo = new RunnabilityInfo(); + // Return most permissible runnability for non-macOS platforms. + if (objectGraph.client.deviceType !== "mac") { + return runnabilityInfo; + } + // Use media API attributes for non-bundles. + if (data.type !== "app-bundles") { + runnabilityInfo.runsOnIntel = + (_a = contentAttributes.contentAttributeAsBoolean(objectGraph, data, "runsOnIntel", contentAttributes.defaultAttributePlatform(objectGraph))) !== null && _a !== void 0 ? _a : true; + runnabilityInfo.runsOnAppleSilicon = contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "runsOnAppleSilicon", contentAttributes.defaultAttributePlatform(objectGraph)); + runnabilityInfo.requiresRosetta = contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "requiresRosetta", contentAttributes.defaultAttributePlatform(objectGraph)); + return runnabilityInfo; + } + const bundleAppsData = mediaRelationship.relationshipCollection(data, "apps"); + // Return most permissible runnability when there no children available + if (bundleAppsData.length === 0) { + return runnabilityInfo; + } + // Synthesize runnability info from bundle apps + for (const appData of bundleAppsData) { + if (serverData.isNull(appData.attributes)) { + continue; + } + const appRunnabilityInfo = macOSRunnabilityInfoFromData(objectGraph, appData); + runnabilityInfo.runsOnIntel = runnabilityInfo.runsOnIntel && appRunnabilityInfo.runsOnIntel; + runnabilityInfo.runsOnAppleSilicon = + runnabilityInfo.runsOnAppleSilicon && appRunnabilityInfo.runsOnAppleSilicon; + runnabilityInfo.requiresRosetta = runnabilityInfo.requiresRosetta || appRunnabilityInfo.requiresRosetta; + } + return runnabilityInfo; +} +/** + * Determines if a given macOS app is buyable on this device. + * + */ +export function isMacOSAppBuyableAndRunnableFromData(objectGraph, data, isAppleSiliconSupportEnabled, isRosettaAvailable) { + const runnabilityInfo = macOSRunnabilityInfoFromData(objectGraph, data); + if (isAppleSiliconSupportEnabled) { + return (runnabilityInfo.runsOnAppleSilicon && + (!runnabilityInfo.requiresRosetta || (runnabilityInfo.requiresRosetta && isRosettaAvailable))); + } + else { + return runnabilityInfo.runsOnIntel; + } +} +/** + * Determines if a given piece of content is preorderable on the provided device. + * + * @param {models.AppPlatform[]} appPlatforms The app platforms supported by the content + * @param {DeviceType} device The device type to check + * @param {boolean} supportsMacOSCompatibleIOSBinary Whether app and device support macOS compatible iOS binary. + * @param {boolean} supportsVisionOSCompatibleIOSBinary Whether app and device support visionOS compatible iOS binary. + * @returns {boolean} True if any of the app platforms are buyable on the given device, false if not + */ +export function preorderableOnDevice(objectGraph, appPlatforms, device, supportsMacOSCompatibleIOSBinary, supportsVisionOSCompatibleIOSBinary) { + const platforms = preorderableAppPlatformsForDevice(objectGraph, device, supportsMacOSCompatibleIOSBinary, supportsVisionOSCompatibleIOSBinary); + // Do any of the platforms supported by the device match any of the content's app platforms? + return platforms.some((platform) => supportsPlatform(appPlatforms, platform)); +} +/** + * Determines if any of a given array of app platforms can be run on the provided device. + * + * @param appPlatforms The app platforms supported by a piece of content. + * @param device The device type to check. + * @param {boolean} supportsMacOSCompatibleIOSBinary Whether app and device support macOS compatible iOS binary. + * @param {boolean} supportsVisionOSCompatibleIOSBinary Whether app and device support visionOS compatible iOS binary. + * @returns `true` if any of the app platforms can be run on the given device; `false` otherwise. + */ +export function runnableOnDevice(objectGraph, appPlatforms, device, supportsMacOSCompatibleIOSBinary, supportsVisionOSCompatibleIOSBinary, isMacOSAppRunnableOnDevice = true) { + const runnablePlatforms = runnableAppPlatformsForDevice(objectGraph, device, supportsMacOSCompatibleIOSBinary, supportsVisionOSCompatibleIOSBinary); + // Do any of the platforms supported by the device match any of the content's app platforms? + if (!runnablePlatforms.some((platform) => supportsPlatform(appPlatforms, platform))) { + return false; + } + if (objectGraph.client.isMac && appPlatforms.includes("mac")) { + return isMacOSAppRunnableOnDevice; + } + return true; +} +/** + * Determines if a given piece of content is runnable on the provided device. + * + * @param data The product data to use. + * @param device + * @param {boolean} doesClientSupportMacOSCompatibleIOSBinary Whether the client supports macOS compatible iOS binaries + * @returns {boolean} True if the product can be run on the provided device, false if not + */ +export function runnableOnDeviceWithData(objectGraph, data, device, doesClientSupportMacOSCompatibleIOSBinary) { + // (1) Required capabilities mismatch + if (!lockups.deviceHasCapabilitiesFromData(objectGraph, data)) { + return false; + } + // (2) 32-bit only, unsupported deletable system app, doesn't meet minimum OS requirements, or doesn't support current platform + // Note that Filter.UnsupportedPlatform only checks if the product is buyable, not runnable + const filter = 2 /* filtering.Filter.ThirtyTwoBit */ | + 4 /* filtering.Filter.UnsupportedSystemDeletableApps */ | + 512 /* filtering.Filter.MinimumOSRequirement */ | + 128 /* filtering.Filter.UnsupportedPlatform */ | + 8192 /* filtering.Filter.MacOSRosetta */; + if (filtering.shouldFilter(objectGraph, data, filter)) { + return false; + } + // (3) Finally, check if any of the product platforms are supported on this device + const supportsMacOSCompatibleIOSBinary = supportsMacOSCompatibleIOSBinaryFromData(objectGraph, data, doesClientSupportMacOSCompatibleIOSBinary); + const supportsVisionOSCompatibleIOSBinary = supportsVisionOSCompatibleIOSBinaryFromData(objectGraph, data); + const runnableAppPlatforms = runnableAppPlatformsForDevice(objectGraph, device, supportsMacOSCompatibleIOSBinary, supportsVisionOSCompatibleIOSBinary); + const productAppPlatforms = supportedAppPlatformsFromData(objectGraph, data); + return runnableAppPlatforms.some((platform) => supportsPlatform(productAppPlatforms, platform)); +} +/** + * Determines which screenshot keys (MediaType) we need to use to pull the appropriate screenshots + * from the server data. + * + * @param appPlatform The app platform requested + * @param screenSize The size of the screen being used to display the screenshots + * @returns An array of ScreenshotType strings that can be used on the server data + * */ +export function mediaTypesForAppPlatform(objectGraph, appPlatform, screenSize) { + switch (appPlatform) { + case "mac": { + return ["mac"]; + } + case "watch": { + if (screenSize.isEqualTo(screenSizeWatchUltra) || screenSize.isEqualTo(screenSizeN230)) { + // 2022 is the preferred dropwell for Ultra devices + return ["appleWatch_2022", "appleWatch_2024", "appleWatch_2021", "appleWatch_2018", "appleWatch"]; + } + else { + return ["appleWatch_2024", "appleWatch_2022", "appleWatch_2021", "appleWatch_2018", "appleWatch"]; + } + } + case "tv": { + return ["appleTV"]; + } + case "vision": { + return ["appleVisionPro"]; + } + case "pad": { + const types = []; + if ((screenSize.isEqualTo(screenSizeIPadPro2018) || + screenSize.isEqualTo(screenSizeIPadPro2018Landscape) || + screenSize.isEqualTo(screenSizeJ720) || + screenSize.isEqualTo(screenSizeJ720Landscape)) && + objectGraph.client.screenCornerRadius > 0.0) { + types.push("ipadPro_2018"); + types.push("ipad_11"); + types.push("ipadPro"); + types.push("ipad_10_5"); + types.push("ipad"); + } + else if (screenSize.isEqualTo(screenSizeIPadPro)) { + types.push("ipadPro"); + types.push("ipadPro_2018"); + types.push("ipad_11"); + types.push("ipad_10_5"); + types.push("ipad"); + } + else if (screenSize.isEqualTo(screenSizeIPad11) || + screenSize.isEqualTo(screenSizeIPad11Landscape) || + screenSize.isEqualTo(screenSizeIPadJ310) || + screenSize.isEqualTo(screenSizeIPadJ310Landscape) || + screenSize.isEqualTo(screenSizeJ717) || + screenSize.isEqualTo(screenSizeJ717Landscape)) { + types.push("ipad_11"); + types.push("ipadPro_2018"); + types.push("ipadPro"); + types.push("ipad_10_5"); + types.push("ipad"); + } + else if (screenSize.isEqualTo(screenSizeIPad105)) { + types.push("ipad_10_5"); + types.push("ipad"); + types.push("ipad_11"); + types.push("ipadPro"); + types.push("ipadPro_2018"); + } + else if (screenSize.isEqualTo(screenSizeIPadAir2020)) { + types.push("ipad_11"); + types.push("ipadPro"); + types.push("ipadPro_2018"); + types.push("ipad_10_5"); + types.push("ipad"); + } + else if (screenSize.isEqualTo(screenSizeIPad102)) { + types.push("ipad"); + types.push("ipad_10_5"); + types.push("ipad_11"); + types.push("ipadPro"); + types.push("ipadPro_2018"); + } + else { + // Regardless of screen size match, we should add on 'some' iPad. + types.push("ipadPro_2018"); + types.push("ipad_11"); + types.push("ipad"); + types.push("ipad_10_5"); + types.push("ipadPro"); + } + return types; + } + case "phone": { + /** Phone Best Match Policy ** + + The best match is given by |B| + |L| + |S|, where: + B: Exact type match + L: All types larger than the exact type, in increasing order + S: All types smaller than the exact type, in decreasing order + + Example: + Types for iphone6 == [iphone6, iphone6+, iphone_5_8, iphone5, iphone] + Types for iphone5 == [iphone5, iphone6, iphone6+, iphone_5_8, iphone] + + ** */ + // Grab the exact match. + let perfectMatch; + if (screenSize.isEqualTo(screenSizeIphone65) || screenSize.isEqualTo(screenSizeIPhone134)) { + perfectMatch = "iphone_6_5"; + } + else if (screenSize.isEqualTo(screenSizeIPhone58) || + screenSize.isEqualTo(screenSizeIPhone131) || + screenSize.isEqualTo(screenSizeIPhone132)) { + perfectMatch = "iphone_5_8"; + } + else if (screenSize.isEqualTo(screenSizeIPhoneOriginal)) { + perfectMatch = "iphone"; + } + else if (screenSize.isEqualTo(screenSizeIPhone5)) { + perfectMatch = "iphone5"; + } + else if (screenSize.isEqualTo(screenSizeIPhone6)) { + perfectMatch = "iphone6"; + } + else if (screenSize.isEqualTo(screenSizeIPhone6Plus)) { + perfectMatch = "iphone6+"; + } + else if (screenSize.isEqualTo(screenSizeIPhone61) || screenSize.isEqualTo(screenSizeD93)) { + perfectMatch = "iphone_d73"; + } + else if (screenSize.isEqualTo(screenSizeIPhone67) || + screenSize.isEqualTo(screenSizeD94) || + screenSize.isEqualTo(screenSizeD23)) { + perfectMatch = "iphone_d74"; + } + else { + perfectMatch = "iphone_5_8"; + } + // Append remaining types to our exact match. + const perfectMatchIndex = decreasingPhoneTypes.indexOf(perfectMatch); + const largerTypes = decreasingPhoneTypes.slice(0, perfectMatchIndex); + largerTypes.reverse(); + const smallerTypes = decreasingPhoneTypes.slice(perfectMatchIndex + 1); + const perfectMatchArray = [perfectMatch]; + return perfectMatchArray.concat(largerTypes, smallerTypes); + } + default: { + return []; + } + } +} +export function combinedFileSizeFromData(objectGraph, data) { + var _a; + if (serverData.isNull(data)) { + return null; + } + // This background asset information is for the work done in SydneyB + const backgroundAssetsInfo = contentAttributes.contentAttributeAsDictionary(objectGraph, data, "backgroundAssetsInfo"); + // This background asset information is for the work done in SydneyE + const backgroundAssetsInfoWithOptional = contentAttributes.contentAttributeAsDictionary(objectGraph, data, "backgroundAssetsInfoWithOptional"); + const isIOSBinaryCompatibleWithMac = supportsMacOSCompatibleIOSBinaryFromData(objectGraph, data, true); + const isMacOnly = contentDeviceFamily.dataOnlyHasDeviceFamily(objectGraph, data, "mac"); + const isWebViewingMac = objectGraph.client.isWeb && ((_a = objectGraph.activeIntent) === null || _a === void 0 ? void 0 : _a.platform) === "mac"; + if ((objectGraph.client.isMac || isWebViewingMac || isMacOnly) && !isIOSBinaryCompatibleWithMac) { + const macFileSize = objectGraph.bag.enableProductPageInstallSize + ? macInstallSizeInBytesFromData(objectGraph, data) + : offers.macFileSizeInBytesFromData(objectGraph, data); + if (serverData.isDefinedNonNullNonEmpty(backgroundAssetsInfoWithOptional)) { + const maxEssentialInstallSizeInBytes = serverData.asNumber(backgroundAssetsInfoWithOptional, "maxEssentialInstallSizeInBytes"); + return new modelsBase.CombinedFileSize(macFileSize, null, null, maxEssentialInstallSizeInBytes); + } + else if (serverData.isDefinedNonNullNonEmpty(backgroundAssetsInfo)) { + const maxDownloadSizeInBytes = serverData.asNumber(backgroundAssetsInfo, "maxDownloadSizeInBytes"); + const maxInstallSizeInBytes = serverData.asNumber(backgroundAssetsInfo, "maxInstallSizeInBytes"); + return new modelsBase.CombinedFileSize(macFileSize, maxDownloadSizeInBytes, maxInstallSizeInBytes, null); + } + return new modelsBase.CombinedFileSize(macFileSize, null, null, null); + } + else { + /* File Size: Our policy is to rely on thinned variant, device model, and universal (in that order). */ + const fileSizeByDevice = mediaAttributes.attributeAsDictionary(data, "fileSizeByDevice"); + if (fileSizeByDevice) { + /* thinnedApplicationVariantIdentifier can contain two device names. The preferred device, and a compatible device. */ + let fileSizeKeys = []; + if (objectGraph.client.thinnedApplicationVariantIdentifier) { + fileSizeKeys = objectGraph.client.thinnedApplicationVariantIdentifier.split(" "); + } + fileSizeKeys = fileSizeKeys.concat([objectGraph.host.deviceModel, "universal"]); + for (const key of fileSizeKeys) { + const fileSizeValue = serverData.asNumber(fileSizeByDevice[key]); + if (fileSizeValue) { + if (serverData.isDefinedNonNullNonEmpty(backgroundAssetsInfoWithOptional)) { + const maxEssentialInstallSizeInBytes = serverData.asNumber(backgroundAssetsInfoWithOptional, "maxEssentialInstallSizeInBytes"); + return new modelsBase.CombinedFileSize(fileSizeValue, null, null, maxEssentialInstallSizeInBytes); + } + else if (serverData.isDefinedNonNullNonEmpty(backgroundAssetsInfo)) { + const maxDownloadSizeInBytes = serverData.asNumber(backgroundAssetsInfo, "maxDownloadSizeInBytes"); + const maxInstallSizeInBytes = serverData.asNumber(backgroundAssetsInfo, "maxInstallSizeInBytes"); + return new modelsBase.CombinedFileSize(fileSizeValue, maxDownloadSizeInBytes, maxInstallSizeInBytes, null); + } + else { + return new modelsBase.CombinedFileSize(fileSizeValue, null, null, null); + } + } + } + } + } + return null; +} +/** + * Extract the file size and unit from a CombinedFileSize object. + * @param objectGraph Current object graph + * @param combinedFileSize The combined file size object + * @returns A FileSizeAndUnit object + */ +export function fileSizeAndUnitFromCombinedFileSize(objectGraph, combinedFileSize) { + let totalFileSize; + if (isSome(combinedFileSize.maxEssentialInstallSizeInBytes)) { + totalFileSize = combinedFileSize.fileSizeByDevice + combinedFileSize.maxEssentialInstallSizeInBytes; + } + else if (isSome(combinedFileSize.maxInstallSizeInBytes)) { + totalFileSize = combinedFileSize.fileSizeByDevice + combinedFileSize.maxInstallSizeInBytes; + } + else { + totalFileSize = combinedFileSize.fileSizeByDevice; + } + if (totalFileSize <= 0) { + return null; + } + // We split using all whitespace characters because in some locs a non-breaking space is used. + const parts = objectGraph.loc.fileSize(totalFileSize).trim().split(/\s+/); + if (parts.length !== 2) { + return null; + } + return { + size: parts[0], + unit: parts[1], + }; +} +/** + * Extracts the install size for a macOS app. + * @param objectGraph Current object graph + * @param data Product page data + * @returns The install size for the Mac binary, in bytes + */ +function macInstallSizeInBytesFromData(objectGraph, data) { + const deviceData = mediaPlatformAttributes.platformAttributeAsDictionary(data, "osx", "installSizeByDeviceInBytes"); + if (isNothing(deviceData)) { + return null; + } + // macOS does not support app thinning, so there is only ever one macOS device in this list. Unfortunately + // there is no known API that gives us this device name, so we resort to hard-coding for now. + const installSizeInBytes = deviceData["Mac"]; + if (isNothing(installSizeInBytes)) { + return null; + } + return serverData.asNumber(installSizeInBytes); +} +/** + * Determines the primary langauge locale, from a given list of locales. + * @param objectGraph Current object graph + * @param locales The list of locales + * @returns A single LanguageLocale object, or null + */ +export function primaryLanguageLocaleFromLocales(objectGraph, locales) { + const languageCount = locales.length; + if (languageCount <= 0) { + return null; + } + return { + tag: serverData.asString(serverData.traverse(locales, "0.tag")).split("-")[0].toUpperCase(), + name: serverData.asString(serverData.traverse(locales, "0.name")), + }; +} +/** + * Determines the uber artwork for the product, if there is any. + * @param {Data} The data for the product. + * @returns {models.Artwork} The artwork for the uber, or `null` if there is none. + * null. + */ +export function productUberFromData(objectGraph, data, options) { + let uberArtworkData; + let uberArtworkPath = null; + let fallbackUberArtworkPath = null; + let cropCode = null; + let fallbackCropCode = null; + switch (objectGraph.client.deviceType) { + case "mac": + if (options.supportsArcade) { + uberArtworkPath = "editorialArtwork.splashFullScreen"; + cropCode = "sr"; + } + else { + uberArtworkPath = "editorialArtwork.centeredFullscreenBackground"; + cropCode = "ep"; + } + break; + case "tv": + if (options.presentedInTopShelf) { + uberArtworkPath = "editorialArtwork.topShelf"; + cropCode = "sr"; + } + else { + uberArtworkPath = "editorialArtwork.splashFullScreen"; + cropCode = "ta"; + fallbackUberArtworkPath = "editorialArtwork.fullscreenBackground"; + fallbackCropCode = "sr"; + } + break; + case "vision": + uberArtworkPath = "editorialArtwork.productUberStatic16x9"; + cropCode = "sr"; + break; + default: + if (options.supportsArcade) { + if (options.prefersCompactVariant || objectGraph.client.isPhone) { + uberArtworkPath = "editorialArtwork.splashTall"; + cropCode = "oc"; + } + else { + uberArtworkPath = "editorialArtwork.splashFullScreen"; + cropCode = "oh"; + } + } + else { + uberArtworkPath = "editorialArtwork.bannerUber"; + cropCode = "sr"; + } + break; + } + uberArtworkData = contentAttributes.contentAttributeAsDictionary(objectGraph, data, uberArtworkPath); + // If we don't have the desired artwork, we sometimes attempt to use other artwork as a fallback. + if (fallbackUberArtworkPath !== null && serverData.isNullOrEmpty(uberArtworkData)) { + uberArtworkData = contentAttributes.contentAttributeAsDictionary(objectGraph, data, fallbackUberArtworkPath); + // Use the fallback crop if it's available. + if (fallbackCropCode !== null) { + cropCode = fallbackCropCode; + } + } + if (serverData.isDefinedNonNull(uberArtworkData) && serverData.isDefinedNonNull(cropCode)) { + return artworkFromApiArtwork(objectGraph, uberArtworkData, { + cropCode, + useCase: 21 /* ArtworkUseCase.Uber */, + withJoeColorPlaceholder: true, + overrideHeight: null, + overrideWidth: null, + }); + } + return null; +} +/** + * Determines the logo artwork for the product, if there is any. + * @param {Data} The data for the product. + * @returns {models.Artwork} The artwork for the uber, or `null` if there is none. + * null. + */ +export function productLogoArtworkFromData(objectGraph, data) { + let artworkPath = null; + let cropCode = null; + switch (objectGraph.client.deviceType) { + case "tv": + artworkPath = "editorialArtwork.contentLogoTrimmed"; + cropCode = "bb"; + break; + default: + return null; + } + const artworkData = contentAttributes.contentAttributeAsDictionary(objectGraph, data, artworkPath); + if (serverData.isDefinedNonNull(artworkData) && serverData.isDefinedNonNull(cropCode)) { + return artworkFromApiArtwork(objectGraph, artworkData, { + cropCode, + useCase: 0 /* ArtworkUseCase.Default */, + withJoeColorPlaceholder: true, + }); + } + return null; +} +/** + * Determines the editorial video for the product, if there is any. + * @returns {models.Video} The editorial video for the product, or `null` if there is none. + * null. + * @param data + * @param useCase + * @param preferredFlavorsOverride + */ +export function productEditorialVideoFromData(objectGraph, data, useCase, preferredFlavorsOverride, videoPreviewOverride) { + let preferredFlavors = []; + if (serverData.isDefinedNonNullNonEmpty(preferredFlavorsOverride)) { + preferredFlavors = preferredFlavorsOverride; + } + else { + switch (objectGraph.client.deviceType) { + case "mac": + case "tv": + preferredFlavors = ["splashVideo16x9"]; + break; + case "pad": + preferredFlavors = ["splashVideo4x3"]; + break; + case "vision": + preferredFlavors = ["productUberMotion16x9"]; + break; + default: + preferredFlavors = ["splashVideo3x4"]; + } + } + let uberEditorialVideoData = null; + let videoPreviewData = null; + for (const videoFlavor of preferredFlavors) { + uberEditorialVideoData = contentAttributes.contentAttributeAsDictionary(objectGraph, data, [ + "editorialVideo", + videoFlavor, + ]); + videoPreviewData = contentAttributes.contentAttributeAsDictionary(objectGraph, data, [ + "editorialVideo", + videoFlavor, + "previewFrame", + ]); + if (serverData.isDefinedNonNullNonEmpty(uberEditorialVideoData)) { + break; + } + } + // Video Preview based on data, or externally provided override if any. + const videoPreview = videoPreviewOverride !== null && videoPreviewOverride !== void 0 ? videoPreviewOverride : artworkFromApiArtwork(objectGraph, videoPreviewData, { + useCase: useCase, + withJoeColorPlaceholder: true, + cropCode: "sr", + }); + if (serverData.isDefinedNonNull(uberEditorialVideoData)) { + const videoUrl = serverData.asString(uberEditorialVideoData, "video"); + if (serverData.isNull(videoUrl)) { + return null; + } + let playbackControls; + let autoplayPlaybackControls; + if (objectGraph.host.isiOS || objectGraph.host.isMac || objectGraph.host.isTV) { + playbackControls = videoDefaults.standardControls(objectGraph); + autoplayPlaybackControls = { + muteUnmute: true, + }; + } + else { + playbackControls = {}; + autoplayPlaybackControls = {}; + } + const configuration = { + allowsAutoPlay: true, + looping: true, + canPlayFullScreen: false, + playbackControls: playbackControls, + autoPlayPlaybackControls: autoplayPlaybackControls, + }; + return new models.Video(videoUrl, videoPreview, configuration); + } + return null; +} +/** + * Determines the video for the poster lockup, if there is any. + * @param {Data} The data for the lockup. + * @param {useCase} The use case for this artwork. + * @returns {models.Video} The video for the poster lockup, or `null` if there is none. + * null. + */ +export function posterEditorialVideoFromData(objectGraph, data, useCase) { + const editorialVideoData = contentAttributes.contentAttributeAsDictionary(objectGraph, data, [ + "editorialVideo", + "posterCardVideo16x9", + ]); + const videoPreviewData = contentAttributes.contentAttributeAsDictionary(objectGraph, data, [ + "editorialVideo", + "posterCardVideo16x9", + "previewFrame", + ]); + const cropCode = "sr"; + const videoPreview = artworkFromApiArtwork(objectGraph, videoPreviewData, { + useCase: useCase, + withJoeColorPlaceholder: true, + cropCode: cropCode, + }); + if (serverData.isDefinedNonNull(editorialVideoData)) { + const videoUrl = serverData.asString(editorialVideoData, "video"); + if (serverData.isNull(videoUrl)) { + return null; + } + const configuration = { + allowsAutoPlay: true, + looping: true, + canPlayFullScreen: false, + playbackControls: videoDefaults.noControls(objectGraph), + autoPlayPlaybackControls: videoDefaults.noControls(objectGraph), + }; + return new models.Video(videoUrl, videoPreview, configuration); + } + return null; +} +/** + * Determines the artwork for the poster lockup, if there is any. + * @param {Data} The data for the lockup. + * @returns {models.Artwork} The artwork for the poster lockup, or `null` if there is none. + * null. + */ +export function posterArtworkFromData(objectGraph, data) { + const artworkData = contentAttributes.contentAttributeAsDictionary(objectGraph, data, "editorialArtwork.postCard"); + const cropCode = "sr"; + if (serverData.isDefinedNonNull(artworkData)) { + return artworkFromApiArtwork(objectGraph, artworkData, { + cropCode, + useCase: 0 /* ArtworkUseCase.Default */, + withJoeColorPlaceholder: true, + }); + } + return null; +} +/** + * Determines the artwork for the epic heading on a poster lockup, if there is any. + * @param {Data} The data for the product. + * @returns {models.Artwork} The artwork for the epic heading, or `null` if there is none. + * null. + */ +export function posterEpicHeadingArtworkFromData(objectGraph, data) { + const artworkData = contentAttributes.contentAttributeAsDictionary(objectGraph, data, "editorialArtwork.epicHeading"); + const cropCode = "bb"; + if (serverData.isDefinedNonNull(artworkData) && serverData.isDefinedNonNull(cropCode)) { + const epicHeadingArtwork = artworkFromApiArtwork(objectGraph, artworkData, { + cropCode, + useCase: 0 /* ArtworkUseCase.Default */, + }); + if (objectGraph.client.isVision) { + epicHeadingArtwork.backgroundColor = color.named("clear"); + } + return epicHeadingArtwork; + } + return null; +} +/** + * Fetch the most-landscape media from data. Hoisted from Arcade See All. + * Used for: + * - Arcade See All Media Lockups + * - Continue Playing Lockups + */ +export function editorialSplashVideoFromData(objectGraph, data, videoPreviewOverride) { + let preferredEditorialVideoFlavors = null; + switch (objectGraph.client.deviceType) { + case "mac": + case "tv": + case "phone": + case "vision": + preferredEditorialVideoFlavors = ["splashVideo16x9", "splashVideo4x3", "splashVideo3x4"]; + break; + default: + preferredEditorialVideoFlavors = ["splashVideo4x3", "splashVideo16x9", "splashVideo3x4"]; + } + return productEditorialVideoFromData(objectGraph, data, 21 /* ArtworkUseCase.Uber */, preferredEditorialVideoFlavors, videoPreviewOverride); +} +/** + * Determines the URL to use for the developer page. + * @param {Data} developerData The data for the "developer" relationship. + * @returns {string} The string form of the URL for the developer page, or `null` if the developer data is undefined or + * null. + */ +export function developerUrlFromDeveloperData(objectGraph, developerData) { + if (!serverData.isDefinedNonNull(developerData)) { + return null; + } + if (objectGraph.client.isWeb) { + return mediaAttributes.attributeAsString(developerData, "url"); + } + return `${Protocol.internal}:/${Path.developer}/${Path.href}?${Parameters.href}=${developerData.href}`; +} +/** + * Determines the URL to use for the Charts page. + * @param {Data} data The data for the product. + * @returns {string} The string form of the URL for the charts page, or `null` if the data is undefined or + * null. + */ +export function chartUrlFromData(objectGraph, genre, chart) { + const request = new mediaDataFetching.Request(objectGraph) + .forType("charts") + .addingQuery("types", "apps") + .addingQuery("chart", chart) + .addingQuery("genre", genre) + .includingMacOSCompatibleIOSAppsWhenSupported(true); + return mediaUrlBuilder.buildURLFromRequest(objectGraph, request).toString(); +} +/** + * Returns the key into the chart-position badge data for the given client name. + * @param clientIdentifier Identifier of the current client. + * @returns {string} The relevant key in the chart-position badge JSON data. + */ +export function badgeChartKeyForClientIdentifier(objectGraph, clientIdentifier) { + switch (clientIdentifier) { + case client.appStoreIdentifier: + case client.productPageExtensionIdentifier: + return "appStore"; + case client.watchIdentifier: + return "watch"; + case client.messagesIdentifier: + return "messages"; + case client.tvIdentifier: + return "appletv"; + default: + return null; + } +} +/** + * Internal function returning the name and asset name representing the + * storefront content rating for the provided rank. + * @param objectGraph The App Store object graph. + * @param rank A content rating rank from CX. + * @returns A tuple containing the name and asset name representing the rank, + * or `undefined` if rank is unknown/invalid. + */ +function storefrontContentRatingInfoForRank(objectGraph, rank) { + switch (rank) { + // Brazil Self-Rated + case 6: + return ["L", "br.l"]; + case 7: + return ["10", "br.10"]; + case 8: + return ["12", "br.12"]; + case 9: + return ["14", "br.14"]; + case 10: + return ["16", "br.16"]; + case 11: + return ["18", "br.18"]; + // Brazil Official + case 12: + return ["AL", "br.l.official"]; + case 13: + return ["A10", "br.10.official"]; + case 14: + return ["A12", "br.12.official"]; + case 15: + return ["A14", "br.14.official"]; + case 16: + return ["A16", "br.16.official"]; + case 17: + return ["A18", "br.18.official"]; + // Korea + case 20: + return ["All", "kr.all"]; + case 21: + return ["12", "kr.12"]; + case 22: + return ["15", "kr.15"]; + // Australia + case 31: + return ["15+", "AgeRating-AU-15"]; + case 32: + return ["R 18+", "AgeRating-AU-18"]; + // France + case 47: + return ["18+", "AgeRating-FR-18"]; + default: + return undefined; + } +} +/// Returns a localized title for the given app platform. +export function appPlatformTitle(objectGraph, appPlatform) { + switch (appPlatform) { + case "phone": + return objectGraph.loc.string("AppPlatform.Phone"); + case "pad": + return objectGraph.loc.string("AppPlatform.Pad"); + case "vision": + return objectGraph.loc.string("AppPlatform.Vision"); + case "tv": + return objectGraph.loc.string("AppPlatform.TV"); + case "watch": + return objectGraph.loc.string("AppPlatform.Watch"); + case "messages": + return objectGraph.loc.string("AppPlatform.Messages"); + case "mac": + return objectGraph.loc.string("AppPlatform.Mac"); + default: + return ""; + } +} +/** + * Provides the name of the asset representing the storefront content rating + * for the provided `rank`. + * @param objectGraph The App Store object graph. + * @param rank A content rating rank from CX. + * @returns The asset name representing the `rank`, corresponding to a file on + * device, or `undefined` if rank is unknown/invalid. + */ +export function storefrontContentRatingResourceForRank(objectGraph, rank) { + var _a; + return (_a = storefrontContentRatingInfoForRank(objectGraph, rank)) === null || _a === void 0 ? void 0 : _a[1]; +} +/** + * Provides a textual representation of the storefront content rating for the + * provided `rank`, e.g. "18+". This should match the main text displayed in + * the content rating pictogram from `storefrontContentRatingResourceForRank`. + * @param objectGraph The App Store object graph. + * @param rank A content rating rank from CX. + * @returns The textual version of the storefront content rating representing + * the `rank`, or `undefined` if rank is unknown/invalid. + */ +export function storefrontContentRatingNameForRank(objectGraph, rank) { + var _a; + return (_a = storefrontContentRatingInfoForRank(objectGraph, rank)) === null || _a === void 0 ? void 0 : _a[0]; +} +export function promotionalTextFromData(objectGraph, data, productVariantData) { + return contentAttributes.customAttributeAsString(objectGraph, data, productVariantData, "promotionalText"); +} +export function hasMessagesExtensionFromData(objectGraph, data, attributePlatform) { + return contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "hasMessagesExtension", attributePlatform); +} +export function supportsFunCameraFromData(objectGraph, data, attributePlatform) { + return contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "supportsFunCamera", attributePlatform); +} +export function isHiddenFromSpringboardFromData(objectGraph, data) { + return contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "isHiddenFromSpringboard"); +} +function isAppleWatchSupportedFromData(objectGraph, data) { + return contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "isAppleWatchSupported"); +} +function messagesScreenshotsFromData(objectGraph, data, attributePlatform) { + return contentAttributes.contentAttributeAsDictionary(objectGraph, data, "messagesScreenshots", attributePlatform); +} +function screenshotsByTypeFromData(objectGraph, data, productVariantData, isAd, attributePlatform) { + const attributeKey = isAd ? "customScreenshotsByTypeForAd" : "screenshotsByType"; + return contentAttributes.customAttributeAsDictionary(objectGraph, data, productVariantData, attributeKey, attributePlatform); +} +function videoPreviewsByTypeFromData(objectGraph, data, productVariantData, isAd, attributePlatform) { + const attributeKey = isAd ? "customVideoPreviewsByTypeForAd" : "videoPreviewsByType"; + return contentAttributes.customAttributeAsDictionary(objectGraph, data, productVariantData, attributeKey, attributePlatform); +} +/** + * Whether Arcade is supported, based on the provided data. + * @param objectGraph The App Store object graph. + * @param data The data blob to check for Arcade support. + * @param attributePlatformOverride An override platform, from which to fetch the attribute. + * @returns A boolean indicating if Arcade is supported. + */ +export function isArcadeSupported(objectGraph, data, attributePlatformOverride = undefined) { + return contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "supportsArcade", attributePlatformOverride); +} +/** + * Try to get notes for some piece of content, giving preference to the enrichedEditorialnotes, falling back to editorialNotes, + * then finally to itunesNotes. For some data the notes are stored in the attributes not the platformAttributes. + * @param {Data} data + * @param {string} key + * @param {boolean} enableEditorialCardOverrides This means we will also check for editorial-cards as well before chcking the default notes locations + * * @param attributePlatformOverride An override platform, from which to fetch the attribute. + * @returns {string} + */ +export function notesFromData(objectGraph, data, key, enableEditorialCardOverrides = false, attributePlatformOverride = undefined) { + var _a, _b; + if (isNothing(data)) { + return null; + } + let note; + if (enableEditorialCardOverrides) { + const editorialCard = editorialCardFromData(data); + if (mediaAttributes.hasAttributes(editorialCard)) { + note = contentAttributes.contentAttributeAsString(objectGraph, editorialCard, ["editorialNotes", key], attributePlatformOverride); + } + } + note = + (_b = (_a = note !== null && note !== void 0 ? note : contentAttributes.contentAttributeAsString(objectGraph, data, ["enrichedEditorialNotes", key], attributePlatformOverride)) !== null && _a !== void 0 ? _a : contentAttributes.contentAttributeAsString(objectGraph, data, ["editorialNotes", key], attributePlatformOverride)) !== null && _b !== void 0 ? _b : contentAttributes.contentAttributeAsString(objectGraph, data, ["itunesNotes", key], attributePlatformOverride); + return note; +} +/** + * Try and get notes for some piece of content editorialNotes + * @param {Data} data + * @param {string} key + * @param {boolean} enableEditorialCardOverrides This means we will also check for editorial-cards as well before chcking the default notes locations + * @returns {string} + */ +export function editorialNotesFromData(objectGraph, data, key, enableEditorialCardOverrides = false) { + var _a; + let note; + if (enableEditorialCardOverrides) { + const editorialCard = editorialCardFromData(data); + if (mediaAttributes.hasAttributes(editorialCard)) { + note = contentAttributes.contentAttributeAsString(objectGraph, editorialCard, ["editorialNotes", key]); + } + } + note = + (_a = note !== null && note !== void 0 ? note : contentAttributes.contentAttributeAsString(objectGraph, data, ["enrichedEditorialNotes", key])) !== null && _a !== void 0 ? _a : contentAttributes.contentAttributeAsString(objectGraph, data, ["editorialNotes", key]); + return note; +} +/** + * Determines whether the provided data is for a macOS installer. + * @param data The data against which to check for a macOS installer. + */ +export function isMacOSInstaller(objectGraph, data) { + return derivedData.value(data, "isMacOSInstaller", () => { + const isMac = objectGraph.client.isMac; + if (!isMac) { + return false; + } + const bundleId = contentAttributes.contentAttributeAsString(objectGraph, data, "bundleId"); + if (!serverData.isDefinedNonNull(bundleId)) { + return false; + } + return bundleId.startsWith("com.apple.InstallAssistant"); + }); +} +/** + * Check whether an app is unsupported by the current companion configuration. + * @param data The data representing an app listing. + */ +export function isUnsupportedByCurrentCompanion(objectGraph, data) { + const deletableApps = sad.systemApps(objectGraph); + if (objectGraph.host.isWatch) { + // AppConduit will handle determining if SAD apps are supported + if (deletableApps.isUnsupportedDeletableSystemAppFromData(data)) { + return true; + } + else if (objectGraph.client.isTinkerWatch) { + return (!contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "isStandaloneWithCompanionForWatchOS") && !contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "isStandaloneForWatchOS")); + } + return false; + } + else { + if (isUnsupportedDeletableSystemAppFromData(objectGraph, data, objectGraph.client.isTinkerWatch)) { + return true; + } + else if (objectGraph.client.isTinkerWatch) { + if (deletableApps.isSystemAppFromData(data)) { + // We don't consider whether an app is marked as standalone with companion + // when running in standalone mode. We always want SAD apps to be installable. + return (!contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "isDeliveredInIOSAppForWatchOS") && + !contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "isStandaloneForWatchOS")); + } + else { + return (!contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "isStandaloneWithCompanionForWatchOS") && + !contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "isStandaloneForWatchOS")); + } + } + else { + // We only allow standalone system apps to be installed when the watch + // is not running in standalone mode. This simplifies things for other teams. + return (objectGraph.client.isWatch && + deletableApps.isSystemAppFromData(data) && + !contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "isStandaloneForWatchOS")); + } + } +} +/** + * @deprecated Use sad.isUnsupportedDeletableSystemAppFromData instead. + * Check whether a SAD app is unsupported by the current companion configuration. + * + * @param data The data representing an app listing. + * @param isTinkerWatch Whether the current device is a tinker watch + */ +export function isUnsupportedDeletableSystemAppFromData(objectGraph, data, isTinkerWatch) { + if (isTinkerWatch && sad.systemApps(objectGraph).isSystemAppFromData(data)) { + const watchBundleId = mediaAttributes.attributeAsString(data, "watchBundleId"); + if (serverData.isDefinedNonNullNonEmpty(watchBundleId)) { + switch (watchBundleId) { + // rdar://63111354 (On Tinker device, able to attempt to download non-Tinker 1st and 3rd party app) + // These apps should prevented from installing on a Tinker device + case "com.apple.mobilemail.watchkitapp": + case "com.apple.news.watchkitapp": + case "com.apple.iBooks.watchkitapp": + return true; + default: + return false; + } + } + } + return false; +} +/** + * Device Sizes + * + * Please do not use these constants for anything but screenshots. + * Our code should not depend on absolute screen sizes for anything + * not related to selecting the correct screenshots to display. -km + */ +/// The screen size of iPhone 6.5" devices. +export const screenSizeIphone65 = new modelsBase.Size(414.0, 896.0); +/// The screen size of iPhone 5.8" devices. +export const screenSizeIPhone58 = new modelsBase.Size(375.0, 812.0); +/// The screen size of iPhone 6+ like devices. +export const screenSizeIPhone6Plus = new modelsBase.Size(414.0, 736.0); +/// The screen size of iPhone 6 like devices. +export const screenSizeIPhone6 = new modelsBase.Size(375.0, 667.0); +/// The screen size of iPhone 5 like devices. +export const screenSizeIPhone5 = new modelsBase.Size(320.0, 568.0); +/// The screen size of original iPhone like devices. +export const screenSizeIPhoneOriginal = new modelsBase.Size(320.0, 480.0); +/// The screen size of iPad and iPad mini devices. +export const screenSizeIPad = new modelsBase.Size(768.0, 1024.0); +/// The screen size of 7th and 8th gen 10.2" iPads. +export const screenSizeIPad102 = new modelsBase.Size(810.0, 1080.0); +/// The screen size of iPad pro 10.5" devices. +export const screenSizeIPad105 = new modelsBase.Size(834.0, 1112.0); +/// The screen size of iPad pro 11" devices. +export const screenSizeIPad11 = new modelsBase.Size(834.0, 1194.0); +/// The screen size of iPad Pro 11" devices in landscape orientation. +/// See `screenSizeIPadJ310Landscape` for info about the landscape orientation. +export const screenSizeIPad11Landscape = new modelsBase.Size(1194.0, 834.0); +/// The screen size of iPad pro 12.9" devices. +export const screenSizeIPadPro = new modelsBase.Size(1024.0, 1366.0); +/// The screen size of iPad pro 12.9" devices, with rounded corners. +export const screenSizeIPadPro2018 = new modelsBase.Size(1024.0, 1366.0); +/// The screen size of iPad pro 12.9" devices, with rounded corners, in landscape orientation. +/// See `screenSizeIPadJ310Landscape` for info about the landscape orientation. +export const screenSizeIPadPro2018Landscape = new modelsBase.Size(1366.0, 1024.0); +// The screen size of the J310 iPad device. +export const screenSizeIPadJ310 = new modelsBase.Size(744.0, 1133.0); +// The screen size of the J310 iPad device, in landscape orientation. +// rdar: //83176176 (J310: kMGQMainScreenCanvasSizes reports width as largest dimension, contrary to all other iPads and UIKit) +export const screenSizeIPadJ310Landscape = new modelsBase.Size(1133.0, 744.0); +/// The screen size of J720/J721 devices. +export const screenSizeJ720 = new modelsBase.Size(1032.0, 1376.0); +/// The screen size of J720/J721, in landscape orientation. +/// See `screenSizeIPadJ310Landscape` for info about the landscape orientation. +export const screenSizeJ720Landscape = new modelsBase.Size(1376.0, 1032.0); +/// The screen size of J717/J718 devices. +export const screenSizeJ717 = new modelsBase.Size(834.0, 1210.0); +/// The screen size of J717/J718, in landscape orientation. +/// See `screenSizeIPadJ310Landscape` for info about the landscape orientation. +export const screenSizeJ717Landscape = new modelsBase.Size(1210.0, 834.0); +/// The screen size of the 42mm Apple Watch devices. +export const screenSizeWatch = new modelsBase.Size(312.0, 390.0); +/// The screen size of large 2018 Apple Watch devices. +export const screenSizeWatch2018 = new modelsBase.Size(368.0, 448.0); +// The screen size of the large 2021 Apple Watch devices. +export const screenSizeWatch2021 = new modelsBase.Size(396.0, 484.0); +// The screen size for the 2022 Apple Watch devices. +export const screenSizeWatch2022 = new modelsBase.Size(410.0, 502.0); +// The screen size for the 2024 Apple Watch devices. +export const screenSizeWatch2024 = new modelsBase.Size(416.0, 496.0); +// The screen size for the Apple Watch Ultra / Ultra 2 devices. +export const screenSizeWatchUltra = new modelsBase.Size(410.0, 502.0); +/// The screen size for iPad device. +export const screenSizeIPadAir2020 = new modelsBase.Size(820.0, 1180.0); +/// The screen size for iPhone devices. +export const screenSizeIPhone131 = new modelsBase.Size(360.0, 780.0); +export const screenSizeIPhone132 = new modelsBase.Size(390.0, 844.0); +export const screenSizeIPhone134 = new modelsBase.Size(428.0, 926.0); +// The screen size for a 6.1" D73-style device. +export const screenSizeIPhone61 = new modelsBase.Size(393.0, 852.0); +// The screen size for a 6.7" D74-style device. +export const screenSizeIPhone67 = new modelsBase.Size(430.0, 932.0); +/// The screen size for a D93 device. +export const screenSizeD93 = new modelsBase.Size(402.0, 874.0); +/// The screen size for a D94 device. +export const screenSizeD94 = new modelsBase.Size(440.0, 956.0); +/// The screen size for a D23 device. +export const screenSizeD23 = new modelsBase.Size(420.0, 912.0); +/// The screen size for a N230 device. +export const screenSizeN230 = new modelsBase.Size(422.0, 514.0); +/// All phone types, in order of decreasing size. +const decreasingPhoneTypes = [ + "iphone_d74", + "iphone_6_5", + "iphone_d73", + "iphone_5_8", + "iphone6+", + "iphone6", + "iphone5", + "iphone", +]; +// region Device Corner Radius +/** + * The reason we need to hardcode these is because we may want to display screenshots with rounding for a device that is + * not the one the user is currently browsing the store with. For example, imagine that the user is browsing the store + * with an iPhone 8 but ends up looking at an app that only has D22 screenshots. They should see the D22 screenshots + * according to the D22 corner rounding, and using the current client's `screenCornerRadius` would not give us the + * proper value. + */ +/// The device corner radius of iPad pro 12.9" devices from 2018. +const deviceCornerRadiusIpadPro2018 = 18.0; +/// The device corner radius of iPad pro 11" devices. +const deviceCornerRadiusIpad11 = 18.0; +/// The device corner radius of iPhone 6.5" devices. +const deviceCornerRadiusIphone65 = 41.5; +/// The device corner radius of iPhone 5.8" devices. +const deviceCornerRadiusIphone58 = 39.0; +/// The device corner radius of iPhone 6.1" devices. +const deviceCornerRadiusIphone61 = 55.0; +/// The device corner radius of iPhone 6.7" devices. +const deviceCornerRadiusIphone67 = 55.0; +/// The device corner radius of large 2018 Apple Watch devices. +const deviceCornerRadiusWatch2018 = 34.0; +/// The outer device corner radius of large Apple Watch devices. +const outerDeviceCornerRadiusWatch = 30.0; +/// The device border thickness for Apple Watch. +const deviceBorderThicknessWatch = 13.0; +/// The device border thickness for 2018 Apple Watch. +const deviceBorderThicknessWatch2018 = 11.0; +/// The device corner radius for 2021 Apple Watch. +const deviceCornerRadiusWatch2021 = 55; +/// The device border thickness for 2021 Apple Watch. +const deviceBorderThicknessWatch2021 = 5.5; +/// The device corner radius for 2022/2024 Apple Watch. +const deviceCornerRadiusWatch2022 = 108; +/// The outer device corner radius for 2022/2024 Apple Watch. +const deviceOuterCornerRadiusWatch2022 = 112.5; +/// The device border thickness for 2022/2024 Apple Watch. +const deviceBorderThicknessWatch2022 = 4.5; +export function currentAppPlatform(objectGraph) { + var _a; + switch (objectGraph.client.deviceType) { + case "web": + return unwrap((_a = objectGraph.activeIntent) === null || _a === void 0 ? void 0 : _a.appPlatform); + default: + return objectGraph.client.deviceType; + } +} +/// The SF Symbol name that represents the media type. +export function systemImageNameForAppPlatform(appPlatform) { + switch (appPlatform) { + case "phone": + return "iphone"; + case "pad": + return "ipad"; + case "tv": + return "tv"; + case "watch": + return "applewatch"; + case "mac": + return "macbook"; + case "messages": + return "message"; + case "vision": + return "visionpro"; + default: + unreachable(appPlatform); + } +} +/** + * Returns the factor by which to multiply an artwork's portrait-equivalent width, in order to compute the artwork's + * device-rounded corner radius. This is useful because we want to display screenshots for device-rounded screenshots + * with a corner radius that is scaled to that of the device screen. + * + * r = w * r' + * r' = R / W + * + * Where: + * r: scaled radius + * r': The return value of this function. + * R: device corner radius + * w: width at which the screenshot will be displayed + * W: device width + * + * We need to have this value here in the JS because we may need to display device-rounded screenshots on a device that + * does not have a device corner radius; there is no native API for querying the corner radius of various devices and, + * even if there were, we want to avoid specific screen-size checks in the native code. + * @param type The screenshot type vended by the server. + * @returns {number} The device corner radius factor, or null if the device does not have a corner radius. + */ +function deviceCornerRadiusFactorForMediaType(objectGraph, type) { + // Let's only bridge over and access client's properties if we need to. + switch (type) { + case "ipadPro_2018": + return deviceCornerRadiusIpadPro2018 / screenSizeIPadPro2018.width; + case "ipad_11": + return deviceCornerRadiusIpad11 / screenSizeIPad11.width; + case "iphone_6_5": + return deviceCornerRadiusIphone65 / screenSizeIphone65.width; + case "iphone_5_8": + return deviceCornerRadiusIphone58 / screenSizeIPhone58.width; + case "iphone_d73": + return deviceCornerRadiusIphone61 / screenSizeIPhone61.width; + case "iphone_d74": + return deviceCornerRadiusIphone67 / screenSizeIPhone67.width; + case "appleWatch_2018": + return deviceCornerRadiusWatch2018 / screenSizeWatch2018.width; + case "appleWatch_2021": + return deviceCornerRadiusWatch2021 / screenSizeWatch2021.width; + case "appleWatch_2022": + return deviceCornerRadiusWatch2022 / screenSizeWatch2022.width; + case "appleWatch_2024": + return deviceCornerRadiusWatch2022 / screenSizeWatch2024.width; + default: + return null; + } +} +function deviceOuterCornerRadiusFactorForMediaType(objectGraph, type) { + switch (type) { + case "appleWatch": + return outerDeviceCornerRadiusWatch / screenSizeWatch.width; + case "appleWatch_2022": + return deviceOuterCornerRadiusWatch2022 / screenSizeWatch2022.width; + case "appleWatch_2024": + return deviceOuterCornerRadiusWatch2022 / screenSizeWatch2024.width; + default: + return deviceCornerRadiusFactorForMediaType(objectGraph, type); + } +} +function deviceBorderThicknessForMediaType(objectGraph, type) { + switch (type) { + case "appleWatch": + return deviceBorderThicknessWatch / screenSizeWatch.width; + case "appleWatch_2018": + return deviceBorderThicknessWatch2018 / screenSizeWatch2018.width; + case "appleWatch_2021": + return deviceBorderThicknessWatch2021 / screenSizeWatch2021.width; + case "appleWatch_2022": + return deviceBorderThicknessWatch2022 / screenSizeWatch2022.width; + case "appleWatch_2024": + return deviceBorderThicknessWatch2022 / screenSizeWatch2024.width; + default: + return null; + } +} +// endregion +/** + Returns a boolean indicating if the client's operating + system is the same or later than the specified version. + + @param version The full version number to check against + @returns true if the operating system is the same or newer than the specified version; false otherwise. + */ +export function isOSAtLeastVersion(objectGraph, version) { + if (serverData.isNull(version) || version.length === 0) { + return true; + } + const versionComponents = version.split("."); + const majorVersion = serverData.asNumber(versionComponents[0]) || 0; + const minorVersion = serverData.asNumber(versionComponents[1]) || 0; + const patchVersion = serverData.asNumber(versionComponents[2]) || 0; + return objectGraph.host.isOSAtLeast(majorVersion, minorVersion, patchVersion); +} +/** + Returns a boolean indicating if the system version of the active, paired watch (if any) is + at least the provided version number. + + @param version The full version number to check against + @returns true if an active, paired watch exists, and its operating system version is the same or newer than the specified version; false otherwise. + */ +export function isActivePairedWatchOSAtLeastVersion(objectGraph, version) { + if (serverData.isNull(version) || version.length === 0) { + return true; + } + const versionComponents = version.split("."); + const majorVersion = serverData.asNumber(versionComponents[0]) || 0; + const minorVersion = serverData.asNumber(versionComponents[1]) || 0; + const patchVersion = serverData.asNumber(versionComponents[2]) || 0; + return objectGraph.client.isActivePairedWatchSystemVersionAtLeastMajorVersionMinorVersionPatchVersion(majorVersion, minorVersion, patchVersion); +} +/** + * Check whether the active paired device's OS is the same or greater than a given version. + */ +export function isActivePairedDeviceAtLeastVersion(objectGraph, version) { + if (serverData.isNull(version) || version.length === 0) { + return true; + } + return objectGraph.client.isPairedSystemVersionAtLeast(version); +} +/** + * Check whether the active paired device's OS is below a given version. + */ +export function isActivePairedWatchOSBelowVersion(objectGraph, version) { + if (serverData.isNull(version) || version.length === 0) { + return false; + } + return objectGraph.client.isActivePairedWatchSystemVersionBelow(version); +} +export function shelfContentTypeCanDisplayArcadeOfferButtons(objectGraph, shelfStyle) { + if (objectGraph.client.isTV) { + switch (shelfStyle) { + case "upsellBreakout": + return true; + default: + return false; + } + } + else { + switch (shelfStyle) { + case "smallLockup": + case "mediumLockup": + case "appTrailerLockup": + case "screenshotsLockup": + case "mixedMediaLockup": + case "upsellBreakout": + case "arcadeShowcase": + return true; + default: + return false; + } + } +} +export function shelfDisplayStyleCanDisplayArcadeOfferButtons(objectGraph, displayStyle) { + switch (displayStyle) { + case "LockupSmall": + case "LockupLarge": + case "BreakoutLarge": + case "Hero": + case "EditorialLockupLarge": + case "EditorialLockupLargeVariant": + case "EditorialLockupMedium": + case "EditorialLockupMediumVariant": + case "StoryMedium": + return true; + default: + return false; + } +} +/** + * The dynamic date string used by apps coming soon. + */ +export function dynamicPreorderDateFromData(objectGraph, data, fallbackLabel) { + const preorderOffer = offers.offerDataFromData(objectGraph, data); + const isPreorder = serverData.asString(preorderOffer, "type") === "preorder"; + if (isPreorder) { + const releaseDateRaw = serverData.asString(preorderOffer, "expectedReleaseDate"); + const dateDisplayFormat = contentAttributes.contentAttributeAsString(objectGraph, data, "expectedReleaseDateDisplayFormat"); + if (serverData.isDefinedNonNullNonEmpty(dateDisplayFormat)) { + if (serverData.isDefinedNonNullNonEmpty(releaseDateRaw)) { + const releaseDate = dateUtil.parseDateOmittingTimeFromString(releaseDateRaw); + const tokenFormatMap = { + "@@expectedDateMY@@": objectGraph.loc.string("PreOrder.Date.MonthYear"), + "@@expectedDateMDY@@": objectGraph.loc.string("PreOrder.Date.MonthDayYear"), + }; + for (const [serverToken, dateFormat] of Object.entries(tokenFormatMap)) { + if (dateDisplayFormat.includes(serverToken)) { + let formattedDate = objectGraph.loc.formatDateWithContext(dateFormat, releaseDate, "middleOfSentence"); + if (objectGraph.client.isTV) { + formattedDate = formattedDate.replace(/ /g, "\u00a0"); + } + return dateDisplayFormat.replace(serverToken, formattedDate); + } + } + } + return dateDisplayFormat; + } + } + // There was no dynamic date to display + return fallbackLabel; +} +/** + * The primary content for an editorial item. + * @param data The data from which to derive the primary content. + */ +export function primaryContentForData(objectGraph, data) { + const primaryContent = mediaRelationship.relationshipData(objectGraph, data, "primary-content"); + if (serverData.isDefinedNonNullNonEmpty(primaryContent)) { + return primaryContent; + } + // If an EI has canvasData, then in MAPI response its "primary-content" relationship will not include the + // primary content meta data. Instead, the primary content data will be included in the "card-contents" relationship. + if (contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "isCanvasAvailable")) { + return mediaRelationship.relationshipData(objectGraph, data, "card-contents"); + } + return null; +} +const grayColorHex = "#9BA9BD"; +export const grayColorCriteria = { + colorHex: grayColorHex, + maxSaturation: 4, + maxBrightness: 9, +}; +/** + * The list of tag background colors we can use to match an app icon background color against, based on saturation and brightness + */ +const saturationBrightnessBasedTagColorBuckets = [ + grayColorCriteria, // Gray +]; +/** + * The list of tag background colors we can use to match an app icon background color against + */ +const hueBasedTagColorBuckets = [ + { colorHex: "#F7816F", minHue: 0, maxHue: 16 }, + { colorHex: "#FF9034", minHue: 17, maxHue: 33 }, + { colorHex: "#E3B059", minHue: 34, maxHue: 59 }, + { colorHex: "#74BD66", minHue: 60, maxHue: 129 }, + { colorHex: "#72C792", minHue: 130, maxHue: 169 }, + { colorHex: "#61BFE2", minHue: 170, maxHue: 209 }, + { colorHex: "#6EA3E9", minHue: 210, maxHue: 239 }, + { colorHex: "#7D69FA", minHue: 240, maxHue: 259 }, + { colorHex: "#B363F7", minHue: 260, maxHue: 289 }, + { colorHex: "#EE7CBD", minHue: 290, maxHue: 360 }, // Pink +]; +/** + * Find the matching tag color for an icon's background color + * + * @param iconBackgroundColor The bgColor from the icon artwork data + * @returns The closest matching color from the HI provided set of tag background colors for this icon color + */ +export function closestTagBackgroundColorForIcon(iconBackgroundColor) { + var _a; + return ((_a = color.findColorBucketForColor(iconBackgroundColor, saturationBrightnessBasedTagColorBuckets, hueBasedTagColorBuckets)) !== null && _a !== void 0 ? _a : color.fromHex(grayColorHex)); +} +/** + * Parses a JoeColorSet out of a MAPI EditorialArtwork JSON object + * @param data An EditorialArtwork JSON object from MAPI + * @returns A JoeColorSet from parsing the input RGB values + */ +export function joeColorHexSetFromData(data) { + var _a, _b, _c, _d, _e; + const textGradient = []; + for (const gradientColorHex of serverData.asArrayOrEmpty(data, "textGradient")) { + if (isSome(gradientColorHex) && serverData.isString(gradientColorHex)) { + textGradient.push(gradientColorHex); + } + } + return { + bgColor: (_a = serverData.asString(data, "bgColor")) !== null && _a !== void 0 ? _a : undefined, + textColor1: (_b = serverData.asString(data, "textColor1")) !== null && _b !== void 0 ? _b : undefined, + textColor2: (_c = serverData.asString(data, "textColor2")) !== null && _c !== void 0 ? _c : undefined, + textColor3: (_d = serverData.asString(data, "textColor3")) !== null && _d !== void 0 ? _d : undefined, + textColor4: (_e = serverData.asString(data, "textColor4")) !== null && _e !== void 0 ? _e : undefined, + textGradient: isDefinedNonNullNonEmpty(textGradient) ? textGradient : undefined, + }; +} +/** + * Parses a JoeColorSet out of a MAPI EditorialArtwork JSON object + * @param data An EditorialArtwork JSON object from MAPI + * @returns A JoeColorSet from parsing the input RGB values + */ +export function joeColorSetFromData(data) { + var _a, _b, _c, _d, _e, _f; + const joeColorHexSet = joeColorHexSetFromData(data); + const textGradient = []; + for (const gradientColorHex of (_a = joeColorHexSet.textGradient) !== null && _a !== void 0 ? _a : []) { + const gradientColor = color.fromHex(gradientColorHex); + if (isSome(gradientColor)) { + textGradient.push(gradientColor); + } + } + return { + bgColor: (_b = color.fromHex(joeColorHexSet.bgColor)) !== null && _b !== void 0 ? _b : undefined, + textColor1: (_c = color.fromHex(joeColorHexSet.textColor1)) !== null && _c !== void 0 ? _c : undefined, + textColor2: (_d = color.fromHex(joeColorHexSet.textColor2)) !== null && _d !== void 0 ? _d : undefined, + textColor3: (_e = color.fromHex(joeColorHexSet.textColor3)) !== null && _e !== void 0 ? _e : undefined, + textColor4: (_f = color.fromHex(joeColorHexSet.textColor4)) !== null && _f !== void 0 ? _f : undefined, + textGradient: isDefinedNonNullNonEmpty(textGradient) ? textGradient : undefined, + }; +} +/** + * Attempt to find the first non-gray placeholder color + * @param joeColorSet The joe color set for a given icon + * @returns The color hext value to use for the joe color placeholder + */ +export function bestJoeColorPlaceholderSelectionLogic(joeColorSet) { + const joeColorKeys = [ + "bgColor", + "textColor1", + "textColor2", + "textColor3", + "textColor4", + ]; + for (const joeColorKey of joeColorKeys) { + const joeColorHex = joeColorSet[joeColorKey]; + if (!serverData.isString(joeColorHex)) { + continue; + } + const isGrayColor = color.doesColorMeetCriteria(color.fromHex(joeColorHex), grayColorCriteria); + if (!isGrayColor) { + return joeColorHex; + } + } + return null; +} +//# sourceMappingURL=content.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/content/device-family.js b/node_modules/@jet-app/app-store/tmp/src/common/content/device-family.js new file mode 100644 index 0000000..8c8c925 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/content/device-family.js @@ -0,0 +1,91 @@ +import { isSome } from "@jet/environment/types/optional"; +import * as derivedData from "../../foundation/json-parsing/derived-data"; +import * as serverData from "../../foundation/json-parsing/server-data"; +import * as mediaAttributes from "../../foundation/media/attributes"; +import * as mediaRelationship from "../../foundation/media/relationships"; +export function dataHasDeviceFamily(objectGraph, data, deviceFamily, recurse = false) { + return derivedData.value(data, `dataHasDeviceFamily.${deviceFamily}`, () => { + if (!data || !deviceFamily) { + return false; + } + const deviceFamilies = deviceFamiliesFromData(objectGraph, data, recurse); + return deviceFamilies.indexOf(deviceFamily) !== -1; + }); +} +export function dataOnlyHasDeviceFamily(objectGraph, data, deviceFamily, recurse = false) { + var _a; + return (((_a = dataHasDeviceFamily(objectGraph, data, deviceFamily, recurse)) !== null && _a !== void 0 ? _a : false) && + deviceFamiliesFromData(objectGraph, data, recurse).length === 1); +} +export function dataHasAnyDeviceFamilies(objectGraph, data, deviceFamilies, recurse = false) { + const dataDeviceFamilies = new Set(deviceFamiliesFromData(objectGraph, data, recurse)); + for (const deviceFamily of deviceFamilies) { + if (dataDeviceFamilies.has(deviceFamily)) { + return true; + } + } + return false; +} +export function dataOnlyHasDeviceFamilies(objectGraph, data, deviceFamilies, recurse = false) { + const dataDeviceFamilies = new Set(deviceFamiliesFromData(objectGraph, data, recurse)); + // Ensure length matches + if (deviceFamilies.length !== dataDeviceFamilies.size) { + return false; + } + // Ensure content matches + return deviceFamilies.every((deviceFamily) => dataDeviceFamilies.has(deviceFamily)); +} +export function deviceFamiliesFromData(objectGraph, data, recurse = false) { + const deviceFamilies = mediaAttributes.attributeAsArrayOrEmpty(data, "deviceFamilies"); + // If there isn't a device families attribute... + if (deviceFamilies.length === 0 && recurse) { + // Use its related apps (if it has them) + const apps = mediaRelationship.relationshipCollection(data, "apps"); + if (isSome(apps)) { + for (const app of apps) { + // Get the device families from the first app that has one (if any) + const appDeviceFamilies = deviceFamiliesFromData(objectGraph, app, recurse); + if (serverData.isDefinedNonNullNonEmpty(appDeviceFamilies)) { + return appDeviceFamilies; + } + } + } + } + return deviceFamilies; +} +/** + * Returns the DeviceFamily for a given device type and model + * @param {base.DeviceType} deviceType The device type to check + * @param {objectGraph.host.deviceModel} deviceModel The device model to check, currently used to identify iPods + * @returns {DeviceFamily} DeviceFamily associated with the input parameters + */ +export function deviceFamilyFromDeviceType(objectGraph, deviceType, deviceModel) { + let deviceFamily = null; + switch (deviceType) { + case "phone": + deviceFamily = "iphone"; + break; + case "pad": + deviceFamily = "ipad"; + break; + case "mac": + deviceFamily = "mac"; + break; + case "tv": + deviceFamily = "tvos"; + break; + case "watch": + deviceFamily = "watch"; + break; + case "vision": + deviceFamily = "realityDevice"; + break; + default: + break; + } + if (serverData.isDefinedNonNull(deviceModel) && deviceModel === "ipod") { + deviceFamily = "ipod"; + } + return deviceFamily; +} +//# sourceMappingURL=device-family.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/content/flow-preview.js b/node_modules/@jet-app/app-store/tmp/src/common/content/flow-preview.js new file mode 100644 index 0000000..1cc3e7d --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/content/flow-preview.js @@ -0,0 +1,485 @@ +import * as validation from "@jet/environment/json/validation"; +import * as models from "../../api/models"; +import * as base from "../../api/models/base"; +import * as serverData from "../../foundation/json-parsing/server-data"; +import * as mediaAttributes from "../../foundation/media/attributes"; +import * as mediaRelationship from "../../foundation/media/relationships"; +import * as http from "../../foundation/network/http"; +import * as urls from "../../foundation/network/urls"; +import * as externalDeepLink from "../linking/external-deep-link"; +import * as lockups from "../lockups/lockups"; +import * as metricsUtil from "../metrics/helpers/util"; +import * as offers from "../offers/offers"; +import * as reviews from "../product-page/reviews"; +import * as sharing from "../sharing"; +import * as contentArtwork from "./artwork/artwork"; +import * as content from "./content"; +import * as contentDeviceFamily from "./device-family"; +import { isSome } from "@jet/environment"; +import { Request } from "../../foundation/media/data-fetching"; +import { buildURLFromRequest } from "../../foundation/media/url-builder"; +/** + * Create and return the flow preview actions configuration for a product. + * @param objectGraph + * @param data The response data to read from. + * @param includeOfferAction Whether to include an offer action, where possible + * @param clientIdentifierOverride The suggested client identifier for the given product + * @param clickAction The flow action for viewing the product + * @param metricsOptions The metrics options to use for reporting metrics actions. + * @param metricsClickOptions TThe metrics click options for the product + * @param externalDeepLinkUrl + * @returns A configuration object for flow preview actions that can be taken on the product + */ +export function flowPreviewActionsConfigurationForProductFromData(objectGraph, data, includeOfferAction, clientIdentifierOverride, clickAction, metricsOptions, metricsClickOptions, externalDeepLinkUrl, lockupSubtitle, lockupTitle) { + return validation.context("flowPreviewActionsConfigurationForProductFromData", () => { + // Flow preview is only supported on iOS + if (objectGraph.client.deviceType !== "phone" && objectGraph.client.deviceType !== "pad") { + return null; + } + const productData = productDataFromData(objectGraph, data); + if (!serverData.isDefinedNonNullNonEmpty(productData)) { + return null; + } + const actions = []; + // Offer + let offerActionIndex = null; + let offerDisplayProperties = null; + const isPreorder = mediaAttributes.attributeAsBooleanOrFalse(productData, "isPreorder"); + if (includeOfferAction) { + const isArcade = content.isArcadeSupported(objectGraph, productData); + const offerType = lockups.offerTypeForMediaType(objectGraph, productData.type, isArcade); + const offerAction = offerActionFromData(objectGraph, productData, isPreorder, isArcade, offerType, clientIdentifierOverride, metricsClickOptions); + offerDisplayProperties = offers.displayPropertiesFromOfferAction(objectGraph, offerAction, offerType, productData, isPreorder, false, null, null, null, null, "flowPreview"); + const wrappedOfferAction = wrappedOfferActionFromData(objectGraph, productData, offerAction, isPreorder, clientIdentifierOverride, metricsClickOptions, metricsOptions, externalDeepLinkUrl); + if (serverData.isDefinedNonNull(wrappedOfferAction) && + serverData.isDefinedNonNull(offerDisplayProperties)) { + offerActionIndex = actions.length; + wrappedOfferAction.artwork = contentArtwork.createArtworkForResource(objectGraph, "systemimage://arrow.down.circle"); + actions.push(wrappedOfferAction); + } + } + // Share + const shareAction = shareActionFromData(objectGraph, productData, metricsOptions); + if (serverData.isDefinedNonNull(shareAction)) { + actions.push(shareAction); + } + // Reviews + const shouldSuppressReviews = reviews.shouldSuppressReviews(objectGraph, productData); + const shouldShowRatingsAndReviews = !isPreorder && !shouldSuppressReviews; + if (shouldShowRatingsAndReviews) { + // See ratings & reviews + if (serverData.isDefinedNonNull(clickAction) && + clickAction instanceof models.FlowAction && + (clickAction.pageData instanceof models.ProductPage || + clickAction.pageData instanceof models.ShelfBasedProductPage)) { + const seeRatingsAndReviewsAction = seeRatingsAndReviewsActionFromData(objectGraph, productData, clickAction); + if (serverData.isDefinedNonNull(seeRatingsAndReviewsAction)) { + actions.push(seeRatingsAndReviewsAction); + } + } + // Write a review + const isTvOnlyApp = contentDeviceFamily.dataOnlyHasDeviceFamily(objectGraph, data, "tvos"); + if (!isTvOnlyApp) { + const writeReviewAction = writeReviewActionFromData(objectGraph, productData, lockupSubtitle, lockupTitle); + if (serverData.isDefinedNonNull(writeReviewAction)) { + actions.push(writeReviewAction); + } + } + } + return new models.FlowPreviewActionsConfiguration(actions, offerDisplayProperties, offerActionIndex); + }); +} +/** + * Create and return the flow preview configuration for a review object + * @param data The response data to read from. + * @param deviceId The UUID for the user's device. + * @param adamId The adam id of the product associated with the review + * @returns A configuration object for flow preview actions that can be taken on the review + */ +export function flowPreviewActionsConfigurationForReviewFromData(objectGraph, data, deviceId, adamId, reviewText) { + var _a; + if (serverData.isNullOrEmpty(data) || + (objectGraph.client.deviceType !== "phone" && + objectGraph.client.deviceType !== "pad" && + objectGraph.client.deviceType !== "mac")) { + return null; + } + const actions = [ + voteActionFromData(objectGraph, data, deviceId, true), + voteActionFromData(objectGraph, data, deviceId, false), + ]; + // Workaround for missing report concern url in bag + if (((_a = objectGraph.bag.reportConcernUrl) === null || _a === void 0 ? void 0 : _a.length) > 0) { + actions.push(reportConcernActionFromData(objectGraph, data, deviceId)); + } + if ((reviewText === null || reviewText === void 0 ? void 0 : reviewText.length) > 0) { + actions.push(copyReviewAction(objectGraph, reviewText)); + } + return new models.FlowPreviewActionsConfiguration(actions); +} +/** + * Create and return the flow preview configuration for a review summary object + * @param data The response data to read from. + * @param adamId The adam ID of the app + * @param appName The name of the app + * @param reviewSummaryId The ID of the review summary + * @param reviewSummaryText The text of the review summary + * @param deviceId The UUID for the customer's device. + * @returns A configuration object for flow preview actions that can be taken on the review summary + */ +export function flowPreviewActionsConfigurationForReviewSummaryFromData(objectGraph, data, adamId, appName, reviewSummaryId, reviewSummaryText, deviceId) { + if (!objectGraph.client.isiOS) { + return null; + } + const actions = []; + const reportConcernAction = reviewSummaryReportConcernActionFromData(objectGraph, data, reviewSummaryId, deviceId); + if (isSome(reportConcernAction)) { + actions.push(reportConcernAction); + } + const learnMoreAction = reviews.reviewSummaryLearnMoreAction(objectGraph); + if (isSome(learnMoreAction)) { + actions.push(learnMoreAction); + } + const fileRadarAction = fileReviewSummaryRadarAction(objectGraph, adamId, appName, reviewSummaryId, reviewSummaryText); + if (isSome(fileRadarAction)) { + actions.push(fileRadarAction); + } + return new models.FlowPreviewActionsConfiguration(actions); +} +/** + * Creates a report a concern action for a given review + * @param data The response data to read from. + * @param deviceId The UUID for the user's device. + * @returns A report concern action + */ +export function reviewSummaryReportConcernActionFromData(objectGraph, data, reviewSummaryId, deviceId) { + return validation.context("reviewSummaryReportConcernActionFromApiRow", () => { + const isEnabled = serverData.asBoolean(data, "enabled"); + if (!isEnabled) { + return null; + } + const sendAction = createReviewSummaryReportConcernHtmlTemplateAction(objectGraph, reviewSummaryId); + const concerns = serverData.asArrayOrEmpty(data, "concerns"); + const reviewSummaryConcerns = concerns.map((concernData) => { + let name; + let uppercaseName; + const concernKind = serverData.asString(concernData, "kind"); + switch (concernKind) { + case "OFFENSIVE": + case "OFFENSIVE_OR_HARMFUL": + name = objectGraph.loc.string("ProductPage.ReviewSummary.ReportAConcern.Offensive.Name"); + uppercaseName = objectGraph.loc.string("ProductPage.ReviewSummary.ReportAConcern.Offensive.UppercaseName"); + break; + case "MISREPRESENTING_THE_APP": + case "MISREPRESENT": + name = objectGraph.loc.string("ProductPage.ReviewSummary.ReportAConcern.Misrepresent.Name"); + uppercaseName = objectGraph.loc.string("ProductPage.ReviewSummary.ReportAConcern.Misrepresent.UppercaseName"); + break; + case "SOMETHING_ELSE": + name = objectGraph.loc.string("ProductPage.ReviewSummary.ReportAConcern.SomethingElse.Name"); + uppercaseName = objectGraph.loc.string("ProductPage.ReviewSummary.ReportAConcern.SomethingElse.UppercaseName"); + break; + default: + break; + } + return new base.ReportConcernReason(concernKind, name, uppercaseName); + }); + if (reviewSummaryConcerns.length === 0) { + return null; + } + const title = objectGraph.loc.string("ACTION_REVIEW_REPORT"); + const explanation = objectGraph.loc.string("ProductPage.ReviewSummary.ReportAConcern.Explanation"); + const action = new models.ReviewSummaryReportConcernAction(reviewSummaryConcerns, title, explanation, sendAction); + action.artwork = contentArtwork.createArtworkForResource(objectGraph, "systemimage://exclamationmark.circle"); + return action; + }); +} +function createReviewSummaryReportConcernHtmlTemplateAction(objectGraph, reviewSummaryId) { + const request = new Request(objectGraph); + request.includeAppBinaryTraitsAttribute = false; + request.resourceType = "concerns"; + const url = buildURLFromRequest(objectGraph, request); + const sendAction = new models.HttpTemplateAction(url.toString()); + sendAction.method = "POST"; + sendAction.disableCache = true; + sendAction.needsMediaToken = true; + sendAction.headers = { "Content-Type": "application/json" }; + sendAction.bodyDictionary = { + report: { contentId: reviewSummaryId, contentKind: "review-summaries", concerns: null }, + }; + const successToast = new models.AlertAction("toast"); + successToast.title = objectGraph.loc.string("TOAST_CONCERN_REPORTED_TITLE"); + successToast.message = objectGraph.loc.string("TOAST_CONCERN_REPORTED_DESCRIPTION"); + successToast.artwork = contentArtwork.createArtworkForResource(objectGraph, "systemimage://exclamationmark.circle"); + sendAction.successAction = successToast; + const failureAlert = new models.AlertAction("default"); + failureAlert.title = objectGraph.loc.string("Alert.GenericError.Title"); + failureAlert.message = objectGraph.loc.string("Alert.GenericError.Message"); + failureAlert.isCancelable = true; + sendAction.failureAction = failureAlert; + return sendAction; +} +/** + * Walks through the given data to extract the data blob for a product + * @param data The response data to read from. + * @returns Response data for a product + */ +function productDataFromData(objectGraph, data) { + return validation.context(`productDataFromData: ${data.type}`, () => { + switch (data.type) { + case "apps": + case "app-bundles": { + return data; + } + case "editorial-items": { + const cardContents = mediaRelationship.relationshipCollection(data, "card-contents"); + if (serverData.isDefinedNonNullNonEmpty(cardContents) && cardContents.length === 1) { + const cardContentsData = cardContents[0]; + return productDataFromData(objectGraph, cardContentsData); + } + break; + } + case "editorial-elements": { + const contents = mediaRelationship.relationshipCollection(data, "contents"); + if (serverData.isDefinedNonNullNonEmpty(contents) && contents.length === 1) { + const contentData = contents[0]; + return productDataFromData(objectGraph, contentData); + } + break; + } + default: { + return null; + } + } + return null; + }); +} +/** + * Create an offer action for the provided product data + * @param data The response data to read from. + * @param isPreorder Whether the product is a pre-order + * @param isArcade Whether the product is an Arcade product + * @param offerType The type of offer for the product + * @param clientIdentifierOverride The suggested client identifier for the given product + * @param metricsClickOptions TThe metrics click options for the product + * @returns An offer action + */ +function offerActionFromData(objectGraph, data, isPreorder, isArcade, offerType, clientIdentifierOverride, metricsClickOptions) { + if (serverData.isNull(data) || data.type !== "apps") { + return null; + } + const offerData = offers.offerDataFromData(objectGraph, data); + const appIcon = content.iconFromData(objectGraph, data, null, clientIdentifierOverride); + const metricsPlatformDisplayStyle = metricsUtil.metricsPlatformDisplayStyleFromData(objectGraph, data, appIcon, clientIdentifierOverride); + const offerAction = offers.offerActionFromOfferData(objectGraph, offerData, data, isPreorder, false, metricsPlatformDisplayStyle, metricsClickOptions, "flowPreview"); + return offerAction; +} +/** + * Wraps an offer action if required + * @param data The response data to read from. + * @param offerAction The provided offer action to wrap + * @param clientIdentifierOverride The suggested client identifier for the given product + * @param metricsClickOptions TThe metrics click options for the product + * @param metricsOptions The metrics options to use for reporting metrics actions. + * @param externalDeepLink The promotional deep link url to use on the product's offer. + * @returns A wrapped offer action + */ +function wrappedOfferActionFromData(objectGraph, data, offerAction, isPreorder, clientIdentifierOverride, metricsClickOptions, metricsOptions, externalDeepLinkUrl) { + if (serverData.isNull(offerAction)) { + return null; + } + let wrappedOfferAction = offers.wrapOfferActionIfNeeded(objectGraph, offerAction, data, isPreorder, metricsClickOptions, "flowPreview", clientIdentifierOverride); + if ((externalDeepLinkUrl === null || externalDeepLinkUrl === void 0 ? void 0 : externalDeepLinkUrl.length) > 0) { + // Configure cross link as well as deep link action. + wrappedOfferAction = externalDeepLink.deepLinkActionWrappingAction(objectGraph, wrappedOfferAction, offerAction.adamId, null, externalDeepLinkUrl, false, metricsClickOptions); + } + return wrappedOfferAction; +} +/** + * Creates and returns an action for sharing a product + * @param data The response data to read from. + * @param metricsOptions The metrics options to use for reporting metrics actions. + * @returns A share action or null + */ +function shareActionFromData(objectGraph, data, metricsOptions) { + const shareAction = sharing.shareProductActionFromData(objectGraph, data, metricsOptions.pageInformation, metricsOptions.locationTracker); + if (serverData.isDefinedNonNull(shareAction)) { + shareAction.title = objectGraph.loc.string("FLOW_PREVIEW_ACTION_SHARE"); + shareAction.artwork = contentArtwork.createArtworkForResource(objectGraph, "systemimage://square.and.arrow.up"); + return shareAction; + } + return shareAction; +} +/** + * Creates and returns an action for navigating to a product's ratings & reviews + * @param data The response data to read from. + * @param clickAction The flow action for viewing the product + * @returns An action or null + */ +function seeRatingsAndReviewsActionFromData(objectGraph, data, clickAction) { + const seeRatingsAndReviewsAction = reviews.seeRatingsAndReviewsActionFromClickAction(objectGraph, data.id, clickAction); + if (serverData.isDefinedNonNull(seeRatingsAndReviewsAction)) { + seeRatingsAndReviewsAction.title = objectGraph.loc.string("FLOW_PREVIEW_ACTION_SEE_RATINGS_AND_REVIEWS"); + seeRatingsAndReviewsAction.artwork = contentArtwork.createArtworkForResource(objectGraph, "systemimage://star"); + seeRatingsAndReviewsAction.animationBehavior = "never"; + } + return seeRatingsAndReviewsAction; +} +/** + * Creates and returns an action for writing a product review + * @param data The response data to read from. + * @returns An action or null + */ +function writeReviewActionFromData(objectGraph, data, lockupSubtitle, lockupTitle) { + const writeReviewAction = reviews.writeReviewActionFromData(objectGraph, data, lockupSubtitle, lockupTitle); + if (serverData.isDefinedNonNull(writeReviewAction)) { + writeReviewAction.title = objectGraph.loc.string("FLOW_PREVIEW_ACTION_WRITE_REVIEW"); + writeReviewAction.artwork = contentArtwork.createArtworkForResource(objectGraph, "systemimage://square.and.pencil"); + } + return writeReviewAction; +} +/** + * Create and returns an action for voting whether a review was helpful or not + * @param data The response data to read from. + * @param deviceId The UUID for the user's device. + * @param helpful Whether or not the vote is indicating the review was helpful, or not helpful. + * @returns A new vote action. + */ +export function voteActionFromData(objectGraph, data, deviceId, helpful) { + const baseVoteUrl = objectGraph.bag.voteUrl; + const reviewId = serverData.asString(data, "id", "coercible"); + const voteUrl = new urls.URL(baseVoteUrl).param("userReviewId", reviewId); + if (objectGraph.client.isVision) { + // We only attach this parameter for visionOS, as the other platforms have legacy handling in place + // that we do not want to interrupt. + voteUrl.param("version", "2"); + } + const action = new models.HttpAction(voteUrl.build()); + const successToast = new models.AlertAction("toast"); + if (helpful) { + action.title = objectGraph.loc.string("ACTION_REVIEW_HELPFUL"); + successToast.title = objectGraph.loc.string("TOAST_HELPFUL_TITLE"); + successToast.message = objectGraph.loc.string("TOAST_HELPFUL_DESCRIPTION"); + action.artwork = contentArtwork.createArtworkForResource(objectGraph, "systemimage://hand.thumbsup"); + successToast.artwork = contentArtwork.createArtworkForResource(objectGraph, objectGraph.client.isVision ? "systemimage://hand.thumbsup.fill" : "systemimage://hand.thumbsup"); + } + else { + action.title = objectGraph.loc.string("ACTION_REVIEW_NOT_HELPFUL"); + successToast.title = objectGraph.loc.string("TOAST_NOT_HELPFUL_TITLE"); + successToast.message = objectGraph.loc.string("TOAST_NOT_HELPFUL_DESCRIPTION"); + action.artwork = contentArtwork.createArtworkForResource(objectGraph, "systemimage://hand.thumbsdown"); + successToast.artwork = contentArtwork.createArtworkForResource(objectGraph, objectGraph.client.isVision ? "systemimage://hand.thumbsdown.fill" : "systemimage://hand.thumbsdown"); + } + action.method = "POST"; + action.isStoreRequest = true; + action.disableCache = true; + action.headers = { "Content-Type": http.FormBuilder.contentType }; + action.body = new http.FormBuilder() + .param("vote", helpful ? "1" : "0") + .param("guid", deviceId) + .build(); + action.successAction = successToast; + return action; +} +/** + * Creates a report a concern action for a given review + * @param data The response data to read from. + * @param deviceId The UUID for the user's device. + * @returns A report concern action + */ +export function reportConcernActionFromData(objectGraph, data, deviceId) { + return validation.context("reportConcernActionFromApiRow", () => { + const reviewId = serverData.asString(data, "id", "coercible"); + const reportConcernUrl = objectGraph.bag.reportConcernUrl; + const sendAction = new models.HttpTemplateAction(reportConcernUrl); + sendAction.method = "POST"; + sendAction.isStoreRequest = true; + sendAction.disableCache = true; + sendAction.needsAuthentication = true; + sendAction.headers = { "Content-Type": http.FormBuilder.contentType }; + sendAction.body = new http.FormBuilder().param("userReviewId", reviewId).param("guid", deviceId).build(); + const reasonParameter = new models.HttpTemplateParameter("selectedReason", "formBody", "decimalPad"); + const explanationParameter = new models.HttpTemplateParameter("explanation", "formBody", "text"); + sendAction.parameters = [reasonParameter, explanationParameter]; + if (!objectGraph.client.isVision) { + const successToast = new models.AlertAction("toast"); + successToast.title = objectGraph.loc.string("TOAST_CONCERN_REPORTED_TITLE"); + successToast.message = objectGraph.loc.string("TOAST_CONCERN_REPORTED_DESCRIPTION"); + successToast.artwork = contentArtwork.createArtworkForResource(objectGraph, "systemimage://exclamationmark.circle"); + sendAction.successAction = successToast; + } + const failureAlert = new models.AlertAction("default"); + failureAlert.title = objectGraph.loc.string("Alert.GenericError.Title"); + failureAlert.message = objectGraph.loc.string("Alert.GenericError.Message"); + failureAlert.isCancelable = true; + sendAction.failureAction = failureAlert; + let concernReasons = objectGraph.bag.reportConcernReasons; + if (serverData.isNullOrEmpty(concernReasons)) { + // Not available until 18G + concernReasons = [ + { + reasonId: "1", + name: "It contains offensive material", + upperCaseName: "IT CONTAINS OFFENSIVE MATERIAL", + }, + { + reasonId: "8", + name: "It's off-topic", + upperCaseName: "IT\u2019S OFF-TOPIC", + }, + { + reasonId: "111003", + name: "It looks like spam", + upperCaseName: "IT LOOKS LIKE SPAM", + }, + { + reasonId: "7", + name: "Something else", + upperCaseName: "SOMETHING ELSE", + }, + ]; + } + const reasons = concernReasons.map((reasonData) => { + return new base.ReportConcernReason(serverData.asString(reasonData, "reasonId"), serverData.asString(reasonData, "name"), serverData.asString(reasonData, "upperCaseName")); + }); + const action = new models.ReportConcernAction(reasons); + action.title = objectGraph.loc.string("ACTION_REVIEW_REPORT"); + action.explanation = objectGraph.bag.reportConcernExplanation; + if (serverData.isNullOrEmpty(action.explanation)) { + // Not available until 18G + action.explanation = "Tell us a little more (Optional)"; + } + action.artwork = contentArtwork.createArtworkForResource(objectGraph, "systemimage://exclamationmark.circle"); + action.sendAction = sendAction; + return action; + }); +} +export function copyReviewAction(objectGraph, reviewText) { + const copyTextAction = new models.CopyTextAction(reviewText); + copyTextAction.title = objectGraph.loc.string("ACTION_REVIEW_COPY"); + copyTextAction.artwork = contentArtwork.createArtworkForResource(objectGraph, "systemimage://doc.on.doc"); + return copyTextAction; +} +/** + * Creates an action for filing a radar against a Review Summary + * @param objectGraph Current object graph + * @param adamId The ID of the app + * @param appName The name of the app + * @param reviewSummaryId The ID of the review summary + * @param reviewSummaryText The text of the review summary + * @returns An action that will deep link to Radar + */ +function fileReviewSummaryRadarAction(objectGraph, adamId, appName, reviewSummaryId, reviewSummaryText) { + if (!["debug", "internal"].includes(objectGraph.client.buildType)) { + return null; + } + const componentId = "999915"; // ASE App Store | Recommendations + const title = `Review Summary Feedback: ${appName}`; + const description = `App ID: ${adamId}\nApp name: ${appName}\nReview summary ID: ${reviewSummaryId}\nReview summary: ${reviewSummaryText}\n\nFeedback: `; + const url = `tap-to-radar://new/problem?componentid=${componentId}&title=${title}&description=${description}`; + const action = new models.ExternalUrlAction(url, true); + action.title = objectGraph.loc.string("Action.ProvideFeedback"); + action.artwork = contentArtwork.createArtworkForResource(objectGraph, "systemimage://ant.circle"); + return action; +} +//# sourceMappingURL=flow-preview.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/content/game-controller.js b/node_modules/@jet-app/app-store/tmp/src/common/content/game-controller.js new file mode 100644 index 0000000..7071edd --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/content/game-controller.js @@ -0,0 +1,77 @@ +import { isFeatureEnabledForCurrentUser } from "../../common/util/lottery"; +import * as serverData from "../../foundation/json-parsing/server-data"; +import * as platformAttributes from "../../foundation/media/platform-attributes"; +import * as contentAttributes from "./attributes"; +export function isGameControllerSupported(objectGraph, data) { + switch (controllerRequirementFromData(objectGraph, data)) { + case "CONTROLLER_OPTIONAL": + case "CONTROLLER_REQUIRED": + case "SIRI_REMOTE_REQUIRED": + case "SIRI_REMOTE_OR_CONTROLLER_REQUIRED": + case "CONTROLLER_RECOMMENDED": + return true; + default: + return false; + } +} +export function isGameControllerRecommended(objectGraph, data) { + return (objectGraph.client.isiOS && + isFeatureEnabledForCurrentUser(objectGraph, objectGraph.bag.gameControllerRecommendedRolloutRate) && + controllerRequirementFromData(objectGraph, data) === "CONTROLLER_RECOMMENDED"); +} +export function isGameControllerRequired(objectGraph, data) { + switch (controllerRequirementFromData(objectGraph, data)) { + case "CONTROLLER_REQUIRED": + case "SIRI_REMOTE_REQUIRED": + case "SIRI_REMOTE_OR_CONTROLLER_REQUIRED": + return true; + default: + return false; + } +} +export function isSpatialControllerRequired(objectGraph, data) { + return spatialControllerRequirementFromData(objectGraph, data) === "REQUIRED"; +} +export function isSpatialControllerSupported(objectGraph, data) { + return (spatialControllerRequirementFromData(objectGraph, data) === "SUPPORTED" || + spatialControllerRequirementFromData(objectGraph, data) === "REQUIRED"); +} +export function controllerRequirementFromData(objectGraph, data) { + // If the data does not contain the current device's OS, we need to use the best alternative that it is compatible with. + const platformAttributeForClient = contentAttributes.defaultAttributePlatform(objectGraph); + if (platformAttributeForClient === null) { + return "NO_BADGE"; + } + const requiresBinaryCompatibilityMode = !platformAttributes.hasPlatformAttribute(data, platformAttributeForClient); + if (requiresBinaryCompatibilityMode) { + const compatibilityControllerRequirementData = contentAttributes.contentAttributeAsDictionary(objectGraph, data, "compatibilityControllerRequirement"); + if (compatibilityControllerRequirementData === null || + serverData.isNullOrEmpty(compatibilityControllerRequirementData)) { + return "NO_BADGE"; + } + const compatibilityControllerRequirement = compatibilityControllerRequirementData[platformAttributeForClient]; + if (serverData.isNullOrEmpty(compatibilityControllerRequirement)) { + return "NO_BADGE"; + } + return compatibilityControllerRequirement; + } + const remoteControllerRequirement = contentAttributes.contentAttributeAsString(objectGraph, data, "remoteControllerRequirement"); + if (serverData.isDefinedNonNullNonEmpty(remoteControllerRequirement)) { + return remoteControllerRequirement; + } + return "NO_BADGE"; +} +export function spatialControllerRequirementFromData(objectGraph, data) { + const platformAttributeForClient = contentAttributes.defaultAttributePlatform(objectGraph); + if (!objectGraph.client.isVision || + platformAttributeForClient === null || + !objectGraph.featureFlags.isEnabled("spatial_controllers_2025A")) { + return "NOT_SUPPORTED"; + } + const spatialRequirement = platformAttributes.platformAttributeAsString(data, platformAttributeForClient, "spatialControllerRequirement"); + if (serverData.isDefinedNonNullNonEmpty(spatialRequirement)) { + return spatialRequirement; + } + return "NOT_SUPPORTED"; +} +//# sourceMappingURL=game-controller.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/content/product-capabilities.js b/node_modules/@jet-app/app-store/tmp/src/common/content/product-capabilities.js new file mode 100644 index 0000000..7c449c8 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/content/product-capabilities.js @@ -0,0 +1,322 @@ +import * as validation from "@jet/environment/json/validation"; +import * as models from "../../api/models"; +import { isFeatureEnabledForCurrentUser } from "../../common/util/lottery"; +import * as mediaAttributes from "../../foundation/media/attributes"; +import * as color from "../../foundation/util/color-util"; +import { createArtworkForResource } from "./artwork/artwork"; +import * as contentAttributes from "./attributes"; +import * as gameController from "./game-controller"; +import { isSome } from "@jet/environment"; +import { supportedGameCenterFeaturesFromData } from "./content"; +import { makeRoutableArticlePageIntent } from "../../api/intents/routable-article-page-intent"; +import { getLocale } from "../locale"; +import { makeRoutableArticlePageCanonicalUrl } from "../today/routable-article-page-url-utils"; +import { openGamesUIAction } from "../arcade/arcade-common"; +import { getPlatform } from "../preview-platform"; +/** + * Creates a list of product capabilities for a given product. + * + * @param objectGraph The object graph + * @param productData The data for the product + * @param isFreeProduct Whether the offer is for a free product + * @returns An array of product capabilities + */ +export function productCapabilitiesFromData(objectGraph, productData, isFreeProduct) { + return validation.context("capabilitiesFromData", () => { + return [ + gameCenterCapabilityFromData(objectGraph, productData), + siriCapabilityFromData(objectGraph, productData), + sharePlayCapabilityFromData(objectGraph, productData), + walletCapabilityFromData(objectGraph, productData), + controllersCapabilityFromData(objectGraph, productData), + familySharingCapabilityFromData(objectGraph, productData, isFreeProduct), + spatialControlsCapabilityFromData(objectGraph, productData), + safariExtensionCapabilityFromData(objectGraph, productData), + ].filter((capability) => capability); + }); +} +/** + * Creates the Game Center capability. + * @param objectGraph The object graph + * @param productData The data for the product + * @param gameInfoSummary The game info summary for the product + * @returns A product capability, or null + */ +export function gameCenterCapabilityFromData(objectGraph, productData) { + const isGameCenterEnabled = isSome(productData) && + contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, productData, "isGameCenterEnabled"); + if (!isGameCenterEnabled) { + return null; + } + if (objectGraph.bag.gameCenterExtendSupportedFeatures) { + const supportedGameCenterFeatures = supportedGameCenterFeaturesFromData(productData); + const supportsLeaderboards = supportedGameCenterFeatures === null || supportedGameCenterFeatures === void 0 ? void 0 : supportedGameCenterFeatures.includes("leaderboards"); + const supportsAchievements = supportedGameCenterFeatures === null || supportedGameCenterFeatures === void 0 ? void 0 : supportedGameCenterFeatures.includes("achievements"); + let captionText; + if (supportsLeaderboards && supportsAchievements) { + captionText = objectGraph.loc.string("CAPABILITY_GAME_CENTER_EXPLANATION_ALL_FEATURES"); + } + else if (supportsLeaderboards) { + captionText = objectGraph.loc.string("CAPABILITY_GAME_CENTER_EXPLANATION_ONLY_LEADERBOARDS"); + } + else if (supportsAchievements) { + captionText = objectGraph.loc.string("CAPABILITY_GAME_CENTER_EXPLANATION_ONLY_ACHIEVEMENTS"); + } + else { + captionText = objectGraph.loc.string("CAPABILITY_GAME_CENTER_EXPLANATION_NO_FEATURES"); + } + const title = objectGraph.loc.string("CAPABILITY_GAME_CENTER_TITLE"); + const artwork = createArtworkForResource(objectGraph, "resource://ProductCapabilityGameCenter", 46, 45); + if (objectGraph.featureFlags.isGSEUIEnabled("de7bbd8e")) { + let linkAction; + if (isSome(productData) && objectGraph.props.enabled("gameProductIdOpenGamesUIAction")) { + linkAction = openGamesUIAction(objectGraph, { + gamePage: { productID: productData.id }, + }); + } + else { + linkAction = openGamesUIAction(objectGraph); + } + const linkActionTitle = objectGraph.loc.string("ProductPage.Capability.GameCenter.ActionTitle"); + captionText = `${captionText}\n${linkActionTitle}`; + const styledText = new models.StyledText(captionText); + const linkedSubstrings = {}; + linkedSubstrings[linkActionTitle] = linkAction; + const linkableCaption = new models.LinkableText(styledText, linkedSubstrings); + const captionTrailingArtwork = createArtworkForResource(objectGraph, "systemimage://arrow.up.forward.square.fill", 16, 16); + return new models.ProductCapability("gameCenter", title, linkableCaption, captionTrailingArtwork, undefined, artwork); + } + else { + const styledText = new models.StyledText(captionText); + const linkableCaption = new models.LinkableText(styledText); + return new models.ProductCapability("gameCenter", title, linkableCaption, null, null, artwork); + } + } + else { + const title = objectGraph.loc.string("CAPABILITY_GAME_CENTER_TITLE"); + const styledText = new models.StyledText(objectGraph.loc.string("CAPABILITY_GAME_CENTER_EXPLANATION")); + const caption = new models.LinkableText(styledText); + const artwork = createArtworkForResource(objectGraph, "resource://ProductCapabilityGameCenter", 46, 45); + return new models.ProductCapability("gameCenter", title, caption, undefined, null, artwork); + } +} +/** + * Creates the Siri capability. + * @param objectGraph The object graph + * @param productData The data for the product + * @returns A product capability, or null + */ +function siriCapabilityFromData(objectGraph, productData) { + if (!contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, productData, "isSiriSupported")) { + return null; + } + const title = objectGraph.loc.string("CAPABILITY_SIRI_TITLE"); + const styledText = new models.StyledText(objectGraph.loc.string("CAPABILITY_SIRI_EXPLANATION")); + const caption = new models.LinkableText(styledText); + const artwork = createArtworkForResource(objectGraph, "resource://ProductCapabilitySiri", 46, 45); + return new models.ProductCapability("siri", title, caption, undefined, null, artwork); +} +/** + * Creates the Wallet capability. + * @param objectGraph The object graph + * @param productData The data for the product + * @returns A product capability, or null + */ +function walletCapabilityFromData(objectGraph, productData) { + if (!contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, productData, "supportsPassbook")) { + return null; + } + const title = objectGraph.loc.string("CAPABILITY_WALLET_TITLE"); + const styledText = new models.StyledText(objectGraph.loc.string("CAPABILITY_WALLET_EXPLANATION")); + const caption = new models.LinkableText(styledText); + const artwork = createArtworkForResource(objectGraph, "resource://ProductCapabilityWallet", 46, 45); + return new models.ProductCapability("wallet", title, caption, undefined, null, artwork); +} +/** + * Creates the Controllers capability. + * @param objectGraph The object graph + * @param productData The data for the product + * @returns A product capability, or null + */ +function controllersCapabilityFromData(objectGraph, productData) { + if (!gameController.isGameControllerSupported(objectGraph, productData)) { + return null; + } + const title = objectGraph.loc.string("CAPABILITY_MFI_CONTROLLERS_TITLE"); // Game Controllers + // Create the Learn More link + let linkAction = null; + const storyId = objectGraph.bag.gameControllerLearnMoreEditorialItemId; + if (isFeatureEnabledForCurrentUser(objectGraph, objectGraph.bag.gameControllerRecommendedRolloutRate) && + (objectGraph.client.isiOS || objectGraph.client.isVision || objectGraph.client.isWeb) && + isSome(storyId) && + (storyId === null || storyId === void 0 ? void 0 : storyId.length) > 0) { + const routableArticlePageIntent = makeRoutableArticlePageIntent({ + ...getLocale(objectGraph), + ...getPlatform(objectGraph), + id: storyId, + }); + linkAction = new models.FlowAction("article"); + linkAction.title = objectGraph.loc.string("ProductPage.Capability.GameController.ActionTitle"); // Learn More + linkAction.pageUrl = makeRoutableArticlePageCanonicalUrl(objectGraph, routableArticlePageIntent); + if (objectGraph.client.isWeb) { + linkAction.destination = routableArticlePageIntent; + } + } + let captionText = linkAction + ? objectGraph.loc.string("ProductPage.Capability.GameController.Explanation.v2.WithNewlineActionTemplate") + : objectGraph.loc.string("ProductPage.Capability.GameController.Explanation.v2"); + const linkedSubstrings = {}; + if (linkAction === null || linkAction === void 0 ? void 0 : linkAction.title) { + captionText = captionText.replace("{learnMoreLink}", linkAction.title); // Learn More + linkedSubstrings[linkAction.title] = linkAction; + } + const styledText = new models.StyledText(captionText); + const linkableCaption = new models.LinkableText(styledText, linkedSubstrings); + const artwork = createArtworkForResource(objectGraph, "resource://ProductCapabilityController", 46, 45); + return new models.ProductCapability("controllers", title, linkableCaption, undefined, null, artwork); +} +/** + * Creates the Spatial Controls capability. + * @param objectGraph The object graph + * @param productData The data for the product + * @returns A product capability, or null + */ +function spatialControlsCapabilityFromData(objectGraph, productData) { + if (!objectGraph.client.isVision || !gameController.isSpatialControllerSupported(objectGraph, productData)) { + return null; + } + const title = objectGraph.loc.string("ProductPage.Badge.SpatialController.Heading"); + // Create the Learn More link + let linkAction = null; + const storyId = objectGraph.bag.spatialControlsLearnMoreEditorialItemId; + if (isSome(storyId) && (storyId === null || storyId === void 0 ? void 0 : storyId.length) > 0) { + const routableArticlePageIntent = makeRoutableArticlePageIntent({ + ...getLocale(objectGraph), + ...getPlatform(objectGraph), + id: storyId, + }); + linkAction = new models.FlowAction("article"); + linkAction.title = objectGraph.loc.string("Action.LearnMore"); + linkAction.pageUrl = makeRoutableArticlePageCanonicalUrl(objectGraph, routableArticlePageIntent); + if (objectGraph.client.isWeb) { + linkAction.destination = routableArticlePageIntent; + } + } + let captionText = linkAction !== null + ? objectGraph.loc.string("ProductPage.Capability.SpatialController.Explanation.WithActionTemplate") + : objectGraph.loc.string("ProductPage.Capability.SpatialController.Explanation"); + const linkedSubstrings = {}; + if (isSome(linkAction === null || linkAction === void 0 ? void 0 : linkAction.title)) { + captionText = captionText.replace("{learnMoreLink}", linkAction.title); + linkedSubstrings[linkAction.title] = linkAction; + } + const styledText = new models.StyledText(captionText); + const linkableCaption = new models.LinkableText(styledText, linkedSubstrings); + const artwork = createArtworkForResource(objectGraph, "resource://ProductCapabilitySpatialControllers"); + return new models.ProductCapability("spatialControllers", title, linkableCaption, undefined, null, artwork); +} +/** + * Creates a family sharing capability for the given product, if any. This is driven off whether family sharing + * is enabled, whether there are any IAPs, and whther any of those IAPs are shareable. + * + * @param objectGraph The object graph + * @param productData The data for the product + * @param isFreeProduct Whether the offer is for a free product + * @returns A product capability, or null + */ +function familySharingCapabilityFromData(objectGraph, productData, isFreeProduct) { + // Check if family sharing is enabled, or if this is a SAD app + const familyShareEnabledDateString = mediaAttributes.attributeAsString(productData, "familyShareEnabledDate"); + if (!familyShareEnabledDateString || + mediaAttributes.attributeAsBooleanOrFalse(productData, "isFirstPartyHideableApp")) { + return null; + } + // Check family sharing was enabled in the past + const familyShareEnabledDate = new Date(familyShareEnabledDateString); + const now = new Date(); + if (!familyShareEnabledDate || familyShareEnabledDate > now) { + return null; + } + const hasInAppPurchases = contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, productData, "hasInAppPurchases"); + const hasFamilyShareableInAppPurchases = hasInAppPurchases && + contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, productData, "hasFamilyShareableInAppPurchases"); + // Create the Learn More link + let linkAction = null; + const storyId = objectGraph.bag.familySubscriptionsLearnMoreEditorialItemId; + const platformSupportsLink = objectGraph.client.isiOS || objectGraph.client.isMac || objectGraph.client.isVision || objectGraph.client.isWeb; + if (isSome(storyId) && (storyId === null || storyId === void 0 ? void 0 : storyId.length) > 0 && platformSupportsLink && hasFamilyShareableInAppPurchases) { + const routableArticlePageIntent = makeRoutableArticlePageIntent({ + ...getLocale(objectGraph), + ...getPlatform(objectGraph), + id: storyId, + }); + linkAction = new models.FlowAction("article"); + linkAction.title = objectGraph.loc.string("CAPABILITY_FAMILY_SHARING_ACTION_TITLE"); + linkAction.pageUrl = makeRoutableArticlePageCanonicalUrl(objectGraph, routableArticlePageIntent); + if (objectGraph.client.isWeb) { + linkAction.destination = routableArticlePageIntent; + } + } + // Generate the caption based on whether the product has IAPs, whether it has shareable IAPs, and whether it is free + const title = objectGraph.loc.string("CAPABILITY_FAMILY_SHARING_TITLE"); + let captionKey; + if (hasFamilyShareableInAppPurchases) { + captionKey = linkAction + ? "CAPABILITY_FAMILY_SHARING_SOME_SHAREABLE_IAPS_EXPLANATION_WITH_ACTION_TEMPLATE" + : "CAPABILITY_FAMILY_SHARING_SOME_SHAREABLE_IAPS_EXPLANATION"; + } + else if (!hasInAppPurchases && !isFreeProduct) { + captionKey = linkAction + ? "CAPABILITY_FAMILY_SHARING_PAID_APP_NO_IAPS_EXPLANATION_WITH_ACTION_TEMPLATE" + : "CAPABILITY_FAMILY_SHARING_PAID_APP_NO_IAPS_EXPLANATION"; + } + if (!captionKey) { + return null; + } + let captionText = objectGraph.loc.string(captionKey); + const linkedSubstrings = {}; + if (linkAction === null || linkAction === void 0 ? void 0 : linkAction.title) { + captionText = captionText.replace("{learnMoreLink}", linkAction.title); + linkedSubstrings[linkAction.title] = linkAction; + } + const styledText = new models.StyledText(captionText); + const linkableCaption = new models.LinkableText(styledText, linkedSubstrings); + const artwork = createArtworkForResource(objectGraph, "resource://ProductCapabilityFamilySharing", 46, 45); + return new models.ProductCapability("familySharing", title, linkableCaption, undefined, linkAction, artwork); +} +/** + * Creates the Safari Extension capability. + * @param objectGraph The object graph + * @param productData The data for the product + * @returns A product capability, or null + */ +function safariExtensionCapabilityFromData(objectGraph, productData) { + const productHasExtension = contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, productData, "hasSafariExtension"); + const platformSupports = objectGraph.client.isMac || objectGraph.client.isiOS; + if (!productHasExtension || !platformSupports) { + return null; + } + const title = objectGraph.loc.string("CAPABILITY_SAFARI_EXTENSION_TITLE"); + const styledText = new models.StyledText(objectGraph.loc.string("CAPABILITY_SAFARI_EXTENSION_EXPLANATION")); + const caption = new models.LinkableText(styledText); + const artwork = createArtworkForResource(objectGraph, "resource://ProductCapabilitySafariExtension", 129, 129); + return new models.ProductCapability("safariExtensions", title, caption, undefined, null, artwork); +} +/** + * Creates the SharePlay capability + * @param objectGraph The object graph + * @param productData The data for the product + * @returns A product capability, or null + */ +function sharePlayCapabilityFromData(objectGraph, productData) { + if (!contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, productData, "supportsSharePlay")) { + return null; + } + const title = objectGraph.loc.string("CAPABILITY_SHAREPLAY_TITLE"); + const styledText = new models.StyledText(objectGraph.loc.string("CAPABILITY_SHAREPLAY_EXPLANATION")); + const caption = new models.LinkableText(styledText); + const artwork = createArtworkForResource(objectGraph, "systemimage://shareplay"); + const artworkTintColor = color.named("systemGreen"); + return new models.ProductCapability("sharePlay", title, caption, undefined, null, artwork, artworkTintColor); +} +//# sourceMappingURL=product-capabilities.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/content/sad.js b/node_modules/@jet-app/app-store/tmp/src/common/content/sad.js new file mode 100644 index 0000000..20fde05 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/content/sad.js @@ -0,0 +1,93 @@ +import { isSome } from "@jet/environment/types/optional"; +import * as serverData from "../../foundation/json-parsing/server-data"; +import * as mediaAttributes from "../../foundation/media/attributes"; +import * as contentAttributes from "./attributes"; +let implementation = null; +/** + * Creates an implementation of the `SystemApps` interface + * which is appropriate for the current platform. + */ +export function systemApps(objectGraph) { + if (implementation !== null) { + return implementation; + } + // Because watchOS is not a proper platform from the back end's perspective, + // we must map adam IDs to bundle IDs using a bag property. + if (objectGraph.client.isWatch) { + const systemAppsMap = new Map(); + const nonDeletableSystemAppsMap = new Map(); + for (const systemApp of objectGraph.bag.systemApps) { + const bundleId = serverData.asString(systemApp, "bundle-id", "coercible"); + const adamId = serverData.asString(systemApp, "id", "coercible"); + if (isSome(bundleId) && isSome(adamId)) { + systemAppsMap.set(adamId, bundleId); + } + } + for (const nonDeletableSystemApp of objectGraph.bag.nonDeletableSystemApps) { + const bundleId = serverData.asString(nonDeletableSystemApp, "bundle-id", "coercible"); + const adamId = serverData.asString(nonDeletableSystemApp, "id", "coercible"); + if (isSome(bundleId) && isSome(adamId)) { + nonDeletableSystemAppsMap.set(adamId, bundleId); + } + } + const unsupportedSystemApps = new Map(); + implementation = { + bundleIdFromData(data) { + const mappedBundleId = systemAppsMap.get(data.id); + if (!serverData.isNull(mappedBundleId)) { + return mappedBundleId; + } + else { + return contentAttributes.contentAttributeAsString(objectGraph, data, "bundleId"); + } + }, + isSystemAppFromData(data) { + return systemAppsMap.has(data.id); + }, + isUnsupportedDeletableSystemAppFromData(data) { + if (systemAppsMap.has(data.id) && !nonDeletableSystemAppsMap.has(data.id)) { + // We must use the bundleID from the bag here, rather than a bundleID from `data`, + // as AppConduit is expecting the bundleIDs which are provided in the bag + const bundleId = systemAppsMap.get(data.id); + if (serverData.isDefinedNonNullNonEmpty(bundleId)) { + const valueFromMap = unsupportedSystemApps.get(bundleId); + if (isSome(valueFromMap)) { + return valueFromMap; + } + const isUnsupported = !objectGraph.client.deletableSystemAppCanBeInstalledOnWatchWithBundleID(bundleId); + unsupportedSystemApps.set(bundleId, isUnsupported); + return isUnsupported; + } + } + return false; + }, + adamIdFromSystemBundleId(bundleId) { + for (const [key, value] of systemAppsMap) { + if (value === bundleId) { + return key; + } + } + return undefined; + }, + }; + } + else { + implementation = { + bundleIdFromData(data) { + return contentAttributes.contentAttributeAsString(objectGraph, data, "bundleId"); + }, + isSystemAppFromData(data) { + return mediaAttributes.attributeAsBooleanOrFalse(data, "isFirstPartyHideableApp"); + }, + isUnsupportedDeletableSystemAppFromData(data) { + return false; + }, + adamIdFromSystemBundleId(bundleId) { + // This path shouldn't run on any other platforms. + return undefined; + }, + }; + } + return implementation; +} +//# sourceMappingURL=sad.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/developer/developer-common.js b/node_modules/@jet-app/app-store/tmp/src/common/developer/developer-common.js new file mode 100644 index 0000000..881c01d --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/developer/developer-common.js @@ -0,0 +1,611 @@ +import * as validation from "@jet/environment/json/validation"; +import * as models from "../../api/models"; +import * as serverData from "../../foundation/json-parsing/server-data"; +import * as mediaAttributes from "../../foundation/media/attributes"; +import * as mediaDataStructure from "../../foundation/media/data-structure"; +import * as mediaRelationship from "../../foundation/media/relationships"; +import { Path, Protocol } from "../../foundation/network/url-constants"; +import * as client from "../../foundation/wrappers/client"; +import { watchosDeveloperRelationshipKey } from "./developer-request"; +import * as content from "../content/content"; +import * as lockups from "../lockups/lockups"; +import * as metricsHelpersClicks from "../metrics/helpers/clicks"; +import * as metricsHelpersImpressions from "../metrics/helpers/impressions"; +import * as metricsHelpersLocation from "../metrics/helpers/location"; +import * as metricsHelpersPage from "../metrics/helpers/page"; +import * as room from "../room/room-common"; +export class DeveloperRoomToken extends room.RoomPageToken { +} +/// Ordering of shelves for macOS developer page per Relationship key. +/// @seealso mediaUrlMapping.macOSDeveloperRelationshipKeys +const macosDeveloperRelationshipOrder = [ + "latest-release-app", + "arcade-apps", + "app-bundles", + "mac-apps", + "mac-os-compatible-ios-apps", +]; +/// Ordering of shelves for iOS developer page per Relationship key. +/// @seealso mediaUrlMapping.iosDeveloperRelationshipKeys +const iosDeveloperRelationshipOrder = [ + "latest-release-app", + "arcade-apps", + "system-apps", + "app-bundles", + "ios-apps", + "imessage-apps", + "watch-apps", + "atv-apps", +]; +/// The threshold for when to show the see all button. +const seeAllThreshold = 8; +/// Ordering of shelves for macOS developer page per Relationship key. +/// @seealso mediaUrlMapping.macOSDeveloperRelationshipKeys +const visionOSDeveloperRelationshipOrder = [ + "latest-release-app", + "xros-apps", + "arcade-apps", + "ios-apps", +]; +/// Ordering of shelves for the web developer page per Relationship key. +/// @seealso mediaUrlMapping.macOSDeveloperRelationshipKeys +const webDeveloperRelationshipOrder = [ + "latest-release-app", + "system-apps", + "app-bundles", + "ios-apps", + "mac-apps", + "arcade-apps", + "xros-apps", + "atv-apps", + "watch-apps", + "imessage-apps", +]; +export class DeveloperPageShelfToken { +} +export function developerPageFromResponse(objectGraph, response) { + return validation.context("developerPageFromResponse", () => { + const developerData = response.data.length ? response.data[0] : null; + if (!developerData) { + return null; + } + const metricsPageInformation = metricsHelpersPage.metricsPageInformationFromMediaApiResponse(objectGraph, "Artist", developerData.id, response); + const locationTracker = metricsHelpersLocation.newLocationTracker(); + const shelves = shelvesForDeveloperData(objectGraph, developerData, metricsPageInformation, locationTracker); + // A single shelf should be vertical + if (shelves.length === 1) { + shelves[0].isHorizontal = false; + } + // Add developer description + const itunesNotes = content.notesFromData(objectGraph, developerData, "standard"); + if (itunesNotes) { + const paragraph = new models.Paragraph(itunesNotes, "text/x-apple-as3-nqml"); + const shelf = new models.Shelf("paragraph"); + shelf.items = [paragraph]; + shelves.unshift(shelf); + } + // Create the page + const page = new models.GenericPage(shelves); + page.title = mediaAttributes.attributeAsString(developerData, "name"); + if (objectGraph.client.deviceType !== "watch") { + page.presentationOptions = ["prefersLargeTitle"]; + } + page.canonicalURL = mediaAttributes.attributeAsString(developerData, "url"); + // Setup metrics + metricsHelpersPage.addMetricsEventsToPageWithInformation(objectGraph, page, metricsPageInformation); + // Add the uber + const uber = mediaAttributes.attributeAsDictionary(developerData, "editorialArtwork.bannerUber"); + if (uber && !objectGraph.client.isVision) { + const uberArtwork = content.artworkFromApiArtwork(objectGraph, uber, { + cropCode: "sr", + useCase: 21 /* content.ArtworkUseCase.Uber */, + }); + page.uber = uberArtwork; + if (objectGraph.client.isiOS || objectGraph.client.isWeb) { + const uberShelf = new models.Shelf("uber"); + const uberModel = new models.Uber("above"); + uberModel.artwork = uberArtwork; + uberShelf.items = [uberModel]; + uberModel.title = page.title; + shelves.unshift(uberShelf); + page.presentationOptions.push("prefersNonStandardBackButton"); + if (!objectGraph.client.isWeb) { + page.presentationOptions.push("prefersOverlayedPageHeader"); + } + } + } + return page; + }); +} +function shelvesForDeveloperData(objectGraph, data, metricsPageInformation, locationTracker) { + switch (objectGraph.client.deviceType) { + case "mac": + return macosShelvesForDeveloperData(objectGraph, data, metricsPageInformation, locationTracker); + case "watch": + return flatShelvesForDeveloperData(objectGraph, "smallLockup", data, watchosDeveloperRelationshipKey, objectGraph.loc.string("DEVELOPER_WATCH"), metricsPageInformation, locationTracker); + case "vision": + return orderedShelvesForDeveloperData(objectGraph, data, visionOSDeveloperRelationshipOrder, metricsPageInformation, locationTracker); + case "web": + return orderedShelvesForDeveloperData(objectGraph, data, webDeveloperRelationshipOrder, metricsPageInformation, locationTracker); + default: + return orderedShelvesForDeveloperData(objectGraph, data, iosDeveloperRelationshipOrder, metricsPageInformation, locationTracker); + } +} +/** + * Returns the iOS shelf title for a given relationship. + * @param relationshipKey The relationship key. + * @param developerData Media API data for the developer page. + * @returns The localized shelf title. + */ +function iosShelfTitle(objectGraph, relationshipKey, developerData) { + switch (relationshipKey) { + case "latest-release-app": + return objectGraph.loc.string("DEVELOPER_LATEST_RELEASE"); + case "system-apps": + return objectGraph.loc.string("DEVELOPER_SYSTEM_APPS"); + case "imessage-apps": + return objectGraph.loc.string("DEVELOPER_IMESSAGE"); + case "watch-apps": + return objectGraph.loc.string("DEVELOPER_WATCH"); + case "atv-apps": + return objectGraph.loc.string("DEVELOPER_TV"); + case "app-bundles": + return objectGraph.loc.string("DEVELOPER_BUNDLES"); + case "xros-apps": + return objectGraph.loc.string("DEVELOPER_VISION"); + case "ios-apps": + const hasApps = mediaAttributes.attributeAsBooleanOrFalse(developerData, "hasApps"); + const hasGames = mediaAttributes.attributeAsBooleanOrFalse(developerData, "hasGames"); + if (hasApps && hasGames) { + return objectGraph.loc.string("DEVELOPER_APPS_AND_GAMES"); + } + else if (hasGames) { + return objectGraph.loc.string("DEVELOPER_GAMES"); + } + else { + return objectGraph.loc.string("DEVELOPER_APPS"); + } + case "arcade-apps": + return objectGraph.loc.string("DEVELOPER_APPLE_ARCADE"); + default: + return null; + } +} +/** + * Returns the web shelf title for a given relationship. + * @param relationshipKey The relationship key. + * @param developerData Media API data for the developer page. + * @returns The localized shelf title. + */ +function webShelfTitle(objectGraph, relationshipKey, developerData) { + switch (relationshipKey) { + case "latest-release-app": + return objectGraph.loc.string("DEVELOPER_LATEST_RELEASE"); + case "system-apps": + return objectGraph.loc.string("DEVELOPER_SYSTEM_APPS"); + case "imessage-apps": + return objectGraph.loc.string("DEVELOPER_IMESSAGE"); + case "watch-apps": + return objectGraph.loc.string("DEVELOPER_WATCH"); + case "atv-apps": + return objectGraph.loc.string("DEVELOPER_TV"); + case "app-bundles": + return objectGraph.loc.string("DEVELOPER_BUNDLES"); + case "xros-apps": + return objectGraph.loc.string("DEVELOPER_VISION"); + case "ios-apps": + return objectGraph.loc.string("DEVELOPER_PHONE_PAD_APPS"); + case "arcade-apps": + return objectGraph.loc.string("DEVELOPER_APPLE_ARCADE"); + case "mac-apps": + return objectGraph.loc.string("DEVELOPER_MAC_APPS"); + case "mac-os-compatible-ios-apps": + return objectGraph.loc.string("DEVELOPER_PHONE_PAD_APPS"); + default: + return null; + } +} +function orderedShelvesForDeveloperData(objectGraph, developerData, developerRelationshipOrdering, metricsPageInformation, locationTracker) { + var _a, _b; + if (objectGraph.host.isiOS) { + // Filter duplicate Arcade apps on iOS + filterDuplicateApps(developerData, "arcade-apps", ["ios-apps", "atv-apps"]); + } + let shelfContentType; + let artworkUseCase; + switch (objectGraph.client.deviceType) { + case "tv": + shelfContentType = "mediumLockup"; + artworkUseCase = 2 /* content.ArtworkUseCase.LockupIconMedium */; + break; + case "web": + shelfContentType = "mediumLockup"; + artworkUseCase = 2 /* content.ArtworkUseCase.LockupIconMedium */; + break; + default: + shelfContentType = "smallLockup"; + artworkUseCase = 1 /* content.ArtworkUseCase.LockupIconSmall */; + break; + } + let shelfCount = 0; + const shelves = []; + for (const relationship of developerRelationshipOrdering) { + const dataContainer = mediaRelationship.relationship(developerData, relationship); + const sectionData = serverData.asArrayOrEmpty(dataContainer, "data"); + const contentCount = sectionData.length; + if (contentCount === 0) { + continue; + } + // Skip the latest release shelf if there are no items + // Note: This typically happens on the Apple developer page + // if the latest release is a system app that you're not eligible for + if (relationship === "latest-release-app" && contentCount === 0) { + continue; + } + // Setup some content specific options + let clientIdentifier; + if (relationship === "imessage-apps") { + clientIdentifier = client.messagesIdentifier; + } + else if (relationship === "watch-apps") { + clientIdentifier = client.watchIdentifier; + } + else if (relationship === "atv-apps") { + clientIdentifier = client.tvIdentifier; + } + else { + clientIdentifier = client.appStoreIdentifier; + } + // Determine the title + let shelfTitle; + if (objectGraph.client.isWeb) { + shelfTitle = webShelfTitle(objectGraph, relationship, developerData); + } + else { + shelfTitle = iosShelfTitle(objectGraph, relationship, developerData); + } + // Create a metrics context + metricsHelpersLocation.pushContentLocation(objectGraph, { + pageInformation: metricsPageInformation, + locationTracker: locationTracker, + idType: "sequential", + id: `${shelfCount}`, + targetType: "swoosh", + }, shelfTitle); + // Create the shelf + const listOptions = { + lockupOptions: { + metricsOptions: { + pageInformation: metricsPageInformation, + locationTracker: locationTracker, + }, + clientIdentifierOverride: clientIdentifier, + artworkUseCase: artworkUseCase, + }, + filter: 76532 /* filtering.Filter.DeveloperPage */, + }; + // Determine the ids we need to load + const remainingData = sectionData.filter((data) => { + return serverData.isNullOrEmpty(data.attributes); + }); + // Vision doesn't support See All on developer page shelves currently. + const shouldShowSeeAll = (((_a = dataContainer.next) === null || _a === void 0 ? void 0 : _a.length) > 0 || contentCount >= seeAllThreshold) && + !(objectGraph.client.isVision || objectGraph.client.isWeb); + const shelf = shelfForData(objectGraph, shelfTitle, developerData.id, sectionData, relationship, shelfContentType, listOptions, metricsPageInformation, locationTracker, dataContainer.href, shouldShowSeeAll); + // Ensure we're not too high + const itemCount = shelf.items.length + remainingData.length; + if (objectGraph.client.isVision) { + if (itemCount < 5) { + shelf.rowsPerColumn = 1; + } + else if (itemCount < 10) { + shelf.rowsPerColumn = 2; + } + else { + shelf.rowsPerColumn = 3; + } + } + else if (objectGraph.client.isWeb) { + shelf.rowsPerColumn = itemCount > 3 ? 2 : 1; + } + else if (itemCount < 3) { + shelf.rowsPerColumn = itemCount; + } + // Add metrics before serializing token for url + const shelfMetricsOptions = { + id: null, + kind: null, + softwareType: null, + targetType: "swoosh", + title: shelf.title, + pageInformation: metricsPageInformation, + locationTracker: locationTracker, + idType: null, + }; + metricsHelpersLocation.popLocation(locationTracker); + metricsHelpersImpressions.addImpressionFields(objectGraph, shelf, shelfMetricsOptions); + metricsHelpersLocation.nextPosition(locationTracker); + if (remainingData.length) { + const token = new DeveloperPageShelfToken(); + token.title = shelfTitle; + token.developerId = developerData.id; + token.contentType = shelfContentType; + token.remainingData = remainingData; + token.lockupListOptions = listOptions; + token.relationship = relationship; + token.roomUrl = dataContainer.href; + token.shouldShowSeeAll = shouldShowSeeAll; + token.hasExistingContent = serverData.isDefinedNonNullNonEmpty(shelf.items); + shelf.url = + `${Protocol.internal}:/${Path.developer}/${Path.shelf}/` + encodeURIComponent(JSON.stringify(token)); + } + // Don't add the shelf if there are no items in it, and there is no more content to fetch. + if (shelf.items.length > 0 || ((_b = shelf.url) === null || _b === void 0 ? void 0 : _b.length) > 0) { + shelves.push(shelf); + shelfCount++; + } + } + return shelves; +} +/** + * Returns the macOS shelf title for a given relationship. + * @param relationshipKey The relationship key. + * @param developerData Media API data for the developer page. + * @returns The localized shelf title. + */ +function macosShelfTitle(objectGraph, relationshipKey, developerData) { + switch (relationshipKey) { + case "latest-release-app": + return objectGraph.loc.string("DEVELOPER_LATEST_RELEASE"); + case "app-bundles": + return objectGraph.loc.string("DEVELOPER_BUNDLES"); + case "mac-apps": + if (objectGraph.appleSilicon.isSupportEnabled) { + return objectGraph.loc.string("DEVELOPER_MAC_APPS"); + } + else { + const hasApps = mediaAttributes.attributeAsBooleanOrFalse(developerData, "hasApps"); + const hasGames = mediaAttributes.attributeAsBooleanOrFalse(developerData, "hasGames"); + if (hasApps && hasGames) { + return objectGraph.loc.string("DEVELOPER_APPS_AND_GAMES"); + } + else if (hasGames) { + return objectGraph.loc.string("DEVELOPER_GAMES"); + } + else { + return objectGraph.loc.string("DEVELOPER_APPS"); + } + } + case "mac-os-compatible-ios-apps": + return objectGraph.loc.string("DEVELOPER_PHONE_PAD_APPS"); + case "arcade-apps": + return objectGraph.loc.string("DEVELOPER_APPLE_ARCADE"); + default: + return null; + } +} +function macosShelvesForDeveloperData(objectGraph, developerData, metricsPageInformation, locationTracker) { + var _a; + // Filter duplicate apps + if (objectGraph.appleSilicon.isSupportEnabled) { + filterDuplicateApps(developerData, "arcade-apps", ["mac-apps", "mac-os-compatible-ios-apps"]); + filterDuplicateApps(developerData, "mac-apps", ["mac-os-compatible-ios-apps"]); + } + else { + filterDuplicateApps(developerData, "arcade-apps", ["mac-apps"]); + } + const shelfContentType = "smallLockup"; + const artworkUseCase = 1 /* content.ArtworkUseCase.LockupIconSmall */; + let shelfCount = 0; + const shelves = []; + for (const relationship of macosDeveloperRelationshipOrder) { + const dataContainer = mediaRelationship.relationship(developerData, relationship); + const sectionData = serverData.asArrayOrEmpty(dataContainer, "data"); + const contentCount = sectionData.length; + if (contentCount === 0) { + continue; + } + // Skip the latest release shelf if there are no items + // Note: This typically happens on the Apple developer page + // if the latest release is a system app that you're not eligible for + if (relationship === "latest-release-app" && contentCount === 0) { + continue; + } + // Determine the title + const shelfTitle = macosShelfTitle(objectGraph, relationship, developerData); + // Create a metrics context + metricsHelpersLocation.pushContentLocation(objectGraph, { + pageInformation: metricsPageInformation, + locationTracker: locationTracker, + idType: "sequential", + id: `${shelfCount}`, + targetType: "swoosh", + }, shelfTitle); + // Create the shelf + const listOptions = { + lockupOptions: { + metricsOptions: { + pageInformation: metricsPageInformation, + locationTracker: locationTracker, + }, + artworkUseCase: artworkUseCase, + }, + filter: 76532 /* filtering.Filter.DeveloperPage */, + }; + // Determine the ids we need to load + const remainingData = sectionData.filter((data) => { + return serverData.isNullOrEmpty(data.attributes); + }); + const shouldShowSeeAll = ((_a = dataContainer.next) === null || _a === void 0 ? void 0 : _a.length) > 0 || contentCount >= seeAllThreshold; + const shelf = shelfForData(objectGraph, shelfTitle, developerData.id, sectionData, relationship, shelfContentType, listOptions, metricsPageInformation, locationTracker, dataContainer.href, shouldShowSeeAll); + // Ensure we're not too high + const itemCount = shelf.items.length + remainingData.length; + if (itemCount < 3) { + shelf.rowsPerColumn = itemCount; + } + // Add metrics before serializing token for url + const shelfMetricsOptions = { + id: null, + kind: null, + softwareType: null, + targetType: "swoosh", + title: shelf.title, + pageInformation: metricsPageInformation, + locationTracker: locationTracker, + idType: null, + }; + metricsHelpersLocation.popLocation(locationTracker); + metricsHelpersImpressions.addImpressionFields(objectGraph, shelf, shelfMetricsOptions); + metricsHelpersLocation.nextPosition(locationTracker); + if (remainingData.length) { + const token = new DeveloperPageShelfToken(); + token.title = shelfTitle; + token.developerId = developerData.id; + token.contentType = "smallLockup"; + token.remainingData = remainingData; + token.lockupListOptions = listOptions; + token.relationship = relationship; + token.roomUrl = dataContainer.href; + token.shouldShowSeeAll = shouldShowSeeAll; + token.hasExistingContent = serverData.isDefinedNonNullNonEmpty(shelf.items); + shelf.url = + `${Protocol.internal}:/${Path.developer}/${Path.shelf}/` + encodeURIComponent(JSON.stringify(token)); + } + // Don't add the shelf if there are no items in it + if (shelf.items.length > 0) { + shelves.push(shelf); + shelfCount++; + } + } + return shelves; +} +/** + * Creates a list of apps for the given relationship type. + */ +function flatShelvesForDeveloperData(objectGraph, contentType, developerData, relationshipType, shelfTitle, metricsPageInformation, locationTracker) { + const dataCollection = mediaRelationship.relationshipCollection(developerData, relationshipType); + const listOptions = { + lockupOptions: { + metricsOptions: { + pageInformation: metricsPageInformation, + locationTracker: locationTracker, + }, + artworkUseCase: 1 /* content.ArtworkUseCase.LockupIconSmall */, + }, + filter: 76532 /* filtering.Filter.DeveloperPage */, + }; + // Create a metrics context + metricsHelpersLocation.pushContentLocation(objectGraph, { + pageInformation: metricsPageInformation, + locationTracker: locationTracker, + idType: "sequential", + id: `0`, + targetType: "swoosh", + }, shelfTitle); + const shelf = shelfForData(objectGraph, shelfTitle, developerData.id, dataCollection, relationshipType, contentType, listOptions, metricsPageInformation, locationTracker, null, false); + // Determine the ids we need to load + const remainingData = dataCollection.filter((data) => { + return serverData.isNullOrEmpty(data.attributes); + }); + // Add metrics before serializing token for url + const shelfMetricsOptions = { + id: null, + kind: null, + softwareType: null, + targetType: "swoosh", + title: shelfTitle, + pageInformation: metricsPageInformation, + locationTracker: locationTracker, + idType: null, + }; + metricsHelpersLocation.popLocation(locationTracker); + metricsHelpersImpressions.addImpressionFields(objectGraph, shelf, shelfMetricsOptions); + metricsHelpersLocation.nextPosition(locationTracker); + if (remainingData.length) { + const token = new DeveloperPageShelfToken(); + token.title = shelfTitle; + token.developerId = developerData.id; + token.contentType = contentType; + token.remainingData = remainingData; + token.lockupListOptions = listOptions; + token.hasExistingContent = serverData.isDefinedNonNullNonEmpty(shelf.items); + shelf.url = + `${Protocol.internal}:/${Path.developer}/${Path.shelf}/` + encodeURIComponent(JSON.stringify(token)); + } + return [shelf]; +} +export function shelfForData(objectGraph, title, developerId, dataArray, developerRelationship, contentType, listOptions, pageInformation, locationTracker, roomUrl, includeSeeAll) { + const shelf = new models.Shelf(contentType); + shelf.title = title; + switch (contentType) { + case "screenshotsLockup": + shelf.items = lockups.screenshotsLockupsFromData(objectGraph, dataArray, listOptions); + shelf.isHorizontal = false; + shelf.presentationHints = { showSupplementaryText: false }; + break; + case "smallLockup": + default: + shelf.items = lockups.lockupsFromData(objectGraph, dataArray, listOptions); + shelf.isHorizontal = objectGraph.client.deviceType !== "watch"; + break; + } + if (includeSeeAll) { + // Create token for the see all room + const roomToken = new DeveloperRoomToken(); + roomToken.title = title; + roomToken.url = roomUrl; + roomToken.developerId = developerId; + roomToken.relationshipId = developerRelationship; + roomToken.clientIdentifierOverride = listOptions.lockupOptions.clientIdentifierOverride; + const seeAllAction = new models.FlowAction("page"); + seeAllAction.pageUrl = developerRoomUrlWithToken(objectGraph, roomToken); + seeAllAction.title = objectGraph.loc.string("ACTION_SEE_ALL"); + seeAllAction.pageData = room.seeAllPage(objectGraph, title); + metricsHelpersClicks.addClickEventToSeeAllAction(objectGraph, seeAllAction, seeAllAction.pageUrl, { + pageInformation, + locationTracker, + }); + shelf.seeAllAction = seeAllAction; + } + return shelf; +} +/** + * Determines the URL to use for the developer room page. + * @param {string} token The token to use. + * @returns {string} The string to use for the developer room page. + */ +function developerRoomUrlWithToken(objectGraph, token) { + if (!serverData.isDefinedNonNull(token)) { + return null; + } + return `${Protocol.internal}:/${Path.developer}/${Path.room}/` + encodeURIComponent(JSON.stringify(token)); +} +/** + * Prune duplicate entries for apps that can appear in other shelves by modifying `developerData` in place. + * For example if `arcade-apps` relation contains id `12345`, we want to prune `12345` from shelves like "Games" and "Apple TV" + */ +function filterDuplicateApps(developerData, authoritativeRelationshipKey, filteredRelationshipKeys) { + const arcadeRelationship = mediaRelationship.relationship(developerData, authoritativeRelationshipKey); + if (serverData.isNull(arcadeRelationship)) { + return; // No arcade relationship + } + const arcadeApps = mediaDataStructure.dataCollectionFromDataContainer(arcadeRelationship); + if (serverData.isNull(arcadeApps)) { + return; // No arcade relationship data + } + // IDs to filter from other shelves since they are in arcade shelf + const arcadeAppIds = arcadeApps.map((app) => app.id); + for (const relationshipKeyToFilter of filteredRelationshipKeys) { + const relationshipToFilter = mediaRelationship.relationship(developerData, relationshipKeyToFilter); + if (serverData.isNull(relationshipToFilter)) { + continue; // Skip if relationship didn't exist + } + const relationshipDataToFilter = mediaDataStructure.dataCollectionFromDataContainer(relationshipToFilter); + if (serverData.isNull(relationshipToFilter)) { + continue; // Skip if relationship had no `data` + } + // Overwrite with filtered data + developerData.relationships[relationshipKeyToFilter].data = relationshipDataToFilter.filter((data) => !arcadeAppIds.includes(data.id)); + } +} +//# sourceMappingURL=developer-common.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/developer/developer-request.js b/node_modules/@jet-app/app-store/tmp/src/common/developer/developer-request.js new file mode 100644 index 0000000..88c7e74 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/developer/developer-request.js @@ -0,0 +1,115 @@ +import { Request, defaultAdditionalPlatformsForClient } from "../../foundation/media/data-fetching"; +import { shouldFetchCustomAttributes } from "../product-page/product-page-variants"; +import { shouldUsePrerenderedIconArtwork } from "../content/content"; +/** + * Returns the attributes to use for a developer media API request. + */ +export function developerAttributes(objectGraph) { + const attributes = ["editorialArtwork", "editorialVideo", "requiredCapabilities", "minimumOSVersion"]; + if (objectGraph.client.isMac) { + attributes.push("screenshotsByType"); + } + else { + attributes.push("isAppleWatchSupported"); + } + if (objectGraph.appleSilicon.isSupportEnabled) { + attributes.push("macRequiredCapabilities"); + } + if (objectGraph.client.isMac) { + attributes.push("hasMacIPAPackage"); + } + if (objectGraph.client.isVision) { + attributes.push("compatibilityControllerRequirement"); + } + if (objectGraph.bag.enableUpdatedAgeRatings) { + attributes.push("ageRating"); + } + if (shouldUsePrerenderedIconArtwork(objectGraph)) { + attributes.push("iconArtwork"); + } + return attributes; +} +export const iosDeveloperRelationshipKeys = [ + "latest-release-app", + "system-apps", + "arcade-apps", + "app-bundles", + "ios-apps", + "imessage-apps", + "watch-apps", + "atv-apps", +]; +export const visionOSDeveloperRelationshipKeys = [ + "latest-release-app", + "xros-apps", + "arcade-apps", + "ios-apps", +]; +export const macosDeveloperRelationshipKeys = [ + "latest-release-app", + "arcade-apps", + "app-bundles", + "mac-apps", +]; +export const webDeveloperRelationshipKeys = [ + "latest-release-app", + "system-apps", + "arcade-apps", + "app-bundles", + "ios-apps", + "imessage-apps", + "watch-apps", + "atv-apps", + "xros-apps", + "mac-apps", +]; +export const watchosDeveloperRelationshipKey = "watch-apps"; +function developerRelationships(objectGraph) { + let relationships = []; + switch (objectGraph.client.deviceType) { + case "mac": + relationships = relationships.concat(macosDeveloperRelationshipKeys); + if (objectGraph.appleSilicon.isSupportEnabled) { + relationships.push("mac-os-compatible-ios-apps"); + } + break; + case "watch": + relationships.push(watchosDeveloperRelationshipKey); + break; + case "vision": + relationships = relationships.concat(visionOSDeveloperRelationshipKeys); + break; + case "web": + relationships = relationships.concat(webDeveloperRelationshipKeys); + break; + default: + relationships = relationships.concat(iosDeveloperRelationshipKeys); + break; + } + return relationships; +} +/** + * Creates a {@linkcode Request} for a "developer" page with the given {@linkcode id} + */ +export function makeDeveloperRequest(objectGraph, id) { + const request = new Request(objectGraph).withIdOfType(id, "developers"); + return addDeveloperRequestProperties(objectGraph, request); +} +/** + * Add the expected request attributes (relationships, platforms, etc) to a request + * for a "developer" resource + */ +export function addDeveloperRequestProperties(objectGraph, request) { + request + .includingAdditionalPlatforms(defaultAdditionalPlatformsForClient(objectGraph)) + .includingRelationships(developerRelationships(objectGraph)) + .includingAttributes(developerAttributes(objectGraph)) + .includingMacOSCompatibleIOSAppsWhenSupported() + .usingCustomAttributes(shouldFetchCustomAttributes(objectGraph)); + if (objectGraph.client.isWeb) { + // The "web" client needs to load *all* of the data for SEO purposes + request.addingQuery("sparseLimit[developers:ios-apps]", "40"); + } + return request; +} +//# sourceMappingURL=developer-request.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-action-util.js b/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-action-util.js new file mode 100644 index 0000000..9c50926 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-action-util.js @@ -0,0 +1,58 @@ +import * as lockups from "../lockups/lockups"; +import { CollectionShelfDisplayStyle } from "./editorial-page-types"; +/** + * For a given piece of MAPI data create the primary clickAction for the component with this data + * @param objectGraph + * @param data + * @param shelfToken + */ +export function createPrimaryActionForComponentFromData(objectGraph, data, shelfToken, baseClickOptions) { + const clickOptions = createClickOptionsFromData(objectGraph, data, shelfToken, baseClickOptions); + const primaryAction = lockups.actionFromData(objectGraph, data, clickOptions, shelfToken.clientIdentifierOverride); + return primaryAction; +} +function createClickOptionsFromData(objectGraph, data, shelfToken, baseClickOptions) { + const targetType = clickTargetForCollectionDisplayStyle(objectGraph, shelfToken.collectionDisplayStyle); + const clickOptions = { + ...shelfToken.metricsImpressionOptions, + ...baseClickOptions, + id: data.id, + targetType: targetType, + }; + return clickOptions; +} +export function clickTargetForCollectionDisplayStyle(objectGraph, displayStyle) { + switch (displayStyle) { + case CollectionShelfDisplayStyle.Hero: + return "hero"; + case CollectionShelfDisplayStyle.TextOnly: + return "textOnly"; + case CollectionShelfDisplayStyle.TextWithArtwork: + return "textWithArtwork"; + case CollectionShelfDisplayStyle.BrickSmall: + return "brickSmall"; + case CollectionShelfDisplayStyle.BrickMedium: + return "brickMedium"; + case CollectionShelfDisplayStyle.BrickLarge: + return "brickLarge"; + case CollectionShelfDisplayStyle.EditorialLockupMedium: + case CollectionShelfDisplayStyle.EditorialLockupMediumVariant: + return "editorialLockupMedium"; + case CollectionShelfDisplayStyle.EditorialLockupLarge: + case CollectionShelfDisplayStyle.EditorialLockupLargeVariant: + return "editorialLockupLarge"; + case CollectionShelfDisplayStyle.LockupSmall: + return "lockupSmall"; + case CollectionShelfDisplayStyle.LockupLarge: + return "lockupLarge"; + case CollectionShelfDisplayStyle.StorySmall: + return "storySmall"; + case CollectionShelfDisplayStyle.StoryMedium: + return "storyMedium"; + case CollectionShelfDisplayStyle.BreakoutLarge: + return "largeBreakout"; + default: + return "lockup"; + } +} +//# sourceMappingURL=editorial-action-util.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-data-util.js b/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-data-util.js new file mode 100644 index 0000000..041bde4 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-data-util.js @@ -0,0 +1,173 @@ +import * as serverData from "../../foundation/json-parsing/server-data"; +import * as mediaAttributes from "../../foundation/media/attributes"; +import * as mediaRelationship from "../../foundation/media/relationships"; +import * as content from "../content/content"; +import { EditorialShelfType } from "./editorial-page-types"; +/** + * Take the remaining content fromt he shelfToken if this is not the first render, otherwise + * use the initial data. + * + * @param objectGraph + * @param shelfToken + */ +export function extractRemainingItemsOrInitialShelfContents(objectGraph, shelfToken) { + if (!shelfToken.isFirstRender) { + return serverData.isDefinedNonNullNonEmpty(shelfToken.remainingItems) ? shelfToken.remainingItems : []; + } + const initialShelfContent = mediaRelationship.relationshipCollection(shelfToken.data, "contents"); + return serverData.isDefinedNonNullNonEmpty(initialShelfContent) ? initialShelfContent : []; +} +/** + * Find the related product data from some MAPI data that was programmed on an editorial page. + * + * @param objectGraph + * @param data + * @returns the product MAPI data from the correct relationship or the data itself if the supplied data is already a product + */ +export function extractProductData(objectGraph, data) { + let productData = null; + switch (data.type) { + case "editorial-items": + case "editorial-pages": + case "editorial-shelves-header": + productData = + mediaRelationship.relationshipData(objectGraph, data, "primary-content") || + mediaRelationship.relationshipData(objectGraph, data, "card-contents") || + mediaRelationship.relationshipData(objectGraph, data, "contents") || + mediaRelationship.relationshipData(objectGraph, data, "app"); + break; + case "apps": + case "in-apps": + case "app-bundles": + productData = data; + break; + default: + break; + } + return productData; +} +/** + * Find the related collection of product data from some MAPI data that was programmed on an editorial page. + * + * @param objectGraph + * @param data + * @returns collection of related content for in the relationship of the supplied MAPI data + */ +export function extractCollectionData(objectGraph, data) { + let collectionData = null; + switch (data.type) { + case "editorial-items": + case "editorial-pages": + case "editorial-shelves-header": + collectionData = + mediaRelationship.relationshipCollection(data, "primary-contents", true) || + mediaRelationship.relationshipCollection(data, "card-contents", true) || + mediaRelationship.relationshipCollection(data, "contents", true); + break; + case "tags": + collectionData = serverData.asArray(data.meta, "associations.apps.data"); + break; + default: + collectionData = null; + } + return collectionData; +} +/** + * Find the related editorial card used for determining override information + * + * @param objectGraph + * @param data + * @returns The editorial-card data object to use for overrides + */ +export function extractEditorialCardData(objectGraph, data) { + if (serverData.isNullOrEmpty(data)) { + return null; + } + const editorialCardsData = serverData.asArrayOrEmpty(data.meta, "associations.editorial-cards.data"); + if (serverData.isNullOrEmpty(editorialCardsData)) { + return null; + } + return editorialCardsData[0]; +} +/** + * Find the related editorial client params from this data object + * + * @param objectGraph + * @param data + * @returns The EditorialClientParams data object to use for this component + */ +export function extractEditorialClientParams(objectGraph, data) { + const baseEditorialClientParams = mediaAttributes.attributeAsDictionary(data, "editorialClientParams", {}); + const editorialCardData = extractEditorialCardData(objectGraph, data); + const cardEditorialClientParams = mediaAttributes.attributeAsDictionary(editorialCardData, "editorialClientParams", {}); + return serverData.asInterface({ + ...baseEditorialClientParams, + ...cardEditorialClientParams, + }); +} +/** + * Use the client params to create the display options for this component + * + * @param clientParams + * @returns The editorialDisplayOptions data object to use for this component + */ +export function editorialDisplayOptionsFromClientParams(clientParams) { + return { + showAppIcon: serverData.asBooleanWithDefault(clientParams.showAppIcon, false), + suppressLockup: serverData.asBooleanWithDefault(clientParams.suppressLockup, false), + useGeneratedBackground: serverData.asBooleanWithDefault(clientParams.useGeneratedBackground, false), + useMaterialBlur: serverData.asBooleanWithDefault(clientParams.useMaterialBlur, true), + useTextProtectionColor: serverData.asBooleanWithDefault(clientParams.useTextProtectionColor, false), + }; +} +/** + * Get the primary content for this MAPI data object, depending on the type it is. If the data + * does not have any attributes this method will return null, since we'll need to fetch it. + * @param objectGraph + * @param data + */ +export function extractHydratedPrimaryContentData(objectGraph, data) { + if (!mediaAttributes.hasAttributes(data)) { + return null; + } + let primaryData; + switch (data.type) { + case "editorial-items": + primaryData = content.primaryContentForData(objectGraph, data); + break; + default: + primaryData = data; + break; + } + if (!mediaAttributes.hasAttributes(primaryData)) { + return null; + } + return primaryData; +} +/** + * Method to check whether we need the primary content relationship for a piece of content + * to be rendered + * + * @param objectGraph + * @param data The MAPI date we're checking for + */ +export function requiresPrimaryContent(objectGraph, data) { + const linkData = mediaAttributes.attributeAsDictionary(data, "link"); + const isLinkAction = serverData.isDefinedNonNullNonEmpty(linkData); + const isStoryAction = mediaAttributes.attributeAsBooleanOrFalse(data, "isCanvasAvailable"); + return !isLinkAction && !isStoryAction; +} +/** + * Extracts the collection shelf display style, for any collection / recommendation shelf + * @param objectGraph Current object graph + * @param shelfData The input shelf data + * @returns The collection shelf display style, if any + */ +export function collectionShelfDisplayStyleFromShelfData(objectGraph, shelfData) { + const shelfType = shelfData.type; + if (shelfType !== EditorialShelfType.Collection && shelfType !== EditorialShelfType.Recommendations) { + return null; + } + return mediaAttributes.attributeAsString(shelfData, "displayStyle"); +} +//# sourceMappingURL=editorial-data-util.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-media-util.js b/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-media-util.js new file mode 100644 index 0000000..218f74f --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-media-util.js @@ -0,0 +1,511 @@ +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 mediaPlatformAttributes from "../../foundation/media/platform-attributes"; +import * as videoDefaults from "../constants/video-constants"; +import * as contentAttributes from "../content/attributes"; +import * as content from "../content/content"; +import * as editorialDataUtil from "./editorial-data-util"; +import * as color from "../../foundation/util/color-util"; +/** + * The flavors for editorial art that can be used + */ +export var EditorialMediaFlavor; +(function (EditorialMediaFlavor) { + EditorialMediaFlavor["StoryCenteredMotion16x9"] = "storyCenteredMotion16x9"; + EditorialMediaFlavor["StoryCenteredStatic16x9"] = "storyCenteredStatic16x9"; + EditorialMediaFlavor["StoryDetailMotion3x4"] = "storyDetailMotion3x4"; + EditorialMediaFlavor["StoryDetailStatic3x4"] = "storyDetailStatic3x4"; + EditorialMediaFlavor["HeroMotion16x9"] = "heroMotion16x9"; + EditorialMediaFlavor["HeroStatic16x9"] = "heroStatic16x9"; + EditorialMediaFlavor["HeroMotionRTL16x9"] = "heroMotionRTL16x9"; + EditorialMediaFlavor["HeroStaticRTL16x9"] = "heroStaticRTL16x9"; + EditorialMediaFlavor["LargeBreakoutMotion16x9"] = "largeBreakoutMotion16x9"; + EditorialMediaFlavor["LargeBreakoutStatic16x9"] = "largeBreakoutStatic16x9"; + EditorialMediaFlavor["LargeBreakoutRTLMotion16x9"] = "largeBreakoutRTLMotion16x9"; + EditorialMediaFlavor["LargeBreakoutRTLStatic16x9"] = "largeBreakoutRTLStatic16x9"; + EditorialMediaFlavor["StoryCardMotion3x4"] = "storyCardMotion3x4"; + EditorialMediaFlavor["StoryCardStatic3x4"] = "storyCardStatic3x4"; + EditorialMediaFlavor["StorySearchStatic16x9"] = "storySearchStatic16x9"; + EditorialMediaFlavor["SubscriptionHero"] = "subscriptionHero"; + EditorialMediaFlavor["UniversalAMotion16x9"] = "universalAMotion16x9"; + EditorialMediaFlavor["UniversalAStatic16x9"] = "universalAStatic16x9"; + EditorialMediaFlavor["BrickStatic16x9"] = "brickStatic16x9"; + EditorialMediaFlavor["BrickStaticRTL16x9"] = "brickStaticRTL16x9"; + EditorialMediaFlavor["SearchCategoryBrick"] = "searchCategoryBrick"; +})(EditorialMediaFlavor || (EditorialMediaFlavor = {})); +/** + * The different locations for where editorial art is displayed + */ +export var EditorialMediaPlacement; +(function (EditorialMediaPlacement) { + EditorialMediaPlacement["Hero"] = "hero"; + EditorialMediaPlacement["LargeBreakout"] = "largeBreakout"; + EditorialMediaPlacement["StoryCard"] = "storyCard"; + EditorialMediaPlacement["StoryDetail"] = "storyDetail"; + EditorialMediaPlacement["StoryDetailLandscape"] = "storyDetailLandscape"; + EditorialMediaPlacement["Search"] = "search"; + EditorialMediaPlacement["Brick"] = "brick"; + EditorialMediaPlacement["EditorialLockup"] = "editorialLockup"; + EditorialMediaPlacement["EditorialPage"] = "editorialPage"; +})(EditorialMediaPlacement || (EditorialMediaPlacement = {})); +/** + * The mapping between the various placements and the preferred flavor for that location + */ +const preferredEditorialMediaFlavorMap = { + hero: [ + EditorialMediaFlavor.HeroMotion16x9, + EditorialMediaFlavor.HeroStatic16x9, + EditorialMediaFlavor.StoryCenteredMotion16x9, + EditorialMediaFlavor.StoryCenteredStatic16x9, + EditorialMediaFlavor.UniversalAMotion16x9, + EditorialMediaFlavor.UniversalAStatic16x9, + ], + largeBreakout: [ + EditorialMediaFlavor.LargeBreakoutMotion16x9, + EditorialMediaFlavor.LargeBreakoutStatic16x9, + EditorialMediaFlavor.StoryCenteredMotion16x9, + EditorialMediaFlavor.StoryCenteredStatic16x9, + EditorialMediaFlavor.UniversalAMotion16x9, + EditorialMediaFlavor.UniversalAStatic16x9, + ], + storyCard: [ + EditorialMediaFlavor.StoryCardMotion3x4, + EditorialMediaFlavor.StoryCardStatic3x4, + EditorialMediaFlavor.StoryCenteredMotion16x9, + EditorialMediaFlavor.StoryCenteredStatic16x9, + EditorialMediaFlavor.UniversalAMotion16x9, + EditorialMediaFlavor.UniversalAStatic16x9, + ], + storyDetail: [ + EditorialMediaFlavor.StoryDetailMotion3x4, + EditorialMediaFlavor.StoryDetailStatic3x4, + EditorialMediaFlavor.StoryCenteredMotion16x9, + EditorialMediaFlavor.StoryCenteredStatic16x9, + EditorialMediaFlavor.UniversalAMotion16x9, + EditorialMediaFlavor.UniversalAStatic16x9, + ], + storyDetailLandscape: [ + EditorialMediaFlavor.StoryCenteredMotion16x9, + EditorialMediaFlavor.StoryCenteredStatic16x9, + EditorialMediaFlavor.UniversalAMotion16x9, + EditorialMediaFlavor.UniversalAStatic16x9, + EditorialMediaFlavor.StoryDetailMotion3x4, + EditorialMediaFlavor.StoryDetailStatic3x4, + ], + search: [ + EditorialMediaFlavor.StorySearchStatic16x9, + EditorialMediaFlavor.StoryCenteredStatic16x9, + EditorialMediaFlavor.UniversalAStatic16x9, + ], + brick: [ + EditorialMediaFlavor.BrickStatic16x9, + EditorialMediaFlavor.HeroStatic16x9, + EditorialMediaFlavor.StoryCenteredStatic16x9, + EditorialMediaFlavor.UniversalAStatic16x9, + EditorialMediaFlavor.SearchCategoryBrick, + ], + editorialLockup: [EditorialMediaFlavor.SubscriptionHero], + editorialPage: [ + EditorialMediaFlavor.StoryCardStatic3x4, + EditorialMediaFlavor.HeroStatic16x9, + EditorialMediaFlavor.StoryCenteredStatic16x9, + EditorialMediaFlavor.SubscriptionHero, + ], +}; +/** + * The mapping between the various placements and the preferred flavor for that location, for RTL flavors + */ +const preferredEditorialMediaRTLFlavorMap = { + hero: [ + EditorialMediaFlavor.HeroMotionRTL16x9, + EditorialMediaFlavor.HeroStaticRTL16x9, + EditorialMediaFlavor.StoryCenteredMotion16x9, + EditorialMediaFlavor.StoryCenteredStatic16x9, + EditorialMediaFlavor.UniversalAMotion16x9, + EditorialMediaFlavor.UniversalAStatic16x9, + ], + largeBreakout: [ + EditorialMediaFlavor.LargeBreakoutRTLMotion16x9, + EditorialMediaFlavor.LargeBreakoutRTLStatic16x9, + EditorialMediaFlavor.StoryCenteredMotion16x9, + EditorialMediaFlavor.StoryCenteredStatic16x9, + EditorialMediaFlavor.UniversalAMotion16x9, + EditorialMediaFlavor.UniversalAStatic16x9, + ], + storyCard: [ + EditorialMediaFlavor.StoryCardMotion3x4, + EditorialMediaFlavor.StoryCardStatic3x4, + EditorialMediaFlavor.StoryCenteredMotion16x9, + EditorialMediaFlavor.StoryCenteredStatic16x9, + EditorialMediaFlavor.UniversalAMotion16x9, + EditorialMediaFlavor.UniversalAStatic16x9, + ], + storyDetail: [ + EditorialMediaFlavor.StoryDetailMotion3x4, + EditorialMediaFlavor.StoryDetailStatic3x4, + EditorialMediaFlavor.StoryCenteredMotion16x9, + EditorialMediaFlavor.StoryCenteredStatic16x9, + EditorialMediaFlavor.UniversalAMotion16x9, + EditorialMediaFlavor.UniversalAStatic16x9, + ], + storyDetailLandscape: [ + EditorialMediaFlavor.StoryCenteredMotion16x9, + EditorialMediaFlavor.StoryCenteredStatic16x9, + EditorialMediaFlavor.UniversalAMotion16x9, + EditorialMediaFlavor.UniversalAStatic16x9, + EditorialMediaFlavor.StoryDetailMotion3x4, + EditorialMediaFlavor.StoryDetailStatic3x4, + ], + search: [ + EditorialMediaFlavor.StorySearchStatic16x9, + EditorialMediaFlavor.StoryCenteredStatic16x9, + EditorialMediaFlavor.UniversalAStatic16x9, + ], + brick: [ + EditorialMediaFlavor.BrickStaticRTL16x9, + EditorialMediaFlavor.HeroStatic16x9, + EditorialMediaFlavor.StoryCenteredStatic16x9, + EditorialMediaFlavor.UniversalAStatic16x9, + EditorialMediaFlavor.SearchCategoryBrick, + ], + editorialLockup: [EditorialMediaFlavor.SubscriptionHero], + editorialPage: [ + EditorialMediaFlavor.StoryCardStatic3x4, + EditorialMediaFlavor.HeroStatic16x9, + EditorialMediaFlavor.StoryCenteredStatic16x9, + EditorialMediaFlavor.SubscriptionHero, + ], +}; +/** + * Create the best suitable editorial artwork / video for the given placement, including the additional data that might be + * needed about this artwork + * + * @param objectGraph + * @param data + * @param placement + */ +export function editorialMediaDataForPlacement(objectGraph, data, placement) { + const editorialCardData = editorialDataUtil.extractEditorialCardData(objectGraph, data); + const preferredEditorialMediaFlavors = preferredEditorialMediaFlavorMap[placement]; + const editorialMediaData = editorialMediaDataForPlacementAndFlavors(objectGraph, data, editorialCardData, placement, preferredEditorialMediaFlavors, false); + const preferredEditorialMediaRTLFlavors = preferredEditorialMediaRTLFlavorMap[placement]; + const rtlEditorialMediaData = editorialMediaDataForPlacementAndFlavors(objectGraph, data, editorialCardData, placement, preferredEditorialMediaRTLFlavors, true); + editorialMediaData.rtlArtwork = rtlEditorialMediaData.rtlArtwork; + editorialMediaData.rtlArtworkData = rtlEditorialMediaData.rtlArtworkData; + editorialMediaData.rtlVideo = rtlEditorialMediaData.rtlVideo; + editorialMediaData.rtlBackgroundColor = rtlEditorialMediaData.rtlBackgroundColor; + editorialMediaData.rtlTextColorOverride = rtlEditorialMediaData.rtlTextColorOverride; + return editorialMediaData; +} +/** + * Creates the `EditorialMediaData` object for a specific placement and flavors. + * + * Note: This will only populate either the LTR properties or the RTL properties on the `EditorialMediaData` + * object, never both. Which ones are populated is based on the `isRTL` flag. + * + * @param objectGraph Current object graph + * @param data The data blob for the main item + * @param editorialCardData The data blob for the editorial card + * @param placement The placement for the media + * @param flavors The flavor candidates for the media + * @param isRTL Whether the placement is for RTL media + * @returns A built `EditorialMediaData` object + */ +function editorialMediaDataForPlacementAndFlavors(objectGraph, data, editorialCardData, placement, flavors, isRTL) { + let editorialMediaData = {}; + for (const flavor of flavors) { + // Check the editorial card first for override media + if (serverData.isDefinedNonNull(editorialCardData)) { + editorialMediaData = + editorialVideoFromDataForFlavor(objectGraph, editorialCardData, placement, flavor, isRTL) || + editorialArtworkFromDataForFlavor(objectGraph, editorialCardData, placement, flavor, isRTL); + if (serverData.isDefinedNonNullNonEmpty(editorialMediaData)) { + break; + } + } + // If no override found, then check the main data + editorialMediaData = + editorialVideoFromDataForFlavor(objectGraph, data, placement, flavor, isRTL) || + editorialArtworkFromDataForFlavor(objectGraph, data, placement, flavor, isRTL); + if (serverData.isDefinedNonNullNonEmpty(editorialMediaData)) { + break; + } + } + if (isNothing(editorialMediaData)) { + editorialMediaData = {}; + } + return editorialMediaData; +} +/** + * Create the best suitable editorial artwork for the given flavor, including the additional data that might be + * needed about this artwork, returns null if there is no editorial artwork for this flavor + * + * @param objectGraph + * @param data + * @param placement + * @param flavor + * @param isRTL + */ +function editorialArtworkFromDataForFlavor(objectGraph, data, placement, flavor, isRTL) { + let editorialArtwork = null; + let editorialArtworkData = null; + let editorialArtworkBackgroundColor = null; + const editorialArtworkPath = `editorialArtwork.${flavor}`; + const attributePlatform = contentAttributes.bestAttributePlatformFromData(objectGraph, data); + if (serverData.isNull(attributePlatform)) { + return null; + } + editorialArtworkData = + mediaAttributes.attributeAsDictionary(data, editorialArtworkPath) || + mediaPlatformAttributes.platformAttributeAsDictionary(data, attributePlatform, editorialArtworkPath); + if (!serverData.isDefinedNonNullNonEmpty(editorialArtworkData)) { + return null; + } + editorialArtwork = content.artworkFromApiArtwork(objectGraph, editorialArtworkData, { + withJoeColorPlaceholder: true, + useCase: artworkUseCaseForPlacement(placement), + cropCode: cropCodeForPlacementAndFlavor(placement, flavor, isRTL), + }); + if (serverData.isNull(editorialArtwork)) { + return null; + } + editorialArtworkBackgroundColor = editorialArtwork.backgroundColor; + const textColorOverride = editorialTextColorOverride(objectGraph, editorialArtworkData, editorialArtwork); + if (isRTL) { + return { + rtlArtwork: editorialArtwork, + rtlArtworkData: editorialArtworkData, + rtlBackgroundColor: editorialArtworkBackgroundColor, + rtlTextColorOverride: textColorOverride, + }; + } + else { + return { + artwork: editorialArtwork, + artworkData: editorialArtworkData, + backgroundColor: editorialArtworkBackgroundColor, + textColorOverride: textColorOverride, + }; + } +} +/** + * Create the best suitable editorial artwork for the given flavor, including the additional data that might be + * needed about this artwork, returns null if there is no editorial artwork for this flavor + * + * @param objectGraph + * @param data + * @param placement + * @param flavor + */ +function editorialVideoFromDataForFlavor(objectGraph, data, placement, flavor, isRTL) { + let editorialVideo = null; + let previewFrameArtwork = null; + let editorialVideoData = null; + let editorialVideoBackgroundColor = null; + const editorialVideoPath = `editorialVideo.${flavor}`; + const attributePlatform = contentAttributes.bestAttributePlatformFromData(objectGraph, data); + if (isNothing(attributePlatform)) { + return null; + } + editorialVideoData = + mediaAttributes.attributeAsDictionary(data, editorialVideoPath) || + mediaPlatformAttributes.platformAttributeAsDictionary(data, attributePlatform, editorialVideoPath); + if (serverData.isNullOrEmpty(editorialVideoData)) { + return null; + } + const previewFrameData = serverData.asDictionary(editorialVideoData, "previewFrame"); + if (serverData.isDefinedNonNullNonEmpty(previewFrameData)) { + previewFrameArtwork = content.artworkFromApiArtwork(objectGraph, previewFrameData, { + withJoeColorPlaceholder: true, + useCase: artworkUseCaseForPlacement(placement), + cropCode: "sr", + }); + } + if (serverData.isDefinedNonNullNonEmpty(previewFrameArtwork)) { + previewFrameArtwork.crop = "sr"; + editorialVideoBackgroundColor = previewFrameArtwork.backgroundColor; + } + const videoUrl = serverData.asString(editorialVideoData, "video"); + if (serverData.isDefinedNonNullNonEmpty(previewFrameArtwork) && isSome(videoUrl) && videoUrl.length > 0) { + editorialVideo = new models.Video(videoUrl, previewFrameArtwork, { + canPlayFullScreen: false, + allowsAutoPlay: true, + looping: true, + playbackControls: videoPlaybackControls(objectGraph, placement), + autoPlayPlaybackControls: {}, + }); + editorialVideo.editorialMediaFlavor = flavor; + editorialVideo.editorialMediaPlacement = placement; + } + if (isNothing(previewFrameData) || isNothing(previewFrameArtwork)) { + return null; + } + const textColorOverride = editorialTextColorOverride(objectGraph, previewFrameData, previewFrameArtwork); + if (isRTL) { + return { + rtlVideo: editorialVideo, + rtlArtwork: previewFrameArtwork, + rtlArtworkData: previewFrameData, + rtlBackgroundColor: editorialVideoBackgroundColor, + rtlTextColorOverride: textColorOverride, + }; + } + else { + return { + video: editorialVideo, + artwork: previewFrameArtwork, + artworkData: previewFrameData, + backgroundColor: editorialVideoBackgroundColor, + textColorOverride: textColorOverride, + }; + } +} +/** + * Determine the correct artwork use case for each placement + * @param placement + */ +function artworkUseCaseForPlacement(placement) { + switch (placement) { + case EditorialMediaPlacement.Hero: + return 19 /* content.ArtworkUseCase.GroupingHero */; + case EditorialMediaPlacement.LargeBreakout: + return 6 /* content.ArtworkUseCase.ArcadeLargeBreakout */; + case EditorialMediaPlacement.StoryCard: + return 15 /* content.ArtworkUseCase.TodayCardMedia */; + case EditorialMediaPlacement.StoryDetail: + case EditorialMediaPlacement.StoryDetailLandscape: + return 13 /* content.ArtworkUseCase.ArticleImage */; + case EditorialMediaPlacement.Search: + return 9 /* content.ArtworkUseCase.SearchEditorialResult */; + default: + return 0 /* content.ArtworkUseCase.Default */; + } +} +/** + * Determine the correct crop code to use for artwork at a given flavor + * @param placement + * @param flavor + * @param isRTL + */ +function cropCodeForPlacementAndFlavor(placement, flavor, isRTL) { + switch (placement) { + case EditorialMediaPlacement.Hero: + switch (flavor) { + case EditorialMediaFlavor.HeroStatic16x9: + if (!isRTL) { + return "gd"; + } + break; + case EditorialMediaFlavor.HeroStaticRTL16x9: + if (isRTL) { + return "gg"; + } + break; + case EditorialMediaFlavor.StoryCenteredStatic16x9: + return isRTL ? "gh" : "ge"; + case EditorialMediaFlavor.UniversalAStatic16x9: + return isRTL ? "gj" : "gi"; + default: + break; + } + break; + case EditorialMediaPlacement.LargeBreakout: + switch (flavor) { + case EditorialMediaFlavor.StoryCenteredStatic16x9: + return isRTL ? "gk" : "gf"; + case EditorialMediaFlavor.UniversalAStatic16x9: + return isRTL ? "gl" : "gm"; + default: + break; + } + break; + case EditorialMediaPlacement.StoryCard: + case EditorialMediaPlacement.StoryDetail: + case EditorialMediaPlacement.StoryDetailLandscape: + case EditorialMediaPlacement.Search: + switch (flavor) { + case EditorialMediaFlavor.UniversalAStatic16x9: + return "gn"; + default: + break; + } + break; + case EditorialMediaPlacement.Brick: + switch (flavor) { + case EditorialMediaFlavor.SearchCategoryBrick: + return "SCB.ApSCBL01"; + default: + break; + } + break; + default: + break; + } + return "sr"; +} +/** + * Determines whether a given piece of editorial media is considered dark, light, or unknown. + * @param objectGraph Current object graph + * @param mediaData The data for the editorial media + * @param isRTL Whether the media to check is for RTL + * @returns True if the media is considered to be dark, false if considered light, or undefined if unknown. + */ +export function isMediaDark(objectGraph, editorialMediaData, isRTL = false) { + if (isNothing(editorialMediaData)) { + return null; + } + const textColorOverride = isRTL + ? editorialMediaData.rtlTextColorOverride + : editorialMediaData.textColorOverride; + const backgroundColor = isRTL + ? editorialMediaData.rtlBackgroundColor + : editorialMediaData.backgroundColor; + if (isSome(textColorOverride)) { + // Custom text colors can either be white or black. If the custom color is white, we set the + // media as dark, which will implicitly use white text. The behaviour is reversed for black text. + return color.isColorEqualToColor(textColorOverride, color.white); + } + else if (isSome(backgroundColor)) { + // If no custom text color is set, use the media background color to determine the darkness. + return color.isDarkColor(backgroundColor); + } + return null; +} +/** + * Determines if an editorially set custom text color should be used. + * @param objectGraph Current object graph + * @param artworkData The data that represents the editorial artwork + * @param artwork The parsed editorial artwork + * @returns True if the component should respect an editorial color + */ +function editorialTextColorOverride(objectGraph, artworkData, artwork) { + const textColor = artwork === null || artwork === void 0 ? void 0 : artwork.textColor; + if (isNothing(textColor)) { + return null; + } + const useCustomTextColor = serverData.asBooleanOrFalse(artworkData, "useCustomTextColor"); + const customTextColorIsValid = color.isColorEqualToColor(textColor, color.black) || color.isColorEqualToColor(textColor, color.white); + if (useCustomTextColor && customTextColorIsValid) { + return textColor; + } + return null; +} +/** + * Determines the video playback controls for a given placement. + * @param objectGraph Current object graph + * @param placement The placement for the view containing the video + * @returns The video playback controls + */ +function videoPlaybackControls(objectGraph, placement) { + switch (placement) { + case EditorialMediaPlacement.Hero: + case EditorialMediaPlacement.StoryDetail: + case EditorialMediaPlacement.StoryDetailLandscape: + return videoDefaults.noControls(objectGraph); + default: + return videoDefaults.standardControls(objectGraph); + } +} +//# sourceMappingURL=editorial-media-util.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-component-media-util.js b/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-component-media-util.js new file mode 100644 index 0000000..9a39901 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-component-media-util.js @@ -0,0 +1,76 @@ +import * as serverData from "../../foundation/json-parsing/server-data"; +import * as lockups from "../lockups/lockups"; +import * as editorialDataUtil from "./editorial-data-util"; +import * as editorialMediaUtil from "./editorial-media-util"; +/** + * Get the editorial artwork / video data for a component on an editorial page + * @param objectGraph + * @param data + * @param collectionDisplayStyle + */ +export function editorialMediaDataFromData(objectGraph, data, collectionDisplayStyle) { + switch (collectionDisplayStyle) { + case "EditorialLockupMedium": + case "EditorialLockupLarge": + case "EditorialLockupMediumVariant": + case "EditorialLockupLargeVariant": + return editorialMediaUtil.editorialMediaDataForPlacement(objectGraph, data, editorialMediaUtil.EditorialMediaPlacement.EditorialLockup); + case "BrickSmall": + case "BrickMedium": + case "BrickLarge": + return editorialMediaUtil.editorialMediaDataForPlacement(objectGraph, data, editorialMediaUtil.EditorialMediaPlacement.Brick); + case "StoryMedium": + case "StorySmall": + return editorialMediaUtil.editorialMediaDataForPlacement(objectGraph, data, editorialMediaUtil.EditorialMediaPlacement.StoryCard); + case "Hero": + return editorialMediaUtil.editorialMediaDataForPlacement(objectGraph, data, editorialMediaUtil.EditorialMediaPlacement.Hero); + case "BreakoutLarge": + return editorialMediaUtil.editorialMediaDataForPlacement(objectGraph, data, editorialMediaUtil.EditorialMediaPlacement.LargeBreakout); + default: + return {}; + } +} +/** + * Get the app icons to display when there is no editorial artwork / video + * @param objectGraph + * @param data + * @param lockupOptions + */ +export function editorialFallbackAppIconsFromData(objectGraph, data, lockupOptions) { + const collectionLockups = editorialCollectionAppsFromData(objectGraph, data, { lockupOptions: lockupOptions }); + if (serverData.isNullOrEmpty(collectionLockups)) { + return undefined; + } + return collectionLockups === null || collectionLockups === void 0 ? void 0 : collectionLockups.map((lockup) => { + return lockup.icon; + }); +} +/** + * Get lockup to be displayed on an editorial component + * @param objectGraph + * @param data + * @param lockupOptions + */ +export function editorialAppLockupFromData(objectGraph, data, lockupOptions) { + const productData = editorialDataUtil.extractProductData(objectGraph, data); + if (serverData.isNullOrEmpty(productData)) { + return null; + } + const appLockup = lockups.lockupFromData(objectGraph, productData, lockupOptions); + return appLockup; +} +/** + * Get the lockups from the collection this data is pointing to for a component on an editorial page + * @param objectGraph + * @param data + * @param lockupOptions + */ +export function editorialCollectionAppsFromData(objectGraph, data, lockupOptions) { + const collectionData = editorialDataUtil.extractCollectionData(objectGraph, data); + if (serverData.isNullOrEmpty(collectionData)) { + return null; + } + const collectionApps = lockups.lockupsFromData(objectGraph, collectionData, lockupOptions); + return collectionApps; +} +//# sourceMappingURL=editorial-page-component-media-util.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-controller-util.js b/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-controller-util.js new file mode 100644 index 0000000..db81f0d --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-controller-util.js @@ -0,0 +1,209 @@ +import { isNothing } from "@jet/environment"; +import * as editorialMediaUtil from "./editorial-media-util"; +import * as mediaAttributes from "../../foundation/media/attributes"; +import * as mediaDataFetching from "../../foundation/media/data-fetching"; +import * as mediaDataStructure from "../../foundation/media/data-structure"; +import * as mediaRelationship from "../../foundation/media/relationships"; +import * as metricsHelpersLocation from "../../common/metrics/helpers/location"; +import * as metricsHelpersPage from "../../common/metrics/helpers/page"; +import * as metricsHelpersUtil from "../../common/metrics/helpers/util"; +import * as models from "../../api/models"; +import * as onDevicePersonalization from "../../common/personalization/on-device-personalization"; +import * as refresh from "../../common/refresh/page-refresh-controller"; +import * as serverData from "../../foundation/json-parsing/server-data"; +import * as sharing from "../../common/sharing"; +import * as shelfBatching from "../../common/grouping/shelf-batching"; +import { createBaseShelfToken } from "./editorial-page-shelf-token"; +import { buildEditorialShelf } from "./editorial-page-shelf-builder"; +import { buildArcadeLockup } from "./editorial-page-shelf-builder/editorial-page-arcade-lockup-builder"; +import { shelfForTermsAndConditions } from "../grouping/grouping-common"; +import { EditorialShelfType } from "./editorial-page-types"; +/** + * Returns the number of shelves to hydrate on first load + */ +export function pageSparseCount(objectGraph) { + if (objectGraph.client.isWeb) { + // Returning `null` will avoid `Request` from setting the count at all + return null; + } + else { + return 5; + } +} +/** + * Returns the number of hydrated items in the hydrated shelves + */ +export function pageSparseLimit(objectGraph) { + return 12; +} +/** + * Modify request for a items fetched for grouping shelf items. (i.e. individual IDs). + * This should be ideally be in grouping builder's `prepareRequest`, but that would change how url-driven requests are configured. + * @param objectGraph + * @param request Request to modify. + */ +export function prepareMAPIRequest(objectGraph, request) { + const baseAttributes = ["editorialArtwork", "editorialVideo", "editorialClientParams"]; + const editorialItemsAttributes = [ + ...baseAttributes, + "showExpectedReleaseDate", + "expectedReleaseDateDisplayFormat", + "enrichedEditorialNotes", + "shortEditorialNotes", + ]; + const editorialPagesAttributes = [...baseAttributes, "enrichedEditorialNotes"]; + const contentAttributes = [ + ...baseAttributes, + "minimumOSVersion", + "screenshotsByType", + "videoPreviewsByType", + "expectedReleaseDateDisplayFormat", + "isAppleWatchSupported", + "minimumOSVersion", + "compatibilityControllerRequirement", + ]; + if (objectGraph.appleSilicon.isSupportEnabled) { + contentAttributes.push("macRequiredCapabilities"); + } + if (objectGraph.client.isMac) { + contentAttributes.push("hasMacIPAPackage"); + } + if (objectGraph.client.isVision) { + request.addingRelationshipLimit("editorial-shelves-collection:contents", 100); + } + if (objectGraph.client.isWeb) { + request.addingRelationshipLimit("editorial-shelves-collection:contents", 20); + } + const appsAttributes = [...contentAttributes]; + const features = ["personalization", "supportsCustomTextColor"]; + request + .includingAgeRestrictions() + .includingAdditionalPlatforms(mediaDataFetching.defaultAdditionalPlatformsForClient(objectGraph)) + .enablingFeatures(features) + .includingRelationshipsForUpsell(true) + .includingScopedAttributes("editorial-items", editorialItemsAttributes) + .includingScopedAttributes("editorial-pages", editorialPagesAttributes) + .includingScopedAttributes("apps", appsAttributes); +} +export function shareSheetActionFromData(objectGraph, editorialPageData) { + var _a; + const url = mediaAttributes.attributeAsString(editorialPageData, "url"); + const title = mediaAttributes.attributeAsString(editorialPageData, "editorialNotes.name"); + if (serverData.isNull(url) || serverData.isNull(title)) { + return null; + } + const subtitle = objectGraph.loc.string("SHARE_EDITORIAL_PAGE_SUBTITLE"); + const artwork = (_a = editorialMediaUtil.editorialMediaDataForPlacement(objectGraph, editorialPageData, editorialMediaUtil.EditorialMediaPlacement.EditorialPage)) === null || _a === void 0 ? void 0 : _a.artwork; + // Create share sheet model (bail out if unable to do so) + const shareData = sharing.shareSheetDataForGenericPage(objectGraph, title, url, subtitle, null, artwork); + if (isNothing(shareData)) { + return null; + } + const activities = sharing.shareSheetActivitiesForGenericPage(objectGraph, url); + return new models.ShareSheetAction(shareData, activities); +} +export async function renderPage(objectGraph, data, parameters) { + var _a, _b; + const editorialPageData = mediaDataStructure.dataFromDataContainer(objectGraph, data); + if (!serverData.isDefinedNonNullNonEmpty(editorialPageData)) { + return null; + } + const canvasData = mediaRelationship.relationshipCollection(editorialPageData, "canvas"); + if (!serverData.isDefinedNonNullNonEmpty(canvasData)) { + return null; + } + let pageType = "Editorial"; + const typeFromServer = serverData.asString(editorialPageData, "type"); + if (typeFromServer === "tags") { + pageType = "TagRoom"; + } + const metricsPageInformation = metricsHelpersPage.metricsPageInformationFromMediaApiResponse(objectGraph, pageType, editorialPageData.id, data); + const metricsLocationTracker = metricsHelpersLocation.newLocationTracker(); + const onDevicePersonalizationMetricsData = onDevicePersonalization.metricsData(objectGraph); + metricsPageInformation.recoMetricsData = metricsHelpersUtil.combinedRecoMetricsDataFromMetricsData(metricsPageInformation.recoMetricsData, null, onDevicePersonalizationMetricsData); + const pageRefreshController = refresh.newPageRefreshControllerFromResponse(data); + const { isArcadePage } = parameters; + const shelves = []; + let pageData = null; + let arcadeLockup = null; + if (serverData.isDefinedNonNull(editorialPageData.attributes)) { + pageData = { + id: editorialPageData.id, + type: editorialPageData.type, + attributes: editorialPageData.attributes, + relationships: editorialPageData.relationships, + meta: editorialPageData.meta, + }; + } + else { + return null; + } + let hasUpsellbreakout = false; + for (const shelfData of canvasData) { + const shelfIndex = shelves.length; + const shelfToken = createBaseShelfToken(objectGraph, pageData.id, shelfData, isArcadePage, shelfIndex, metricsPageInformation, metricsLocationTracker); + const shelf = buildEditorialShelf(objectGraph, pageData, shelfToken); + if (serverData.isNull(shelf)) { + continue; + } + if (shelves.length === 0) { + const existingPresentationHints = shelf.presentationHints; + shelf.presentationHints = { + ...existingPresentationHints, + isFirstShelf: true, + }; + } + if (shelfToken.type === EditorialShelfType.Upsell) { + hasUpsellbreakout = true; + } + if (shelf.contentType === "upsellBreakout") { + arcadeLockup = buildArcadeLockup(objectGraph, shelfToken); + } + shelves.push(shelf); + } + const page = isArcadePage ? new models.ArcadePage(shelves) : new models.GenericPage(shelves); + page.title = hasUpsellbreakout + ? null + : (_a = mediaAttributes.attributeAsString(editorialPageData, "editorialNotes.name")) !== null && _a !== void 0 ? _a : mediaAttributes.attributeAsString(editorialPageData, "name"); + page.canonicalURL = mediaAttributes.attributeAsString(editorialPageData, "url"); + page.shareAction = shareSheetActionFromData(objectGraph, editorialPageData); + page.pageRefreshPolicy = refresh.pageRefreshPolicyForController(objectGraph, pageRefreshController); + // Create the ArcadeLockup for Arcade's sticky header + if (isArcadePage && serverData.isDefinedNonNull(arcadeLockup)) { + page.subscriptionLockup = arcadeLockup; + } + if (isArcadePage) { + metricsPageInformation.isCrossfireReferralCandidate = true; + } + metricsHelpersPage.addMetricsEventsToPageWithInformation(objectGraph, page, metricsPageInformation); + if (objectGraph.client.isVision) { + page.presentationOptions.push("prefersRevealingPageTitleWhenNotRoot"); + } + const isLastShelfSeeAllGames = ((_b = shelves[shelves.length - 1]) === null || _b === void 0 ? void 0 : _b.contentType) === "arcadeFooter"; + if (objectGraph.client.deviceType !== "watch" && + objectGraph.client.deviceType !== "tv" && + !isLastShelfSeeAllGames) { + const url = objectGraph.bag.termsAndConditionsURL; + if (!serverData.isNull(url)) { + const termsAndConditionsShelf = shelfForTermsAndConditions(objectGraph, url); + shelves.push(termsAndConditionsShelf); + } + } + if (shelves.length > 0) { + const isLargeHeroBreakout = shelves[0].contentType === "largeHeroBreakout"; + const isUpsell = shelves[0].contentType === "upsellBreakout"; + const isHeroCarousel = shelves[0].contentType === "heroCarousel"; + const isMediaPageHeader = shelves[0].contentType === "mediaPageHeader"; + const shouldOverlayPageHeader = isLargeHeroBreakout || isUpsell || isHeroCarousel || isMediaPageHeader; + if (shouldOverlayPageHeader && !preprocessor.GAMES_TARGET) { + page.presentationOptions.push("prefersOverlayedPageHeader"); + } + if (isMediaPageHeader) { + page.presentationOptions.push("prefersNonStandardBackButton"); + page.presentationOptions.push("prefersHiddenPageTitle"); + } + } + shelfBatching.addBatchGroupsToPage(page); + return page; +} +//# sourceMappingURL=editorial-page-controller-util.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-editorial-copy-util.js b/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-editorial-copy-util.js new file mode 100644 index 0000000..7e8da86 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-editorial-copy-util.js @@ -0,0 +1,247 @@ +import * as serverData from "../../foundation/json-parsing/server-data"; +import * as mediaAttributes from "../../foundation/media/attributes"; +import * as mediaPlatformAttributes from "../../foundation/media/platform-attributes"; +import * as contentAttributes from "../content/attributes"; +import * as editorialDataUtil from "./editorial-data-util"; +/** + * Get the text that should be displayed as the badge (some times referred to as caption) for + * an editorial component + * @param objectGraph + * @param data + * @param collectionDisplayStyle + */ +export function editorialBadgeFromData(objectGraph, data, collectionDisplayStyle) { + let editorialBadge = null; + let editorialBadgeOverride = null; + let editorialBadgePaths = []; + // Check the editorial card first for override content + const editorialCardData = editorialDataUtil.extractEditorialCardData(objectGraph, data); + switch (data.type) { + case "editorial-items": + case "editorial-pages": + case "editorial-shelves-header": + editorialBadgePaths = ["enrichedEditorialNotes.badge", "editorialNotes.badge", "label"]; + editorialBadge = findFirstMatchingString(objectGraph, data, editorialBadgePaths, false); + editorialBadgeOverride = findFirstMatchingString(objectGraph, editorialCardData, editorialBadgePaths, false); + break; + case "apps": + case "in-apps": + case "app-bundles": + editorialBadgePaths = ["editorialNotes.badge"]; + editorialBadge = findFirstMatchingString(objectGraph, data, editorialBadgePaths, true); + editorialBadgeOverride = findFirstMatchingString(objectGraph, editorialCardData, editorialBadgePaths, true); + break; + default: + break; + } + const badge = serverData.isDefinedNonNull(editorialBadgeOverride) ? editorialBadgeOverride : editorialBadge; + if (serverData.isNullOrEmpty(badge)) { + return null; + } + return badge; +} +/** + * Get the text that should be displayed as the title for + * an editorial component + * @param objectGraph + * @param data + * @param collectionDisplayStyle + */ +export function editorialTitleFromData(objectGraph, data, collectionDisplayStyle) { + let editorialTitle = null; + let editorialTitlePaths = []; + switch (collectionDisplayStyle) { + case "EditorialLockupMediumVariant": + case "EditorialLockupLargeVariant": + switch (data.type) { + case "editorial-items": + case "editorial-pages": + case "editorial-shelves-header": + editorialTitlePaths = ["enrichedEditorialNotes.name", "editorialNotes.name"]; + editorialTitle = attributeAsString(data, editorialTitlePaths); + break; + case "apps": + case "in-apps": + case "app-bundles": + editorialTitlePaths = ["editorialNotes.tagline"]; + editorialTitle = findFirstMatchingString(objectGraph, data, editorialTitlePaths, true); + break; + default: + break; + } + break; + default: + switch (data.type) { + case "editorial-items": + case "editorial-pages": + case "editorial-shelves-header": + editorialTitlePaths = ["enrichedEditorialNotes.name", "editorialNotes.name"]; + editorialTitle = findFirstMatchingString(objectGraph, data, editorialTitlePaths, false); + break; + case "tags": + case "apps": + case "in-apps": + case "app-bundles": + editorialTitlePaths = ["name"]; + editorialTitle = findFirstMatchingString(objectGraph, data, editorialTitlePaths, false); + break; + default: + break; + } + } + // Check the editorial card first for override content + const editorialCardData = editorialDataUtil.extractEditorialCardData(objectGraph, data); + const editorialTitleOverride = findFirstMatchingString(objectGraph, editorialCardData, editorialTitlePaths, false); + const title = serverData.isDefinedNonNull(editorialTitleOverride) ? editorialTitleOverride : editorialTitle; + if (serverData.isNullOrEmpty(title)) { + return null; + } + return title; +} +/** + * Get the text that should be displayed as the description for + * an editorial component + * @param objectGraph + * @param data + */ +export function editorialDescriptionFromData(objectGraph, data, preferHeroDescription = false) { + let editorialDescription = null; + let editorialDescriptionPaths = []; + switch (data.type) { + case "editorial-items": + if (preferHeroDescription) { + editorialDescriptionPaths = ["enrichedEditorialNotes.tagline", "editorialNotes.tagline"]; + } + else { + const ignoreShortNotes = mediaAttributes.attributeAsBooleanOrFalse(data, "ignoreITunesShortNotes"); + if (!ignoreShortNotes) { + editorialDescriptionPaths = ["enrichedEditorialNotes.short", "editorialNotes.short"]; + } + } + editorialDescription = findFirstMatchingString(objectGraph, data, editorialDescriptionPaths, false); + break; + case "editorial-pages": + case "editorial-shelves-header": + editorialDescriptionPaths = ["enrichedEditorialNotes.tagline", "editorialNotes.tagline"]; + editorialDescription = findFirstMatchingString(objectGraph, data, editorialDescriptionPaths, false); + break; + case "apps": + case "in-apps": + case "app-bundles": + editorialDescriptionPaths = ["editorialNotes.tagline"]; + editorialDescription = findFirstMatchingString(objectGraph, data, editorialDescriptionPaths, true); + break; + default: + break; + } + // Check the editorial card first for override content + const editorialCardData = editorialDataUtil.extractEditorialCardData(objectGraph, data); + const editorialDescriptionOverride = findFirstMatchingString(objectGraph, editorialCardData, editorialDescriptionPaths, false); + const description = serverData.isDefinedNonNull(editorialDescriptionOverride) + ? editorialDescriptionOverride + : editorialDescription; + if (serverData.isNullOrEmpty(description)) { + return null; + } + return description; +} +/** + * Get the text that should be displayed as the call to action on a button for + * an editorial component + * @param objectGraph + * @param data + */ +export function editorialCallToActionFromData(objectGraph, data) { + let editorialCallToAction = null; + let editorialCallToActionPaths = []; + switch (data.type) { + case "editorial-items": + case "editorial-pages": + case "editorial-shelves-header": + editorialCallToActionPaths = ["enrichedEditorialNotes.callToAction", "editorialNotes.callToAction"]; + editorialCallToAction = findFirstMatchingString(objectGraph, data, editorialCallToActionPaths, false); + break; + case "apps": + case "in-apps": + case "app-bundles": + editorialCallToActionPaths = ["enrichedEditorialNotes.callToAction", "editorialNotes.callToAction"]; + editorialCallToAction = findFirstMatchingString(objectGraph, data, editorialCallToActionPaths, true); + break; + default: + break; + } + // Check the editorial card first for override content + const editorialCardData = editorialDataUtil.extractEditorialCardData(objectGraph, data); + const editorialCallToActionOverride = findFirstMatchingString(objectGraph, editorialCardData, editorialCallToActionPaths, false); + let callToAction = null; + if ((editorialCallToActionOverride === null || editorialCallToActionOverride === void 0 ? void 0 : editorialCallToActionOverride.length) > 0) { + callToAction = editorialCallToActionOverride; + } + else if (serverData.isDefinedNonNullNonEmpty(editorialCallToAction)) { + callToAction = editorialCallToAction; + } + else { + if (preprocessor.GAMES_TARGET) { + callToAction = objectGraph.loc.string("Card.Collection.ActionButton.Title"); + } + else { + callToAction = objectGraph.loc.string("HERO_CAROUSEL_OVERLAY_BUTTON_TITLE_VIEW"); + } + } + return callToAction; +} +// region Debug Helpers +/** + * Retrieve the specified attribute from the data as a string. + * + * @param data The data from which to retrieve the attribute. + * @param attributePath The object path for the attribute. + * @returns {string} The attribute value as a string. + */ +function attributeAsString(data, attributePath) { + if (serverData.isNullOrEmpty(data)) { + return null; + } + return mediaAttributes.attributeAsString(data, attributePath); +} +/** + * Retrieve the specified attribute from the data as a string. Returning the path if null or missing + * + * If the attribute lives under the platform-specific attributes, then a platform may be provided to properly call in to + * the nested structure. + * @param data The data from which to retrieve the attribute. + * @param platform The platform to look up + * @param attributePath The object path for the attribute. + * @returns {string} The attribute value as a string. + */ +export function platformAttributeAsString(data, platform, attributePath) { + if (serverData.isNullOrEmpty(data)) { + return null; + } + return mediaPlatformAttributes.platformAttributeAsString(data, platform, attributePath); +} +/** + * Retrieve the specified attribute from the data as a string. Trying to find the best match for the given paths. + * @param objectGraph The object graph to use lookups + * @param mediaApiData The media data to use to query for the string + * @param stringPaths The different paths to try to find the string + * @param usePlatformAttributes Whether to check the platform attributes or not + * @returns The first matching string or null if none found + */ +function findFirstMatchingString(objectGraph, mediaApiData, stringPaths, usePlatformAttributes) { + if (serverData.isNullOrEmpty(mediaApiData)) { + return null; + } + const attributePlatform = contentAttributes.bestAttributePlatformFromData(objectGraph, mediaApiData); + for (const path of stringPaths) { + const matchingString = usePlatformAttributes + ? platformAttributeAsString(mediaApiData, attributePlatform, path) + : attributeAsString(mediaApiData, path); + if (serverData.isDefinedNonNullNonEmpty(matchingString)) { + return matchingString; + } + } + return null; +} +// endregion +//# sourceMappingURL=editorial-page-editorial-copy-util.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-hero-util.js b/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-hero-util.js new file mode 100644 index 0000000..7ecca30 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-hero-util.js @@ -0,0 +1,88 @@ +import { isSome } from "@jet/environment/types/optional"; +import * as models from "../../api/models"; +import * as serverData from "../../foundation/json-parsing/server-data"; +import * as mediaAttributes from "../../foundation/media/attributes"; +import * as mediaDataStructure from "../../foundation/media/data-structure"; +import * as content from "../content/content"; +import * as heroCarouselOverlayCommon from "../grouping/hero/hero-carousel-overlay-common"; +import { deepLinkUrlFromData } from "../linking/external-deep-link"; +import * as lockups from "../lockups/lockups"; +import * as metricsHelpersImpressions from "../metrics/helpers/impressions"; +import * as editorialComponentMediaUtil from "./editorial-page-component-media-util"; +import * as editorialCopyUtil from "./editorial-page-editorial-copy-util"; +import * as editorialOverlayContentUtil from "./editorial-page-overlay-content-util"; +import { CollectionShelfDisplayStyle } from "./editorial-page-types"; +// region Overlay +export function heroOverlayFromData(objectGraph, data, shelfToken, editorialClientParams) { + const heroCarouselItemOverlay = new models.HeroCarouselItemOverlay(); + heroCarouselItemOverlay.overlayType = heroCarouselOverlayCommon.overlayTypeFromData(objectGraph, data); + heroCarouselItemOverlay.displayOptions = { + horizontalPlacement: "leading", + textAlignment: "leading", + isOverDarkContent: heroCarouselOverlayCommon.overlayIsOverDarkContent(objectGraph, data), + }; + heroCarouselItemOverlay.badgeText = editorialCopyUtil.editorialBadgeFromData(objectGraph, data, CollectionShelfDisplayStyle.Hero); + heroCarouselItemOverlay.titleText = editorialCopyUtil.editorialTitleFromData(objectGraph, data, CollectionShelfDisplayStyle.Hero); + if (!editorialClientParams.suppressTagline) { + heroCarouselItemOverlay.descriptionText = editorialCopyUtil.editorialDescriptionFromData(objectGraph, data, true); + } + heroCarouselItemOverlay.callToActionText = editorialCopyUtil.editorialCallToActionFromData(objectGraph, data); + heroCarouselItemOverlay.buttonTitle = heroCarouselOverlayCommon.overlayButtonTitleFromData(objectGraph, data); + const lockupOptions = { + metricsOptions: { + pageInformation: shelfToken.metricsPageInformation, + locationTracker: shelfToken.metricsLocationTracker, + targetType: "lockupSmall", + recoMetricsData: mediaDataStructure.metricsFromMediaApiObject(data), + }, + metricsClickOptions: { + id: data.id, + pageInformation: shelfToken.metricsPageInformation, + locationTracker: shelfToken.metricsLocationTracker, + actionDetails: { + franchise: heroCarouselItemOverlay.badgeText, + }, + }, + artworkUseCase: 1 /* content.ArtworkUseCase.LockupIconSmall */, + offerEnvironment: "dark", + canDisplayArcadeOfferButton: content.shelfDisplayStyleCanDisplayArcadeOfferButtons(objectGraph, shelfToken.collectionDisplayStyle), + isContainedInPreorderExclusiveShelf: false, + externalDeepLinkUrl: deepLinkUrlFromData(objectGraph, data), + }; + const appLockup = editorialComponentMediaUtil.editorialAppLockupFromData(objectGraph, data, lockupOptions); + const overlayContent = editorialOverlayContentUtil.extractOverlayContent(objectGraph, data, lockupOptions); + heroCarouselItemOverlay.lockup = overlayContent.lockup; + heroCarouselItemOverlay.collectionIcons = overlayContent.collectionIcons; + // If this EI can and should show the expected release date of an arcade app, override the badge. + const fallbackLabel = mediaAttributes.attributeAsString(data, "label"); + // For hero items the design is to use editorial labels instead of "Apple Arcade" so we replace that text here + if (serverData.isDefinedNonNullNonEmpty(fallbackLabel) && serverData.isDefinedNonNull(appLockup)) { + appLockup.heading = fallbackLabel; + } + const suppressLockup = isSome(editorialClientParams.suppressLockup) && editorialClientParams.suppressLockup; + if (serverData.isDefinedNonNullNonEmpty(heroCarouselItemOverlay.lockup) && !suppressLockup) { + heroCarouselItemOverlay.clickAction = heroCarouselItemOverlay.lockup.clickAction; + heroCarouselItemOverlay.impressionMetrics = heroCarouselItemOverlay.lockup.impressionMetrics; + } + else { + const heroCarouselItemOverlayMetricsOptions = { + targetType: "hero", + pageInformation: shelfToken.metricsPageInformation, + locationTracker: shelfToken.metricsLocationTracker, + recoMetricsData: mediaDataStructure.metricsFromMediaApiObject(data), + }; + const heroCarouselOverlayItemClickOptions = { + ...heroCarouselItemOverlayMetricsOptions, + id: data.id, + actionDetails: { + franchise: heroCarouselItemOverlay.badgeText, + }, + }; + heroCarouselItemOverlay.clickAction = lockups.editorialItemActionFromData(objectGraph, data, heroCarouselOverlayItemClickOptions, shelfToken.clientIdentifierOverride); + const heroCarouselItemOverlayImpressionsOptions = metricsHelpersImpressions.impressionOptions(objectGraph, data, heroCarouselItemOverlay.titleText, heroCarouselItemOverlayMetricsOptions); + metricsHelpersImpressions.addImpressionFields(objectGraph, heroCarouselItemOverlay, heroCarouselItemOverlayImpressionsOptions); + } + return heroCarouselItemOverlay; +} +// endregion +//# sourceMappingURL=editorial-page-hero-util.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-intent-controller-utils.js b/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-intent-controller-utils.js new file mode 100644 index 0000000..e3c2690 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-intent-controller-utils.js @@ -0,0 +1,5 @@ +import { generateRoutes } from "../util/generate-routes"; +import { makeEditorialPageIntentByID } from "../../api/intents/editorial/editorial-page-intent"; +const { routes: editorialPageRoutes, makeCanonicalUrl: makeEditorialPageURL } = generateRoutes(makeEditorialPageIntentByID, "/{platform}/editorial/{id}"); +export { editorialPageRoutes, makeEditorialPageURL }; +//# sourceMappingURL=editorial-page-intent-controller-utils.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-lockup-utils.js b/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-lockup-utils.js new file mode 100644 index 0000000..35e1cf9 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-lockup-utils.js @@ -0,0 +1,91 @@ +// region Lockup Creation +import * as serverData from "../../foundation/json-parsing/server-data"; +import * as mediaDataStructure from "../../foundation/media/data-structure"; +import * as content from "../content/content"; +import * as adLockups from "../lockups/ad-lockups"; +import * as lockups from "../lockups/lockups"; +import { clickTargetForCollectionDisplayStyle } from "./editorial-action-util"; +import { CollectionShelfFilterOverride } from "./editorial-page-types"; +import { isNothing } from "@jet/environment/types/optional"; +// region Lockup Creation +/** + * Create a lockup for shelfContents to display within an editorial page shelf + * @param objectGraph + * @param lockupData shelfContents to create lockup for. + * @param shelfToken Shelf shelfToken + * @param itemPosition The position of the item in the shelf + * @param collectionDisplayStyle The collection display style + */ +export function lockupFromData(objectGraph, lockupData, shelfToken, itemPosition, collectionDisplayStyle) { + if (serverData.isNullOrEmpty(lockupData)) { + return null; + } + // Set the ordinal + let ordinalString; + if (shelfToken.showOrdinals) { + ordinalString = objectGraph.loc.decimal(shelfToken.ordinalIndex); + } + let offerStyle = null; + if (serverData.isDefinedNonNull(shelfToken.shelfBackground) && + (shelfToken.shelfBackground.type === "color" || shelfToken.shelfBackground.type === "interactive")) { + offerStyle = "white"; + } + let clientIdentifierOverride; + if (serverData.isDefinedNonNullNonEmpty(shelfToken)) { + clientIdentifierOverride = shelfToken.clientIdentifierOverride; + } + const contentMetricsOptions = { + ...shelfToken.metricsImpressionOptions, + id: lockupData.id, + idType: "its_id", + }; + // Create the lockup + const lockupOptions = { + ordinal: ordinalString, + metricsOptions: { + ...contentMetricsOptions, + recoMetricsData: mediaDataStructure.metricsFromMediaApiObject(lockupData), + isAdvert: adLockups.isAdvert(objectGraph, lockupData), + targetType: clickTargetForCollectionDisplayStyle(objectGraph, collectionDisplayStyle), + }, + clientIdentifierOverride: clientIdentifierOverride, + artworkUseCase: 0 /* content.ArtworkUseCase.Default */, + offerStyle: offerStyle, + canDisplayArcadeOfferButton: content.shelfDisplayStyleCanDisplayArcadeOfferButtons(objectGraph, collectionDisplayStyle), + isContainedInPreorderExclusiveShelf: shelfToken.filterOverrides.includes(CollectionShelfFilterOverride.ShowOnlyPreorder), + }; + let lockup; + switch (collectionDisplayStyle) { + case "EditorialLockupMediumVariant": + case "EditorialLockupMedium": + case "EditorialLockupLargeVariant": + case "EditorialLockupLarge": + lockupOptions.offerEnvironment = "light"; + lockup = lockups.imageLockupFromData(objectGraph, lockupData, lockupOptions, collectionDisplayStyle); + break; + case "Poster": + lockup = lockups.posterLockupFromData(objectGraph, lockupData, lockupOptions); + break; + default: + lockup = lockups.lockupFromData(objectGraph, lockupData, lockupOptions); + } + if (serverData.isNull(lockup) || !lockup.isValid()) { + return null; + } + lockup.id = lockupIdentifier(shelfToken, itemPosition, lockup.adamId); + return lockup; +} +/** + * @param shelfToken The shelf token + * @param itemPosition The position of the item in the shelf + * @param adamId The adamID associated with the lockup + * @returns A string for uniquely identifying the lockup in this shelf + */ +function lockupIdentifier(shelfToken, itemPosition, adamId) { + if (isNothing(adamId)) { + return null; + } + return `${shelfToken.id}_${itemPosition}_${adamId}`; +} +// endregion +//# sourceMappingURL=editorial-page-lockup-utils.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-media-api-utils.js b/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-media-api-utils.js new file mode 100644 index 0000000..42e838a --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-media-api-utils.js @@ -0,0 +1,39 @@ +import * as editorialPageControllerUtil from "./editorial-page-controller-util"; +import { Request as MediaAPIRequest, defaultAdditionalPlatformsForClient } from "../../foundation/media/data-fetching"; +import { Parameters } from "../../foundation/network/url-constants"; +import { shouldFetchCustomAttributes } from "../product-page/product-page-variants"; +import { isEditorialPageIntentByName, } from "../../api/intents/editorial/editorial-page-intent"; +import { setPreviewPlatform } from "../preview-platform"; +export function makeEditorialPageRequest(objectGraph, intent) { + const mediaApiRequest = makeBaseMediaAPIRequest(objectGraph, intent); + if (isEditorialPageIntentByName(intent)) { + mediaApiRequest.addingQuery(Parameters.name, intent.name); + } + else { + mediaApiRequest.withIdOfType(intent.id, "editorial-pages"); + } + prepareMediaAPIRequest(objectGraph, mediaApiRequest); + setPreviewPlatform(objectGraph, mediaApiRequest); + return mediaApiRequest; +} +function makeBaseMediaAPIRequest(objectGraph, locale) { + return new MediaAPIRequest(objectGraph, `/v1/editorial/${locale.storefront}`).forType("editorial-pages"); +} +function prepareMediaAPIRequest(objectGraph, mediaApiRequest) { + mediaApiRequest.includingAdditionalPlatforms(defaultAdditionalPlatformsForClient(objectGraph)); + mediaApiRequest.usingCustomAttributes(shouldFetchCustomAttributes(objectGraph)); + // <rdar://53420717> For performance reasons, we limit shelves to 3 items on watchOS. + if (objectGraph.client.isWatch) { + mediaApiRequest.addingRelationshipLimit("contents", 3); + } + mediaApiRequest + .includingAgeRestrictions() + .includingScopedRelationships("editorial-shelves", ["contents"]) + .includingScopedRelationships("editorial-pages", ["primary-contents"]) + .includingRelationships(["canvas"]) + .withSparseCount(editorialPageControllerUtil.pageSparseCount(objectGraph)) + .withSparseLimit(editorialPageControllerUtil.pageSparseLimit(objectGraph)) + .includingAssociateKeys("editorial-shelves-collection:contents", ["editorial-cards"]); + editorialPageControllerUtil.prepareMAPIRequest(objectGraph, mediaApiRequest); +} +//# sourceMappingURL=editorial-page-media-api-utils.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-overlay-content-util.js b/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-overlay-content-util.js new file mode 100644 index 0000000..f76d85a --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-overlay-content-util.js @@ -0,0 +1,20 @@ +import * as serverData from "../../foundation/json-parsing/server-data"; +import { popLocation, pushBasicLocation } from "../metrics/helpers/location"; +import * as editorialComponentMediaUtil from "./editorial-page-component-media-util"; +export function extractOverlayContent(objectGraph, itemData, lockupOptions) { + const overlayContent = {}; + pushBasicLocation(objectGraph, lockupOptions.metricsOptions, "collectionIcons"); + const collectionIcons = editorialComponentMediaUtil.editorialFallbackAppIconsFromData(objectGraph, itemData, lockupOptions); + popLocation(lockupOptions.metricsOptions.locationTracker); + const appLockup = editorialComponentMediaUtil.editorialAppLockupFromData(objectGraph, itemData, lockupOptions); + if (serverData.isDefinedNonNullNonEmpty(collectionIcons) && + (collectionIcons.length > 1 || serverData.isNullOrEmpty(appLockup))) { + overlayContent.collectionIcons = collectionIcons; + } + else if (serverData.isDefinedNonNull(appLockup)) { + overlayContent.lockup = appLockup; + overlayContent.collectionIcons = [appLockup.icon]; + } + return overlayContent; +} +//# sourceMappingURL=editorial-page-overlay-content-util.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-shelf-builder/editorial-page-arcade-lockup-builder.js b/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-shelf-builder/editorial-page-arcade-lockup-builder.js new file mode 100644 index 0000000..edb539c --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-shelf-builder/editorial-page-arcade-lockup-builder.js @@ -0,0 +1,17 @@ +import * as models from "../../../api/models"; +import * as serverData from "../../../foundation/json-parsing/server-data"; +import * as arcadeCommon from "../../arcade/arcade-common"; +import * as lockups from "../../lockups/lockups"; +export function buildArcadeLockup(objectGraph, upsellBreakoutShelfToken) { + const metricsOptions = { + pageInformation: upsellBreakoutShelfToken.metricsPageInformation, + locationTracker: upsellBreakoutShelfToken.metricsLocationTracker, + }; + const upsellData = arcadeCommon.upsellFromRelationshipOf(objectGraph, upsellBreakoutShelfToken.data); + if (serverData.isNullOrEmpty(upsellData)) { + return null; + } + const arcadeLockup = lockups.arcadeLockupFromData(objectGraph, upsellData, metricsOptions, models.marketingItemContextFromString("arcadeTabHeader"), "colored", objectGraph.client.isMac ? "arcade" : "navigationBar"); + return arcadeLockup; +} +//# sourceMappingURL=editorial-page-arcade-lockup-builder.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-shelf-builder/editorial-page-arcade-see-all-games-shelf-builder.js b/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-shelf-builder/editorial-page-arcade-see-all-games-shelf-builder.js new file mode 100644 index 0000000..a197c19 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-shelf-builder/editorial-page-arcade-see-all-games-shelf-builder.js @@ -0,0 +1,71 @@ +import * as arcadeCommon from "../../arcade/arcade-common"; +import * as color from "../../../foundation/util/color-util"; +import * as content from "../../content/content"; +import * as editorialDataUtil from "../editorial-data-util"; +import * as metricsHelpersImpressions from "../../metrics/helpers/impressions"; +import * as metricsHelpersLocation from "../../metrics/helpers/location"; +import * as models from "../../../api/models"; +import * as serverData from "../../../foundation/json-parsing/server-data"; +import * as gamesComponentBuilder from "../../../gameservicesui/src/editorial-page/editorial-component-builder"; +export function buildArcadeSeeAllGamesShelf(objectGraph, shelfToken) { + const footer = new models.ArcadeFooter(); + const shelf = new models.Shelf("arcadeFooter"); + shelf.items = [footer]; + const impressionOptions = { + targetType: "arcadeSeeAllGamesFooter", + pageInformation: shelfToken.metricsPageInformation, + locationTracker: shelfToken.metricsLocationTracker, + title: objectGraph.loc.string("Arcade.SeeAllGames.Button.Title"), + id: shelfToken.id, + kind: "footer", + softwareType: "Arcade", + }; + metricsHelpersImpressions.addImpressionFields(objectGraph, footer, impressionOptions); + metricsHelpersLocation.pushContentLocation(objectGraph, impressionOptions, impressionOptions.title); + if (preprocessor.GAMES_TARGET) { + footer.buttonAction = gamesComponentBuilder.makeArcadeSeeAllAction(objectGraph); + } + else { + footer.buttonAction = arcadeCommon.seeAllArcadeGamesPageFlowAction(objectGraph, "releaseDate", shelfToken.metricsPageInformation, shelfToken.metricsLocationTracker); + } + const buttonImpressionOptions = { + targetType: "button", + pageInformation: shelfToken.metricsPageInformation, + locationTracker: shelfToken.metricsLocationTracker, + title: footer.buttonAction.title, + id: "SeeAllGames", + kind: "button", + softwareType: "Arcade", + }; + metricsHelpersImpressions.addImpressionFields(objectGraph, footer.buttonAction, buttonImpressionOptions); + metricsHelpersLocation.popLocation(impressionOptions.locationTracker); + const termsAndConditionsUrl = objectGraph.bag.termsAndConditionsURL; + const shouldAddTermsAndConditions = !serverData.isNull(termsAndConditionsUrl) && objectGraph.client.deviceType !== "tv"; + if (shouldAddTermsAndConditions) { + const termsAndConditionTitle = objectGraph.loc.string("TermsAndConditions.Title"); + const urlAction = new models.ExternalUrlAction(termsAndConditionsUrl); + const footnote = new models.Footnote(termsAndConditionTitle); + footnote.clickAction = urlAction; + footnote.presentationStyle = ["hasChevron", "textLightensOnHighlight", "hasSeparator"]; + footer.footnote = footnote; + } + shelf.background = { + type: "color", + color: color.named("placeholderBackground"), + }; + const shelfContents = editorialDataUtil.extractRemainingItemsOrInitialShelfContents(objectGraph, shelfToken); + if (serverData.isDefinedNonNullNonEmpty(shelfContents)) { + const metricsOptions = { + pageInformation: shelfToken.metricsPageInformation, + locationTracker: shelfToken.metricsLocationTracker, + }; + footer.icons = content.impressionableAppIconsFromDataCollection(objectGraph, shelfContents, metricsOptions, { + useCase: 2 /* content.ArtworkUseCase.LockupIconMedium */, + }); + } + else { + footer.icons = []; + } + return shelf; +} +//# sourceMappingURL=editorial-page-arcade-see-all-games-shelf-builder.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-shelf-builder/editorial-page-chart-shelf-builder.js b/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-shelf-builder/editorial-page-chart-shelf-builder.js new file mode 100644 index 0000000..58e6862 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-shelf-builder/editorial-page-chart-shelf-builder.js @@ -0,0 +1,106 @@ +import { isNothing, isSome } from "@jet/environment"; +import * as models from "../../../api/models"; +import { attributeAsString } from "../../../foundation/media/attributes"; +import { chartUrlFromData } from "../../content/content"; +import { addClickEventToSeeAllAction } from "../../metrics/helpers/clicks"; +import { insertPlaceholdersIntoShelfIfRequired } from "../../placeholders/placeholders"; +import { CollectionShelfLayoutDirection } from "../editorial-page-types"; +import { createShelfTokenUrlIfNecessary } from "./editorial-page-collection-shelf-builder/editorial-page-collection-shelf-common"; +import { buildLockupChartShelf, buildLockupSmallShelf, } from "./editorial-page-collection-shelf-builder/editorial-page-lockup-collection-shelf-builder"; +import { viewChartListActionWithGames } from "../../../gameservicesui/src/intent-controllers/page/charts-list-page-intent-controller"; +import * as editorialDataUtil from "../editorial-data-util"; +import { makeGamesFromCollection } from "../../../gameservicesui/src/models/cards/collection-card"; +export function buildChartShelf(objectGraph, shelfToken) { + var _a; + shelfToken.showOrdinals = true; + let shelf; + if (preprocessor.GAMES_TARGET) { + shelf = buildLockupChartShelf(objectGraph, shelfToken); + } + else { + /// Currently the charts shelf doesn't return a `displayStyle`, we only support LockupSmall. + shelf = buildLockupSmallShelf(objectGraph, shelfToken, false, CollectionShelfLayoutDirection.Horizontal); + } + if (isNothing(shelf)) { + return undefined; + } + shelf.url = createShelfTokenUrlIfNecessary(objectGraph, shelf, shelfToken); + insertPlaceholdersIntoShelfIfRequired(objectGraph, shelf, shelfToken); + if (preprocessor.GAMES_TARGET) { + if (isSome(shelf.header)) { + shelf.header.titleAction = (_a = chartPageSeeAllActionForGames(objectGraph, shelfToken)) !== null && _a !== void 0 ? _a : undefined; + } + else { + shelf.seeAllAction = chartPageSeeAllActionForGames(objectGraph, shelfToken); + } + shelf.id = `shelf_${shelfToken.id}`; + } + else { + replaceShelfSeeAllAction(objectGraph, shelf, chartPageSeeAllAction(objectGraph, shelfToken)); + } + return shelf; +} +function chartPageSeeAllActionForGames(objectGraph, shelfToken) { + var _a; + const gamesData = editorialDataUtil.extractRemainingItemsOrInitialShelfContents(objectGraph, shelfToken); + const games = makeGamesFromCollection(objectGraph, gamesData, {}, {}); + const genre = attributeAsString(shelfToken.data, "genre"); + const chart = attributeAsString(shelfToken.data, "chart"); + if (isSome(games) && isSome(genre) && isSome(chart)) { + const chartUrl = chartUrlFromData(objectGraph, genre, chart); + return viewChartListActionWithGames(games, shelfToken.title, (_a = shelfToken.subtitle) !== null && _a !== void 0 ? _a : "", chartUrl, shelfToken.data.id, // TODO: provide real `reco_id` rdar://133659618 ([Play Now Feed] Use metrics.meta.reco_id from shelf meta in metrics) + objectGraph); + } + return null; +} +function chartPageSeeAllAction(objectGraph, shelfToken) { + const genre = attributeAsString(shelfToken.data, "genre"); + const chart = attributeAsString(shelfToken.data, "chart"); + const action = new models.FlowAction("page"); + action.pageUrl = chartUrlFromData(objectGraph, genre, chart); + action.title = objectGraph.loc.string("ACTION_SEE_ALL"); + action.referrerUrl = shelfToken.metricsPageInformation.pageUrl; + addClickEventToSeeAllAction(objectGraph, action, action.pageUrl, { + pageInformation: shelfToken.metricsPageInformation, + locationTracker: shelfToken.metricsLocationTracker, + }); + return action; +} +/** + * Update the shelf header to use the provided seeAll action. + * @param objectGraph The App Store object graph used to check feature flags + * @param shelfHeader The shelf header to update + * @param seeAllAction The "See All" action to apply + */ +export function replaceShelfHeaderSeeAllAction(objectGraph, shelfHeader, seeAllAction) { + if (objectGraph.featureFlags.isEnabled("shelf_header")) { + // Modern headers make title tappable with a chevron (>). + shelfHeader.titleAction = seeAllAction; + } + else { + // Legacy headers show "See All" textual button on trailing edge. + shelfHeader.accessoryAction = seeAllAction; + } +} +/** + * Update the shelf header to use the provided seeAll action. + * @param objectGraph The App Store object graph used to check feature flags + * @param shelf The shelf whose header needs updating + * @param seeAllAction The see all action to apply + */ +export function replaceShelfSeeAllAction(objectGraph, shelf, seeAllAction) { + if (objectGraph.featureFlags.isEnabled("shelf_header")) { + if (isSome(shelf.header)) { + replaceShelfHeaderSeeAllAction(objectGraph, shelf.header, seeAllAction); + } + else { + shelf.header = { + titleAction: seeAllAction, + }; + } + } + else { + shelf.seeAllAction = seeAllAction; + } +} +//# sourceMappingURL=editorial-page-chart-shelf-builder.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-shelf-builder/editorial-page-collection-shelf-builder/editorial-page-breakout-large-collection-shelf-builder.js b/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-shelf-builder/editorial-page-collection-shelf-builder/editorial-page-breakout-large-collection-shelf-builder.js new file mode 100644 index 0000000..093336d --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-shelf-builder/editorial-page-collection-shelf-builder/editorial-page-breakout-large-collection-shelf-builder.js @@ -0,0 +1,94 @@ +import * as models from "../../../../api/models"; +import * as serverData from "../../../../foundation/json-parsing/server-data"; +import * as mediaAttributes from "../../../../foundation/media/attributes"; +import * as mediaDataStructure from "../../../../foundation/media/data-structure"; +import * as metricsHelpersImpressions from "../../../metrics/helpers/impressions"; +import * as editorialActionUtil from "../../editorial-action-util"; +import * as editorialDataUtil from "../../editorial-data-util"; +import * as editorialComponentMediaUtil from "../../editorial-page-component-media-util"; +import * as editorialCopyUtil from "../../editorial-page-editorial-copy-util"; +import * as editorialOverlayContentUtil from "../../editorial-page-overlay-content-util"; +import { createShelfTokenUrlIfNecessary, shouldDefer } from "./editorial-page-collection-shelf-common"; +import { deepLinkUrlFromData } from "../../../linking/external-deep-link"; +import { isMediaDark } from "../../editorial-media-util"; +import { insertPlaceholdersIntoShelfIfRequired } from "../../../placeholders/placeholders"; +export function buildBreakoutLargeShelf(objectGraph, shelfToken) { + const items = []; + const shelf = new models.Shelf("largeHeroBreakout"); + shelf.isHorizontal = mediaAttributes.attributeAsString(shelfToken.data, "layoutDirection") === "Horizontal"; + const shelfContents = editorialDataUtil.extractRemainingItemsOrInitialShelfContents(objectGraph, shelfToken); + for (const itemData of shelfContents) { + const primaryItemData = editorialDataUtil.extractHydratedPrimaryContentData(objectGraph, itemData); + if ((editorialDataUtil.requiresPrimaryContent(objectGraph, itemData) && + !mediaAttributes.hasAttributes(primaryItemData)) || + shouldDefer(shelfToken)) { + shelfToken.isDeferring = true; + shelfToken.remainingItems.push(itemData); + continue; + } + const metricsOptions = { + targetType: "largeBreakout", + pageInformation: shelfToken.metricsPageInformation, + locationTracker: shelfToken.metricsLocationTracker, + recoMetricsData: mediaDataStructure.metricsFromMediaApiObject(itemData), + }; + const editorialClientParams = editorialDataUtil.extractEditorialClientParams(objectGraph, itemData); + const hideEditorialMedia = serverData.asBooleanOrFalse(editorialClientParams.hideEditorialMedia); + const mediaData = !hideEditorialMedia + ? editorialComponentMediaUtil.editorialMediaDataFromData(objectGraph, itemData, shelfToken.collectionDisplayStyle) + : null; + const lockupOptions = { + metricsOptions, + clientIdentifierOverride: shelfToken.clientIdentifierOverride, + artworkUseCase: 0 /* content.ArtworkUseCase.Default */, + offerEnvironment: "lightOverArtwork", + canDisplayArcadeOfferButton: true, + externalDeepLinkUrl: deepLinkUrlFromData(objectGraph, itemData), + }; + const overlayContent = editorialOverlayContentUtil.extractOverlayContent(objectGraph, itemData, lockupOptions); + if (serverData.isNullOrEmpty(mediaData) && serverData.isNullOrEmpty(overlayContent.collectionIcons)) { + continue; + } + const badgeText = editorialCopyUtil.editorialBadgeFromData(objectGraph, itemData, shelfToken.collectionDisplayStyle); + const badgeType = serverData.isDefinedNonNull(badgeText) + ? { type: "text", title: badgeText } + : { type: "none", title: null }; + const largeBreakoutDetails = new models.BreakoutDetails(editorialCopyUtil.editorialTitleFromData(objectGraph, itemData, shelfToken.collectionDisplayStyle), editorialCopyUtil.editorialDescriptionFromData(objectGraph, itemData, true), badgeType, null); + const largeBreakout = new models.LargeHeroBreakout(largeBreakoutDetails, { position: "leading", wantsBlur: true }, null, mediaData === null || mediaData === void 0 ? void 0 : mediaData.artwork, mediaData === null || mediaData === void 0 ? void 0 : mediaData.video, overlayContent.collectionIcons, mediaData === null || mediaData === void 0 ? void 0 : mediaData.backgroundColor); + largeBreakout.rtlArtwork = mediaData === null || mediaData === void 0 ? void 0 : mediaData.rtlArtwork; + largeBreakout.rtlVideo = mediaData === null || mediaData === void 0 ? void 0 : mediaData.rtlVideo; + largeBreakout.rtlBackgroundColor = mediaData === null || mediaData === void 0 ? void 0 : mediaData.rtlBackgroundColor; + // Configure impressions + const contentMetricsOptions = { + ...metricsOptions, + id: itemData.id, + idType: "its_contentId", + }; + const impressionOptions = metricsHelpersImpressions.impressionOptions(objectGraph, itemData, largeBreakout.details.title, contentMetricsOptions); + metricsHelpersImpressions.addImpressionFields(objectGraph, largeBreakout, impressionOptions); + const breakoutAction = editorialActionUtil.createPrimaryActionForComponentFromData(objectGraph, itemData, shelfToken); + if (serverData.isDefinedNonNull(breakoutAction)) { + breakoutAction.title = editorialCopyUtil.editorialCallToActionFromData(objectGraph, itemData); + } + largeBreakout.details.callToActionButtonAction = breakoutAction; + largeBreakout.clickAction = breakoutAction; + largeBreakout.editorialDisplayOptions = + editorialDataUtil.editorialDisplayOptionsFromClientParams(editorialClientParams); + largeBreakout.lockup = overlayContent.lockup; + largeBreakout.isMediaDark = isMediaDark(objectGraph, mediaData); + largeBreakout.isRTLMediaDark = isMediaDark(objectGraph, mediaData, true); + items.push(largeBreakout); + } + // We don't need this in our incomplete shelf URL, so we'll preemptively remove it. + delete shelfToken.maxItemCount; + shelf.eyebrow = shelfToken.eyebrow; + shelf.eyebrowArtwork = shelfToken.eyebrowArtwork; + shelf.title = shelfToken.title; + shelf.subtitle = shelfToken.subtitle; + shelf.items = items; + shelf.rowsPerColumn = mediaAttributes.attributeAsNumber(shelfToken.data, "rowCount"); + shelf.url = createShelfTokenUrlIfNecessary(objectGraph, shelf, shelfToken); + insertPlaceholdersIntoShelfIfRequired(objectGraph, shelf, shelfToken); + return shelf; +} +//# sourceMappingURL=editorial-page-breakout-large-collection-shelf-builder.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-shelf-builder/editorial-page-collection-shelf-builder/editorial-page-brick-collection-shelf-builder.js b/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-shelf-builder/editorial-page-collection-shelf-builder/editorial-page-brick-collection-shelf-builder.js new file mode 100644 index 0000000..cefe63e --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-shelf-builder/editorial-page-collection-shelf-builder/editorial-page-brick-collection-shelf-builder.js @@ -0,0 +1,146 @@ +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 mediaDataStructure from "../../../../foundation/media/data-structure"; +import * as content from "../../../content/content"; +import * as metricsHelpersImpressions from "../../../metrics/helpers/impressions"; +import * as metricsHelpersLocation from "../../../metrics/helpers/location"; +import * as editorialActionUtil from "../../editorial-action-util"; +import * as editorialDataUtil from "../../editorial-data-util"; +import * as editorialMediaUtil from "../../editorial-page-component-media-util"; +import * as editorialCopyUtil from "../../editorial-page-editorial-copy-util"; +import { CollectionShelfDisplayStyle } from "../../editorial-page-types"; +import { createShelfTokenUrlIfNecessary, shouldDefer } from "./editorial-page-collection-shelf-common"; +import { deepLinkUrlFromData } from "../../../linking/external-deep-link"; +import { isMediaDark } from "../../editorial-media-util"; +import { insertPlaceholdersIntoShelfIfRequired } from "../../../placeholders/placeholders"; +import { convertToShelfModel, makeShelfId, } from "../../../../gameservicesui/src/editorial-page/editorial-component-builder"; +import { createSmallBrickFromData } from "../../../../gameservicesui/src/models/cards/small-brick"; +export function buildBrickSmallShelf(objectGraph, shelfToken) { + return buildBrickShelf(objectGraph, shelfToken, CollectionShelfDisplayStyle.BrickSmall, "smallBrick"); +} +export function buildBrickMediumShelf(objectGraph, shelfToken) { + return buildBrickShelf(objectGraph, shelfToken, CollectionShelfDisplayStyle.BrickMedium, "brick"); +} +export function buildBrickLargeShelf(objectGraph, shelfToken) { + return buildBrickShelf(objectGraph, shelfToken, CollectionShelfDisplayStyle.BrickLarge, "largeBrick"); +} +// region Brick Creation +function buildBrickShelf(objectGraph, shelfToken, displayStyle, brickComponentType) { + const items = []; + const shelf = new models.Shelf(brickComponentType); + shelf.isHorizontal = mediaAttributes.attributeAsString(shelfToken.data, "layoutDirection") === "Horizontal"; + const shelfContents = editorialDataUtil.extractRemainingItemsOrInitialShelfContents(objectGraph, shelfToken); + for (const itemData of shelfContents) { + const metricsOptions = { + ...shelfToken.metricsImpressionOptions, + targetType: editorialActionUtil.clickTargetForCollectionDisplayStyle(objectGraph, displayStyle), + recoMetricsData: mediaDataStructure.metricsFromMediaApiObject(itemData), + }; + if (!itemData.attributes || shouldDefer(shelfToken)) { + shelfToken.remainingItems.push(itemData); + shelfToken.isDeferring = true; + continue; + } + if (preprocessor.GAMES_TARGET && displayStyle === CollectionShelfDisplayStyle.BrickSmall) { + const clickAction = editorialActionUtil.createPrimaryActionForComponentFromData(objectGraph, itemData, shelfToken); + const smallBrick = createSmallBrickFromData(objectGraph, itemData, makeShelfId(shelfToken), clickAction, {}, {}, true); + if (isNothing(smallBrick)) { + continue; + } + items.push(convertToShelfModel(smallBrick)); + } + else { + const brick = buildBrick(objectGraph, itemData, shelfToken.collectionDisplayStyle, metricsOptions, metricsOptions, shelfToken.clientIdentifierOverride); + brick.clickAction = editorialActionUtil.createPrimaryActionForComponentFromData(objectGraph, itemData, shelfToken); + if (!brick.isValid()) { + continue; + } + items.push(brick); + } + metricsHelpersLocation.nextPosition(shelfToken.metricsImpressionOptions.locationTracker); + } + if (serverData.isDefinedNonNull(shelfToken.presentationHints)) { + shelf.presentationHints = shelfToken.presentationHints; + } + // We don't need this in our incomplete shelf URL, so we'll preemptively remove it. + delete shelfToken.maxItemCount; + shelf.eyebrow = shelfToken.eyebrow; + shelf.eyebrowArtwork = shelfToken.eyebrowArtwork; + shelf.title = shelfToken.title; + shelf.subtitle = shelfToken.subtitle; + shelf.items = items; + shelf.rowsPerColumn = mediaAttributes.attributeAsNumber(shelfToken.data, "rowCount"); + shelf.url = createShelfTokenUrlIfNecessary(objectGraph, shelf, shelfToken); + insertPlaceholdersIntoShelfIfRequired(objectGraph, shelf, shelfToken); + return shelf; +} +/** + * Creates a Brick from the input data. + * Note: the clickAction will need to be assigned post-creation. + * + * @param objectGraph Current object graph + * @param data The data for the Brick + * @param collectionDisplayStyle The desired display style for the brick + * @param metricsOptions Metrics options for impressions + * @param lockupMetricsOptions Metrics options for the collection icon lockups + * @param clientIdentifierOverride Any preferred client identifier override + * @returns A built Brick object + */ +export function buildBrick(objectGraph, data, collectionDisplayStyle, metricsOptions, lockupMetricsOptions, clientIdentifierOverride) { + const brick = new models.Brick(); + const editorialClientParams = editorialDataUtil.extractEditorialClientParams(objectGraph, data); + const hideEditorialMedia = serverData.asBooleanOrFalse(editorialClientParams.hideEditorialMedia); + if (!hideEditorialMedia) { + const editorialMediaData = editorialMediaUtil.editorialMediaDataFromData(objectGraph, data, collectionDisplayStyle); + if (isSome(editorialMediaData === null || editorialMediaData === void 0 ? void 0 : editorialMediaData.artwork)) { + brick.artworks = [editorialMediaData.artwork]; + brick.isMediaDark = isMediaDark(objectGraph, editorialMediaData); + } + if (isSome(editorialMediaData === null || editorialMediaData === void 0 ? void 0 : editorialMediaData.rtlArtwork)) { + brick.rtlArtwork = editorialMediaData === null || editorialMediaData === void 0 ? void 0 : editorialMediaData.rtlArtwork; + brick.isRTLMediaDark = isMediaDark(objectGraph, editorialMediaData, true); + } + } + const lockupOptions = { + metricsOptions: lockupMetricsOptions, + clientIdentifierOverride: clientIdentifierOverride, + artworkUseCase: 0 /* content.ArtworkUseCase.Default */, + canDisplayArcadeOfferButton: true, + externalDeepLinkUrl: deepLinkUrlFromData(objectGraph, data), + }; + metricsHelpersLocation.pushBasicLocation(objectGraph, lockupOptions.metricsOptions, "collectionIcons"); + brick.collectionIcons = editorialMediaUtil.editorialFallbackAppIconsFromData(objectGraph, data, lockupOptions); + metricsHelpersLocation.popLocation(lockupOptions.metricsOptions.locationTracker); + if (serverData.isNullOrEmpty(brick.collectionIcons)) { + const primaryAppIcon = content.iconFromData(objectGraph, data, { + useCase: 0 /* content.ArtworkUseCase.Default */, + }); + if (serverData.isDefinedNonNullNonEmpty(primaryAppIcon)) { + brick.collectionIcons = [primaryAppIcon]; + } + } + brick.caption = editorialCopyUtil.editorialBadgeFromData(objectGraph, data, collectionDisplayStyle); + brick.title = editorialCopyUtil.editorialTitleFromData(objectGraph, data, collectionDisplayStyle); + brick.subtitle = editorialCopyUtil.editorialDescriptionFromData(objectGraph, data); + brick.accessibilityLabel = brick.title; + brick.editorialDisplayOptions = editorialDataUtil.editorialDisplayOptionsFromClientParams(editorialClientParams); + // Configure impressions + const metricsIdType = serverData.isDefinedNonNullNonEmpty(brick.artworks) + ? "its_contentId" + : "collection_id"; + const contentMetricsOptions = { + ...metricsOptions, + id: data.id, + idType: metricsIdType, + }; + const impressionOptions = metricsHelpersImpressions.impressionOptions(objectGraph, data, brick.title, contentMetricsOptions); + metricsHelpersImpressions.addImpressionFields(objectGraph, brick, impressionOptions); + // Safe area + brick.artworkSafeArea = models.ChartOrCategorySafeArea.defaultTileArtworkSafeArea; + brick.textSafeArea = models.ChartOrCategorySafeArea.defaultTileTextSafeArea; + return brick; +} +// endregion +//# sourceMappingURL=editorial-page-brick-collection-shelf-builder.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-shelf-builder/editorial-page-collection-shelf-builder/editorial-page-collection-shelf-common.js b/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-shelf-builder/editorial-page-collection-shelf-builder/editorial-page-collection-shelf-common.js new file mode 100644 index 0000000..5af39c4 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-shelf-builder/editorial-page-collection-shelf-builder/editorial-page-collection-shelf-common.js @@ -0,0 +1,248 @@ +import { buildEditorialShelf } from ".."; +import * as models from "../../../../api/models"; +import * as serverData from "../../../../foundation/json-parsing/server-data"; +import { Parameters, Path, Protocol } from "../../../../foundation/network/url-constants"; +import * as urls from "../../../../foundation/network/urls"; +import * as mediaUrlMapping from "../../../builders/url-mapping"; +import * as metricsHelpersClicks from "../../../metrics/helpers/clicks"; +import * as metricsHelpersLocation from "../../../metrics/helpers/location"; +import { createLocationTrackerCopy, setCurrentPosition } from "../../../metrics/helpers/location"; +import * as placeholders from "../../../placeholders/placeholders"; +import { createBaseShelfToken } from "../../editorial-page-shelf-token"; +import { collectionDisplayStyleOverride, CollectionShelfDisplayStyle, CollectionShelfFilterOverride, EditorialShelfType, } from "../../editorial-page-types"; +import { isNothing, isSome } from "@jet/environment"; +import * as gamesComponentBuilder from "../../../../gameservicesui/src/editorial-page/editorial-component-builder"; +import { makeEditorialShelfCollectionPageIntent } from "../../../../api/intents/editorial/editorial-shelf-collection-page-intent"; +import { makeEditorialShelfCollectionPageURL } from "../../editorial-shelf-collection-page-utils"; +import { getLocale } from "../../../locale"; +import { getPlatform } from "../../../preview-platform"; +import { collectionShelfDisplayStyleFromShelfData } from "../../editorial-data-util"; +// region Shelf Tokens +/** + * Whether we should defer building the rest of a shelf given a shelf token + * @param token + */ +export function shouldDefer(token) { + return token && token.isDeferring && token.isFirstRender; +} +// endregion +// region Shelf Urls +/** + * Configure `url` on a standard editorial page shelf if it needs to fetch more content. + * @param objectGraph + * @param shelf The shelf we'll be adding the url to + * @param shelfToken Token to encode in URL for subsequent fetch. + */ +export function createShelfTokenUrlIfNecessary(objectGraph, shelf, shelfToken) { + if (serverData.isNullOrEmpty(shelfToken)) { + return null; + } + if (objectGraph.client.isWeb) { + return null; + } + const shouldAddFirstRenderUrl = shelfToken.remainingItems.length || shelfToken.recommendationsHref || shelfToken.onDeviceRecommendationsUseCase; + if (shouldAddFirstRenderUrl && shelfToken.isFirstRender) { + return editorialPageShelfUrl(shelfToken); + } + else { + return null; + } +} +/** + * Updates a shelf URL based on the provided token. + * @param objectGraph Current object graph + * @param shelf Shelf to add url to. + * @param token Token to encode in the url. + */ +function updateShelfTokenUrlWithNewShelfToken(objectGraph, shelf, shelfToken) { + const originalShelfUrl = urls.URL.from(shelf.url); + const updatedUrl = urls.URL.from(createShelfTokenUrlIfNecessary(objectGraph, shelf, shelfToken)); + // Add missing query params to the updated URL from the original. + for (const key of Object.keys(originalShelfUrl.query)) { + if (serverData.isNull(updatedUrl.query[key])) { + updatedUrl.query[key] = originalShelfUrl.query[key]; + } + } + // Finally, update the shelf's URL. + shelf.url = updatedUrl.build(); +} +/** + * Stores the impression metrics for a placeholder shelf in the shelf token. + * @param objectGraph Current object graph + * @param shelf The shelf + * @param shelfToken The shelf token + */ +export function storePlaceholderShelfImpressionMetrics(objectGraph, shelf, shelfToken) { + if (isNothing(shelf.url) || isNothing(shelf.impressionMetrics) || !shelfToken.showingPlaceholders) { + return; + } + const originalShelfUrlString = shelf.url; + try { + // Extract the token from the URL. + // Note: Although we have access to the shelfToken here, we do not know that + // the url was constructed from token in its current state. To be safe, + // if not efficient, we reverse engineer the URL to get the token. + const originalShelfUrl = urls.URL.from(originalShelfUrlString); + const encodedToken = originalShelfUrl.query[Parameters.token]; + const shelfTokenFromUrl = JSON.parse(decodeURIComponent(encodedToken)); + // Modify the token to include the impressions metrics. + shelfTokenFromUrl.originalPlaceholderShelfImpressionMetrics = shelf.impressionMetrics; + updateShelfTokenUrlWithNewShelfToken(objectGraph, shelf, shelfTokenFromUrl); + } + catch { + shelf.url = originalShelfUrlString; + } +} +/** + * // Merge the original impression metrics with the newly created impression metrics. + * @param objectGraph Current object graph + * @param shelf The hydrated shelf + * @param shelfToken The shelf token + */ +export function mergeShelfImpressionMetricsWithPlaceholders(objectGraph, shelf, shelfToken) { + if (serverData.isNullOrEmpty(shelfToken.originalPlaceholderShelfImpressionMetrics)) { + return; + } + // If the `shelf.impressionMetrics` is null, we just defer to the original metrics. + if (serverData.isNull(shelf.impressionMetrics)) { + shelf.impressionMetrics = shelfToken.originalPlaceholderShelfImpressionMetrics; + } + else { + for (const key in shelfToken.originalPlaceholderShelfImpressionMetrics.fields) { + if (Object.prototype.hasOwnProperty.call(shelfToken.originalPlaceholderShelfImpressionMetrics.fields, key)) { + shelf.impressionMetrics.fields[key] = shelfToken.originalPlaceholderShelfImpressionMetrics.fields[key]; + } + } + } +} +/** + * Configure `url` on a standard editorial page shelf if it needs to fetch more content. + * @param objectGraph + * @param shelf The shelf we'll be adding the url to + * @param shelfToken Token to encode in URL for subsequent fetch. + */ +export function createShelfSeeAllActionIfNecessary(objectGraph, shelf, shelfToken) { + if (serverData.isNullOrEmpty(shelfToken) || serverData.isNullOrEmpty(shelf)) { + return null; + } + if (shouldShowSeeAll(shelfToken)) { + const action = new models.FlowAction("page"); + // The web client makes a separate request for See All pages, so we don't sidepack for web. + if (!placeholders.isPlaceholderShelf(shelf) && !objectGraph.client.isWeb) { + const contentShelfId = `shelf_${shelfToken.id}`; + action.pageData = createSeeAllPage(objectGraph, shelfToken, shelf.contentType, contentShelfId, isSome(shelfToken.recommendationsHref)); + } + const additionalPageUrlQueryParams = {}; + if (shelfToken.type === EditorialShelfType.Recommendations) { + additionalPageUrlQueryParams[Parameters.editorialPageId] = shelfToken.pageId; + } + if (objectGraph.client.isWeb) { + const destinationIntent = makeEditorialShelfCollectionPageIntent({ + ...getLocale(objectGraph), + ...getPlatform(objectGraph), + id: shelfToken.id, + }); + action.destination = destinationIntent; + action.pageUrl = makeEditorialShelfCollectionPageURL(objectGraph, destinationIntent); + } + else { + action.pageUrl = mediaUrlMapping.hrefToRoutableUrl(objectGraph, shelfToken.data.href, additionalPageUrlQueryParams); + } + action.title = objectGraph.loc.string("ACTION_SEE_ALL"); + action.referrerUrl = shelfToken.metricsPageInformation.pageUrl; + // This can be called after we've gone through and added items to a shelf, meaning the locationPosition + // would have been incremented to the number of shelf items, the see all should always have a position of + // 0 so we reset it here and use that, without mucking with the active location tracker. + const resetLocationTracker = createLocationTrackerCopy(shelfToken.metricsLocationTracker); + setCurrentPosition(resetLocationTracker, 0); + metricsHelpersClicks.addClickEventToSeeAllAction(objectGraph, action, shelfToken.seeAllUrl, { + pageInformation: shelfToken.metricsPageInformation, + locationTracker: resetLocationTracker, + }); + return action; + } + else { + return null; + } +} +/** + * Determine whether lockup shelves should show "See All" on given platforms. + * + * @param shelfToken Token to encode in URL for subsequent fetch. + * @return Whether or not a lockup shelf with given properties should have a "See All" action. + */ +export function shouldShowSeeAll(shelfToken) { + if (shelfToken.isSeeAll) { + return false; + } + switch (shelfToken.collectionDisplayStyle) { + case CollectionShelfDisplayStyle.BreakoutLarge: + case CollectionShelfDisplayStyle.Poster: + return false; + default: + break; + } + // On all other platforms, we always to attach "See All", except AppStore_ComingSoon, + // which should not because the see all page won't drop lockups whose pre-order has + // been released (which the AppStore_ComingSoon swoosh will). + // + // The Games app is able to show the see all irrespective of the preorder flag. + return (!shelfToken.filterOverrides.includes(CollectionShelfFilterOverride.ShowOnlyPreorder) || + preprocessor.GAMES_TARGET); +} +/** + * Returns the URL schema for editorial page shelves that may need to fetch additional content. + * Editorial-Page-Related builders can extend on this scheme if needed, e.g. query param on Continue Playing shelves. + * @param shelfToken Token to encode in URL for subsequent fetch. + * @param parseContext + */ +export function editorialPageShelfUrl(shelfToken) { + const shelfUrl = new urls.URL() + .set("protocol", Protocol.internal) + .append("pathname", Path.editorialPage) + .append("pathname", Path.shelf) + .param(Parameters.token, encodeURIComponent(JSON.stringify(shelfToken))); + return shelfUrl.build(); +} +/** + * Creates a page that can be used for side-packing see all pages into a room. + * + * @param objectGraph + * @param {EditorialPageShelfToken} shelfToken The shelf token used to build the original shelf + * @param {ShelfContentType} preferredShelfContentType The content type to use for the page + * @param {string} parentShelfId The ID of the parent shelf that the contains the sidepack items + * @returns {GenericPage} A GenericPage which will use the parentShelfItems from the see all to render the initial room + */ +export function createSeeAllPage(objectGraph, shelfToken, preferredShelfContentType, parentShelfId, isComplete) { + // Create a new shelf token with a new location tracker in order to get accurate metrics for the items in the shelf. + const newLocationTracker = metricsHelpersLocation.newLocationTracker(); + const collectionDisplayStyle = collectionShelfDisplayStyleFromShelfData(objectGraph, shelfToken.data); + const newToken = createBaseShelfToken(objectGraph, shelfToken.pageId, shelfToken.data, shelfToken.isArcadePage, shelfToken.shelfIndex, shelfToken.metricsPageInformation, newLocationTracker, true, collectionDisplayStyleOverride(collectionDisplayStyle)); + const contentShelf = buildEditorialShelf(objectGraph, undefined, newToken); + if (contentShelf === null) { + return null; + } + contentShelf.title = null; + contentShelf.eyebrow = null; + contentShelf.isHorizontal = false; + contentShelf.id = parentShelfId; + let pageHeader; + if (preprocessor.GAMES_TARGET) { + pageHeader = gamesComponentBuilder.makeGamesPageHeader(shelfToken, shelfToken.title, contentShelf.items, shelfToken.subtitle); + } + else { + pageHeader = new models.PageHeader(null, shelfToken.title, null); + } + const headerShelf = new models.Shelf("pageHeader"); + headerShelf.id = "shelf_page_header"; + headerShelf.items = [pageHeader]; + const shelves = [headerShelf, contentShelf]; + const page = new models.GenericPage(shelves); + page.isIncomplete = !(isComplete !== null && isComplete !== void 0 ? isComplete : false); + if (isNothing(page.pageMetrics.pageFields)) { + page.pageMetrics.pageFields = {}; + } + return page; +} +// endregion +//# sourceMappingURL=editorial-page-collection-shelf-common.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-shelf-builder/editorial-page-collection-shelf-builder/editorial-page-hero-collection-shelf-builder.js b/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-shelf-builder/editorial-page-collection-shelf-builder/editorial-page-hero-collection-shelf-builder.js new file mode 100644 index 0000000..adc7087 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-shelf-builder/editorial-page-collection-shelf-builder/editorial-page-hero-collection-shelf-builder.js @@ -0,0 +1,137 @@ +import * as models from "../../../../api/models"; +import * as serverData from "../../../../foundation/json-parsing/server-data"; +import * as mediaAttributes from "../../../../foundation/media/attributes"; +import * as mediaDataStructure from "../../../../foundation/media/data-structure"; +import { shallowCopyOf } from "../../../../foundation/util/objects"; +import * as breakoutsCommon from "../../../arcade/breakouts-common"; +import * as contentAttributes from "../../../content/attributes"; +import * as metricsHelpersImpressions from "../../../metrics/helpers/impressions"; +import * as metricsHelpersLocation from "../../../metrics/helpers/location"; +import * as article from "../../../today/article"; +import * as editorialDataUtil from "../../editorial-data-util"; +import * as editorialMediaUtil from "../../editorial-media-util"; +import { editorialTitleFromData } from "../../editorial-page-editorial-copy-util"; +import { heroOverlayFromData } from "../../editorial-page-hero-util"; +import * as editorialOverlayContentUtil from "../../editorial-page-overlay-content-util"; +import { CollectionShelfDisplayStyle } from "../../editorial-page-types"; +import { createShelfTokenUrlIfNecessary, shouldDefer } from "./editorial-page-collection-shelf-common"; +export function buildHeroShelf(objectGraph, shelfToken) { + if (shelfToken.shelfIndex !== 0) { + return null; + } + const shelf = new models.Shelf("heroCarousel"); + const heroCarousel = new models.HeroCarousel(); + heroCarousel.autoScrollConfiguration = heroAutoscrollConfiguration(objectGraph); + const shelfContents = editorialDataUtil.extractRemainingItemsOrInitialShelfContents(objectGraph, shelfToken); + for (const itemData of shelfContents) { + const primaryItemData = editorialDataUtil.extractHydratedPrimaryContentData(objectGraph, itemData); + if ((editorialDataUtil.requiresPrimaryContent(objectGraph, itemData) && + !mediaAttributes.hasAttributes(primaryItemData)) || + shouldDefer(shelfToken)) { + shelfToken.isDeferring = true; + shelfToken.remainingItems.push(itemData); + continue; + } + const heroCarouselItemMetricsOptions = { + ...shelfToken.metricsImpressionOptions, + idType: "its_contentId", + targetType: "hero", + pageInformation: shelfToken.metricsPageInformation, + locationTracker: shelfToken.metricsLocationTracker, + recoMetricsData: mediaDataStructure.metricsFromMediaApiObject(itemData), + }; + const heroMediaData = editorialMediaUtil.editorialMediaDataForPlacement(objectGraph, itemData, editorialMediaUtil.EditorialMediaPlacement.Hero); + const lockupOptions = { + metricsOptions: heroCarouselItemMetricsOptions, + clientIdentifierOverride: shelfToken.clientIdentifierOverride, + artworkUseCase: 0 /* content.ArtworkUseCase.Default */, + offerEnvironment: "lightOverArtwork", + canDisplayArcadeOfferButton: true, + }; + const overlayContent = editorialOverlayContentUtil.extractOverlayContent(objectGraph, itemData, lockupOptions); + const heroCarouselItem = new models.HeroCarouselItem(); + const productData = article.productDataFromArticle(objectGraph, itemData); + const metricsTitle = editorialTitleFromData(objectGraph, itemData, CollectionShelfDisplayStyle.Hero); + const heroCarouselItemImpressionOptions = metricsHelpersImpressions.impressionOptions(objectGraph, itemData, metricsTitle, heroCarouselItemMetricsOptions); + heroCarouselItemImpressionOptions.isPreorder = contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, productData, "isPreorder"); + metricsHelpersImpressions.addImpressionFields(objectGraph, heroCarouselItem, heroCarouselItemImpressionOptions); + // Push the heroCarouselItem here so that the click action has the item in its location + // but we do not want to add it to the overall location tracker, so pop it right after adding it to the button + // actiono + // <rdar://problem/60883269> Metrics: Arcade: Container values requested in Location field + metricsHelpersLocation.pushContentLocation(objectGraph, heroCarouselItemImpressionOptions, metricsTitle); + const editorialClientParams = editorialDataUtil.extractEditorialClientParams(objectGraph, itemData); + const hideEditorialMedia = serverData.asBooleanOrFalse(editorialClientParams.hideEditorialMedia); + heroCarouselItem.overlay = heroOverlayFromData(objectGraph, itemData, shelfToken, editorialClientParams); + heroCarouselItem.collectionIcons = overlayContent.collectionIcons; + heroCarouselItem.editorialDisplayOptions = + editorialDataUtil.editorialDisplayOptionsFromClientParams(editorialClientParams); + populateHeroCarouselItemMedia(objectGraph, heroCarouselItem, hideEditorialMedia, heroMediaData, false); + if (heroCarouselItem.isValid()) { + heroCarousel.items.push(heroCarouselItem); + } + const rtlHeroCarouselItem = shallowCopyOf(heroCarouselItem); + populateHeroCarouselItemMedia(objectGraph, rtlHeroCarouselItem, hideEditorialMedia, heroMediaData, true); + if (rtlHeroCarouselItem.isValid()) { + heroCarousel.rtlItems.push(rtlHeroCarouselItem); + } + metricsHelpersLocation.popLocation(heroCarouselItemImpressionOptions.locationTracker); + metricsHelpersLocation.nextPosition(heroCarouselItemImpressionOptions.locationTracker); + } + // We don't need this in our incomplete shelf URL, so we'll preemptively remove it. + delete shelfToken.maxItemCount; + shelf.isHorizontal = false; + shelf.eyebrow = shelfToken.eyebrow; + shelf.eyebrowArtwork = shelfToken.eyebrowArtwork; + shelf.title = shelfToken.title; + shelf.subtitle = shelfToken.subtitle; + shelf.items = [heroCarousel]; + shelf.url = createShelfTokenUrlIfNecessary(objectGraph, shelf, shelfToken); + return shelf; +} +/** + * Populates the media properties of a `HeroCarouselItem`. + * @param objectGraph Current object graph + * @param heroCarouselItem The item to populate + * @param hideEditorialMedia Whether to hide editorial media + * @param heroMediaData The `EditorialMediaData` to derive the media properties from + * @param isRTL Whether this item will be used in an RTL layout + */ +function populateHeroCarouselItemMedia(objectGraph, heroCarouselItem, hideEditorialMedia, heroMediaData, isRTL) { + if (!hideEditorialMedia) { + const backgroundColor = isRTL ? heroMediaData === null || heroMediaData === void 0 ? void 0 : heroMediaData.rtlBackgroundColor : heroMediaData === null || heroMediaData === void 0 ? void 0 : heroMediaData.backgroundColor; + heroCarouselItem.backgroundColor = backgroundColor; + const artworkData = isRTL ? heroMediaData === null || heroMediaData === void 0 ? void 0 : heroMediaData.rtlArtworkData : heroMediaData === null || heroMediaData === void 0 ? void 0 : heroMediaData.artworkData; + if (!objectGraph.client.isMac || objectGraph.props.isNotEnabled("macOSArcadeHeaderUpdates")) { + heroCarouselItem.titleEffect = breakoutsCommon.titleEffectFromArtwork(objectGraph, artworkData); + } + const artwork = isRTL ? heroMediaData === null || heroMediaData === void 0 ? void 0 : heroMediaData.rtlArtwork : heroMediaData === null || heroMediaData === void 0 ? void 0 : heroMediaData.artwork; + heroCarouselItem.artwork = artwork; + const video = isRTL ? heroMediaData === null || heroMediaData === void 0 ? void 0 : heroMediaData.rtlVideo : heroMediaData === null || heroMediaData === void 0 ? void 0 : heroMediaData.video; + heroCarouselItem.video = video; + } + heroCarouselItem.isMediaDark = editorialMediaUtil.isMediaDark(objectGraph, heroMediaData, isRTL); +} +/** + * Generates the auto scroll configuration for the hero carousel + * @param objectGraph Current object graph + * @returns Built auto scroll configuration + */ +export function heroAutoscrollConfiguration(objectGraph) { + return { + isAutoScrollEnabled: objectGraph.bag.heroCarouselAutoScrollDuration > 0.0, + autoScrollInterval: objectGraph.bag.heroCarouselAutoScrollDuration, + }; +} +/** + * Decorates the input shelf metrics options with shelf specific fields. + * @param objectGraph Current object graph + * @param shelfMetricsOptions Base shelf metrics options + * @returns Decorated shelf metrics options + */ +export function decorateHeroShelfMetricsOptions(objectGraph, shelfMetricsOptions) { + shelfMetricsOptions.title = "heroCarousel"; + const autoScrollConfiguration = heroAutoscrollConfiguration(objectGraph); + shelfMetricsOptions.autoAdvanceInterval = autoScrollConfiguration.autoScrollInterval; +} +//# sourceMappingURL=editorial-page-hero-collection-shelf-builder.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-shelf-builder/editorial-page-collection-shelf-builder/editorial-page-lockup-collection-shelf-builder.js b/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-shelf-builder/editorial-page-collection-shelf-builder/editorial-page-lockup-collection-shelf-builder.js new file mode 100644 index 0000000..12525c4 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-shelf-builder/editorial-page-collection-shelf-builder/editorial-page-lockup-collection-shelf-builder.js @@ -0,0 +1,193 @@ +import { isSome } from "@jet/environment"; +import * as models from "../../../../api/models"; +import * as serverData from "../../../../foundation/json-parsing/server-data"; +import * as mediaAttributes from "../../../../foundation/media/attributes"; +import * as filtering from "../../../filtering"; +import * as metricsHelpersLocation from "../../../metrics/helpers/location"; +import { insertPlaceholdersIntoShelfIfRequired } from "../../../placeholders/placeholders"; +import * as editorialDataUtil from "../../editorial-data-util"; +import { lockupFromData } from "../../editorial-page-lockup-utils"; +import { CollectionShelfDisplayStyle, CollectionShelfFilterOverride, collectionShelfLayoutDirection, CollectionShelfLayoutDirection, displayLimitForCollectionShelf, } from "../../editorial-page-types"; +import { createShelfTokenUrlIfNecessary, shouldDefer } from "./editorial-page-collection-shelf-common"; +import * as gamesComponentBuilder from "../../../../gameservicesui/src/editorial-page/editorial-component-builder"; +import * as charts from "../../../../gameservicesui/src/utility/charts"; +import { arcadeColor } from "../../../arcade/arcade-common"; +import { artworkTemplateForBundleImage, createArtworkForResource } from "../../../content/artwork/artwork"; +export function buildEditorialLockupHierarchicalPortrait(objectGraph, shelfToken) { + return buildLockupShelf(objectGraph, shelfToken, CollectionShelfDisplayStyle.EditorialLockupHierarchicalPortrait, "mediumImageLockup", true, CollectionShelfLayoutDirection.Vertical); +} +export function buildEditorialLockupHierarchicalRows(objectGraph, shelfToken) { + return buildLockupShelf(objectGraph, shelfToken, CollectionShelfDisplayStyle.EditorialLockupHierarchicalRows, "mediumImageLockup", true, CollectionShelfLayoutDirection.Vertical); +} +export function buildEditorialLockupMediumShelf(objectGraph, shelfToken) { + return buildLockupShelf(objectGraph, shelfToken, CollectionShelfDisplayStyle.EditorialLockupMedium, "mediumImageLockup"); +} +export function buildEditorialLockupMediumVariantShelf(objectGraph, shelfToken) { + return buildLockupShelf(objectGraph, shelfToken, CollectionShelfDisplayStyle.EditorialLockupMediumVariant, "mediumImageLockup"); +} +export function buildEditorialLockupLargeShelf(objectGraph, shelfToken) { + return buildLockupShelf(objectGraph, shelfToken, CollectionShelfDisplayStyle.EditorialLockupLarge, "largeImageLockup"); +} +export function buildEditorialLockupLargeVariantShelf(objectGraph, shelfToken) { + return buildLockupShelf(objectGraph, shelfToken, CollectionShelfDisplayStyle.EditorialLockupLargeVariant, "largeImageLockup"); +} +export function buildLockupChartShelf(objectGraph, shelfToken) { + return buildLockupShelf(objectGraph, shelfToken, CollectionShelfDisplayStyle.Charts, shelfToken.isSeeAll ? "smallLockup" : "mediumImageLockup", true, CollectionShelfLayoutDirection.Vertical); +} +export function buildLockupMedium(objectGraph, shelfToken) { + shelfToken.metricsImpressionOptions.shouldOmitImpressionIndex = true; + return buildLockupShelf(objectGraph, shelfToken, CollectionShelfDisplayStyle.LockupMedium, "mediumLockup"); +} +export function buildLockupSmallShelf(objectGraph, shelfToken, insertPlaceholdersIfRequired = true, overrideShelfLayoutDirection) { + if (objectGraph.featureFlags.isEnabled("force_display_lockup_ordinals")) { + // For 2023F Charts testing before server is ready. Can be removed in the 2024A timeline. + shelfToken.showOrdinals = true; + } + return buildLockupShelf(objectGraph, shelfToken, CollectionShelfDisplayStyle.LockupSmall, "smallLockup", insertPlaceholdersIfRequired, overrideShelfLayoutDirection); +} +export function buildLockupLargeShelf(objectGraph, shelfToken, insertPlaceholdersIfRequired = true) { + return buildLockupShelf(objectGraph, shelfToken, CollectionShelfDisplayStyle.LockupLarge, "largeLockup", insertPlaceholdersIfRequired); +} +export function buildEditorialLockupPosterShelf(objectGraph, shelfToken) { + const shelf = buildLockupShelf(objectGraph, shelfToken, CollectionShelfDisplayStyle.Poster, "posterLockup"); + if (isSome(shelf)) { + shelf.horizontalScrollTargetBehavior = models.ShelfHorizontalScrollTargetBehavior.CenterAligned; + } + return shelf; +} +// region Lockup Creation +function buildLockupShelf(objectGraph, shelfToken, displayStyle, lockupComponentType, insertPlaceholdersIfRequired = true, overrideShelfLayoutDirection) { + const items = []; + const shelf = new models.Shelf(lockupComponentType); + const layoutDirection = overrideShelfLayoutDirection !== null && overrideShelfLayoutDirection !== void 0 ? overrideShelfLayoutDirection : collectionShelfLayoutDirection(shelfToken); + shelf.isHorizontal = layoutDirection === CollectionShelfLayoutDirection.Horizontal; + const shelfContents = editorialDataUtil.extractRemainingItemsOrInitialShelfContents(objectGraph, shelfToken); + const displayLimit = displayLimitForCollectionShelf(displayStyle, layoutDirection, shelfToken); + for (const [index, itemData] of shelfContents.entries()) { + // If we encounter a type of app-events, this means they have been incorrectly programmed, + // and we should throw the shelf away. + if (itemData.type === "app-events") { + return null; + } + if (serverData.isNull(itemData.attributes) || shouldDefer(shelfToken)) { + shelfToken.isDeferring = true; + shelfToken.remainingItems.push(itemData); + continue; + } + // Filter out unwanted content + if (filtering.shouldFilter(objectGraph, itemData, 80894 /* filtering.Filter.All */)) { + continue; + } + let lockup = null; + if (preprocessor.GAMES_TARGET && gamesComponentBuilder.shouldUseChartCard(displayStyle, shelfToken)) { + lockup = gamesComponentBuilder.makeChartCard(objectGraph, itemData, shelfToken, index, "Editorial"); + } + else if (preprocessor.GAMES_TARGET && gamesComponentBuilder.shouldUseMediaCard(displayStyle)) { + lockup = gamesComponentBuilder.makeEditorialMediaCard(objectGraph, itemData, "Card", shelfToken, "Editorial"); + } + else if (preprocessor.GAMES_TARGET && gamesComponentBuilder.shouldUseGameLockup(displayStyle)) { + lockup = gamesComponentBuilder.makeGameLockupShelfModel(objectGraph, itemData, shelfToken); + } + else { + lockup = lockupFromData(objectGraph, itemData, shelfToken, items.length, displayStyle); + } + if (lockup) { + items.push(lockup); + metricsHelpersLocation.nextPosition(shelfToken.metricsImpressionOptions.locationTracker); + shelfToken.ordinalIndex++; + if (items.length === displayLimit) { + break; + } + } + } + if (displayStyle === CollectionShelfDisplayStyle.EditorialLockupMediumVariant || + displayStyle === CollectionShelfDisplayStyle.EditorialLockupLargeVariant) { + if (serverData.isNull(shelf.presentationHints)) { + shelf.presentationHints = { showSupplementaryText: true }; + } + else { + shelf.presentationHints = { + ...shelf.presentationHints, + showSupplementaryText: true, + }; + } + } + // We don't need this in our incomplete shelf URL, so we'll preemptively remove it. + delete shelfToken.maxItemCount; + shelf.eyebrow = shelfToken.eyebrow; + shelf.eyebrowArtwork = shelfToken.eyebrowArtwork; + shelf.title = shelfToken.title; + shelf.subtitle = shelfToken.subtitle; + // Hydrate Games data if the eyebrow has not already been populated from the shelf token. + if (preprocessor.GAMES_TARGET && shelf.eyebrow == null && shelf.eyebrowArtwork == null) { + populateGamesHeaderContent(shelf, shelfToken, objectGraph); + } + if (preprocessor.GAMES_TARGET && + gamesComponentBuilder.shouldUseComponentGrid(displayStyle, layoutDirection, shelfToken)) { + const item = gamesComponentBuilder.makeComponentGrid(objectGraph, items, shelfToken, displayStyle); + shelf.items = isSome(item) ? [item] : []; + } + else { + shelf.items = items; + } + shelf.rowsPerColumn = mediaAttributes.attributeAsNumber(shelfToken.data, "rowCount"); + shelf.shouldFilterApps = !shelfToken.filterOverrides.includes(CollectionShelfFilterOverride.ShowInstalled); + shelf.url = createShelfTokenUrlIfNecessary(objectGraph, shelf, shelfToken); + if (insertPlaceholdersIfRequired) { + let placeholderDisplayLimit = displayLimit; + if (isSome(displayLimit) && + displayLimit > 4 && + (displayStyle === CollectionShelfDisplayStyle.Charts || + displayStyle === CollectionShelfDisplayStyle.EditorialLockupMedium)) { + // TODO: Remove this override when this radar is complete: rdar://148395979 ([EditorialPage] Placeholders for Charts and 4UP are not rendering as a ComponentGrid) + placeholderDisplayLimit = 4; + } + insertPlaceholdersIntoShelfIfRequired(objectGraph, shelf, shelfToken, placeholderDisplayLimit); + } + return shelf; +} +/** + * Hydrates game related header data + * + * Note that both the header and non header fields should be hydrated, since header + * preference can be switched off. In this case we will not have the capacity to color the Arcade eyebrow + */ +function populateGamesHeaderContent(shelf, shelfToken, objectGraph) { + switch (serverData.asString(shelfToken.data.attributes, "chart")) { + case "combined-game-center" /* TopChartType.CombinedGameCenter */: + const gameCenterEyebrow = charts.makeChartsTopSubtitle("game-center", objectGraph); + const gameCenterEyebrowArtwork = createArtworkForResource(objectGraph, artworkTemplateForBundleImage("ProductCapabilityGameCenter")); + shelf.eyebrow = gameCenterEyebrow; + shelf.eyebrowArtwork = gameCenterEyebrowArtwork; + shelf.header = { + eyebrow: gameCenterEyebrow, + eyebrowArtwork: gameCenterEyebrowArtwork, + eyebrowArtworkSize: 14, + title: shelfToken.title, + subtitle: shelfToken.subtitle, + }; + break; + case "top-arcade" /* TopChartType.TopArcade */: + const color = arcadeColor; + const arcadeEyebrow = charts.makeChartsTopSubtitle("arcade", objectGraph); + shelf.eyebrow = arcadeEyebrow; + shelf.header = { + eyebrow: arcadeEyebrow, + title: shelfToken.title, + subtitle: shelfToken.subtitle, + configuration: { + eyebrowColor: { + type: "rgb", + red: color.red, + green: color.green, + blue: color.blue, + }, + }, + }; + break; + default: + break; + } +} +// endregion +//# sourceMappingURL=editorial-page-lockup-collection-shelf-builder.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-shelf-builder/editorial-page-collection-shelf-builder/editorial-page-story-card-collection-shelf-builder.js b/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-shelf-builder/editorial-page-collection-shelf-builder/editorial-page-story-card-collection-shelf-builder.js new file mode 100644 index 0000000..3e78c25 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-shelf-builder/editorial-page-collection-shelf-builder/editorial-page-story-card-collection-shelf-builder.js @@ -0,0 +1,70 @@ +import * as models from "../../../../api/models"; +import { isNull } from "../../../../foundation/json-parsing/server-data"; +import * as mediaAttributes from "../../../../foundation/media/attributes"; +import * as mediaDataStructure from "../../../../foundation/media/data-structure"; +import * as metricsHelpersLocation from "../../../metrics/helpers/location"; +import { insertPlaceholdersIntoShelfIfRequired } from "../../../placeholders/placeholders"; +import { clickTargetForCollectionDisplayStyle } from "../../editorial-action-util"; +import * as editorialDataUtil from "../../editorial-data-util"; +import * as editorialMediaUtil from "../../editorial-media-util"; +import { CollectionShelfDisplayStyle, collectionShelfLayoutDirection } from "../../editorial-page-types"; +import { createShelfTokenUrlIfNecessary, shouldDefer } from "./editorial-page-collection-shelf-common"; +import { buildStoryCard } from "./editorial-page-story-card-utils"; +import * as gamesComponentBuilder from "../../../../gameservicesui/src/editorial-page/editorial-component-builder"; +import { isSome } from "@jet/environment/types/optional"; +export function buildSmallStoryCardShelf(objectGraph, shelfToken) { + return buildStoryCardShelf(objectGraph, shelfToken, CollectionShelfDisplayStyle.StorySmall, "smallStoryCard"); +} +export function buildMediumStoryCardShelf(objectGraph, shelfToken) { + return buildStoryCardShelf(objectGraph, shelfToken, CollectionShelfDisplayStyle.StoryMedium, "mediumStoryCard"); +} +function buildStoryCardShelf(objectGraph, shelfToken, displayStyle, storyComponentType) { + const items = []; + const shelf = new models.Shelf(storyComponentType); + const layoutDirection = collectionShelfLayoutDirection(shelfToken); + shelf.isHorizontal = layoutDirection === "Horizontal"; + const shelfContents = editorialDataUtil.extractRemainingItemsOrInitialShelfContents(objectGraph, shelfToken); + for (const itemData of shelfContents) { + const metricsOptions = { + ...shelfToken.metricsImpressionOptions, + targetType: clickTargetForCollectionDisplayStyle(objectGraph, displayStyle), + recoMetricsData: mediaDataStructure.metricsFromMediaApiObject(itemData), + }; + if (!mediaAttributes.hasAttributes(itemData) || shouldDefer(shelfToken)) { + shelfToken.remainingItems.push(itemData); + shelfToken.isDeferring = true; + continue; + } + let storyCard = null; + if (preprocessor.GAMES_TARGET && gamesComponentBuilder.shouldUseMediaCard(displayStyle)) { + storyCard = gamesComponentBuilder.makeEditorialMediaCard(objectGraph, itemData, "Card", shelfToken, "Editorial"); + } + else { + storyCard = buildStoryCard(objectGraph, itemData, editorialMediaUtil.EditorialMediaPlacement.StoryCard, metricsOptions, shelfToken.collectionDisplayStyle, true, shelfToken); + } + if (isNull(storyCard)) { + continue; + } + items.push(storyCard); + metricsHelpersLocation.nextPosition(shelfToken.metricsImpressionOptions.locationTracker); + } + // We don't need this in our incomplete shelf URL, so we'll preemptively remove it. + delete shelfToken.maxItemCount; + shelf.eyebrow = shelfToken.eyebrow; + shelf.eyebrowArtwork = shelfToken.eyebrowArtwork; + shelf.title = shelfToken.title; + shelf.subtitle = shelfToken.subtitle; + if (preprocessor.GAMES_TARGET && + gamesComponentBuilder.shouldUseComponentGrid(displayStyle, layoutDirection, shelfToken)) { + const item = gamesComponentBuilder.makeComponentGrid(objectGraph, items, shelfToken, displayStyle); + shelf.items = isSome(item) ? [item] : []; + } + else { + shelf.items = items; + } + shelf.rowsPerColumn = mediaAttributes.attributeAsNumber(shelfToken.data, "rowCount"); + shelf.url = createShelfTokenUrlIfNecessary(objectGraph, shelf, shelfToken); + insertPlaceholdersIntoShelfIfRequired(objectGraph, shelf, shelfToken); + return shelf; +} +//# sourceMappingURL=editorial-page-story-card-collection-shelf-builder.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-shelf-builder/editorial-page-collection-shelf-builder/editorial-page-story-card-utils.js b/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-shelf-builder/editorial-page-collection-shelf-builder/editorial-page-story-card-utils.js new file mode 100644 index 0000000..ff85e3d --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-shelf-builder/editorial-page-collection-shelf-builder/editorial-page-story-card-utils.js @@ -0,0 +1,128 @@ +import * as models from "../../../../api/models"; +import * as content from "../../../content/content"; +import * as mediaDataStructure from "../../../../foundation/media/data-structure"; +import * as metricsHelpersImpressions from "../../../metrics/helpers/impressions"; +import * as editorialCopyUtil from "../../editorial-page-editorial-copy-util"; +import * as editorialOverlayContentUtil from "../../editorial-page-overlay-content-util"; +import { asBooleanOrFalse, isDefinedNonNull, isDefinedNonNullNonEmpty, } from "../../../../foundation/json-parsing/server-data"; +import * as editorialDataUtil from "../../editorial-data-util"; +import * as editorialActionUtil from "../../editorial-action-util"; +import * as editorialMediaUtil from "../../editorial-media-util"; +import * as mediaAttributes from "../../../../foundation/media/attributes"; +import { deepLinkUrlFromData } from "../../../linking/external-deep-link"; +import { shouldFilter } from "../../../filtering"; +import { mixedMediaItemFromEditorialData } from "../../../../gameservicesui/src/common/mixed-media-item-editorial-artwork"; +/** + * Build an `EditorialStoryCard` from the provided data. + * @param objectGraph The object graph. + * @param itemData Data for the card item being created. + * @param mediaPlacement The placement within which the card is being created for media. + * @param metricsOptions A set of metrics options. + * @param collectionDisplayStyle The collection display style for the shelf this is in. + * @param allowSuppressLockup Whether any lockup can be suppressed + * @param shelfToken A shelf token, if being presented within a shelf. If not, it's being used in another context. + * @returns The built editorial story card. + */ +export function buildStoryCard(objectGraph, itemData, mediaPlacement, metricsOptions, collectionDisplayStyle, allowSuppressLockup, shelfToken) { + var _a, _b; + const editorialClientParams = editorialDataUtil.extractEditorialClientParams(objectGraph, itemData); + const storyCard = new models.EditorialStoryCard(); + storyCard.title = editorialCopyUtil.editorialTitleFromData(objectGraph, itemData, collectionDisplayStyle); + if (!editorialClientParams.suppressShort) { + storyCard.description = editorialCopyUtil.editorialDescriptionFromData(objectGraph, itemData); + } + const hideEditorialMedia = asBooleanOrFalse(editorialClientParams.hideEditorialMedia); + if (!hideEditorialMedia) { + const mediaData = editorialMediaUtil.editorialMediaDataForPlacement(objectGraph, itemData, mediaPlacement); + storyCard.artwork = mediaData === null || mediaData === void 0 ? void 0 : mediaData.artwork; + storyCard.video = mediaData === null || mediaData === void 0 ? void 0 : mediaData.video; + storyCard.isMediaDark = (_a = editorialMediaUtil.isMediaDark(objectGraph, mediaData)) !== null && _a !== void 0 ? _a : undefined; + if (mediaPlacement === editorialMediaUtil.EditorialMediaPlacement.StoryDetail) { + if (preprocessor.GAMES_TARGET) { + storyCard.mixedMediaItem = mixedMediaItemFromEditorialData(objectGraph, itemData, "DetailStory"); + } + const landscapeMediaData = editorialMediaUtil.editorialMediaDataForPlacement(objectGraph, itemData, editorialMediaUtil.EditorialMediaPlacement.StoryDetailLandscape); + storyCard.landscapeArtwork = landscapeMediaData === null || landscapeMediaData === void 0 ? void 0 : landscapeMediaData.artwork; + storyCard.landscapeVideo = landscapeMediaData === null || landscapeMediaData === void 0 ? void 0 : landscapeMediaData.video; + storyCard.isLandscapeMediaDark = + (_b = editorialMediaUtil.isMediaDark(objectGraph, landscapeMediaData)) !== null && _b !== void 0 ? _b : undefined; + } + } + const lockupOptions = { + metricsOptions: { + pageInformation: metricsOptions.pageInformation, + locationTracker: metricsOptions.locationTracker, + recoMetricsData: mediaDataStructure.metricsFromMediaApiObject(itemData), + }, + artworkUseCase: 1 /* content.ArtworkUseCase.LockupIconSmall */, + offerEnvironment: editorialClientParams.useMaterialBlur ? "light" : "lightOverArtwork", + canDisplayArcadeOfferButton: content.shelfDisplayStyleCanDisplayArcadeOfferButtons(objectGraph, collectionDisplayStyle), + isContainedInPreorderExclusiveShelf: false, + externalDeepLinkUrl: deepLinkUrlFromData(objectGraph, itemData), + useJoeColorIconPlaceholder: preprocessor.GAMES_TARGET, + }; + const overlayContent = editorialOverlayContentUtil.extractOverlayContent(objectGraph, itemData, lockupOptions); + const suppressLockup = allowSuppressLockup && asBooleanOrFalse(editorialClientParams.suppressLockup); + if (!suppressLockup) { + storyCard.lockup = overlayContent.lockup; + } + // For Arcade app that is preorder and bincompat, we should override the badge to always display "COMING SOON" in upper cased. + const lockupData = editorialDataUtil.extractProductData(objectGraph, itemData); + const isPreorder = mediaAttributes.attributeAsBooleanOrFalse(lockupData, "isPreorder"); + const isArcadeLockup = content.isArcadeSupported(objectGraph, lockupData); + const supportsVisionOSCompatibleIOSBinary = content.supportsVisionOSCompatibleIOSBinaryFromData(objectGraph, lockupData); + // If there is lockup data, and we're not suppressing the lockup, check the data is valid for this platform. + // If this is for a story detail page, ignore these checks so that the artwork card is still presented as expected. + if (isDefinedNonNullNonEmpty(lockupData) && + !suppressLockup && + shouldFilter(objectGraph, lockupData, 80894 /* Filter.All */) && + mediaPlacement !== editorialMediaUtil.EditorialMediaPlacement.StoryDetail) { + return null; + } + let badgeText; + if (isPreorder && isArcadeLockup && supportsVisionOSCompatibleIOSBinary) { + badgeText = objectGraph.loc.string("ARCADE_PREORDER_COMING_SOON"); + } + else { + badgeText = editorialCopyUtil.editorialBadgeFromData(objectGraph, itemData, collectionDisplayStyle); + } + if (objectGraph.client.isVision && badgeText === objectGraph.loc.string("APPLE_ARCADE")) { + storyCard.badge = { + type: "arcadeWordmark", + }; + } + else { + storyCard.badge = { + type: "text", + title: badgeText, + }; + } + storyCard.collectionIcons = overlayContent.collectionIcons; + if (isDefinedNonNull(shelfToken)) { + const baseClickOptions = { + id: itemData.id, + pageInformation: shelfToken.metricsPageInformation, + locationTracker: shelfToken.metricsLocationTracker, + actionDetails: { + franchise: storyCard.badge.title, + }, + idType: "editorial_id", + }; + storyCard.clickAction = editorialActionUtil.createPrimaryActionForComponentFromData(objectGraph, itemData, shelfToken, baseClickOptions); + } + storyCard.editorialDisplayOptions = + editorialDataUtil.editorialDisplayOptionsFromClientParams(editorialClientParams); + // Configure impressions + const contentMetricsOptions = { + ...metricsOptions, + id: itemData.id, + idType: "editorial_id", + }; + const impressionOptions = metricsHelpersImpressions.impressionOptions(objectGraph, itemData, storyCard.title, contentMetricsOptions); + metricsHelpersImpressions.addImpressionFields(objectGraph, storyCard, impressionOptions); + if (!storyCard.isValid()) { + return null; + } + return storyCard; +} +//# sourceMappingURL=editorial-page-story-card-utils.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-shelf-builder/editorial-page-collection-shelf-builder/editorial-page-text-only-collection-shelf-builder.js b/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-shelf-builder/editorial-page-collection-shelf-builder/editorial-page-text-only-collection-shelf-builder.js new file mode 100644 index 0000000..3f9f2b7 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-shelf-builder/editorial-page-collection-shelf-builder/editorial-page-text-only-collection-shelf-builder.js @@ -0,0 +1,54 @@ +import * as models from "../../../../api/models"; +import * as serverData from "../../../../foundation/json-parsing/server-data"; +import * as mediaAttributes from "../../../../foundation/media/attributes"; +import * as metricsHelpersLocation from "../../../metrics/helpers/location"; +import * as editorialActionUtil from "../../editorial-action-util"; +import * as editorialDataUtil from "../../editorial-data-util"; +import * as editorialCopyUtil from "../../editorial-page-editorial-copy-util"; +import { CollectionShelfLayoutDirection } from "../../editorial-page-types"; +import { createShelfTokenUrlIfNecessary, shouldDefer } from "./editorial-page-collection-shelf-common"; +import * as metricsHelpersImpressions from "../../../metrics/helpers/impressions"; +import { insertPlaceholdersIntoShelfIfRequired } from "../../../placeholders/placeholders"; +export function buildTextOnlyShelf(objectGraph, shelfToken) { + var _a; + const items = []; + const shelf = new models.Shelf("action"); + shelf.isHorizontal = + mediaAttributes.attributeAsString(shelfToken.data, "layoutDirection") === + CollectionShelfLayoutDirection.Horizontal; + const shelfContents = editorialDataUtil.extractRemainingItemsOrInitialShelfContents(objectGraph, shelfToken); + for (const itemData of shelfContents) { + if (serverData.isNull(itemData.attributes) || shouldDefer(shelfToken)) { + shelfToken.isDeferring = true; + shelfToken.remainingItems.push(itemData); + continue; + } + const action = editorialActionUtil.createPrimaryActionForComponentFromData(objectGraph, itemData, shelfToken); + action.title = editorialCopyUtil.editorialTitleFromData(objectGraph, itemData, shelfToken.collectionDisplayStyle); + // Configure impressions + const contentMetricsOptions = { + ...shelfToken.metricsImpressionOptions, + id: itemData.id, + idType: "editorial_id", + targetType: "textOnly", + }; + const impressionOptions = metricsHelpersImpressions.impressionOptions(objectGraph, itemData, action.title, contentMetricsOptions); + metricsHelpersImpressions.addImpressionFields(objectGraph, action, impressionOptions); + if (action.isValid()) { + items.push(action); + metricsHelpersLocation.nextPosition(shelfToken.metricsImpressionOptions.locationTracker); + } + } + // We don't need this in our incomplete shelf URL, so we'll preemptively remove it. + delete shelfToken.maxItemCount; + shelf.eyebrow = shelfToken.eyebrow; + shelf.eyebrowArtwork = shelfToken.eyebrowArtwork; + shelf.title = shelfToken.title; + shelf.subtitle = shelfToken.subtitle; + shelf.items = items; + shelf.rowsPerColumn = (_a = mediaAttributes.attributeAsNumber(shelfToken.data, "rowCount")) !== null && _a !== void 0 ? _a : 1; + shelf.url = createShelfTokenUrlIfNecessary(objectGraph, shelf, shelfToken); + insertPlaceholdersIntoShelfIfRequired(objectGraph, shelf, shelfToken); + return shelf; +} +//# sourceMappingURL=editorial-page-text-only-collection-shelf-builder.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-shelf-builder/editorial-page-collection-shelf-builder/editorial-page-text-with-artwork-collection-shelf-builder.js b/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-shelf-builder/editorial-page-collection-shelf-builder/editorial-page-text-with-artwork-collection-shelf-builder.js new file mode 100644 index 0000000..7587b49 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-shelf-builder/editorial-page-collection-shelf-builder/editorial-page-text-with-artwork-collection-shelf-builder.js @@ -0,0 +1,66 @@ +import * as models from "../../../../api/models"; +import * as serverData from "../../../../foundation/json-parsing/server-data"; +import * as mediaAttributes from "../../../../foundation/media/attributes"; +import * as content from "../../../content/content"; +import * as metricsHelpersLocation from "../../../metrics/helpers/location"; +import { insertPlaceholdersIntoShelfIfRequired } from "../../../placeholders/placeholders"; +import * as editorialActionUtil from "../../editorial-action-util"; +import * as editorialDataUtil from "../../editorial-data-util"; +import * as editorialCopyUtil from "../../editorial-page-editorial-copy-util"; +import { CollectionShelfLayoutDirection } from "../../editorial-page-types"; +import { createShelfTokenUrlIfNecessary, shouldDefer } from "./editorial-page-collection-shelf-common"; +import * as gamesComponentBuilder from "../../../../gameservicesui/src/editorial-page/editorial-component-builder"; +export function buildTextWithArtworkShelf(objectGraph, shelfToken) { + var _a; + const items = []; + const shelf = new models.Shelf("action"); + shelf.isHorizontal = + mediaAttributes.attributeAsString(shelfToken.data, "layoutDirection") === + CollectionShelfLayoutDirection.Horizontal; + const shelfContents = editorialDataUtil.extractRemainingItemsOrInitialShelfContents(objectGraph, shelfToken); + for (const itemData of shelfContents) { + if (serverData.isNull(itemData.attributes) || shouldDefer(shelfToken)) { + shelfToken.isDeferring = true; + shelfToken.remainingItems.push(itemData); + continue; + } + const action = editorialActionUtil.createPrimaryActionForComponentFromData(objectGraph, itemData, shelfToken); + action.title = editorialCopyUtil.editorialTitleFromData(objectGraph, itemData, shelfToken.collectionDisplayStyle); + let artwork; + if (preprocessor.GAMES_TARGET) { + const editorialArtwork = mediaAttributes.attributeAsDictionary(itemData, "editorialArtwork"); + artwork = serverData.asDictionary(editorialArtwork, "contentIconTrimmed"); + } + else { + artwork = mediaAttributes.attributeAsDictionary(itemData, "artwork"); + } + if (serverData.isDefinedNonNullNonEmpty(artwork)) { + action.artwork = content.artworkFromApiArtwork(objectGraph, artwork, { + allowingTransparency: true, + useCase: 20 /* content.ArtworkUseCase.CategoryIcon */, + }); + } + if (action.isValid()) { + items.push(action); + metricsHelpersLocation.nextPosition(shelfToken.metricsImpressionOptions.locationTracker); + } + } + // We don't need this in our incomplete shelf URL, so we'll preemptively remove it. + delete shelfToken.maxItemCount; + shelf.eyebrow = shelfToken.eyebrow; + shelf.eyebrowArtwork = shelfToken.eyebrowArtwork; + shelf.title = shelfToken.title; + shelf.subtitle = shelfToken.subtitle; + if (preprocessor.GAMES_TARGET) { + shelf.items = [gamesComponentBuilder.makeRibbonBar(objectGraph, items)]; + shelf.contentType = "ribbonBar"; + } + else { + shelf.items = items; + } + shelf.rowsPerColumn = (_a = mediaAttributes.attributeAsNumber(shelfToken.data, "rowCount")) !== null && _a !== void 0 ? _a : 1; + shelf.url = createShelfTokenUrlIfNecessary(objectGraph, shelf, shelfToken); + insertPlaceholdersIntoShelfIfRequired(objectGraph, shelf, shelfToken); + return shelf; +} +//# sourceMappingURL=editorial-page-text-with-artwork-collection-shelf-builder.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-shelf-builder/editorial-page-collection-shelf-builder/index.js b/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-shelf-builder/editorial-page-collection-shelf-builder/index.js new file mode 100644 index 0000000..ae02877 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-shelf-builder/editorial-page-collection-shelf-builder/index.js @@ -0,0 +1,113 @@ +import * as serverData from "../../../../foundation/json-parsing/server-data"; +import { attributeAsString } from "../../../../foundation/media/attributes"; +import { CollectionShelfDisplayStyle } from "../../editorial-page-types"; +import { buildBreakoutLargeShelf } from "./editorial-page-breakout-large-collection-shelf-builder"; +import { buildBrickLargeShelf, buildBrickMediumShelf, buildBrickSmallShelf, } from "./editorial-page-brick-collection-shelf-builder"; +import { createShelfSeeAllActionIfNecessary } from "./editorial-page-collection-shelf-common"; +import { buildHeroShelf } from "./editorial-page-hero-collection-shelf-builder"; +import { buildEditorialLockupLargeShelf, buildEditorialLockupLargeVariantShelf, buildEditorialLockupMediumShelf, buildEditorialLockupMediumVariantShelf, buildLockupLargeShelf, buildLockupSmallShelf, buildEditorialLockupPosterShelf, buildEditorialLockupHierarchicalRows, buildEditorialLockupHierarchicalPortrait, buildLockupMedium, } from "./editorial-page-lockup-collection-shelf-builder"; +import { buildMediumStoryCardShelf, buildSmallStoryCardShelf, } from "./editorial-page-story-card-collection-shelf-builder"; +import { buildTextOnlyShelf } from "./editorial-page-text-only-collection-shelf-builder"; +import { buildTextWithArtworkShelf } from "./editorial-page-text-with-artwork-collection-shelf-builder"; +import { collectionShelfDisplayStyleFromShelfData } from "../../editorial-data-util"; +import * as gamesHeroShelfBuilder from "../../../../gameservicesui/src/editorial-page/editorial-page-hero-collection-shelf-builder"; +import { isSome } from "@jet/environment"; +export function buildCollectionShelf(objectGraph, shelfToken) { + let collectionDisplayStyle = attributeAsString(shelfToken.data, "displayStyle"); + if (preprocessor.GAMES_TARGET) { + // A way to override display style as design wants to see large cards in "See all" rooms. + const shelfTokenStyle = shelfToken.collectionDisplayStyle; + if (isSome(shelfTokenStyle) && shelfTokenStyle !== collectionDisplayStyle) { + collectionDisplayStyle = shelfToken.collectionDisplayStyle; + } + } + if (serverData.isNullOrEmpty(collectionDisplayStyle)) { + return null; + } + let shelf = null; + switch (collectionDisplayStyle) { + case CollectionShelfDisplayStyle.Hero: + if (preprocessor.GAMES_TARGET) { + shelf = gamesHeroShelfBuilder.buildHeroShelf(objectGraph, shelfToken); + } + else { + shelf = buildHeroShelf(objectGraph, shelfToken); + } + break; + case CollectionShelfDisplayStyle.TextOnly: + shelf = buildTextOnlyShelf(objectGraph, shelfToken); + break; + case CollectionShelfDisplayStyle.TextWithArtwork: + shelf = buildTextWithArtworkShelf(objectGraph, shelfToken); + break; + case CollectionShelfDisplayStyle.BrickSmall: + shelf = buildBrickSmallShelf(objectGraph, shelfToken); + break; + case CollectionShelfDisplayStyle.BrickMedium: + shelf = buildBrickMediumShelf(objectGraph, shelfToken); + break; + case CollectionShelfDisplayStyle.BrickLarge: + shelf = buildBrickLargeShelf(objectGraph, shelfToken); + break; + case CollectionShelfDisplayStyle.EditorialLockupHierarchicalPortrait: + shelf = buildEditorialLockupHierarchicalPortrait(objectGraph, shelfToken); + break; + case CollectionShelfDisplayStyle.EditorialLockupHierarchicalRows: + shelf = buildEditorialLockupHierarchicalRows(objectGraph, shelfToken); + break; + case CollectionShelfDisplayStyle.EditorialLockupMedium: + shelf = buildEditorialLockupMediumShelf(objectGraph, shelfToken); + break; + case CollectionShelfDisplayStyle.EditorialLockupMediumVariant: + shelf = buildEditorialLockupMediumVariantShelf(objectGraph, shelfToken); + break; + case CollectionShelfDisplayStyle.EditorialLockupLarge: + shelf = buildEditorialLockupLargeShelf(objectGraph, shelfToken); + break; + case CollectionShelfDisplayStyle.EditorialLockupLargeVariant: + shelf = buildEditorialLockupLargeVariantShelf(objectGraph, shelfToken); + break; + case CollectionShelfDisplayStyle.LockupMedium: + shelf = buildLockupMedium(objectGraph, shelfToken); + break; + case CollectionShelfDisplayStyle.Poster: + shelf = buildEditorialLockupPosterShelf(objectGraph, shelfToken); + break; + case CollectionShelfDisplayStyle.StorySmall: + shelf = buildSmallStoryCardShelf(objectGraph, shelfToken); + break; + case CollectionShelfDisplayStyle.StoryMedium: + shelf = buildMediumStoryCardShelf(objectGraph, shelfToken); + break; + case CollectionShelfDisplayStyle.LockupSmall: + shelf = buildLockupSmallShelf(objectGraph, shelfToken); + break; + case CollectionShelfDisplayStyle.LockupLarge: + shelf = buildLockupLargeShelf(objectGraph, shelfToken); + break; + case CollectionShelfDisplayStyle.BreakoutLarge: + shelf = buildBreakoutLargeShelf(objectGraph, shelfToken); + break; + default: + break; + } + if (serverData.isDefinedNonNullNonEmpty(shelf)) { + shelf.seeAllAction = createShelfSeeAllActionIfNecessary(objectGraph, shelf, shelfToken); + shelf.id = `shelf_${shelfToken.id}`; + } + return shelf; +} +/** + * Decorates the input shelf metrics options with shelf specific fields. + * @param objectGraph Current object graph + * @param shelfMetricsOptions Base shelf metrics options + * @returns Decorated shelf metrics options + */ +export function decorateCollectionShelfMetricsOptions(objectGraph, shelfData, shelfMetricsOptions) { + const collectionDisplayStyle = collectionShelfDisplayStyleFromShelfData(objectGraph, shelfData); + if (serverData.isNullOrEmpty(collectionDisplayStyle)) { + return; + } + shelfMetricsOptions.displayStyle = collectionDisplayStyle; +} +//# sourceMappingURL=index.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-shelf-builder/editorial-page-engagement-shelf-builder.js b/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-shelf-builder/editorial-page-engagement-shelf-builder.js new file mode 100644 index 0000000..76756a7 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-shelf-builder/editorial-page-engagement-shelf-builder.js @@ -0,0 +1,4 @@ +export function buildEngagementShelf(objectGraph, shelfToken) { + return null; +} +//# sourceMappingURL=editorial-page-engagement-shelf-builder.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-shelf-builder/editorial-page-game-center-shelf-builder.js b/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-shelf-builder/editorial-page-game-center-shelf-builder.js new file mode 100644 index 0000000..7de4145 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-shelf-builder/editorial-page-game-center-shelf-builder.js @@ -0,0 +1,4 @@ +export function buildGameCenterShelf(objectGraph, shelfToken) { + return null; +} +//# sourceMappingURL=editorial-page-game-center-shelf-builder.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-shelf-builder/editorial-page-header-shelf-builder.js b/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-shelf-builder/editorial-page-header-shelf-builder.js new file mode 100644 index 0000000..0f3a98f --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-shelf-builder/editorial-page-header-shelf-builder.js @@ -0,0 +1,157 @@ +import { attributeAsString } from "@apple-media-services/media-api"; +import { isSome } from "@jet/environment/types/optional"; +import * as models from "../../../api/models"; +import { asBooleanOrFalse, isDefinedNonNullNonEmpty } from "../../../foundation/json-parsing/server-data"; +import { attributeAsDictionary } from "../../../foundation/media/attributes"; +import { metricsFromMediaApiObject } from "../../../foundation/media/data-structure"; +import * as color from "../../../foundation/util/color-util"; +import { shallowCopyOf } from "../../../foundation/util/objects"; +import * as gamesComponentBuilder from "../../../gameservicesui/src/editorial-page/editorial-component-builder"; +import * as content from "../../content/content"; +import { deepLinkUrlFromData } from "../../linking/external-deep-link"; +import { popLocation, pushBasicLocation } from "../../metrics/helpers/location"; +import { areAppTagsEnabled } from "../../util/app-tags-util"; +import { editorialDisplayOptionsFromClientParams, extractEditorialClientParams } from "../editorial-data-util"; +import { editorialMediaDataForPlacement, EditorialMediaPlacement, isMediaDark, } from "../editorial-media-util"; +import * as editorialComponentMediaUtil from "../editorial-page-component-media-util"; +import { heroOverlayFromData } from "../editorial-page-hero-util"; +import * as metricsHelpersImpressions from "../../metrics/helpers/impressions"; +/** + * Build a header shelf for the editorial page, if present. + * @param objectGraph The App Store object graph. + * @param pageData The data for the editorial page. + * @param shelfToken The shelf token. + * @returns A Shelf, if the data is available. + */ +export function buildHeaderShelf(objectGraph, pageData, shelfToken) { + var _a, _b, _c; + const shouldUseShelfData = isDefinedNonNullNonEmpty(attributeAsDictionary(shelfToken.data, "editorialNotes")); + const headerData = shouldUseShelfData ? shelfToken.data : pageData; + if (!headerData) { + return null; + } + if (preprocessor.GAMES_TARGET) { + return gamesComponentBuilder.makeGamesPageHeaderFromData(objectGraph, headerData, shelfToken); + } + const editorialArtworkData = isDefinedNonNullNonEmpty(attributeAsDictionary(headerData, "editorialArtwork")); + if (editorialArtworkData) { + const pageHeader = new models.HeroCarousel(); + const pageHeaderCarouselItem = new models.HeroCarouselItem(); + const editorialClientParams = extractEditorialClientParams(objectGraph, headerData); + const pageHeaderCarouselItemOverlay = heroOverlayFromData(objectGraph, headerData, shelfToken, editorialClientParams); + pageHeaderCarouselItemOverlay.overlayType = "text"; + pageHeaderCarouselItemOverlay.callToActionText = null; + pageHeaderCarouselItem.clickAction = null; + pageHeaderCarouselItemOverlay.collectionIcons = null; + pageHeaderCarouselItemOverlay.lockup = null; + pageHeaderCarouselItemOverlay.callToActionText = null; + pageHeaderCarouselItem.overlay = pageHeaderCarouselItemOverlay; + const lockupOptions = { + artworkUseCase: 1 /* content.ArtworkUseCase.LockupIconSmall */, + offerStyle: "transparent", + offerEnvironment: "dark", + canDisplayArcadeOfferButton: content.shelfDisplayStyleCanDisplayArcadeOfferButtons(objectGraph, shelfToken.collectionDisplayStyle), + isContainedInPreorderExclusiveShelf: false, + metricsOptions: { + pageInformation: shelfToken.metricsPageInformation, + locationTracker: shelfToken.metricsLocationTracker, + recoMetricsData: metricsFromMediaApiObject(headerData), + }, + externalDeepLinkUrl: deepLinkUrlFromData(objectGraph, headerData), + }; + pushBasicLocation(objectGraph, lockupOptions.metricsOptions, "collectionIcons"); + const collectionIcons = editorialComponentMediaUtil.editorialFallbackAppIconsFromData(objectGraph, headerData, lockupOptions); + popLocation(lockupOptions.metricsOptions.locationTracker); + pageHeaderCarouselItem.collectionIcons = collectionIcons; + const heroMediaData = editorialMediaDataForPlacement(objectGraph, headerData, EditorialMediaPlacement.Hero); + const hideEditorialMedia = asBooleanOrFalse(editorialClientParams.hideEditorialMedia); + populatePageHeaderCarouselItemMedia(objectGraph, pageHeaderCarouselItem, hideEditorialMedia, heroMediaData, false); + pageHeaderCarouselItem.editorialDisplayOptions = editorialDisplayOptionsFromClientParams(editorialClientParams); + pageHeader.items = [pageHeaderCarouselItem]; + const rtlPageHeaderCarouselItem = shallowCopyOf(pageHeaderCarouselItem); + populatePageHeaderCarouselItemMedia(objectGraph, rtlPageHeaderCarouselItem, hideEditorialMedia, heroMediaData, true); + pageHeader.rtlItems = [rtlPageHeaderCarouselItem]; + const shelf = new models.Shelf("heroCarousel"); + shelf.items = [pageHeader]; + return shelf; + } + else { + const editorialName = attributeAsString(headerData, "name"); + if (isDefinedNonNullNonEmpty(editorialName) && areAppTagsEnabled(objectGraph, null)) { + const shelf = new models.Shelf("mediaPageHeader"); + shelf.id = "mediaPageHeader"; + const editorialBadge = (_a = attributeAsString(headerData, "editorialNotes.badge")) !== null && _a !== void 0 ? _a : null; + const lockupOptions = { + artworkUseCase: 1 /* content.ArtworkUseCase.LockupIconSmall */, + offerStyle: "transparent", + offerEnvironment: "dark", + canDisplayArcadeOfferButton: content.shelfDisplayStyleCanDisplayArcadeOfferButtons(objectGraph, shelfToken.collectionDisplayStyle), + isContainedInPreorderExclusiveShelf: false, + metricsOptions: { + pageInformation: shelfToken.metricsPageInformation, + locationTracker: shelfToken.metricsLocationTracker, + recoMetricsData: metricsFromMediaApiObject(headerData), + }, + externalDeepLinkUrl: deepLinkUrlFromData(objectGraph, headerData), + useJoeColorIconPlaceholder: true, + joeColorPlaceholderSelectionLogic: content.bestJoeColorPlaceholderSelectionLogic, + }; + let collectionIcons = (_b = editorialComponentMediaUtil.editorialFallbackAppIconsFromData(objectGraph, shelfToken.data, lockupOptions)) !== null && _b !== void 0 ? _b : undefined; + let backgroundColor; + if (isSome(collectionIcons) && (collectionIcons === null || collectionIcons === void 0 ? void 0 : collectionIcons.length) >= 3) { + collectionIcons = collectionIcons.slice(0, 3); + const collectionIconBackgroundColor = collectionIcons[0].backgroundColor; + if (isSome(collectionIconBackgroundColor) && (collectionIconBackgroundColor === null || collectionIconBackgroundColor === void 0 ? void 0 : collectionIconBackgroundColor.type) === "rgb") { + backgroundColor = content.closestTagBackgroundColorForIcon(collectionIconBackgroundColor); + } + const isArtworkDark = color.isDarkColor(backgroundColor); + const editorialClientParams = extractEditorialClientParams(objectGraph, shelfToken.data); + const mediaPageHeader = new models.MediaPageHeader(editorialBadge, editorialName, null, undefined, undefined, collectionIcons, false, backgroundColor !== null && backgroundColor !== void 0 ? backgroundColor : undefined, convertHintToIconCollectionStyle((_c = editorialClientParams.fallbackDisplayStyleHint) !== null && _c !== void 0 ? _c : "one", collectionIcons === null || collectionIcons === void 0 ? void 0 : collectionIcons.length), isArtworkDark ? "dark" : "light"); + // Configure impressions + const metricsOptions = { + pageInformation: shelfToken.metricsPageInformation, + locationTracker: shelfToken.metricsLocationTracker, + recoMetricsData: metricsFromMediaApiObject(headerData), + }; + const impressionOptions = metricsHelpersImpressions.impressionOptionsForTagHeader(objectGraph, headerData, editorialName, metricsOptions); + metricsHelpersImpressions.addImpressionFieldsToTagRoomHeader(objectGraph, mediaPageHeader, impressionOptions); + shelf.items = [mediaPageHeader]; + return shelf; + } + else { + return null; + } + } + else { + return null; + } + } +} +function convertHintToIconCollectionStyle(hint, artworkCount) { + if (artworkCount === 2) { + return "TwoUp"; + } + else { + switch (hint) { + case "one": + return "ThreeUp"; + case "two": + return "Fan"; + case "three": + return "Asymmetrical"; + default: + return "ThreeUp"; + } + } +} +function populatePageHeaderCarouselItemMedia(objectGraph, pageHeaderCarouselItem, hideEditorialMedia, heroMediaData, isRTL) { + if (!hideEditorialMedia) { + pageHeaderCarouselItem.backgroundColor = isRTL + ? heroMediaData === null || heroMediaData === void 0 ? void 0 : heroMediaData.rtlBackgroundColor + : heroMediaData === null || heroMediaData === void 0 ? void 0 : heroMediaData.backgroundColor; + pageHeaderCarouselItem.artwork = isRTL ? heroMediaData === null || heroMediaData === void 0 ? void 0 : heroMediaData.rtlArtwork : heroMediaData === null || heroMediaData === void 0 ? void 0 : heroMediaData.artwork; + pageHeaderCarouselItem.video = isRTL ? heroMediaData === null || heroMediaData === void 0 ? void 0 : heroMediaData.rtlVideo : heroMediaData === null || heroMediaData === void 0 ? void 0 : heroMediaData.video; + } + pageHeaderCarouselItem.isMediaDark = isMediaDark(objectGraph, heroMediaData, isRTL); +} +//# sourceMappingURL=editorial-page-header-shelf-builder.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-shelf-builder/editorial-page-image-shelf-builder.js b/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-shelf-builder/editorial-page-image-shelf-builder.js new file mode 100644 index 0000000..44ce98e --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-shelf-builder/editorial-page-image-shelf-builder.js @@ -0,0 +1,4 @@ +export function buildImageShelf(objectGraph, shelfToken) { + return null; +} +//# sourceMappingURL=editorial-page-image-shelf-builder.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-shelf-builder/editorial-page-marker-shelf-builder/editorial-page-quick-links-marker-shelf-builder.js b/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-shelf-builder/editorial-page-marker-shelf-builder/editorial-page-quick-links-marker-shelf-builder.js new file mode 100644 index 0000000..0f3b482 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-shelf-builder/editorial-page-marker-shelf-builder/editorial-page-quick-links-marker-shelf-builder.js @@ -0,0 +1,71 @@ +import { isSome } from "@jet/environment/types/optional"; +import * as models from "../../../../api/models"; +import * as artworkBuilder from "../../../content/artwork/artwork"; +import * as metricsHelpersClicks from "../../../metrics/helpers/clicks"; +export function buildQuickLinksShelf(objectGraph, shelfToken) { + if (objectGraph.user && objectGraph.user.isManagedAppleID) { + return null; + } + // Watch App Store does not support titled button stacks, + // and has different footer buttons. + if (objectGraph.client.isWatch) { + const shelf = new models.Shelf("action"); + const items = []; + const accountFlowAction = new models.FlowAction("account"); + accountFlowAction.title = objectGraph.loc.string("ACCOUNT", "Account"); + accountFlowAction.artwork = artworkBuilder.createArtworkForResource(objectGraph, "systemimage://person.crop.circle"); + const accountFlowClickOptions = { + targetType: "button", + id: "account", + pageInformation: shelfToken.metricsPageInformation, + locationTracker: shelfToken.metricsLocationTracker, + }; + metricsHelpersClicks.addClickEventToAction(objectGraph, accountFlowAction, accountFlowClickOptions); + items.push(accountFlowAction); + shelf.items = items; + return shelf; + } + else { + const items = []; + if (isSome(objectGraph.bag.aboutAppStoreUrl)) { + const aboutAppStoreAction = new models.ExternalUrlAction(objectGraph.bag.aboutAppStoreUrl); + aboutAppStoreAction.title = objectGraph.loc.string("QuickLinks.AboutTheAppStore.Title"); + items.push(aboutAppStoreAction); + } + if (isSome(objectGraph.bag.aboutInAppPurchasesEditorialItemId)) { + const aboutIAPsFlowAction = new models.FlowAction("article"); + aboutIAPsFlowAction.title = objectGraph.loc.string("QuickLinks.AboutInAppPurchases.Title"); + aboutIAPsFlowAction.pageUrl = `https://apps.apple.com/story/id${objectGraph.bag.aboutInAppPurchasesEditorialItemId}`; + items.push(aboutIAPsFlowAction); + } + if (isSome(objectGraph.bag.reportProblemUrl)) { + const reportProblemAction = new models.ExternalUrlAction(objectGraph.bag.reportProblemUrl); + reportProblemAction.title = objectGraph.loc.string("REPORT_A_PROBLEM", "Report a Problem"); + items.push(reportProblemAction); + } + if (isSome(objectGraph.bag.requestARefundUrl)) { + const requestRefundAction = new models.ExternalUrlAction(objectGraph.bag.requestARefundUrl); + requestRefundAction.title = objectGraph.loc.string("QuickLinks.RequestARefund.Title"); + items.push(requestRefundAction); + } + if (isSome(objectGraph.bag.changePaymentMethodUrl)) { + const changePaymentMethodAction = new models.ExternalUrlAction(objectGraph.bag.changePaymentMethodUrl); + changePaymentMethodAction.title = objectGraph.loc.string("QuickLinks.ChangePaymentMethod.Title"); + items.push(changePaymentMethodAction); + } + if (isSome(objectGraph.bag.aboutFrenchAppStoreEditorialItemId)) { + const aboutFrenchAppStoreFlowAction = new models.FlowAction("article"); + aboutFrenchAppStoreFlowAction.title = objectGraph.loc.string("QuickLinks.AboutFrenchAppStore.Title"); + aboutFrenchAppStoreFlowAction.pageUrl = `https://apps.apple.com/story/id${objectGraph.bag.aboutFrenchAppStoreEditorialItemId}`; + items.push(aboutFrenchAppStoreFlowAction); + } + const shelf = new models.Shelf("action"); + shelf.title = objectGraph.loc.string("QuickLinks.Title"); + shelf.items = items; + shelf.isHorizontal = true; + shelf.rowsPerColumn = 1; + shelfToken.metricsImpressionOptions.title = shelf.title; + return shelf; + } +} +//# sourceMappingURL=editorial-page-quick-links-marker-shelf-builder.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-shelf-builder/editorial-page-marker-shelf-builder/index.js b/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-shelf-builder/editorial-page-marker-shelf-builder/index.js new file mode 100644 index 0000000..6392f23 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-shelf-builder/editorial-page-marker-shelf-builder/index.js @@ -0,0 +1,20 @@ +import * as serverData from "../../../../foundation/json-parsing/server-data"; +import { attributeAsString } from "../../../../foundation/media/attributes"; +import { MarkerShelfKind } from "../../editorial-page-types"; +import { buildQuickLinksShelf } from "./editorial-page-quick-links-marker-shelf-builder"; +export function buildMarkerShelf(objectGraph, shelfToken) { + const markerKind = attributeAsString(shelfToken.data, "kind"); + if (serverData.isNullOrEmpty(markerKind)) { + return null; + } + let shelf = null; + switch (markerKind) { + case MarkerShelfKind.QuickLinks: + shelf = buildQuickLinksShelf(objectGraph, shelfToken); + break; + default: + break; + } + return shelf; +} +//# sourceMappingURL=index.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-shelf-builder/editorial-page-recommendations-shelf-builder.js b/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-shelf-builder/editorial-page-recommendations-shelf-builder.js new file mode 100644 index 0000000..828d652 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-shelf-builder/editorial-page-recommendations-shelf-builder.js @@ -0,0 +1,77 @@ +import { isNothing, isSome } from "@jet/environment"; +import * as models from "../../../api/models"; +import { isDefinedNonNullNonEmpty } from "../../../foundation/json-parsing/server-data"; +import { attributeAsBooleanOrFalse, attributeAsString } from "../../../foundation/media/attributes"; +import { Request } from "../../../foundation/media/data-fetching"; +import { relationshipCollection } from "../../../foundation/media/relationships"; +import { buildURLFromRequest } from "../../../foundation/media/url-builder"; +import { Parameters } from "../../../foundation/network/url-constants"; +import { insertPlaceholdersIntoShelfIfRequired } from "../../placeholders/placeholders"; +import { CollectionShelfDisplayStyle } from "../editorial-page-types"; +import { createShelfSeeAllActionIfNecessary, createShelfTokenUrlIfNecessary, } from "./editorial-page-collection-shelf-builder/editorial-page-collection-shelf-common"; +import { buildEditorialLockupHierarchicalPortrait, buildEditorialLockupLargeShelf, buildLockupLargeShelf, buildLockupSmallShelf, } from "./editorial-page-collection-shelf-builder/editorial-page-lockup-collection-shelf-builder"; +export function buildRecommendationsShelf(objectGraph, shelfToken) { + let collectionDisplayStyle = attributeAsString(shelfToken.data, "displayStyle"); + if (preprocessor.GAMES_TARGET) { + // A way to override display style as design wants to see large cards in "See all" rooms. + const shelfTokenStyle = shelfToken.collectionDisplayStyle; + if (isSome(shelfTokenStyle) && shelfTokenStyle !== collectionDisplayStyle) { + collectionDisplayStyle = shelfToken.collectionDisplayStyle; + } + } + let shelf = null; + switch (collectionDisplayStyle) { + case CollectionShelfDisplayStyle.LockupSmall: + shelf = buildLockupSmallShelf(objectGraph, shelfToken, false); + break; + case CollectionShelfDisplayStyle.LockupLarge: + shelf = buildLockupLargeShelf(objectGraph, shelfToken, false); + break; + case CollectionShelfDisplayStyle.EditorialLockupHierarchicalPortrait: + shelf = buildEditorialLockupHierarchicalPortrait(objectGraph, shelfToken); + break; + case CollectionShelfDisplayStyle.EditorialLockupLarge: + shelf = buildEditorialLockupLargeShelf(objectGraph, shelfToken); + break; + default: + shelf = shelfToken.isFirstRender ? makeHiddenShelf() : null; + break; + } + if (isNothing(shelf)) { + return null; + } + shelf.id = `shelf_${shelfToken.id}`; + addRecommendationsHrefIfNecessary(objectGraph, shelfToken); + shelf.url = createShelfTokenUrlIfNecessary(objectGraph, shelf, shelfToken); + insertPlaceholdersIntoShelfIfRequired(objectGraph, shelf, shelfToken); + shelf.seeAllAction = createShelfSeeAllActionIfNecessary(objectGraph, shelf, shelfToken); + return shelf; +} +/** + * Add the recommendations href for out of band reqeusts if necessary + * @param shelfToken The shelf token to add the href to + */ +function addRecommendationsHrefIfNecessary(objectGraph, shelfToken) { + const hasRecommendedContents = isDefinedNonNullNonEmpty(relationshipCollection(shelfToken.data, "contents")) || + isDefinedNonNullNonEmpty(shelfToken.remainingItems); + const isPersonalizationAvailable = attributeAsBooleanOrFalse(shelfToken.data, "isPersonalizationAvailable"); + const baseHref = shelfToken.data.href; + const shouldAddHref = isPersonalizationAvailable && !hasRecommendedContents && isDefinedNonNullNonEmpty(baseHref); + if (!shouldAddHref) { + return; + } + const recommendationsRequest = new Request(objectGraph, shelfToken.data.href) + .addingQuery(Parameters.editorialPageId, shelfToken.pageId) + .includingRelationships(["contents"]); + const recommendationsUrl = buildURLFromRequest(objectGraph, recommendationsRequest); + shelfToken.recommendationsHref = recommendationsUrl.toString(); +} +/** + * Creates a hidden shelf for when we're performing a secondary fetch and need to hide a shelf that returns null + */ +function makeHiddenShelf() { + const hiddenShelf = new models.Shelf("placeholder"); + hiddenShelf.isHidden = true; + return hiddenShelf; +} +//# sourceMappingURL=editorial-page-recommendations-shelf-builder.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-shelf-builder/editorial-page-tag-shelf-builder.js b/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-shelf-builder/editorial-page-tag-shelf-builder.js new file mode 100644 index 0000000..9378611 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-shelf-builder/editorial-page-tag-shelf-builder.js @@ -0,0 +1,4 @@ +export function buildTagShelf(objectGraph, shelfToken) { + return null; +} +//# sourceMappingURL=editorial-page-tag-shelf-builder.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-shelf-builder/editorial-page-text-shelf-builder.js b/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-shelf-builder/editorial-page-text-shelf-builder.js new file mode 100644 index 0000000..3b8fd26 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-shelf-builder/editorial-page-text-shelf-builder.js @@ -0,0 +1,19 @@ +import * as models from "../../../api/models"; +import { isNothing } from "@jet/environment"; +import { makeShelfId } from "../../../gameservicesui/src/editorial-page/editorial-component-builder"; +import * as mediaAPI from "@apple-media-services/media-api"; +export function buildTextShelf(objectGraph, shelfToken) { + if (!preprocessor.GAMES_TARGET) { + return null; + } + const shelf = new models.Shelf("paragraph"); + shelf.id = makeShelfId(shelfToken).toString(); + const editorialCopy = mediaAPI.attributeAsString(shelfToken.data, "editorialCopy"); + if (isNothing(editorialCopy)) { + return null; + } + const paragraph = new models.Paragraph(editorialCopy, "text/x-apple-as3-nqml", "article"); + shelf.items = [paragraph]; + return shelf; +} +//# sourceMappingURL=editorial-page-text-shelf-builder.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-shelf-builder/editorial-page-upsell-shelf-builder.js b/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-shelf-builder/editorial-page-upsell-shelf-builder.js new file mode 100644 index 0000000..deb0ac9 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-shelf-builder/editorial-page-upsell-shelf-builder.js @@ -0,0 +1,31 @@ +import * as models from "../../../api/models"; +import * as serverData from "../../../foundation/json-parsing/server-data"; +import * as arcadeCommon from "../../arcade/arcade-common"; +import * as arcadeUpsell from "../../arcade/arcade-upsell"; +import * as metricsHelpersLocation from "../../metrics/helpers/location"; +import * as metricsHelpersUtil from "../../metrics/helpers/util"; +import { makeGamesPageUpsellHeaderFromData } from "../../../gameservicesui/src/editorial-page/editorial-component-builder"; +export function buildUpsellShelf(objectGraph, shelfToken) { + if (preprocessor.GAMES_TARGET) { + return makeGamesPageUpsellHeaderFromData(objectGraph, shelfToken); + } + const upsellData = arcadeCommon.upsellFromRelationshipOf(objectGraph, shelfToken.data); + if (serverData.isNullOrEmpty(upsellData)) { + return null; + } + const marketingItemData = upsellData.marketingItemData; + if (serverData.isNullOrEmpty(marketingItemData) || serverData.isNullOrEmpty(marketingItemData.attributes)) { + return null; + } + const metricsOptions = { + pageInformation: shelfToken.metricsPageInformation, + locationTracker: shelfToken.metricsLocationTracker, + mercuryMetricsData: metricsHelpersUtil.marketingItemTopLevelBaseFieldsFromData(objectGraph, marketingItemData), + }; + const upsellBreakout = arcadeUpsell.createUpsellBreakout(objectGraph, marketingItemData, metricsOptions); + metricsHelpersLocation.nextPosition(shelfToken.metricsLocationTracker); + const shelf = new models.Shelf("upsellBreakout"); + shelf.items = [upsellBreakout]; + return shelf; +} +//# sourceMappingURL=editorial-page-upsell-shelf-builder.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-shelf-builder/editorial-page-video-clip-shelf-builder.js b/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-shelf-builder/editorial-page-video-clip-shelf-builder.js new file mode 100644 index 0000000..b5548db --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-shelf-builder/editorial-page-video-clip-shelf-builder.js @@ -0,0 +1,4 @@ +export function buildVideoClipShelf(objectGraph, shelfToken) { + return null; +} +//# sourceMappingURL=editorial-page-video-clip-shelf-builder.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-shelf-builder/index.js b/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-shelf-builder/index.js new file mode 100644 index 0000000..59dbabe --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-shelf-builder/index.js @@ -0,0 +1,83 @@ +import { isNullOrEmpty } from "../../../foundation/json-parsing/server-data"; +import { EditorialShelfType } from "../editorial-page-types"; +import { buildChartShelf } from "./editorial-page-chart-shelf-builder"; +import { buildCollectionShelf } from "./editorial-page-collection-shelf-builder"; +import { buildEngagementShelf } from "./editorial-page-engagement-shelf-builder"; +import { buildGameCenterShelf } from "./editorial-page-game-center-shelf-builder"; +import { buildHeaderShelf } from "./editorial-page-header-shelf-builder"; +import { buildImageShelf } from "./editorial-page-image-shelf-builder"; +import { buildMarkerShelf } from "./editorial-page-marker-shelf-builder"; +import { buildArcadeSeeAllGamesShelf } from "./editorial-page-arcade-see-all-games-shelf-builder"; +import { buildRecommendationsShelf } from "./editorial-page-recommendations-shelf-builder"; +import { buildTagShelf } from "./editorial-page-tag-shelf-builder"; +import { buildTextShelf } from "./editorial-page-text-shelf-builder"; +import { buildUpsellShelf } from "./editorial-page-upsell-shelf-builder"; +import { buildVideoClipShelf } from "./editorial-page-video-clip-shelf-builder"; +import { nextPosition, popLocation, pushContentLocation } from "../../metrics/helpers/location"; +import { addImpressionFields } from "../../metrics/helpers/impressions"; +import { storePlaceholderShelfImpressionMetrics } from "./editorial-page-collection-shelf-builder/editorial-page-collection-shelf-common"; +import { isSome } from "@jet/environment/types/optional"; +export function buildEditorialShelf(objectGraph, pageData, shelfToken) { + let shelf = null; + if (shelfToken.isFirstRender) { + pushContentLocation(objectGraph, shelfToken.metricsImpressionOptions, shelfToken.title); + } + switch (shelfToken.type) { + case EditorialShelfType.ArcadeSeeAllGames: + shelf = buildArcadeSeeAllGamesShelf(objectGraph, shelfToken); + break; + case EditorialShelfType.Collection: + shelf = buildCollectionShelf(objectGraph, shelfToken); + break; + case EditorialShelfType.Chart: + shelf = buildChartShelf(objectGraph, shelfToken); + break; + case EditorialShelfType.Tag: + shelf = buildTagShelf(objectGraph, shelfToken); + break; + case EditorialShelfType.Engagement: + shelf = buildEngagementShelf(objectGraph, shelfToken); + break; + case EditorialShelfType.Text: + shelf = buildTextShelf(objectGraph, shelfToken); + break; + case EditorialShelfType.Image: + shelf = buildImageShelf(objectGraph, shelfToken); + break; + case EditorialShelfType.VideoClip: + shelf = buildVideoClipShelf(objectGraph, shelfToken); + break; + case EditorialShelfType.Header: + shelf = buildHeaderShelf(objectGraph, pageData, shelfToken); + break; + case EditorialShelfType.Recommendations: + shelf = buildRecommendationsShelf(objectGraph, shelfToken); + break; + case EditorialShelfType.GameCenter: + shelf = buildGameCenterShelf(objectGraph, shelfToken); + break; + case EditorialShelfType.Upsell: + shelf = buildUpsellShelf(objectGraph, shelfToken); + break; + case EditorialShelfType.Marker: + shelf = buildMarkerShelf(objectGraph, shelfToken); + break; + default: + break; + } + if (isNullOrEmpty(shelf === null || shelf === void 0 ? void 0 : shelf.items) && isNullOrEmpty(shelf === null || shelf === void 0 ? void 0 : shelf.url)) { + shelf = null; + } + if (shelfToken.isFirstRender) { + popLocation(shelfToken.metricsImpressionOptions.locationTracker); + } + addImpressionFields(objectGraph, shelf, shelfToken.metricsImpressionOptions); + if (shelfToken.isFirstRender) { + nextPosition(shelfToken.metricsImpressionOptions.locationTracker); + } + if (isSome(shelf)) { + storePlaceholderShelfImpressionMetrics(objectGraph, shelf, shelfToken); + } + return shelf; +} +//# sourceMappingURL=index.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-shelf-metrics.js b/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-shelf-metrics.js new file mode 100644 index 0000000..e1f18ed --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-shelf-metrics.js @@ -0,0 +1,51 @@ +import * as serverData from "../../foundation/json-parsing/server-data"; +import * as mediaDataStructure from "../../foundation/media/data-structure"; +import { collectionShelfDisplayStyleFromShelfData } from "./editorial-data-util"; +import { decorateCollectionShelfMetricsOptions } from "./editorial-page-shelf-builder/editorial-page-collection-shelf-builder"; +import { decorateHeroShelfMetricsOptions } from "./editorial-page-shelf-builder/editorial-page-collection-shelf-builder/editorial-page-hero-collection-shelf-builder"; +import { CollectionShelfDisplayStyle, EditorialShelfType } from "./editorial-page-types"; +/** + * Creates the shelf metrics options for a given shelf data item. + * @param objectGraph Current object graph + * @param shelfData The data for the shelf + * @param title The metrics title for the shelf + * @param metricsPageInformation Current metrics page information + * @param metricsLocationTracker Current metrics location tracker + * @returns Built shelf metrics options + */ +export function buildShelfMetricsOptions(objectGraph, shelfData, title, metricsPageInformation, metricsLocationTracker) { + const shelfType = shelfData.type; + const collectionDisplayStyle = collectionShelfDisplayStyleFromShelfData(objectGraph, shelfData); + const shelfMetricsOptions = { + id: serverData.asString(shelfData, "id"), + kind: null, + softwareType: null, + targetType: "swoosh", + title: title, + pageInformation: metricsPageInformation, + locationTracker: metricsLocationTracker, + idType: "shelf_id", + shelfType: shelfType, + recoMetricsData: mediaDataStructure.metricsFromMediaApiObject(shelfData), + canonicalId: serverData.asString(shelfData.meta, "canonicalId"), + }; + switch (shelfType) { + case EditorialShelfType.Collection: + decorateCollectionShelfMetricsOptions(objectGraph, shelfData, shelfMetricsOptions); + switch (collectionDisplayStyle) { + case CollectionShelfDisplayStyle.Hero: + decorateHeroShelfMetricsOptions(objectGraph, shelfMetricsOptions); + break; + default: + break; + } + break; + case EditorialShelfType.Recommendations: + decorateCollectionShelfMetricsOptions(objectGraph, shelfData, shelfMetricsOptions); + break; + default: + break; + } + return shelfMetricsOptions; +} +//# sourceMappingURL=editorial-page-shelf-metrics.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-shelf-token.js b/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-shelf-token.js new file mode 100644 index 0000000..6857cdb --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-shelf-token.js @@ -0,0 +1,120 @@ +import * as serverData from "../../foundation/json-parsing/server-data"; +import * as mediaAttributes from "../../foundation/media/attributes"; +import * as mediaRelationship from "../../foundation/media/relationships"; +import * as content from "../content/content"; +import { recoMetricsDataForFCData } from "../grouping/shelf-controllers/grouping-shelf-controller-common"; +import { collectionShelfDisplayStyleFromShelfData, extractEditorialClientParams } from "./editorial-data-util"; +import { buildShelfMetricsOptions } from "./editorial-page-shelf-metrics"; +import { GameCenterShelfClientFilter, } from "./editorial-page-types"; +import { isSome } from "@jet/environment"; +/** + * Create the base shelf token to start parsing an editorial page shelf. This will create a new shelf token with the + * base metrics options + * + * @param objectGraph The App Store dependency graph for making native calls and viewing properties + * @param pageId The id of the editorial page this shelf is on. + * @param shelfData The media api data for this specific shelf + * @param isArcadePage Whether this shelf is on the arcade page or not + * @param shelfIndex The index of this shelf on the page, at the time of building + * @param metricsPageInformation The metrics page information for the editorial page + * @param metricsLocationTracker The location tracker for building metrics location information + * @param isSeeAll Whether the shelf is being built as a shelf for a sidepacked See All page. + * @param collectionDisplayStyleOverride A way to override collection display style that is coming from mediaAPI. + */ +export function createBaseShelfToken(objectGraph, pageId, shelfData, isArcadePage, shelfIndex, metricsPageInformation, metricsLocationTracker, isSeeAll = false, collectionDisplayStyle = null) { + const shelfToken = { + id: serverData.asString(shelfData, "id"), + type: shelfData.type, + collectionDisplayStyle: collectionDisplayStyle !== null && collectionDisplayStyle !== void 0 ? collectionDisplayStyle : collectionShelfDisplayStyleFromShelfData(objectGraph, shelfData), + filterOverrides: mediaAttributes.attributeAsArrayOrEmpty(shelfData, "filterOverrides"), + pageId: pageId, + data: shelfData, + presentationHints: {}, + clientIdentifierOverride: null, + isFirstRender: true, + shouldFilter: true, + gamesFilter: gamesFilterFromShelfData(shelfData), + hasExistingContent: false, + title: null, + subtitle: null, + eyebrow: null, + titleArtwork: null, + remainingItems: [], + metricsImpressionOptions: null, + metricsPageInformation: metricsPageInformation, + metricsLocationTracker: metricsLocationTracker, + recoMetricsData: recoMetricsDataForFCData(objectGraph, shelfData), + isDeferring: false, + showOrdinals: false, + ordinalIndex: 1, + isSearchLandingPage: false, + isArcadePage: isArcadePage, + shelfIndex: shelfIndex, + isSeeAll: isSeeAll, + }; + applyBaseShelfTokenTitleValues(objectGraph, shelfData, shelfToken); + return shelfToken; +} +/** + * Apply the values for the title, subtitle, and eyebrow to the shelf token, as well as the impressionOptions + * + * @param objectGraph The App Store dependency graph for making native calls and viewing properties + * @param shelfData The media api data for this specific shelf + * @param shelfToken The shelf token to apply the title values to + */ +export function applyBaseShelfTokenTitleValues(objectGraph, shelfData, shelfToken) { + let title = mediaAttributes.attributeAsString(shelfData, "editorialNotes.name"); + let subtitle = mediaAttributes.attributeAsString(shelfData, "editorialNotes.tagline"); + let eyebrow = mediaAttributes.attributeAsString(shelfData, "editorialNotes.badge"); + let titleArtwork = null; + // 'Similar To' Personalised Shelf + // Check if personalised shelf has a badge-content, if it's got valid content, and the clients support eyebrows and title artwork: + // - Reco shelf title becomes the eyebrow + // - Featured App in badge-content becomes the shelf title, and we add the icon as title artwork + // - No subtitle should be shown + // - Flag to use the eyebrown name for metrics + let wantsEyebrowNameForMetrics = false; + const badgeContent = mediaRelationship.relationshipData(objectGraph, shelfData, "badge-content"); + if (serverData.isDefinedNonNullNonEmpty(badgeContent)) { + eyebrow = objectGraph.loc.uppercased(mediaAttributes.attributeAsString(shelfData, "editorialNotes.name")); + title = mediaAttributes.attributeAsString(badgeContent, "editorialNotes.name"); + titleArtwork = content.iconFromData(objectGraph, badgeContent, { + useCase: 1 /* content.ArtworkUseCase.LockupIconSmall */, + }); + subtitle = null; + wantsEyebrowNameForMetrics = true; + } + if (preprocessor.GAMES_TARGET) { + const editorialClientParams = extractEditorialClientParams(objectGraph, shelfData); + // This flag hides the whole shelf header content altogether + if (isSome(editorialClientParams) && editorialClientParams.suppressName) { + title = null; + subtitle = null; + eyebrow = null; + titleArtwork = null; + } + } + const shelfMetricsOptionsTitle = wantsEyebrowNameForMetrics ? eyebrow : title; + const shelfMetricsOptions = buildShelfMetricsOptions(objectGraph, shelfData, shelfMetricsOptionsTitle, shelfToken.metricsPageInformation, shelfToken.metricsLocationTracker); + shelfToken.title = title; + shelfToken.subtitle = subtitle; + shelfToken.eyebrow = eyebrow; + shelfToken.titleArtwork = titleArtwork; + shelfToken.metricsImpressionOptions = shelfMetricsOptions; +} +function gamesFilterFromShelfData(shelfData) { + const clientFilter = mediaAttributes.attributeAsString(shelfData, "clientFilter"); + let gamesFilter = null; + switch (clientFilter) { + case GameCenterShelfClientFilter.ArcadeGames: + gamesFilter = "arcade"; + break; + case GameCenterShelfClientFilter.AllGames: + gamesFilter = "all"; + break; + default: + break; + } + return gamesFilter; +} +//# sourceMappingURL=editorial-page-shelf-token.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-types.js b/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-types.js new file mode 100644 index 0000000..22daf5f --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-page-types.js @@ -0,0 +1,139 @@ +import { isNothing } from "@jet/environment"; +import * as mediaAttributes from "../../foundation/media/attributes"; +export var EditorialShelfType; +(function (EditorialShelfType) { + EditorialShelfType["ArcadeSeeAllGames"] = "editorial-shelves-arcade-see-all-games"; + EditorialShelfType["Collection"] = "editorial-shelves-collection"; + EditorialShelfType["Chart"] = "editorial-shelves-chart"; + EditorialShelfType["Tag"] = "editorial-shelves-tag"; + EditorialShelfType["Engagement"] = "editorial-shelves-engagement"; + EditorialShelfType["Text"] = "editorial-shelves-text"; + EditorialShelfType["Image"] = "editorial-shelves-image"; + EditorialShelfType["VideoClip"] = "editorial-shelves-video-clip"; + EditorialShelfType["Header"] = "editorial-shelves-header"; + EditorialShelfType["Recommendations"] = "editorial-shelves-recommendation"; + EditorialShelfType["GameCenter"] = "editorial-shelves-game-center"; + EditorialShelfType["Upsell"] = "editorial-shelves-upsell"; + EditorialShelfType["Marker"] = "editorial-shelves-marker"; +})(EditorialShelfType || (EditorialShelfType = {})); +export var CollectionShelfDisplayStyle; +(function (CollectionShelfDisplayStyle) { + CollectionShelfDisplayStyle["Hero"] = "Hero"; + CollectionShelfDisplayStyle["TextOnly"] = "TextOnly"; + CollectionShelfDisplayStyle["TextWithArtwork"] = "TextWithArtwork"; + CollectionShelfDisplayStyle["BrickSmall"] = "BrickSmall"; + CollectionShelfDisplayStyle["BrickMedium"] = "BrickMedium"; + CollectionShelfDisplayStyle["BrickLarge"] = "BrickLarge"; + CollectionShelfDisplayStyle["Charts"] = "Charts"; + CollectionShelfDisplayStyle["EditorialLockupHierarchicalPortrait"] = "EditorialLockupHierarchicalPortrait"; + CollectionShelfDisplayStyle["EditorialLockupHierarchicalRows"] = "EditorialLockupHierarchicalRows"; + CollectionShelfDisplayStyle["EditorialLockupMedium"] = "EditorialLockupMedium"; + CollectionShelfDisplayStyle["EditorialLockupMediumVariant"] = "EditorialLockupMediumVariant"; + CollectionShelfDisplayStyle["EditorialLockupLarge"] = "EditorialLockupLarge"; + CollectionShelfDisplayStyle["EditorialLockupLargeVariant"] = "EditorialLockupLargeVariant"; + CollectionShelfDisplayStyle["LockupMedium"] = "LockupMedium"; + CollectionShelfDisplayStyle["Lockup4Up"] = "Lockup4Up"; + CollectionShelfDisplayStyle["Poster"] = "Poster"; + CollectionShelfDisplayStyle["StorySmall"] = "StorySmall"; + CollectionShelfDisplayStyle["StoryMedium"] = "StoryMedium"; + CollectionShelfDisplayStyle["LockupSmall"] = "LockupSmall"; + CollectionShelfDisplayStyle["LockupLarge"] = "LockupLarge"; + CollectionShelfDisplayStyle["BreakoutLarge"] = "BreakoutLarge"; +})(CollectionShelfDisplayStyle || (CollectionShelfDisplayStyle = {})); +/** + * Returns the limit for a collection shelf to display in a non-See All page. + * @param {CollectionShelfDisplayStyle} displayStyle Collection shelf display style. + * @returns {number | undefined} Number of items to display. + */ +export function displayLimitForCollectionShelf(displayStyle, layoutDirection, shelfToken) { + if (!preprocessor.GAMES_TARGET || shelfToken.isSeeAll) { + return undefined; + } + switch (displayStyle) { + case CollectionShelfDisplayStyle.EditorialLockupLarge: + // "2 Cards" style https://quip-apple.com/qjjZAy4x977f + return layoutDirection === "Condensed" ? 2 : undefined; + case CollectionShelfDisplayStyle.EditorialLockupHierarchicalPortrait: + // "3 Cards" style https://quip-apple.com/qjjZAy4x977f + return 3; + case CollectionShelfDisplayStyle.EditorialLockupMedium: + // "4 Cards (4up)" style https://quip-apple.com/qjjZAy4x977f + return layoutDirection === "Condensed" ? 4 : undefined; + case CollectionShelfDisplayStyle.EditorialLockupHierarchicalRows: + // "4 Cards (Hierarchical)" style https://quip-apple.com/qjjZAy4x977f + return 4; + case CollectionShelfDisplayStyle.Charts: + // "4 Cards (Charts)" style https://quip-apple.com/qjjZAy4x977f + return 6; + case CollectionShelfDisplayStyle.LockupSmall: + // "6 Rows" style https://quip-apple.com/qjjZAy4x977f + return layoutDirection === "Condensed" ? 9 : undefined; + default: + return undefined; + } +} +/** + * Returns the display style override for See All pages. + * @param {CollectionShelfDisplayStyle} displayStyle Collection shelf display style. + * @returns {number | undefined} Number of items to display. + */ +export function collectionDisplayStyleOverride(displayStyle) { + if (!preprocessor.GAMES_TARGET || isNothing(displayStyle)) { + return null; + } + switch (displayStyle) { + // These shelves should maintain their display style in See All pages. + case CollectionShelfDisplayStyle.BrickSmall: + case CollectionShelfDisplayStyle.LockupMedium: + return displayStyle; + // All other shelves will display in EditorialLockupLarge display style in See All pages. + default: + return CollectionShelfDisplayStyle.EditorialLockupLarge; + } +} +export var MarkerShelfKind; +(function (MarkerShelfKind) { + MarkerShelfKind["QuickLinks"] = "QuickLinks"; +})(MarkerShelfKind || (MarkerShelfKind = {})); +export var CollectionShelfLayoutDirection; +(function (CollectionShelfLayoutDirection) { + CollectionShelfLayoutDirection["Vertical"] = "Vertical"; + CollectionShelfLayoutDirection["Horizontal"] = "Horizontal"; + CollectionShelfLayoutDirection["Condensed"] = "Condensed"; +})(CollectionShelfLayoutDirection || (CollectionShelfLayoutDirection = {})); +export function collectionShelfLayoutDirection(shelfToken) { + const layoutDirection = mediaAttributes.attributeAsString(shelfToken.data, "layoutDirection"); + if (layoutDirection === CollectionShelfLayoutDirection.Horizontal) { + return CollectionShelfLayoutDirection.Horizontal; + } + if (layoutDirection === CollectionShelfLayoutDirection.Condensed) { + return CollectionShelfLayoutDirection.Condensed; + } + // Make default one as vertical for backward compatibility when code used to check `=== "Horizontal"`. + return CollectionShelfLayoutDirection.Vertical; +} +export var CollectionShelfFilterOverride; +(function (CollectionShelfFilterOverride) { + CollectionShelfFilterOverride["ShowInstalled"] = "ShowInstalled"; + CollectionShelfFilterOverride["ShowAllPlatforms"] = "ShowAllPlatforms"; + CollectionShelfFilterOverride["ShowOnlyPreorder"] = "ShowOnlyPreorder"; +})(CollectionShelfFilterOverride || (CollectionShelfFilterOverride = {})); +export var GameCenterShelfClientFilter; +(function (GameCenterShelfClientFilter) { + GameCenterShelfClientFilter["AllGames"] = "AllGames"; + GameCenterShelfClientFilter["ArcadeGames"] = "ArcadeGames"; +})(GameCenterShelfClientFilter || (GameCenterShelfClientFilter = {})); +export var GameCenterShelfKind; +(function (GameCenterShelfKind) { + GameCenterShelfKind["FriendsArePlaying"] = "FriendsArePlaying"; + GameCenterShelfKind["ContinuePlaying"] = "ContinuePlaying"; +})(GameCenterShelfKind || (GameCenterShelfKind = {})); +export var UpsellShelfPlacement; +(function (UpsellShelfPlacement) { + UpsellShelfPlacement["ArcadeTabHeader"] = "ArcadeTabHeader"; +})(UpsellShelfPlacement || (UpsellShelfPlacement = {})); +export var UpsellShelfServiceType; +(function (UpsellShelfServiceType) { + UpsellShelfServiceType["Arcade"] = "Arcade"; +})(UpsellShelfServiceType || (UpsellShelfServiceType = {})); +//# sourceMappingURL=editorial-page-types.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-shelf-collection-page-utils.js b/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-shelf-collection-page-utils.js new file mode 100644 index 0000000..6544691 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/editorial-pages/editorial-shelf-collection-page-utils.js @@ -0,0 +1,120 @@ +import { isSome, unwrapOptional as unwrap } from "@jet/environment/types/optional"; +import { GenericPage, Shelf, PageHeader } from "../../api/models"; +import { makeEditorialShelfCollectionPageIntent, } from "../../api/intents/editorial/editorial-shelf-collection-page-intent"; +import { Request, defaultAdditionalPlatformsForClient } from "../../foundation/media/data-fetching"; +import { dataFromDataContainer } from "../../foundation/media/data-structure"; +import { attributeAsString } from "../../foundation/media/attributes"; +import { isDefinedNonNullNonEmpty } from "../../foundation/json-parsing/server-data"; +import { metricsPageInformationFromMediaApiResponse, addMetricsEventsToPageWithInformation, } from "../../common/metrics/helpers/page"; +import { newLocationTracker } from "../../common/metrics/helpers/location"; +import { createBaseShelfToken } from "../../common/editorial-pages/editorial-page-shelf-token"; +import { buildEditorialShelf } from "../../common/editorial-pages/editorial-page-shelf-builder"; +import { newPageRefreshControllerFromResponse, pageRefreshPolicyForController, } from "../../common/refresh/page-refresh-controller"; +import { shouldFetchCustomAttributes } from "../product-page/product-page-variants"; +import { prepareMAPIRequest } from "./editorial-page-controller-util"; +import { generateRoutes } from "../util/generate-routes"; +import { setPreviewPlatform } from "../preview-platform"; +import { makeGamesPageHeader } from "../../gameservicesui/src/editorial-page/editorial-component-builder"; +import { collectionShelfDisplayStyleFromShelfData } from "./editorial-data-util"; +import { collectionDisplayStyleOverride } from "./editorial-page-types"; +import { shouldUsePrerenderedIconArtwork } from "../content/content"; +// MARK: - `EditorialShelfCollectionPageIntentController` URL Building +const { routes: routesWithoutPlatformSegment, makeCanonicalUrl: makeCanonicalUrlWithoutPlatformSegment } = generateRoutes(makeEditorialShelfCollectionPageIntent, "/collections/{id}"); +const { routes: routesWithPlatformSegment, makeCanonicalUrl: makeCanonicalUrlWithPlatformSegment } = generateRoutes(makeEditorialShelfCollectionPageIntent, "/{platform}/collections/{id}"); +export function editorialShelfCollectionPageRoutes(objectGraph) { + return [...routesWithoutPlatformSegment(objectGraph), ...routesWithPlatformSegment(objectGraph)]; +} +export function makeEditorialShelfCollectionPageURL(objectGraph, intent) { + if (intent.platform) { + return makeCanonicalUrlWithPlatformSegment(objectGraph, intent); + } + else { + return makeCanonicalUrlWithoutPlatformSegment(objectGraph, intent); + } +} +// MARK: - Media API Request Building +export function defaultRequestAttributes(objectGraph) { + const attributes = [ + "editorialArtwork", + "editorialVideo", + "isAppleWatchSupported", + "requiredCapabilities", + "expectedReleaseDateDisplayFormat", + "showExpectedReleaseDate", + "badge-content", + "compatibilityControllerRequirement", + ]; + if (objectGraph.appleSilicon.isSupportEnabled) { + attributes.push("macRequiredCapabilities"); + } + if (objectGraph.client.isMac) { + attributes.push("hasMacIPAPackage"); + } + if (objectGraph.bag.enableUpdatedAgeRatings) { + attributes.push("ageRating"); + } + if (shouldUsePrerenderedIconArtwork(objectGraph)) { + attributes.push("iconArtwork"); + } + return attributes; +} +/** + * Creates a {@linkcode Request} to fetch a single `editorial-shelves-collection` item by {@linkcode id} + */ +export function makeEditorialShelfCollectionPageRequest(objectGraph, intent) { + const request = new Request(objectGraph, `/v1/editorial/${intent.storefront}`).withIdOfType(intent.id, "editorial-shelves-collection"); + request + .includingAgeRestrictions() + .includingAdditionalPlatforms(defaultAdditionalPlatformsForClient(objectGraph)) + .includingAttributes(defaultRequestAttributes(objectGraph)) + .usingCustomAttributes(shouldFetchCustomAttributes(objectGraph)); + if (objectGraph.client.isWatch) { + request.addingRelationshipLimit("contents", 3); + } + request + .includingRelationships(["contents"]) + .includingScopedRelationships("editorial-pages", ["primary-contents"]) + .includingAssociateKeys("editorial-shelves-collection:contents", ["editorial-cards"]); + prepareMAPIRequest(objectGraph, request); + setPreviewPlatform(objectGraph, request); + return request; +} +// MARK: - Editorial Shelves Collection Page Rendering +export function renderEditorialShelfCollectionPage(objectGraph, data) { + var _a; + const shelfData = dataFromDataContainer(objectGraph, data); + if (!isDefinedNonNullNonEmpty(shelfData)) { + return null; + } + const metricsPageInformation = metricsPageInformationFromMediaApiResponse(objectGraph, "Room", shelfData.id, data); + const metricsPageLocationTracker = newLocationTracker(); + const pageRefreshController = newPageRefreshControllerFromResponse(data); + const collectionDisplayStyle = collectionShelfDisplayStyleFromShelfData(objectGraph, shelfData); + const shelfToken = createBaseShelfToken(objectGraph, undefined, shelfData, false, 0, metricsPageInformation, metricsPageLocationTracker, preprocessor.GAMES_TARGET, collectionDisplayStyleOverride(collectionDisplayStyle)); + const shelves = []; + const shelf = unwrap(buildEditorialShelf(objectGraph, undefined, shelfToken)); + shelf.title = null; + shelf.eyebrow = null; + shelf.isHorizontal = false; + const headerTitle = attributeAsString(shelfData, "editorialNotes.name"); + if (isSome(headerTitle)) { + // There must be a title to create a pageHeader. + const headerShelf = new Shelf("pageHeader"); + headerShelf.id = "shelf_page_header"; + let pageHeader; + if (preprocessor.GAMES_TARGET) { + pageHeader = makeGamesPageHeader(shelfToken, headerTitle, shelf.items, (_a = attributeAsString(shelfData, "editorialNotes.tagline")) !== null && _a !== void 0 ? _a : undefined); + } + else { + pageHeader = new PageHeader(null, headerTitle, attributeAsString(shelfData, "editorialNotes.tagline")); + } + headerShelf.items = [pageHeader]; + shelves.push(headerShelf); + } + shelves.push(shelf); + const page = new GenericPage(shelves); + page.pageRefreshPolicy = pageRefreshPolicyForController(objectGraph, pageRefreshController); + addMetricsEventsToPageWithInformation(objectGraph, page, metricsPageInformation); + return page; +} +//# sourceMappingURL=editorial-shelf-collection-page-utils.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/filtering.js b/node_modules/@jet-app/app-store/tmp/src/common/filtering.js new file mode 100644 index 0000000..a313802 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/filtering.js @@ -0,0 +1,446 @@ +/** + * Created by keithpk on 4/25/17. + */ +import * as derivedData from "../foundation/json-parsing/derived-data"; +import * as serverData from "../foundation/json-parsing/server-data"; +import * as mediaAttributes from "../foundation/media/attributes"; +import * as mediaPlatformAttributes from "../foundation/media/platform-attributes"; +import * as client from "../foundation/wrappers/client"; +import * as contentAttributes from "./content/attributes"; +import * as content from "./content/content"; +import * as sad from "./content/sad"; +export function shouldFilter(objectGraph, data, type = 80894 /* Filter.All */) { + let filter = false; + if (type & 2 /* Filter.ThirtyTwoBit */) { + filter = filter || shouldFilter32Bit(objectGraph, data); + } + if (type & 4 /* Filter.UnsupportedSystemDeletableApps */) { + filter = filter || shouldFilterDeletableSystemApp(objectGraph, data); + } + if (type & 8 /* Filter.UnsupportedWatchAppOnWatchClient */) { + filter = filter || shouldFilterUnsupportedWatchAppOnWatchClient(objectGraph, data); + } + if (type & 16 /* Filter.DisabledMerchandisableIAPs */) { + filter = filter || shouldFilterMerchandisableInAppPurchase(objectGraph, data); + } + if (type & 32 /* Filter.LegacyApps */) { + filter = filter || shouldFilterLegacyApps(objectGraph, data); + } + if (type & 64 /* Filter.LegacyProductionMacOSInstaller */) { + filter = filter || isLegacyProductionMacOSInstaller(objectGraph, data); + } + if (type & 128 /* Filter.UnsupportedPlatform */) { + filter = filter || shouldFilterUnsupportedPlatform(objectGraph, data); + } + if (type & 16384 /* Filter.UnsupportedPlatformForMacCharts */) { + filter = filter || shouldFilterUnsupportedPlatformForMacCharts(objectGraph, data); + } + if (type & 256 /* Filter.UnsupportedPreorders */) { + filter = filter || shouldFilterUnsupportedPreorders(objectGraph, data); + } + if (type & 512 /* Filter.MinimumOSRequirement */) { + filter = filter || shouldFilterForMinimumOSRequirement(objectGraph, data); + } + if (type & 1024 /* Filter.SADWatchApps */) { + filter = filter || shouldFilterSADWatchApps(objectGraph, data); + } + if (type & 2048 /* Filter.MinimumCompanionOSRequirement */) { + filter = filter || shouldFilterForMinimumCompanionOSRequirement(objectGraph, data); + } + if (type & 4096 /* Filter.MacOSCompatibleIOSBinary */) { + filter = filter || shouldFilterForMacOSCompatibleIOSBinary(objectGraph, data); + } + if (type & 8192 /* Filter.MacOSRosetta */) { + filter = filter || shouldFilterRosetta(objectGraph, data); + } + if (type & 32768 /* Filter.VisionOSCompatibility */) { + filter = filter || shouldFilterForVisionOSCompatibility(objectGraph, data); + } + if (type & 65536 /* Filter.SADMinimumOSRequirement */) { + filter = filter || shouldFilterForSADMinimumOSRequirement(objectGraph, data); + } + return filter; +} +function shouldFilter32Bit(objectGraph, data) { + if (objectGraph.client.isWatch) { + // We still ship 32 bit watches and software. + return false; + } + return (contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "is32bitOnly") || + // I don't get why we need a different field for macOS, but so is life + contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "requires32bit")); +} +function shouldFilterUnsupportedWatchAppOnWatchClient(objectGraph, data) { + return derivedData.value(data, "shouldFilterUnsupportedWatchAppOnWatchClient", () => { + const isWatchSupported = contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "isAppleWatchSupported"); + const isBundle = data.type === "app-bundles"; + if (objectGraph.host.clientIdentifier === client.watchIdentifier && !isWatchSupported && !isBundle) { + return true; + } + return false; + }); +} +function shouldFilterLegacyApps(objectGraph, data) { + return derivedData.value(data, "shouldFilterLegacyApps", () => { + const bundleId = contentAttributes.contentAttributeAsString(objectGraph, data, "bundleId"); + if (objectGraph.client.isiOS || objectGraph.client.isVision) { + switch (bundleId) { + // "Find My iPhone" has been replaced by "Find My" in Yukon + case "com.apple.mobileme.fmip1": + return true; + // Old 2nd party Shortcuts is no longer available on iOS 13 + // <rdar://problem/51756700> In Search results user will see 2 Shortcuts apps + case "is.workflow.my.app": + return true; + default: + break; + } + } + if (objectGraph.client.isVision) { + switch (bundleId) { + // iTunes Store is not available on visionOS + case "com.apple.MobileStore": + return true; + default: + break; + } + } + if (objectGraph.client.isWatch) { + switch (bundleId) { + // "Find My Friends" has been replaced by "Find My" in Grace + // rdar://62885564 (Find my app changes) + case "com.apple.mobileme.fmf1": + return true; + case "com.apple.findmy": + // "Find My" has been replaced by "Find People" in JupiterC. + // rdar://85616308 (2021 JS: Find People (watch app) - Filter Search and Disable Offer Button for watchOS 8.3 and above) + return content.isOSAtLeastVersion(objectGraph, "8.3"); + case "com.apple.findmy.findpeople": + // "Find My" has been replaced by "Find People" in JupiterC. + // Older clients should filter the new "Find People" app. + // rdar://85616308 (2021 JS: Find People (watch app) - Filter Search and Disable Offer Button for watchOS 8.3 and above) + return !content.isOSAtLeastVersion(objectGraph, "8.3"); + default: + break; + } + } + if (objectGraph.client.isMac) { + switch (bundleId) { + // Facetime is now bundled into macOS + case "com.apple.FaceTime": + return true; + // These are betas of old macOS releases + case "com.apple.InstallAssistant.Seed.macOS1013Seed1": + case "com.apple.InstallAssistant.OSX12PublicSeed2": + case "com.apple.InstallAssistant.OSX12CustomerSeed1": + case "com.apple.InstallAssistant.OSX12Seed2": + return true; + // These are old version of macOS server that are no longer relevant + case "com.apple.Server.v3": + case "com.apple.Server.v2": + case "com.apple.Server.v1": + return true; + default: + break; + } + } + return false; + }); +} +function isLegacyProductionMacOSInstaller(objectGraph, data) { + return derivedData.value(data, "isLegacyMacOSInstaller", () => { + const bundleId = contentAttributes.contentAttributeAsString(objectGraph, data, "bundleId"); + const isMac = objectGraph.client.isMac; + if (!isMac) { + return false; + } + switch (bundleId) { + // These are production releases of old OSes + case "com.apple.InstallAssistant.ElCapitan2": + case "com.apple.InstallAssistant.ElCapitan": + case "com.apple.InstallAssistant.Yosemite": + case "com.apple.InstallAssistant.Mavericks": + case "com.apple.InstallAssistant.MountainLion": + case "com.apple.InstallAssistant.Lion": + case "com.apple.InstallAssistant.Sierra": + case "com.apple.InstallAssistant.HighSierra": + case "com.apple.InstallAssistant.Mojave": + return true; + default: + break; + } + return false; + }); +} +/** + * For Mac charts, we want to show Almond apps regardless of AS/Intel configuration + * @param objectGraph The object graph reference + * @param data The data blob representing a single application + * @returns Whether the application should be filtered out for charts on Mac based on its available platforms + */ +function shouldFilterUnsupportedPlatformForMacCharts(objectGraph, data) { + return derivedData.value(data, "shouldFilterUnsupportedPlatformForCharts", () => { + if (data.type !== "apps" && data.type !== "app-bundles") { + return false; + } + // All apps should be considered "supported" by the "web" client + if (objectGraph.client.isWeb) { + return false; + } + const appPlatforms = content.supportedAppPlatformsFromData(objectGraph, data); + const supportsMacOSCompatibleIOSBinary = content.supportsMacOSCompatibleIOSBinaryFromData(objectGraph, data, true); + const supportsVisionOSCompatibleIOSBinary = content.supportsVisionOSCompatibleIOSBinaryFromData(objectGraph, data); + // This check purposely omits passing additional parameters to consider Rosetta for compatibility + // because doing so would result in the app being filtered from Charts on macOS. + return !content.buyableOnDevice(objectGraph, data, appPlatforms, objectGraph.client.deviceType, supportsMacOSCompatibleIOSBinary, supportsVisionOSCompatibleIOSBinary); + }); +} +function shouldFilterUnsupportedPlatform(objectGraph, data) { + return derivedData.value(data, "shouldFilterUnsupportedPlatform", () => { + if (data.type !== "apps" && data.type !== "app-bundles") { + return false; + } + // All apps should be considered "supported" by the "web" client + if (objectGraph.client.isWeb) { + return false; + } + const appPlatforms = content.supportedAppPlatformsFromData(objectGraph, data); + // rdar://83688780 (Top chart - several categories pages are missing apps) + // For now, we will support showing almond apps for intel and AS mac while charts backend looks to enable server-side filtering. + const supportsMacOSCompatibleIOSBinary = content.supportsMacOSCompatibleIOSBinaryFromData(objectGraph, data, objectGraph.appleSilicon.isSupportEnabled); + const supportsVisionOSCompatibleIOSBinary = content.supportsVisionOSCompatibleIOSBinaryFromData(objectGraph, data); + // This check purposely omits passing additional parameters to consider Rosetta for compatibility + // because doing so would result in the app being filtered from Charts on macOS. + return !content.buyableOnDevice(objectGraph, data, appPlatforms, objectGraph.client.deviceType, supportsMacOSCompatibleIOSBinary, supportsVisionOSCompatibleIOSBinary); + }); +} +function shouldFilterUnsupportedPreorders(objectGraph, data) { + return derivedData.value(data, "shouldFilterUnsupportedPreordersOnPlatform", () => { + const isPreorder = mediaAttributes.attributeAsBooleanOrFalse(data, "isPreorder"); + if (!isPreorder) { + return false; + } + if (data.type !== "apps") { + return false; + } + // On the web, we show preoderable apps across all device types for the browsing experience + if (objectGraph.client.isWeb) { + return false; + } + const appPlatforms = content.supportedAppPlatformsFromData(objectGraph, data); + const supportsMacOSCompatibleIOSBinary = content.supportsMacOSCompatibleIOSBinaryFromData(objectGraph, data, objectGraph.appleSilicon.isSupportEnabled); + const supportsVisionOSCompatibleIOSBinary = content.supportsVisionOSCompatibleIOSBinaryFromData(objectGraph, data); + return !content.preorderableOnDevice(objectGraph, appPlatforms, objectGraph.client.deviceType, supportsMacOSCompatibleIOSBinary, supportsVisionOSCompatibleIOSBinary); + }); +} +function shouldFilterDeletableSystemApp(objectGraph, data) { + return derivedData.value(data, "shouldFilterDeletableSystemApp", () => { + const systemApps = sad.systemApps(objectGraph); + if (!systemApps.isSystemAppFromData(data)) { + return false; + } + const bundleId = systemApps.bundleIdFromData(data); + const model = objectGraph.host.deviceModel; + if (objectGraph.client.isWeb) { + return false; + } + const isPhone = model.startsWith("iPhone"); + const isPad = model.startsWith("iPad"); + let countrySupportsTVApp; + if (content.isOSAtLeastVersion(objectGraph, "12.3") || objectGraph.client.isVision) { + // All countries support the tv app for PeaceF+ clients. + countrySupportsTVApp = true; + } + else { + countrySupportsTVApp = objectGraph.bag.isTVAppEnabled; + } + switch (bundleId) { + // These apps are only available on iPhone + case "com.apple.Bridge": + case "com.apple.Passbook": + case "com.apple.visionproapp": + return !isPhone; + // These apps are only avaliable on iPad + case "com.apple.Photo-Booth": + return !isPad; + // TV.app is only enabled in the US storefront + case "com.apple.tv": + return !countrySupportsTVApp; + // Videos.app everywhere else + case "com.apple.videos": + return countrySupportsTVApp; + // iCloud Drive is replaced by Files on Tigris clients + case "com.apple.iCloudDriveApp": + return true; + // FaceTime & Phone are not available in some Arab countries; defer to MobileGestalt + case "com.apple.facetime": + case "com.apple.mobilephone": + return (!deviceHasCapability(objectGraph, "any-telephony", data) || + !deviceHasCapability(objectGraph, "venice", data)); + case "com.apple.Fitness": + if (isPad || isPhone) { + let isFitnessAppInstallationAllowed = false; + if (serverData.isDefinedNonNull(objectGraph.user.isFitnessAppInstallationAllowed)) { + isFitnessAppInstallationAllowed = objectGraph.user.isFitnessAppInstallationAllowed; + } + return !isFitnessAppInstallationAllowed; + } + else { + /// Should be safe to filter out on all other platforms since tvOS has it bundled and watchOS has it as Activity. + return true; + } + case "com.apple.measure": + return !deviceHasCapability(objectGraph, "arkit", data); + case "com.apple.Jellyfish": + return !deviceHasCapability(objectGraph, "front-depth-camera", data); + // Old 2nd party Shortcuts is no longer available on iOS 13 + // <rdar://problem/49929227> SAD: Filter our Shortcuts 2nd party app in App Store on iOS 13 + case "is.workflow.my.app": + return true; + // Find My apps are being combined in iOS 13. Filter out the existing ones + // <rdar://problem/48297097> SAD: Filter FM apps from app store results + case "com.apple.mobileme.fmf1": + return true; + case "com.apple.NanoHeartRhythm": + return !objectGraph.client.isElectrocardiogramInstallationAllowed; + // This app has hardware requirements we must enforce. + // <rdar://problem/48789916> WAS: SAD: Ursa App - HW exclusion + case "com.apple.NanoCompass.watchkitapp": + return !deviceHasCapability(objectGraph, "magnetometer", data); + case "com.apple.NanoOxygenSaturation.watchkitapp": + return !objectGraph.client.isScandiumInstallationAllowed; + case "com.apple.DeepBreathing": + return true; + case "com.apple.NanoRadio": + return true; + case "com.apple.Depth": + return !objectGraph.client.isCharonSupported; + case "com.apple.Mandrake": + return !objectGraph.client.isMandrakeSupported; + case "com.apple.GenerativePlaygroundApp": + return !deviceHasCapability(objectGraph, "generative-model-systems", data); + default: + break; + } + return false; + }); +} +function shouldFilterMerchandisableInAppPurchase(objectGraph, data) { + if (data.type !== "in-apps") { + return false; + } + // If a contingent offer exists than do not filter + if (serverData.isDefinedNonNullNonEmpty(serverData.asDictionary(data, "meta.contingentItemOffer"))) { + return false; + } + if (serverData.isDefinedNonNullNonEmpty(serverData.asDictionary(data, "meta.discountOffer"))) { + return false; + } + return !mediaAttributes.attributeAsBooleanOrFalse(data, "isMerchandisedEnabled"); +} +/** + * Returns whether an app supports at least the current version of the OS + * @param {JSONData} data + * @returns {boolean} + */ +function shouldFilterForMinimumOSRequirement(objectGraph, data) { + // If the device is a phone but the app is watch-only, check that the watch version is supported. + if (objectGraph.client.isPhone && + contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "isStandaloneForWatchOS")) { + const minimumWatchOSVersionString = contentAttributes.contentAttributeAsString(objectGraph, data, "minimumWatchOSVersion"); + if (!content.isActivePairedWatchOSAtLeastVersion(objectGraph, minimumWatchOSVersionString)) { + return true; + } + } + if (objectGraph.client.isWatch) { + const minWatchOSVersion = contentAttributes.contentAttributeAsString(objectGraph, data, "minimumWatchOSVersion"); + return !content.isOSAtLeastVersion(objectGraph, minWatchOSVersion); + } + const minOSVersionString = content.minimumOSVersionFromData(objectGraph, data, objectGraph.appleSilicon.isSupportEnabled); + return !content.isOSAtLeastVersion(objectGraph, minOSVersionString); +} +/** + * Returns whether an app supports at least the current version of the companion's OS + * @param {JSONData} data + * @returns {boolean} + */ +function shouldFilterForMinimumCompanionOSRequirement(objectGraph, data) { + if (objectGraph.client.deviceType !== "watch") { + return false; + } + // We do not need to check the companion OS version when an app is standalone, or can run standalone. + if (contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "isStandaloneForWatchOS") || + contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "isStandaloneWithCompanionForWatchOS")) { + return false; + } + const attributePlatform = contentAttributes.bestAttributePlatformFromData(objectGraph, data); + const minCompanionVersion = mediaPlatformAttributes.platformAttributeAsString(data, attributePlatform, "minimumOSVersion"); + return !content.isActivePairedDeviceAtLeastVersion(objectGraph, minCompanionVersion); +} +function shouldFilterSADWatchApps(objectGraph, data) { + if (objectGraph.client.isWatch || objectGraph.client.isWeb) { + return false; + } + const isFirstPartyHideableApp = mediaAttributes.attributeAsBooleanOrFalse(data, "isFirstPartyHideableApp"); + const isWatchOnlyApp = !contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "isDeliveredInIOSAppForWatchOS") && + contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "isStandaloneForWatchOS"); + return isFirstPartyHideableApp && isWatchOnlyApp; +} +function shouldFilterForMacOSCompatibleIOSBinary(objectGraph, data) { + if (objectGraph.client.deviceType !== "mac") { + return false; // mac only filter. + } + let isIOSBinaryMacOSCompatible = contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "isIOSBinaryMacOSCompatible", contentAttributes.defaultAttributePlatform(objectGraph)); + // Override for News in Moltres + if (preprocessor.GAMES_TARGET && data.id === "1066498020") { + isIOSBinaryMacOSCompatible = true; + } + return isIOSBinaryMacOSCompatible && !objectGraph.appleSilicon.isSupportEnabled; +} +function shouldFilterRosetta(objectGraph, data) { + // Only apply Rosetta filtering to macOS apps on macOS devices. + const appPlatforms = content.supportedAppPlatformsFromData(objectGraph, data); + if (objectGraph.client.deviceType !== "mac" || !appPlatforms.includes("mac")) { + return false; + } + const isBuyable = content.isMacOSAppBuyableAndRunnableFromData(objectGraph, data, objectGraph.appleSilicon.isSupportEnabled, objectGraph.appleSilicon.isRosettaAvailable); + return !isBuyable; +} +function shouldFilterForVisionOSCompatibility(objectGraph, data) { + // Only apply filtering to visionOS compatible apps when running in the Vision companion app. + if (!objectGraph.client.isCompanionVisionApp) { + return false; + } + const isVisionOSCompatibleIOSBinary = mediaPlatformAttributes.platformAttributeAsBooleanOrFalse(data, "ios", "isXROSCompatible"); + const supportedAppPlatforms = content.supportedAppPlatformsFromData(objectGraph, data); + const isVisionOSBinary = supportedAppPlatforms.indexOf("vision") > -1; + return !isVisionOSBinary && !isVisionOSCompatibleIOSBinary; +} +/** + * Checks whether filtering should be applied due to minimum OS, specifically for a SAD app. This check + * exists separately to `shouldFilterForMinimumOSRequirement` as offer buttons aren't typically disabled + * for apps that don't meet the minOS, but they should be for SAD apps. + * + * @param objectGraph Current object graph + * @param data The product data + * @returns True if the product should be filtered + */ +function shouldFilterForSADMinimumOSRequirement(objectGraph, data) { + const systemApps = sad.systemApps(objectGraph); + if (!systemApps.isSystemAppFromData(data)) { + return false; + } + return shouldFilterForMinimumOSRequirement(objectGraph, data); +} +/** + * Checks whether a device has a given capability. + * @param objectGraph Current object graph + * @param capability The capability string + * @param data The data related to the product + * @returns True if the device supports the capability. + */ +function deviceHasCapability(objectGraph, capability, data) { + const supportsVisionOSCompatibleIOSBinary = content.supportsVisionOSCompatibleIOSBinaryFromData(objectGraph, data); + return objectGraph.client.deviceHasCapabilitiesIncludingCompatibilityCheckIsVisionOSCompatibleIOSApp([capability], supportsVisionOSCompatibleIOSBinary); +} +//# sourceMappingURL=filtering.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/grouping/grouping-common.js b/node_modules/@jet-app/app-store/tmp/src/common/grouping/grouping-common.js new file mode 100644 index 0000000..53294d3 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/grouping/grouping-common.js @@ -0,0 +1,319 @@ +import { isSome } from "@jet/environment"; +import * as validation from "@jet/environment/json/validation"; +import * as models from "../../api/models"; +import * as serverData from "../../foundation/json-parsing/server-data"; +import * as mediaAttributes from "../../foundation/media/attributes"; +import * as mediaRelationship from "../../foundation/media/relationships"; +import * as artworkBuilder from "../content/artwork/artwork"; +import * as metricsHelpersClicks from "../metrics/helpers/clicks"; +import * as types from "./grouping-types"; +import { GroupingAppEventShelfController } from "./shelf-controllers/grouping-app-event-shelf-controller"; +import { GroupingArcadeFooterShelfController } from "./shelf-controllers/grouping-arcade-footer-shelf-controller"; +import { GroupingBrickShelfController } from "./shelf-controllers/grouping-brick-shelf-controller"; +import { GroupingRibbonBarShelfController } from "./shelf-controllers/grouping-ribbon-bar-shelf-controller"; +import { GroupingCategoryShelfController } from "./shelf-controllers/grouping-category-shelf-controller"; +import { GroupingEditorialCardShelfController } from "./shelf-controllers/grouping-editorial-card-shelf-controller"; +import { GroupingEditorialStoryCardShelfController } from "./shelf-controllers/grouping-editorial-story-card-shelf-controller"; +import { GroupingGameCenterActivityFeedController } from "./shelf-controllers/grouping-game-center-activity-feed-shelf-controller"; +import { GroupingGameCenterContinuePlayingShelfController } from "./shelf-controllers/grouping-game-center-continue-playing-shelf-controller"; +import { GroupingGameCenterPopularWithYourFriendsController } from "./shelf-controllers/grouping-game-center-popular-with-your-friends-shelf-controller"; +import { GroupingGameCenterReengagementShelfController } from "./shelf-controllers/grouping-game-center-reengagement-shelf-controller"; +import { GroupingGameCenterSuggestedFriendsController } from "./shelf-controllers/grouping-game-center-suggested-friends-shelf-controller"; +import { GroupingHeroCarouselShelfController } from "./shelf-controllers/grouping-hero-carousel-shelf-controller"; +import { GroupingHorizontalCardShelfController } from "./shelf-controllers/grouping-horizontal-card-shelf-controller"; +import { GroupingLargeBreakoutShelfController } from "./shelf-controllers/grouping-large-breakout-shelf-controller"; +import { GroupingLinkShelfController } from "./shelf-controllers/grouping-link-shelf-controller"; +import { GroupingLockupShelfController } from "./shelf-controllers/grouping-lockup-shelf-controller"; +import { GroupingPersonalizedLockupShelfController } from "./shelf-controllers/grouping-personalized-lockup-shelf-controller"; +import { createShelfAccessibilityMetadata, } from "./shelf-controllers/grouping-shelf-controller"; +import * as groupingShelfControllerCommon from "./shelf-controllers/grouping-shelf-controller-common"; +import { GroupingSmallBreakoutShelfController } from "./shelf-controllers/grouping-small-breakout-shelf-controller"; +import { ArcadeDownloadPackShelfController } from "./shelf-controllers/arcade-download-pack-shelf-controller"; +import { flowActionForAccountURL } from "../account/account-links-regex-parser"; +import { GroupingMediaPageHeaderShelfController } from "./shelf-controllers/grouping-tags-header-shelf-controller"; +import { applySearchAdMissedOpportunityToShelvesIfNeeded } from "../ads/ad-common"; +import { GroupingTagBrickShelfController } from "./shelf-controllers/grouping-tag-brick-shelf-controller"; +// region Constants +/// The controllers that know how to render specific shelves +const shelfControllers = [ + new GroupingAppEventShelfController(), + new GroupingArcadeFooterShelfController(), + new GroupingBrickShelfController(), + new GroupingRibbonBarShelfController(), + new GroupingCategoryShelfController(), + new GroupingEditorialCardShelfController(), + new GroupingEditorialStoryCardShelfController(), + new GroupingGameCenterContinuePlayingShelfController(), + new GroupingGameCenterPopularWithYourFriendsController(), + new GroupingGameCenterReengagementShelfController(), + new GroupingGameCenterSuggestedFriendsController(), + new GroupingGameCenterActivityFeedController(), + new GroupingHeroCarouselShelfController(), + new GroupingHorizontalCardShelfController(), + new GroupingLargeBreakoutShelfController(), + new GroupingLinkShelfController(), + new GroupingLockupShelfController(), + new GroupingPersonalizedLockupShelfController(), + new GroupingSmallBreakoutShelfController(), + new ArcadeDownloadPackShelfController(), + new GroupingMediaPageHeaderShelfController(), + new GroupingTagBrickShelfController(), +]; +/** + * For a given flattened grouping go through and use our controllers to render the associated shelves, this should be used + * for the initial page load. The second fetches for these shelves are handled by the routing system and the routed + * shelf controller. This method will add all the shelves to the current groupingParseContext. + * + * @param objectGraph The App Store dependency graph + * @param flattenedGrouping The original grouping page response data from MAPI flattened into the list of featured content + * @param groupingParseContext The current parse context for this rendering of the grouping. + * @protected + */ +export function insertInitialShelvesIntoGroupingParseContext(objectGraph, flattenedGrouping, groupingParseContext) { + validation.catchingContext(`parseGrouping`, () => { + for (const mediaApiData of flattenedGrouping.data) { + const featuredContentId = mediaAttributes.attributeAsNumber(mediaApiData, "editorialElementKind"); + if (types.isTopChart(featuredContentId)) { + const editorialId = serverData.asString(mediaApiData, "id"); + groupingParseContext.chartSet = lookupChartSetForEditorialId(editorialId, flattenedGrouping.editorialChartSets); + } + else { + groupingParseContext.chartSet = null; + } + const baseRequirements = groupingShelfControllerCommon.createBaseShelfRequirements(objectGraph, mediaApiData, groupingParseContext); + const token = baseRequirements.shelfToken; + const baseMetricsOptions = baseRequirements.metricsOptions; + let shelf; + for (const shelfController of shelfControllers) { + if (shelfController.supports(objectGraph, mediaApiData, featuredContentId)) { + shelf = validation.catchingContext(`parseGroupingShelf`, () => { + return shelfController.createShelf(objectGraph, mediaApiData, groupingParseContext, token, baseMetricsOptions); + }); + break; + } + } + if (serverData.isDefinedNonNull(shelf)) { + groupingParseContext.shelves.push(shelf); + applySearchAdMissedOpportunityToShelvesIfNeeded(objectGraph, groupingParseContext.shelves, "searchLanding", baseMetricsOptions.id, groupingParseContext.metricsPageInformation); + } + } + }); +} +/** + * Parse a single editorially programmed chart from media API data. + * @returns The chart model or null. + */ +function makeEditorialChartFromMediaApiData(chartData) { + const fcKind = mediaAttributes.attributeAsNumber(chartData, "editorialElementKind"); + if (!types.isTopChart(fcKind)) { + return null; + } + return { + id: serverData.asString(chartData, "id"), + href: mediaAttributes.attributeAsString(chartData, "chartHref"), + name: mediaAttributes.attributeAsString(chartData, "name"), + type: mediaAttributes.attributeAsString(chartData, "chart"), + }; +} +/** + * Parse a collection of charts, programmed by editorial, from media api data. + * @returns The chart set model, or null. + */ +function makeEditorialChartSetFromMediaApiData(chartSetData) { + const fcKind = mediaAttributes.attributeAsNumber(chartSetData, "editorialElementKind"); + if (fcKind !== 424 /* types.FeaturedContentID.AppStore_ChartSet */) { + return null; + } + return { + id: serverData.asString(chartSetData, "id"), + name: mediaAttributes.attributeAsString(chartSetData, "name"), + editorialCharts: mediaRelationship + .relationshipCollection(chartSetData, "children") + .map((chartData) => makeEditorialChartFromMediaApiData(chartData)) + .filter((chart) => isSome(chart)), + }; +} +/** + * Given a list of chart sets, find the chart set that has the provided editorial identifier. + * + * @param editorialId The chart or chart set identifier from editorial. + * @param chartSets The list of all chart sets from the grouping. + * @returns The charts associated with the editorial item from the grouping. + */ +function lookupChartSetForEditorialId(editorialId, editorialChartSets) { + for (const chartSet of editorialChartSets) { + if (chartSet.id === editorialId) { + return chartSet; // editorialId is for a chart set itself + } + for (const chart of chartSet.editorialCharts) { + if (chart.id === editorialId) { + return chartSet; // / editorialId is for a specific chart from the chart set. + } + } + } + return null; +} +/** + * Take the media API data for a single grouping, and flatten that tree structure into a single list of media api data objects. + * + * @param mediaApiGroupingData The original media API data for a single grouping + */ +export function flattenMediaApiGroupingData(objectGraph, mediaApiGroupingData) { + const groupingData = []; + const chartSets = []; + function addMediaApiDataToGroupingData(mediaApiData) { + const featuredContentId = mediaAttributes.attributeAsNumber(mediaApiData, "editorialElementKind"); + validation.catchingContext(`flattenGroupingTree.addMediaApiDataToGroupingData: ${featuredContentId}`, () => { + const groupingTabData = mediaRelationship.relationshipData(objectGraph, mediaApiData, "tabs"); + if (serverData.isDefinedNonNull(groupingTabData)) { + addMediaApiDataToGroupingData(groupingTabData); + } + else if (types.shouldContinueToWalkChildren(featuredContentId)) { + const featuredContentChildren = mediaRelationship.relationshipCollection(mediaApiData, "children"); + for (const featuredContentChild of featuredContentChildren) { + addMediaApiDataToGroupingData(featuredContentChild); + } + } + else if (featuredContentId === 424 /* types.FeaturedContentID.AppStore_ChartSet */) { + // Keep track of chartSets programmed by editorial, e.g. "Top Charts" includes "Top Free Apps" and "Top Paid Apps". + const editorialChartSet = makeEditorialChartSetFromMediaApiData(mediaApiData); + if (isSome(editorialChartSet)) { + chartSets.push(editorialChartSet); + } + // Add charts from the chartSet, as defined by editorial, to grouping page data. + const chartSetChildren = mediaRelationship.relationshipCollection(mediaApiData, "children"); + for (const chartSetChild of chartSetChildren) { + addMediaApiDataToGroupingData(chartSetChild); + } + } + else { + groupingData.push(mediaApiData); + } + }); + } + addMediaApiDataToGroupingData(mediaApiGroupingData); + return { + data: groupingData, + editorialChartSets: chartSets, + originalGroupingData: mediaApiGroupingData, + }; +} +// endregion +// region Shelves +/** + * Create a shelf for footer buttons, e.g "Send Gift" and "Redeem". + * + * @return Shelf configured to display `FooterButtons`s. + */ +export function shelfForFooterButtons(objectGraph, metricsPageInformation, metricsLocationTracker) { + var _a, _b, _c, _d, _e, _f; + if (objectGraph.user && objectGraph.user.isManagedAppleID) { + return null; + } + // Watch App Store does not support titled button stacks, + // and has different footer buttons. + if (objectGraph.client.isWatch) { + const shelf = new models.Shelf("action"); + const items = []; + const accountFlowAction = new models.FlowAction("account"); + accountFlowAction.title = objectGraph.loc.string("ACCOUNT", "Account"); + accountFlowAction.artwork = artworkBuilder.createArtworkForResource(objectGraph, "systemimage://person.crop.circle"); + const accountFlowClickOptions = { + targetType: "button", + id: "account", + pageInformation: metricsPageInformation, + locationTracker: metricsLocationTracker, + }; + metricsHelpersClicks.addClickEventToAction(objectGraph, accountFlowAction, accountFlowClickOptions); + items.push(accountFlowAction); + shelf.items = items; + shelf.accessibilityMetadata = createShelfAccessibilityMetadata(objectGraph, shelf); + return shelf; + } + else { + const items = []; + const redeemTitle = objectGraph.loc.string("FOOTER_REDEEM", "Redeem"); + const redeemActionUrl = objectGraph.client.isVision + ? objectGraph.bag.redeemCodeLanding + : objectGraph.bag.redeemUrl; + const redeemFlowAction = flowActionForAccountURL(objectGraph, redeemActionUrl); + const redeemButton = new models.TitledButton(redeemTitle, redeemFlowAction); + redeemButton.id = "redeem"; + items.push(redeemButton); + if (objectGraph.bag.isMonetaryGiftingEnabled) { + const giftTitle = objectGraph.loc.string("FOOTER_SEND_GIFT", "Send Gift"); + const giftActionUrl = "gift"; + const giftFlowAction = flowActionForAccountURL(objectGraph, giftActionUrl); + const giftButton = new models.TitledButton(giftTitle, giftFlowAction); + giftButton.id = "gift"; + items.push(giftButton); + } + const topUpUrl = objectGraph.bag.accountTopUpURL; + if (isSome(topUpUrl)) { + const topUpTitle = (_a = objectGraph.bag.accountTopUpTitle) !== null && _a !== void 0 ? _a : objectGraph.loc.string("FOOTER_ADD_MONEY"); + const topUpFlowAction = flowActionForAccountURL(objectGraph, topUpUrl); + const topUpButton = new models.TitledButton(topUpTitle, topUpFlowAction); + topUpButton.id = "topUp"; + items.push(topUpButton); + } + let shelf; + if (objectGraph.client.isVision) { + shelf = new models.Shelf("titledButton"); + shelf.items = items; + } + else { + shelf = new models.Shelf("titledButtonStack"); + const stack = new models.TitledButtonStack(items); + // On compact width, we want to place a line break after each button. + stack.compactLineBreaks = stack.buttons.map((value, index) => index); + shelf.items = [stack]; + const accessibilityLabel = objectGraph.loc.string("Shelves.Accessibility.Label"); + const roleDescription = objectGraph.loc.string("Shelves.Accessibility.RoleDescription"); + shelf.accessibilityMetadata = { + label: (_d = (_b = shelf.title) !== null && _b !== void 0 ? _b : (_c = shelf.header) === null || _c === void 0 ? void 0 : _c.title) !== null && _d !== void 0 ? _d : accessibilityLabel, + roleDescription: roleDescription.replace("%d", `${(_f = (_e = stack.buttons) === null || _e === void 0 ? void 0 : _e.length) !== null && _f !== void 0 ? _f : 0}`), + }; + } + return shelf; + } +} +/** + * Create a T&C shelf that on click opens an external url. + * + * @param objectGraph + * @param destinationUrl Url to open when terms and conditions is tapped. + * @param hasSeparator Whether the footnote has a separator above it. + * @return Shelf configured to display Terms and Conditions. + */ +export function shelfForTermsAndConditions(objectGraph, destinationUrl, hasSeparator = true) { + const urlAction = new models.ExternalUrlAction(destinationUrl); + const termsAndConditionTitle = objectGraph.loc.string("TermsAndConditions.Title"); + const footnote = new models.Footnote(termsAndConditionTitle); + footnote.clickAction = urlAction; + footnote.presentationStyle = ["hasChevron", "textLightensOnHighlight"]; + if (hasSeparator) { + footnote.presentationStyle.push("hasSeparator"); + } + const shelf = new models.Shelf("footnote"); + shelf.items = [footnote]; + if (objectGraph.bag.emailSupportLinkURL) { + const emailSupportAction = new models.ExternalUrlAction(objectGraph.bag.emailSupportLinkURL); + const emailSupport = new models.Footnote("Email Support"); + emailSupport.clickAction = emailSupportAction; + emailSupport.presentationStyle = ["hasChevron", "textLightensOnHighlight"]; + if (hasSeparator) { + emailSupport.presentationStyle.push("hasSeparator"); + } + shelf.items.push(emailSupport); + } + return shelf; +} +export function shelfForUnifiedMessage(objectGraph, placement, context, deliveryMethod) { + const shelf = new models.Shelf("unifiedMessage"); + shelf.id = placement; + shelf.items = [new models.UnifiedMessage(placement, context, deliveryMethod)]; + shelf.isHidden = true; + return shelf; +} +// endregion +//# sourceMappingURL=grouping-common.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/grouping/grouping-page-url.js b/node_modules/@jet-app/app-store/tmp/src/common/grouping/grouping-page-url.js new file mode 100644 index 0000000..4e4c608 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/grouping/grouping-page-url.js @@ -0,0 +1,8 @@ +import { makeGroupingPageIntentByID } from "../../api/intents/grouping-page-intent"; +import { generateRoutes } from "../util/generate-routes"; +const { routes: groupingPageRoutes, makeCanonicalUrl } = generateRoutes(makeGroupingPageIntentByID, "/{platform}/grouping/{id}"); +export { groupingPageRoutes }; +export function makeGroupingPageCanonicalURL(objectGraph, intent) { + return makeCanonicalUrl(objectGraph, intent); +} +//# sourceMappingURL=grouping-page-url.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/grouping/grouping-request.js b/node_modules/@jet-app/app-store/tmp/src/common/grouping/grouping-request.js new file mode 100644 index 0000000..2ec9021 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/grouping/grouping-request.js @@ -0,0 +1,107 @@ +import { Request, defaultAdditionalPlatformsForClient, defaultSparseLimitForClient, } from "../../foundation/media/data-fetching"; +import { configureContingentItemsForGroupingRequest, configureOfferItemsForMediaRequest, configureTagsForMediaRequest, } from "../builders/url-mapping-utils"; +import { shouldFetchCustomAttributes } from "../product-page/product-page-variants"; +import { appContingentItemsAreEnabled, appEventsAreEnabled, appOfferItemsAreEnabled, } from "../app-promotions/app-promotions-common"; +import { areAppTagsEnabled } from "../util/app-tags-util"; +import * as mediaRequestUtils from "../../common/builders/url-mapping-utils"; +import { shouldUsePrerenderedIconArtwork } from "../content/content"; +import { AppEventsAttributes } from "../../gameservicesui/src/foundation/media-api/requests/recommendation-request-types"; +/** + * Create a "base" Media API {@linkcode Request} object for a "Grouping" page + * + * This is used to provision an initial {@linkcode Request} object that will be further + * modified later to include the specific page that should be fetched + * + * @see {@linkcode prepareGroupingPageRequest} to complete configuration of the `Request` object + */ +export function makeBaseGroupingPageRequest(objectGraph) { + // TODO: Update this code to work with genreId when this radar is complete + // <rdar://problem/37676122> Media Api: Need ability to lookup grouping resource by genreId + // TODO: Remove isAppleWatchSupported, once the following radar is resolved + // <rdar://problem/38923866> Media API: Include isAppleWatchSupported in secondary lookups + // Add this back in 18E when grouping supports additional platforms + // fixed with <rdar://problem/38899888> App Store Personalization: grouping: remove tabs query param for Apps realm + // .includingAdditionalPlatforms(mediaDataFetching.defaultAdditionalPlatformsForClient()) + const attributes = [ + "editorialArtwork", + "editorialVideo", + "isAppleWatchSupported", + "requiredCapabilities", + "expectedReleaseDateDisplayFormat", + "showExpectedReleaseDate", + "badge-content", + ]; + if (objectGraph.appleSilicon.isSupportEnabled) { + attributes.push("macRequiredCapabilities"); + } + if (objectGraph.client.isMac) { + attributes.push("hasMacIPAPackage"); + } + if (objectGraph.bag.enableUpdatedAgeRatings) { + attributes.push("ageRating"); + } + if (shouldUsePrerenderedIconArtwork(objectGraph)) { + attributes.push("iconArtwork"); + } + const mediaApiRequest = new Request(objectGraph) + .forType("groupings") + .includingAgeRestrictions() + .includingAttributes(attributes) + .includingRelationshipsForUpsell(true); + if (areAppTagsEnabled(objectGraph, "grouping")) { + mediaRequestUtils.configureTagsForMediaRequest(mediaApiRequest); + } + return mediaApiRequest; +} +/** + * Complete the configuration of a "Grouping Page" {@linkcode Request} + */ +export function prepareGroupingPageRequest(objectGraph, request) { + request.includingAdditionalPlatforms(defaultAdditionalPlatformsForClient(objectGraph)); + request.includingAgeRestrictions(); + request.usingCustomAttributes(shouldFetchCustomAttributes(objectGraph)); + // <rdar://53420717> For performance reasons, we limit shelves to 3 items on watchOS + if (objectGraph.client.isWatch) { + request.addingRelationshipLimit("contents", 3); + } + if (objectGraph.client.isWeb) { + // The "web" client needs to load *all* of the data for SEO purposes + // Without this, shelves will render with some missing items + request.withSparseCount(40); + const sparseLimit = defaultSparseLimitForClient(objectGraph); + if (sparseLimit) { + request.includingScopedSparseLimit("editorial-elements:contents", sparseLimit); + } + } + request.includingMacOSCompatibleIOSAppsWhenSupported(); + if (appEventsAreEnabled(objectGraph)) { + request.enablingFeature("appEvents"); + request.includingMetaKeys("editorial-elements:contents", ["personalizationData", "cppData"]); + request.includingScopedAttributes("app-events", AppEventsAttributes); + request.includingScopedRelationships("app-events", ["app"]); + } + if (areAppTagsEnabled(objectGraph, "grouping")) { + configureTagsForMediaRequest(request); + } + if (appContingentItemsAreEnabled(objectGraph)) { + request.enablingFeature("contingentItems"); + configureContingentItemsForGroupingRequest(request); + } + if (appOfferItemsAreEnabled(objectGraph)) { + request.enablingFeature("offerItems"); + configureOfferItemsForMediaRequest(request); + } + if (objectGraph.client.isiOS) { + request.enablingFeature("categoryHeaders"); + request.enablingFeature("onDevicePersonalization"); + } + // Enable category features + if (objectGraph.bag.enableFeaturedCategoriesOnGroupings) { + request.enablingFeature("featuredCategories"); + } + // Enable category bricks + if (objectGraph.bag.enableCategoryBricksOnGroupings) { + request.enablingFeature("categoryBricks"); + } +} +//# sourceMappingURL=grouping-request.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/grouping/grouping-types.js b/node_modules/@jet-app/app-store/tmp/src/common/grouping/grouping-types.js new file mode 100644 index 0000000..33e01b7 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/grouping/grouping-types.js @@ -0,0 +1,128 @@ +export const topChartFeaturedContentIds = new Set([ + 117 /* FeaturedContentID.Common_iPhoneTopChart */, + 122 /* FeaturedContentID.Common_iPhonePaidTopChart */, + 118 /* FeaturedContentID.Common_iPhoneFreeTopChart */, + 195 /* FeaturedContentID.Common_iPadTopChart */, + 194 /* FeaturedContentID.Common_iPadFreeTopChart */, + 197 /* FeaturedContentID.Common_iPadPaidTopChart */, + 369 /* FeaturedContentID.AppStore_iPhoneFreeTopChartV2 */, + 370 /* FeaturedContentID.AppStore_iPhoneGrossingTopCharV2t */, + 371 /* FeaturedContentID.AppStore_iPadFreeTopChartV2 */, + 372 /* FeaturedContentID.AppStore_iPadGrossingTopChartV2 */, + 373 /* FeaturedContentID.AppStore_TVFreeTopChartV2 */, + 374 /* FeaturedContentID.AppStore_TVGrossingTopChartV2 */, + 375 /* FeaturedContentID.AppStore_WatchFreeTopChartV2 */, + 376 /* FeaturedContentID.AppStore_WatchGrossingTopChartV2 */, + 377 /* FeaturedContentID.AppStore_iPhonePaidTopChartV2 */, + 378 /* FeaturedContentID.AppStore_iPadPaidTopChartV2 */, + 379 /* FeaturedContentID.AppStore_TVPaidTopChartV2 */, + 380 /* FeaturedContentID.AppStore_WatchPaidTopChartV2 */, + 395 /* FeaturedContentID.AppStore_iPhoneiMessageFreeTopChart */, + 396 /* FeaturedContentID.AppStore_iPhoneiMessagePaidTopChart */, + 397 /* FeaturedContentID.AppStore_iPhoneiMessageGrossingTopChart */, + 398 /* FeaturedContentID.AppStore_iPadiMessageFreeTopChart */, + 399 /* FeaturedContentID.AppStore_iPadiMessagePaidTopChart */, + 400 /* FeaturedContentID.AppStore_iPadiMessageGrossingTopChart */, + 491 /* FeaturedContentID.AppStore_MacFreeTopChartV2 */, + 492 /* FeaturedContentID.AppStore_MacPaidTopChartV2 */, + 493 /* FeaturedContentID.AppStore_MacGrossingTopChartV2 */, + 504 /* FeaturedContentID.AppStore_SafariExtensionsFreeTopChartV2 */, + 505 /* FeaturedContentID.AppStore_SafariExtensionsPaidTopChartV2 */, + 506 /* FeaturedContentID.AppStore_SafariExtensionsGrossingTopChartV2 */, + 520 /* FeaturedContentID.AppStore_SafariExtensionsFreeTopChartV3_iPhone */, + 521 /* FeaturedContentID.AppStore_SafariExtensionsPaidTopChartV3_iPhone */, + 522 /* FeaturedContentID.AppStore_SafariExtensionsGrossingTopChartV3_iPhone */, + 523 /* FeaturedContentID.AppStore_SafariExtensionsFreeTopChartV3_iPad */, + 524 /* FeaturedContentID.AppStore_SafariExtensionsPaidTopChartV3_iPad */, + 525 /* FeaturedContentID.AppStore_SafariExtensionsGrossingTopChartV3_iPad */, + 530 /* FeaturedContentID.Arcade_iPadChartsV2 */, + 531 /* FeaturedContentID.Arcade_MacChartsV2 */, + 532 /* FeaturedContentID.Arcade_TVChartsV2 */, + 533 /* FeaturedContentID.Arcade_iPhoneChartsV2 */, + 567 /* FeaturedContentID.GameCenter_iPhoneTopCombinedChart */, + 571 /* FeaturedContentID.GameCenter_iPadTopCombinedChart */, +]); +const macTopChartFeaturedContentIds = new Set([ + 491 /* FeaturedContentID.AppStore_MacFreeTopChartV2 */, + 492 /* FeaturedContentID.AppStore_MacPaidTopChartV2 */, + 493 /* FeaturedContentID.AppStore_MacGrossingTopChartV2 */, +]); +const arcadeTopChartFeaturedContentIds = new Set([ + 530 /* FeaturedContentID.Arcade_iPadChartsV2 */, + 531 /* FeaturedContentID.Arcade_MacChartsV2 */, + 532 /* FeaturedContentID.Arcade_TVChartsV2 */, + 533 /* FeaturedContentID.Arcade_iPhoneChartsV2 */, +]); +/// Game Center top charts for iPhone and iPad +export const gameCenterTopChartFeaturedContentIds = new Set([ + 567 /* FeaturedContentID.GameCenter_iPhoneTopCombinedChart */, + 571 /* FeaturedContentID.GameCenter_iPadTopCombinedChart */, +]); +export const recommendationsLockupShelfFeaturedContentIds = new Set([ + 311 /* FeaturedContentID.Sundance_RecommendedAppsShelf */, + 312 /* FeaturedContentID.Sundance_RecommendedGamesShelf */, + 476 /* FeaturedContentID.AppStore_PersonalizedShelfMarker */, +]); +export const recommendationsShelfFeaturedContentIds = new Set([ + ...recommendationsLockupShelfFeaturedContentIds, + 518 /* FeaturedContentID.AppStore_PersonalizedAppEventsMarker */, +]); +export const lockupShelfFeaturedContentIds = new Set([ + 557 /* FeaturedContentID.AppStore_CategoryBreakoutMarker */, + 418 /* FeaturedContentID.AppStore_Shelf */, + 431 /* FeaturedContentID.AppStore_iAPShelf */, + 429 /* FeaturedContentID.AppStore_ScreenShotShelf */, + 430 /* FeaturedContentID.AppStore_VideoShelf */, + 420 /* FeaturedContentID.AppStore_TrailerShelf */, + 304 /* FeaturedContentID.Sundance_iPhoneAppTrailerShelf */, + 305 /* FeaturedContentID.Sundance_iPadAppTrailerShelf */, + 260 /* FeaturedContentID.Sundance_Shelf */, + 497 /* FeaturedContentID.AppStore_ComingSoon */, +]); +const featuredContentIdsRequiringAdditionalTraversal = new Set([ + 413 /* FeaturedContentID.AppStore_Root */, + 414 /* FeaturedContentID.AppStore_TabRoot */, + 254 /* FeaturedContentID.Sundance_Root */, + 255 /* FeaturedContentID.Sundance_TabRoot */, + 256 /* FeaturedContentID.Sundance_Stack */, + 266 /* FeaturedContentID.Sundance_QuickLinks */, + 271 /* FeaturedContentID.Sundance_TabSet */, + 436 /* FeaturedContentID.AppStore_DateBlock */, +]); +export function shouldContinueToWalkChildren(featuredContentId) { + return featuredContentIdsRequiringAdditionalTraversal.has(featuredContentId); +} +export function isLockupShelf(featuredContentId) { + return (isTopChart(featuredContentId) || + isRecommendationsShelf(featuredContentId) || + lockupShelfFeaturedContentIds.has(featuredContentId) || + featuredContentId === 518 /* FeaturedContentID.AppStore_PersonalizedAppEventsMarker */); +} +export function isRecommendationsLockupShelf(featuredContentId) { + return recommendationsLockupShelfFeaturedContentIds.has(featuredContentId); +} +export function isRecommendationsShelf(featuredContentId) { + return recommendationsShelfFeaturedContentIds.has(featuredContentId); +} +export function isMacTopChart(featuredContentId) { + return macTopChartFeaturedContentIds.has(featuredContentId); +} +export function isTopChart(featuredContentId) { + return topChartFeaturedContentIds.has(featuredContentId); +} +/// Game Center top charts are handled differently in some cases, +/// e.g. showing Game Center eyebrow +export function isGameCenterTopChart(featuredContentId) { + return gameCenterTopChartFeaturedContentIds.has(featuredContentId); +} +/** + * Arcade top charts are handled differently in some cases, eg. showing + * a 'See All' button + * + * @param featuredContentId The kind of featured content + * @returns Whether this is an Arcade Top Chart + */ +export function isArcadeTopChart(featuredContentId) { + return arcadeTopChartFeaturedContentIds.has(featuredContentId); +} +//# sourceMappingURL=grouping-types.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/grouping/hero/hero-carousel-overlay-common.js b/node_modules/@jet-app/app-store/tmp/src/common/grouping/hero/hero-carousel-overlay-common.js new file mode 100644 index 0000000..183947f --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/grouping/hero/hero-carousel-overlay-common.js @@ -0,0 +1,263 @@ +// +// hero-carousel-overlay-common.ts +// AppStoreKit +// +// Created by Jonathan Ellenbogen on 12/11/20. +// Copyright (c) 2016 Apple Inc. All rights reserved. +// +import * as models from "../../../api/models"; +import * as serverData from "../../../foundation/json-parsing/server-data"; +import * as mediaAttributes from "../../../foundation/media/attributes"; +import * as mediaDataStructure from "../../../foundation/media/data-structure"; +import * as mediaRelationship from "../../../foundation/media/relationships"; +import * as color from "../../../foundation/util/color-util"; +import * as breakoutsCommon from "../../arcade/breakouts-common"; +import * as contentAttributes from "../../content/attributes"; +import * as content from "../../content/content"; +import * as lockups from "../../lockups/lockups"; +import * as metricsHelpersClicks from "../../metrics/helpers/clicks"; +import * as metricsHelpersImpressions from "../../metrics/helpers/impressions"; +import * as heroCommon from "./hero-common"; +import * as lottery from "../../util/lottery"; +export function overlayFromData(objectGraph, data, requirements) { + const heroCarouselItemOverlay = new models.HeroCarouselItemOverlay(); + heroCarouselItemOverlay.overlayType = overlayTypeFromData(objectGraph, data); + heroCarouselItemOverlay.displayOptions = { + horizontalPlacement: overlayPlacementFromData(objectGraph, data), + textAlignment: overlayTextAlignmentFromData(objectGraph, data), + isOverDarkContent: overlayIsOverDarkContent(objectGraph, data), + }; + const primaryContent = content.primaryContentForData(objectGraph, data); + // If this EI can and should show the expected release date of an app, override the badge. + const fallbackLabel = mediaAttributes.attributeAsString(data, "label"); + if (contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "showExpectedReleaseDate")) { + heroCarouselItemOverlay.badgeText = objectGraph.loc.uppercased(content.dynamicPreorderDateFromData(objectGraph, primaryContent, fallbackLabel)); + } + else { + heroCarouselItemOverlay.badgeText = fallbackLabel; + } + heroCarouselItemOverlay.titleText = + content.editorialNotesFromData(objectGraph, data, "name") || + contentAttributes.contentAttributeAsString(objectGraph, primaryContent, "name"); + if (heroCarouselItemOverlay.overlayType === "singleModule" || + heroCarouselItemOverlay.overlayType === "collectionModule") { + heroCarouselItemOverlay.descriptionText = content.editorialNotesFromData(objectGraph, data, "tagline"); + } + else { + heroCarouselItemOverlay.descriptionText = + content.editorialNotesFromData(objectGraph, data, "short") || + contentAttributes.contentAttributeAsString(objectGraph, primaryContent, "tagline"); + } + heroCarouselItemOverlay.callToActionText = mediaAttributes.attributeAsString(data, "breakoutCallToActionLabel"); + heroCarouselItemOverlay.buttonTitle = overlayButtonTitleFromData(objectGraph, data); + if (heroCarouselItemOverlay.overlayType === "lockup" || heroCarouselItemOverlay.overlayType === "singleModule") { + heroCarouselItemOverlay.lockup = overlayLockupFromData(objectGraph, data, requirements); + } + // For hero items the design is to use editorial labels instead of "Apple Arcade" so we replace that text here + if ((fallbackLabel === null || fallbackLabel === void 0 ? void 0 : fallbackLabel.length) > 0 && serverData.isDefinedNonNull(heroCarouselItemOverlay.lockup)) { + heroCarouselItemOverlay.lockup.heading = fallbackLabel; + } + heroCarouselItemOverlay.collectionIcons = overlayCollectionIconsFromData(objectGraph, data); + if (serverData.isDefinedNonNullNonEmpty(heroCarouselItemOverlay.lockup)) { + heroCarouselItemOverlay.clickAction = heroCarouselItemOverlay.lockup.clickAction; + heroCarouselItemOverlay.impressionMetrics = heroCarouselItemOverlay.lockup.impressionMetrics; + } + else { + const heroCarouselItemOverlayMetricsOptions = { + targetType: "lockup", + pageInformation: requirements.metricsPageInformation, + locationTracker: requirements.metricsLocationTracker, + recoMetricsData: mediaDataStructure.metricsFromMediaApiObject(data), + }; + const overlayMetricsTitle = heroCarouselItemTitleFromData(objectGraph, data); + const heroCarouselItemOverlayAction = breakoutsCommon.actionFromData(objectGraph, data); + heroCarouselItemOverlayAction.title = overlayMetricsTitle; + const heroCarouselItemOverlayClickOptions = { + pageInformation: requirements.metricsPageInformation, + locationTracker: requirements.metricsLocationTracker, + recoMetricsData: mediaDataStructure.metricsFromMediaApiObject(data), + targetType: "lockup", + id: data.id, + }; + metricsHelpersClicks.addClickEventToAction(objectGraph, heroCarouselItemOverlayAction, heroCarouselItemOverlayClickOptions); + heroCarouselItemOverlay.clickAction = heroCarouselItemOverlayAction; + const heroCarouselItemOverlayImpressionsOptions = metricsHelpersImpressions.impressionOptions(objectGraph, data, overlayMetricsTitle, heroCarouselItemOverlayMetricsOptions); + metricsHelpersImpressions.addImpressionFields(objectGraph, heroCarouselItemOverlay, heroCarouselItemOverlayImpressionsOptions); + } + return heroCarouselItemOverlay; +} +export function overlayTypeFromData(objectGraph, data) { + const displayMaterial = mediaAttributes.attributeAsBooleanOrFalse(data, "displayBreakoutMaterial"); + const editorialItemKind = mediaAttributes.attributeAsString(data, "kind"); + const relatedCollection = mediaRelationship.relationshipCollection(data, "card-contents"); + const tagline = content.editorialNotesFromData(objectGraph, data, "tagline"); + const canPresentHeroModuleStyle = allowHeroModuleStylePresentation(objectGraph) && (tagline === null || tagline === void 0 ? void 0 : tagline.length) > 0; + let relatedApp = mediaRelationship.relationshipData(objectGraph, data, "primary-content"); + if (serverData.isNullOrEmpty(relatedApp) && serverData.isDefinedNonNullNonEmpty(relatedCollection)) { + relatedApp = relatedCollection[0]; + } + if (serverData.isDefinedNonNull(relatedApp) && editorialItemKind === "App") { + return canPresentHeroModuleStyle ? "singleModule" : "lockup"; + } + else if (serverData.isDefinedNonNullNonEmpty(relatedCollection) && editorialItemKind === "Collection") { + return canPresentHeroModuleStyle ? "collectionModule" : "collectionLockup"; + } + else if (displayMaterial) { + return "materialText"; + } + else { + return objectGraph.client.isTV ? "materialText" : "text"; + } +} +export function heroCarouselItemTitleFromData(objectGraph, data) { + const overlayType = overlayTypeFromData(objectGraph, data); + const primaryContent = mediaRelationship.relationshipData(objectGraph, data, "primary-content"); + switch (overlayType) { + case "lockup": + case "singleModule": + return mediaAttributes.attributeAsString(primaryContent, "name"); + case "materialText": + case "text": + case "collectionLockup": + case "collectionModule": + return (content.editorialNotesFromData(objectGraph, data, "name") || + contentAttributes.contentAttributeAsString(objectGraph, primaryContent, "name")); + default: + return null; + } +} +export function overlayLockupFromData(objectGraph, data, requirements) { + let relatedApp = mediaRelationship.relationshipData(objectGraph, data, "primary-content"); + const relatedCollection = mediaRelationship.relationshipCollection(data, "card-contents"); + if (serverData.isNullOrEmpty(relatedApp) && serverData.isDefinedNonNullNonEmpty(relatedCollection)) { + relatedApp = relatedCollection[0]; + } + if (serverData.isNullOrEmpty(relatedApp)) { + return null; + } + // Create the lockup + const lockupOptions = { + metricsOptions: { + pageInformation: requirements.metricsPageInformation, + locationTracker: requirements.metricsLocationTracker, + recoMetricsData: mediaDataStructure.metricsFromMediaApiObject(data), + }, + artworkUseCase: requirements.lockupArtworkUseCase, + offerStyle: "transparent", + offerEnvironment: "dark", + canDisplayArcadeOfferButton: requirements.canDisplayArcadeOfferButton, + shouldHideArcadeHeader: objectGraph.featureFlags.isEnabled("hide_arcade_header_on_arcade_tab"), + isContainedInPreorderExclusiveShelf: requirements.isContainedInPreorderExclusiveShelf, + }; + const lockup = lockups.lockupFromData(objectGraph, relatedApp, lockupOptions); + const primaryContent = content.primaryContentForData(objectGraph, data); + const editorialTagline = content.editorialNotesFromData(objectGraph, data, "short") || + contentAttributes.contentAttributeAsString(objectGraph, primaryContent, "tagline"); + if ((editorialTagline === null || editorialTagline === void 0 ? void 0 : editorialTagline.length) > 0) { + lockup.subtitle = editorialTagline; + } + return lockup; +} +export function overlayCollectionIconsFromData(objectGraph, data) { + const relatedCollection = mediaRelationship.relationshipCollection(data, "card-contents"); + if (serverData.isNullOrEmpty(relatedCollection)) { + return null; + } + const icons = []; + for (const item of relatedCollection) { + const icon = content.iconFromData(objectGraph, item, { + useCase: 1 /* content.ArtworkUseCase.LockupIconSmall */, + withJoeColorPlaceholder: true, + }); + if (serverData.isDefinedNonNullNonEmpty(icon)) { + icons.push(icon); + } + } + return serverData.isDefinedNonNullNonEmpty(icons) ? icons : null; +} +export function overlayButtonTitleFromData(objectGraph, data) { + if (objectGraph.client.deviceType !== "tv") { + return null; + } + const overlayType = overlayTypeFromData(objectGraph, data); + switch (overlayType) { + case "lockup": + return objectGraph.loc.string("HERO_CAROUSEL_OVERLAY_BUTTON_TITLE_GAME"); + case "materialText": + case "text": + let buttonTitle = objectGraph.loc.string("HERO_CAROUSEL_OVERLAY_BUTTON_TITLE_VIEW"); + if (buttonTitle === "HERO_CAROUSEL_OVERLAY_BUTTON_TITLE_VIEW") { + buttonTitle = objectGraph.loc.string("HERO_CAROUSEL_OVERLAY_BUTTON_TITLE_GAME"); + } + return buttonTitle; + case "collectionLockup": + return objectGraph.loc.string("HERO_CAROUSEL_OVERLAY_BUTTON_TITLE_COLLECTION"); + default: + return null; + } +} +export function overlayIsOverDarkContent(objectGraph, data) { + const heroVideoData = heroCommon.heroVideoFromData(objectGraph, data); + const heroArtworkData = heroCommon.heroArtworkFromData(objectGraph, data); + const backgroundColor = heroVideoData.backgroundColor || heroArtworkData.backgroundColor; + if (!backgroundColor) { + return true; + } + return color.isDarkColor(backgroundColor, 50); +} +export function overlayPlacementFromData(objectGraph, data) { + if (objectGraph.client.isPhone) { + return "center"; + } + if (objectGraph.client.isTV) { + return "leading"; + } + const heroCarouselItemPlacementString = mediaAttributes.attributeAsString(data, "breakoutTextAlignment"); + if (!serverData.isDefinedNonNullNonEmpty(heroCarouselItemPlacementString)) { + return objectGraph.client.isMac ? "center" : "leading"; + } + switch (heroCarouselItemPlacementString.toLowerCase()) { + case "left": + return "leading"; + case "center": + return "center"; + case "right": + return "trailing"; + default: + return "leading"; + } +} +/** + * The text alignment to use for a particular detail position on large breakouts + * @param data + */ +function overlayTextAlignmentFromData(objectGraph, data) { + const placement = overlayPlacementFromData(objectGraph, data); + const isTV = objectGraph.client.isTV; + switch (placement) { + case "leading": + return "leading"; + case "trailing": + return "leading"; + case "center": + if (isTV) { + return "leading"; + } + else { + return objectGraph.client.isMac ? "center" : "leading"; + } + default: + return "leading"; + } +} +/** + * Decides whether to account for the Hero 3.0 styles. + * + * @param objectGraph The object graph. + */ +function allowHeroModuleStylePresentation(objectGraph) { + const isNotTV = !objectGraph.client.isTV; + const isFeatureEnabledForCurrentUser = lottery.isFeatureEnabledForCurrentUser(objectGraph, objectGraph.bag.hero3RolloutRate); + return isNotTV && isFeatureEnabledForCurrentUser; +} +//# sourceMappingURL=hero-carousel-overlay-common.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/grouping/hero/hero-common.js b/node_modules/@jet-app/app-store/tmp/src/common/grouping/hero/hero-common.js new file mode 100644 index 0000000..4a67b72 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/grouping/hero/hero-common.js @@ -0,0 +1,90 @@ +// +// hero-carousel-overlay-common.ts +// AppStoreKit +// +// Created by Jonathan Ellenbogen on 12/11/20. +// Copyright (c) 2016 Apple Inc. All rights reserved. +// +import * as models from "../../../api/models"; +import * as serverData from "../../../foundation/json-parsing/server-data"; +import { artworkDictionaryFromData, videoDictionaryFromData } from "../../arcade/breakouts-common"; +import * as content from "../../content/content"; +export function heroArtworkFromData(objectGraph, data, presentedInTopShelf = false) { + const artworkDictionary = artworkDictionaryFromData(objectGraph, data); + const isPhone = objectGraph.client.isPhone; + const isTV = objectGraph.client.isTV; + let heroArtworkKey = isPhone ? "breakoutTall" : "breakoutFullScreen"; + if (presentedInTopShelf) { + heroArtworkKey = "topShelf"; + } + const heroArtworkData = serverData.asDictionary(artworkDictionary, heroArtworkKey); + let heroArtwork = null; + let backgroundColor = null; + if (serverData.isDefinedNonNullNonEmpty(heroArtworkData)) { + heroArtwork = content.artworkFromApiArtwork(objectGraph, heroArtworkData, { + withJoeColorPlaceholder: true, + useCase: 6 /* content.ArtworkUseCase.ArcadeLargeBreakout */, + }); + if (serverData.isDefinedNonNullNonEmpty(heroArtwork)) { + if (isPhone) { + heroArtwork.crop = "oa"; + } + else if (presentedInTopShelf) { + heroArtwork.crop = "ta"; + } + else if (isTV) { + heroArtwork.crop = "od"; + } + else { + heroArtwork.crop = "ob"; + } + backgroundColor = heroArtwork.backgroundColor; + } + } + return { + artwork: heroArtwork, + artworkData: heroArtworkData, + backgroundColor: backgroundColor, + }; +} +export function heroVideoFromData(objectGraph, data, isUpsell = false, wants9x16 = false) { + const videoDictionary = videoDictionaryFromData(objectGraph, data); + const use9x16 = objectGraph.client.isPhone || wants9x16; + const heroVideoKey = use9x16 ? "sizzleVideo9x16" : "sizzleVideo16x9"; + const heroVideoFallbackKey = use9x16 ? "breakoutVideo9x16" : "breakoutVideo16x9"; + const heroVideoData = serverData.asDictionary(videoDictionary, heroVideoKey) || + serverData.asDictionary(videoDictionary, heroVideoFallbackKey); + let heroVideo = null; + let backgroundColor = null; + let artworkData = null; + if (serverData.isDefinedNonNullNonEmpty(heroVideoData)) { + artworkData = serverData.asDictionary(heroVideoData, "previewFrame"); + const artwork = content.artworkFromApiArtwork(objectGraph, artworkData, { + withJoeColorPlaceholder: true, + useCase: 6 /* content.ArtworkUseCase.ArcadeLargeBreakout */, + }); + if (serverData.isDefinedNonNullNonEmpty(artwork)) { + artwork.crop = "sr"; + backgroundColor = artwork.backgroundColor; + } + // Get the video URL + const videoUrl = serverData.asString(heroVideoData, "video"); + if (serverData.isDefinedNonNullNonEmpty(artwork) && (videoUrl === null || videoUrl === void 0 ? void 0 : videoUrl.length) > 0) { + heroVideo = new models.Video(videoUrl, artwork, { + canPlayFullScreen: false, + allowsAutoPlay: true, + looping: true, + playbackControls: { + prominentPlay: isUpsell, + }, + autoPlayPlaybackControls: {}, + }); + } + } + return { + video: heroVideo, + artworkData: artworkData, + backgroundColor: backgroundColor, + }; +} +//# sourceMappingURL=hero-common.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/grouping/render-grouping-page.js b/node_modules/@jet-app/app-store/tmp/src/common/grouping/render-grouping-page.js new file mode 100644 index 0000000..8640079 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/grouping/render-grouping-page.js @@ -0,0 +1,246 @@ +import { isSome, isNothing } from "@jet/environment/types/optional"; +import { FlowAction, GenericPage, Shelf } from "../../api/models"; +import { attributeAsNumber, attributeAsString } from "../../foundation/media/attributes"; +import { dataFromDataContainer } from "../../foundation/media/data-structure"; +import { asNumber, isDefinedNonNull, isDefinedNonNullNonEmpty, isNull, } from "../../foundation/json-parsing/server-data"; +import { hasRelationship, relationship } from "../../foundation/media/relationships"; +import { URL } from "../../foundation/network/urls"; +import { appStoreIdentifier } from "../../foundation/wrappers/client"; +import { createArtworkForResource } from "../content/artwork/artwork"; +import { addMetricsEventsToPageWithInformation, metricsPageInformationFromMediaApiResponse, } from "../metrics/helpers/page"; +import { newPageRefreshControllerFromResponse, pageRefreshPolicyForController, } from "../refresh/page-refresh-controller"; +import { metricsData } from "../personalization/on-device-personalization"; +import { impressionEventsFromData } from "../personalization/on-device-impression-demotion"; +import { combinedRecoMetricsDataFromMetricsData } from "../metrics/helpers/util"; +import { newLocationTracker } from "../metrics/helpers/location"; +import { flattenMediaApiGroupingData, insertInitialShelvesIntoGroupingParseContext, shelfForFooterButtons, shelfForTermsAndConditions, shelfForUnifiedMessage, } from "./grouping-common"; +import { addBatchGroupsToPage } from "./shelf-batching"; +import { nameFromGroupingData } from "../lockups/lockups"; +// The key that contains the impression data for the grouping page fetched as an additional request. +export const GroupingImpressionAdditionalDataKey = "amdImpressionData"; +export function groupingParseContextFromDataContainer(objectGraph, data, additionalPageRequirements) { + const mediaApiGroupingData = dataFromDataContainer(objectGraph, data); + if (isNothing(mediaApiGroupingData)) { + return null; + } + const metricsPageInformation = metricsPageInformationFromMediaApiResponse(objectGraph, "Genre", mediaApiGroupingData.id, data); + const onDevicePersonalizationMetricsData = metricsData(objectGraph); + metricsPageInformation.recoMetricsData = combinedRecoMetricsDataFromMetricsData(metricsPageInformation.recoMetricsData, null, onDevicePersonalizationMetricsData); + return { + shelves: [], + metricsPageInformation: metricsPageInformation, + metricsLocationTracker: newLocationTracker(), + pageGenreAdamId: attributeAsString(mediaApiGroupingData, "id"), + pageGenreId: null, + hasAuthenticatedUser: isDefinedNonNull(objectGraph.user.dsid), + refreshController: newPageRefreshControllerFromResponse(data), + recoImpressionData: impressionEventsFromData(objectGraph, additionalPageRequirements === null || additionalPageRequirements === void 0 ? void 0 : additionalPageRequirements[GroupingImpressionAdditionalDataKey]), + }; +} +export function flattenedGroupingFromDataContainer(objectGraph, data) { + const mediaApiGroupingData = dataFromDataContainer(objectGraph, data); + if (!mediaApiGroupingData) { + return null; + } + if (!hasRelationship(mediaApiGroupingData, "tabs")) { + return null; + } + return flattenMediaApiGroupingData(objectGraph, mediaApiGroupingData); +} +// TODO: Once 18E is in we need to get the genreId from the chart URL +export function genreIdFromChartURL(chartURLString) { + if (isNull(chartURLString)) { + return null; + } + const chartURL = URL.from(chartURLString); + return asNumber(chartURL.query, "genre"); +} +function groupingPageTitleForData(objectGraph, groupingData, genreId) { + var _a; + let pageTitle = null; + switch (genreId) { + case 36 /* GenreIds.Apps */: + if (objectGraph.host.clientIdentifier === appStoreIdentifier && !objectGraph.client.isWatch) { + if (objectGraph.client.isMac) { + pageTitle = macDiscoverPageTitleForData(objectGraph, groupingData); + } + else { + pageTitle = objectGraph.loc.string("GROUPING_APPS"); + } + } + else { + if (objectGraph.client.isTV) { + pageTitle = objectGraph.loc.string("GROUPING_APPS"); + } + else if (objectGraph.client.isWeb) { + pageTitle = + ((_a = objectGraph.activeIntent) === null || _a === void 0 ? void 0 : _a.previewPlatform) === "mac" + ? macDiscoverPageTitleForData(objectGraph, groupingData) + : objectGraph.loc.string("GROUPING_APPS"); + } + else { + pageTitle = objectGraph.loc.string("GROUPING_APP_STORE", "App Store"); + } + } + break; + case 39 /* GenreIds.Discover */: + pageTitle = objectGraph.loc.string("GROUPING_DISCOVER"); + break; + default: + pageTitle = nameFromGroupingData(objectGraph, groupingData); + break; + } + return pageTitle; +} +/** + * Create a shelf for header buttons, e.g. "Search". + * + * @return Shelf configured to display header buttons. + */ +function shelfForHeaderButtons(objectGraph) { + const shelf = new Shelf("action"); + const items = []; + const searchFlowAction = new FlowAction("search"); + searchFlowAction.title = objectGraph.loc.string("SEARCH", "Search"); + searchFlowAction.artwork = createArtworkForResource(objectGraph, "systemimage://magnifyingglass"); + items.push(searchFlowAction); + shelf.items = items; + return shelf; +} +/** + * Returns the page title for the Discover tab on macOS + * + * @param objectGraph + * @param {Data} groupingData MAPI data blob describing the grouping + * @return Page title string + */ +function macDiscoverPageTitleForData(objectGraph, groupingData) { + const tabs = relationship(groupingData, "tabs"); + if (isDefinedNonNull(tabs) && isDefinedNonNull(tabs.data)) { + for (const tabData of tabs.data) { + const tabName = attributeAsString(tabData, "name"); + const featuredContentId = attributeAsNumber(tabData, "editorialElementKind"); + const token = attributeAsString(tabData, "token"); + if (isSome(tabName) && + tabName.length > 0 && + isDefinedNonNull(featuredContentId) && + isDefinedNonNullNonEmpty(token) && + token === "macOS" && + featuredContentId === 414 /* FeaturedContentID.AppStore_TabRoot */) { + return tabName; + } + } + } + return objectGraph.loc.string("GROUPING_DISCOVER"); +} +/** + * Insert the unified message shelves into the arcade page + * + * @param objectGraph The App Store dependency graph + * @param page The generic page to insert shelves into + * @param genreId The genre Id of the generic page, e.g. Apps or Games + */ +function insertUnifiedMessageShelves(objectGraph, page, genreId) { + if (page.shelves.length <= 0) { + return; + } + // Insert Unified message shelves. These are hidden by default and act as 'placeholder' locations + // for the marketing teams to target with inline banners or bubble tip messages + switch (genreId) { + case 36 /* GenreIds.Apps */: + page.shelves.splice(0, 0, shelfForUnifiedMessage(objectGraph, "appsPageHeader")); + page.shelves.splice(6, 0, shelfForUnifiedMessage(objectGraph, "appsPageMid")); + page.shelves.push(shelfForUnifiedMessage(objectGraph, "appsPageFooter")); + break; + case 6014 /* GenreIds.Games */: + page.shelves.splice(0, 0, shelfForUnifiedMessage(objectGraph, "gamesPageHeader")); + page.shelves.splice(6, 0, shelfForUnifiedMessage(objectGraph, "gamesPageMid")); + page.shelves.push(shelfForUnifiedMessage(objectGraph, "gamesPageFooter")); + break; + default: + break; + } +} +export function groupingPageFromFlattenedGrouping(objectGraph, flattenedGrouping, groupingParseContext) { + groupingParseContext.pageGenreId = + attributeAsNumber(flattenedGrouping.originalGroupingData, "genre") || + genreIdFromChartURL(attributeAsString(flattenedGrouping.originalGroupingData, "chartUrl")); + // Construct the shelves + insertInitialShelvesIntoGroupingParseContext(objectGraph, flattenedGrouping, groupingParseContext); + if (objectGraph.client.isWatch) { + // Remove title of first shelf with cards if needed. + if (groupingParseContext.shelves.length > 0 && groupingParseContext.shelves[0].contentType === "todayCard") { + groupingParseContext.shelves[0].title = undefined; + } + // Prepend Header Buttons + if (objectGraph.client.deviceType !== "watch") { + const headerButtonShelf = shelfForHeaderButtons(objectGraph); + groupingParseContext.shelves.unshift(headerButtonShelf); + } + } + // TODO: Add for ATV + // Append Footer Buttons + if (objectGraph.client.deviceType !== "tv" && objectGraph.client.deviceType !== "web") { + const footerButtonShelf = shelfForFooterButtons(objectGraph, groupingParseContext.metricsPageInformation, groupingParseContext.metricsLocationTracker); + if (footerButtonShelf) { + groupingParseContext.shelves.push(footerButtonShelf); + } + } + // TODO: Add for ATV + // Append T&C + if (objectGraph.client.deviceType !== "watch" && objectGraph.client.deviceType !== "tv") { + const url = objectGraph.bag.termsAndConditionsURL; + if (!isNull(url)) { + const termsAndConditionsShelf = shelfForTermsAndConditions(objectGraph, url); + groupingParseContext.shelves.push(termsAndConditionsShelf); + } + } + // Determine the title + const pageTitle = groupingPageTitleForData(objectGraph, flattenedGrouping.originalGroupingData, groupingParseContext.pageGenreId); + // Build the page + const page = new GenericPage(groupingParseContext.shelves); + switch (objectGraph.client.deviceType) { + case "tv": + page.title = pageTitle; + break; + case "watch": + page.title = pageTitle; + page.presentationOptions = ["prefersLargeTitleWhenRoot"]; + break; + case "web": + page.title = pageTitle; + page.presentationOptions = ["prefersLargeTitleWhenRoot"]; + const firstShelfType = page.shelves[0].contentType; + if (["heroCarousel", "editorialCard", "largeStoryCard"].includes(firstShelfType)) { + page.presentationOptions.push("prefersOverlayedPageHeader"); + } + break; + default: + page.title = pageTitle; + if ((groupingParseContext.pageGenreId === 39 /* GenreIds.Discover */ || + (groupingParseContext.pageGenreId === 36 /* GenreIds.Apps */ && objectGraph.client.isMac)) && + !objectGraph.client.isIconArtworkCapable) { + page.presentationOptions = ["prefersRevealNavigationBarOnMouseOver"]; + } + else { + page.presentationOptions = ["prefersLargeTitleWhenRoot"]; + } + break; + } + if (objectGraph.client.isiOS) { + if (groupingParseContext.shelves[0].contentType === "mediaPageHeader") { + page.presentationOptions.push("prefersNonStandardBackButton"); + page.presentationOptions.push("prefersHiddenPageTitle"); + page.presentationOptions.push("prefersOverlayedPageHeader"); + } + } + page.pageRefreshPolicy = pageRefreshPolicyForController(objectGraph, groupingParseContext === null || groupingParseContext === void 0 ? void 0 : groupingParseContext.refreshController); + addMetricsEventsToPageWithInformation(objectGraph, page, groupingParseContext.metricsPageInformation); + // Add unified message shelves + if (objectGraph.client.isiOS) { + insertUnifiedMessageShelves(objectGraph, page, groupingParseContext.pageGenreId); + } + addBatchGroupsToPage(page); + return page; +} +//# sourceMappingURL=render-grouping-page.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/grouping/shelf-batching.js b/node_modules/@jet-app/app-store/tmp/src/common/grouping/shelf-batching.js new file mode 100644 index 0000000..3746b11 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/grouping/shelf-batching.js @@ -0,0 +1,27 @@ +import { isSome } from "@jet/environment"; +import * as models from "../../api/models"; +/// The number of shelves allowed in a single shelf batch. +const shelfBatchGroupMaxCount = 5; +/** + * For a given generic page group the shelves in this page into batch groups + * allowing us to separate our calls for incomplete shelves into groups based on where + * the shelves are on the page. This will not muck with any pre-existing shelfBatchGroups + * + * @param page: The GenericPage to add shelf batches to. + */ +export function addBatchGroupsToPage(page) { + let currentShelfBatchGroup = 0; + let currentShelfBatchGroupCount = 0; + for (const shelf of page.shelves) { + if (isSome(shelf.batchGroup) && shelf.batchGroup.length > 0) { + continue; + } + shelf.batchGroup = `${models.genericShelfBatchGroupBase}${currentShelfBatchGroup}`; + currentShelfBatchGroupCount++; + if (currentShelfBatchGroupCount === shelfBatchGroupMaxCount) { + currentShelfBatchGroupCount = 0; + currentShelfBatchGroup++; + } + } +} +//# sourceMappingURL=shelf-batching.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/grouping/shelf-controllers/arcade-download-pack-shelf-controller.js b/node_modules/@jet-app/app-store/tmp/src/common/grouping/shelf-controllers/arcade-download-pack-shelf-controller.js new file mode 100644 index 0000000..0e89233 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/grouping/shelf-controllers/arcade-download-pack-shelf-controller.js @@ -0,0 +1,295 @@ +import * as models from "../../../api/models"; +import * as mediaDataFetching from "../../../foundation/media/data-fetching"; +import * as mediaNetwork from "../../../foundation/media/network"; +import * as urls from "../../../foundation/network/urls"; +import * as lockups from "../../lockups/lockups"; +import * as metricsHelpersLocation from "../../metrics/helpers/location"; +import { GroupingShelfController } from "./grouping-shelf-controller"; +import * as validation from "@jet/environment/json/validation"; +import { ResponseMetadata } from "../../../foundation/network/network"; +import * as groupingShelfControllerCommon from "./grouping-shelf-controller-common"; +import { Parameters, Path, Protocol } from "../../../foundation/network/url-constants"; +import * as content from "../../content/content"; +import { developerAttributes } from "../../../common/developer/developer-request"; +import { lockupShelfTokenFromBaseTokenAndMediaApiData } from "./grouping-lockup-shelf-controller-common"; +import { isNothing, isSome } from "@jet/environment/types/optional"; +import { arcadeOnboardingSubscriptionStatusFromString } from "../../../api/models"; +import * as metricsHelpersImpressions from "../../metrics/helpers/impressions"; +export class ArcadeDownloadPackShelfController extends GroupingShelfController { + constructor() { + super("ArcadeDownloadPackShelfController"); + // Stable identifier of the shelf to match in native. + // Used to make a shelf refresh request after user finishes onboarding. + this.shelfId = "arcadeDownloadPackShelf"; + this.supportedFeaturedContentIds = new Set([566 /* groupingTypes.FeaturedContentID.AppStore_ArcadeDownloadPackMarker */]); + } + shelfRoute(objectGraph) { + return [ + ...super.shelfRoute(objectGraph), + { + protocol: Protocol.internal, + path: `/${Path.grouping}/${Path.shelf}/{token}`, + query: [Parameters.isArcadeDownloadPackShelfPlaceholder], + }, + ]; + } + shelfTokenFromBaseTokenAndMediaApiData(objectGraph, mediaApiData, baseShelfToken, groupingParseContext) { + return lockupShelfTokenFromBaseTokenAndMediaApiData(objectGraph, mediaApiData, baseShelfToken, groupingParseContext); + } + initialShelfDataFromGroupingMediaApiData(objectGraph, mediaApiData) { + return { + shelfContents: [], + categoriesContents: [], + apps: [], + title: "", + }; + } + async secondaryShelfDataForShelfUrl(objectGraph, shelfUrl, shelfToken, parameters) { + const downloadPackData = objectGraph.arcade.getDownloadPackGames(objectGraph.bag.arcadeDownloadPackShelfTTLInSeconds); + if (isNothing(downloadPackData) || downloadPackData.apps.length === 0) { + return { + shelfContents: [], + categoriesContents: [], + apps: [], + title: "", + }; + } + const downloadPackApps = downloadPackData.apps; + const shelfTitle = this.shelfTitle(objectGraph, arcadeOnboardingSubscriptionStatusFromString(downloadPackData.subscriptionStatus)); + // Check whether it was a call from native to render a placeholder. + if (isSome(shelfUrl.query[Parameters.isArcadeDownloadPackShelfPlaceholder])) { + return { + shelfContents: [], + categoriesContents: [], + apps: downloadPackApps, + title: shelfTitle, + }; + } + // Do hydration call otherwise. + const adamIDs = downloadPackApps.map((app) => app.adamId); + const categoryIDs = downloadPackApps.map((app) => app.categoryId); + const appsType = "apps"; + const categoriesType = "editorial-items"; + const batchRequest = new mediaDataFetching.Request(objectGraph) + .addingQuery(`ids[${appsType}]`, Array.from(adamIDs).join(",")) + .addingQuery(`ids[${categoriesType}]`, Array.from(categoryIDs).join(",")) + .includingAdditionalPlatforms(mediaDataFetching.defaultAdditionalPlatformsForClient(objectGraph)) + .includingAttributes(developerAttributes(objectGraph)); + return await mediaNetwork + .fetchData(objectGraph, batchRequest) + .then(async (dataContainer) => { + const shelfData = { + shelfContents: dataContainer.data.filter((data) => { + return data.type === appsType; + }), + categoriesContents: dataContainer.data.filter((data) => { + return data.type === categoriesType; + }), + apps: downloadPackApps, + title: shelfTitle, + responseTimingValues: dataContainer[ResponseMetadata.timingValues], + }; + return shelfData; + }); + } + _createShelf(objectGraph, shelfToken, shelfData, groupingParseContext) { + if (shelfToken.isFirstRender) { + const downloadPackData = objectGraph.arcade.getDownloadPackGames(objectGraph.bag.arcadeDownloadPackShelfTTLInSeconds); + if (isNothing(downloadPackData) || downloadPackData.apps.length === 0) { + // No suggested games to display. Adding an empty hidden shelf with `refreshUrl` for future refresh from native when onboarding ends. + const shelf = this.emptyShelfWithRefreshUrl(objectGraph); + shelf.refreshUrl = this.shelfRefreshURL(shelfToken); + // Even though the shelf is hidden, need to increase position here, so when it becomes visible the index stays the same. + metricsHelpersLocation.nextPosition(groupingParseContext.metricsLocationTracker); + return shelf; + } + else { + const shelf = this.placeholderShelf(objectGraph, shelfToken, downloadPackData.apps, this.shelfTitle(objectGraph, arcadeOnboardingSubscriptionStatusFromString(downloadPackData.subscriptionStatus))); + metricsHelpersLocation.nextPosition(groupingParseContext.metricsLocationTracker); + return shelf; + } + } + else { + // If `shelfData` contains only `records` then it was a request for a placeholder shelf from native. + if (shelfData.apps.length > 0 && + (isNothing(shelfData.shelfContents) || shelfData.shelfContents.length === 0)) { + return this.placeholderShelf(objectGraph, shelfToken, shelfData.apps, shelfData.title); + } + else { + const shelfMetricsOptions = this.shelfMetrics(shelfData.title, shelfToken); + metricsHelpersLocation.pushContentLocation(objectGraph, shelfMetricsOptions, shelfData.title); + const shelf = this.downloadPackShelf(objectGraph, shelfToken, shelfData); + // `refreshUrl` allows to update the shelf content if the onboarding experience was triggered again. + shelf.refreshUrl = this.shelfRefreshURL(shelfToken); + metricsHelpersLocation.popLocation(shelfToken.metricsLocationTracker); + metricsHelpersImpressions.addImpressionFields(objectGraph, shelf, shelfMetricsOptions); + return shelf; + } + } + } + shelfMetricsOptionsFromBaseMetricsOptions(objectGraph, shelfToken, baseMetricsOptions) { + // Otherwise `baseMetricsOptions` will be used, however, at this point shelf doesn't have a title. + return null; + } + emptyShelfWithRefreshUrl(objectGraph) { + const shelf = new models.Shelf(this.useCustomDownloadPackCardShelf(objectGraph) ? "arcadeDownloadPackCard" : "smallLockup"); + shelf.id = this.shelfId; + shelf.isHidden = true; + return shelf; + } + placeholderShelf(objectGraph, shelfToken, downloadPackApps, title) { + const shelf = this.useCustomDownloadPackCardShelf(objectGraph) + ? this.downloadPackCardPlaceholderShelf(objectGraph, shelfToken, downloadPackApps.length) + : this.smallLockupsPlaceholderShelf(objectGraph, shelfToken, downloadPackApps); + shelf.url = urls.URL.from(groupingShelfControllerCommon.groupingShelfUrl(shelfToken)).build(); + shelf.title = title; + return shelf; + } + downloadPackCardPlaceholderShelf(objectGraph, shelfToken, expectedItemCount) { + const shelf = new models.Shelf("arcadeDownloadPackCard"); + shelf.id = this.shelfId; + // For `arcadeDownloadPackCard` the same `ArcadeDownloadPackCard` model is used to render lockup placeholders. + const card = new models.ArcadeDownloadPackCard(); + card.numberOfPlaceholders = expectedItemCount; + shelf.items = [card]; + // See `shelfFetchShouldMergeWhenFetched` where `shelfToken.shelfStyle` is used to override `shelf.mergeWhenFetched` flag. + shelfToken.shelfStyle = shelf.contentType; + return shelf; + } + smallLockupsPlaceholderShelf(objectGraph, shelfToken, downloadPackApps) { + const shelf = new models.Shelf("smallLockup"); + shelf.id = this.shelfId; + shelf.items = []; + shelf.isHorizontal = true; + shelf.rowsPerColumn = 2; + // Use standard small lockup placeholders for iPad shelf. + shelf.items = Array(downloadPackApps.length).fill(new models.Placeholder()); + shelf.placeholderContentType = shelf.contentType; + shelf.contentType = "placeholder"; + shelfToken.showingPlaceholders = true; + shelfToken.remainingItems = downloadPackApps.map((value) => { + return { + id: value.adamId, + type: "apps", + }; + }); + return shelf; + } + downloadPackShelf(objectGraph, shelfToken, shelfData) { + const categories = this.categoriesMapFromResponse(objectGraph, shelfData.categoriesContents, shelfData.apps); + const useCustomShelf = this.useCustomDownloadPackCardShelf(objectGraph); + const contentType = useCustomShelf ? "arcadeDownloadPackCard" : "smallLockup"; + const apps = this.lockupsFromResponse(objectGraph, shelfToken, categories, useCustomShelf, // In the custom `arcadeDownloadPack` shelf app lockups the game category goes into lockup header, otherwise, in a standard shelf, it goes into subtitle. + useCustomShelf, // In the custom `arcadeDownloadPack` shelf app lockups have dark environment. + content.artworkUseCaseFromShelfStyle(objectGraph, contentType), isSome(shelfData.shelfContents) ? shelfData.shelfContents : [], shelfData.apps); + const shelf = new models.Shelf(contentType); + shelf.id = this.shelfId; + shelf.title = shelfData.title; + if (useCustomShelf) { + const card = new models.ArcadeDownloadPackCard(); + card.lockups = apps; + shelf.items = [card]; + } + else { + shelf.items = apps; + shelf.isHorizontal = true; + shelf.rowsPerColumn = 2; + } + shelf.isHidden = apps.length === 0; + return shelf; + } + // On iPhone there is a bespoke 'arcadeDownloadPackCard' games card shelf. + // On iPad, always use `smallLockup` instead of `shelfToken.shelfStyle` from server + // to make editorial configuration work easier. + useCustomDownloadPackCardShelf(objectGraph) { + return objectGraph.client.isPhone; + } + shelfRefreshURL(shelfToken) { + return urls.URL.from(groupingShelfControllerCommon.groupingShelfUrl(shelfToken)) + .param(Parameters.isArcadeDownloadPackShelfPlaceholder, "true") + .build(); + } + lockupsFromResponse(objectGraph, shelfToken, categoryTitlesForAdamIDs, showCategoryHeader, darkEnvironment, artworkUseCase, responseData, records) { + return validation.context("lockupsFromResponse", () => { + const lockupDataMap = new Map(); + for (const lockupData of responseData) { + lockupDataMap.set(lockupData.id, lockupData); + } + const locationTracker = shelfToken.metricsLocationTracker; + const pageInformation = shelfToken.metricsPageInformation; + // Order of the apps from the local storage (records) must be preserved, + // so user always sees the same list of the apps like it was on the onboarding game suggestions screen. + const items = []; + for (const app of records) { + const lockupData = lockupDataMap.get(app.adamId); + if (isNothing(lockupData)) { + continue; + } + const lockup = lockups.lockupFromData(objectGraph, lockupData, { + offerStyle: darkEnvironment ? "transparent" : undefined, + offerEnvironment: darkEnvironment ? "dark" : undefined, + metricsOptions: { + pageInformation: pageInformation, + locationTracker: locationTracker, + badges: { + categoryId: app.categoryId, + }, + }, + metricsClickOptions: { + id: lockupData.id, + pageInformation: pageInformation, + locationTracker: locationTracker, + badges: { + categoryId: app.categoryId, + }, + }, + artworkUseCase: artworkUseCase, + shouldHideArcadeHeader: true, + isSubtitleHidden: showCategoryHeader, + }); + if (isNothing(lockup)) { + continue; + } + lockups.cleanupArcadeDownloadPackLockupMetricsIfNeeded(lockup, objectGraph); + metricsHelpersLocation.nextPosition(locationTracker); + if (showCategoryHeader) { + lockup.heading = categoryTitlesForAdamIDs.get(lockup.adamId); + } + else { + lockup.subtitle = categoryTitlesForAdamIDs.get(lockup.adamId); + } + items.push(lockup); + } + return items; + }); + } + categoriesMapFromResponse(objectGraph, responseData, records) { + const result = new Map(); + for (const app of records) { + const category = responseData.find((data) => data.id === app.categoryId); + if (!category) { + continue; + } + const editorialNotesName = content.editorialNotesFromData(objectGraph, category, "name"); + result.set(app.adamId, editorialNotesName); + } + return result; + } + shelfTitle(objectGraph, subscriptionStatus) { + return objectGraph.loc.string(subscriptionStatus === "new" ? "Arcade.DownloadPack.ShelfTitle.NewUser" : "Arcade.DownloadPack.ShelfTitle"); + } + shelfMetrics(title, shelfToken) { + return { + id: shelfToken.id, + kind: null, + softwareType: "Arcade", + targetType: "swoosh", + title: title, + pageInformation: shelfToken.metricsPageInformation, + locationTracker: shelfToken.metricsLocationTracker, + idType: "its_contentId", + fcKind: shelfToken.featuredContentId, + }; + } +} +//# sourceMappingURL=arcade-download-pack-shelf-controller.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/grouping/shelf-controllers/grouping-app-event-shelf-controller.js b/node_modules/@jet-app/app-store/tmp/src/common/grouping/shelf-controllers/grouping-app-event-shelf-controller.js new file mode 100644 index 0000000..6acbb2a --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/grouping/shelf-controllers/grouping-app-event-shelf-controller.js @@ -0,0 +1,260 @@ +import { isSome } from "@jet/environment"; +import * as models from "../../../api/models"; +import * as serverData from "../../../foundation/json-parsing/server-data"; +import * as mediaDataFetching from "../../../foundation/media/data-fetching"; +import * as mediaDataStructure from "../../../foundation/media/data-structure"; +import { fetchData } from "../../../foundation/media/network"; +import * as mediaRelationship from "../../../foundation/media/relationships"; +import * as appPromotionCommon from "../../app-promotions/app-promotions-common"; +import * as metricsHelpersUtil from "../../metrics/helpers/util"; +import * as onDevicePersonalization from "../../personalization/on-device-personalization"; +import * as placeholders from "../../placeholders/placeholders"; +import { addVariantParametersToRequestForItems } from "../../product-page/product-page-variants"; +import * as refresh from "../../refresh/page-refresh-controller"; +import { GroupingShelfController } from "./grouping-shelf-controller"; +import * as groupingShelfControllerCommon from "./grouping-shelf-controller-common"; +import { attributeAsBooleanOrFalse } from "../../../foundation/media/attributes"; +import { ResponseMetadata } from "../../../foundation/network/network"; +import { shelfTitleAttributePathForFeaturedContentId } from "./grouping-shelf-controller-common"; +import * as mediaAttributes from "../../../foundation/media/attributes"; +export class GroupingAppEventShelfController extends GroupingShelfController { + // region Constructors + constructor() { + super("GroupingAppEventShelfController"); + this.supportedFeaturedContentIds = new Set([ + 519 /* groupingTypes.FeaturedContentID.AppStore_AppEventsMarker */, + 518 /* groupingTypes.FeaturedContentID.AppStore_PersonalizedAppEventsMarker */, + ]); + } + // endregion + // region Shelf Creation Prerequisites + /** + * For a given mediaApiData extract the actual shelfContents array needed to render this shelf + * + * @param mediaApiData The outer shelfContents object containing the shelf contents + */ + initialShelfDataFromGroupingMediaApiData(objectGraph, mediaApiData) { + return { shelfContents: mediaRelationship.relationshipCollection(mediaApiData, "contents") }; + } + /** + * For a given url that this controller handles, we should return a promise that will result in the `ShelfData` + * needed to render this shelf + * + * @param objectGraph The App Store dependency graph + * @param shelfUrl The url that this controller handled on a secondary fetch + * @param parameters The extracted parameters from the shelf url + */ + async secondaryShelfDataForShelfUrl(objectGraph, shelfUrl, shelfToken, parameters) { + var _a; + if (((_a = shelfToken.recommendationsHref) === null || _a === void 0 ? void 0 : _a.length) > 0) { + try { + const mediaApiData = await GroupingShelfController.secondaryGroupingShelfMediaApiData(objectGraph, shelfUrl, shelfToken, parameters); + const shelfResponseData = mediaDataStructure.dataFromDataContainer(objectGraph, mediaApiData); + const shelfContents = this.initialShelfDataFromGroupingMediaApiData(objectGraph, shelfResponseData); + shelfContents.responseTimingValues = mediaApiData[ResponseMetadata.timingValues]; + const title = mediaAttributes.attributeAsString(shelfResponseData, shelfTitleAttributePathForFeaturedContentId(objectGraph, shelfToken.featuredContentId)); + if (serverData.isDefinedNonNull(title) && (title === null || title === void 0 ? void 0 : title.length) > 0) { + shelfContents.shelfTitle = title; + } + return shelfContents; + } + catch { + return { shelfContents: [] }; + } + } + else { + const appEvents = []; + const contingentItems = []; + const offerItems = []; + for (const remainingItem of shelfToken.remainingItems) { + switch (remainingItem.type) { + case "contingent-items": + contingentItems.push(remainingItem); + break; + case "offer-items": + offerItems.push(remainingItem); + break; + case "app-events": + appEvents.push(remainingItem); + break; + default: + break; + } + } + const appEventsRequest = new mediaDataFetching.Request(objectGraph, appEvents); + addVariantParametersToRequestForItems(objectGraph, appEventsRequest, appEvents); + const contingentItemsRequest = new mediaDataFetching.Request(objectGraph, contingentItems); + addVariantParametersToRequestForItems(objectGraph, contingentItemsRequest, contingentItems); + const offerItemsRequest = new mediaDataFetching.Request(objectGraph, offerItems); + addVariantParametersToRequestForItems(objectGraph, offerItemsRequest, offerItems); + const hydrationResults = await Promise.all([ + this.fetchRemainingItems(objectGraph, appEventsRequest), + this.fetchRemainingItems(objectGraph, contingentItemsRequest), + this.fetchRemainingItems(objectGraph, offerItemsRequest), + ]); + const hydratedDataMap = { ...hydrationResults[0], ...hydrationResults[1], ...hydrationResults[2] }; + const shelfContents = []; + for (const remainingItem of shelfToken.remainingItems) { + const data = hydratedDataMap[remainingItem.id]; + if (isSome(data)) { + shelfContents.push(data); + } + } + groupingShelfControllerCommon.flushRequestedItemsFromShelfToken(shelfToken, new Set([...offerItemsRequest.ids, ...contingentItemsRequest.ids, ...appEventsRequest.ids])); + return { shelfContents: shelfContents }; + } + } + async fetchRemainingItems(objectGraph, remainingItemsRequest) { + const apiDataMap = {}; + const addDataToMap = (data) => { + for (const item of data.data) { + apiDataMap[item.id] = item; + } + }; + if (remainingItemsRequest.ids.size > 0) { + groupingShelfControllerCommon.prepareGroupingShelfRequest(objectGraph, remainingItemsRequest); + try { + await fetchData(objectGraph, remainingItemsRequest).then((mediaApiData) => { + addDataToMap(mediaApiData); + }); + } + catch (fetchError) { + objectGraph.console.error("Error fetching remaining items", remainingItemsRequest.ids); + } + } + return apiDataMap; + } + /** + * For a given mediaApiData create an updated shelf token that contains all the additional data for this specific shelf type + * + * @param objectGraph The App Store dependency graph + * @param baseShelfToken The base grouping shelf token created by the grouping-controller + * @param mediaApiData The outer data object containing the FC properties and data + * @param groupingParseContext The parse context for the grouping page so far + */ + shelfTokenFromBaseTokenAndMediaApiData(objectGraph, mediaApiData, baseShelfToken, groupingParseContext) { + const shouldPersonalizeData = baseShelfToken.featuredContentId === 518 /* groupingTypes.FeaturedContentID.AppStore_PersonalizedAppEventsMarker */; + let personalizedDataResult = null; + const shelfData = this.initialShelfDataFromGroupingMediaApiData(objectGraph, mediaApiData); + if (shouldPersonalizeData && serverData.isDefinedNonNullNonEmpty(shelfData.shelfContents)) { + personalizedDataResult = this.personalizedDataResultFromDataItems(objectGraph, shelfData.shelfContents); + } + const appEventShelfToken = { + ...baseShelfToken, + shouldPersonalizeData: shouldPersonalizeData, + personalizedDataResult: personalizedDataResult, + }; + const hasContents = serverData.isDefinedNonNullNonEmpty(shelfData.shelfContents); + const shelfPersonalizationAvailable = !attributeAsBooleanOrFalse(mediaApiData, "noPersonalizationAvailable"); + if (!hasContents && shelfPersonalizationAvailable) { + appEventShelfToken.recommendationsHref = mediaApiData.href; + appEventShelfToken.isValidRecommendationsShelf = true; + } + else { + appEventShelfToken.isValidRecommendationsShelf = hasContents; + } + return appEventShelfToken; + } + // endregion + // region Metrics + /** + * Return the shelf metrics options to use for this specific shelf. Using the base options from the grouping + * page controller + * + * @param objectGraph The App Store dependency graph + * @param shelfToken The shelf shelfToken for this current shelf creation request + * @param baseMetricsOptions The minimum set of metrics options for this shelf, created by the + * grouping page controller + */ + shelfMetricsOptionsFromBaseMetricsOptions(objectGraph, shelfToken, baseMetricsOptions) { + const shelfMetricsOptions = { ...baseMetricsOptions }; + if (serverData.isDefinedNonNullNonEmpty(shelfToken.personalizedDataResult)) { + const recoMetricsData = metricsHelpersUtil.combinedRecoMetricsDataFromMetricsData(baseMetricsOptions.recoMetricsData, shelfToken.personalizedDataResult.processingType, null); + shelfMetricsOptions.recoMetricsData = recoMetricsData; + } + return shelfMetricsOptions; + } + // endregion + // region Shelf Creation + /** + * + * @param objectGraph The App Store dependency graph + * @param shelfToken The shelf shelfToken for this current shelf creation request + * @param shelfData The media api shelfContents array for this shelf + * @param groupingParseContext The parse context used to generate the grouping page on the initial page load, + * this will be missing when this controller renders a secondary or incomplete shelf fetch. + */ + _createShelf(objectGraph, shelfToken, shelfData, groupingParseContext) { + var _a; + if (!appPromotionCommon.appEventsAreEnabled(objectGraph)) { + return null; + } + if (!shelfToken.isValidRecommendationsShelf) { + return null; + } + const metricsOptions = { + pageInformation: shelfToken.metricsPageInformation, + locationTracker: shelfToken.metricsLocationTracker, + recoMetricsData: shelfToken.metricsPageInformation.recoMetricsData, + }; + // First personalize the data, as this may cause things to be re-ordered + let sortedShelfContents = shelfData.shelfContents; + // Only try to use the personalized data the first render, second renders the shelfContents has already been personalized + if (serverData.isDefinedNonNullNonEmpty(shelfToken.personalizedDataResult) && shelfToken.isFirstRender) { + sortedShelfContents = shelfToken.personalizedDataResult.personalizedData; + } + // Now find which items are hydrated, and add the rest to the token + const hydratedShelfContents = []; + for (const data of sortedShelfContents) { + if (serverData.isNull(data.attributes) || groupingShelfControllerCommon.shouldDefer(shelfToken)) { + shelfToken.isDeferring = true; + shelfToken.remainingItems.push(data); + continue; + } + hydratedShelfContents.push(data); + } + // Now create our shelf from the hydrated items + const displayableAppEvents = appPromotionCommon.appPromotionsFromData(objectGraph, hydratedShelfContents, null, false, false, metricsOptions, false, true, shelfToken.isArcadePage, false); + refresh.addNextPreferredContentRefreshDate(displayableAppEvents.nextAppEventPromotionStartDate, groupingParseContext === null || groupingParseContext === void 0 ? void 0 : groupingParseContext.refreshController); + const appPromotions = displayableAppEvents.appPromotions; + const shelfType = "appPromotion"; + const shelf = new models.Shelf(shelfType); + shelf.isHorizontal = true; + shelf.title = (_a = shelfData.shelfTitle) !== null && _a !== void 0 ? _a : shelfToken.title; + shelf.items = appPromotions; + const willHydrateShelfLater = serverData.isNullOrEmpty(shelf.items) && shelfToken.isFirstRender; + if (willHydrateShelfLater && placeholders.placeholdersEnabled(objectGraph)) { + placeholders.insertPlaceholdersIntoGenericPageShelf(objectGraph, shelf, shelfToken, shelfToken.featuredContentId); + } + shelf.url = groupingShelfControllerCommon.createShelfTokenUrlIfNecessaryForShelf(objectGraph, shelf, shelfToken); + if (serverData.isNullOrEmpty(shelf.items) && serverData.isNullOrEmpty(shelf.url)) { + return shelfToken.isFirstRender ? null : GroupingAppEventShelfController.makeHiddenShelf(shelfToken); + } + return shelf; + } + // region Helpers + /** + * Personalizes the input list of app event data items. + * @param objectGraph + * @param dataItems The array of app event data items + */ + personalizedDataResultFromDataItems(objectGraph, dataItems) { + // Collect the app IDs we are interested in + const appIds = new Set(); + for (const data of dataItems) { + const appId = serverData.asString(data, "meta.personalizationData.appId"); + if ((appId === null || appId === void 0 ? void 0 : appId.length) > 0) { + appIds.add(appId); + } + } + // Now personalize the data + const personalizationDataContainer = onDevicePersonalization.personalizationDataContainerForAppIds(objectGraph, appIds); + return onDevicePersonalization.personalizeDataItems(objectGraph, "groupingAppEvent", dataItems, false, personalizationDataContainer, null, null, null, true); + } + static makeHiddenShelf(shelfToken) { + const hiddenShelf = new models.Shelf(shelfToken.shelfStyle); + hiddenShelf.isHidden = true; + return hiddenShelf; + } +} +//# sourceMappingURL=grouping-app-event-shelf-controller.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/grouping/shelf-controllers/grouping-arcade-footer-shelf-controller.js b/node_modules/@jet-app/app-store/tmp/src/common/grouping/shelf-controllers/grouping-arcade-footer-shelf-controller.js new file mode 100644 index 0000000..d9f324d --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/grouping/shelf-controllers/grouping-arcade-footer-shelf-controller.js @@ -0,0 +1,160 @@ +import * as models from "../../../api/models"; +import * as serverData from "../../../foundation/json-parsing/server-data"; +import * as mediaDataStructure from "../../../foundation/media/data-structure"; +import * as mediaNetwork from "../../../foundation/media/network"; +import { ResponseMetadata } from "../../../foundation/network/network"; +import * as color from "../../../foundation/util/color-util"; +import * as arcadeCommon from "../../arcade/arcade-common"; +import * as content from "../../content/content"; +import * as metricsHelpersImpressions from "../../metrics/helpers/impressions"; +import * as metricsHelpersLocation from "../../metrics/helpers/location"; +import { GroupingShelfController } from "./grouping-shelf-controller"; +import * as groupingShelfControllerCommon from "./grouping-shelf-controller-common"; +export class GroupingArcadeFooterShelfController extends GroupingShelfController { + // region Constructors + constructor() { + super("GroupingArcadeFooterShelfController"); + this.supportedFeaturedContentIds = new Set([-1 /* groupingTypes.FeaturedContentID.Native_GroupingShelf */]); + this.supportedNativeGroupingShelfIds = new Set([1 /* groupingTypes.NativeGroupingShelfID.Arcade_SeeAllGamesFooter */]); + } + // endregion + // region Metrics + shouldImpressShelf() { + return false; + } + // endregion + // region Shelf Creation Prerequisites + /** + * For a given mediaApiData extract the actual shelfContents array needed to render this shelf + * + * @param mediaApiData The outer shelfContents object containing the shelf contents + */ + initialShelfDataFromGroupingMediaApiData(objectGraph, mediaApiData) { + return { + shelfContents: mediaDataStructure.dataCollectionFromResultsListContainer(mediaApiData), + responseTimingValues: null, + }; + } + /** + * For a given url that this controller handles, we should return a promise that will result in the `ShelfData` + * needed to render this shelf + * + * @param objectGraph The App Store dependency graph + * @param shelfUrl The url that this controller handled on a secondary fetch + * @param parameters The extracted parameters from the shelf url + */ + async secondaryShelfDataForShelfUrl(objectGraph, shelfUrl, shelfToken, parameters) { + const footerRequest = arcadeCommon.arcadeAppsRequestForIcons(objectGraph, this.numberOfIconsForArcadeAppGrid(objectGraph.client.deviceType)); + return await mediaNetwork + .fetchData(objectGraph, footerRequest) + .then((mediaApiData) => { + const shelfData = this.initialShelfDataFromGroupingMediaApiData(objectGraph, mediaApiData); + shelfData.responseTimingValues = mediaApiData[ResponseMetadata.timingValues]; + return shelfData; + }); + } + /** + * For a given mediaApiData create an updated shelf token that contains all the additional data for this specific shelf type + * + * @param objectGraph The App Store dependency graph + * @param baseShelfToken The base grouping shelf token created by the grouping-controller + * @param mediaApiData The outer data object containing the FC properties and data + * @param groupingParseContext The parse context for the grouping page so far + */ + shelfTokenFromBaseTokenAndMediaApiData(objectGraph, mediaApiData, baseShelfToken, groupingParseContext) { + const footerShelfToken = { + ...baseShelfToken, + shouldIncludeShelfUrl: baseShelfToken.isFirstRender, + }; + footerShelfToken.showingPlaceholders = baseShelfToken.isFirstRender; + return footerShelfToken; + } + // endregion + // region Shelf Creation + /** + * + * @param objectGraph The App Store dependency graph + * @param shelfToken The shelf shelfToken for this current shelf creation request + * @param shelfData The media api shelfContents array for this shelf + * @param groupingParseContext The parse context used to generate the grouping page on the initial page load, + * this will be missing when this controller renders a secondary or incomplete shelf fetch. + */ + _createShelf(objectGraph, shelfToken, shelfData, groupingParseContext) { + const footer = new models.ArcadeFooter(); + const shelf = new models.Shelf("arcadeFooter"); + shelf.items = [footer]; + const impressionOptions = { + targetType: "arcadeSeeAllGamesFooter", + pageInformation: shelfToken.metricsPageInformation, + locationTracker: shelfToken.metricsLocationTracker, + title: objectGraph.loc.string("Arcade.SeeAllGames.Button.Title"), + id: shelfToken.id, + kind: "footer", + softwareType: "Arcade", + }; + metricsHelpersImpressions.addImpressionFields(objectGraph, footer, impressionOptions); + metricsHelpersLocation.pushContentLocation(objectGraph, impressionOptions, impressionOptions.title); + footer.buttonAction = arcadeCommon.seeAllArcadeGamesPageFlowAction(objectGraph, "releaseDate", shelfToken.metricsPageInformation, shelfToken.metricsLocationTracker); + const buttonImpressionOptions = { + targetType: "button", + pageInformation: shelfToken.metricsPageInformation, + locationTracker: shelfToken.metricsLocationTracker, + title: footer.buttonAction.title, + id: "arcade-see-all-games-button", + kind: "button", + softwareType: "Arcade", + }; + metricsHelpersImpressions.addImpressionFields(objectGraph, footer.buttonAction, buttonImpressionOptions); + metricsHelpersLocation.popLocation(impressionOptions.locationTracker); + const termsAndConditionsUrl = objectGraph.bag.termsAndConditionsURL; + const shouldAddTermsAndConditions = !serverData.isNull(termsAndConditionsUrl) && objectGraph.client.deviceType !== "tv"; + if (objectGraph.client.isiOS) { + if (shouldAddTermsAndConditions) { + const termsAndConditionTitle = objectGraph.loc.string("TermsAndConditions.Title"); + const urlAction = new models.ExternalUrlAction(termsAndConditionsUrl); + const footnote = new models.Footnote(termsAndConditionTitle); + footnote.clickAction = urlAction; + footnote.presentationStyle = ["hasChevron", "textLightensOnHighlight", "hasSeparator"]; + footer.footnote = footnote; + } + shelf.background = { + type: "color", + color: color.named("placeholderBackground"), + }; + } + if (serverData.isDefinedNonNullNonEmpty(shelfData.shelfContents)) { + const metricsOptions = { + pageInformation: shelfToken.metricsPageInformation, + locationTracker: shelfToken.metricsLocationTracker, + }; + footer.icons = content.impressionableAppIconsFromDataCollection(objectGraph, shelfData.shelfContents, metricsOptions, { + useCase: 2 /* content.ArtworkUseCase.LockupIconMedium */, + }); + } + else { + footer.icons = []; + } + if (shelfToken.shouldIncludeShelfUrl) { + shelf.url = groupingShelfControllerCommon.groupingShelfUrl(shelfToken); + } + return shelf; + } + // region Helpers + /** + * The maximum number of icons that can be displayed in a device's icon grid, i.e. Arcade Grid Footer. + * This is used to limit the number of icon data to fetch to what we'll actually use. + * Note that views are expected to handle situations where we have less than the max number of icons. + * + * @param deviceType The device type that arcade icon grid is being displayed in. + * @returns The maximum number of icons to fetch. + */ + numberOfIconsForArcadeAppGrid(deviceType) { + switch (deviceType) { + case "phone": + return 9; + default: // iPad, provided `footerShowsIconsForPlatform == true`. + return 20; + } + } +} +//# sourceMappingURL=grouping-arcade-footer-shelf-controller.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/grouping/shelf-controllers/grouping-brick-shelf-controller.js b/node_modules/@jet-app/app-store/tmp/src/common/grouping/shelf-controllers/grouping-brick-shelf-controller.js new file mode 100644 index 0000000..671aac3 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/grouping/shelf-controllers/grouping-brick-shelf-controller.js @@ -0,0 +1,313 @@ +import * as models from "../../../api/models"; +import * as serverData from "../../../foundation/json-parsing/server-data"; +import * as mediaAttributes from "../../../foundation/media/attributes"; +import * as mediaDataStructure from "../../../foundation/media/data-structure"; +import * as mediaRelationship from "../../../foundation/media/relationships"; +import * as content from "../../content/content"; +import * as flowPreview from "../../content/flow-preview"; +import * as lockupsEditorialContext from "../../lockups/editorial-context"; +import * as metricsHelpersClicks from "../../metrics/helpers/clicks"; +import * as metricsHelpersImpressions from "../../metrics/helpers/impressions"; +import * as metricsHelpersLocation from "../../metrics/helpers/location"; +import * as placeholders from "../../placeholders/placeholders"; +import { GroupingShelfController } from "./grouping-shelf-controller"; +import * as groupingShelfControllerCommon from "./grouping-shelf-controller-common"; +import * as metricsLocation from "../../../common/metrics/helpers/location"; +import { defaultLayoutSize } from "../../../foundation/media/data-fetching"; +import * as color from "../../../foundation/util/color-util"; +import { isSome } from "@jet/environment"; +export class GroupingBrickShelfController extends GroupingShelfController { + // region Constructors + constructor() { + super("GroupingBrickShelfController"); + this.supportedFeaturedContentIds = new Set([ + 421 /* groupingTypes.FeaturedContentID.AppStore_BrickRow */, + 422 /* groupingTypes.FeaturedContentID.AppStore_Brick */, + 423 /* groupingTypes.FeaturedContentID.AppStore_CustomBrick */, + 261 /* groupingTypes.FeaturedContentID.Sundance_BrickRow */, + ]); + } + // endregion + // region Shelf Creation Prerequisites + /** + * For a given mediaApiData extract the actual shelfContents array needed to render this shelf + * + * @param mediaApiData The outer shelfContents object containing the shelf contents + */ + initialShelfDataFromGroupingMediaApiData(objectGraph, mediaApiData) { + return { shelfContents: mediaRelationship.relationshipCollection(mediaApiData, "children") }; + } + /** + * For a given url that this controller handles, we should return a promise that will result in the `ShelfData` + * needed to render this shelf + * + * @param objectGraph The App Store dependency graph + * @param shelfUrl The url that this controller handled on a secondary fetch + * @param parameters The extracted parameters from the shelf url + */ + async secondaryShelfDataForShelfUrl(objectGraph, shelfUrl, shelfToken, parameters) { + return await GroupingShelfController.secondaryGroupingShelfDataForShelfUrl(objectGraph, shelfUrl, shelfToken, parameters).then((shelfData) => { + return { + shelfContents: groupingShelfControllerCommon.mergeContentDataIntoEditorialData(shelfData.shelfContents, mediaRelationship.relationshipCollection(shelfToken.featuredContentData, "children")), + }; + }); + } + /** + * For a given mediaApiData create an updated shelf token that contains all the additional data for this specific shelf type + * + * @param objectGraph The App Store dependency graph + * @param baseShelfToken The base grouping shelf token created by the grouping-controller + * @param mediaApiData The outer data object containing the FC properties and data + * @param groupingParseContext The parse context for the grouping page so far + */ + shelfTokenFromBaseTokenAndMediaApiData(objectGraph, mediaApiData, baseShelfToken, groupingParseContext) { + // If suppress text is not provided, default to hiding. + let suppressText = mediaAttributes.attributeAsBoolean(mediaApiData, "suppressText"); + if (serverData.isNull(suppressText)) { + suppressText = true; + } + const brickShelfToken = { + ...baseShelfToken, + showSupplementaryText: !suppressText, + }; + brickShelfToken.clientIdentifierOverride = lockupsEditorialContext.clientIdentifierForEditorialContextInData(objectGraph, mediaApiData); + return brickShelfToken; + } + // endregion + // region Shelf Creation + /** + * + * @param objectGraph The App Store dependency graph + * @param shelfToken The shelf shelfToken for this current shelf creation request + * @param shelfData The media api shelfContents array for this shelf + * @param groupingParseContext The parse context used to generate the grouping page on the initial page load, + * this will be missing when this controller renders a secondary or incomplete shelf fetch. + */ + _createShelf(objectGraph, shelfToken, shelfData, groupingParseContext) { + const items = []; + const remainingBricks = []; // Data where metadata was missing. + const displayStyle = serverData.asString(shelfToken.featuredContentData.attributes, "displayStyle"); + const isCategoryBrick = displayStyle === "small"; + let shelf; + if (isCategoryBrick) { + // i.e. category bricks shelf + shelf = new models.Shelf("categoryBrick"); + const layoutSize = serverData.asNumber(shelfToken.featuredContentData.attributes, "layoutStyle.layoutSize"); + shelf.rowsPerColumn = layoutSize !== null && layoutSize !== void 0 ? layoutSize : defaultLayoutSize(objectGraph); + metricsLocation.currentLocation(shelfToken.metricsLocationTracker).name = "Browse Categories"; + } + else { + // i.e. "medium", also the default bricks shelf + shelf = new models.Shelf("brick"); + } + shelf.isHorizontal = true; + for (const brickData of shelfData.shelfContents) { + const brickModel = GroupingBrickShelfController.createBrick(objectGraph, brickData, isCategoryBrick, shelfToken.metricsPageInformation, shelfToken.metricsLocationTracker, shelfToken, groupingParseContext); + if (!brickModel) { + remainingBricks.push(brickData); + continue; + } + items.push(brickModel); + metricsHelpersLocation.nextPosition(shelfToken.metricsLocationTracker); + } + if (serverData.isDefinedNonNull(shelfToken.presentationHints)) { + shelf.presentationHints = shelfToken.presentationHints; + } + if (serverData.isDefinedNonNull(shelfToken.showSupplementaryText)) { + shelf.presentationHints = { + ...shelf.presentationHints, + showSupplementaryText: shelfToken.showSupplementaryText, + }; + } + // We don't need this in our incomplete shelf URL, so we'll preemptively remove it. + delete shelfToken.maxItemCount; + // Override `featuredContentData` to only have remaining bricks that must be fetched. + if (serverData.isDefinedNonNull(serverData.traverse(shelfToken.featuredContentData, "relationships.children.data"))) { + shelfToken.featuredContentData.relationships["children"].data = remainingBricks; + } + // Set metadata + shelf.title = shelfToken.title; + shelf.subtitle = shelfToken.subtitle; + if (isCategoryBrick) { + const displayCount = serverData.asNumber(shelfToken.featuredContentData.attributes, "displayCount"); + shelf.items = items.slice(0, displayCount !== null && displayCount !== void 0 ? displayCount : items.length); + } + else { + shelf.items = items; + } + // See all + const hasSeeAll = serverData.asBooleanOrFalse(shelfToken.featuredContentData.attributes, "hasSeeAll"); + if (isCategoryBrick && hasSeeAll && !objectGraph.client.isWeb) { + // Setup shelf + const seeAllShelf = new models.Shelf("categoryBrick"); + seeAllShelf.items = this.sortCategories(objectGraph, items); + seeAllShelf.presentationHints = { isSeeAllContext: true }; + // Setup Page + const seeAllPage = new models.GenericPage([seeAllShelf]); + seeAllPage.title = shelfToken.title; + // Setup action + const seeAllAction = new models.FlowAction("page"); + seeAllAction.title = objectGraph.loc.string("ACTION_SEE_ALL"); + seeAllAction.pageData = seeAllPage; + // Connect action + shelf.seeAllAction = seeAllAction; + // Metrics + metricsHelpersClicks.addClickEventToSeeAllAction(objectGraph, seeAllAction, null, { + pageInformation: shelfToken.metricsPageInformation, + locationTracker: shelfToken.metricsLocationTracker, + }); + } + // If no items should we display placeholders for this shelf? + const willHydrateShelfLater = shelf && serverData.isNullOrEmpty(shelf.items) && shelfToken.isFirstRender; + if (willHydrateShelfLater && placeholders.placeholdersEnabled(objectGraph)) { + placeholders.insertPlaceholdersIntoGenericPageShelf(objectGraph, shelf, shelfToken, shelfToken.featuredContentId); + } + if (!willHydrateShelfLater && + GroupingBrickShelfController.shouldDisplayChooseYourFavoritesBrick(objectGraph, shelfToken, isCategoryBrick, groupingParseContext)) { + const brickModel = GroupingBrickShelfController.createChooseYourFavoritesBrick(objectGraph, shelfToken.metricsPageInformation, shelfToken.metricsLocationTracker); + shelf.items.splice(0, 0, brickModel); + } + shelfToken.presentationHints = shelf.presentationHints; + shelf.url = groupingShelfControllerCommon.createShelfTokenUrlIfNecessaryForShelf(objectGraph, shelf, shelfToken); + return shelf; + } + // region Static Helpers + // Whether to display Choose Your Favorites brick. + // It will return TRUE when: + // - This is an Arcade page + // - This is a Category brick + // - Feature flag is enabled + // - The user is subscribed or trial enrolled + static shouldDisplayChooseYourFavoritesBrick(objectGraph, shelfToken, isCategoryBrick, groupingParseContext) { + return (shelfToken.isArcadePage && + isCategoryBrick && + objectGraph.featureFlags.isEnabled("arcade_choose_your_favorites_brick_Future") && + isSome(groupingParseContext === null || groupingParseContext === void 0 ? void 0 : groupingParseContext.additionalShelfParameters) && + ((groupingParseContext === null || groupingParseContext === void 0 ? void 0 : groupingParseContext.additionalShelfParameters.isSubscribed) === "true" || + (groupingParseContext === null || groupingParseContext === void 0 ? void 0 : groupingParseContext.additionalShelfParameters.isTrialEnrolled) === "true")); + } + static createBrick(objectGraph, brickData, searchCategoryBricks, metricsPageInformation, metricsLocationTracker, shelfToken, groupingParseContext) { + const metricsOptions = { + targetType: searchCategoryBricks ? "tile" : "brick", + pageInformation: metricsPageInformation, + locationTracker: metricsLocationTracker, + recoMetricsData: mediaDataStructure.metricsFromMediaApiObject(brickData), + }; + const metadata = groupingShelfControllerCommon.metadataForFCData(objectGraph, brickData, shelfToken, false, null, metricsOptions, groupingParseContext); + if (!metadata) { + return null; + } + const brickModel = new models.Brick(); + // Setup Artwork + const artworkOptions = { + useCase: 18 /* content.ArtworkUseCase.GroupingBrick */, + }; + if (searchCategoryBricks) { + const artworks = content.searchChartOrCategoryArtworkFromData(objectGraph, metadata.content, content.SearchChartOrCategoryBrickUseCase.categoryBreakout, models.GenericSearchPageShelfDisplayStyleDensity.Density1); + brickModel.artworks = artworks; + } + else if (metadata.artwork && + (shelfToken === null || shelfToken === void 0 ? void 0 : shelfToken.featuredContentId) !== 261 /* groupingTypes.FeaturedContentID.Sundance_BrickRow */) { + let artworkDict = serverData.asDictionary(metadata.artwork, "subscriptionHero"); + if (!artworkDict) { + artworkDict = serverData.asDictionary(metadata.artwork, "originalFlowcaseBrick"); + } + const artwork = groupingShelfControllerCommon.groupingArtworkFromApiArtwork(objectGraph, artworkDict, artworkOptions); + brickModel.artworks = [artwork]; + } + else { + const artwork = groupingShelfControllerCommon.artworkFromFC(objectGraph, brickData, 1060, 520, artworkOptions); + brickModel.artworks = [artwork]; + } + brickModel.accessibilityLabel = metadata.title; + // Set supplementary text. + brickModel.shortEditorialDescription = metadata.title; + // Set action + brickModel.clickAction = metadata.action; + // Set personalization + const brickFeaturedContentId = mediaAttributes.attributeAsNumber(brickData, "editorialElementKind"); + if (brickFeaturedContentId === 435 /* groupingTypes.FeaturedContentID.AppStore_MSOBrickMarker */) { + brickModel.personalizationStyle = "mso"; + } + // Set flow preview actions + const contentData = mediaRelationship.relationshipData(objectGraph, brickData, "contents"); + if (serverData.isDefinedNonNull(contentData)) { + const metricsClickOptions = metricsHelpersClicks.clickOptionsForLockup(objectGraph, contentData, metricsOptions); + brickModel.flowPreviewActionsConfiguration = flowPreview.flowPreviewActionsConfigurationForProductFromData(objectGraph, brickData, true, shelfToken === null || shelfToken === void 0 ? void 0 : shelfToken.clientIdentifierOverride, brickModel.clickAction, metricsOptions, metricsClickOptions); + } + // Configure impressions + const impressionOptions = metricsHelpersImpressions.impressionOptions(objectGraph, brickData, metadata.title, metricsOptions); + metricsHelpersImpressions.addImpressionFields(objectGraph, brickModel, impressionOptions); + // Safe area + brickModel.artworkSafeArea = models.ChartOrCategorySafeArea.defaultTileArtworkSafeArea; + brickModel.textSafeArea = models.ChartOrCategorySafeArea.defaultTileTextSafeArea; + if (!brickModel.isValid()) { + return null; + } + return brickModel; + } + /// Creates and returns Choose Your Favorites brick. + static createChooseYourFavoritesBrick(objectGraph, metricsPageInformation, metricsLocationTracker) { + const brickModel = new models.Brick(); + // Artwork + const artwork = new models.Artwork("", 1060, 520, [], color.fromHex("efac78")); + brickModel.artworks = [artwork]; + // Labels + const title = objectGraph.loc.string("ARCADE_CHOOSE_YOUR_FAVORITES_BRICK_TITLE"); + brickModel.accessibilityLabel = title; + brickModel.shortEditorialDescription = title; + // Action + const flowAction = new models.FlowAction("arcadeDownloadPackCategories"); + flowAction.presentationContext = "presentModalFormSheet"; + flowAction.pageData = "unknown"; + brickModel.clickAction = flowAction; + // Impressions + const metricsOptions = { + targetType: "brick", + pageInformation: metricsPageInformation, + locationTracker: metricsLocationTracker, + recoMetricsData: null, + }; + const impressionOptions = metricsHelpersImpressions.impressionOptionsForArcadeChooseYourFavoritesBrick(metricsOptions); + metricsHelpersImpressions.addImpressionFields(objectGraph, brickModel, impressionOptions); + // Safe area + brickModel.artworkSafeArea = models.ChartOrCategorySafeArea.defaultTileArtworkSafeArea; + brickModel.textSafeArea = models.ChartOrCategorySafeArea.defaultTileTextSafeArea; + return brickModel; + } + // endregion + // region Metrics + /** + * Return the shelf metrics options to use for this specific shelf. Using the base options from the grouping + * page controller + * + * @param objectGraph The App Store dependency graph + * @param shelfToken The shelf shelfToken for this current shelf creation request + * @param baseMetricsOptions The minimum set of metrics options for this shelf, created by the + * grouping page controller + */ + shelfMetricsOptionsFromBaseMetricsOptions(objectGraph, shelfToken, baseMetricsOptions) { + const shelfMetricsOptions = { ...baseMetricsOptions }; + const displayStyle = serverData.asString(shelfToken.featuredContentData.attributes, "displayStyle"); + // If this is a Category Bricks shelf, configure metrics title accordingly. + if (displayStyle === "small") { + shelfMetricsOptions.title = "Browse Categories"; + } + return shelfMetricsOptions; + } + // endregion + // region Sort + sortCategories(objectGraph, categories) { + return categories.sort((category1, category2) => { + try { + return category1.shortEditorialDescription.localeCompare(category2.shortEditorialDescription, objectGraph.loc.safeIdentifier, { + usage: "sort", + }); + } + catch (e) { + return 0; + } + }); + } +} +//# sourceMappingURL=grouping-brick-shelf-controller.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/grouping/shelf-controllers/grouping-category-shelf-controller.js b/node_modules/@jet-app/app-store/tmp/src/common/grouping/shelf-controllers/grouping-category-shelf-controller.js new file mode 100644 index 0000000..38d8324 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/grouping/shelf-controllers/grouping-category-shelf-controller.js @@ -0,0 +1,207 @@ +import * as models from "../../../api/models"; +import * as serverData from "../../../foundation/json-parsing/server-data"; +import * as mediaAttributes from "../../../foundation/media/attributes"; +import * as mediaDataStructure from "../../../foundation/media/data-structure"; +import * as mediaRelationship from "../../../foundation/media/relationships"; +import * as content from "../../content/content"; +import * as lockups from "../../lockups/lockups"; +import * as metricsHelpersClicks from "../../metrics/helpers/clicks"; +import * as metricsHelpersImpressions from "../../metrics/helpers/impressions"; +import * as metricsHelpersLocation from "../../metrics/helpers/location"; +import { GroupingShelfController } from "./grouping-shelf-controller"; +import * as groupingShelfControllerCommon from "./grouping-shelf-controller-common"; +export class GroupingCategoryShelfController extends GroupingShelfController { + // region Constructors + constructor() { + super("GroupingCategoryShelfController"); + this.supportedFeaturedContentIds = new Set([425 /* groupingTypes.FeaturedContentID.AppStore_GenreStack */]); + } + // endregion + // region Shelf Creation Prerequisites + /** + * For a given mediaApiData extract the actual shelfContents array needed to render this shelf + * + * @param mediaApiData The outer shelfContents object containing the shelf contents + */ + initialShelfDataFromGroupingMediaApiData(objectGraph, mediaApiData) { + const childListBoxes = mediaRelationship.relationshipData(objectGraph, mediaApiData, "children"); + if (childListBoxes) { + return { shelfContents: mediaRelationship.relationshipCollection(childListBoxes, "children") }; + } + else { + return { shelfContents: [] }; + } + } + /** + * For a given url that this controller handles, we should return a promise that will result in the `ShelfData` + * needed to render this shelf + * + * @param objectGraph The App Store dependency graph + * @param shelfUrl The url that this controller handled on a secondary fetch + * @param parameters The extracted parameters from the shelf url + */ + async secondaryShelfDataForShelfUrl(objectGraph, shelfUrl, shelfToken, parameters) { + return await GroupingShelfController.secondaryGroupingShelfDataForShelfUrl(objectGraph, shelfUrl, shelfToken, parameters).then((shelfData) => { + const childListBoxes = mediaRelationship.relationshipData(objectGraph, shelfToken.featuredContentData, "children"); + if (childListBoxes) { + const hydratedListBoxSections = groupingShelfControllerCommon.mergeContentDataIntoEditorialData(shelfData.shelfContents, mediaRelationship.relationshipCollection(childListBoxes, "children")); + return { shelfContents: hydratedListBoxSections }; + } + else { + return { shelfContents: [] }; + } + }); + } + /** + * For a given mediaApiData create an updated shelf token that contains all the additional data for this specific shelf type + * + * @param objectGraph The App Store dependency graph + * @param baseShelfToken The base grouping shelf token created by the grouping-controller + * @param mediaApiData The outer data object containing the FC properties and data + * @param groupingParseContext The parse context for the grouping page so far + */ + shelfTokenFromBaseTokenAndMediaApiData(objectGraph, mediaApiData, baseShelfToken, groupingParseContext) { + return baseShelfToken; + } + // endregion + // region Shelf Creation + /** + * + * @param objectGraph The App Store dependency graph + * @param shelfToken The shelf shelfToken for this current shelf creation request + * @param shelfData The media api shelfContents array for this shelf + * @param groupingParseContext The parse context used to generate the grouping page on the initial page load, + * this will be missing when this controller renders a secondary or incomplete shelf fetch. + */ + _createShelf(objectGraph, shelfToken, shelfData, groupingParseContext) { + var _a; + if (serverData.isNullOrEmpty(shelfData.shelfContents)) { + return null; + } + const items = []; + let itemsHaveRectangularArtwork = false; + for (const category of shelfData.shelfContents) { + const grouping = mediaRelationship.relationshipData(objectGraph, category, "grouping"); + if (serverData.isNull(grouping)) { + continue; + } + const adamId = groupingShelfControllerCommon.contentIdFromContentItem(objectGraph, grouping); + if (serverData.isNull(category.attributes) || + serverData.isNull(grouping.attributes) || + groupingShelfControllerCommon.shouldDefer(shelfToken)) { + shelfToken.isDeferring = true; + shelfToken.remainingItems.push(grouping); + continue; + } + const metricsBase = { + targetType: "listItem", + pageInformation: shelfToken.metricsPageInformation, + locationTracker: shelfToken.metricsLocationTracker, + recoMetricsData: mediaDataStructure.metricsFromMediaApiObject(category), + }; + const action = lockups.actionFromData(objectGraph, grouping, { ...metricsBase, id: adamId }, shelfToken.clientIdentifierOverride); + metricsHelpersImpressions.addImpressionFields(objectGraph, action, { + ...metricsBase, + kind: "link", + softwareType: serverData.asBooleanOrFalse(groupingParseContext === null || groupingParseContext === void 0 ? void 0 : groupingParseContext.isArcadePage) ? "Arcade" : null, + title: action.title, + id: adamId, + }); + const artwork = mediaAttributes.attributeAsDictionary(grouping, "artwork"); + if (artwork.width > artwork.height) { + itemsHaveRectangularArtwork = true; + } + if (serverData.isDefinedNonNullNonEmpty(artwork)) { + action.artwork = content.artworkFromApiArtwork(objectGraph, artwork, { + allowingTransparency: true, + useCase: 20 /* content.ArtworkUseCase.CategoryIcon */, + }); + } + items.push(action); + metricsHelpersLocation.nextPosition(shelfToken.metricsLocationTracker); + } + // The top level shelf on mac should always be sorted + // <rdar://problem/40954563> LOC: Global: MAS: Order of categories appears in English alphabetical order instead of in each language's alphabetical order. + if (objectGraph.client.isMac) { + this.sortCategories(objectGraph, items); + } + const shelf = this.shelfForCategoryActions(objectGraph, items, shelfToken); + shelf.title = shelfToken.title; + shelf.subtitle = shelfToken.subtitle; + shelf.url = groupingShelfControllerCommon.createShelfTokenUrlIfNecessaryForShelf(objectGraph, shelf, shelfToken); + if (itemsHaveRectangularArtwork) { + const existingPresentationHints = (_a = shelf.presentationHints) !== null && _a !== void 0 ? _a : {}; + shelf.presentationHints = { + ...existingPresentationHints, + itemsHaveRectangularArtwork: true, + }; + } + return shelf; + } + shelfForCategoryActions(objectGraph, categories, shelfToken) { + const shelf = new models.Shelf("action"); + // Limit for number of items (`null` when there's no limit). + let itemLimit; + // Configure shelf orientation and limit + switch (objectGraph.client.deviceType) { + case "tv": + shelf.isHorizontal = true; + itemLimit = 8; + break; + case "mac": + shelf.isHorizontal = false; + itemLimit = null; + break; + case "web": + shelf.isHorizontal = true; + shelf.rowsPerColumn = 3; + itemLimit = 24; + break; + default: + shelf.isHorizontal = false; + itemLimit = 6; + break; + } + // Apply limit (if any) + if (itemLimit !== null && categories.length > itemLimit) { + shelf.items = categories.slice(0, itemLimit); + const allCategoriesAction = new models.FlowAction("page"); + allCategoriesAction.title = objectGraph.loc.string("ACTION_SEE_ALL"); + metricsHelpersClicks.addClickEventToSeeAllAction(objectGraph, allCategoriesAction, null, { + pageInformation: shelfToken.metricsPageInformation, + locationTracker: shelfToken.metricsLocationTracker, + }); + shelf.seeAllAction = allCategoriesAction; + const allCategoriesShelf = new models.Shelf("action"); + allCategoriesShelf.isHorizontal = false; + this.sortCategories(objectGraph, categories); + allCategoriesShelf.items = categories; + const allCategoriesPage = new models.GenericPage([allCategoriesShelf]); + allCategoriesPage.title = objectGraph.loc.string("PAGE_TITLE_CATEGORIES"); + allCategoriesAction.pageData = allCategoriesPage; + } + else { + shelf.items = categories; + } + return shelf; + } + // region Helpers + /** + * Sort the category actions alphabetically + * @param objectGraph + * @param categories The cateogries we're going to be displaying + */ + sortCategories(objectGraph, categories) { + categories.sort((category1, category2) => { + try { + return category1.title.localeCompare(category2.title, objectGraph.loc.safeIdentifier, { + usage: "sort", + }); + } + catch (e) { + return 0; + } + }); + } +} +//# sourceMappingURL=grouping-category-shelf-controller.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/grouping/shelf-controllers/grouping-editorial-card-shelf-controller.js b/node_modules/@jet-app/app-store/tmp/src/common/grouping/shelf-controllers/grouping-editorial-card-shelf-controller.js new file mode 100644 index 0000000..12dc94f --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/grouping/shelf-controllers/grouping-editorial-card-shelf-controller.js @@ -0,0 +1,229 @@ +import * as models from "../../../api/models"; +import * as serverData from "../../../foundation/json-parsing/server-data"; +import * as mediaAttributes from "../../../foundation/media/attributes"; +import * as mediaDataStructure from "../../../foundation/media/data-structure"; +import * as mediaRelationship from "../../../foundation/media/relationships"; +import * as color from "../../../foundation/util/color-util"; +import * as flowPreview from "../../content/flow-preview"; +import * as metricsHelpersClicks from "../../metrics/helpers/clicks"; +import * as metricsHelpersImpressions from "../../metrics/helpers/impressions"; +import * as metricsHelpersLocation from "../../metrics/helpers/location"; +import * as metricsHelpersUtil from "../../metrics/helpers/util"; +import * as onDevicePersonalization from "../../personalization/on-device-personalization"; +import { GroupingShelfController } from "./grouping-shelf-controller"; +import * as groupingShelfControllerCommon from "./grouping-shelf-controller-common"; +export class GroupingEditorialCardShelfController extends GroupingShelfController { + // region Constructors + constructor() { + super("GroupingEditorialCardShelfController"); + this.supportedFeaturedContentIds = new Set([ + 415 /* groupingTypes.FeaturedContentID.AppStore_HeroList */, + 416 /* groupingTypes.FeaturedContentID.AppStore_Hero */, + 501 /* groupingTypes.FeaturedContentID.AppStore_PersonalizedHeroMarker */, + 417 /* groupingTypes.FeaturedContentID.AppStore_CustomHero */, + 258 /* groupingTypes.FeaturedContentID.Sundance_Flowcase */, + ]); + } + // endregion + // region Shelf Creation Prerequisites + /** + * For a given mediaApiData extract the actual shelfContents array needed to render this shelf + * + * @param mediaApiData The outer shelfContents object containing the shelf contents + */ + initialShelfDataFromGroupingMediaApiData(objectGraph, mediaApiData) { + return { shelfContents: mediaRelationship.relationshipCollection(mediaApiData, "children") }; + } + /** + * For a given url that this controller handles, we should return a promise that will result in the `ShelfData` + * needed to render this shelf + * + * @param objectGraph The App Store dependency graph + * @param shelfUrl The url that this controller handled on a secondary fetch + * @param parameters The extracted parameters from the shelf url + */ + async secondaryShelfDataForShelfUrl(objectGraph, shelfUrl, shelfToken, parameters) { + return await GroupingShelfController.secondaryGroupingShelfDataForShelfUrl(objectGraph, shelfUrl, shelfToken, parameters); + } + /** + * For a given mediaApiData create an updated shelf token that contains all the additional data for this specific shelf type + * + * @param objectGraph The App Store dependency graph + * @param baseShelfToken The base grouping shelf token created by the grouping-controller + * @param mediaApiData The outer data object containing the FC properties and data + * @param groupingParseContext The parse context for the grouping page so far + */ + shelfTokenFromBaseTokenAndMediaApiData(objectGraph, mediaApiData, baseShelfToken, groupingParseContext) { + return baseShelfToken; + } + // endregion + // region Shelf Creation + /** + * + * @param objectGraph The App Store dependency graph + * @param shelfToken The shelf shelfToken for this current shelf creation request + * @param shelfData The media api shelfContents array for this shelf + * @param groupingParseContext The parse context used to generate the grouping page on the initial page load, + * this will be missing when this controller renders a secondary or incomplete shelf fetch. + */ + _createShelf(objectGraph, shelfToken, shelfData, groupingParseContext) { + const shelf = new models.Shelf("editorialCard"); + shelf.isHorizontal = true; + const personalizationDataContainer = this.personalizationDataContainerForEditorialCardItemsDataArray(objectGraph, shelfData.shelfContents); + const items = []; + for (const data of shelfData.shelfContents) { + const card = GroupingEditorialCardShelfController.makeEditorialCard(objectGraph, data, personalizationDataContainer, groupingParseContext, shelfToken); + if (serverData.isNullOrEmpty(card) || !card.isValid()) { + continue; + } + items.push(card); + metricsHelpersLocation.nextPosition(shelfToken.metricsLocationTracker); + } + // We don't need this in our incomplete shelf URL, so we'll preemptively remove it. + delete shelfToken.maxItemCount; + if (objectGraph.client.isVision) { + shelf.presentationHints = { ...shelf.presentationHints, showSupplementaryText: true }; + } + shelf.items = items; + shelf.url = groupingShelfControllerCommon.createShelfTokenUrlIfNecessaryForShelf(objectGraph, shelf, shelfToken); + return shelf; + } + static makeEditorialCard(objectGraph, itemData, personalizationDataContainer, groupingParseContext, shelfToken) { + var _a, _b, _c; + const metricsOptions = { + targetType: "hero", + pageInformation: shelfToken === null || shelfToken === void 0 ? void 0 : shelfToken.metricsPageInformation, + locationTracker: shelfToken === null || shelfToken === void 0 ? void 0 : shelfToken.metricsLocationTracker, + recoMetricsData: mediaDataStructure.metricsFromMediaApiObject(itemData), + id: itemData.id, + idType: "editorial_id", + }; + const featuredContentId = mediaAttributes.attributeAsNumber(itemData, "editorialElementKind"); + const shouldPersonalizeContent = featuredContentId === 501 /* groupingTypes.FeaturedContentID.AppStore_PersonalizedHeroMarker */; + const metadata = groupingShelfControllerCommon.metadataForFCData(objectGraph, itemData, shelfToken, shouldPersonalizeContent, personalizationDataContainer, metricsOptions, groupingParseContext, () => { + shelfToken === null || shelfToken === void 0 ? void 0 : shelfToken.remainingItems.push(itemData); + }); + if (!metadata) { + return null; + } + const hasContentId = ((_b = (_a = metadata.content) === null || _a === void 0 ? void 0 : _a.id) === null || _b === void 0 ? void 0 : _b.length) > 0; + if (hasContentId) { + metricsOptions.id = metadata.content.id; + metricsOptions.idType = "its_id"; + metricsOptions.adamId = metadata.content.id; + } + const card = new models.EditorialCard(); + // Set caption + let caption = mediaAttributes.attributeAsString(itemData, "designBadge"); + if (!caption) { + caption = metadata.caption; + } + card.caption = caption; + // Set title + let title = mediaAttributes.attributeAsString(itemData, "title"); + if (!title) { + title = metadata.title; + } + card.title = title; + // Set subtitle + let subtitle = groupingShelfControllerCommon.unescapeHtmlString(mediaAttributes.attributeAsString(itemData, "designTag")); + if (!subtitle) { + subtitle = metadata.subtitle; + } + card.subtitle = subtitle; + // Setup Artwork + const artworkOptions = { + useCase: 19 /* content.ArtworkUseCase.GroupingHero */, + withJoeColorPlaceholder: true, + }; + if (metadata.artwork && (shelfToken === null || shelfToken === void 0 ? void 0 : shelfToken.featuredContentId) !== 258 /* groupingTypes.FeaturedContentID.Sundance_Flowcase */) { + let artworkDict = serverData.asDictionary(metadata.artwork, "subscriptionHero"); + if (serverData.isNull(artworkDict) && serverData.isDefinedNonNull(metadata.appEvent)) { + artworkDict = serverData.asDictionary(metadata.artwork, "eventCard"); + } + card.artwork = groupingShelfControllerCommon.groupingArtworkFromApiArtwork(objectGraph, artworkDict, artworkOptions); + } + else { + card.artwork = groupingShelfControllerCommon.artworkFromFC(objectGraph, itemData, 416, 204, artworkOptions); + } + // Set action + card.clickAction = metadata.action; + // App event formatted dates + if (serverData.isDefinedNonNull(metadata.appEvent)) { + card.appEventFormattedDates = metadata.appEvent.formattedDates; + } + // Lockup + card.lockup = metadata.lockup; + // Overlay style + if (serverData.isDefinedNonNull(card.artwork) && serverData.isDefinedNonNull(card.artwork.backgroundColor)) { + const isArtworkDark = color.isDarkColor(card.artwork.backgroundColor); + card.mediaOverlayStyle = isArtworkDark ? "dark" : "light"; + if (serverData.isDefinedNonNull(card.lockup) && + serverData.isDefinedNonNull(card.lockup.offerDisplayProperties) && + objectGraph.host.isiOS) { + const offerEnvironment = isArtworkDark ? "dark" : "light"; + card.lockup.offerDisplayProperties = + card.lockup.offerDisplayProperties.newOfferDisplayPropertiesChangingAppearance(false, "transparent", offerEnvironment); + } + } + // Set adamId + card.adamId = serverData.asString(metadata.content, "id"); + // Set flow preview actions + const contentData = mediaRelationship.relationshipData(objectGraph, itemData, "contents"); + if (serverData.isDefinedNonNull(contentData)) { + const metricsClickOptions = metricsHelpersClicks.clickOptionsForLockup(objectGraph, contentData, metricsOptions); + metricsClickOptions.targetType = metricsOptions.targetType; + card.flowPreviewActionsConfiguration = flowPreview.flowPreviewActionsConfigurationForProductFromData(objectGraph, itemData, false, shelfToken === null || shelfToken === void 0 ? void 0 : shelfToken.clientIdentifierOverride, card.clickAction, metricsOptions, metricsClickOptions); + } + // Configure impressions + const impressionOptions = metricsHelpersImpressions.impressionOptions(objectGraph, (_c = metadata.content) !== null && _c !== void 0 ? _c : itemData, metadata.title, metricsOptions); + if (serverData.isDefinedNonNull(metadata.onDevicePersonalizationDataProcessingType)) { + const recoMetricsData = metricsHelpersUtil.combinedRecoMetricsDataFromMetricsData(impressionOptions.recoMetricsData, metadata.onDevicePersonalizationDataProcessingType, null); + impressionOptions.recoMetricsData = recoMetricsData; + } + if (serverData.isDefinedNonNull(metadata.appEvent)) { + impressionOptions.inAppEventId = metadata.appEvent.appEventId; + if (serverData.isDefinedNonNull(metadata.appEvent.lockup)) { + impressionOptions.relatedSubjectIds = [metadata.appEvent.lockup.adamId]; + } + } + if (serverData.isDefinedNonNull(shelfToken)) { + metricsHelpersImpressions.addImpressionFields(objectGraph, card, impressionOptions); + } + return card; + } + // region Helpers + /** + * Iterates through all the editorial cards data, and creates a set of personalization data that is targetted only to these cards. + * + * @param objectGraph + * @param dataArray The input array of editorial card data items + * @returns Any relevant OnDevicePersonalizaionData + */ + personalizationDataContainerForEditorialCardItemsDataArray(objectGraph, dataArray) { + var _a, _b; + if (!onDevicePersonalization.isPersonalizationAvailable(objectGraph)) { + return null; + } + // First iterate through and extract any relevant App IDs from meta.personalizationData for each card's contents. + const appIds = new Set(); + for (const data of dataArray) { + const featuredContentId = mediaAttributes.attributeAsNumber(data, "editorialElementKind"); + const shouldPersonalizeContent = featuredContentId === 501 /* groupingTypes.FeaturedContentID.AppStore_PersonalizedHeroMarker */; + const isLinkNode = ((_a = serverData.asString(data, "url")) === null || _a === void 0 ? void 0 : _a.length) > 0; + const containsLinkNode = ((_b = mediaAttributes.attributeAsString(data, "link.url")) === null || _b === void 0 ? void 0 : _b.length) > 0; + const isContentNode = mediaRelationship.hasRelationship(data, "contents", false); + if (shouldPersonalizeContent && !isLinkNode && !containsLinkNode && isContentNode) { + const contentDataItems = mediaRelationship.relationshipCollection(data, "contents"); + for (const contentData of contentDataItems) { + const appId = serverData.asString(contentData, "meta.personalizationData.appId"); + if ((appId === null || appId === void 0 ? void 0 : appId.length) > 0) { + appIds.add(appId); + } + } + } + } + return onDevicePersonalization.personalizationDataContainerForAppIds(objectGraph, appIds); + } +} +//# sourceMappingURL=grouping-editorial-card-shelf-controller.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/grouping/shelf-controllers/grouping-editorial-story-card-shelf-controller.js b/node_modules/@jet-app/app-store/tmp/src/common/grouping/shelf-controllers/grouping-editorial-story-card-shelf-controller.js new file mode 100644 index 0000000..73a0725 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/grouping/shelf-controllers/grouping-editorial-story-card-shelf-controller.js @@ -0,0 +1,143 @@ +import * as models from "../../../api/models"; +import * as serverData from "../../../foundation/json-parsing/server-data"; +import * as mediaAttributes from "../../../foundation/media/attributes"; +import * as mediaRelationship from "../../../foundation/media/relationships"; +import { ShelfParameters } from "../../../foundation/network/url-constants"; +import * as color from "../../../foundation/util/color-util"; +import * as content from "../../content/content"; +import * as metricsHelpersLocation from "../../metrics/helpers/location"; +import { createTodayBaseCard } from "../../today/cards/today-base-card-builder"; +import { TodayParseContext } from "../../today/today-types"; +import { GroupingShelfController, routesForFeaturedContentIds } from "./grouping-shelf-controller"; +import * as groupingShelfControllerCommon from "./grouping-shelf-controller-common"; +export class GroupingEditorialStoryCardShelfController extends GroupingShelfController { + // region Constructors + constructor() { + super("GroupingEditorialStoryCardShelfController"); + this.supportedFeaturedContentIds = new Set([475 /* groupingTypes.FeaturedContentID.AppStore_HorizontalCardSwoosh */]); + } + // endregion + // region Shelf Builder + shelfRoute(objectGraph) { + return routesForFeaturedContentIds(this.supportedFeaturedContentIds, [ + `${ShelfParameters.contentType}=editorialStoryCard`, + ]); + } + // endregion + // region GroupingShelfController + _supports(objectGraph, mediaApiData, featuredContentId, nativeGroupingShelfId) { + if (!super._supports(objectGraph, mediaApiData, featuredContentId, nativeGroupingShelfId)) { + return false; + } + const displayStyle = mediaAttributes.attributeAsString(mediaApiData, "displayStyle"); + const contentType = groupingShelfControllerCommon.contentTypeForHorizontalCardDisplayStyle(objectGraph, displayStyle); + return contentType === "editorialStoryCard"; + } + // endregion + // region Shelf Creation Prerequisites + /** + * For a given mediaApiData extract the actual shelfContents array needed to render this shelf + * + * @param mediaApiData The outer shelfContents object containing the shelf contents + */ + initialShelfDataFromGroupingMediaApiData(objectGraph, mediaApiData) { + return { shelfContents: mediaRelationship.relationshipCollection(mediaApiData, "children") }; + } + /** + * For a given url that this controller handles, we should return a promise that will result in the `ShelfData` + * needed to render this shelf + * + * @param objectGraph The App Store dependency graph + * @param shelfUrl The url that this controller handled on a secondary fetch + * @param parameters The extracted parameters from the shelf url + */ + async secondaryShelfDataForShelfUrl(objectGraph, shelfUrl, shelfToken, parameters) { + return await GroupingShelfController.secondaryGroupingShelfDataForShelfUrl(objectGraph, shelfUrl, shelfToken, parameters); + } + /** + * For a given mediaApiData create an updated shelf token that contains all the additional data for this specific shelf type + * + * @param objectGraph The App Store dependency graph + * @param baseShelfToken The base grouping shelf token created by the grouping-controller + * @param mediaApiData The outer data object containing the FC properties and data + * @param groupingParseContext The parse context for the grouping page so far + */ + shelfTokenFromBaseTokenAndMediaApiData(objectGraph, mediaApiData, baseShelfToken, groupingParseContext) { + const shelfToken = { ...baseShelfToken }; + const displayStyle = mediaAttributes.attributeAsString(mediaApiData, "displayStyle"); + shelfToken.shelfStyle = groupingShelfControllerCommon.contentTypeForHorizontalCardDisplayStyle(objectGraph, displayStyle); + return shelfToken; + } + // endregion + // region Shelf Creation + /** + * + * @param objectGraph The App Store dependency graph + * @param shelfToken The shelf shelfToken for this current shelf creation request + * @param shelfData The media api shelfContents array for this shelf + * @param groupingParseContext The parse context used to generate the grouping page on the initial page load, + * this will be missing when this controller renders a secondary or incomplete shelf fetch. + */ + _createShelf(objectGraph, shelfToken, shelfData, groupingParseContext) { + const items = []; + for (const card of shelfData.shelfContents) { + if (!mediaAttributes.hasAttributes(card) || groupingShelfControllerCommon.shouldDefer(shelfToken)) { + shelfToken.remainingItems.push(card); + shelfToken.isDeferring = true; + continue; + } + const cardModel = GroupingEditorialStoryCardShelfController.makeStoryCard(objectGraph, card, shelfToken); + if (serverData.isNullOrEmpty(cardModel)) { + continue; + } + items.push(cardModel); + metricsHelpersLocation.nextPosition(shelfToken.metricsLocationTracker); + } + const shelf = new models.Shelf(shelfToken.shelfStyle); + shelf.title = shelfToken.title; + shelf.items = items; + shelf.isHorizontal = true; + shelf.background = { + type: "interactive", + }; + shelf.url = groupingShelfControllerCommon.createShelfTokenUrlIfNecessaryForShelf(objectGraph, shelf, shelfToken); + shelf.isHorizontal = true; + return shelf; + } + static makeStoryCard(objectGraph, itemData, shelfToken) { + // Prefer `subscriptionHero` if it's available for grouping pages, and fallback to `mediaCard` if not. + let artworkData = mediaAttributes.attributeAsDictionary(itemData, "editorialArtwork.subscriptionHero"); + if (serverData.isNullOrEmpty(artworkData)) { + artworkData = mediaAttributes.attributeAsDictionary(itemData, "editorialArtwork.mediaCard"); + } + const artwork = content.artworkFromApiArtwork(objectGraph, artworkData, { + cropCode: "fn", + withJoeColorPlaceholder: true, + useCase: 16 /* content.ArtworkUseCase.TodaySmallStoryCard */, + }); + if (serverData.isNull(artwork)) { + return null; + } + const title = mediaAttributes.attributeAsString(itemData, "editorialNotes.name"); + const heading = mediaAttributes.attributeAsString(itemData, "label"); + const description = mediaAttributes.attributeAsString(itemData, "editorialNotes.short"); + const cardModel = new models.EditorialStoryCard(title, artwork, null, heading, { + type: "text", + title: heading, + }, description); + const basicCard = createTodayBaseCard(objectGraph, itemData, null, new TodayParseContext(shelfToken === null || shelfToken === void 0 ? void 0 : shelfToken.metricsPageInformation, shelfToken === null || shelfToken === void 0 ? void 0 : shelfToken.metricsLocationTracker)); + if (serverData.isDefinedNonNull(basicCard)) { + cardModel.clickAction = basicCard.clickAction; + } + const backgroundStyle = color.isDarkColor(artwork.backgroundColor) + ? "dark" + : "light"; + cardModel.shelfBackground = { + type: "artwork", + artwork: artwork, + style: backgroundStyle, + }; + return cardModel; + } +} +//# sourceMappingURL=grouping-editorial-story-card-shelf-controller.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/grouping/shelf-controllers/grouping-game-center-activity-feed-shelf-controller.js b/node_modules/@jet-app/app-store/tmp/src/common/grouping/shelf-controllers/grouping-game-center-activity-feed-shelf-controller.js new file mode 100644 index 0000000..4fa27fe --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/grouping/shelf-controllers/grouping-game-center-activity-feed-shelf-controller.js @@ -0,0 +1,223 @@ +import * as models from "../../../api/models"; +import { Parameters, Path, Protocol } from "../../../foundation/network/url-constants"; +import * as urls from "../../../foundation/network/urls"; +import * as metricsHelpersClicks from "../../metrics/helpers/clicks"; +import * as metricsHelpersImpressions from "../../metrics/helpers/impressions"; +import * as metricsHelpersLocation from "../../metrics/helpers/location"; +import { GroupingShelfController } from "./grouping-shelf-controller"; +import * as groupingShelfControllerCommon from "./grouping-shelf-controller-common"; +import { ActionMetrics } from "../../../api/models"; +import { makeGameCenterHeader } from "../../arcade/arcade-common"; +export class GroupingGameCenterActivityFeedController extends GroupingShelfController { + // region Constructors + constructor() { + super("GroupingGameCenterActivityFeedController"); + this.batchGroupKey = "gameCenter"; + this.supportedFeaturedContentIds = new Set([ + 548 /* groupingTypes.FeaturedContentID.AppStore_GameCenterActivityFeedMarker */, + ]); + } + // endregion + // region Shelf Builder + shelfRoute(objectGraph) { + return [ + ...super.shelfRoute(objectGraph), + { + protocol: Protocol.internal, + path: `/${Path.grouping}/${Path.shelf}/{token}`, + query: [Parameters.isGameCenterActivityFeedShelf], + }, + ]; + } + // endregion + // region Shelf Creation Prerequisites + /** + * For a given mediaApiData extract the actual shelfContents array needed to render this shelf + * + * @param mediaApiData The outer shelfContents object containing the shelf contents + */ + initialShelfDataFromGroupingMediaApiData(objectGraph, mediaApiData) { + return { + shelfContents: [], + activities: [], + }; + } + /** + * For a given url that this controller handles, we should return a promise that will result in the `ShelfData` + * needed to render this shelf + * + * @param objectGraph The App Store dependency graph + * @param shelfUrl The url that this controller handled on a secondary fetch + * @param parameters The extracted parameters from the shelf url + */ + async secondaryShelfDataForShelfUrl(objectGraph, shelfUrl, shelfToken, parameters) { + const filter = this.gameCategoryFilter(shelfToken.gamesFilter); + const activityLimit = 20; + return await objectGraph.gameCenter.fetchActivityFeedCards(filter, activityLimit).then((activities) => { + return { + shelfContents: [], + activities: activities, + }; + }); + } + /** + * For a given mediaApiData create an updated shelf token that contains all the additional data for this specific shelf type + * + * @param objectGraph The App Store dependency graph + * @param baseShelfToken The base grouping shelf token created by the grouping-controller + * @param mediaApiData The outer data object containing the FC properties and data + * @param groupingParseContext The parse context for the grouping page so far + */ + shelfTokenFromBaseTokenAndMediaApiData(objectGraph, mediaApiData, baseShelfToken, groupingParseContext) { + return baseShelfToken; + } + incompleteShelfFetchStrategy(objectGraph) { + return models.IncompleteShelfFetchStrategy.OnPageLoad; + } + // endregion + // region Shelf Creation + /** + * + * @param objectGraph The App Store dependency graph + * @param shelfToken The shelf shelfToken for this current shelf creation request + * @param shelfData The media api shelfContents array for this shelf + * @param groupingParseContext The parse context used to generate the grouping page on the initial page load, + * this will be missing when this controller renders a secondary or incomplete shelf fetch. + */ + _createShelf(objectGraph, shelfToken, shelfData, groupingParseContext) { + if (shelfToken.isFirstRender) { + return this.pendingActivityFeedShelfForGrouping(objectGraph, shelfToken, groupingParseContext === null || groupingParseContext === void 0 ? void 0 : groupingParseContext.isArcadePage); + } + else { + return this.activityFeedShelfForGrouping(objectGraph, shelfData, shelfToken, groupingParseContext === null || groupingParseContext === void 0 ? void 0 : groupingParseContext.isArcadePage); + } + } + pendingActivityFeedShelfForGrouping(objectGraph, shelfToken, isArcadePage) { + if (objectGraph.client.deviceType !== "phone" && objectGraph.client.deviceType !== "pad") { + return null; + } + const shelf = this.activityFeedShelfForGrouping(objectGraph, { + shelfContents: [], + activities: [], + }, shelfToken, isArcadePage); + const groupingShelfUrl = urls.URL.from(groupingShelfControllerCommon.groupingShelfUrl(shelfToken)); + shelf.url = groupingShelfUrl.param(Parameters.isGameCenterActivityFeedShelf, "true").build(); + shelf.isHidden = shelf.items.length === 0; + shelf.batchGroup = this.batchGroupKey; + return shelf; + } + activityFeedShelfForGrouping(objectGraph, shelfData, shelfToken, isArcadePage) { + if (objectGraph.client.deviceType !== "phone" && objectGraph.client.deviceType !== "pad") { + return null; + } + const shelf = this.activityFeedShelf(objectGraph, shelfData.activities, shelfToken, isArcadePage); + const title = objectGraph.loc.string("Arcade.ActivityFeed.RecentActivity"); + shelf.header = makeGameCenterHeader(objectGraph, title); + // Connect the shelf's seeAllAction + groupingShelfControllerCommon.replaceShelfSeeAllAction(objectGraph, shelf, shelf.seeAllAction); + shelf.batchGroup = this.batchGroupKey; + // Hide when empty. + shelf.isHidden = shelf.items.length === 0; + return shelf; + } + // region Helpers + activityFeedShelf(objectGraph, activities, token, isArcadePage = false) { + const shelf = new models.Shelf("gameCenterActivityFeedCard"); + shelf.isHorizontal = true; + shelf.mergeWhenFetched = true; + shelf.batchGroup = this.batchGroupKey; + shelf.items = activities; + shelf.isHidden = shelf.items.length === 0; + activities.forEach((item, index) => { + const metricsImpressionOptions = { + id: "friendActivity", + idType: "static", + targetType: "chiclet", + kind: null, + softwareType: isArcadePage ? "Arcade" : null, + title: "", + pageInformation: token.metricsPageInformation, + locationTracker: token.metricsLocationTracker, + }; + metricsHelpersImpressions.addImpressionFields(objectGraph, item, metricsImpressionOptions); + // Create action metrics for click events. + const profileActionMetrics = new ActionMetrics(); + const profileAvatarActionMetrics = new ActionMetrics(); + const leaderboardActionMetrics = new ActionMetrics(); + const achievementActionMetrics = new ActionMetrics(); + const appActionMetrics = new ActionMetrics(); + // Create targetId and metrics array. + const targetIdAndMetricsArray = [ + { + targetId: "playerName", + metrics: profileActionMetrics, + }, + { + targetId: "profileImage", + metrics: profileAvatarActionMetrics, + }, + { + targetId: "leaderboardAchievement", + metrics: leaderboardActionMetrics, + }, + { + targetId: "achievement", + metrics: achievementActionMetrics, + }, + { + targetId: item.adamID || "gameIcon", + metrics: appActionMetrics, + }, + ]; + // Loop through `targetIdAndMetricsArray` and call `addClickEventToActivityFeedMetrics` for each metrics. + targetIdAndMetricsArray.forEach((targetIdAndMetricsDictionary) => metricsHelpersClicks.addClickEventToActivityFeedMetrics(objectGraph, targetIdAndMetricsDictionary.metrics, token.title, targetIdAndMetricsDictionary.targetId, { + pageInformation: token.metricsPageInformation, + locationTracker: token.metricsLocationTracker, + })); + // Assign action metrics to `item`. + item.profileActionMetrics = profileActionMetrics; + item.profileAvatarActionMetrics = profileAvatarActionMetrics; + item.leaderboardActionMetrics = leaderboardActionMetrics; + item.achievementActionMetrics = achievementActionMetrics; + item.appActionMetrics = appActionMetrics; + // Proceed to next position. + metricsHelpersLocation.nextPosition(token.metricsLocationTracker); + }); + // Setup see all action + let seeAllAction; + if (!objectGraph.featureFlags.isGSEUIEnabled("de7bbd8e")) { + seeAllAction = new models.GameCenterDashboardAction(); + seeAllAction.title = objectGraph.loc.string("Arcade.ActivityFeed.AllActivity", objectGraph.loc.string("ACTION_SEE_ALL")); + metricsHelpersClicks.addClickEventToSeeAllAction(objectGraph, seeAllAction, null, { + pageInformation: token.metricsPageInformation, + locationTracker: token.metricsLocationTracker, + }); + } + shelf.seeAllAction = seeAllAction; + return shelf; + } + // endregion + /** + * Convert GameCategoryFilter to GamesFilter. Ideally these would be the same, but seed 1 has already left the station. + * @param gamesFilter + */ + gameCategoryFilter(gamesFilter) { + if (gamesFilter === "nonArcade") { + return "nonarcade"; + } + return gamesFilter; + } + // endregion + // region Metrics + shelfMetricsOptionsFromBaseMetricsOptions(objectGraph, shelfToken, baseMetricsOptions) { + return { + ...baseMetricsOptions, + title: "Friend Activity", + badges: { + gameCenter: true, + }, + idType: "shelf_id", + }; + } +} +//# sourceMappingURL=grouping-game-center-activity-feed-shelf-controller.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/grouping/shelf-controllers/grouping-game-center-continue-playing-shelf-controller.js b/node_modules/@jet-app/app-store/tmp/src/common/grouping/shelf-controllers/grouping-game-center-continue-playing-shelf-controller.js new file mode 100644 index 0000000..6a85582 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/grouping/shelf-controllers/grouping-game-center-continue-playing-shelf-controller.js @@ -0,0 +1,332 @@ +import * as validation from "@jet/environment/json/validation"; +import * as models from "../../../api/models"; +import * as serverData from "../../../foundation/json-parsing/server-data"; +import * as mediaDataFetching from "../../../foundation/media/data-fetching"; +import * as mediaNetwork from "../../../foundation/media/network"; +import * as mediaRelationship from "../../../foundation/media/relationships"; +import { ResponseMetadata } from "../../../foundation/network/network"; +import { Parameters, Path, Protocol } from "../../../foundation/network/url-constants"; +import * as urls from "../../../foundation/network/urls"; +import * as contentAttributes from "../../content/attributes"; +import * as content from "../../content/content"; +import * as flowPreview from "../../content/flow-preview"; +import * as filtering from "../../filtering"; +import * as lockups from "../../lockups/lockups"; +import * as metricsHelpersClicks from "../../metrics/helpers/clicks"; +import * as metricsHelpersImpressions from "../../metrics/helpers/impressions"; +import { GroupingShelfController } from "./grouping-shelf-controller"; +import * as groupingShelfControllerCommon from "./grouping-shelf-controller-common"; +import { makeGameCenterHeader } from "../../arcade/arcade-common"; +import { injectCrossfireFlowForGameCenter } from "./grouping-game-center-popular-with-your-friends-shelf-controller"; +export class GroupingGameCenterContinuePlayingShelfController extends GroupingShelfController { + // region Constructors + constructor() { + super("GroupingGameCenterContinuePlayingShelfController"); + this.batchGroupKey = "gameCenterContinuePlaying"; + this.supportedFeaturedContentIds = new Set([500 /* groupingTypes.FeaturedContentID.AppStore_ContinuePlayingMarker */]); + } + // endregion + // region GroupingShelfController + _supports(objectGraph, mediaApiData, featuredContentId, nativeGroupingShelfId) { + return (super._supports(objectGraph, mediaApiData, featuredContentId, nativeGroupingShelfId) && + this.supportsVideoCardShelf(objectGraph, objectGraph.host.platform)); + } + // endregion + // region Shelf Builder + shelfRoute(objectGraph) { + return [ + ...super.shelfRoute(objectGraph), + { + protocol: Protocol.internal, + path: `/${Path.grouping}/${Path.shelf}/{token}`, + query: [Parameters.isGameCenterContinuePlayingShelf], + }, + ]; + } + // endregion + // region Shelf Creation Prerequisites + /** + * For a given mediaApiData extract the actual shelfContents array needed to render this shelf + * + * @param mediaApiData The outer shelfContents object containing the shelf contents + */ + initialShelfDataFromGroupingMediaApiData(objectGraph, mediaApiData) { + return { shelfContents: mediaRelationship.relationshipCollection(mediaApiData, "contents") }; + } + /** + * For a given url that this controller handles, we should return a promise that will result in the `ShelfData` + * needed to render this shelf + * + * @param objectGraph The App Store dependency graph + * @param shelfUrl The url that this controller handled on a secondary fetch + * @param parameters The extracted parameters from the shelf url + */ + async secondaryShelfDataForShelfUrl(objectGraph, shelfUrl, shelfToken, parameters) { + const startTime = Date.now(); + const maxNumberOfGames = this.maximumNumberOfRecentGamesToRequest(); + const recentlyPlayedGamesPromise = objectGraph.gameCenter.fetchRecentlyPlayedGamesWithinSeconds(this.gameCategoryFilter(shelfToken.gamesFilter), maxNumberOfGames, objectGraph.bag.recentlyPlayedGamesWindowInSeconds); + return await recentlyPlayedGamesPromise.then(async (recentlyPlayedIds) => { + const endTime = Date.now(); + objectGraph.console.log("grouping-gamecenter-builder: requestForContinuePlaying NATIVE took " + + (endTime - startTime).toString(10) + + " milliseconds."); + let shelfDataPromise; + if (recentlyPlayedIds.length === 0) { + // One of the few exceptions where TS minifiers cannot derive data container type + // without some help, in this case using explicit type for the value passed to promise. + const emptyShelfData = { shelfContents: [] }; + return await Promise.resolve(emptyShelfData); + } + else { + const request = new mediaDataFetching.Request(objectGraph) + .withIdsOfType(recentlyPlayedIds.slice(0, this.maximumNumberOfRecentGamesToShow()), "apps") + .includingAgeRestrictions(); + groupingShelfControllerCommon.prepareGroupingShelfRequest(objectGraph, request); + shelfDataPromise = mediaNetwork + .fetchData(objectGraph, request, {}) + .then((dataContainer) => { + const shelfData = { + shelfContents: dataContainer.data, + responseTimingValues: dataContainer[ResponseMetadata.timingValues], + }; + return shelfData; + }); + } + return await shelfDataPromise; + }); + } + /** + * For a given mediaApiData create an updated shelf token that contains all the additional data for this specific shelf type + * + * @param objectGraph The App Store dependency graph + * @param baseShelfToken The base grouping shelf token created by the grouping-controller + * @param mediaApiData The outer data object containing the FC properties and data + * @param groupingParseContext The parse context for the grouping page so far + */ + shelfTokenFromBaseTokenAndMediaApiData(objectGraph, mediaApiData, baseShelfToken, groupingParseContext) { + return baseShelfToken; + } + incompleteShelfFetchStrategy(objectGraph) { + return models.IncompleteShelfFetchStrategy.OnPageLoad; + } + // endregion + // region Shelf Creation + /** + * + * @param objectGraph The App Store dependency graph + * @param shelfToken The shelf shelfToken for this current shelf creation request + * @param shelfData The media api shelfContents array for this shelf + * @param groupingParseContext The parse context used to generate the grouping page on the initial page load, + * this will be missing when this controller renders a secondary or incomplete shelf fetch. + */ + _createShelf(objectGraph, shelfToken, shelfData, groupingParseContext) { + if (shelfToken.isFirstRender) { + return this.pendingContinuePlayingForGrouping(objectGraph, shelfToken); + } + else { + return this.continuePlayingShelfForGrouping(objectGraph, shelfData.shelfContents, shelfToken); + } + } + pendingContinuePlayingForGrouping(objectGraph, shelfToken) { + const shelf = this.continuePlayingShelfForGrouping(objectGraph, [], shelfToken); + if (!shelf) { + return null; + } + const groupingShelfUrl = urls.URL.from(groupingShelfControllerCommon.groupingShelfUrl(shelfToken)); + shelf.url = groupingShelfUrl.param(Parameters.isGameCenterContinuePlayingShelf, "true").build(); + shelf.batchGroup = this.batchGroupKey; + return shelf; + } + continuePlayingShelfForGrouping(objectGraph, shelfContents, shelfToken) { + return validation.context("continuePlayingShelfForGrouping", () => { + const shelf = this.videoCardContinuePlayingShelf(objectGraph, shelfContents, shelfToken); + shelf.mergeWhenFetched = false; // Always replace + shelf.batchGroup = this.batchGroupKey; + shelf.isHidden = shelf.items.length === 0; + shelf.header = makeGameCenterHeader(objectGraph, objectGraph.loc.string("GameCenter.ContinuePlayingShelf.Title"), shelfToken.subtitle); + return shelf; + }); + } + // endregion + // region Video Card Shelf + supportsVideoCardShelf(objectGraph, platform) { + switch (platform) { + case "iOS": + case "tvOS": + case "macOS": + return true; + default: + return false; + } + } + videoCardContinuePlayingShelf(objectGraph, dataArray, shelfToken) { + return validation.context("videoCardContinuePlayingShelf", () => { + const shelf = new models.Shelf("videoCard"); + shelf.isHorizontal = true; + shelf.batchGroup = this.batchGroupKey; + const items = []; + for (const data of dataArray) { + // Filter out unwanted content + if (filtering.shouldFilter(objectGraph, data)) { + continue; + } + const item = this.editorialSplashVideoCardForContinuePlaying(objectGraph, data, shelfToken); + if (item) { + items.push(item); + } + } + shelf.items = items; + return shelf; + }); + } + /** + * Create a `VideoCard` configured for CP Shelf. + * Specifically, it uses: + * - TV Top Shelf Static Image Still + * - Arcade Product Page Uber Video + * + * @param objectGraph + * @param data + * @param shelfToken + */ + editorialSplashVideoCardForContinuePlaying(objectGraph, data, shelfToken) { + return validation.context("editorialSplashVideoCardForContinuePlaying", () => { + var _a; + const lockupMetricsOptions = { + pageInformation: shelfToken.metricsPageInformation, + locationTracker: shelfToken.metricsLocationTracker, + targetType: "lockup", + }; + const shouldHideArcadeHeader = objectGraph.featureFlags.isEnabled("hide_arcade_header_on_arcade_tab") && + serverData.asBooleanOrFalse(shelfToken.isArcadePage); + const isArcadeLockup = content.isArcadeSupported(objectGraph, data); + const lockupOptions = { + metricsOptions: lockupMetricsOptions, + artworkUseCase: 1 /* content.ArtworkUseCase.LockupIconSmall */, + offerEnvironment: "dark", + offerStyle: "white", + canDisplayArcadeOfferButton: true, + shouldHideArcadeHeader: shouldHideArcadeHeader, + isSubtitleHidden: isArcadeLockup && !shouldHideArcadeHeader, + }; + const video = this.editorialSplashVideoWithTopShelfStill(objectGraph, data); + if (!video || !video.preview) { + return null; + } + const lockup = lockups.lockupFromData(objectGraph, data, lockupOptions); + if (!lockup) { + return null; + } + lockup.clickAction = injectCrossfireFlowForGameCenter(objectGraph, lockup.clickAction); + const clickAction = this.clickActionForVideoCard(objectGraph, data, objectGraph.host.platform, lockupMetricsOptions, shelfToken.clientIdentifierOverride); + if (!clickAction) { + return null; + } + const videoCard = new models.VideoCard(); + videoCard.video = video; + videoCard.lockup = lockup; + videoCard.overlayStyle = "dark"; + videoCard.clickAction = clickAction; + // Set flow preview actions + const metricsClickOptions = metricsHelpersClicks.clickOptionsForLockup(objectGraph, data, lockupMetricsOptions); + videoCard.flowPreviewActionsConfiguration = flowPreview.flowPreviewActionsConfigurationForProductFromData(objectGraph, data, true, shelfToken.clientIdentifierOverride, videoCard.clickAction, lockupMetricsOptions, metricsClickOptions); + // Configure impressions borrowing lockup values for now. + const impressionOptions = metricsHelpersImpressions.impressionOptions(objectGraph, data, lockup.title, lockupMetricsOptions); + metricsHelpersImpressions.addImpressionFields(objectGraph, videoCard, impressionOptions); + (_a = videoCard.impressionMetrics) === null || _a === void 0 ? true : delete _a.fields["impressionIndex"]; + return videoCard; + }); + } + /** + * Return a video to use for continue playing with: + * - Product Uber + * - TV Top Shelf Static Still + */ + editorialSplashVideoWithTopShelfStill(objectGraph, data) { + return validation.context("editorialSplashVideoWithTopShelfStill", () => { + // TV Top Shelf Still (If Any): + let previewOverride = null; + const artworkData = contentAttributes.contentAttributeAsDictionary(objectGraph, data, "editorialArtwork.topShelf"); + if (serverData.isDefinedNonNull(artworkData)) { + previewOverride = content.artworkFromApiArtwork(objectGraph, artworkData, { + withJoeColorPlaceholder: true, + useCase: 23 /* content.ArtworkUseCase.VideoCardStill */, + cropCode: "sr", + }); + } + return content.editorialSplashVideoFromData(objectGraph, data, previewOverride); + }); + } + // endregion + // region Helpers + /** + * Returns the click action to use for given platform for video card + */ + clickActionForVideoCard(objectGraph, data, platform, metricsOptions, clientIdentifierOverride) { + const lockupClickMetricsOptions = metricsHelpersClicks.clickOptionsForLockup(objectGraph, data, metricsOptions); + let productPageAction = lockups.actionFromData(objectGraph, data, lockupClickMetricsOptions, clientIdentifierOverride); + productPageAction = injectCrossfireFlowForGameCenter(objectGraph, productPageAction); + // Wrap in Open for tv only. + if (platform === "tvOS") { + const openAppAction = new models.OpenAppAction(data.id, "app"); + const openAppClickOptions = { + actionType: "open", + id: data.id, + contextualAdamId: data.id, + anonymizationOptions: metricsOptions.anonymizationOptions, + pageInformation: metricsOptions.pageInformation, + locationTracker: metricsOptions.locationTracker, + }; + metricsHelpersClicks.addClickEventToAction(objectGraph, openAppAction, openAppClickOptions); + const stateAction = new models.OfferStateAction(data.id, productPageAction); + stateAction.openAction = openAppAction; + // If the app is downloading and the user clicks this item, take them to the product page instead of cancelling the download. + stateAction.cancelAction = productPageAction; + return stateAction; + } + else { + return productPageAction; + } + } + /** + * The maximum number of games we should ask GameCenterServer for. Note that some of the games it fetches will + * not be Arcade nor platform-compatible games. For this reason, you should ask for more than you need. + * As this call is performant enough with a time limit, we just request the maximum amount. + * + * The maximum that GameCenterServer will accept is 200. + */ + maximumNumberOfRecentGamesToRequest() { + return 200; + } + /** + * The maximum number of games to display on the shelf. + * + * The maximum that Media API will accept is 100. + */ + maximumNumberOfRecentGamesToShow() { + return 10; + } + /** + * Convert GameCategoryFilter to GamesFilter. Ideally these would be the same, but seed 1 has already left the station. + * @param gamesFilter + */ + gameCategoryFilter(gamesFilter) { + if (gamesFilter === "nonArcade") { + return "nonarcade"; + } + return gamesFilter; + } + // endregion + // region Metrics + shelfMetricsOptionsFromBaseMetricsOptions(objectGraph, shelfToken, baseMetricsOptions) { + return { + ...baseMetricsOptions, + badges: { + gameCenter: true, + }, + idType: "its_contentId", + title: objectGraph.loc.string("GameCenter.ContinuePlayingShelf.Title"), + }; + } +} +//# sourceMappingURL=grouping-game-center-continue-playing-shelf-controller.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/grouping/shelf-controllers/grouping-game-center-popular-with-your-friends-shelf-controller.js b/node_modules/@jet-app/app-store/tmp/src/common/grouping/shelf-controllers/grouping-game-center-popular-with-your-friends-shelf-controller.js new file mode 100644 index 0000000..a3eb652 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/grouping/shelf-controllers/grouping-game-center-popular-with-your-friends-shelf-controller.js @@ -0,0 +1,314 @@ +import * as models from "../../../api/models"; +import * as serverData from "../../../foundation/json-parsing/server-data"; +import * as mediaAttributes from "../../../foundation/media/attributes"; +import * as mediaDataFetching from "../../../foundation/media/data-fetching"; +import * as mediaDataStructure from "../../../foundation/media/data-structure"; +import * as mediaNetwork from "../../../foundation/media/network"; +import { ResponseMetadata } from "../../../foundation/network/network"; +import { Parameters, Path, Protocol } from "../../../foundation/network/url-constants"; +import * as urls from "../../../foundation/network/urls"; +import * as contentAttributes from "../../content/attributes"; +import * as content from "../../content/content"; +import * as lockups from "../../lockups/lockups"; +import * as metricsHelpersClicks from "../../metrics/helpers/clicks"; +import * as metricsHelpersLocation from "../../metrics/helpers/location"; +import { GroupingShelfController } from "./grouping-shelf-controller"; +import * as groupingShelfControllerCommon from "./grouping-shelf-controller-common"; +import { makeGameCenterHeader, openGamesUIAction } from "../../arcade/arcade-common"; +export class GroupingGameCenterPopularWithYourFriendsController extends GroupingShelfController { + // region Constructors + constructor() { + super("GroupingGameCenterPopularWithYourFriendsController"); + this.batchGroupKey = "gameCenter"; + this.supportedFeaturedContentIds = new Set([ + 495 /* groupingTypes.FeaturedContentID.AppStore_PopularWithYourFriendsMarker */, + ]); + } + // endregion + // region Shelf Builder + shelfRoute(objectGraph) { + return [ + ...super.shelfRoute(objectGraph), + { + protocol: Protocol.internal, + path: `/${Path.grouping}/${Path.shelf}/{token}`, + query: [Parameters.isGameCenterPopularWithYourFriendsShelf], + }, + ]; + } + // endregion + // region Shelf Creation Prerequisites + /** + * For a given mediaApiData extract the actual shelfContents array needed to render this shelf + * + * @param mediaApiData The outer shelfContents object containing the shelf contents + */ + initialShelfDataFromGroupingMediaApiData(objectGraph, mediaApiData) { + return { + shelfContents: [], + }; + } + /** + * For a given url that this controller handles, we should return a promise that will result in the `ShelfData` + * needed to render this shelf + * + * @param objectGraph The App Store dependency graph + * @param shelfUrl The url that this controller handled on a secondary fetch + * @param parameters The extracted parameters from the shelf url + */ + async secondaryShelfDataForShelfUrl(objectGraph, shelfUrl, shelfToken, parameters) { + const popularWithYourFriendsPromise = objectGraph.gameCenter.fetchGamesPopularWithFriends(this.gameCategoryFilter(shelfToken.gamesFilter), 30); + return await popularWithYourFriendsPromise.then(async (response) => { + const gameplayRecords = response + .map((item) => this.gameplayHistoryFromData(item)) + .sort((a, b) => b.records.length - a.records.length); + const adamIds = gameplayRecords + .filter((record) => this.isCompatibleGameCenterPlatform(objectGraph, record.platformId)) + .map((record) => record.adamId); + if (adamIds.length === 0) { + // One of the few exceptions where TS minifiers cannot derive data container type + // without some help, in this case using explicit type for the value passed to promise. + const emptyShelfData = { shelfContents: [] }; + return await Promise.resolve(emptyShelfData); + } + // <rdar://problem/62490641> Send payload to MAPI to allow reco to reorder the "Popular with friends" shelf. + const request = new mediaDataFetching.Request(objectGraph) + // MAPI will refuse to serve this request if there are more than 100 items. + .withIdsOfType(adamIds.slice(0, 100), "apps") + .includingAgeRestrictions(); + groupingShelfControllerCommon.prepareGroupingShelfRequest(objectGraph, request); + return await mediaNetwork + .fetchData(objectGraph, request, {}) + .then((dataContainer) => { + const shelfData = { + shelfContents: dataContainer.data, + responseTimingValues: dataContainer[ResponseMetadata.timingValues], + }; + return shelfData; + }); + }); + } + /** + * For a given mediaApiData create an updated shelf token that contains all the additional data for this specific shelf type + * + * @param objectGraph The App Store dependency graph + * @param baseShelfToken The base grouping shelf token created by the grouping-controller + * @param mediaApiData The outer data object containing the FC properties and data + * @param groupingParseContext The parse context for the grouping page so far + */ + shelfTokenFromBaseTokenAndMediaApiData(objectGraph, mediaApiData, baseShelfToken, groupingParseContext) { + return baseShelfToken; + } + incompleteShelfFetchStrategy(objectGraph) { + return models.IncompleteShelfFetchStrategy.OnPageLoad; + } + // endregion + // region Shelf Creation + /** + * + * @param objectGraph The App Store dependency graph + * @param shelfToken The shelf shelfToken for this current shelf creation request + * @param shelfData The media api shelfContents array for this shelf + * @param groupingParseContext The parse context used to generate the grouping page on the initial page load, + * this will be missing when this controller renders a secondary or incomplete shelf fetch. + */ + _createShelf(objectGraph, shelfToken, shelfData, groupingParseContext) { + if (shelfToken.isFirstRender) { + return this.pendingPopularWithFriendsShelfForGrouping(objectGraph, shelfData, shelfToken); + } + else { + return this.popularWithFriendsShelfForGrouping(objectGraph, shelfData, shelfToken); + } + } + pendingPopularWithFriendsShelfForGrouping(objectGraph, shelfData, shelfToken) { + const shelf = this.popularWithFriendsShelfForGrouping(objectGraph, shelfData, shelfToken); + const groupingShelfUrl = urls.URL.from(groupingShelfControllerCommon.groupingShelfUrl(shelfToken)); + shelf.url = groupingShelfUrl.param(Parameters.isGameCenterPopularWithYourFriendsShelf, "true").build(); + return shelf; + } + popularWithFriendsShelfForGrouping(objectGraph, shelfData, shelfToken) { + const shelf = this.popularWithFriendsShelf(objectGraph, shelfData.shelfContents, shelfToken); + shelf.mergeWhenFetched = true; + shelf.batchGroup = this.batchGroupKey; + // Hide when empty. + shelf.isHidden = shelf.items.length === 0; + // Configure header + shelf.header.title = shelfToken.title; + shelf.header.subtitle = shelfToken.subtitle; + return shelf; + } + popularWithFriendsShelf(objectGraph, shelfContents, shelfToken) { + const shelfStyle = shelfToken.shelfStyle || "mediumLockup"; + const shelf = new models.Shelf(shelfStyle); + shelf.isHorizontal = true; + const maxNumberOfPlayersBeforeSeeAll = objectGraph.client.isTV ? 20 : 12; + const items = []; + for (let index = 0; index < shelfContents.length; index++) { + const data = shelfContents[index]; + const lockupOptions = { + metricsOptions: { + pageInformation: shelfToken.metricsPageInformation, + locationTracker: shelfToken.metricsLocationTracker, + recoMetricsData: mediaDataStructure.metricsFromMediaApiObject(data), + anonymizationOptions: { + anonymizationString: `"GAME"${index + 1}`, + }, + }, + artworkUseCase: content.artworkUseCaseFromShelfStyle(objectGraph, shelfStyle), + canDisplayArcadeOfferButton: content.shelfContentTypeCanDisplayArcadeOfferButtons(objectGraph, shelfStyle), + shouldHideArcadeHeader: objectGraph.featureFlags.isEnabled("hide_arcade_header_on_arcade_tab") && shelfToken.isArcadePage, + shouldShowFriendsPlayingShowcase: true, + }; + // GameCenter should *not* be sending us IDs for non-GameCenter apps, but in the odd case that they do, we + // we check against the app's metadata to make sure we are displaying GC apps only here. + const isGameCenterEnabled = contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "isGameCenterEnabled"); + // It's possible that a pre-order has become available to friends, but not you yet (due to CDN/timezone reasons). + const isPreorder = mediaAttributes.attributeAsBooleanOrFalse(data, "isPreorder"); + if (isPreorder || !isGameCenterEnabled) { + continue; + } + const lockup = lockups.lockupFromData(objectGraph, data, lockupOptions); + lockup.clickAction = injectCrossfireFlowForGameCenter(objectGraph, lockup.clickAction); + if (serverData.isDefinedNonNull(lockup)) { + items.push(lockup); + metricsHelpersLocation.nextPosition(lockupOptions.metricsOptions.locationTracker); + } + } + let thresholdForPlatform; + switch (objectGraph.client.deviceType) { + case "phone": + thresholdForPlatform = 2; + break; + case "pad": + thresholdForPlatform = 6; + break; + case "mac": + thresholdForPlatform = 6; + break; + case "tv": + thresholdForPlatform = 6; + break; + default: + thresholdForPlatform = 0; + } + shelf.header = makeGameCenterHeader(objectGraph); + if (items.length < thresholdForPlatform) { + shelf.isHidden = true; + return shelf; + } + shelf.items = items.slice(0, maxNumberOfPlayersBeforeSeeAll); + shelf.isHidden = false; + shelf.batchGroup = "gameCenter"; + if (items.length > maxNumberOfPlayersBeforeSeeAll) { + // The shelf for the see all page + const shelfForSeeAllItems = new models.Shelf("mediumLockup"); + shelfForSeeAllItems.items = items; + shelfForSeeAllItems.rowsPerColumn = 1; + // See all page + const seeAllPage = new models.GenericPage([shelfForSeeAllItems]); + seeAllPage.title = shelfToken.title; + // The action that opens the see all page + const seeAllAction = new models.FlowAction("page"); + seeAllAction.title = objectGraph.loc.string("ACTION_SEE_ALL"); + seeAllAction.pageData = seeAllPage; + // Metrics + metricsHelpersClicks.addClickEventToSeeAllAction(objectGraph, seeAllAction, null, { + pageInformation: shelfToken.metricsPageInformation, + locationTracker: shelfToken.metricsLocationTracker, + }); + // Connect the shelf's seeAllAction + groupingShelfControllerCommon.replaceShelfSeeAllAction(objectGraph, shelf, seeAllAction); + } + shelf.footerTitle = objectGraph.loc.string("Lockup.Footer.GamesApp"); + shelf.footerAction = openGamesUIAction(objectGraph); + shelf.footerStyle = { + $kind: "games", + bundleID: "com.apple.games", + width: 16, + height: 16, + }; + return shelf; + } + // endregion + // region Helpers + /** + * Maps GKGamePlatform to client's `deviceType` + * @param objectGraph + * @param platformId The platform ID as defined by GameCenter's GKGamePlatform + */ + isCompatibleGameCenterPlatform(objectGraph, platformId) { + switch (platformId) { + case 1: + return objectGraph.client.isiOS; + case 2: + return objectGraph.client.isMac; + case 3: + return objectGraph.client.isTV; + case 4: + return objectGraph.client.isWatch; + default: + return false; + } + } + gameplayHistoryFromData(data) { + const adamId = serverData.asString(data, "adamId"); + const platformId = serverData.asNumber(data, "platformId"); + const isArcade = serverData.asBooleanOrFalse(data, "isArcade"); + const records = this.gameplayHistoryRecordFromData(serverData.asArrayOrEmpty(data, "records")); + return new models.GameCenterGameplayHistory(adamId, platformId, isArcade, records); + } + gameplayHistoryRecordFromData(data) { + return data.map((recordData) => { + const playerId = serverData.asString(recordData, "playerId"); + const timestamp = serverData.asNumber(recordData, "timestamp"); + return new models.GameCenterGameplayHistoryRecord(playerId, timestamp); + }); + } + /** + * Convert GameCategoryFilter to GamesFilter. Ideally these would be the same, but seed 1 has already left the station. + * @param gamesFilter + */ + gameCategoryFilter(gamesFilter) { + if (gamesFilter === "nonArcade") { + return "nonarcade"; + } + return gamesFilter; + } + // endregion + // region Metrics + shelfMetricsOptionsFromBaseMetricsOptions(objectGraph, shelfToken, baseMetricsOptions) { + return { + ...baseMetricsOptions, + badges: { + gameCenter: true, + }, + idType: "its_contentId", + }; + } +} +/** + * For evaluating how we attribute referral from Game Center shelves to the product page + * We going to inject a hardcoded refApp `com.apple.gamecenter.from.browse` through crossfire pipeline for tracking the page event and buy action + * + * - Modify the FlowAction to include the GameCenter ReferrerData. + * - Replace the click action with a compound action of CrossfireReferralAction + FlowAction + */ +export function injectCrossfireFlowForGameCenter(objectGraph, clickAction) { + if (["iOS", "macOS", "tvOS"].includes(objectGraph.host.platform) && clickAction instanceof models.FlowAction) { + const gameCenterCrossfireReferrerData = { + app: "com.apple.gamecenter.from.browse", + kind: { + name: "gameCenter", + }, + }; + clickAction.referrerData = gameCenterCrossfireReferrerData; + return new models.CompoundAction([ + new models.CrossfireReferralAction(gameCenterCrossfireReferrerData), + clickAction, + ]); + } + else { + return clickAction; + } +} +//# sourceMappingURL=grouping-game-center-popular-with-your-friends-shelf-controller.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/grouping/shelf-controllers/grouping-game-center-reengagement-shelf-controller.js b/node_modules/@jet-app/app-store/tmp/src/common/grouping/shelf-controllers/grouping-game-center-reengagement-shelf-controller.js new file mode 100644 index 0000000..f4b3d74 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/grouping/shelf-controllers/grouping-game-center-reengagement-shelf-controller.js @@ -0,0 +1,254 @@ +import * as validation from "@jet/environment/json/validation"; +import * as models from "../../../api/models"; +import * as actions from "../../../api/models/actions"; +import * as serverData from "../../../foundation/json-parsing/server-data"; +import * as mediaDataFetching from "../../../foundation/media/data-fetching"; +import * as mediaNetwork from "../../../foundation/media/network"; +import { ResponseMetadata } from "../../../foundation/network/network"; +import { Parameters, Path, Protocol } from "../../../foundation/network/url-constants"; +import * as urls from "../../../foundation/network/urls"; +import * as color from "../../../foundation/util/color-util"; +import * as content from "../../content/content"; +import * as lockups from "../../lockups/lockups"; +import * as metricsHelpersClicks from "../../metrics/helpers/clicks"; +import * as metricsHelpersImpressions from "../../metrics/helpers/impressions"; +import * as metricsHelpersLocation from "../../metrics/helpers/location"; +import { GroupingShelfController } from "./grouping-shelf-controller"; +import * as groupingShelfControllerCommon from "./grouping-shelf-controller-common"; +import { injectCrossfireFlowForGameCenter } from "./grouping-game-center-popular-with-your-friends-shelf-controller"; +export class GroupingGameCenterReengagementShelfController extends GroupingShelfController { + // region Constructors + constructor() { + super("GroupingGameCenterReengagementShelfController"); + this.batchGroupKey = "gameCenter"; + this.supportedFeaturedContentIds = new Set([494 /* groupingTypes.FeaturedContentID.AppStore_GameCenterReengagement */]); + } + // endregion + // region Shelf Builder + shelfRoute(objectGraph) { + return [ + ...super.shelfRoute(objectGraph), + { + protocol: Protocol.internal, + path: `/${Path.grouping}/${Path.shelf}/{token}`, + query: [Parameters.isGameCenterReengagementShelf], + }, + ]; + } + // endregion + // region Shelf Creation Prerequisites + /** + * For a given mediaApiData extract the actual shelfContents array needed to render this shelf + * + * @param mediaApiData The outer shelfContents object containing the shelf contents + */ + initialShelfDataFromGroupingMediaApiData(objectGraph, mediaApiData) { + return { + shelfContents: [], + achievementData: null, + achievementSummaryData: null, + }; + } + /** + * For a given url that this controller handles, we should return a promise that will result in the `ShelfData` + * needed to render this shelf + * + * @param objectGraph The App Store dependency graph + * @param shelfUrl The url that this controller handled on a secondary fetch + * @param parameters The extracted parameters from the shelf url + */ + async secondaryShelfDataForShelfUrl(objectGraph, shelfUrl, shelfToken, parameters) { + return await objectGraph.gameCenter.fetchRengagementDataForLocalPlayer().then(async (reengagementData) => { + const adamID = serverData.asString(reengagementData, "adamId"); + const achievement = serverData.asJSONData(reengagementData["achievement"]); + const achievementSummary = serverData.asJSONData(reengagementData["achievementSummary"]); + if (serverData.isNullOrEmpty(adamID)) { + // One of the few exceptions where TS minifiers cannot derive data container type + // without some help, in this case using explicit type for the value passed to promise. + const emptyShelfData = { + shelfContents: [], + responseTimingValues: null, + achievementData: null, + achievementSummaryData: null, + }; + return await Promise.resolve(emptyShelfData); + } + const request = new mediaDataFetching.Request(objectGraph) + .withIdOfType(adamID, "apps") + .includingAgeRestrictions(); + groupingShelfControllerCommon.prepareGroupingShelfRequest(objectGraph, request); + return await mediaNetwork + .fetchData(objectGraph, request, {}) + .then((dataContainer) => { + return { + shelfContents: dataContainer.data, + responseTimingValues: dataContainer[ResponseMetadata.timingValues], + achievementData: achievement, + achievementSummaryData: achievementSummary, + }; + }); + }); + } + /** + * For a given mediaApiData create an updated shelf token that contains all the additional data for this specific shelf type + * + * @param objectGraph The App Store dependency graph + * @param baseShelfToken The base grouping shelf token created by the grouping-controller + * @param mediaApiData The outer data object containing the FC properties and data + * @param groupingParseContext The parse context for the grouping page so far + */ + shelfTokenFromBaseTokenAndMediaApiData(objectGraph, mediaApiData, baseShelfToken, groupingParseContext) { + return baseShelfToken; + } + // endregion + // region Shelf Creation + /** + * + * @param objectGraph The App Store dependency graph + * @param shelfToken The shelf shelfToken for this current shelf creation request + * @param shelfData The media api shelfContents array for this shelf + * @param groupingParseContext The parse context used to generate the grouping page on the initial page load, + * this will be missing when this controller renders a secondary or incomplete shelf fetch. + */ + _createShelf(objectGraph, shelfToken, shelfData, groupingParseContext) { + if (shelfToken.isFirstRender) { + return this.pendingGameCenterReengagementShelf(objectGraph, shelfData, shelfToken, groupingParseContext === null || groupingParseContext === void 0 ? void 0 : groupingParseContext.isArcadePage); + } + else { + return this.gameCenterReengagementShelf(objectGraph, shelfData, shelfToken, groupingParseContext === null || groupingParseContext === void 0 ? void 0 : groupingParseContext.isArcadePage); + } + } + pendingGameCenterReengagementShelf(objectGraph, shelfData, shelfToken, isArcadePage) { + const shelf = this.gameCenterReengagementShelf(objectGraph, shelfData, shelfToken, isArcadePage); + if (!shelf) { + return null; + } + const groupingShelfUrl = urls.URL.from(groupingShelfControllerCommon.groupingShelfUrl(shelfToken)); + shelf.url = groupingShelfUrl.param(Parameters.isGameCenterReengagementShelf, "true").build(); + shelf.batchGroup = this.batchGroupKey; + return shelf; + } + gameCenterReengagementShelf(objectGraph, shelfData, shelfToken, isArcadePage = false) { + return validation.context("gameCenterReengagementShelf", () => { + if (!serverData.isDefinedNonNullNonEmpty(shelfData.shelfContents)) { + return null; + } + const shelf = new models.Shelf("gameCenterReengagement"); + shelf.isHorizontal = false; + shelf.mergeWhenFetched = false; // Always replace + shelf.batchGroup = this.batchGroupKey; + const heroMetricsOptions = { + id: shelfToken.id, + kind: null, + softwareType: isArcadePage ? "Arcade" : null, + targetType: "achievements", + title: "Achievements Hero", + pageInformation: shelfToken.metricsPageInformation, + locationTracker: shelfToken.metricsLocationTracker, + idType: "its_contentId", + fcKind: shelfToken.featuredContentId, + badges: { + gameCenter: true, + }, + }; + // Hero Background / Artwork + const artwork = content.productEditorialVideoFromData(objectGraph, shelfData.shelfContents[0], 21 /* content.ArtworkUseCase.Uber */); + let backgroundColor = color.named("componentBackground"); + let preview = null; + if (serverData.isDefinedNonNullNonEmpty(artwork)) { + preview = artwork.preview; + backgroundColor = preview.backgroundColor; + } + // Build lockup used by reegnagement shelf + const lockupListOptions = { + lockupOptions: { + metricsOptions: { + pageInformation: shelfToken.metricsPageInformation, + locationTracker: shelfToken.metricsLocationTracker, + }, + offerStyle: "white", + artworkUseCase: content.artworkUseCaseFromShelfStyle(objectGraph, "smallLockup"), + isSubtitleHidden: true, + }, + }; + metricsHelpersLocation.pushContentLocation(objectGraph, heroMetricsOptions, heroMetricsOptions.title); + const heroLockup = lockups.lockupsFromData(objectGraph, shelfData.shelfContents, lockupListOptions)[0]; + heroLockup.clickAction = injectCrossfireFlowForGameCenter(objectGraph, heroLockup.clickAction); + metricsHelpersLocation.popLocation(shelfToken.metricsLocationTracker); + // Click action used by hero view (achievement card) + let heroAction = null; + if (serverData.isDefinedNonNullNonEmpty(heroLockup)) { + heroAction = new actions.GameCenterAchievementsAction(heroLockup.bundleId); + heroAction.title = "Achievements Hero"; // `addClickEventToAction` uses `title` for `name` field currently. + metricsHelpersClicks.addClickEventToAction(objectGraph, heroAction, heroMetricsOptions); + } + const shelfBadge = objectGraph.loc.string("GameCenter.Reengagement.Badge.GameCenter"); + const achievement = this.achievementFromData(objectGraph, shelfData.achievementData); + const achievementCounts = this.achievementCountsFromData(objectGraph, shelfData.achievementSummaryData); + const shelfMetadata = this.shelfMetadataForAchievement(objectGraph, achievement, achievementCounts); + const heroItem = new models.GameCenterReengagement("gamecenter.fill", shelfBadge, shelfMetadata.title, shelfMetadata.subtitle, achievement, heroLockup, backgroundColor, preview, heroAction); + shelf.items = [heroItem]; + /** + * For reengagement, the shelf is a container fully fulled by `heroItem`. + * Match what breakouts do by impressing `heroItem` directly instead of the `shelf`. + */ + metricsHelpersImpressions.addImpressionFields(objectGraph, heroItem, heroMetricsOptions); + return shelf; + }); + } + // region Helpers + achievementStatusFromData(objectGraph, data) { + const statusType = serverData.asString(data, "type"); + const status = new models.GameCenterAchievementStatus(statusType); + status.percent = serverData.asNumber(data, "percent"); + status.date = serverData.asString(data, "date"); + status.artwork = new models.Artwork(serverData.asString(data, "artwork.template"), serverData.asNumber(data, "artwork.width"), serverData.asNumber(data, "artwork.height"), []); + return status; + } + achievementFromData(objectGraph, data) { + const id = serverData.asString(data, "id"); + const title = serverData.asString(data, "title"); + const subtitle = serverData.asString(data, "subtitle"); + const status = this.achievementStatusFromData(objectGraph, serverData.asDictionary(data, "status")); + return new models.GameCenterAchievement(id, title, subtitle, status); + } + achievementCountsFromData(objectGraph, data) { + const completedAchievements = serverData.asNumber(data, "completedAchievements"); + const totalAchievements = serverData.asNumber(data, "totalAchievements"); + return { completed: completedAchievements, total: totalAchievements }; + } + shelfMetadataForAchievement(objectGraph, achievement, achievementCounts) { + if (!serverData.isDefinedNonNull(achievement)) { + return { title: "", subtitle: null }; + } + if (achievementCounts.completed === 0) { + return { + title: objectGraph.loc.string("GameCenter.Reengagement.Achievement.First.Title"), + subtitle: objectGraph.loc.string("GameCenter.Reengagement.Achievement.First.Subtitle"), + }; + } + switch (achievement.status.type) { + case "locked": + case "hidden": + case "inprogress": + return { + title: objectGraph.loc.string("GameCenter.Reengagement.Achievement.KeepPlaying.Title"), + subtitle: objectGraph.loc.string("GameCenter.Reengagement.Achievement.KeepPlaying.Subtitle"), + }; + case "completed": + const achievementsTotalCount = objectGraph.loc.stringWithCount("GameCenter.AchievementSummary.TotalToCompleteCount", achievementCounts.total); + const achievementsCompletedCount = objectGraph.loc.stringWithCount("GameCenter.AchievementSummary.NumberCompletedCount", achievementCounts.completed); + const subtitle = objectGraph.loc + .string("GameCenter.AchievementSummary.CompletedCount.Subtitle") + .replace("@@completedCount@@", achievementsCompletedCount) + .replace("@@totalCount@@", achievementsTotalCount); + return { + title: objectGraph.loc.string("GameCenter.Reengagement.Achievement.CompletedCount.Title"), + subtitle: subtitle, + }; + default: + return { title: "", subtitle: null }; + } + } +} +//# sourceMappingURL=grouping-game-center-reengagement-shelf-controller.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/grouping/shelf-controllers/grouping-game-center-suggested-friends-shelf-controller.js b/node_modules/@jet-app/app-store/tmp/src/common/grouping/shelf-controllers/grouping-game-center-suggested-friends-shelf-controller.js new file mode 100644 index 0000000..e9eb3f4 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/grouping/shelf-controllers/grouping-game-center-suggested-friends-shelf-controller.js @@ -0,0 +1,279 @@ +import * as models from "../../../api/models"; +import * as actions from "../../../api/models/actions"; +import { Parameters, Path, Protocol } from "../../../foundation/network/url-constants"; +import * as urls from "../../../foundation/network/urls"; +import { makeGameCenterHeader, openGamesUIAction } from "../../arcade/arcade-common"; +import * as metricsHelpersClicks from "../../metrics/helpers/clicks"; +import * as metricsHelpersImpressions from "../../metrics/helpers/impressions"; +import * as metricsHelpersLocation from "../../metrics/helpers/location"; +import { GroupingShelfController } from "./grouping-shelf-controller"; +import * as groupingShelfControllerCommon from "./grouping-shelf-controller-common"; +export class GroupingGameCenterSuggestedFriendsController extends GroupingShelfController { + // region Constructors + constructor() { + super("GroupingGameCenterSuggestedFriendsController"); + this.batchGroupKey = "gameCenter"; + this.supportedFeaturedContentIds = new Set([496 /* groupingTypes.FeaturedContentID.AppStore_SuggestedFriendsMarker */]); + } + // endregion + // region Shelf Builder + shelfRoute(objectGraph) { + return [ + ...super.shelfRoute(objectGraph), + { + protocol: Protocol.internal, + path: `/${Path.grouping}/${Path.shelf}/{token}`, + query: [Parameters.isGameCenterSuggestedFriendsShelf], + }, + ]; + } + // endregion + // region Shelf Creation Prerequisites + /** + * For a given mediaApiData extract the actual shelfContents array needed to render this shelf + * + * @param mediaApiData The outer shelfContents object containing the shelf contents + */ + initialShelfDataFromGroupingMediaApiData(objectGraph, mediaApiData) { + return { + $kind: "friendingViaPush", + shelfContents: [], + suggestedFriends: [], + }; + } + /** + * For a given url that this controller handles, we should return a promise that will result in the `ShelfData` + * needed to render this shelf + * + * @param objectGraph The App Store dependency graph + * @param shelfUrl The url that this controller handled on a secondary fetch + * @param parameters The extracted parameters from the shelf url + */ + async secondaryShelfDataForShelfUrl(objectGraph, shelfUrl, shelfToken, parameters) { + return await objectGraph.gameCenter.fetchSuggestedFriends(10).then((suggestions) => { + if (objectGraph.props.enabled("gameCenterFriendingViaPush")) { + return { + $kind: "friendingViaPush", + shelfContents: [], + suggestedFriends: suggestions, + }; + } + else { + return { + $kind: "legacy", + shelfContents: [], + suggestedFriends: suggestions, + }; + } + }); + } + /** + * For a given mediaApiData create an updated shelf token that contains all the additional data for this specific shelf type + * + * @param objectGraph The App Store dependency graph + * @param baseShelfToken The base grouping shelf token created by the grouping-controller + * @param mediaApiData The outer data object containing the FC properties and data + * @param groupingParseContext The parse context for the grouping page so far + */ + shelfTokenFromBaseTokenAndMediaApiData(objectGraph, mediaApiData, baseShelfToken, groupingParseContext) { + return baseShelfToken; + } + incompleteShelfFetchStrategy(objectGraph) { + return models.IncompleteShelfFetchStrategy.OnPageLoad; + } + // endregion + // region Shelf Creation + /** + * + * @param objectGraph The App Store dependency graph + * @param shelfToken The shelf shelfToken for this current shelf creation request + * @param shelfData The media api shelfContents array for this shelf + * @param groupingParseContext The parse context used to generate the grouping page on the initial page load, + * this will be missing when this controller renders a secondary or incomplete shelf fetch. + */ + _createShelf(objectGraph, shelfToken, shelfData, groupingParseContext) { + if (shelfToken.isFirstRender) { + return this.pendingSuggestedFriendsShelfForGrouping(objectGraph, shelfToken, groupingParseContext === null || groupingParseContext === void 0 ? void 0 : groupingParseContext.isArcadePage); + } + else { + return this.suggestedFriendsShelfForGrouping(objectGraph, shelfData, shelfToken, groupingParseContext === null || groupingParseContext === void 0 ? void 0 : groupingParseContext.isArcadePage); + } + } + pendingSuggestedFriendsShelfForGrouping(objectGraph, shelfToken, isArcadePage) { + if (objectGraph.client.deviceType !== "phone" && objectGraph.client.deviceType !== "pad") { + return null; + } + const shelf = this.suggestedFriendsShelfForGrouping(objectGraph, { + $kind: objectGraph.props.enabled("gameCenterFriendingViaPush") ? "friendingViaPush" : "legacy", + shelfContents: [], + suggestedFriends: [], + }, shelfToken, isArcadePage); + const groupingShelfUrl = urls.URL.from(groupingShelfControllerCommon.groupingShelfUrl(shelfToken)); + shelf.url = groupingShelfUrl.param(Parameters.isGameCenterSuggestedFriendsShelf, "true").build(); + shelf.isHidden = shelf.items.length === 0; + shelf.batchGroup = this.batchGroupKey; + return shelf; + } + suggestedFriendsShelfForGrouping(objectGraph, shelfData, shelfToken, isArcadePage) { + if (objectGraph.client.deviceType !== "phone" && objectGraph.client.deviceType !== "pad") { + return null; + } + let shelf; + if (shelfData.$kind === "friendingViaPush") { + shelf = this.suggestedFriendsShelf(objectGraph, shelfData.suggestedFriends, shelfToken, isArcadePage); + } + else { + shelf = this.legacySuggestedFriendsShelf(objectGraph, shelfData.suggestedFriends, shelfToken, isArcadePage); + } + shelf.header = makeGameCenterHeader(objectGraph, shelfToken.title, shelfToken.subtitle); + shelf.batchGroup = this.batchGroupKey; + // Hide when empty. + shelf.isHidden = shelf.items.length === 0; + shelf.footerTitle = objectGraph.loc.string("Lockup.Footer.GamesApp"); + shelf.footerAction = openGamesUIAction(objectGraph); + shelf.footerStyle = { + $kind: "games", + bundleID: "com.apple.games", + width: 16, + height: 16, + }; + return shelf; + } + // region Helpers + suggestedFriendsShelf(objectGraph, suggestions, token, isArcadePage = false) { + const suggestionPrefix = "FRIEND_SUGGESTION"; + if (objectGraph.client.deviceType !== "phone" && objectGraph.client.deviceType !== "pad") { + return null; + } + const shelf = new models.Shelf("smallContactCard"); + shelf.isHorizontal = true; + shelf.mergeWhenFetched = true; + shelf.batchGroup = "gameCenter"; + const enrichedSuggestions = []; + for (let index = 0; index < suggestions.length; index++) { + const suggestionId = `${suggestionPrefix}${index + 1}`; + const suggestedFriend = suggestions[index]; + const buttonText = objectGraph.loc.string("INVITE"); + const subtitle = objectGraph.loc.string("FROM_CONTACTS"); + const metricsClickOptions = { + pageInformation: token.metricsPageInformation, + locationTracker: token.metricsLocationTracker, + id: suggestionId, + anonymizationOptions: { + anonymizationString: suggestionId, + }, + }; + let invitationType; + let shouldShowMessagesBadge; + if (suggestedFriend.supportsFriendingViaPush && suggestedFriend.contactAssociationID) { + invitationType = { + contact: { + contactID: suggestedFriend.contactID, + contactAssociationID: suggestedFriend.contactAssociationID, + }, + }; + shouldShowMessagesBadge = false; + } + else { + invitationType = { + messages: { + contactID: suggestedFriend.contactID, + }, + }; + shouldShowMessagesBadge = true; + } + const buttonAction = new actions.GameCenterInvitePlayerAction(invitationType); + metricsHelpersClicks.addClickEventToAction(objectGraph, buttonAction, { + ...metricsClickOptions, + actionType: "inviteFriend", + }); + const removeButtonAction = new actions.GameCenterDenylistPlayerAction(suggestedFriend.contactID); + metricsHelpersClicks.addClickEventToAction(objectGraph, removeButtonAction, { + ...metricsClickOptions, + actionType: "removeFriendSuggestion", + }); + const metricsImpressionOptions = { + pageInformation: token.metricsPageInformation, + locationTracker: token.metricsLocationTracker, + title: suggestionId, + id: suggestionId, + kind: "friendSuggestion", + softwareType: isArcadePage ? "Arcade" : null, + anonymizationOptions: { + anonymizationString: suggestionId, + }, + }; + const enrichedSuggestion = new models.SmallContactCard(suggestedFriend.contactID, suggestedFriend.fullName, subtitle, buttonText, suggestedFriend.contactID, buttonAction, removeButtonAction, shouldShowMessagesBadge); + metricsHelpersImpressions.addImpressionFields(objectGraph, enrichedSuggestion, metricsImpressionOptions); + enrichedSuggestions.push(enrichedSuggestion); + metricsHelpersLocation.nextPosition(token.metricsLocationTracker); + } + shelf.items = enrichedSuggestions; + shelf.isHidden = shelf.items.length === 0; + return shelf; + } + legacySuggestedFriendsShelf(objectGraph, cards, token, isArcadePage = false) { + const suggestionPrefix = "FRIEND_SUGGESTION"; + if (objectGraph.client.deviceType !== "phone" && objectGraph.client.deviceType !== "pad") { + return null; + } + const shelf = new models.Shelf("smallContactCard"); + shelf.isHorizontal = true; + shelf.mergeWhenFetched = true; + shelf.batchGroup = "gameCenter"; + const enrichedSuggestions = []; + for (let index = 0; index < cards.length; index++) { + const suggestionId = `${suggestionPrefix}${index + 1}`; + const smallContactCard = cards[index]; + smallContactCard.buttonText = objectGraph.loc.string("INVITE"); + smallContactCard.subtitle = objectGraph.loc.string("FROM_CONTACTS"); + const metricsClickOptions = { + pageInformation: token.metricsPageInformation, + locationTracker: token.metricsLocationTracker, + id: suggestionId, + anonymizationOptions: { + anonymizationString: suggestionId, + }, + }; + smallContactCard.buttonAction = new actions.LegacyGameCenterInvitePlayerAction(smallContactCard.contactId); + metricsHelpersClicks.addClickEventToAction(objectGraph, smallContactCard.buttonAction, { + ...metricsClickOptions, + actionType: "inviteFriend", + }); + smallContactCard.removeButtonAction = new actions.GameCenterDenylistPlayerAction(smallContactCard.contactId); + metricsHelpersClicks.addClickEventToAction(objectGraph, smallContactCard.removeButtonAction, { + ...metricsClickOptions, + actionType: "removeFriendSuggestion", + }); + const metricsImpressionOptions = { + pageInformation: token.metricsPageInformation, + locationTracker: token.metricsLocationTracker, + title: suggestionId, + id: suggestionId, + kind: "friendSuggestion", + softwareType: isArcadePage ? "Arcade" : null, + anonymizationOptions: { + anonymizationString: suggestionId, + }, + }; + metricsHelpersImpressions.addImpressionFields(objectGraph, smallContactCard, metricsImpressionOptions); + enrichedSuggestions.push(smallContactCard); + metricsHelpersLocation.nextPosition(token.metricsLocationTracker); + } + shelf.items = enrichedSuggestions; + shelf.isHidden = shelf.items.length === 0; + return shelf; + } + // endregion + // region Metrics + shelfMetricsOptionsFromBaseMetricsOptions(objectGraph, shelfToken, baseMetricsOptions) { + return { + ...baseMetricsOptions, + badges: { + gameCenter: true, + }, + idType: "its_contentId", + }; + } +} +//# sourceMappingURL=grouping-game-center-suggested-friends-shelf-controller.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/grouping/shelf-controllers/grouping-hero-carousel-shelf-controller.js b/node_modules/@jet-app/app-store/tmp/src/common/grouping/shelf-controllers/grouping-hero-carousel-shelf-controller.js new file mode 100644 index 0000000..b7a406c --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/grouping/shelf-controllers/grouping-hero-carousel-shelf-controller.js @@ -0,0 +1,182 @@ +import * as models from "../../../api/models"; +import * as serverData from "../../../foundation/json-parsing/server-data"; +import * as mediaAttributes from "../../../foundation/media/attributes"; +import * as mediaDataStructure from "../../../foundation/media/data-structure"; +import * as mediaRelationship from "../../../foundation/media/relationships"; +import * as breakoutsCommon from "../../arcade/breakouts-common"; +import * as contentAttributes from "../../content/attributes"; +import * as content from "../../content/content"; +import * as metricsHelpersClicks from "../../metrics/helpers/clicks"; +import * as metricsHelpersImpressions from "../../metrics/helpers/impressions"; +import * as metricsHelpersLocation from "../../metrics/helpers/location"; +import * as article from "../../today/article"; +import * as heroCarouselOverlayCommon from "../hero/hero-carousel-overlay-common"; +import * as heroCommon from "../hero/hero-common"; +import { GroupingShelfController } from "./grouping-shelf-controller"; +import * as groupingShelfControllerCommon from "./grouping-shelf-controller-common"; +export class GroupingHeroCarouselShelfController extends GroupingShelfController { + // region Constructors + constructor() { + super("GroupingHeroCarouselShelfController"); + this.supportedFeaturedContentIds = new Set([480 /* groupingTypes.FeaturedContentID.AppStore_Breakout */]); + } + // endregion + // region GroupingShelfController + _supports(objectGraph, mediaApiData, featuredContentId, nativeGroupingShelfId) { + if (!super._supports(objectGraph, mediaApiData, featuredContentId, nativeGroupingShelfId)) { + return false; + } + const breakoutStyle = mediaAttributes.attributeAsString(mediaApiData, "displayStyle"); + return breakoutStyle === "hero"; + } + // endregion + // region Shelf Creation Prerequisites + /** + * For a given mediaApiData extract the actual shelfContents array needed to render this shelf + * + * @param mediaApiData The outer shelfContents object containing the shelf contents + */ + initialShelfDataFromGroupingMediaApiData(objectGraph, mediaApiData) { + return { shelfContents: mediaRelationship.relationshipCollection(mediaApiData, "contents") }; + } + /** + * For a given url that this controller handles, we should return a promise that will result in the `ShelfData` + * needed to render this shelf + * + * @param objectGraph The App Store dependency graph + * @param shelfUrl The url that this controller handled on a secondary fetch + * @param parameters The extracted parameters from the shelf url + */ + async secondaryShelfDataForShelfUrl(objectGraph, shelfUrl, shelfToken, parameters) { + return await GroupingShelfController.secondaryGroupingShelfDataForShelfUrl(objectGraph, shelfUrl, shelfToken, parameters); + } + /** + * For a given mediaApiData create an updated shelf token that contains all the additional data for this specific shelf type + * + * @param objectGraph The App Store dependency graph + * @param baseShelfToken The base grouping shelf token created by the grouping-controller + * @param mediaApiData The outer data object containing the FC properties and data + * @param groupingParseContext The parse context for the grouping page so far + */ + shelfTokenFromBaseTokenAndMediaApiData(objectGraph, mediaApiData, baseShelfToken, groupingParseContext) { + return baseShelfToken; + } + // endregion + // region Metrics + shouldImpressShelf() { + return false; + } + // endregion + // region Shelf Creation + /** + * + * @param objectGraph The App Store dependency graph + * @param shelfToken The shelf shelfToken for this current shelf creation request + * @param shelfData The media api shelfContents array for this shelf + * @param groupingParseContext The parse context used to generate the grouping page on the initial page load, + * this will be missing when this controller renders a secondary or incomplete shelf fetch. + */ + _createShelf(objectGraph, shelfToken, shelfData, groupingParseContext) { + if (groupingParseContext.shelves.length !== 0) { + return null; + } + const shelf = new models.Shelf("heroCarousel"); + const mediaApiData = shelfToken.featuredContentData; + const heroCarouselMetricsOptions = { + targetType: "swoosh", + pageInformation: shelfToken.metricsPageInformation, + locationTracker: shelfToken.metricsLocationTracker, + recoMetricsData: mediaDataStructure.metricsFromMediaApiObject(mediaApiData), + }; + const heroCarousel = new models.HeroCarousel(); + heroCarousel.autoScrollConfiguration = { + isAutoScrollEnabled: objectGraph.bag.heroCarouselAutoScrollDuration > 0.0, + autoScrollInterval: objectGraph.bag.heroCarouselAutoScrollDuration, + }; + const heroCarouselImpressionOptions = metricsHelpersImpressions.impressionOptions(objectGraph, mediaApiData, "heroCarousel", heroCarouselMetricsOptions); + heroCarouselImpressionOptions.autoAdvanceInterval = heroCarousel.autoScrollConfiguration.autoScrollInterval; + metricsHelpersImpressions.addImpressionFields(objectGraph, shelf, heroCarouselImpressionOptions); + metricsHelpersLocation.pushContentLocation(objectGraph, heroCarouselImpressionOptions, "heroCarousel"); + for (const itemData of shelfData.shelfContents) { + if (serverData.isNull(itemData.attributes) || groupingShelfControllerCommon.shouldDefer(shelfToken)) { + shelfToken.isDeferring = true; + shelfToken.remainingItems.push(itemData); + continue; + } + const primaryContent = content.primaryContentForData(objectGraph, itemData); + if (breakoutsCommon.requiresPrimaryContent(objectGraph, itemData) && + !mediaAttributes.hasAttributes(primaryContent)) { + shelfToken.isDeferring = true; + shelfToken.remainingItems.push(itemData); + shelfToken.relationshipToFetch = "primary-content"; + continue; + } + const heroCarouselItemMetricsOptions = { + targetType: "largeBreakout", + pageInformation: shelfToken.metricsPageInformation, + locationTracker: shelfToken.metricsLocationTracker, + recoMetricsData: mediaDataStructure.metricsFromMediaApiObject(itemData), + }; + const heroVideoData = heroCommon.heroVideoFromData(objectGraph, itemData); + const heroArtworkData = heroCommon.heroArtworkFromData(objectGraph, itemData); + if (serverData.isNullOrEmpty(heroVideoData.video) && serverData.isNullOrEmpty(heroArtworkData.artwork)) { + continue; + } + const heroCarouselItem = new models.HeroCarouselItem(); + const productData = article.productDataFromArticle(objectGraph, itemData); + const metricsTitle = heroCarouselOverlayCommon.heroCarouselItemTitleFromData(objectGraph, itemData); + const heroCarouselItemImpressionOptions = metricsHelpersImpressions.impressionOptions(objectGraph, itemData, metricsTitle, heroCarouselItemMetricsOptions); + heroCarouselItemImpressionOptions.isPreorder = contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, productData, "isPreorder"); + metricsHelpersImpressions.addImpressionFields(objectGraph, heroCarouselItem, heroCarouselItemImpressionOptions); + // Push the heroCarouselItem here so that the click action has the item in its location + // but we do not want to add it to the overall location tracker, so pop it right after adding it to the button + // action + // <rdar://problem/60883269> Metrics: Arcade: Container values requested in Location field + metricsHelpersLocation.pushContentLocation(objectGraph, heroCarouselItemImpressionOptions, metricsTitle); + const titleEffectArtwork = heroVideoData.artworkData || heroArtworkData.artworkData; + const backgroundColor = heroVideoData.backgroundColor || heroArtworkData.backgroundColor; + const overlayRequirements = { + metricsPageInformation: shelfToken.metricsPageInformation, + metricsLocationTracker: shelfToken.metricsLocationTracker, + canDisplayArcadeOfferButton: content.shelfContentTypeCanDisplayArcadeOfferButtons(objectGraph, shelfToken.shelfStyle), + lockupArtworkUseCase: content.artworkUseCaseFromShelfStyle(objectGraph, shelfToken.shelfStyle), + isContainedInPreorderExclusiveShelf: shelfToken.featuredContentId === 497 /* groupingTypes.FeaturedContentID.AppStore_ComingSoon */, + }; + heroCarouselItem.overlay = heroCarouselOverlayCommon.overlayFromData(objectGraph, itemData, overlayRequirements); + heroCarouselItem.backgroundColor = backgroundColor; + if (!objectGraph.client.isMac || objectGraph.props.isNotEnabled("macOSArcadeHeaderUpdates")) { + heroCarouselItem.titleEffect = breakoutsCommon.titleEffectFromArtwork(objectGraph, titleEffectArtwork); + } + heroCarouselItem.artwork = heroArtworkData.artwork; + heroCarouselItem.video = heroVideoData.video; + if (objectGraph.client.isWeb) { + const portraitVideo = heroCommon.heroVideoFromData(objectGraph, itemData, false, true); + heroCarouselItem.portraitVideo = portraitVideo === null || portraitVideo === void 0 ? void 0 : portraitVideo.video; + } + const heroCarouselItemAction = breakoutsCommon.actionFromData(objectGraph, itemData); + const heroCarouselItemClickOptions = { + pageInformation: shelfToken.metricsPageInformation, + locationTracker: shelfToken.metricsLocationTracker, + recoMetricsData: mediaDataStructure.metricsFromMediaApiObject(itemData), + targetType: "largeBreakout", + id: itemData.id, + }; + if (heroCarouselItemAction) { + metricsHelpersClicks.addClickEventToAction(objectGraph, heroCarouselItemAction, heroCarouselItemClickOptions); + heroCarouselItem.clickAction = heroCarouselItemAction; + } + heroCarousel.items.push(heroCarouselItem); + metricsHelpersLocation.popLocation(heroCarouselItemImpressionOptions.locationTracker); + metricsHelpersLocation.nextPosition(heroCarouselItemImpressionOptions.locationTracker); + } + if (serverData.isDefinedNonNullNonEmpty(heroCarousel.items)) { + shelf.items = [heroCarousel]; + groupingParseContext.pageTitleEffect = heroCarousel.items[0].titleEffect; + } + shelf.url = groupingShelfControllerCommon.createShelfTokenUrlIfNecessaryForShelf(objectGraph, shelf, shelfToken); + metricsHelpersLocation.popLocation(heroCarouselImpressionOptions.locationTracker); + metricsHelpersLocation.nextPosition(heroCarouselImpressionOptions.locationTracker); + return shelf; + } +} +//# sourceMappingURL=grouping-hero-carousel-shelf-controller.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/grouping/shelf-controllers/grouping-horizontal-card-shelf-controller.js b/node_modules/@jet-app/app-store/tmp/src/common/grouping/shelf-controllers/grouping-horizontal-card-shelf-controller.js new file mode 100644 index 0000000..d02a4b6 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/grouping/shelf-controllers/grouping-horizontal-card-shelf-controller.js @@ -0,0 +1,122 @@ +import * as models from "../../../api/models"; +import * as mediaAttributes from "../../../foundation/media/attributes"; +import * as mediaRelationship from "../../../foundation/media/relationships"; +import * as todayHorizontalCardUtil from "../../today/today-horizontal-card-util"; +import { TodayParseContext } from "../../today/today-types"; +import { GroupingShelfController } from "./grouping-shelf-controller"; +import * as groupingShelfControllerCommon from "./grouping-shelf-controller-common"; +export class GroupingHorizontalCardShelfController extends GroupingShelfController { + // region Constructors + constructor() { + super("GroupingHorizontalCardShelfController"); + this.supportedFeaturedContentIds = new Set([475 /* groupingTypes.FeaturedContentID.AppStore_HorizontalCardSwoosh */]); + } + // endregion + // region GroupingShelfController + _supports(objectGraph, mediaApiData, featuredContentId, nativeGroupingShelfId) { + if (!super._supports(objectGraph, mediaApiData, featuredContentId, nativeGroupingShelfId)) { + return false; + } + const displayStyle = mediaAttributes.attributeAsString(mediaApiData, "displayStyle"); + const contentType = groupingShelfControllerCommon.contentTypeForHorizontalCardDisplayStyle(objectGraph, displayStyle); + return contentType !== "editorialStoryCard"; + } + // endregion + // region Shelf Creation Prerequisites + /** + * For a given mediaApiData extract the actual shelfContents array needed to render this shelf + * + * @param mediaApiData The outer shelfContents object containing the shelf contents + */ + initialShelfDataFromGroupingMediaApiData(objectGraph, mediaApiData) { + return { shelfContents: mediaRelationship.relationshipCollection(mediaApiData, "contents") }; + } + /** + * For a given url that this controller handles, we should return a promise that will result in the `ShelfData` + * needed to render this shelf + * + * @param objectGraph The App Store dependency graph + * @param shelfUrl The url that this controller handled on a secondary fetch + * @param parameters The extracted parameters from the shelf url + */ + async secondaryShelfDataForShelfUrl(objectGraph, shelfUrl, shelfToken, parameters) { + return await GroupingShelfController.secondaryGroupingShelfDataForShelfUrl(objectGraph, shelfUrl, shelfToken, parameters); + } + /** + * For a given mediaApiData create an updated shelf token that contains all the additional data for this specific shelf type + * + * @param objectGraph The App Store dependency graph + * @param baseShelfToken The base grouping shelf token created by the grouping-controller + * @param mediaApiData The outer data object containing the FC properties and data + * @param groupingParseContext The parse context for the grouping page so far + */ + shelfTokenFromBaseTokenAndMediaApiData(objectGraph, mediaApiData, baseShelfToken, groupingParseContext) { + const shelfToken = { ...baseShelfToken }; + const displayStyle = mediaAttributes.attributeAsString(mediaApiData, "displayStyle"); + shelfToken.shelfStyle = groupingShelfControllerCommon.contentTypeForHorizontalCardDisplayStyle(objectGraph, displayStyle); + return shelfToken; + } + // endregion + // region Shelf Creation + /** + * + * @param objectGraph The App Store dependency graph + * @param shelfToken The shelf shelfToken for this current shelf creation request + * @param shelfData The media api shelfContents array for this shelf + * @param groupingParseContext The parse context used to generate the grouping page on the initial page load, + * this will be missing when this controller renders a secondary or incomplete shelf fetch. + */ + _createShelf(objectGraph, shelfToken, shelfData, groupingParseContext) { + var _a; + const cardUnavailable = function (cardData) { + shelfToken.remainingItems.push(cardData); + return false; + }; + let shelf; + if (objectGraph.client.isiOS && objectGraph.featureFlags.isEnabled("mini_today_cards_grouping")) { + const todayParseContext = new TodayParseContext(shelfToken.metricsPageInformation, shelfToken.metricsLocationTracker); + shelf = todayHorizontalCardUtil.shelfForMiniTodayCards(objectGraph, (_a = shelfData.shelfContents) !== null && _a !== void 0 ? _a : [], shelfToken.title, shelfToken.subtitle, todayParseContext); + } + else { + const cardContext = { + metricsLocationTracker: shelfToken.metricsLocationTracker, + metricsPageInformation: shelfToken.metricsPageInformation, + }; + let resolvedContentType; + // First check if the client explicitly supports small story cards. + // If it does, don't change the content type. + const isSmallStoryCardsSupported = objectGraph.host.isiOS || objectGraph.host.isMac || objectGraph.host.isWeb; + if (isSmallStoryCardsSupported && shelfToken.shelfStyle === "smallStoryCard") { + resolvedContentType = shelfToken.shelfStyle; + } + else { + switch (objectGraph.client.deviceType) { + case "mac": + case "tv": + case "web": + resolvedContentType = shelfToken.shelfStyle; + break; + case "watch": + resolvedContentType = "todayCard"; + break; + default: + resolvedContentType = "todayBrick"; + break; + } + } + shelf = todayHorizontalCardUtil.shelfForHorizontalCardItems(objectGraph, shelfData.shelfContents, resolvedContentType, shelfToken.title, shelfToken.subtitle, cardContext, cardUnavailable); + if (shelf.contentType === "smallStoryCard" && Array.isArray(shelf.items)) { + shelf.items = shelf.items.filter((item) => { + if (!(item instanceof models.TodayCard)) { + return true; + } + return todayHorizontalCardUtil.isHorizontalCardSupportedForKind(objectGraph, item.media.kind, "smallStoryCard"); + }); + } + } + shelf.url = groupingShelfControllerCommon.createShelfTokenUrlIfNecessaryForShelf(objectGraph, shelf, shelfToken); + shelf.isHorizontal = true; + return shelf; + } +} +//# sourceMappingURL=grouping-horizontal-card-shelf-controller.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/grouping/shelf-controllers/grouping-large-breakout-shelf-controller.js b/node_modules/@jet-app/app-store/tmp/src/common/grouping/shelf-controllers/grouping-large-breakout-shelf-controller.js new file mode 100644 index 0000000..7b14c97 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/grouping/shelf-controllers/grouping-large-breakout-shelf-controller.js @@ -0,0 +1,193 @@ +import * as models from "../../../api/models"; +import * as serverData from "../../../foundation/json-parsing/server-data"; +import * as mediaAttributes from "../../../foundation/media/attributes"; +import * as mediaDataStructure from "../../../foundation/media/data-structure"; +import * as mediaRelationship from "../../../foundation/media/relationships"; +import * as breakoutsCommon from "../../arcade/breakouts-common"; +import * as contentAttributes from "../../content/attributes"; +import * as content from "../../content/content"; +import * as flowPreview from "../../content/flow-preview"; +import * as metricsHelpersClicks from "../../metrics/helpers/clicks"; +import * as metricsHelpersImpressions from "../../metrics/helpers/impressions"; +import * as metricsHelpersLocation from "../../metrics/helpers/location"; +import * as article from "../../today/article"; +import * as heroCommon from "../hero/hero-common"; +import { GroupingShelfController } from "./grouping-shelf-controller"; +import * as groupingShelfControllerCommon from "./grouping-shelf-controller-common"; +export class GroupingLargeBreakoutShelfController extends GroupingShelfController { + // region Constructors + constructor(builderClass = null) { + super(builderClass || "GroupingLargeBreakoutShelfController"); + this.supportedFeaturedContentIds = new Set([480 /* groupingTypes.FeaturedContentID.AppStore_Breakout */]); + } + // endregion + // region GroupingShelfController + _supports(objectGraph, mediaApiData, featuredContentId, nativeGroupingShelfId) { + if (!super._supports(objectGraph, mediaApiData, featuredContentId, nativeGroupingShelfId)) { + return false; + } + const breakoutStyle = mediaAttributes.attributeAsString(mediaApiData, "displayStyle"); + return breakoutStyle === "large"; + } + // endregion + // region Shelf Creation Prerequisites + /** + * For a given mediaApiData extract the actual shelfContents array needed to render this shelf + * + * @param mediaApiData The outer shelfContents object containing the shelf contents + */ + initialShelfDataFromGroupingMediaApiData(objectGraph, mediaApiData) { + return { shelfContents: mediaRelationship.relationshipCollection(mediaApiData, "contents") }; + } + /** + * For a given url that this controller handles, we should return a promise that will result in the `ShelfData` + * needed to render this shelf + * + * @param objectGraph The App Store dependency graph + * @param shelfUrl The url that this controller handled on a secondary fetch + * @param parameters The extracted parameters from the shelf url + */ + async secondaryShelfDataForShelfUrl(objectGraph, shelfUrl, shelfToken, parameters) { + return await GroupingShelfController.secondaryGroupingShelfDataForShelfUrl(objectGraph, shelfUrl, shelfToken, parameters); + } + /** + * For a given mediaApiData create an updated shelf token that contains all the additional data for this specific shelf type + * + * @param objectGraph The App Store dependency graph + * @param baseShelfToken The base grouping shelf token created by the grouping-controller + * @param mediaApiData The outer data object containing the FC properties and data + * @param groupingParseContext The parse context for the grouping page so far + */ + shelfTokenFromBaseTokenAndMediaApiData(objectGraph, mediaApiData, baseShelfToken, groupingParseContext) { + return baseShelfToken; + } + // endregion + // region Metrics + shouldImpressShelf() { + return false; + } + // endregion + // region Shelf Creation + isInHeroPosition() { + return false; + } + /** + * + * @param objectGraph The App Store dependency graph + * @param shelfToken The shelf shelfToken for this current shelf creation request + * @param shelfData The media api shelfContents array for this shelf + * @param groupingParseContext The parse context used to generate the grouping page on the initial page load, + * this will be missing when this controller renders a secondary or incomplete shelf fetch. + */ + _createShelf(objectGraph, shelfToken, shelfData, groupingParseContext) { + const isFirstShelf = serverData.isDefinedNonNullNonEmpty(groupingParseContext) && + serverData.isNullOrEmpty(groupingParseContext.pageTitleEffect) && + groupingParseContext.shelves.length === 0; + const trimmedShelfContents = serverData.isDefinedNonNullNonEmpty(shelfData.shelfContents) + ? [shelfData.shelfContents[0]] + : []; + const items = []; + for (const data of trimmedShelfContents) { + if (serverData.isNull(data.attributes) || groupingShelfControllerCommon.shouldDefer(shelfToken)) { + shelfToken.isDeferring = true; + shelfToken.remainingItems.push(data); + continue; + } + const metricsOptions = { + targetType: this.isInHeroPosition() ? "heroBreakout" : "largeBreakout", + pageInformation: shelfToken.metricsPageInformation, + locationTracker: shelfToken.metricsLocationTracker, + recoMetricsData: mediaDataStructure.metricsFromMediaApiObject(data), + }; + const heroVideoData = heroCommon.heroVideoFromData(objectGraph, data); + const heroArtworkData = heroCommon.heroArtworkFromData(objectGraph, data); + const titleEffectArtwork = heroVideoData.artworkData || heroArtworkData.artworkData; + const largeHeroBreakout = GroupingLargeBreakoutShelfController.createLargeBreakout(objectGraph, data, shelfToken, this.isInHeroPosition(), isFirstShelf, metricsOptions); + if (serverData.isNullOrEmpty(largeHeroBreakout)) { + continue; + } + items.push(largeHeroBreakout); + if (isFirstShelf) { + groupingParseContext.pageTitleEffect = breakoutsCommon.titleEffectFromArtwork(objectGraph, titleEffectArtwork); + } + } + const shelf = new models.Shelf("largeHeroBreakout"); + shelf.isHorizontal = false; + shelf.items = items; + shelf.url = groupingShelfControllerCommon.createShelfTokenUrlIfNecessaryForShelf(objectGraph, shelf, shelfToken); + if (groupingParseContext.shelves.length === 0) { + shelf.presentationHints = { isFirstShelf: true }; + } + return shelf; + } + static createLargeBreakout(objectGraph, data, shelfToken, isHeroPosition, isFirstShelf, metricsOptions) { + const primaryContent = content.primaryContentForData(objectGraph, data); + if (breakoutsCommon.requiresPrimaryContent(objectGraph, data) && + !mediaAttributes.hasAttributes(primaryContent)) { + shelfToken.isDeferring = true; + shelfToken.remainingItems.push(data); + shelfToken.relationshipToFetch = "primary-content"; + return null; + } + const heroVideoData = heroCommon.heroVideoFromData(objectGraph, data); + const heroArtworkData = heroCommon.heroArtworkFromData(objectGraph, data); + if (serverData.isNullOrEmpty(heroVideoData.video) && serverData.isNullOrEmpty(heroArtworkData.artwork)) { + return null; + } + const backgroundColor = heroVideoData.backgroundColor || heroArtworkData.backgroundColor; + const heading = isHeroPosition ? null : mediaAttributes.attributeAsString(data, "breakoutBadge"); + // If this EI can and should show the expected release date of an app, override the badge. + let badgeTitle; + const fallbackLabel = mediaAttributes.attributeAsString(data, "label"); + if (contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "showExpectedReleaseDate")) { + badgeTitle = objectGraph.loc.uppercased(content.dynamicPreorderDateFromData(objectGraph, primaryContent, fallbackLabel)); + } + else { + badgeTitle = fallbackLabel; + } + let badge = { type: "none" }; + if ((badgeTitle === null || badgeTitle === void 0 ? void 0 : badgeTitle.length) > 0) { + badge = { + type: "text", + title: badgeTitle, + }; + } + const title = content.editorialNotesFromData(objectGraph, data, "name") || + contentAttributes.contentAttributeAsString(objectGraph, primaryContent, "name"); + const description = content.editorialNotesFromData(objectGraph, data, "short") || + contentAttributes.contentAttributeAsString(objectGraph, primaryContent, "tagline"); + const backgroundStyle = breakoutsCommon.detailBackgroundStyleFromData(objectGraph, data, true, isFirstShelf, false); + const detailPosition = breakoutsCommon.detailPositionFromData(objectGraph, data, isFirstShelf, false); + const details = new models.BreakoutDetails(title, description, badge, null, backgroundStyle, breakoutsCommon.detailTextAlignmentForDetailPosition(objectGraph, detailPosition, data)); + const largeHeroBreakout = new models.LargeHeroBreakout(details, { + position: detailPosition || "leading", + }, heading, heroArtworkData.artwork, heroVideoData.video, null, backgroundColor); + const impressionOptions = metricsHelpersImpressions.impressionOptions(objectGraph, data, largeHeroBreakout.details.title, metricsOptions); + const productData = article.productDataFromArticle(objectGraph, data); + const isPreorder = contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, productData, "isPreorder"); + impressionOptions.isPreorder = isPreorder; + metricsHelpersImpressions.addImpressionFields(objectGraph, largeHeroBreakout, impressionOptions); + // Push the breakout here so that the click action has the breakout in its location + // but we do not want to add it to the overall location tracker, so pop it right after adding it to the button + // action + // <rdar://problem/60883269> Metrics: Arcade: Container values requested in Location field + metricsHelpersLocation.pushContentLocation(objectGraph, impressionOptions, largeHeroBreakout.details.title); + const breakoutAction = breakoutsCommon.actionFromData(objectGraph, data); + const breakoutClickOptions = { + pageInformation: shelfToken.metricsPageInformation, + locationTracker: shelfToken.metricsLocationTracker, + recoMetricsData: mediaDataStructure.metricsFromMediaApiObject(data), + targetType: "button", + id: data.id, + }; + metricsHelpersClicks.addClickEventToAction(objectGraph, breakoutAction, breakoutClickOptions); + largeHeroBreakout.details.callToActionButtonAction = breakoutAction; + largeHeroBreakout.clickAction = breakoutAction; + metricsHelpersLocation.popLocation(metricsOptions.locationTracker); + // Set flow preview actions + largeHeroBreakout.flowPreviewActionsConfiguration = + flowPreview.flowPreviewActionsConfigurationForProductFromData(objectGraph, data, true, shelfToken.clientIdentifierOverride, breakoutAction, metricsOptions, breakoutClickOptions); + return largeHeroBreakout; + } +} +//# sourceMappingURL=grouping-large-breakout-shelf-controller.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/grouping/shelf-controllers/grouping-link-shelf-controller.js b/node_modules/@jet-app/app-store/tmp/src/common/grouping/shelf-controllers/grouping-link-shelf-controller.js new file mode 100644 index 0000000..09865f5 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/grouping/shelf-controllers/grouping-link-shelf-controller.js @@ -0,0 +1,169 @@ +import { isNothing } from "@jet/environment"; +import * as models from "../../../api/models"; +import * as serverData from "../../../foundation/json-parsing/server-data"; +import * as mediaAttributes from "../../../foundation/media/attributes"; +import * as mediaDataStructure from "../../../foundation/media/data-structure"; +import * as mediaRelationship from "../../../foundation/media/relationships"; +import { createArtworkForResource } from "../../content/artwork/artwork"; +import * as lockupsEditorialContext from "../../lockups/editorial-context"; +import * as metricsHelpersClicks from "../../metrics/helpers/clicks"; +import * as metricsHelpersImpressions from "../../metrics/helpers/impressions"; +import * as metricsHelpersLocation from "../../metrics/helpers/location"; +import { GroupingShelfController } from "./grouping-shelf-controller"; +import * as groupingShelfControllerCommon from "./grouping-shelf-controller-common"; +export class GroupingLinkShelfController extends GroupingShelfController { + // region Constructors + constructor() { + super("GroupingLinkShelfController"); + this.supportedFeaturedContentIds = new Set([ + 437 /* groupingTypes.FeaturedContentID.AppStore_LinkList */, + 265 /* groupingTypes.FeaturedContentID.Sundance_LinkList */, + ]); + } + // endregion + // region Shelf Creation Prerequisites + /** + * For a given mediaApiData extract the actual shelfContents array needed to render this shelf + * + * @param mediaApiData The outer shelfContents object containing the shelf contents + */ + initialShelfDataFromGroupingMediaApiData(objectGraph, mediaApiData) { + let shelfContents = mediaRelationship.relationshipCollection(mediaApiData, "children"); + if (serverData.isNullOrEmpty(shelfContents)) { + shelfContents = mediaAttributes.attributeAsArrayOrEmpty(mediaApiData, "links"); + } + return { shelfContents: shelfContents }; + } + /** + * For a given url that this controller handles, we should return a promise that will result in the `ShelfData` + * needed to render this shelf + * + * @param objectGraph The App Store dependency graph + * @param shelfUrl The url that this controller handled on a secondary fetch + * @param parameters The extracted parameters from the shelf url + */ + async secondaryShelfDataForShelfUrl(objectGraph, shelfUrl, shelfToken, parameters) { + return await GroupingShelfController.secondaryGroupingShelfDataForShelfUrl(objectGraph, shelfUrl, shelfToken, parameters); + } + /** + * For a given mediaApiData create an updated shelf token that contains all the additional data for this specific shelf type + * + * @param objectGraph The App Store dependency graph + * @param baseShelfToken The base grouping shelf token created by the grouping-controller + * @param mediaApiData The outer data object containing the FC properties and data + * @param groupingParseContext The parse context for the grouping page so far + */ + shelfTokenFromBaseTokenAndMediaApiData(objectGraph, mediaApiData, baseShelfToken, groupingParseContext) { + const linkShelfToken = { + ...baseShelfToken, + shouldHideShelf: mediaAttributes.attributeAsBooleanOrFalse(mediaApiData, "hide"), + areContentLinks: serverData.isDefinedNonNullNonEmpty(mediaRelationship.relationshipCollection(mediaApiData, "children")), + }; + linkShelfToken.clientIdentifierOverride = lockupsEditorialContext.clientIdentifierForEditorialContextInData(objectGraph, mediaApiData); + return linkShelfToken; + } + // endregion + // region Shelf Creation + /** + * + * @param objectGraph The App Store dependency graph + * @param shelfToken The shelf shelfToken for this current shelf creation request + * @param shelfData The media api shelfContents array for this shelf + * @param groupingParseContext The parse context used to generate the grouping page on the initial page load, + * this will be missing when this controller renders a secondary or incomplete shelf fetch. + */ + _createShelf(objectGraph, shelfToken, shelfData, groupingParseContext) { + if (shelfToken.shouldHideShelf) { + return null; + } + const items = []; + for (let linkIndex = 0; linkIndex < shelfData.shelfContents.length; linkIndex++) { + const link = shelfData.shelfContents[linkIndex]; + const metricsBase = { + targetType: "link", + pageInformation: shelfToken.metricsPageInformation, + locationTracker: shelfToken.metricsLocationTracker, + }; + if (shelfToken.areContentLinks) { + metricsBase.recoMetricsData = mediaDataStructure.metricsFromMediaApiObject(link); + } + let action = null; + if (shelfToken.isSearchLandingPage) { + const searchAdAction = this.trendingSearchLinkFromData(objectGraph, link, shelfToken.metricsLocationTracker); + if (serverData.isNull(searchAdAction) || serverData.isNull(searchAdAction.action)) { + continue; + } + metricsHelpersImpressions.addImpressionFields(objectGraph, searchAdAction === null || searchAdAction === void 0 ? void 0 : searchAdAction.action, { + ...metricsBase, + kind: "link", + softwareType: serverData.asBooleanOrFalse(groupingParseContext === null || groupingParseContext === void 0 ? void 0 : groupingParseContext.isArcadePage) ? "Arcade" : null, + title: searchAdAction.action.title, + id: `${linkIndex}`, + idType: "sequential", + }); + action = searchAdAction; + } + else { + const metadata = groupingShelfControllerCommon.metadataForFCData(objectGraph, link, shelfToken, false, null, metricsBase, null); + if (serverData.isNullOrEmpty(metadata) || serverData.isNullOrEmpty(metadata.action.title)) { + continue; + } + action = metadata.action; + const contentId = groupingShelfControllerCommon.contentIdFromContentItem(objectGraph, link); + if (contentId) { + metricsHelpersImpressions.addImpressionFields(objectGraph, action, { + ...metricsBase, + kind: "link", + softwareType: serverData.asBooleanOrFalse(groupingParseContext === null || groupingParseContext === void 0 ? void 0 : groupingParseContext.isArcadePage) ? "Arcade" : null, + title: action.title, + id: contentId, + }); + } + } + if (serverData.isNullOrEmpty(action)) { + continue; + } + items.push(action); + metricsHelpersLocation.nextPosition(shelfToken.metricsLocationTracker); + } + if (serverData.isNullOrEmpty(items)) { + return null; + } + const actionsShouldHaveArtwork = shelfToken.isSearchLandingPage && + (objectGraph.client.isPhone || objectGraph.client.isVision) && + items.length >= 6; + if (actionsShouldHaveArtwork) { + for (const item of items) { + if (item instanceof models.SearchAdAction) { + item.action.artwork = createArtworkForResource(objectGraph, "systemimage://magnifyingglass"); + } + } + } + const shelf = new models.Shelf("action"); + shelf.isHorizontal = false; + shelf.items = items; + shelf.title = shelfToken.title; + shelf.subtitle = shelfToken.subtitle; + shelf.presentationHints = { isWidthConstrained: true }; + shelf.url = groupingShelfControllerCommon.createShelfTokenUrlIfNecessaryForShelf(objectGraph, shelf, shelfToken); + if (shelfToken.isSearchLandingPage && (objectGraph.client.isPhone || objectGraph.client.isPad)) { + shelf.contentsMetadata = { + type: "searchLandingTrendingSection", + numberOfColumns: items.length >= 6 ? 2 : 1, + }; + } + return shelf; + } + // region Helpers + trendingSearchLinkFromData(objectGraph, link, locationTracker) { + const term = serverData.asString(link, "label"); + if (isNothing(term) || term.length === 0) { + return null; + } + const searchAction = new models.SearchAction(term, term, null, "suggested"); + metricsHelpersClicks.addEventsToSearchAction(objectGraph, searchAction, "button", locationTracker); + metricsHelpersLocation.nextPosition(locationTracker); + return new models.SearchAdAction(searchAction); + } +} +//# sourceMappingURL=grouping-link-shelf-controller.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/grouping/shelf-controllers/grouping-lockup-shelf-controller-common.js b/node_modules/@jet-app/app-store/tmp/src/common/grouping/shelf-controllers/grouping-lockup-shelf-controller-common.js new file mode 100644 index 0000000..5c9273f --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/grouping/shelf-controllers/grouping-lockup-shelf-controller-common.js @@ -0,0 +1,608 @@ +import * as models from "../../../api/models"; +import * as serverData from "../../../foundation/json-parsing/server-data"; +import * as mediaAttributes from "../../../foundation/media/attributes"; +import * as mediaDataStructure from "../../../foundation/media/data-structure"; +import * as mediaRelationship from "../../../foundation/media/relationships"; +import * as adIncidents from "../../ads/ad-incident-recorder"; +import * as adStitch from "../../ads/ad-stitcher"; +import { searchLandingPagePositionInfo } from "../../ads/on-device-ad-stitch"; +import * as mediaUrlMapping from "../../builders/url-mapping"; +import * as videoDefaults from "../../constants/video-constants"; +import * as content from "../../content/content"; +import { CollectionShelfDisplayStyle } from "../../editorial-pages/editorial-page-types"; +import * as filtering from "../../filtering"; +import * as adLockups from "../../lockups/ad-lockups"; +import * as lockupsEditorialContext from "../../lockups/editorial-context"; +import * as lockups from "../../lockups/lockups"; +import * as metricsHelpersClicks from "../../metrics/helpers/clicks"; +import * as metricsHelpersLocation from "../../metrics/helpers/location"; +import * as placeholders from "../../placeholders/placeholders"; +import * as room from "../../room/room-common"; +import * as topChartsCommon from "../../top-charts-common"; +import * as pageCommon from "../../util/page-common"; +import * as groupingTypes from "../grouping-types"; +import * as groupingShelfControllerCommon from "./grouping-shelf-controller-common"; +import * as color from "../../../foundation/util/color-util"; +import { pageRouter } from "../../builders/routing"; +import { isNothing, isSome } from "@jet/environment/types/optional"; +import * as modelsBase from "../../../api/models/base"; +import * as metricsLocation from "../../../common/metrics/helpers/location"; +import { makeGameCenterHeader, openGamesUIAction } from "../../arcade/arcade-common"; +import { makeRoomPageIntent } from "../../../api/intents/room-page-intent"; +import { makeChartsPageIntent } from "../../../api/intents/charts-page-intent"; +import { makeChartsPageURL } from "../../../common/charts/charts-page-url"; +import { getLocale } from "../../locale"; +import { getPlatform } from "../../preview-platform"; +import { genreIdFromChartURL } from "../../../common/grouping/render-grouping-page"; +import { injectCrossfireFlowForGameCenter } from "./grouping-game-center-popular-with-your-friends-shelf-controller"; +/** + * Determine the shelfContentType to use for a shelf given the shelf token + * @param objectGraph + * @param mediaApiData + * @param shelfToken + * @param groupingParseContext + */ +export function shelfContentType(objectGraph, mediaApiData, shelfToken, groupingParseContext) { + const featuredContentId = shelfToken.featuredContentId; + if (featuredContentId === 557 /* groupingTypes.FeaturedContentID.AppStore_CategoryBreakoutMarker */ || + featuredContentId === 418 /* groupingTypes.FeaturedContentID.AppStore_Shelf */ || + featuredContentId === 495 /* groupingTypes.FeaturedContentID.AppStore_PopularWithYourFriendsMarker */ || + groupingTypes.isRecommendationsLockupShelf(featuredContentId)) { + let displayStyle = mediaAttributes.attributeAsString(mediaApiData, "displayStyle"); + if (!displayStyle) { + if (featuredContentId === 311 /* groupingTypes.FeaturedContentID.Sundance_RecommendedAppsShelf */ || + featuredContentId === 312 /* groupingTypes.FeaturedContentID.Sundance_RecommendedGamesShelf */) { + displayStyle = "large"; + } + else if (featuredContentId === 495 /* groupingTypes.FeaturedContentID.AppStore_PopularWithYourFriendsMarker */ || + featuredContentId === 557 /* groupingTypes.FeaturedContentID.AppStore_CategoryBreakoutMarker */) { + displayStyle = "medium"; + } + else { + displayStyle = "small"; + } + } + return `${displayStyle}Lockup`; + } + else if (featuredContentId === 431 /* groupingTypes.FeaturedContentID.AppStore_iAPShelf */) { + return "inAppPurchaseTiledLockup"; + } + else if (featuredContentId === 429 /* groupingTypes.FeaturedContentID.AppStore_ScreenShotShelf */) { + return "screenshotsLockup"; + } + else if (featuredContentId === 304 /* groupingTypes.FeaturedContentID.Sundance_iPhoneAppTrailerShelf */ || + featuredContentId === 305 /* groupingTypes.FeaturedContentID.Sundance_iPadAppTrailerShelf */ || + featuredContentId === 430 /* groupingTypes.FeaturedContentID.AppStore_VideoShelf */ || + featuredContentId === 420 /* groupingTypes.FeaturedContentID.AppStore_TrailerShelf */) { + return "appTrailerLockup"; + } + else if (groupingTypes.isTopChart(featuredContentId)) { + return "smallLockup"; + } + else if (featuredContentId === 497 /* groupingTypes.FeaturedContentID.AppStore_ComingSoon */) { + return "posterLockup"; + } + else { + switch (groupingParseContext.shelves.length % 3) { + case 0: { + return "smallLockup"; + } + case 1: { + return "mediumLockup"; + } + default: { + return "largeLockup"; + } + } + } +} +// region Shelf Token +export function lockupShelfTokenFromBaseTokenAndMediaApiData(objectGraph, mediaApiData, baseShelfToken, groupingParseContext) { + const lockupShelfToken = { + ...baseShelfToken, + shouldFilter: !mediaAttributes.attributeAsBooleanOrFalse(mediaApiData, "doNotFilter"), + chartUrl: mediaAttributes.attributeAsString(mediaApiData, "chartHref"), + chartIdentifier: mediaAttributes.attributeAsString(mediaApiData, "chart"), + roomRelationshipData: mediaRelationship.relationshipData(objectGraph, mediaApiData, "room"), + }; + if (groupingTypes.isTopChart(lockupShelfToken.featuredContentId)) { + lockupShelfToken.seeAllUrl = topChartsCommon.makeSeeAllUrlFromMediaApiData(objectGraph, mediaApiData, // chart shelf data includes selected chart + groupingParseContext.chartSet); + lockupShelfToken.showOrdinals = true; + lockupShelfToken.shouldFilter = false; + } + else if (serverData.isDefinedNonNullNonEmpty(lockupShelfToken.roomRelationshipData)) { + lockupShelfToken.seeAllUrl = mediaUrlMapping.hrefToRoutableUrl(objectGraph, lockupShelfToken.roomRelationshipData.href); + } + lockupShelfToken.shelfStyle = shelfContentType(objectGraph, mediaApiData, lockupShelfToken, groupingParseContext); + lockupShelfToken.clientIdentifierOverride = lockupsEditorialContext.clientIdentifierForEditorialContextInData(objectGraph, mediaApiData); + return lockupShelfToken; +} +// endregion +// region Shelf Creation +/** + * + * @param objectGraph The App Store dependency graph + * @param shelfToken The shelf shelfToken for this current shelf creation request + * @param shelfData The media api shelfContents array for this shelf + * @param groupingParseContext The parse context used to generate the grouping page on the initial page load, + * this will be missing when this controller renders a secondary or incomplete shelf fetch. + */ +export function createLockupShelf(objectGraph, shelfToken, shelfData, groupingParseContext) { + const filterType = groupingTypes.isMacTopChart(shelfToken.featuredContentId) + ? 84862 /* filtering.Filter.ChartsMac */ + : 80894 /* filtering.Filter.All */; + let items = []; + // Stitch First Position Ad (only if lockup array is nonempty) + if (serverData.isDefinedNonNullNonEmpty(shelfData.shelfContents)) { + const adLockup = lockupFromAdStitcher(objectGraph, groupingParseContext === null || groupingParseContext === void 0 ? void 0 : groupingParseContext.adStitcher, groupingParseContext === null || groupingParseContext === void 0 ? void 0 : groupingParseContext.adIncidentRecorder, searchLandingPagePositionInfo, shelfToken); + if (adLockup && adLockup instanceof models.Lockup) { + items.push(adLockup); + metricsHelpersLocation.nextPosition(shelfToken.metricsLocationTracker); + shelfToken.ordinalIndex++; + shelfData.shelfContents = shelfData.shelfContents.filter((data) => data.id !== adLockup.adamId); // Filter dupe + } + } + // Data to use for `seeAllContents` in Category Breakout shelf + let seeAllContents; + // Build Lockups + for (const lockupData of shelfData.shelfContents) { + // If we encounter a type of app-events, this means they have been incorrectly programmed, + // and we should throw the shelf away. + if (lockupData.type === "app-events") { + return null; + } + // If we encounter a type of `editorial-items` in + // Category Breakout shelf, it is to use for `seeAllContents`. + if (shelfToken.featuredContentId === 557 /* groupingTypes.FeaturedContentID.AppStore_CategoryBreakoutMarker */ && + lockupData.type === "editorial-items") { + seeAllContents = lockupData; + continue; + } + if (serverData.isNull(lockupData.attributes) || groupingShelfControllerCommon.shouldDefer(shelfToken)) { + shelfToken.isDeferring = true; + shelfToken.remainingItems.push(lockupData); + continue; + } + // Filter out unwanted content + if (filtering.shouldFilter(objectGraph, lockupData, filterType)) { + continue; + } + const lockup = lockupFromData(objectGraph, lockupData, shelfToken); + if (lockup) { + items.push(lockup); + metricsHelpersLocation.nextPosition(shelfToken.metricsLocationTracker); + shelfToken.ordinalIndex++; + } + } + // Truncate non-Arcade top charts to multiples of 3 + if (groupingTypes.isTopChart(shelfToken.featuredContentId) && + !groupingTypes.isArcadeTopChart(shelfToken.featuredContentId) && + !groupingTypes.isGameCenterTopChart(shelfToken.featuredContentId) && + !objectGraph.client.isMac) { + items = pageCommon.truncateItems(items, 3); + } + // We don't need this in our incomplete shelf URL, so we'll preemptively remove it. + delete shelfToken.maxItemCount; + // Create shelf header + let shelfHeader; + if (groupingTypes.isGameCenterTopChart(shelfToken.featuredContentId)) { + // Show Game Center eyebrow for Game Center top chart shelves + shelfHeader = makeGameCenterHeader(objectGraph, shelfToken.title); + } + else { + shelfHeader = { + eyebrow: shelfToken.eyebrow, + eyebrowArtwork: shelfToken.eyebrowArtwork, + title: shelfToken.title, + titleArtwork: shelfToken.titleArtwork, + subtitle: shelfToken.subtitle, + configuration: shelfToken.shelfHeaderConfiguration, + }; + } + // Create shelf + const shelf = new models.Shelf(shelfToken.shelfStyle); + shelf.isHorizontal = true; + shelf.items = items; + shelf.shouldFilterApps = shelfToken.shouldFilter; + if (groupingTypes.isGameCenterTopChart(shelfToken.featuredContentId)) { + shelf.footerTitle = objectGraph.loc.string("Lockup.Footer.GamesApp"); + shelf.footerAction = openGamesUIAction(objectGraph); + shelf.footerStyle = { + $kind: "games", + bundleID: "com.apple.games", + width: 16, + height: 16, + }; + shelf.items.forEach((lockup, index) => { + if (isSome(lockup) && lockup instanceof models.Lockup) { + lockup.clickAction = injectCrossfireFlowForGameCenter(objectGraph, lockup.clickAction); + } + }); + } + if (shelfToken.featuredContentId === 557 /* groupingTypes.FeaturedContentID.AppStore_CategoryBreakoutMarker */) { + // Configure Category Breakout shelf + configureCategoryBreakoutShelf(objectGraph, shelfToken, shelf, shelfHeader, items, seeAllContents, groupingParseContext); + } + else if (!objectGraph.props.enabled("redownloadButtonTintUsingOfferTheme")) { + shelf.items.forEach((lockup, index) => { + if (isSome(lockup) && lockup instanceof models.Lockup) { + lockup.redownloadButtonTint = color.named("systemBlue"); + } + }); + } + // If no items should we display placeholders for this shelf? + const willHydrateShelfLater = shelf && serverData.isNullOrEmpty(shelf.items) && shelfToken.isFirstRender; + if (willHydrateShelfLater && placeholders.placeholdersEnabled(objectGraph)) { + placeholders.insertPlaceholdersIntoGenericPageShelf(objectGraph, shelf, shelfToken, shelfToken.featuredContentId); + } + // Create the action + let action; + if (groupingTypes.isTopChart(shelfToken.featuredContentId)) { + action = new models.FlowAction("topCharts"); + } + else { + action = new models.FlowAction("page"); + // The web client makes a separate request for See All pages, so we don't sidepack for web. + if (!placeholders.isPlaceholderShelf(shelf) && !objectGraph.client.isWeb) { + // Create the sidepacked room page + const preferredShelfStyle = preferredRoomSeeAllShelfStyle(objectGraph, shelfToken.shelfStyle); + action.pageData = room.seeAllPage(objectGraph, shelfToken.title, preferredShelfStyle); + } + } + if (objectGraph.client.isWeb) { + if (groupingTypes.isTopChart(shelfToken.featuredContentId)) { + const genre = (shelfToken.seeAllUrl && genreIdFromChartURL(shelfToken.seeAllUrl)) || shelfToken.pageGenreId || ""; + const destination = makeChartsPageIntent({ + ...getLocale(objectGraph), + ...getPlatform(objectGraph), + chart: shelfToken.chartIdentifier, + genreId: genre.toString(), + }); + action.destination = destination; + action.pageUrl = makeChartsPageURL(objectGraph, destination); + } + else { + const destination = makeRoomPageIntent({ + ...getLocale(objectGraph), + ...getPlatform(objectGraph), + id: shelfToken.id, + }); + action.destination = destination; + action.pageUrl = room.makeCanonicalRoomPageUrl(objectGraph, destination); + } + } + else { + action.pageUrl = shelfToken.seeAllUrl; + } + action.title = objectGraph.loc.string("ACTION_SEE_ALL"); + action.referrerUrl = shelfToken.metricsPageInformation.pageUrl; + metricsHelpersClicks.addClickEventToSeeAllAction(objectGraph, action, shelfToken.seeAllUrl, { + pageInformation: shelfToken.metricsPageInformation, + locationTracker: shelfToken.metricsLocationTracker, + }); + const shouldIncludeSeeAllAction = shouldShowSeeAll(objectGraph, shelf.items, shelfToken); + const seeAllActionIsValid = serverData.isDefinedNonNull(action.pageUrl) || serverData.isDefinedNonNull(action.pageData); + if (shouldIncludeSeeAllAction && seeAllActionIsValid) { + groupingShelfControllerCommon.replaceShelfHeaderSeeAllAction(objectGraph, shelfHeader, action); + } + if (shelfToken.shelfStyle === "screenshotsLockup" || shelfToken.shelfStyle === "appTrailerLockup") { + // `suppressTagline` is passed from MediaAPI as a string (see rdar://89933411) + const shouldShowSupplementaryText = mediaAttributes.attributeAsString(shelfToken.featuredContentData, "suppressTagline") !== "true"; + if (serverData.isNull(shelf.presentationHints)) { + shelf.presentationHints = { showSupplementaryText: shouldShowSupplementaryText }; + } + else { + shelf.presentationHints = { + ...shelf.presentationHints, + showSupplementaryText: shouldShowSupplementaryText, + }; + } + } + // On macOS, posters should take up the full width unless the shelf has enough items. + if (shelfToken.shelfStyle === "posterLockup" && objectGraph.client.isMac && shelf.items.length < 3) { + if (serverData.isNull(shelf.presentationHints)) { + shelf.presentationHints = { isLowDensity: true }; + } + else { + shelf.presentationHints = { ...shelf.presentationHints, isLowDensity: true }; + } + } + shelf.header = shelfHeader; + shelf.url = groupingShelfControllerCommon.createShelfTokenUrlIfNecessaryForShelf(objectGraph, shelf, shelfToken); + return shelf; +} +// endregion +// region Lockup Creation +/** + * Create a lockup for shelfContents to display within a grouping shelf + * @param objectGraph + * @param lockupData shelfContents to create lockup for. + * @param shelfToken Shelf shelfToken + */ +export function lockupFromData(objectGraph, lockupData, shelfToken) { + if (serverData.isNullOrEmpty(lockupData)) { + return null; + } + // Set the ordinal + let ordinalString; + if (shelfToken.showOrdinals) { + ordinalString = objectGraph.loc.decimal(shelfToken.ordinalIndex); + } + let offerStyle = null; + if (serverData.isDefinedNonNull(shelfToken.shelfBackground) && + (shelfToken.shelfBackground.type === "color" || shelfToken.shelfBackground.type === "interactive")) { + offerStyle = "white"; + } + let clientIdentifierOverride; + if (serverData.isDefinedNonNullNonEmpty(shelfToken)) { + clientIdentifierOverride = shelfToken.clientIdentifierOverride; + } + // Create the lockup + const lockupOptions = { + ordinal: ordinalString, + metricsOptions: { + pageInformation: shelfToken.metricsPageInformation, + locationTracker: shelfToken.metricsLocationTracker, + recoMetricsData: mediaDataStructure.metricsFromMediaApiObject(lockupData), + isAdvert: adLockups.isAdvert(objectGraph, lockupData), + }, + clientIdentifierOverride: clientIdentifierOverride, + artworkUseCase: content.artworkUseCaseFromShelfStyle(objectGraph, shelfToken.shelfStyle), + offerStyle: offerStyle, + canDisplayArcadeOfferButton: content.shelfContentTypeCanDisplayArcadeOfferButtons(objectGraph, shelfToken.shelfStyle), + isContainedInPreorderExclusiveShelf: shelfToken.featuredContentId === 497 /* groupingTypes.FeaturedContentID.AppStore_ComingSoon */, + shouldHideArcadeHeader: objectGraph.featureFlags.isEnabled("hide_arcade_header_on_arcade_tab") && shelfToken.isArcadePage, + }; + let lockup; + switch (shelfToken.shelfStyle) { + case "appTrailerLockup": + lockup = lockups.trailersLockupFromData(objectGraph, lockupData, lockupOptions, videoDefaults.defaultVideoConfiguration(objectGraph)); + break; + case "screenshotsLockup": + lockup = lockups.screenshotsLockupFromData(objectGraph, lockupData, lockupOptions); + break; + case "posterLockup": + lockup = lockups.posterLockupFromData(objectGraph, lockupData, lockupOptions); + break; + case "inAppPurchaseLockup": + case "inAppPurchaseTiledLockup": + lockup = lockups.inAppPurchaseLockupFromData(objectGraph, lockupData, lockupOptions); + break; + case "smallImageLockup": + case "mediumImageLockup": + case "largeImageLockup": + lockup = lockups.imageLockupFromData(objectGraph, lockupData, lockupOptions, CollectionShelfDisplayStyle.EditorialLockupLarge); + break; + default: + lockup = lockups.lockupFromData(objectGraph, lockupData, lockupOptions); + } + if (serverData.isNull(lockup) || !lockup.isValid()) { + return null; + } + return lockup; +} +// endregion +// region See All +/** + * Determine whether lockup shelves should show "See All" on given platforms. + * This covers both top charts shelves and lockup shelves. + * Special care should be taken to not make assumptions about the hydration state of the lockup shelves. + * + * @param objectGraph The App Store object graph. + * @param readyItems Items that will be vended as part of `shelf.items` in this shelf. This may be subset of items in shelf following subsequent fetch. + * @param shelfToken Grouping shelf shelfToken for shelf. This is used for determining whether shelf is part of initial page render, and to check whether there are more items to load. + * @return Whether or not a lockup shelf with given properties should have a "See All" action. + */ +export function shouldShowSeeAll(objectGraph, readyItems, shelfToken) { + // We do not want to attach "See All" if the grouping type is a Category Breakout Marker. + if (shelfToken.featuredContentId === 557 /* groupingTypes.FeaturedContentID.AppStore_CategoryBreakoutMarker */) { + return false; + } + // We do not want to attach "See All" if the grouping type is one of the Arcade Top Chart types + if (groupingTypes.isArcadeTopChart(shelfToken.featuredContentId)) { + return false; + } + // We do not want to attach "See All" if the grouping type is one of the Game Center Top Chart types + if (groupingTypes.isGameCenterTopChart(shelfToken.featuredContentId)) { + return false; + } + // On tvOS, "See All" is exposed as a clickable cell in-line with lockup cells. We want to show "See All" cells for: + // 1. Shelf that have completed loading, i.e. don't have `shelfToken.remainingItems`. + // 2. Lockup shelves that have *a lot* of content, which we clip within grouping shelves and show in full only in in the "See All" page. + // + // *Note* that we are *NOT* showing "See All" for shelves that have a "See All" url configured on tvOS. + // It appears that every shelf has a "See All" url, and that url returns the exact same contents as contents of shelf's `contents.data` as part of initial page fetch causing this: + // <rdar://problem/50230334> RFW4: Grouping: Shelves shouldn't have see all unless there's more to show. + // + // It appears we don't have capability to know in advance whether "See All" url has more to show than what is part of `contents.data` in initial page fetch. + // In our experiments, it looks like they are always the same. If this changes, we'd also want: + // const hasSeeAllRoom = shelfToken.seeAllUrl?.length > 0; + // if (hasSeeAllRoom) { + // return true; + // } + // + // In it's current form, we don't do (2), since we don't limit the number of lockups in a shelf we display within groupings. That is tracked under: + // <rdar://problem/50671635> Grouping: Limit number of items in shelves + if (objectGraph.client.isTV) { + // Whether or not this shelf is a partially hydrated shelf. This means there should be a secondary fetch request which causes the shelf will grow. + // We'll opt to *NEVER* show "See All" if the shelf will grow, and tack it on in the secondary shelf fetch. + const willHaveSubsequentFetch = serverData.isDefinedNonNullNonEmpty(shelfToken.remainingItems); + if (willHaveSubsequentFetch) { + return false; + } + // Whether or not this shelf is a nonempty top charts. + const isNonemptyTopCharts = groupingTypes.isTopChart(shelfToken.featuredContentId) && serverData.isDefinedNonNullNonEmpty(readyItems); + if (isNonemptyTopCharts) { + return true; + } + // Otherwise, don't show "See All" on tvOS. + return false; + } + // On all other platforms, we always to attach "See All", except AppStore_ComingSoon, + // which should not because the see all page won't drop lockups whose pre-order has + // been released (which the AppStore_ComingSoon swoosh will). + return shelfToken.featuredContentId !== 497 /* groupingTypes.FeaturedContentID.AppStore_ComingSoon */; +} +/** + * Coerces the provided shelf content type into a type suitable for use in a 'See All' room. + * @param objectGraph + * @param originalShelfStyle The content type that defines the 'See All' parent. + * @returns {models.ShelfContentType} The content type to use for the 'See All' room. Returns `null` if there is no + * preference, at which point the room builder will provide its default. + */ +export function preferredRoomSeeAllShelfStyle(objectGraph, originalShelfStyle) { + if (!originalShelfStyle) { + return null; + } + switch (originalShelfStyle) { + case "inAppPurchaseLockup": + case "inAppPurchaseTiledLockup": + return originalShelfStyle; + default: + return null; + } +} +// endregion +// region Category Breakout +export function configureCategoryBreakoutShelf(objectGraph, shelfToken, shelf, shelfHeader, items, seeAllContents, groupingParseContext) { + // If `seeAllContents` is not hydrated, fetch it from the relationship data. + if (isNothing(seeAllContents)) { + seeAllContents = mediaRelationship.relationshipData(objectGraph, shelfToken.featuredContentData, "see-all-contents"); + } + // Trailing artwork + shelfHeader.configuration.includeTrailingArtwork = true; + // If `seeAllContents` is sparse, add it to the remaining items to hydrate. + if (!mediaAttributes.hasAttributes(seeAllContents)) { + const badge = mediaAttributes.attributeAsString(shelfToken.featuredContentData, "name"); + shelfHeader.eyebrow = badge; + shelfHeader.title = ""; + shelf.footerTitle = ""; + shelfToken.isDeferring = true; + shelfToken.remainingItems.push(seeAllContents); + return; + } + // Badge + const badge = mediaAttributes.attributeAsString(shelfToken.featuredContentData, "name"); + shelfHeader.eyebrow = badge; + // Title + const editorialNotes = mediaAttributes.attributeAsDictionary(seeAllContents, "editorialNotes"); + const title = serverData.asString(editorialNotes, "name"); + shelfHeader.title = title; + // Configure location name to the updated title + metricsLocation.currentLocation(shelfToken.metricsLocationTracker).name = title; + // Configure impression name to the updated title if placeholder shelf used + if (serverData.isDefinedNonNullNonEmpty(shelfToken.originalPlaceholderShelfImpressionMetrics)) { + shelfToken.originalPlaceholderShelfImpressionMetrics.fields.name = title; + } + // Artwork + const editorialArtwork = mediaAttributes.attributeAsDictionary(seeAllContents, "editorialArtwork"); + // Drop the shelf if there is no editorial artwork. + if (isNothing(editorialArtwork)) { + shelf.isHidden = true; + return; + } + // Background artwork + const backgroundArtworkDict = serverData.asDictionary(editorialArtwork, "storyBackgroundStatic16x9"); + const backgroundArtworkOptions = { + useCase: 28 /* content.ArtworkUseCase.CategoryBreakoutShelf */, + withJoeColorPlaceholder: true, + }; + const backgroundArtwork = groupingShelfControllerCommon.groupingArtworkFromApiArtwork(objectGraph, backgroundArtworkDict, backgroundArtworkOptions); + const backgroundStyle = color.isDarkColor(backgroundArtwork.backgroundColor) ? "dark" : "light"; + shelf.background = { + type: "artwork", + artwork: backgroundArtwork, + style: backgroundStyle, + }; + // Lockup offerDisplayProperties + items.forEach((lockup, index) => { + if (isSome(lockup) && lockup instanceof models.Lockup) { + lockup.offerDisplayProperties = lockup.offerDisplayProperties.newOfferDisplayPropertiesChangingAppearance(false, "transparent", backgroundStyle); + lockup.subtitleTextFilter = "plusLight"; + if (!objectGraph.props.enabled("redownloadButtonTintUsingOfferTheme")) { + lockup.redownloadButtonTint = color.named("white"); + } + } + }); + // Trailing artwork + const trailingArtworkDict = serverData.asDictionary(editorialArtwork, "contentGraphicTrimmed"); + // Drop the shelf if there is no trailing artwork. + if (isNothing(trailingArtworkDict)) { + shelf.isHidden = true; + return; + } + const trailingArtworkOptions = { + contentMode: modelsBase.ArtworkContentMode.scaleAspectFit, + useCase: 18 /* content.ArtworkUseCase.GroupingBrick */, + }; + const trailingArtwork = groupingShelfControllerCommon.groupingArtworkFromApiArtwork(objectGraph, trailingArtworkDict, trailingArtworkOptions); + shelfHeader.trailingArtwork = trailingArtwork; + // Footer + const actionUrl = serverData.asString(seeAllContents.attributes, "url"); + const footerTitle = serverData.asString(seeAllContents.attributes, "breakoutCallToActionLabel"); + const clickOptions = { + id: seeAllContents.id, + idType: "its_id", + targetType: "button", + actionType: "navigate", + pageInformation: shelfToken.metricsPageInformation, + locationTracker: shelfToken.metricsLocationTracker, + }; + if (isSome(actionUrl)) { + // Footer title + shelf.footerTitle = footerTitle; + // Footer action + const flowPage = objectGraph.required(pageRouter).fetchFlowPage(actionUrl); + const flowAction = new models.FlowAction(flowPage); + flowAction.title = footerTitle; + flowAction.pageUrl = actionUrl; + metricsHelpersClicks.addClickEventToAction(objectGraph, flowAction, clickOptions); + shelf.footerAction = flowAction; + } + else { + const metricsOptions = { + targetType: "button", + pageInformation: shelfToken.metricsPageInformation, + locationTracker: shelfToken.metricsLocationTracker, + recoMetricsData: mediaDataStructure.metricsFromMediaApiObject(seeAllContents), + }; + const metadata = groupingShelfControllerCommon.metadataForFCData(objectGraph, seeAllContents, shelfToken, false, null, metricsOptions, groupingParseContext); + if (isSome(metadata.action)) { + // Clear `actionMetrics` before adding a click event with updated `title`. + metadata.action.actionMetrics.clearAll(); + metadata.action.title = footerTitle !== null && footerTitle !== void 0 ? footerTitle : serverData.asString(editorialNotes, "callToAction"); + metricsHelpersClicks.addClickEventToAction(objectGraph, metadata.action, clickOptions); + shelf.footerAction = metadata.action; + shelf.footerTitle = metadata.action.title; + } + } +} +// endregion +// region Ads +/** + * Performs `lockupFromData`, but additional with Ad stitch related side-effects. + * @param objectGraph The AppStore dependency graph + * @param adStitcher Stitcher to source shelfContents from. May be undefined + * @param adIncidentRecorder Incident recorder for adding ads. + * @param positionInfo Position this ad is being stitched into. + * @param shelfToken shelfToken for shelf this ad lockup will be in. + */ +export function lockupFromAdStitcher(objectGraph, adStitcher, adIncidentRecorder, positionInfo, shelfToken) { + const task = adStitch.consumeTask(adStitcher, positionInfo); + if (serverData.isNull(task)) { + return null; // no task for position + } + // Try to create lockup + const lockupData = task.data; + const lockup = lockupFromData(objectGraph, lockupData, shelfToken); + if (serverData.isDefinedNonNull(lockup)) { + shelfToken.includedAdAdamIds = [lockupData.id]; + } + else { + adIncidents.recordLockupFromDataFailed(objectGraph, adIncidentRecorder, lockupData); + } + return lockup; +} +// endregion +//# sourceMappingURL=grouping-lockup-shelf-controller-common.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/grouping/shelf-controllers/grouping-lockup-shelf-controller.js b/node_modules/@jet-app/app-store/tmp/src/common/grouping/shelf-controllers/grouping-lockup-shelf-controller.js new file mode 100644 index 0000000..211db36 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/grouping/shelf-controllers/grouping-lockup-shelf-controller.js @@ -0,0 +1,104 @@ +import * as serverData from "../../../foundation/json-parsing/server-data"; +import * as mediaRelationship from "../../../foundation/media/relationships"; +import * as mediaAttributes from "../../../foundation/media/attributes"; +import * as groupingTypes from "../grouping-types"; +import { createLockupShelf, lockupShelfTokenFromBaseTokenAndMediaApiData, } from "./grouping-lockup-shelf-controller-common"; +import { GroupingShelfController } from "./grouping-shelf-controller"; +export class GroupingLockupShelfController extends GroupingShelfController { + // region Constructors + constructor(builderClass = null) { + super(builderClass || "GroupingLockupShelfController"); + this.supportedFeaturedContentIds = new Set([ + ...groupingTypes.topChartFeaturedContentIds, + ...groupingTypes.lockupShelfFeaturedContentIds, + ]); + } + // endregion + // region Shelf Creation Prerequisites + /** + * For a given mediaApiData extract the actual shelfContents array needed to render this shelf + * + * @param mediaApiData The outer shelfContents object containing the shelf contents + */ + initialShelfDataFromGroupingMediaApiData(objectGraph, mediaApiData) { + const shelfContents = mediaRelationship.relationship(mediaApiData, "contents"); + let shelfData = shelfContents ? shelfContents.data : null; + if (!shelfData || shelfData.length === 0) { + shelfData = mediaRelationship.relationshipCollection(mediaApiData, "children"); + } + return { shelfContents: shelfData }; + } + /** + * For a given url that this controller handles, we should return a promise that will result in the `ShelfData` + * needed to render this shelf + * + * @param objectGraph The App Store dependency graph + * @param shelfUrl The url that this controller handled on a secondary fetch + * @param parameters The extracted parameters from the shelf url + */ + async secondaryShelfDataForShelfUrl(objectGraph, shelfUrl, shelfToken, parameters) { + return await GroupingShelfController.secondaryGroupingShelfDataForShelfUrl(objectGraph, shelfUrl, shelfToken, parameters); + } + /** + * For a given mediaApiData create an updated shelf token that contains all the additional data for this specific shelf type + * + * @param objectGraph The App Store dependency graph + * @param baseShelfToken The base grouping shelf token created by the grouping-controller + * @param mediaApiData The outer data object containing the FC properties and data + * @param groupingParseContext The parse context for the grouping page so far + */ + shelfTokenFromBaseTokenAndMediaApiData(objectGraph, mediaApiData, baseShelfToken, groupingParseContext) { + return lockupShelfTokenFromBaseTokenAndMediaApiData(objectGraph, mediaApiData, baseShelfToken, groupingParseContext); + } + // endregion + // region Metrics + /** + * Return the shelf metrics options to use for this specific shelf. Using the base options from the grouping + * page controller + * + * @param objectGraph The App Store dependency graph + * @param shelfToken The shelf shelfToken for this current shelf creation request + * @param baseMetricsOptions The minimum set of metrics options for this shelf, created by the + * grouping page controller + */ + shelfMetricsOptionsFromBaseMetricsOptions(objectGraph, shelfToken, baseMetricsOptions) { + const shelfMetricsOptions = { ...baseMetricsOptions }; + shelfMetricsOptions.displayStyle = shelfToken.shelfStyle; + // Reconfigure Category Breakout metrics options + if (shelfToken.featuredContentId === 557 /* groupingTypes.FeaturedContentID.AppStore_CategoryBreakoutMarker */) { + const seeAllContents = mediaRelationship.relationshipData(objectGraph, shelfToken.featuredContentData, "see-all-contents"); + const editorialNotes = mediaAttributes.attributeAsDictionary(seeAllContents, "editorialNotes"); + const title = serverData.asString(editorialNotes, "name"); + shelfMetricsOptions.title = title; + shelfMetricsOptions.idType = "its_contentId"; + shelfMetricsOptions.badges = { forYou: true }; + shelfMetricsOptions.targetType = "swooshBreakout"; + } + return shelfMetricsOptions; + } + // endregion + // region Shelf Creation + /** + * + * @param objectGraph The App Store dependency graph + * @param shelfToken The shelf shelfToken for this current shelf creation request + * @param shelfData The media api shelfContents array for this shelf + * @param groupingParseContext The parse context used to generate the grouping page on the initial page load, + * this will be missing when this controller renders a secondary or incomplete shelf fetch. + */ + _createShelf(objectGraph, shelfToken, shelfData, groupingParseContext) { + const isCategoryBreakoutShelf = shelfToken.featuredContentId === 557 /* groupingTypes.FeaturedContentID.AppStore_CategoryBreakoutMarker */; + if (isCategoryBreakoutShelf) { + const hasNoContents = !serverData.isDefinedNonNullNonEmpty(shelfData.shelfContents); + const isClientPad = objectGraph.client.isPad; + // Drop Category Breakout IF + // - It has no contents OR + // - Client is an iPad OR + if (hasNoContents || isClientPad) { + return null; + } + } + return createLockupShelf(objectGraph, shelfToken, shelfData, groupingParseContext); + } +} +//# sourceMappingURL=grouping-lockup-shelf-controller.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/grouping/shelf-controllers/grouping-personalized-lockup-shelf-controller.js b/node_modules/@jet-app/app-store/tmp/src/common/grouping/shelf-controllers/grouping-personalized-lockup-shelf-controller.js new file mode 100644 index 0000000..7e28d30 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/grouping/shelf-controllers/grouping-personalized-lockup-shelf-controller.js @@ -0,0 +1,368 @@ +import * as validation from "@jet/environment/json/validation"; +import * as models from "../../../api/models"; +import * as serverData from "../../../foundation/json-parsing/server-data"; +import * as mediaAttributes from "../../../foundation/media/attributes"; +import * as mediaDataStructure from "../../../foundation/media/data-structure"; +import * as mediaNetwork from "../../../foundation/media/network"; +import * as mediaRelationship from "../../../foundation/media/relationships"; +import { ResponseMetadata } from "../../../foundation/network/network"; +import { Parameters, Path, Protocol } from "../../../foundation/network/url-constants"; +import * as urls from "../../../foundation/network/urls"; +import * as metricsHelpersImpressions from "../../metrics/helpers/impressions"; +import * as metricsHelpersLocation from "../../metrics/helpers/location"; +import * as odpCommon from "../../personalization/on-device-recommendations-common"; +import { defaultRoomShelfContentType } from "../../room/room-page"; +import { platformPrefersLargeTitles } from "../../room/room-common"; +import * as groupingTypes from "../grouping-types"; +import { createLockupShelf, lockupShelfTokenFromBaseTokenAndMediaApiData, } from "./grouping-lockup-shelf-controller-common"; +import { GroupingShelfController, routesForFeaturedContentIds } from "./grouping-shelf-controller"; +import * as groupingShelfControllerCommon from "./grouping-shelf-controller-common"; +import { isNothing, isSome } from "@jet/environment"; +export class GroupingPersonalizedLockupShelfController extends GroupingShelfController { + // region Constructors + constructor() { + super("GroupingPersonalizedLockupShelfController"); + this.supportedFeaturedContentIds = groupingTypes.recommendationsLockupShelfFeaturedContentIds; + } + // endregion + // region Shelf Builder + shelfRoute(objectGraph) { + return routesForFeaturedContentIds(this.supportedFeaturedContentIds, [ + `${Parameters.isOnDeviceRecommendationsShelf}?`, + `${Parameters.onDeviceRecommendationsUseCase}?`, + ]); + } + // endregion + // region Shelf Creation Prerequisites + /** + * For a given mediaApiData extract the actual shelfContents array needed to render this shelf + * + * @param mediaApiData The outer shelfContents object containing the shelf contents + */ + initialShelfDataFromGroupingMediaApiData(objectGraph, mediaApiData) { + const shelfContents = mediaRelationship.relationship(mediaApiData, "contents"); + let shelfData = shelfContents ? shelfContents.data : null; + if (!shelfData || shelfData.length === 0) { + shelfData = mediaRelationship.relationshipCollection(mediaApiData, "children"); + } + const recoMetrics = mediaDataStructure.metricsFromMediaApiObject(shelfContents); + return { + shelfContents: shelfData || [], + containsODPShelfContents: false, + recoMetrics: recoMetrics, + candidates: null, + isHiddenShelf: objectGraph.client.isWeb, + responseTimingValues: null, + }; + } + /** + * For a given url that this controller handles, we should return a promise that will result in the `ShelfData` + * needed to render this shelf + * + * @param objectGraph The App Store dependency graph + * @param shelfUrl The url that this controller handled on a secondary fetch + * @param parameters The extracted parameters from the shelf url + */ + async secondaryShelfDataForShelfUrl(objectGraph, shelfUrl, shelfToken, parameters) { + var _a; + const onDeviceRecommendationsUseCase = parameters[Parameters.onDeviceRecommendationsUseCase]; + if ((onDeviceRecommendationsUseCase === null || onDeviceRecommendationsUseCase === void 0 ? void 0 : onDeviceRecommendationsUseCase.length) > 0) { + return await odpCommon + .recommendedAppsForUseCase(objectGraph, onDeviceRecommendationsUseCase, "shelf") + .then((response) => { + return { + shelfContents: mediaDataStructure.dataCollectionFromDataContainer(response.dataContainer), + containsODPShelfContents: true, + recoMetrics: response.recoMetrics, + candidates: response.candidates, + isHiddenShelf: false, + }; + }) + .catch(async (error) => { + if (error instanceof odpCommon.NoODPCandidatesError) { + return await GroupingPersonalizedLockupShelfController.fetchODPFallbackContent(objectGraph, shelfUrl, shelfToken, parameters).catch((fallbackError) => { + return GroupingPersonalizedLockupShelfController.makeHiddenShelfData(shelfToken); + }); + } + else { + return GroupingPersonalizedLockupShelfController.makeHiddenShelfData(shelfToken); + } + }); + } + else if (((_a = shelfToken.recommendationsHref) === null || _a === void 0 ? void 0 : _a.length) > 0) { + return await GroupingShelfController.secondaryGroupingShelfMediaApiData(objectGraph, shelfUrl, shelfToken, parameters).then((mediaApiData) => { + const personalizedFeaturedContentData = mediaDataStructure.dataFromDataContainer(objectGraph, mediaApiData); + const personalizedLockupShelfData = this.initialShelfDataFromGroupingMediaApiData(objectGraph, personalizedFeaturedContentData); + personalizedLockupShelfData.responseTimingValues = mediaApiData[ResponseMetadata.timingValues]; + personalizedLockupShelfData.shelfTitle = mediaAttributes.attributeAsString(personalizedFeaturedContentData, "name"); + return personalizedLockupShelfData; + }); + } + else { + return await GroupingShelfController.secondaryGroupingShelfMediaApiData(objectGraph, shelfUrl, shelfToken, parameters).then((mediaApiData) => { + const personalizedFeaturedContentData = mediaDataStructure.dataCollectionFromDataContainer(mediaApiData); + const personalizedLockupShelfData = { + shelfContents: personalizedFeaturedContentData || [], + containsODPShelfContents: false, + recoMetrics: null, + candidates: null, + isHiddenShelf: false, + responseTimingValues: mediaApiData[ResponseMetadata.timingValues], + }; + personalizedLockupShelfData.shelfTitle = shelfToken.title; + return personalizedLockupShelfData; + }); + } + } + /** + * For a given mediaApiData create an updated shelf token that contains all the additional data for this specific shelf type + * + * @param objectGraph The App Store dependency graph + * @param baseShelfToken The base grouping shelf token created by the grouping-controller + * @param mediaApiData The outer data object containing the FC properties and data + * @param groupingParseContext The parse context for the grouping page so far + */ + shelfTokenFromBaseTokenAndMediaApiData(objectGraph, mediaApiData, baseShelfToken, groupingParseContext) { + const personalizedLockupShelfToken = lockupShelfTokenFromBaseTokenAndMediaApiData(objectGraph, mediaApiData, baseShelfToken, groupingParseContext); + this.addPersonalizationValuesToShelfToken(objectGraph, personalizedLockupShelfToken, mediaApiData, groupingParseContext); + return personalizedLockupShelfToken; + } + // endregion + // region Shelf Creation + /** + * + * @param objectGraph The App Store dependency graph + * @param shelfToken The shelf shelfToken for this current shelf creation request + * @param shelfData The media api shelfContents array for this shelf + * @param groupingParseContext The parse context used to generate the grouping page on the initial page load, + * this will be missing when this controller renders a secondary or incomplete shelf fetch. + */ + _createShelf(objectGraph, shelfToken, shelfData, groupingParseContext) { + var _a, _b; + if (!shelfToken.isValidRecommendationsShelf) { + return null; + } + let shelf = null; + if (isNothing(shelfToken.title)) { + // If we've fetched the title in a secondary lookup, update the title here + // in the token, since other places may rely on it. + shelfToken.title = shelfData.shelfTitle; + } + if (shelfData.containsODPShelfContents) { + shelf = this.personalizedShelf(objectGraph, shelfData, shelfToken, groupingParseContext); + const seeAllUrl = new urls.URL() + .set("protocol", Protocol.internal) + .path(`${Path.onDeviceRecommendations}`) + .param(Parameters.onDeviceRecommendationsUseCase, shelfToken.onDeviceRecommendationsUseCase) + .param(Parameters.token, JSON.stringify(shelfToken)) + .build(); + const seeAllAction = new models.FlowAction("page", seeAllUrl); + seeAllAction.title = objectGraph.loc.string("ACTION_SEE_ALL"); + seeAllAction.pageData = this.odpSeeAllPage(objectGraph, shelfData.shelfTitle, defaultRoomShelfContentType); + groupingShelfControllerCommon.replaceShelfSeeAllAction(objectGraph, shelf, seeAllAction); + shelf.mergeWhenFetched = false; + } + else if (shelfData.isHiddenShelf) { + shelf = GroupingPersonalizedLockupShelfController.makeHiddenShelf(shelfToken); + } + else if (serverData.isDefinedNonNull(shelfToken.recommendationsHref)) { + shelf = this.personalizedShelf(objectGraph, shelfData, shelfToken, groupingParseContext); + shelf.url = this.addOnDeviceQueryParamsIfNecessary(objectGraph, shelf.url, shelfToken); + } + else { + shelf = createLockupShelf(objectGraph, shelfToken, shelfData, groupingParseContext); + shelf.url = this.addOnDeviceQueryParamsIfNecessary(objectGraph, shelf.url, shelfToken); + } + const isUsingHeader = isSome(shelf.header); + const isMissingHeaderTitle = isUsingHeader && serverData.isNullOrEmpty((_a = shelf.header) === null || _a === void 0 ? void 0 : _a.title); + const isMissingLegacyShelfTitle = !isUsingHeader && serverData.isNullOrEmpty(shelf.title); + const hasShelfDataTitle = ((_b = shelfData.shelfTitle) === null || _b === void 0 ? void 0 : _b.length) > 0; + if (isMissingHeaderTitle && hasShelfDataTitle) { + shelf.header.title = shelfData.shelfTitle; + } + else if (isMissingLegacyShelfTitle && hasShelfDataTitle) { + shelf.title = shelfData.shelfTitle; + } + return shelf; + } + // endregion + // region Helpers + /** + * + * @param objectGraph The App Store dependency graph + * @param shelfData The media api shelfContents array for this shelf + * @param shelfToken The shelf shelfToken for this current shelf creation request + * @param groupingParseContext The parse context used to generate the grouping page on the initial page load, + * @private + */ + personalizedShelf(objectGraph, shelfData, shelfToken, groupingParseContext) { + /** + * Update shelf impressions for personalized shelves. + * <rdar://problem/69308786> reco_algo_id doesn't get passed to Figaro for out of band personalization marker + * + * By happenstance, the location tracker encoded in the shelf token usually contains the swoosh location, given the happy path fetches *more* items. + * For Personalized shelves, we need to pop the "empty" shelf location and replace it with the fully populated one. + */ + const location = metricsHelpersLocation.currentLocation(shelfToken.metricsLocationTracker); + if (location && location.fcKind === shelfToken.featuredContentId) { + metricsHelpersLocation.popLocation(shelfToken.metricsLocationTracker); + } + const shelfMetricsOptions = { + id: shelfToken.id, + kind: null, + softwareType: serverData.asBooleanOrFalse(groupingParseContext === null || groupingParseContext === void 0 ? void 0 : groupingParseContext.isArcadePage) ? "Arcade" : null, + targetType: "swoosh", + title: shelfToken.title, + pageInformation: shelfToken.metricsPageInformation, + locationTracker: shelfToken.metricsLocationTracker, + idType: "its_contentId", + fcKind: shelfToken.featuredContentId, + recoMetricsData: shelfData.recoMetrics, + displayStyle: shelfToken.shelfStyle, + }; + metricsHelpersLocation.pushContentLocation(objectGraph, shelfMetricsOptions, shelfToken.title); + const shelf = createLockupShelf(objectGraph, shelfToken, shelfData, groupingParseContext); + metricsHelpersLocation.popLocation(shelfToken.metricsLocationTracker); + metricsHelpersImpressions.addImpressionFields(objectGraph, shelf, shelfMetricsOptions); + return shelf; + } + /** + * Check in the media api data for this shelf and add any of the necessary personalization fields to the shelfToken. + * + * @param objectGrpah + * @param shelfToken The shelf token in its current state + * @param mediaApiData The media api data for this section of the grouping page + * @param groupingParseContext The parse context for the grouping page so far + * @private + */ + addPersonalizationValuesToShelfToken(objectGrpah, shelfToken, mediaApiData, groupingParseContext) { + const isPersonalizedShelfMarker = shelfToken.featuredContentId === 476 /* groupingTypes.FeaturedContentID.AppStore_PersonalizedShelfMarker */; + // If we don't have a logged in user don't show personalized shelves + // For search grouping we allow these shelves even if there is no logged in user. + // For the personalized shelf markers we are going to allow reco to send back a fallback list of ids so that + // even logged out users can see these shelves. + // rdar://64005675 (Client to not drop personalized marker response for non-signed users (dsId - 0)) + if (!groupingParseContext.hasAuthenticatedUser && + !shelfToken.isSearchLandingPage && + !isPersonalizedShelfMarker) { + objectGrpah.console.log(`Skipping recommendations shelf with fcID ${shelfToken.featuredContentId}: No user logged-in`); + return; + } + const onDevicePersonalizationUseCase = mediaAttributes.attributeAsString(mediaApiData, "onDevicePersonalizationUseCase"); + const usingOnDevicePersonalization = (onDevicePersonalizationUseCase === null || onDevicePersonalizationUseCase === void 0 ? void 0 : onDevicePersonalizationUseCase.length) > 0; + if (usingOnDevicePersonalization) { + shelfToken.onDeviceRecommendationsUseCase = onDevicePersonalizationUseCase; + shelfToken.recommendationsHref = mediaApiData.href; + shelfToken.isValidRecommendationsShelf = true; + return; + } + const shelfContents = mediaRelationship.relationshipCollection(mediaApiData, "contents"); + const hasContents = serverData.isDefinedNonNullNonEmpty(shelfContents); + const shelfPersonalizationAvailable = !mediaAttributes.attributeAsBooleanOrFalse(mediaApiData, "noPersonalizationAvailable"); + if (!hasContents && shelfPersonalizationAvailable) { + shelfToken.recommendationsHref = mediaApiData.href; + shelfToken.isValidRecommendationsShelf = true; + // tslint:disable-next-line:no-redundant-jump + } + else { + shelfToken.isValidRecommendationsShelf = hasContents; + } + } + /** + * For a given shelf url, add the on device personalization query param if its needed + * + * @param objectGraph + * @param urlString The original url string for this shelf + * @param shelfToken The shelf shelfToken for this current shelf creation request + */ + addOnDeviceQueryParamsIfNecessary(objectGraph, urlString, shelfToken) { + var _a; + if (serverData.isNullOrEmpty(urlString)) { + return null; + } + const isOnDeviceRecommendationsGamesForYouEnabled = objectGraph.host.isiOS; + if (!isOnDeviceRecommendationsGamesForYouEnabled) { + return urlString; + } + if (((_a = shelfToken.onDeviceRecommendationsUseCase) === null || _a === void 0 ? void 0 : _a.length) > 0) { + const url = new urls.URL(urlString); + url.param(Parameters.isOnDeviceRecommendationsShelf, "true"); + url.param(Parameters.onDeviceRecommendationsUseCase, shelfToken.onDeviceRecommendationsUseCase); + return url.build(); + } + else { + return urlString; + } + } + /** + * Creates a page that can be used for side-packing see all pages into a room. + * + * @param objectGraph + * @param {string} title The title of the destination page + * @param {ShelfContentType} preferredShelfContentType The content type to use for the page + * @returns {GenericPage} A GenericPage which will use the parentShelfItems from the see all to render the initial room + */ + odpSeeAllPage(objectGraph, title, preferredShelfContentType) { + const shelf = new models.Shelf(preferredShelfContentType || defaultRoomShelfContentType); + shelf.isHorizontal = false; + shelf.items = "parentShelfItems"; + const page = new models.GenericPage([shelf]); + page.isIncomplete = true; + page.title = title; + if (platformPrefersLargeTitles(objectGraph)) { + page.presentationOptions = ["prefersLargeTitle"]; + } + return page; + } + // endregion + // region ODP Fallback + static async fetchODPFallbackContent(objectGraph, url, shelfToken, parameters) { + return await new Promise((resolve, reject) => { + const fallbackRequest = groupingShelfControllerCommon.generateShelfRequest(objectGraph, shelfToken, parameters); + if (!fallbackRequest) { + const errorMessage = `OnDeviceRecommendationsShelfController: Could not construct media API request for: ${url}`; + validation.unexpectedType("defaultValue", errorMessage, null); + reject(new Error(errorMessage)); + return; + } + groupingShelfControllerCommon.prepareGroupingShelfRequest(objectGraph, fallbackRequest); + fallbackRequest.attributingTo(url.build()); + mediaNetwork + .fetchData(objectGraph, fallbackRequest) + .then((dataContainer) => { + const shelfData = mediaDataStructure.dataFromDataContainer(objectGraph, dataContainer); + const shelfContents = mediaRelationship.relationship(shelfData, "contents"); + const recoMetrics = mediaDataStructure.metricsFromMediaApiObject(shelfContents); + const fallbackShelfData = { + shelfContents: mediaDataStructure.dataCollectionFromDataContainer(shelfContents), + containsODPShelfContents: false, + recoMetrics: recoMetrics, + candidates: null, + responseTimingValues: dataContainer[ResponseMetadata.timingValues], + }; + resolve(fallbackShelfData); + }) + .catch((error) => { + const errorMessage = `OnDeviceRecommendationsShelfController: Failed to fetch fallback shelf contents: ${url}`; + validation.unexpectedType("defaultValue", errorMessage, null); + reject(error); + }); + }); + } + static makeHiddenShelfData(shelfToken) { + const hiddenShelfData = { + shelfContents: [], + containsODPShelfContents: false, + recoMetrics: null, + candidates: null, + isHiddenShelf: true, + }; + return hiddenShelfData; + } + static makeHiddenShelf(shelfToken) { + const hiddenShelf = new models.Shelf(shelfToken.shelfStyle); + hiddenShelf.isHidden = true; + return hiddenShelf; + } +} +//# sourceMappingURL=grouping-personalized-lockup-shelf-controller.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/grouping/shelf-controllers/grouping-ribbon-bar-shelf-controller.js b/node_modules/@jet-app/app-store/tmp/src/common/grouping/shelf-controllers/grouping-ribbon-bar-shelf-controller.js new file mode 100644 index 0000000..bc0211c --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/grouping/shelf-controllers/grouping-ribbon-bar-shelf-controller.js @@ -0,0 +1,269 @@ +import { attributeAsString } from "@apple-media-services/media-api"; +import { isNothing, isSome } from "@jet/environment"; +import * as models from "../../../api/models"; +import * as serverData from "../../../foundation/json-parsing/server-data"; +import * as mediaDataStructure from "../../../foundation/media/data-structure"; +import * as mediaRelationship from "../../../foundation/media/relationships"; +import { seeAllArcadeGamesPageFlowAction } from "../../arcade/arcade-common"; +import { pageRouter } from "../../builders/routing"; +import { hrefToRoutableUrl } from "../../builders/url-mapping"; +import { categoryArtworkData } from "../../categories"; +import * as artworkBuilder from "../../content/artwork/artwork"; +import * as content from "../../content/content"; +import * as lockups from "../../lockups/lockups"; +import * as metricsHelpersClicks from "../../metrics/helpers/clicks"; +import * as metricsHelpersImpressions from "../../metrics/helpers/impressions"; +import * as metricsHelpersLocation from "../../metrics/helpers/location"; +import { areAppTagsEnabled } from "../../util/app-tags-util"; +import * as lottery from "../../util/lottery"; +import { GroupingShelfController } from "./grouping-shelf-controller"; +import * as groupingShelfControllerCommon from "./grouping-shelf-controller-common"; +export class GroupingRibbonBarShelfController extends GroupingShelfController { + // region Constructors + constructor() { + super("GroupingRibbonBarShelfController"); + this.supportedFeaturedContentIds = new Set([556 /* groupingTypes.FeaturedContentID.AppStore_RibbonBarMarker */]); + } + // endregion + // region Shelf Creation Prerequisites + /** + * For a given mediaApiData extract the actual shelfContents array needed to render this shelf + * + * @param mediaApiData The outer shelfContents object containing the shelf contents + */ + initialShelfDataFromGroupingMediaApiData(objectGraph, mediaApiData) { + return { shelfContents: mediaRelationship.relationshipCollection(mediaApiData, "contents") }; + } + /** + * For a given url that this controller handles, we should return a promise that will result in the `ShelfData` + * needed to render this shelf + * + * @param objectGraph The App Store dependency graph + * @param shelfUrl The url that this controller handled on a secondary fetch + * @param parameters The extracted parameters from the shelf url + */ + async secondaryShelfDataForShelfUrl(objectGraph, shelfUrl, shelfToken, parameters) { + return await GroupingShelfController.secondaryGroupingShelfDataForShelfUrl(objectGraph, shelfUrl, shelfToken, parameters); + } + /** + * For a given mediaApiData create an updated shelf token that contains all the additional data for this specific shelf type + * + * @param objectGraph The App Store dependency graph + * @param baseShelfToken The base grouping shelf token created by the grouping-controller + * @param mediaApiData The outer data object containing the FC properties and data + * @param groupingParseContext The parse context for the grouping page so far + */ + shelfTokenFromBaseTokenAndMediaApiData(objectGraph, mediaApiData, baseShelfToken, groupingParseContext) { + return baseShelfToken; + } + // endregion + // region Shelf Creation + /** + * + * @param objectGraph The App Store dependency graph + * @param shelfToken The shelf shelfToken for this current shelf creation request + * @param shelfData The media api shelfContents array for this shelf + * @param groupingParseContext The parse context used to generate the grouping page on the initial page load, + * this will be missing when this controller renders a secondary or incomplete shelf fetch. + */ + _createShelf(objectGraph, shelfToken, shelfData, groupingParseContext) { + var _a; + const items = []; + const shelf = new models.Shelf("ribbonBar"); + shelf.isHorizontal = true; + const isSAGUpliftEnabledForCurrentUser = lottery.isFeatureEnabledForCurrentUser(objectGraph, objectGraph.bag.arcadeCategoryBarSAGUpliftDisplayRate); + // Display See All Games facet when: + // - This is an Arcade page AND + // - This is the first render of the shelf AND + // - Bag is enabled for current user + if (shelfToken.isArcadePage && shelfToken.isFirstRender && isSAGUpliftEnabledForCurrentUser) { + const seeAllGamesRibbonItem = GroupingRibbonBarShelfController.createSeeAllGamesRibbonItem(objectGraph, shelfToken.metricsPageInformation, shelfToken.metricsLocationTracker); + items.push(seeAllGamesRibbonItem); + metricsHelpersLocation.nextPosition(shelfToken.metricsLocationTracker); + } + // If we have any hydrated items stored in the token, it means we found an unhydrated priorized item. In this + // case, we combine the initial hydrated items with the secondary hydrated items, and build the entire set + // of ribbon items from this list. As we are manipulating the order, we also need to replace the original + // shelf rather than merge. + let combinedShelfContents = (_a = shelfData.shelfContents) !== null && _a !== void 0 ? _a : []; + if (isSome(shelfToken.initialHydratedItems) && shelfToken.initialHydratedItems.length > 0) { + combinedShelfContents = shelfToken.initialHydratedItems.concat(combinedShelfContents); + } + // Tracks the initial set of hydrated items from the initial shelf load. This will be stored in the shelf + // token only if we find a prioritized item that is deferred. + const initialHydratedItems = []; + let isPrioritizedItemDeferred = false; + for (const ribbonData of combinedShelfContents) { + if (serverData.isNull(ribbonData.attributes) || groupingShelfControllerCommon.shouldDefer(shelfToken)) { + shelfToken.isDeferring = true; + shelfToken.remainingItems.push(ribbonData); + if (GroupingRibbonBarShelfController.shouldPriorizeItemWithData(objectGraph, ribbonData)) { + isPrioritizedItemDeferred = true; + } + continue; + } + let isTextOnly = false; + if (isSome(shelfToken.featuredContentData)) { + const displayStyle = attributeAsString(shelfToken.featuredContentData, "displayStyle"); + isTextOnly = displayStyle === "textOnly"; + } + const ribbonModel = GroupingRibbonBarShelfController.createRibbonItem(objectGraph, ribbonData, shelfToken.metricsPageInformation, shelfToken.metricsLocationTracker, isTextOnly, shelfToken, groupingParseContext); + if (isSome(ribbonModel)) { + if (GroupingRibbonBarShelfController.shouldPriorizeItemWithData(objectGraph, ribbonData)) { + items.unshift(ribbonModel); + } + else { + items.push(ribbonModel); + } + initialHydratedItems.push(ribbonData); + } + metricsHelpersLocation.nextPosition(shelfToken.metricsLocationTracker); + } + if (objectGraph.client.isiOS || + objectGraph.featureFlags.isEnabled("shelves_2_0_arcade") || + objectGraph.featureFlags.isEnabled("shelves_2_0_generic")) { + shelf.items = items; + if (isPrioritizedItemDeferred && initialHydratedItems.length > 0) { + shelfToken.initialHydratedItems = initialHydratedItems; + } + } + else { + // Only set `shelf.items` if there are any `items` present + // so that hydrated items reload when they are fetched. + if (items.length > 0) { + const ribbonBar = new models.RibbonBar(items); + shelf.items = [ribbonBar]; + } + } + shelf.url = groupingShelfControllerCommon.createShelfTokenUrlIfNecessaryForShelf(objectGraph, shelf, shelfToken); + return shelf; + } + // region Static Helpers + static createRibbonItem(objectGraph, itemData, metricsPageInformation, metricsLocationTracker, isTextOnly, shelfToken, groupingParseContext) { + var _a, _b, _c; + const metricsOptions = { + targetType: "facet", + pageInformation: metricsPageInformation, + locationTracker: metricsLocationTracker, + recoMetricsData: mediaDataStructure.metricsFromMediaApiObject(itemData), + }; + let metadata; + if (itemData.type === "tags") { + if (!areAppTagsEnabled(objectGraph, "grouping")) { + return null; + } + metadata = groupingShelfControllerCommon.metadataForTag(objectGraph, itemData, shelfToken, metricsOptions); + } + else { + metadata = groupingShelfControllerCommon.metadataForFCData(objectGraph, itemData, shelfToken, false, null, metricsOptions, groupingParseContext); + } + const metricsClickOptions = metricsHelpersClicks.clickOptionsForLockup(objectGraph, itemData, metricsOptions); + metricsClickOptions.targetType = metricsOptions.targetType; + const actionFromData = lockups.actionFromData(objectGraph, itemData, metricsClickOptions, shelfToken === null || shelfToken === void 0 ? void 0 : shelfToken.clientIdentifierOverride); + const action = (_a = metadata === null || metadata === void 0 ? void 0 : metadata.action) !== null && _a !== void 0 ? _a : actionFromData; + const editorialNotesName = (_b = content.editorialNotesFromData(objectGraph, itemData, "name")) !== null && _b !== void 0 ? _b : attributeAsString(itemData, "name"); + const title = (_c = metadata === null || metadata === void 0 ? void 0 : metadata.title) !== null && _c !== void 0 ? _c : editorialNotesName; + const ribbonModel = new models.RibbonBarItem(title, action); + // Setup Artwork + const artworkDict = categoryArtworkData(objectGraph, itemData); + let artwork; + if (isTextOnly) { + artwork = null; + } + else { + if (isSome(artworkDict)) { + artwork = content.artworkFromApiArtwork(objectGraph, artworkDict, { + useCase: 29 /* content.ArtworkUseCase.RibbonBarFacet */, + }); + } + else { + let resource; + if (shelfToken.isArcadePage) { + resource = "resource://arcade-ribbon-bar-fallback-icon"; + } + else { + resource = "resource://appstore-ribbon-bar-fallback-icon"; + } + artwork = artworkBuilder.createArtworkForResource(objectGraph, resource, 36, 36); + } + } + ribbonModel.artwork = artwork; + ribbonModel.accessibilityLabel = title; + // Configure impressions + const impressionOptions = metricsHelpersImpressions.impressionOptions(objectGraph, itemData, title, metricsOptions); + metricsHelpersImpressions.addImpressionFields(objectGraph, ribbonModel, impressionOptions); + if (!ribbonModel.isValid()) { + return null; + } + return ribbonModel; + } + /// Creates and returns Arcade See All Games ribbon item. + static createSeeAllGamesRibbonItem(objectGraph, metricsPageInformation, metricsLocationTracker) { + const title = objectGraph.loc.string("Arcade.CategoryBar.AllGames.Title"); + const action = seeAllArcadeGamesPageFlowAction(objectGraph, "releaseDate", metricsPageInformation, metricsLocationTracker, title, "AllGames", "none", "facet"); + const ribbonModel = new models.RibbonBarItem(title, action); + // Setup Artwork + // To update this artwork, run `cat artwork.png | base64` and prefix with `data:image/png;base64,`. + const resource = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIIAAACACAYAAADHy7H2AAABdWlDQ1BrQ0dDb2xvclNwYWNlRGlzcGxheVAzAAAokXWQvUvDUBTFT6tS0DqIDh0cMolD1NIKdnFoKxRFMFQFq1OafgltfCQpUnETVyn4H1jBWXCwiFRwcXAQRAcR3Zw6KbhoeN6XVNoi3sfl/Ticc7lcwBtQGSv2AijplpFMxKS11Lrke4OHnlOqZrKooiwK/v276/PR9d5PiFlNu3YQ2U9cl84ul3aeAlN//V3Vn8maGv3f1EGNGRbgkYmVbYsJ3iUeMWgp4qrgvMvHgtMunzuelWSc+JZY0gpqhrhJLKc79HwHl4plrbWD2N6f1VeXxRzqUcxhEyYYilBRgQQF4X/8044/ji1yV2BQLo8CLMpESRETssTz0KFhEjJxCEHqkLhz634PrfvJbW3vFZhtcM4v2tpCAzidoZPV29p4BBgaAG7qTDVUR+qh9uZywPsJMJgChu8os2HmwiF3e38M6Hvh/GMM8B0CdpXzryPO7RqFn4Er/QcXKWq8UwZBywAAAARjSUNQDA0AAW4D4+8AAAB4ZVhJZk1NACoAAAAIAAUBEgADAAAAAQABAAABGgAFAAAAAQAAAEoBGwAFAAAAAQAAAFIBKAADAAAAAQACAACHaQAEAAAAAQAAAFoAAAAAAAAASAAAAAEAAABIAAAAAQACoAIABAAAAAEAAACCoAMABAAAAAEAAACAAAAAACBAcQEAAAAJcEhZcwAACxMAAAsTAQCanBgAAAIGaVRYdFhNTDpjb20uYWRvYmUueG1wAAAAAAA8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJYTVAgQ29yZSA2LjAuMCI+CiAgIDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+CiAgICAgIDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiCiAgICAgICAgICAgIHhtbG5zOnRpZmY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vdGlmZi8xLjAvIgogICAgICAgICAgICB4bWxuczpleGlmPSJodHRwOi8vbnMuYWRvYmUuY29tL2V4aWYvMS4wLyI+CiAgICAgICAgIDx0aWZmOk9yaWVudGF0aW9uPjE8L3RpZmY6T3JpZW50YXRpb24+CiAgICAgICAgIDxleGlmOlBpeGVsWERpbWVuc2lvbj4yMTk2PC9leGlmOlBpeGVsWERpbWVuc2lvbj4KICAgICAgICAgPGV4aWY6UGl4ZWxZRGltZW5zaW9uPjIxNjA8L2V4aWY6UGl4ZWxZRGltZW5zaW9uPgogICAgICA8L3JkZjpEZXNjcmlwdGlvbj4KICAgPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4KIUPY5gAAGmtJREFUeAHtXXuMJMV5r+qZ2Qe7h299bA7O3B0GGwOXBwEsbMXCC1H+QJjHgpbHEcyBEiLl4cgJxLKElIkUS4DtQCCSpZOSEAx3hjW+YwmclAfa4MQScfBL2Tg5ED5uyfFYw0Lu9nZvHl35/b7qmu3Z65numZ3dndnpupvt7np+31e/+qq6uuorpVKXSiCVQCqBVAKpBFIJpBJIJZBKIJVAKoF6EtD1ApcbZpRa0fxJHwpAMe3hVprfleS1ZRVlRkayqr8/ozbPa7Wh36gTW3118KBRw8NG7djR2sqamrJ0D73mqdmztRqY8QQKc8M+yirpfN5fSWgYA/zdOJarKnfoNQNabLkrwe/MjFbnnqvV9LRXkfGm+bLOT5ZaweuygWCuvLKXhOgDB060gqDl5mHyylNTY6SpoMfHy8vNL5ze5PPIe2pF8g6X08i9GRvrUacf0/qR5cm/aSCIUA5N9uhHJxcc4ebma7aoTHabMmYLNPZpaDcbAJE+hGfg13RZLn939Y0xnucVlPGPI/9ZlPUO7t9Qpb7XUfnHXDyza6QvTJ/zb+YqgD9woAAmRLuhAvpVdmG70t6ZyujTkecQaBnwld/jad0yXgNayyjjBCB+FGX8HH5vqkJxWo8/e9jxIoCYmfH1ZHMaoimCTR7dgJr0dV755o6xYVUo3YiKvgJEfgJyolAAANWjPKuxhdimSnJsLrm6jgY6GuVSHc/jbhbiP4xq+gGENqH37vtHpjK7dgEMj1bAuiSnRI8EgdN45pbRzyDX6wDyS5D4LFw/jDIHlNYefja/leCVOVt+i7gj2N/B7xXw+qLKZZ/Uj40LKAgINIYCwhpyDZNs7rorp3fvJjHK3Dx6p9LmbuXp86XSSajvs5oYTrXMSqIvZcRLSxwydCIH0lABymThw58UpMrl4xDQ8ypT/rJ+fOLV5WgGJ1gzduWwyvU+AAauA68bAQBwB5aMKYOeEoq2/LaQz4qwOCYBZ/ixZWVQak5D7Qjrvoj4Z/B/WD+x/yFcVRi4fE7imHlixwGhUz1m57V/CUK+IML3/XmQeYI1LjVkQDDIRNVL/ga1whsE0xvRws8n+yMWo8KFuxP6uWcXbiPhr0iDCRApi7JOUdlMRpXK00iyC9rhhWbA4Pg1t157AYh/Gt3eeQAZKxxdklQ8LsIjKygkS0un47eaboYt5WWRLysbKyOXOa+ShhLlP2SIH3lmmTkIfQCNERSZx1Qx+9vUCA7ACE/kyEAiZzgI4xsAnNl53UMqm/0CxF6EBkAfrUogrQe13YP7HpCHrgPjAotgkEgcoOO0fad9to8MQqDghF2rPCM9gYSfe+YVz4xHvyDfIF4GVwhDyu1FIHk6BhB8gJa7FTL/trn1+k9xrMBuAmGJHMdABL0Zu3obyj2gvAxB8AESUy2TPvLJH8smDSw3+JFGchPQKtfwPdPjORwuz6xOGw+PEkF4DvyQP8uB5pMye3GlvAENM6vK/hy6iM/72eJfw08JGKQL51O8o1ATOSCsH5nPm1tHb0eCR1E4+6HjATEWo4lyWoVI5Mq2nKKocd9Mqb7yp/XfTBwNd21JKDE7R19QWe9yAOs9xOcbAytbGkSS9Csex/JKFcOBbAYNdFCVS3ejm/g6/LTK53WS12kyFesCNTNvdl6FkbH5kiTQAQgokvYRi5DG/gEioIiyxi/PoZ3tUAven0hg4SA1VV1n7ro4xwgCeguC9/FIbWLFzsB2cVb27FuoHU5Aa6HT0F80O2/4OPyMOnIkll+ykggIiGcz0z03Ievz8eMoPFEBLGS1HWsrACdVbRndFwcPNxLI0kXw3bue2/2ynaQx5nehCRiTKjnIsl7CNQ0jJPqkm8hlPoLX6RtIDQf28qofQ1osEKANMuwSmA9e1H5d3g6UKUCwmBuIyX2tgwUR0oqL6HC3o9FcJiQtLHAME+lkgAhWzW2jv4gIv4IfB4bCq80uMlm7eLI+MYhExWhzBTW5JWwytp5jIyAjafnm86ObIMxz2bTg7LDfltK+fy1Q2UkUMd5C/24uFmLPss08kvDhYSuTkvokQM8xgaiEyLjt6ekFWuw8lfPPFBKPHI3FcDwQBgZsnEIZo2ezOQBCe4ogmiq+dPky8DbqYxJl06Xx6tIzO6Kza2NfO4eBhmvYdaOu/K1CrfsGUof0eCDMzVk0efoM5DOIH/vPWITVKXO1g0gr1CWclqlgJaPoyRrqkh+P6Hy9XUDfSZxaylGnmm9LPZiXxlQ/3FPjnPqqy0k8EAYGbPZKnYYsOXKmqqybKcLbz0mXZjaaO6/h9LfCnEg071uutl2BNsMSL5gUk/vO+MO6KXNGIqPVJpKMLt2+TNehP1oY4QRvv22BoD0KkGqns0RTBVnTr+Yx60h37FhVCL3YakLv3IP4tkXvTnPkCxqQF3Nqhfg/y5/EbyUMN/FAGBwMpGH6pJ/l9GhnQYH8BjzoHIa5diRtRwthWSiVt5qOb0pIgVlKbbuU6ljt/WSrO6gh+fKbiN54ICxmY+cN7IBk0bez7sCvtnxE0p23vjuwCESmjIl59x0gMkGbe9bjtZr0RoBQnbJLnoCIjuwfpHoaWAOSAiEW0J2sEWKZq0RIgVARRfRN56qDaH5q+aZAqCEZ+25Ud6BdI2VneqdAqFtv3aIPkrw+1hVUGrheJJBqhPVSk8vkIwXCMgW4XpKnQFgvNblMPlIgLFOA6yV5CoT1UpPL5CMFwjIFuF6Sp0BYLzW5TD5SICxTgOsleQqE9VKTy+Sju4DQPZ8OGoZFdwGhgU8Hss4P4uwW7HQXEBpuJ0zQwQtTGuA3BUIDwlrPUZMDwV8XLQOWG7z4DmKSVe747eAVSl7yxbfxQKgs+4YtBDqsjxVJxotTorfJH9fVl1WxYDe4vhpBWT5vuZqcxN4QE2zkcYCIiN++XpZf2q9I6OKB4JZ9e3oOwuFSf2ydIBwSlrDW0cKAhdE/1Z+z9pQqy/SrCUR0jBMF7tz42ylcLjJh+Q3Wmpu5xYD6d/FAOLrZCsMo2ggoQEpMExZv/RLaKVTDKtnMAi2ewAba0Ek8CADGxgKZwFobIdF5jnx5sl1PLM5JZdFgxkn8hlmLB4Lb+6j020jIVsJ9AXUzDRfQJvdYlS6V+q6zjqZmZ2ttXrH7HgxM2DFNJ+3jIIta9nlmxWCGljrDxp08OalbZ/FAGJgJMtBv4IatJD5Nm9R+QAbpB8286NfpJxZRxsejgRCMiXxP2VFEwD3TdYTjzm+Y+AHZ7wMM/ys019rwG2IovlJ7zrWbQgc3TQNVh4OWFYOvUAlrfSstBDt+YKUT28T/S8iZPZuW16KrOBg7QDA/oqUVOETtEGdHbtgA62UxxH0dw/ppofzcVthH2L27tGiASv8gkF8WBUS3qDaTmd3Harjf8V2A+HtC3uIO75OphS1n8SzkXkI/+xbQwrTRoDk59Zr5hAiELRvAWOvv673P0kor9HhgI7oOdbEaQVrO7CwrHq68Hy3r/3DD7fEwPkG/tnacBOD4gHz+u96z/0cQmFZnnVXTMil3Q7PrgLmg95DuOZ3xepCmRF5Dwm47pjkIgCOd/apYQgdRfpYeYgoogU3qWCAwM1g8PyF2B/dMvIiCngPiOKBKsOteUq/lH2qtHrRsvE+bB4WQsbG+0Nb3aNoqLShzP/rZBcgYecB0RnTs9vC13cIJmNfjtv/n1HkX/b0QNpKMvERAEMG99JKYnINhx7vxO4TZBNpLoBptx4ZCmggCDxZYqdof0Xue+SdnJhDPdR20QVmMiO15+hXg/R4YsuxHAlgdsR1N3cSrHCjCtwiF9Vt9GrTBDOrmXtFstMuc0Ix/IiCQN752iRnbb00cwSBqDGJBgd6HQIgTOgXPe6GNaVbRuXJJQ0AHbDNnswOqWN6LLuGPA1rswDcBYQQDoyHtX6lS8avI61T0MZQX8hdArDG/jgZcjRgLg3FwQwOot+vH9/04eDNKPLPYsLZzBp/NTaPnm4x5XGcyF0F9oiJgRl7DAB/JWn0wOD5YUbCJCGMYtNquzdf1E8+IoU1p4Qn6SqSvODKFjMmPglX2P4KQv4IfbRlyjEGgIIwVsurT0MAk/uG0AvQIOdRBBlr6FYwM7szt3fevYj0/P1l2tJP+OOcEGBevKjxQsbS1qNXO0buN9u+EMM6B9WDbfTC2AJaCopDwv6WOZBtmbMdIwUjJ1gnen43C24F/v+aYhjGtrcjE2iBMKsvgM4UK49y/hLt7UfTleMR5FEHBlldGY3T88L+lzvKLLDnwreSM+X5W9mF471XHyvfpCZgYXi0z/Y6KxVdKsE3DjtnSZyGDSxF+Pn4fAXE018uBC8HBltpqh/GJwakx+igynsH1Z3j+iSrr7+on9/2UhQld4+NFCGtZNYPEWo2N8U1C3jbMbVd/VJUzn0URF+J3NkJ/AddTUQrMC0Ej2YEbvFrmSD/VPFU/p/rfRBn/A2h+X/WbSdqYZknNgoBpF+HFpwadPcXlEE5xOflgDDnxZMg7RRVP6VWZEx5U17LKqiItA42Yy5RUrjyvtl00t/QtgK9MPF+qMp1clbj5BwEWvlG48yrCOSGMpgf7wWuupbyykIH+sppTBTUwcDxS1jipRs0NF924JkxX0vuWVA6EkAGROcXvEkNDpShBJSWomXgCyHfxVvPWIDVPWbVAC9SjQwDBsQin35dZAfXKqRUmA0HMjkr4zEzRnaFRK34S/5YAYWlBUjGc3+apZLOzK1KGfD3cglPkpnCGBL4boJBlqf+lPCR9DroNT9EA15EV5pen5o2M+PySuFb8JpVLGi+VQCqBVAKpBFIJpBJYHxJYkYFcOlhcAXBwaV0wWFz6utyK0loChPT1sbVHD8dVbNu9Pq7phFJ/L05lObGgtk1gQkk+NFXkl04oVUSR+KZpjVA1xcyDwjf2XYYp5k+h5PPwSzLFzLL57i9Xvo/jxj3DuzIv4OLRT1wQKXKKuYAp5t6VnmLeecPZmLe6DMQ0MsXs+JAvVHwIXBXPITm4cF4Zp/2mmDmzxnl30QgHf3gPiNyFejwHVdnAR6eKXAI+cRHn/Gs9O3/5umM/wbgPMfbjz/uQ2ve0b+7T33rmu4zNrqvZ6VdWDPPAH2NuueaX8SX6Xjxejt+m6I9OFQAzWcg5vsJXBrOOnV8oetWthPNP1UcnUMT1INOQxB5V6LkPPB5zdVOVPMEDM2/IVT5D3zZ6Po6gfVy182doo76m9+7/EhlsBgwEAQTEmsJn6Ou4puErWPTRiwoIfYaWcInDeKvkKp+hQWIOdYDP0OVXcLzEHfqJiX9b8c/Q7nxlc/M1l+CI3OchlGHj++GjUByw3HWV5FJphaEK4cKUTB9W7OwFGHaSkGbAIOluvfYBlcndo0ol7uvA52xZCsmyHJ/uyuir4Ryf0IE8cE9jraL+EACKownNGFZjPS8DSpxfCcJc3Lp0Jf48LJqA5yv/5vVnYGXSU/iwPIyVSh+gIAqB+fDHe/5W27lyQ3RAbZZKOC85cwuO9f2LgCCutUzkCBpGxGmwv6eyAgIu2uWaBpQhfVGb8Esc4FO/MVxsewq+gP8dzu6+UO9+uchP5+QhiSMzsU7GAoOD7I+w3sP/GhavfhRL1fgNnF/A1qLiSUk9R5rIm48zEKnGfx8rjH5DxjU447peQoY5zWFuuu4cCPirqljkfknml0hezGO1nAjftvl+0PpznctywcyfBzxgHIdP8glcMsampno52IIwP4M8P4fv7XaZFrHY3o78FaTFaPNFIXV8fEGAXY/uodesXDycg53JADhchqcS7Kevl+kKh9mteb3Qgsexcukq1Vu+SkqcTFZuLBAANlljINlpM4pxAU8Osy0kUe+TjJAVikWgYo8T1i8q9UmMbX4VHkYdOsSVzZGOIKFalQPRtQboRaNkk/W0kVmuimewWi6L67zOZqG4/WtYMNcquG6uHiGxQFB3XZyVg6a56kepi4OegN1EfNp6Ja9WGLUWV/na171PS7GVjb0RRLjT1U0Pl93x0FN2LaL52ln9OdpwxSY/n+rrEnPL1TyrEzu/Aw0nD9F/4iuzsMEOsLZs2oYstqIfsjm5kqPzbR9f2nPQ2H/Bv0rbY36HXsOCX1u5JxE6Pe1kciEGxQxuf73nmLDdA/Z5+iW812zHC8VWCTq4IZYHx7TL6uTr3HBQ5cUzEci181SzneRIP2gmDhTBrIIRdTTvFQMa5uOdxGSFVmu/4gS43YjxDWZ44bCqqSbwg4TRwggC5VLZMJrZjMy4KpkDxQAc4YhtfY/XbTYKc5osqiWpQ0O1eCd/5PCMQPt1Dq/ConTZJYAA7OJQd7oYIxmMUksYDLNugzsSWG2ERPBeqjsRCJYXowfVcB9XG2OH8MlrKaXVVOwmmKEACDZt5/wlcLG5hxfwAIc7Q2MZvK/l4oHwapDUNwPInHmybdneqFau7eQvUggI0qpXzRe5kzvybGh6C4PCn+Z8A9tYZzlb3cHOCj2QlPh4IFT6zOCDEiAgZdkCk5azpvFYuQEBGZXrsRMsH4sgybWakRFum2M8gN6yGxG7nb0sv+GPgDHUxgPBZeB1pEAc9e7KV6t4CI8wusO7fDV26Tvr6pvE9Zs4YmdJIKW2UQmkQIiV2LrQhLFcdhcQ4juFisAwOJDYbnBRCVinN90FhG6p1SbA2l1AaEJA3ZIkBUK31HQMnykQYgTULcEpELqlpmP4TIEQI6BuCU6B0C01HcNnCoQYAXVLcAqEbqnpGD5TIMQIqFuCUyB0S03H8JkCIUZA3RKcAqFGTWMtVld9mUiBUAMIzruBD5YuSUdeUyDEVlsHr1CK5W0xQgqERVlE3nXomkXLi04O4uRA8GU/A5eBdrK2xEYXY/ctRFZ73vrSrK8cm8f9csmFGZnlmnrW47WasHgguLOhPb1g1/lj6VbnDaQC8OJsJy3WTlRwqmO1NPJ2gChmdjSOD1bJF39WZ7SGT8H6ZcuJsccfJyAnHgib3ZHAPu0hYPNrh42nq8b+eh6G9GFVBK6yTF+e5A/QYipb5nl8sGwSWQzvkDtyjHrlRdO4h3V/mq+ShPN213ggLO4c5hmC3BnMTbF1M3WZt9VVKlW/7w65UDMz0Xs4jzxrN/362p6Z2Hnaj3XDA09xsTzI+suYDj0eCG7vo2/eRM7OSkonAYG0Cp8wlPAW7pW0epq7j3KzZ1uRaWNPve0kTi3l4AubkXy/gObKOlPqxjGP2i6KXednd/24p6jr3JwVWE/msCqZd6AuN9uxQlTktvTj8Naenq6DkQEO+dCPHMAYoI7TeqpOaHsG2YE8BsOG2/pgds+bFkKtfYQ6g+Qkm2Dt7melH9v3LiB1MOg3NTdAtr2zLYRvADQ2hfOE9MtC86GsVf9RDLguwy//ByzGua4wKmZb+qFafFiTI20/VUXvDSFySwvsI4jtpMAAFeT6z7TGAcT14L7c9i+SFqz8S+tih1SP+RcRTF8fLb5EOncsjt478RPw+GNEsqYA8NABjvv50A2SWPMCjYdZmmt0gyGG4scINrJVK8Xckyjkv6FyqHrqqppQGat+K/VvK46nt2YCyydP6Uf3vy+2IisCiiaN5oUlxJhv4OBP3pbr97DR+ayyLzlegMYegkGtN2A752mWL6aScd51HC2JgEBkmV27+nB9Txn/gSDTUyAcWiyzAIwraRXD5QWBHQJed7WXGQDN/6mKOUt3z9FYALvDyfTeZ/4WBjsnoWo3gkm+k7NHtBBbRX7qFmWp4V+2fpyoh27B6Af14xOvCq1btsTyy/wTAYERFU5YF9t9FI5RD0M4tEwGIWPixQTawYHCXSVh6I8l2nq4OLX8wuHuninj4kMM+EfjWbCb5G1EPz+L1vFbAPEx0QY0RJnAVeYTcrnbYatxGjaYPsw8UTwFbodlji53XZqvo9VdGe7uw2ncvQtbGs/lG4onlcxnmQEVK3dZlcsNQht8U+/dZw2M5kcySc92CBftiqt5lSPoYa6NEWDd8yGooT+UwaPv44BqAQRbDF9VZG42EBdjyzytna519+TBysVeOf6kOVk6N60b3oDqwl2YROQfJpeBS3DPkdIAgJqRCjR6FwTzgjMfjLDEzvGLE2AvAE3fhtlh2J8uc2x0HKW6lkaS2aBCsnQ8ktZqHuxz2H/pPcljGscvnwOeOafBf4s8s8wc6mBApF72vwnNR9Cz0YrhdKZO4kLEJ4kOCtB/OtVpbrn2DhBxN34XSD9MGPgyk8FWR0HZCmrxpAyKCQAjAyNUgsnChz/IDL9ymbOHBzCv8mWNE9/NH1zZG/u6WIN9J1AxVedl7kc1wNakNyTVTlZxLC/oKaFYxy8hwcpqnVvUPwRcBuXhPGiUyDPLOXg35hAKfVjv2fcgC3WG03mf1DUMBGZszbqO+FQ7ENSw6indCGKuADGfQCgNONEoZ08wSGMSCqd1zomZwBPVaOaROWwR493ZmB8CDBP6if3/wAJlbBNxUm0jxISBZG69/tcg/FGUdQnyOAtXWJqDBtKoFYKQbiV4Zb7CL76XwPAPfu/g9wqUx4so9ym95zuv45nmgxvSBExD1zTJ0ocemsRxwJOVDxuwbLpFZbLbQPAWkASbwGoDiuAbBs5LDqtIKbv5P2LGycNA1UfL17Mo6x0MCN9Qpb7XORZwGTfTHbi0S69ije3SA0WdFy1Hgfer7MJ2tMozURmnIz4MV0FFa8M3DrbcVjq8tcAMsIeZXaPeRcUfUYXitB5/9rArhADgtLl7/XX+Sa9NA8EV4MzVtfocZpd/o1c0Go0pVYKvAFC4frzRbCLjC/hhl3ol8o4sMIGnAOD0Y7rZrs8VsWwguIzcOUpq8zz0QD/eJbb6cirZML7t0+3Y4RS6S9L8dWrK0s2pU34bGJixLXBu2Ec5paQj5WYJELD9zl1ZVThoZyhZ7tBrBrTY9/VW8koiye8MjhzmEcu0DOtkvGm+rPN28N4sLy5dy4DgMgxfUfMrmj/LQgGtA1iY+CbuV5rfduK1CfGkSVIJpBJIJZBKIJVAKoFUAqkEUgmkEuhoCfw/vJfHMO4YqncAAAAASUVORK5CYII="; + const artwork = artworkBuilder.createArtworkForResource(objectGraph, resource, 36, 36); + ribbonModel.artwork = artwork; + ribbonModel.accessibilityLabel = title; + // Configure impressions + const metricsOptions = { + targetType: "facet", + pageInformation: metricsPageInformation, + locationTracker: metricsLocationTracker, + recoMetricsData: null, + }; + const impressionOptions = metricsHelpersImpressions.impressionOptionsForArcadeSeeAllGamesRibbonItem(metricsOptions); + metricsHelpersImpressions.addImpressionFields(objectGraph, ribbonModel, impressionOptions); + return ribbonModel; + } + static createTagsRibbonShelf(objectGraph, data) { + const tagsData = mediaRelationship.relationshipViewsCollection(data, "categorizations"); + const ribbonBarShelf = new models.Shelf("ribbonFlow"); + const ribbonItems = []; + for (const tagData of tagsData) { + const name = attributeAsString(tagData, "name"); + let pageUrl = null; + switch (tagData.type) { + case "genres": + pageUrl = attributeAsString(tagData, "url"); + break; + case "tags": + const href = serverData.asString(tagData, "href"); + pageUrl = hrefToRoutableUrl(objectGraph, href); + break; + default: + break; + } + if (isNothing(pageUrl)) { + continue; + } + const flowPage = objectGraph.required(pageRouter).fetchFlowPage(pageUrl); + const flowAction = new models.FlowAction(flowPage); + flowAction.pageUrl = pageUrl; + if (isSome(name)) { + const ribbonItem = new models.RibbonBarItem(name, flowAction); + ribbonItems.push(ribbonItem); + } + } + ribbonBarShelf.items = ribbonItems; + ribbonBarShelf.isHorizontal = true; + return ribbonBarShelf; + } + /** + * Determines if an item should be given priority and moved to the front of the list. + * @param objectGraph Current object graph + * @param data The data for the item + * @returns True if the item should be moved to the front of the list. + */ + static shouldPriorizeItemWithData(objectGraph, data) { + var _a, _b; + const displayDeviceDrivenContent = (_b = (_a = objectGraph.userDefaults) === null || _a === void 0 ? void 0 : _a.bool("displayDeviceDrivenContent")) !== null && _b !== void 0 ? _b : false; + return (displayDeviceDrivenContent && + objectGraph.bag.ribbonBarVisionEditorialItemIds.includes(data.id) && + objectGraph.bag.enableDeviceDrivenDiscoveryContent); + } +} +//# sourceMappingURL=grouping-ribbon-bar-shelf-controller.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/grouping/shelf-controllers/grouping-shelf-controller-common.js b/node_modules/@jet-app/app-store/tmp/src/common/grouping/shelf-controllers/grouping-shelf-controller-common.js new file mode 100644 index 0000000..a1b05cd --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/grouping/shelf-controllers/grouping-shelf-controller-common.js @@ -0,0 +1,992 @@ +import { isNothing, isSome } from "@jet/environment"; +import * as models from "../../../api/models"; +import * as serverData from "../../../foundation/json-parsing/server-data"; +import * as mediaAttributes from "../../../foundation/media/attributes"; +import * as mediaDataFetching from "../../../foundation/media/data-fetching"; +import * as mediaDataStructure from "../../../foundation/media/data-structure"; +import * as mediaRelationship from "../../../foundation/media/relationships"; +import { Parameters, Path, Protocol } from "../../../foundation/network/url-constants"; +import * as urls from "../../../foundation/network/urls"; +import * as appEvents from "../../app-promotions/app-event"; +import * as appPromotionsCommon from "../../app-promotions/app-promotions-common"; +import { pageRouter } from "../../builders/routing"; +import * as legacyArtwork from "../../content/artwork/legacy-artwork"; +import * as contentAttributes from "../../content/attributes"; +import * as content from "../../content/content"; +import * as externalDeepLink from "../../linking/external-deep-link"; +import * as lockups from "../../lockups/lockups"; +import * as metricsHelpersClicks from "../../metrics/helpers/clicks"; +import * as metricsHelpersLocation from "../../metrics/helpers/location"; +import * as onDevicePersonalization from "../../personalization/on-device-personalization"; +import * as productPageVariants from "../../product-page/product-page-variants"; +import * as refresh from "../../refresh/page-refresh-controller"; +import * as mediaRequestUtils from "../../builders/url-mapping-utils"; +import { defaultTodayCardConfiguration, lockupsForRelatedContent } from "../../today/today-card-util"; +import { isLockupShelf, } from "../grouping-types"; +import { categoryArtworkData } from "../../categories"; +import { hrefToRoutableUrl } from "../../builders/url-mapping"; +import { clientIdentifierForEditorialContextInData } from "../../lockups/editorial-context"; +import { areAppTagsEnabled } from "../../util/app-tags-util"; +import { AppEventsAttributes } from "../../../gameservicesui/src/foundation/media-api/requests/recommendation-request-types"; +/** + * Create the base 2 requirements to start parsing a grouping page shelf. These include the base shelf token and the + * base metrics options + * + * @param objectGraph The App Store dependency graph for making native calls and viewing properties + * @param mediaApiData The media api data for this specific shelf + * @param groupingParseContext The grouping page parsing context. + */ +export function createBaseShelfRequirements(objectGraph, mediaApiData, groupingParseContext) { + const featuredContentId = mediaAttributes.attributeAsNumber(mediaApiData, "editorialElementKind"); + // Populate the gamesFilter, if it is provided. + let gamesFilter = null; + const rawGamesFilter = mediaAttributes.attributeAsString(mediaApiData, "gamesFilter"); + switch (rawGamesFilter) { + case "arcade": + case "nonArcade": + case "all": + gamesFilter = rawGamesFilter; + break; + default: + if (featuredContentId === 495 /* FeaturedContentID.AppStore_PopularWithYourFriendsMarker */ || + featuredContentId === 500 /* FeaturedContentID.AppStore_ContinuePlayingMarker */) { + gamesFilter = "arcade"; // Defaulting to Arcade because this param won't be passed until 20I. + } + break; + } + // Build Shelf eyebrow, title, and subtitle + let eyebrow = null; + let title = mediaAttributes.attributeAsString(mediaApiData, shelfTitleAttributePathForFeaturedContentId(objectGraph, featuredContentId)); + let titleArtwork = null; + let badges = null; + let subtitle = mediaAttributes.attributeAsString(mediaApiData, "tagline"); + const shelfHeaderConfiguration = {}; + // 'Similar To' Personalised Shelf + // Check if personalised shelf has a badge-content, if it's got valid content, and the clients support eyebrows and title artwork: + // - Reco shelf title becomes the eyebrow + // - Featured App in badge-content becomes the shelf title, and we add the icon as title artwork + // - No subtitle should be shown + // - Flag to use the eyebrown name for metrics + let wantsEyebrowNameForMetrics = false; + const badgeContent = mediaRelationship.relationshipCollection(mediaApiData, "badge-content")[0]; + if (featuredContentId === 476 /* FeaturedContentID.AppStore_PersonalizedShelfMarker */ && + serverData.isDefinedNonNullNonEmpty(badgeContent)) { + eyebrow = objectGraph.loc.uppercased(mediaAttributes.attributeAsString(mediaApiData, shelfTitleAttributePathForFeaturedContentId(objectGraph, featuredContentId))); + subtitle = null; + const dataType = badgeContent.type; + if (dataType === "collections") { + title = content.notesFromData(objectGraph, badgeContent, "name"); + const artworkData = categoryArtworkData(objectGraph, badgeContent, false); + // Reconfigure eyebrow text color only if there is eyebrow artwork. + if (isSome(artworkData)) { + titleArtwork = content.artworkFromApiArtwork(objectGraph, artworkData, { + useCase: 1 /* content.ArtworkUseCase.LockupIconSmall */, + style: "unadorned", + }); + const eyebrowColor = { type: "named", name: "secondaryText" }; + shelfHeaderConfiguration.eyebrowColor = eyebrowColor; + } + badges = { forYou: true }; + } + else { + wantsEyebrowNameForMetrics = true; + title = mediaAttributes.attributeAsString(badgeContent, shelfTitleAttributePathForFeaturedContentId(objectGraph, featuredContentId)); + titleArtwork = content.iconFromData(objectGraph, badgeContent, { + useCase: 1 /* content.ArtworkUseCase.LockupIconSmall */, + }); + } + } + const shelfToken = { + featuredContentId, + id: serverData.asString(mediaApiData, "id"), + presentationHints: {}, + metricsPageInformation: groupingParseContext.metricsPageInformation, + metricsLocationTracker: groupingParseContext.metricsLocationTracker, + pageGenreId: groupingParseContext.pageGenreId, + featuredContentData: mediaApiData, + title: title, + subtitle: subtitle, + eyebrow: eyebrow, + titleArtwork: titleArtwork, + shelfHeaderConfiguration: shelfHeaderConfiguration, + shouldFilter: false, + gamesFilter: gamesFilter, + remainingItems: [], + isFirstRender: true, + isDeferring: false, + showOrdinals: false, + hasExistingContent: false, + showingPlaceholders: false, + ordinalIndex: 1, + isSearchLandingPage: groupingParseContext.isSearchLandingPage, + isArcadePage: groupingParseContext === null || groupingParseContext === void 0 ? void 0 : groupingParseContext.isArcadePage, + }; + const shelfMetricsOptions = { + id: shelfToken.id, + kind: null, + softwareType: serverData.asBooleanOrFalse(groupingParseContext === null || groupingParseContext === void 0 ? void 0 : groupingParseContext.isArcadePage) ? "Arcade" : null, + targetType: "swoosh", + title: wantsEyebrowNameForMetrics ? shelfToken.eyebrow : shelfToken.title, + badges: badges, + pageInformation: groupingParseContext.metricsPageInformation, + locationTracker: groupingParseContext.metricsLocationTracker, + idType: "its_contentId", + fcKind: featuredContentId, + recoMetricsData: recoMetricsDataForFCData(objectGraph, mediaApiData), + }; + return { + shelfToken: shelfToken, + metricsOptions: shelfMetricsOptions, + }; +} +// endregion +// region Shelf Tokens +/** + * Whether we should defer building the rest of a shelf given a shelf token + * @param token + */ +export function shouldDefer(token) { + return token && token.isDeferring && token.isFirstRender; +} +/** + * Due to upstream oddities, we get the shelf title in the form of `title` for these markers. These are + * unique markers in that they are not filled by reco. + * + * For personalization markers, they are programmed in DJ with the name field hidden, and it is filled in by + * Reco. Without wanting to expose loc strings to that name field, we unfortunately have to use this other + * attribute key. + * + * This will all go away once these markers are replaced with proper Reco equivalents (i.e. when popular- + * with-your-friends and suggested-friends move to Reco). + */ +export function shelfTitleAttributePathForFeaturedContentId(objectGraph, id) { + switch (id) { + case 548 /* FeaturedContentID.AppStore_GameCenterActivityFeedMarker */: + case 495 /* FeaturedContentID.AppStore_PopularWithYourFriendsMarker */: + case 496 /* FeaturedContentID.AppStore_SuggestedFriendsMarker */: + return "title"; + default: + return "name"; + } +} +/** + * Returns the URL schema for grouping shelves that may need to fetch additional content. + * Grouping-Related builders can extend on this scheme if needed, e.g. query param on Continue Playing shelves. + * @param token Token to encode in URL for subsequent fetch. + */ +export function groupingShelfUrl(token) { + let shelfUrl = new urls.URL() + .set("protocol", Protocol.internal) + .append("pathname", Path.grouping) + .append("pathname", Path.shelf) + .append("pathname", encodeURIComponent(JSON.stringify(token))) + .param(Parameters.groupingFeaturedContentId, `${token.featuredContentId}`); + if (isSome(token.nativeGroupingShelfId)) { + shelfUrl = shelfUrl.param(Parameters.nativeGroupingShelfId, `${token.nativeGroupingShelfId}`); + } + return shelfUrl.build(); +} +/** + * Configure `url` on a standard grouping shelf if it needs to fetch more content. + * @param shelf Shelf to add url to. + * @param token Token tode encode in URL for subsequent fetch. + */ +export function createShelfTokenUrlIfNecessaryForShelf(objectGraph, shelf, token) { + if (serverData.isNullOrEmpty(token)) { + return null; + } + // Web does not curently support pagination via "fetch more", so shelf token URLs are not needed. + // Note: removing these URLs for web reduces view model size by ~45%. + if (objectGraph.client.isWeb) { + return null; + } + // Ensure the token has a `shelfStyle` so we can construct an empty shelf in the case of a hydration error, + // or the shelf being empty upon hydration. + if (serverData.isNull(token.shelfStyle)) { + token.shelfStyle = shelf.contentType; + } + const hasNonPlaceholderItems = shelf.contentType !== "placeholder" && serverData.isDefinedNonNullNonEmpty(shelf.items); + token.hasExistingContent = token.hasExistingContent || (hasNonPlaceholderItems && token.isFirstRender); + const shouldAddFirstRenderUrl = token.remainingItems.length || token.recommendationsHref || token.onDeviceRecommendationsUseCase; + if (shouldAddFirstRenderUrl && token.isFirstRender) { + return groupingShelfUrl(token); + } + else { + return null; + } +} +/** + * Updates a shelf URL based on the provided token. + * @param shelf Shelf to add url to. + * @param token Token to encode in the url. + */ +export function updateShelfUrlWithNewToken(objectGraph, shelf, token) { + const originalShelfUrl = urls.URL.from(shelf.url); + const updatedUrl = urls.URL.from(createShelfTokenUrlIfNecessaryForShelf(objectGraph, shelf, token)); + // Add missing query params to the updated URL from the original. + for (const key of Object.keys(originalShelfUrl.query)) { + if (serverData.isNull(updatedUrl.query[key])) { + updatedUrl.query[key] = originalShelfUrl.query[key]; + } + } + // Finally, update the shelf's URL. + shelf.url = updatedUrl.build(); +} +/** + * From a grouping shelf token determine the list of unhydrated MAPI data to fetch + * this will also handle the case were whe need to fetch additional relationship data + * + * This method will also hoist up ids of content needed to be fetched from an unhydrated relationship if needed. For <rdar://problem/42797176>. + * + * This is necessary to accommodate some MAPI objects which have a nested relationship that isn't hydrated and cannot + * be hydrated by re-fetching the content at parent ID. + * + * @param token The shelfToken for the shelf we're fetching data for + */ +export function unhydratedRemainingItemsFromShelfToken(objectGraph, token) { + var _a; + const shouldFetchRelationshipItems = ((_a = token.relationshipToFetch) === null || _a === void 0 ? void 0 : _a.length) > 0; + let remainingItems = token.remainingItems; + if (shouldFetchRelationshipItems) { + remainingItems = token.remainingItems.map((remainingItem) => { + return mediaRelationship.relationshipData(objectGraph, remainingItem, token.relationshipToFetch); + }); + } + return remainingItems; +} +/** + * Given a MAPI response for a list of unhydrated shelf items, and the original grouping shelf token determine + * the list of hydrated MAPI data. This also handles the case where we need to hoist up relationship data + * + * If ids to be fetched were hoisted from nested relationships, replace the original unhydrated nested + * relationship on parent with hydrated data. + * + * @param token + * @param mediaApiData + */ +export function hydratedRemainingItemsForShelfTokenFromMediaApiData(objectGraph, token, mediaApiData) { + var _a; + const didFetchRelationshipItems = ((_a = token.relationshipToFetch) === null || _a === void 0 ? void 0 : _a.length) > 0; + let hyrdatedItems = mediaDataStructure.dataCollectionFromDataContainer(mediaApiData); + if (didFetchRelationshipItems) { + const dataMapping = {}; + for (const dataItem of mediaApiData.data) { + dataMapping[dataItem.id] = dataItem; + } + hyrdatedItems = []; + for (const remainingItem of token.remainingItems) { + const unhydratedRelationshipData = mediaRelationship.relationshipData(objectGraph, remainingItem, token.relationshipToFetch); + if (serverData.isDefinedNonNullNonEmpty(unhydratedRelationshipData)) { + remainingItem.relationships[token.relationshipToFetch].data = [ + dataMapping[unhydratedRelationshipData.id], + ]; + } + hyrdatedItems.push(remainingItem); + } + } + return hyrdatedItems; +} +/** + * Deletes all remainingItems that have been requested for hydration from the shelfToken. + * + * @param shelfToken + * @param requestedItems + */ +export function flushRequestedItemsFromShelfToken(shelfToken, requestedItemIds) { + shelfToken.remainingItems = shelfToken.remainingItems.filter((remainingItem) => { + return !requestedItemIds.has(remainingItem.id); + }); +} +// endregion +// region Shelf Requests +/** + * Generates a media API request for fetching a shelf's data + * @param objectGraph + * @param {URL} url The URL of the page being requested + * @param {Parameters} parameters The parameters that were extracted from the URL + * @returns {Request} A media API request + */ +export function generateShelfRequest(objectGraph, token, parameters) { + var _a; + // Whether or not token is for fetching additional items that were unhydated. + // These tokens are used when a server returned explicit IDs for the contents of shelves, and some (or all) of those IDs were fully unhydrated. + const isFetchingAdditionalContent = serverData.isDefinedNonNullNonEmpty(token.remainingItems); + // Whether or not token is for fetching personalized recommendations. + // Recommendation shelves can sometimes return with no IDs provided within its shelf. We are expected to use the `recommendationsHref` to fetch in this case. + // Note that server can sometimes choose to prepopulate IDs for personalized shelves, even if `recommendationsHref` is present. + const isPersonalizedRefetch = !isFetchingAdditionalContent && ((_a = token.recommendationsHref) === null || _a === void 0 ? void 0 : _a.length) > 0; + if (isFetchingAdditionalContent) { + // Remaining items to fetch, since they were unhydrated in original page response. + const remainingItemsToFetch = unhydratedRemainingItemsFromShelfToken(objectGraph, token); + // MAPI Request + const mediaApiRequest = new mediaDataFetching.Request(objectGraph, remainingItemsToFetch, true); + productPageVariants.addVariantParametersToRequestForItems(objectGraph, mediaApiRequest, remainingItemsToFetch); + prepareGroupingShelfRequest(objectGraph, mediaApiRequest); + return mediaApiRequest; + } + else if (isPersonalizedRefetch) { + const mediaApiRequest = new mediaDataFetching.Request(objectGraph, token.recommendationsHref).includingAgeRestrictions(); + prepareGroupingShelfRequest(objectGraph, mediaApiRequest); + if (appPromotionsCommon.appEventsAreEnabled(objectGraph)) { + mediaApiRequest.enablingFeature("appEvents"); + mediaApiRequest.includingMetaKeys("editorial-elements:contents", ["personalizationData", "cppData"]); + mediaApiRequest.includingScopedAttributes("app-events", AppEventsAttributes); + mediaApiRequest.includingScopedRelationships("app-events", ["app"]); + } + if (appPromotionsCommon.appContingentItemsAreEnabled(objectGraph)) { + mediaApiRequest.enablingFeature("contingentItems"); + mediaRequestUtils.configureContingentItemsForGroupingRequest(mediaApiRequest); + } + if (appPromotionsCommon.appOfferItemsAreEnabled(objectGraph)) { + mediaApiRequest.enablingFeature("offerItems"); + mediaRequestUtils.configureOfferItemsForMediaRequest(mediaApiRequest); + } + if (areAppTagsEnabled(objectGraph, "grouping")) { + mediaRequestUtils.configureTagsForMediaRequest(mediaApiRequest); + } + return mediaApiRequest; + } + return null; +} +/** + * Modify request for a items fetched for grouping shelf items. (i.e. individual IDs). + * This should be ideally be in grouping builder's `prepareRequest`, but that would change how url-driven requests are configured. + * @param objectGraph + * @param request Request to modify. + */ +export function prepareGroupingShelfRequest(objectGraph, request) { + request + .includingAdditionalPlatforms(mediaDataFetching.defaultAdditionalPlatformsForClient(objectGraph)) + .includingRelationshipsForUpsell(true) + .includingMacOSCompatibleIOSAppsWhenSupported(true) + .usingCustomAttributes(productPageVariants.shouldFetchCustomAttributes(objectGraph)); + let attributes = ["editorialArtwork", "editorialVideo", "minimumOSVersion"]; + if (request.includesResourceType("app-events") && appPromotionsCommon.appEventsAreEnabled(objectGraph)) { + request.enablingFeature("appEvents"); + request.includingMetaKeys("editorial-elements:contents", ["personalizationData", "cppData"]); + request.includingScopedAttributes("app-events", AppEventsAttributes); + request.includingScopedRelationships("app-events", ["app"]); + } + if (request.includesResourceType("contingent-items") && + appPromotionsCommon.appContingentItemsAreEnabled(objectGraph)) { + request.enablingFeature("contingentItems"); + mediaRequestUtils.configureContingentItemsForGroupingRequest(request); + attributes = []; + } + if (request.includesResourceType("offer-items") && appPromotionsCommon.appOfferItemsAreEnabled(objectGraph)) { + request.enablingFeature("offerItems"); + mediaRequestUtils.configureOfferItemsForMediaRequest(request); + attributes = []; + } + if (request.includesResourceType("apps") || request.includesResourceType("app-events")) { + attributes = attributes.concat("screenshotsByType", "videoPreviewsByType", "expectedReleaseDateDisplayFormat"); + } + if (areAppTagsEnabled(objectGraph, "grouping")) { + mediaRequestUtils.configureTagsForMediaRequest(request); + } + request.includingAttributes(attributes); +} +// endregion +// region Metrics +export function recoMetricsDataForFCData(objectGraph, mediaApiData) { + const featuredContentId = mediaAttributes.attributeAsNumber(mediaApiData, "editorialElementKind"); + switch (featuredContentId) { + // All of these kinds just require walking their children + case 425 /* FeaturedContentID.AppStore_GenreStack */: + case 415 /* FeaturedContentID.AppStore_HeroList */: + case 416 /* FeaturedContentID.AppStore_Hero */: + case 417 /* FeaturedContentID.AppStore_CustomHero */: + case 501 /* FeaturedContentID.AppStore_PersonalizedHeroMarker */: + case 258 /* FeaturedContentID.Sundance_Flowcase */: + case 421 /* FeaturedContentID.AppStore_BrickRow */: + case 422 /* FeaturedContentID.AppStore_Brick */: + case 423 /* FeaturedContentID.AppStore_CustomBrick */: + case 261 /* FeaturedContentID.Sundance_BrickRow */: + case 584 /* FeaturedContentID.AppStore_TagsBrick */: + case 587 /* FeaturedContentID.AppStore_PersonalizedTagsBrick */: { + const childrenRelationship = mediaRelationship.relationship(mediaApiData, "children"); + return mediaDataStructure.metricsFromMediaApiObject(childrenRelationship); + } + case 437 /* FeaturedContentID.AppStore_LinkList */: + case 265 /* FeaturedContentID.Sundance_LinkList */: { + const contentRelationship = mediaRelationship.relationship(mediaApiData, "children"); + const textLinks = mediaAttributes.attributeAsArrayOrEmpty(mediaApiData, "links"); + if (serverData.isDefinedNonNullNonEmpty(contentRelationship)) { + return mediaDataStructure.metricsFromMediaApiObject(contentRelationship); + } + else if (serverData.isDefinedNonNullNonEmpty(textLinks)) { + return mediaDataStructure.metricsFromMediaApiObject(mediaApiData); + } + return null; + } + case 414 /* FeaturedContentID.AppStore_TabRoot */: + case 424 /* FeaturedContentID.AppStore_ChartSet */: + case 566 /* FeaturedContentID.AppStore_ArcadeDownloadPackMarker */: { + return null; // unapplicable fckinds + } + default: { + if (isLockupShelf(featuredContentId)) { + let childrenRelationship = mediaRelationship.relationship(mediaApiData, "contents"); + if (serverData.isNull(childrenRelationship)) { + return null; + } + const contentItems = childrenRelationship.data; + if (!contentItems || contentItems.length === 0) { + childrenRelationship = mediaRelationship.relationship(mediaApiData, "children"); + } + return mediaDataStructure.metricsFromMediaApiObject(childrenRelationship); + } + else { + objectGraph.console.warn("Unknown featured content ID:", featuredContentId); + return null; + } + } + } +} +// endregion +// region Artwork +/** + * Creating an artwork model with the crop required for a grouping page piece of art + */ +export function groupingArtworkFromApiArtwork(objectGraph, artworkData, options) { + const artwork = content.artworkFromApiArtwork(objectGraph, artworkData, options); + if (artwork) { + artwork.crop = "sr"; + } + return artwork; +} +export function artworkFromFC(objectGraph, node, width, height, options) { + const artworkData = mediaAttributes.attributeAsDictionary(node, "artwork"); + if (artworkData instanceof Array) { + const artwork = legacyArtwork.closestArtworkMatchingSize(objectGraph, artworkData, width, height); + artwork.crop = "bb"; + return artwork; + } + else if (artworkData != null) { + return groupingArtworkFromApiArtwork(objectGraph, artworkData, options); + } + return null; +} +export function artworkForTags(objectGraph, node, width, height, options, metricsOptions) { + const lockupData = serverData.asArrayOrEmpty(node.meta, "associations.apps.data"); + const artworks = []; + if (isSome(lockupData)) { + for (const lockup of lockupData) { + const lockupOptions = { + artworkUseCase: 1 /* content.ArtworkUseCase.LockupIconSmall */, + metricsOptions: metricsOptions, + useJoeColorIconPlaceholder: true, + joeColorPlaceholderSelectionLogic: content.bestJoeColorPlaceholderSelectionLogic, + }; + const lockupFromData = lockups.lockupFromData(objectGraph, lockup, lockupOptions); + const lockupIcon = lockupFromData === null || lockupFromData === void 0 ? void 0 : lockupFromData.icon; + if (isSome(lockupIcon)) { + artworks.push(lockupIcon); + } + } + } + return artworks; +} +// endregion +// region FC Metadata +export function metadataForFCData(objectGraph, fcData, token, shouldPersonalizeContent, personalizationDataContainer, metricsOptions, context, unavailableCallback) { + var _a, _b, _c; + const callUnavailable = function (contentData) { + if (unavailableCallback) { + unavailableCallback(); + } + else { + token === null || token === void 0 ? void 0 : token.remainingItems.push(contentData); + } + }; + const isLinkNode = ((_a = serverData.asString(fcData, "url")) === null || _a === void 0 ? void 0 : _a.length) > 0; + const containsLinkNode = ((_b = mediaAttributes.attributeAsString(fcData, "link.url")) === null || _b === void 0 ? void 0 : _b.length) > 0; + const isContentNodeUsingPrimaryContent = mediaRelationship.hasRelationship(fcData, "primary-content", false); + const isContentNode = mediaRelationship.hasRelationship(fcData, "contents", false) || isContentNodeUsingPrimaryContent; + // Define whether data is a Category Grouping kind. + let isCategoryGroupingKind = mediaAttributes.attributeAsString(fcData, "kind") === "CategoryGrouping"; + if (containsLinkNode || isLinkNode) { + return metadataForLink(objectGraph, fcData, token, metricsOptions, unavailableCallback); + } + else if (isContentNode) { + let contentData; + // Define category grouping content to set when content is a Category Grouping kind. + let categoryGroupingContent; + let personalizedDataResult; + if (shouldPersonalizeContent && !isContentNodeUsingPrimaryContent) { + const contentDataItems = mediaRelationship.relationshipCollection(fcData, "contents"); + personalizedDataResult = onDevicePersonalization.personalizeDataItems(objectGraph, "groupingCommon", contentDataItems, true, personalizationDataContainer, false, 1); + const personalizedContentDataItems = personalizedDataResult.personalizedData; + if (personalizedContentDataItems.length === 0) { + return null; + } + contentData = personalizedContentDataItems[0]; + } + else { + contentData = isContentNodeUsingPrimaryContent + ? mediaRelationship.relationshipData(objectGraph, fcData, "primary-content") + : mediaRelationship.relationshipData(objectGraph, fcData, "contents"); + } + // Check whether content is a Category Grouping kind. + if (mediaAttributes.attributeAsString(contentData, "kind") === "CategoryGrouping") { + categoryGroupingContent = contentData; + // Update content to its primary content. + contentData = mediaRelationship.relationshipData(objectGraph, contentData, "primary-content"); + isCategoryGroupingKind = true; + } + // If the content data is null don't even bother with the call unavailable since we have no way of knowing how to fetch it + if (serverData.isNull(contentData)) { + return null; + } + else if (serverData.isNull(contentData.attributes) || shouldDefer(token)) { + if (serverData.isDefinedNonNullNonEmpty(token)) { + token.isDeferring = true; + } + callUnavailable(contentData); + return null; + } + // Generate the subtitle + let subtitle = content.notesFromData(objectGraph, contentData, "tagline") || + lockups.subtitleFromData(objectGraph, contentData); + // Generate the click action + // Using the fcData here because the ids need to match the id used for the impressions, otherwise the reporting + // heat maps dont work. + // rdar://61527868 (Metrics: Arcade Data Mismatch (Location and Impressions have different name fields)) + const metricsClickOptions = metricsHelpersClicks.clickOptionsForLockup(objectGraph, fcData, metricsOptions); + metricsClickOptions.targetType = metricsOptions.targetType; + let action = lockups.actionFromData(objectGraph, contentData, metricsClickOptions, token === null || token === void 0 ? void 0 : token.clientIdentifierOverride); + // Understand if we're dealing with an article here, so when we go to generate app events, we know if we + // should use the action we've created above or the app event action. + const isArticle = mediaAttributes.attributeAsBooleanOrFalse(contentData, "isCanvasAvailable"); + // Find the artwork and title depending on the content type + let artwork = null; + let caption = null; + // Find lockup (if any) + let lockup = null; + // Find app event (if any) + let appEvent; + const shortEditorialDescription = mediaAttributes.attributeAsString(contentData, "itunesNotes.short"); + const hasContentId = ((_c = contentData.id) === null || _c === void 0 ? void 0 : _c.length) > 0; + const contentMetricsOptions = { + ...metricsOptions, + id: hasContentId ? contentData.id : fcData.id, + idType: hasContentId ? "its_id" : "editorial_id", + }; + switch (contentData.type) { + case "groupings": { + artwork = mediaAttributes.attributeAsDictionary(contentData, "artwork"); + // If data is a Category Grouping kind, reconfigure content. + if (isCategoryGroupingKind) { + contentData = categoryGroupingContent !== null && categoryGroupingContent !== void 0 ? categoryGroupingContent : fcData; + } + break; + } + case "editorial-items": { + // Check for an app-event relationship + const relatedCardContents = mediaRelationship.relationshipData(objectGraph, contentData, "card-contents"); + if (serverData.isDefinedNonNullNonEmpty(relatedCardContents)) { + const clickOptions = { + ...contentMetricsOptions, + inAppEventId: relatedCardContents.id, + }; + const parentAppData = mediaRelationship.relationshipData(objectGraph, relatedCardContents, "app"); + if (serverData.isDefinedNonNull(parentAppData)) { + clickOptions.relatedSubjectIds = [parentAppData.id]; + } + const appEventOrDate = appEvents.appEventOrPromotionStartDateFromData(objectGraph, relatedCardContents, null, false, true, "dark", "white", false, clickOptions, false, true, null, token.isArcadePage, false); + const cardDisplayStyle = mediaAttributes.attributeAsString(contentData, "cardDisplayStyle"); + if (cardDisplayStyle === "AppEventCard") { + if (appEventOrDate instanceof Date) { + // If we get a date back, we have a valid app event, but it starts in the future. + // We don't want the object containing this event to render yet, so return early. + refresh.addNextPreferredContentRefreshDate(appEventOrDate, context.refreshController); + return null; + } + else if (isNothing(appEventOrDate)) { + return null; + } + else { + appEvent = appEventOrDate; + if (!isArticle) { + action = appEvent.clickAction; + } + if (serverData.isNullOrEmpty(subtitle)) { + subtitle = content.notesFromData(objectGraph, relatedCardContents, "short"); + } + } + } + } + caption = mediaAttributes.attributeAsString(contentData, "label"); + if (caption) { + // This is a hack for AOTD/GOTD + caption = caption.replace(/\n/g, " "); + } + const relatedContent = mediaRelationship.relationshipData(objectGraph, contentData, "contents"); + const tagline = serverData.asString(contentData, "editorialNotes.tagline"); + if (serverData.isNullOrEmpty(subtitle)) { + if (tagline) { + subtitle = tagline; + } + else if (relatedContent) { + subtitle = content.notesFromData(objectGraph, relatedContent, "short"); + } + } + if (serverData.isNullOrEmpty(subtitle) && serverData.isDefinedNonNull(appEvent)) { + subtitle = appEvent.subtitle; + } + let crossLinkSubtitle = mediaAttributes.attributeAsString(contentData, "editorialNotes.short"); + if (isNothing(crossLinkSubtitle) || crossLinkSubtitle.length === 0) { + crossLinkSubtitle = subtitle; + } + const cardConfig = defaultTodayCardConfiguration(objectGraph); + if (serverData.isNull(appEvent) && + externalDeepLink.deepLinkUrlFromData(objectGraph, contentData) && + !objectGraph.client.isiOS) { + cardConfig.crossLinkSubtitle = crossLinkSubtitle; + } + cardConfig.clientIdentifierOverride = clientIdentifierForEditorialContextInData(objectGraph, contentData); + if (serverData.isDefinedNonNull(appEvent)) { + lockup = appEvent.lockup; + } + else { + // On iOS and Web, we always attempt to create a lockup. On other platforms, only do this is there is a cross link. + // iOS offer style will be determined later based on artwork. + const offerEnvironment = objectGraph.client.isiOS ? null : "dark"; + const offerStyle = objectGraph.client.isiOS ? null : "white"; + if (objectGraph.client.isiOS || + objectGraph.client.isWeb || + externalDeepLink.deepLinkUrlFromData(objectGraph, contentData)) { + metricsHelpersLocation.pushContentLocation(objectGraph, contentMetricsOptions, token === null || token === void 0 ? void 0 : token.title); + const relatedLockups = lockupsForRelatedContent(objectGraph, mediaRelationship.relationshipCollection(contentData, "card-contents"), cardConfig, metricsOptions.pageInformation, metricsOptions.locationTracker, offerEnvironment, offerStyle, externalDeepLink.deepLinkUrlFromData(objectGraph, contentData)); + if (relatedLockups.length === 1) { + lockup = relatedLockups[0]; + } + metricsHelpersLocation.popLocation(contentMetricsOptions.locationTracker); + } + } + } + // falls through + default: { + // Create a lockup if possible on iOS + const validLockupContentTypes = [ + "apps", + "arcade-apps", + "app-bundles", + "in-apps", + ]; + if (serverData.isNull(lockup) && + validLockupContentTypes.indexOf(contentData.type) > -1 && + objectGraph.host.isiOS) { + metricsHelpersLocation.pushContentLocation(objectGraph, contentMetricsOptions, token === null || token === void 0 ? void 0 : token.title); + const lockupOptions = { + metricsOptions: { + pageInformation: metricsOptions.pageInformation, + locationTracker: metricsOptions.locationTracker, + recoMetricsData: mediaDataStructure.metricsFromMediaApiObject(contentData), + }, + clientIdentifierOverride: token === null || token === void 0 ? void 0 : token.clientIdentifierOverride, + artworkUseCase: content.artworkUseCaseFromShelfStyle(objectGraph, token === null || token === void 0 ? void 0 : token.shelfStyle), + canDisplayArcadeOfferButton: true, + shouldHideArcadeHeader: objectGraph.featureFlags.isEnabled("hide_arcade_header_on_arcade_tab") && + token.isArcadePage, + }; + lockup = lockups.lockupFromData(objectGraph, contentData, lockupOptions); + metricsHelpersLocation.popLocation(contentMetricsOptions.locationTracker); + } + artwork = + contentAttributes.contentAttributeAsDictionary(objectGraph, contentData, "editorialArtwork") || + mediaAttributes.attributeAsDictionary(contentData, "editorialArtwork"); + if (serverData.isNullOrEmpty(subtitle) && serverData.isDefinedNonNull(lockup)) { + subtitle = lockup.subtitle; + } + break; + } + } + if (serverData.isDefinedNonNull(action)) { + action.presentationStyle = ["textFollowsTintColor"]; + // If data is NOT a Category Grouping kind, reconfigure action title. + if (!isCategoryGroupingKind) { + // Prefer design tag or editorial name for title when available + const designTag = unescapeHtmlString(mediaAttributes.attributeAsString(fcData, "designTag")); + const editorialTitle = content.notesFromData(objectGraph, contentData, "name"); + action.title = designTag || editorialTitle || action.title || subtitle || caption; + } + } + return { + action: action, + caption: caption, + title: action === null || action === void 0 ? void 0 : action.title, + subtitle: subtitle, + artwork: artwork, + shortEditorialDescription: shortEditorialDescription, + content: contentData, + lockup: lockup, + appEvent: appEvent, + onDevicePersonalizationDataProcessingType: personalizedDataResult === null || personalizedDataResult === void 0 ? void 0 : personalizedDataResult.processingType, + }; + } + return null; +} +/// Gets the action and title for a brick that is backed by tags. +export function metadataForTag(objectGraph, fcData, token, metricsOptions) { + const shortEditorialDescription = mediaAttributes.attributeAsString(fcData, "name"); + const href = serverData.asString(fcData, "href"); + const url = hrefToRoutableUrl(objectGraph, href); + const flowPage = objectGraph.required(pageRouter).fetchFlowPage(url); + const flowAction = new models.FlowAction(flowPage); + flowAction.pageUrl = url; + return { + action: flowAction, + caption: "null", + title: shortEditorialDescription, + subtitle: "null", + artwork: null, + shortEditorialDescription: shortEditorialDescription, + }; +} +function metadataForLink(objectGraph, fcData, token, metricsOptions, unavailableCallback) { + const isFCLinkData = serverData.isDefinedNonNull(serverData.asString(fcData, "url")); + const linkData = isFCLinkData ? fcData : mediaAttributes.attributeAsDictionary(fcData, "link"); + const callUnavailable = function (contentData) { + if (unavailableCallback) { + unavailableCallback(); + } + else { + token === null || token === void 0 ? void 0 : token.remainingItems.push(contentData); + } + }; + if (serverData.isNull(linkData) || shouldDefer(token)) { + callUnavailable(fcData); + return null; + } + const target = serverData.asString(linkData, "target"); + const url = serverData.asString(linkData, "url"); + // Prefer design tag for title when available + const label = serverData.asString(linkData, "label"); + const designTag = unescapeHtmlString(mediaAttributes.attributeAsString(fcData, "designTag")); + const title = designTag || label; + let action = null; + if (target === "external") { + action = new models.ExternalUrlAction(url); + action.title = title; + } + else { + const flowPage = objectGraph.required(pageRouter).fetchFlowPage(url); + const flowAction = new models.FlowAction(flowPage); + flowAction.pageUrl = url; + flowAction.title = title; + action = flowAction; + } + action.presentationStyle = ["textFollowsTintColor"]; + // Configure metrics + const clickOptions = { + ...metricsOptions, + id: "", + }; + metricsHelpersClicks.addClickEventToAction(objectGraph, action, clickOptions); + return { + action: action, + caption: null, + title: title, + subtitle: null, + artwork: null, + shortEditorialDescription: null, + }; +} +// endregion +// region Editorial Data Merging +export function mergeContentDataIntoEditorialData(contentDataArray, editorialItemsDataArray) { + const contentDataMap = {}; + for (const contentData of contentDataArray) { + contentDataMap[contentData.id] = contentData; + } + // The relationships we're interested in merging fetched content for + const relationShipsToMerge = ["contents", "grouping"]; + const mergedEditorialData = []; + // Steps for merging + // 1. Loop through the editorial items array + // 2. For each editorial item loop through the relationships we're interested in + // 3. If this editorial item has any of these relationships loop through that relationsip looking in our content map + // for the hydrated version of each piece of data in the relationship + // 4. If all the content has been fetched for each relationship we can add it to our resulting array of hydrated editorial items. + for (const editorialItemData of editorialItemsDataArray) { + let hasHydratedAllRelationships = true; + for (const relationshipType of relationShipsToMerge) { + const unhydratedRelationshipCollection = mediaRelationship.relationshipCollection(editorialItemData, relationshipType); + if (serverData.isDefinedNonNull(unhydratedRelationshipCollection)) { + const hydratedRelationship = []; + for (const unhydratedData of unhydratedRelationshipCollection) { + const hydratedData = contentDataMap[unhydratedData.id]; + if (serverData.isDefinedNonNullNonEmpty(hydratedData)) { + hydratedRelationship.push(hydratedData); + } + } + if (hydratedRelationship.length === unhydratedRelationshipCollection.length) { + editorialItemData.relationships[relationshipType] = { data: hydratedRelationship }; + } + else { + hasHydratedAllRelationships = false; + } + } + } + if (hasHydratedAllRelationships) { + mergedEditorialData.push(editorialItemData); + } + } + return mergedEditorialData; +} +// endregion +// region Shelf Info +/** + * For incomplete shelf fetches via a secondary lookup, whether a given shelf w/ token should merge or replace. + * @param objectGraph + * @param token The token corresponding to the shelf being built + */ +export function shelfFetchShouldMergeWhenFetched(objectGraph, token) { + /** + * <rdar://problem/60069585> [POLISH] Arcade Coming Soon: If too few items, the coming soon swoosh should show larger items. + * Always reload posterLockup shelves on macOS to adapt if the presentationHint "isLowDensity" changed. + */ + if (token.shelfStyle === "posterLockup" && objectGraph.client.isMac) { + return false; + } + else if (token.showingPlaceholders) { + return false; + } + // Always reload Arcade download pack shelf + // as there is always only a single cell (card) that contains lockups or their placeholders. + if (token.shelfStyle === "arcadeDownloadPackCard") { + return false; + } + if (token.shelfStyle === "ribbonBar" && + isSome(token.initialHydratedItems) && + token.initialHydratedItems.length > 0) { + return false; + } + // Generally true to support partially hydrated shelves. + return true; +} +// endregion +// region Search Landing Shelves +/** + * Modify `shelf` in place for global customization for all SLP shelves. + * @param objectGraph + * @param shelf + * @param token + */ +export function modifyShelfForSearchLandingGrouping(objectGraph, shelf, token) { + shelf.seeAllAction = null; + shelf.isHorizontal = false; + if (shelf.shouldFilterApps) { + // <rdar://problem/64772261> App Store: SLP: Installed app 'Open' button show up inconsistently (Filtering is sometimes not applied) + shelf.filteredItemsMinimumCount = 0; + shelf.filteringExcludedItems = token.includedAdAdamIds; + } +} +// endregion +// region Card Shelves +/** + * The shelf content type to use for the inline card display style. + * @param objectGraph + * @param {HorizontalCardDisplayStyle} style The display style for the inline card. + * @returns {ShelfContentType} The shelf content type to use. + */ +export function contentTypeForHorizontalCardDisplayStyle(objectGraph, style) { + switch (style) { + case "small": + return "smallStoryCard"; + case "medium": + return "mediumStoryCard"; + case "large": + return "largeStoryCard"; + case "card": + if (objectGraph.client.isiOS) { + return "editorialStoryCard"; + } + else { + return null; + } + default: + return null; + } +} +// endregion +/** + * Determine the best content id to use given a media api data object + * @param objectGraph + * @param contentItem The media api data model to find the id from + */ +export function contentIdFromContentItem(objectGraph, contentItem) { + let contentId = mediaAttributes.attributeAsString(contentItem, "adamId"); + if (!contentId) { + contentId = mediaAttributes.attributeAsString(contentItem, "contentId"); + } + if (!contentId) { + contentId = mediaAttributes.attributeAsString(contentItem, "id"); + } + return contentId; +} +export function unescapeHtmlString(str) { + if (serverData.isNull(str)) { + return null; + } + const escapedString = str + .replace(/&/g, "&") + .replace(/>/g, ">") + .replace(/</g, "<") + .replace(/"/g, '"') + .replace(/'/g, "'") + .replace(/`/g, "`") + .replace(/\r\n/g, " ") + .replace(/ /g, " ") + .replace(/<span>/g, "") + .replace(/<\/span>/g, "") + .replace(/<br>/g, " ") + .replace(/\u23ce/g, "") + .replace(/<i>/g, "") + .replace(/<\/i>/g, "") + .replace(/<b>/g, "") + .replace(/<\/b>/g, ""); + if (escapedString.match(/^\s*$/)) { + return null; + } + return escapedString; +} +/** + * Update the shelf header to use the provided seeAll action. + * @param objectGraph The App Store object graph used to check feature flags + * @param shelfHeader The shelf header to update + * @param seeAllAction The "See All" action to apply + */ +export function replaceShelfHeaderSeeAllAction(objectGraph, shelfHeader, seeAllAction) { + if (objectGraph.featureFlags.isEnabled("shelf_header")) { + // Modern headers make title tappable with a chevron (>). + shelfHeader.titleAction = seeAllAction; + } + else { + // Legacy headers show "See All" textual button on trailing edge. + shelfHeader.accessoryAction = seeAllAction; + } +} +/** + * Update the shelf header to use the provided seeAll action. + * @param objectGraph The App Store object graph used to check feature flags + * @param shelf The shelf whose header needs updating + * @param seeAllAction The see all action to apply + */ +export function replaceShelfSeeAllAction(objectGraph, shelf, seeAllAction) { + if (objectGraph.featureFlags.isEnabled("shelf_header")) { + if (isSome(shelf.header)) { + replaceShelfHeaderSeeAllAction(objectGraph, shelf.header, seeAllAction); + } + else { + shelf.header = { + titleAction: seeAllAction, + }; + } + } + else { + shelf.seeAllAction = seeAllAction; + } +} +//# sourceMappingURL=grouping-shelf-controller-common.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/grouping/shelf-controllers/grouping-shelf-controller.js b/node_modules/@jet-app/app-store/tmp/src/common/grouping/shelf-controllers/grouping-shelf-controller.js new file mode 100644 index 0000000..fabf85c --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/grouping/shelf-controllers/grouping-shelf-controller.js @@ -0,0 +1,395 @@ +import { isSome } from "@jet/environment"; +import * as models from "../../../api/models"; +import * as serverData from "../../../foundation/json-parsing/server-data"; +import * as mediaDataFetching from "../../../foundation/media/data-fetching"; +import * as mediaNetwork from "../../../foundation/media/network"; +import { ResponseMetadata } from "../../../foundation/network/network"; +import { Parameters, Path, Protocol } from "../../../foundation/network/url-constants"; +import * as urls from "../../../foundation/network/urls"; +import * as mediaUrlMapping from "../../builders/url-mapping"; +import { shouldUsePrerenderedIconArtwork } from "../../content/content"; +import * as metricsHelpersImpressions from "../../metrics/helpers/impressions"; +import * as metricsHelpersLocation from "../../metrics/helpers/location"; +import * as impressionDemotion from "../../personalization/on-device-impression-demotion"; +import * as productVariants from "../../product-page/product-page-variants"; +import * as refresh from "../../refresh/page-refresh-controller"; +import * as groupingShelfControllerCommon from "./grouping-shelf-controller-common"; +/** + * A GroupingShelfController is responsible for all the logic around parsing and rending + * a single grouping page shelf. + * + * The `ShelfMetadata` is a type the is specific for a given shelf and has some additional data needed to render + * that shelf. + */ +export class GroupingShelfController { + // endregion + // region AnyGroupingShelfController + /** + * Indicates whether this grouping shelf controller can create a shelf for the given mediaApiData. + * @param objectGraph The App Store dependency graph + * @param mediaApiData The outer data object containing the FC properties and data + * @param featuredContentId The featured content id for this shelf data + * @param nativeGroupingShelfId The id of the custom shelf type, one not defined on the server + */ + supports(objectGraph, mediaApiData, featuredContentId, nativeGroupingShelfId) { + return this._supports(objectGraph, mediaApiData, featuredContentId, nativeGroupingShelfId); + } + /** + * Indicates whether this grouping shelf controller can create a shelf for the given mediaApiData. + * @param objectGraph The App Store dependency graph + * @param mediaApiData The outer data object containing the FC properties and data + * @param featuredContentId The featured content id for this shelf data + * @param nativeGroupingShelfId The id of the custom shelf type, one not defined on the server + */ + _supports(objectGraph, mediaApiData, featuredContentId, nativeGroupingShelfId) { + const isFeaturedContentIdSupported = this.supportedFeaturedContentIds.has(featuredContentId); + let isNativeGroupingShelfIdSupported; + if (serverData.isDefinedNonNull(nativeGroupingShelfId)) { + isNativeGroupingShelfIdSupported = this.supportedNativeGroupingShelfIds.has(nativeGroupingShelfId); + } + else { + isNativeGroupingShelfIdSupported = true; + } + return isFeaturedContentIdSupported && isNativeGroupingShelfIdSupported; + } + /** + * This method will return a grouping page shelf regardless of the type of controller + * @param objectGraph The App Store dependency graph + * @param groupingParseContext The parse context for the grouping page so far + * @param mediaApiData The outer data object containing the FC properties and data + * @param baseShelfToken The base grouping shelf token created by the grouping-controller + * @param baseMetricsOptions The minimum set of metrics options for this shelf, created by the + * grouping page controller + */ + createShelf(objectGraph, mediaApiData, groupingParseContext, baseShelfToken, baseMetricsOptions) { + var _a, _b, _c; + const typedMediaApiData = mediaApiData; + const shelfData = this.initialShelfDataFromGroupingMediaApiData(objectGraph, typedMediaApiData); + const shelfToken = this.shelfTokenFromBaseTokenAndMediaApiData(objectGraph, typedMediaApiData, baseShelfToken, groupingParseContext); + const shelfMetricsOptions = this.shelfMetricsOptionsFromBaseMetricsOptions(objectGraph, shelfToken, baseMetricsOptions); + const hasShelfMetricsOptions = serverData.isDefinedNonNullNonEmpty(shelfMetricsOptions); + if (hasShelfMetricsOptions && this.shouldImpressShelf()) { + metricsHelpersLocation.pushContentLocation(objectGraph, shelfMetricsOptions, shelfToken.title); + } + /// Reorder the shelf contents based on the impression data if available + if (serverData.isDefinedNonNullNonEmpty(shelfData.shelfContents)) { + shelfData.shelfContents = impressionDemotion.personalizeDataItems(shelfData.shelfContents, (_a = groupingParseContext.recoImpressionData) !== null && _a !== void 0 ? _a : {}, (_b = baseMetricsOptions === null || baseMetricsOptions === void 0 ? void 0 : baseMetricsOptions.recoMetricsData) !== null && _b !== void 0 ? _b : {}); + } + const shelf = this._createShelf(objectGraph, shelfToken, shelfData, groupingParseContext); + if (hasShelfMetricsOptions && this.shouldImpressShelf()) { + metricsHelpersLocation.popLocation(shelfMetricsOptions.locationTracker); + if (serverData.isDefinedNonNull(shelf)) { + metricsHelpersImpressions.addImpressionFields(objectGraph, shelf, shelfMetricsOptions); + // rdar://84952935 (Placeholder shelves are not being impressed + // For placeholder shelves we end up replacing the entire shelf, so we need to make sure the original + // impression metrics are included in the token, so they can be added when the real content is fetched + // We're doing this here because this is where we decide whether the original shelf should be impressed + if (((_c = shelf.url) === null || _c === void 0 ? void 0 : _c.length) > 0 && + serverData.isDefinedNonNullNonEmpty(shelf.impressionMetrics) && + shelfToken.showingPlaceholders) { + const originalShelfUrlString = shelf.url; + try { + // Extract the token from the URL. + // Note: Although we have access to the shelfToken here, we do not know that + // the url was constructed from token in its current state. To be safe, + // if not efficient, we reverse engineer the URL to get the token. + const originalShelfUrl = urls.URL.from(originalShelfUrlString); + const encodedToken = originalShelfUrl.pathComponents().pop(); + const shelfTokenFromUrl = JSON.parse(decodeURIComponent(encodedToken)); + // Modify the token to include the impressions metrics. + shelfTokenFromUrl.originalPlaceholderShelfImpressionMetrics = shelf.impressionMetrics; + groupingShelfControllerCommon.updateShelfUrlWithNewToken(objectGraph, shelf, shelfTokenFromUrl); + } + catch { + shelf.url = originalShelfUrlString; + } + } + } + } + this.finalizeInitialShelfForDisplay(objectGraph, shelf, shelfToken, shelfData, groupingParseContext); + if (hasShelfMetricsOptions && this.shouldPrepareLocationTrackerForNextPosition()) { + metricsHelpersLocation.nextPosition(groupingParseContext.metricsLocationTracker); + } + return shelf; + } + /** + * Initialize a builder with globally unique name. + * + * @param {string} builderClass Globally unique name. + */ + constructor(builderClass) { + // region Supported Types + this.supportedFeaturedContentIds = new Set([]); + this.supportedNativeGroupingShelfIds = new Set([]); + this.builderClass = builderClass; + } + /** + * Determines the strategy for fetching incomplete shelves based on feature flags and shelf type + * + * @param objectGraph - The application store object graph. + * @returns The strategy for fetching incomplete shelves, either on shelf appearance or on page load. + */ + incompleteShelfFetchStrategy(objectGraph) { + if (objectGraph.client.isiOS) { + return models.IncompleteShelfFetchStrategy.OnShelfWillAppear; + } + else { + return models.IncompleteShelfFetchStrategy.OnPageLoad; + } + } + // endregion + // region Metrics + /** + * Return the shelf metrics options to use for this specific shelf. Using the base options from the grouping + * page controller + * + * @param objectGraph The App Store dependency graph + * @param shelfToken The shelf shelfToken for this current shelf creation request + * @param baseMetricsOptions The minimum set of metrics options for this shelf, created by the + * grouping page controller + */ + shelfMetricsOptionsFromBaseMetricsOptions(objectGraph, shelfToken, baseMetricsOptions) { + return baseMetricsOptions; + } + /** + * Whether the shelf itself should be impressed, there are some cases where the shelf itself + * does not get impressed, just the contents. + */ + shouldImpressShelf() { + return true; + } + /** + * Whether we should move the location tracker to the next position after creating our shelf + */ + shouldPrepareLocationTrackerForNextPosition() { + return true; + } + // endregion + // Shelf Finalization + /** + * This method will set any required fields on our shelf once it has created as part of the initial page rendering. + * This includes things like timing metrics, hiding empty shelves etc. + * + * @param objectGraph The App Store dependency graph + * @param shelf The created shelf + * @param shelfToken The shelf shelfToken for this current shelf creation request + * @param shelfData The media api shelfContents array for this shelf + * @param groupingParseContext The parse context for the grouping page so far + * @private + */ + finalizeInitialShelfForDisplay(objectGraph, shelf, shelfToken, shelfData, groupingParseContext) { + var _a, _b; + if (serverData.isNullOrEmpty(shelf)) { + return; + } + // Should not show see all links on search groupings + if (shelfToken.isSearchLandingPage) { + groupingShelfControllerCommon.modifyShelfForSearchLandingGrouping(objectGraph, shelf, shelfToken); + } + if (((_a = shelf.url) === null || _a === void 0 ? void 0 : _a.length) > 0 && + serverData.isDefinedNonNullNonEmpty(groupingParseContext.additionalShelfParameters)) { + shelf.url = urls.URL.from(shelf.url) + .append("query", groupingParseContext.additionalShelfParameters) + .build(); + } + // If we're on iOS and no prior fetch strategy has been defined, set the fetchStrategy to OnShelfWillAppear. + shelf.fetchStrategy = this.incompleteShelfFetchStrategy(objectGraph); + // Shelf will fetch content after sending follow-up fetch request. + const willFetchShelfContent = isSome(shelf) && ((_b = shelf.url) === null || _b === void 0 ? void 0 : _b.length) > 0; + if (serverData.isNullOrEmpty(shelf.items) && !willFetchShelfContent) { + shelf.isHidden = true; + } + shelf.accessibilityMetadata = createShelfAccessibilityMetadata(objectGraph, shelf); + } + /** + * This method will set any required fields on our shelf once it has been fetched as a result of a secondary fetch. + * This includes things like timing metrics, hiding empty shelves etc. + * + * @param objectGraph The AppStore dependency graph + * @param shelf The created shelf + * @param shelfToken The shelf shelfToken for this current shelf creation request + * @param shelfData The media api shelfContents array for this shelf + * @private + */ + finalizeSecondaryShelfForDisplay(objectGraph, shelf, shelfToken, shelfData) { + if (serverData.isNullOrEmpty(shelf)) { + return; + } + if (shelfToken.remainingItems.length) { + const remainingIds = shelfToken.remainingItems.map((data) => { + return data.id; + }); + objectGraph.console.warn("Could not load items for: " + remainingIds.join(",")); + } + if (shelf) { + shelf.mergeWhenFetched = groupingShelfControllerCommon.shelfFetchShouldMergeWhenFetched(objectGraph, shelfToken); + shelf.networkTimingMetrics = shelfData.responseTimingValues; + shelf.nextPreferredContentRefreshDate = refresh.nextPreferredContentRefreshDateForController(refresh.newPageRefreshController()); + } + // Merge the original impression metrics with the newly created impression metrics. + if (serverData.isDefinedNonNullNonEmpty(shelfToken.originalPlaceholderShelfImpressionMetrics)) { + // If the `shelf.impressionMetrics` is null, we just defer to the original metrics. + if (serverData.isNull(shelf.impressionMetrics)) { + shelf.impressionMetrics = shelfToken.originalPlaceholderShelfImpressionMetrics; + } + else { + for (const key in shelfToken.originalPlaceholderShelfImpressionMetrics.fields) { + if (Object.prototype.hasOwnProperty.call(shelfToken.originalPlaceholderShelfImpressionMetrics.fields, key)) { + shelf.impressionMetrics.fields[key] = + shelfToken.originalPlaceholderShelfImpressionMetrics.fields[key]; + } + } + } + } + if (!shelfToken.hasExistingContent && serverData.isNullOrEmpty(shelf.items)) { + shelf.isHidden = true; + } + // Should not show see all links on search groupings + if (shelfToken.isSearchLandingPage) { + groupingShelfControllerCommon.modifyShelfForSearchLandingGrouping(objectGraph, shelf, shelfToken); + } + shelf.accessibilityMetadata = createShelfAccessibilityMetadata(objectGraph, shelf); + } + // endregion + // region ShelfBuilder + async handleShelf(objectGraph, url, parameters, matchedRuleIdentifier) { + const tokenJson = parameters["token"]; + const shelfToken = JSON.parse(tokenJson); + shelfToken.isFirstRender = false; + try { + const shelfData = await this.secondaryShelfDataForShelfUrl(objectGraph, url, shelfToken, parameters); + const shelf = this._createShelf(objectGraph, shelfToken, shelfData, null); + this.finalizeSecondaryShelfForDisplay(objectGraph, shelf, shelfToken, shelfData); + return shelf; + } + catch (error) { + if (shelfToken && !shelfToken.hasExistingContent) { + const hiddenShelf = new models.Shelf(shelfToken.shelfStyle); + hiddenShelf.isHidden = true; + return hiddenShelf; + } + else { + throw error; + } + } + } + shelfRoute(objectGraph) { + if (serverData.isDefinedNonNullNonEmpty(this.supportedNativeGroupingShelfIds)) { + return routesForNativeGroupingShelfIds(this.supportedNativeGroupingShelfIds); + } + else { + return routesForFeaturedContentIds(this.supportedFeaturedContentIds); + } + } + // endregion + // region Static Base Helpers + /** + * This is a standard default implementation for the secondary shelf data fetch. This can be used for all the + * grouping shelf controls that dont implement a custom ShelfDataType + * @param objectGraph + * @param shelfUrl + * @param parameters + */ + static async secondaryGroupingShelfDataForShelfUrl(objectGraph, shelfUrl, shelfToken, parameters) { + return await GroupingShelfController.secondaryGroupingShelfMediaApiData(objectGraph, shelfUrl, shelfToken, parameters).then((mediaApiData) => { + const hydratedItems = groupingShelfControllerCommon.hydratedRemainingItemsForShelfTokenFromMediaApiData(objectGraph, shelfToken, mediaApiData); + return { + shelfContents: hydratedItems, + responseTimingValues: mediaApiData[ResponseMetadata.timingValues], + }; + }); + } + /** + * This is a standard default implementation for the media api request, for an incomplete grouping shelf. + * + * @param objectGraph + * @param shelfUrl + * @param parameters + */ + static async secondaryGroupingShelfMediaApiData(objectGraph, shelfUrl, shelfToken, parameters) { + const urlString = shelfUrl.build(); + let request; + if (mediaUrlMapping.isMediaUrl(objectGraph, shelfUrl)) { + request = new mediaDataFetching.Request(objectGraph, urlString); + } + else { + request = groupingShelfControllerCommon.generateShelfRequest(objectGraph, shelfToken, parameters); + } + if (!request) { + return await Promise.reject(new Error(`Could not construct media API request for: ${shelfUrl}`)); + } + request.includingAdditionalPlatforms(defaultMediaApiPlatforms(objectGraph)); + request.includingAttributes(defaultMediaApiAttributes(objectGraph)); + request.usingCustomAttributes(productVariants.shouldFetchCustomAttributes(objectGraph)); + request.attributingTo(shelfUrl.build()); + return await mediaNetwork.fetchData(objectGraph, request).then((mediaApiData) => { + groupingShelfControllerCommon.flushRequestedItemsFromShelfToken(shelfToken, request.ids); + return mediaApiData; + }); + } +} +export function createShelfAccessibilityMetadata(objectGraph, shelf) { + var _a; + let accessibilityLabel = objectGraph.loc.string("Shelves.Accessibility.Label"); + if (isSome(shelf.title)) { + accessibilityLabel = `${shelf.title}, ${accessibilityLabel}`; + } + else if (isSome((_a = shelf.header) === null || _a === void 0 ? void 0 : _a.title)) { + accessibilityLabel = `${shelf.header.title}, ${accessibilityLabel}`; + } + const accessibilityRoleDescription = objectGraph.loc.string("Shelves.Accessibility.RoleDescription"); + return { + label: accessibilityLabel, + roleDescription: accessibilityRoleDescription, + }; +} +function routeForFeaturedContentId(featuredContentId, nativeGroupingShelfId, additionalQueryParams) { + const query = serverData.isDefinedNonNullNonEmpty(additionalQueryParams) + ? [...additionalQueryParams] + : []; + query.push(`${Parameters.groupingFeaturedContentId}=${featuredContentId}`); + if (serverData.isDefinedNonNullNonEmpty(nativeGroupingShelfId)) { + query.push(`${Parameters.nativeGroupingShelfId}=${nativeGroupingShelfId}`); + } + return { + protocol: Protocol.internal, + path: `/${Path.grouping}/${Path.shelf}/{token}`, + query: query, + }; +} +export function routesForFeaturedContentIds(featuredContentIds, additonalQueryParams) { + const routes = []; + for (const featuredContentId of featuredContentIds) { + routes.push(routeForFeaturedContentId(featuredContentId, null, additonalQueryParams)); + } + return routes; +} +export function routesForNativeGroupingShelfIds(nativeGroupingShelfIds, additonalQueryParams) { + const routes = []; + for (const nativeGroupingShelfId of nativeGroupingShelfIds) { + routes.push(routeForFeaturedContentId(-1 /* FeaturedContentID.Native_GroupingShelf */, nativeGroupingShelfId, additonalQueryParams)); + } + return routes; +} +// region Media Api Attributes +function defaultMediaApiPlatforms(objectGraph) { + return mediaDataFetching.defaultAdditionalPlatformsForClient(objectGraph); +} +function defaultMediaApiAttributes(objectGraph) { + const attributes = ["editorialArtwork", "isAppleWatchSupported", "requiredCapabilities", "badge-content"]; + if (objectGraph.appleSilicon.isSupportEnabled) { + attributes.push("macRequiredCapabilities"); + } + if (objectGraph.client.isMac) { + attributes.push("hasMacIPAPackage"); + } + if (objectGraph.bag.enableUpdatedAgeRatings) { + attributes.push("ageRating"); + } + if (shouldUsePrerenderedIconArtwork(objectGraph)) { + attributes.push("iconArtwork"); + } + return attributes; +} +// endregion +//# sourceMappingURL=grouping-shelf-controller.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/grouping/shelf-controllers/grouping-small-breakout-shelf-controller.js b/node_modules/@jet-app/app-store/tmp/src/common/grouping/shelf-controllers/grouping-small-breakout-shelf-controller.js new file mode 100644 index 0000000..d7eac20 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/grouping/shelf-controllers/grouping-small-breakout-shelf-controller.js @@ -0,0 +1,168 @@ +import * as models from "../../../api/models"; +import * as serverData from "../../../foundation/json-parsing/server-data"; +import * as mediaAttributes from "../../../foundation/media/attributes"; +import * as mediaDataStructure from "../../../foundation/media/data-structure"; +import * as mediaPlatformAttributes from "../../../foundation/media/platform-attributes"; +import * as mediaRelationship from "../../../foundation/media/relationships"; +import * as color from "../../../foundation/util/color-util"; +import * as breakoutsCommon from "../../arcade/breakouts-common"; +import * as contentAttributes from "../../content/attributes"; +import * as content from "../../content/content"; +import * as flowPreview from "../../content/flow-preview"; +import * as metricsHelpersClicks from "../../metrics/helpers/clicks"; +import * as metricsHelpersImpressions from "../../metrics/helpers/impressions"; +import * as metricsHelpersLocation from "../../metrics/helpers/location"; +import * as article from "../../today/article"; +import { GroupingShelfController } from "./grouping-shelf-controller"; +import * as groupingShelfControllerCommon from "./grouping-shelf-controller-common"; +export class GroupingSmallBreakoutShelfController extends GroupingShelfController { + // region Constructors + constructor() { + super("GroupingSmallBreakoutShelfController"); + this.supportedFeaturedContentIds = new Set([480 /* groupingTypes.FeaturedContentID.AppStore_Breakout */]); + } + // endregion + // region GroupingShelfController + _supports(objectGraph, mediaApiData, featuredContentId, nativeGroupingShelfId) { + if (!super._supports(objectGraph, mediaApiData, featuredContentId, nativeGroupingShelfId)) { + return false; + } + const breakoutStyle = mediaAttributes.attributeAsString(mediaApiData, "displayStyle"); + return breakoutStyle === "small"; + } + // endregion + // region Shelf Creation Prerequisites + /** + * For a given mediaApiData extract the actual shelfContents array needed to render this shelf + * + * @param mediaApiData The outer shelfContents object containing the shelf contents + */ + initialShelfDataFromGroupingMediaApiData(objectGraph, mediaApiData) { + return { shelfContents: mediaRelationship.relationshipCollection(mediaApiData, "contents") }; + } + /** + * For a given url that this controller handles, we should return a promise that will result in the `ShelfData` + * needed to render this shelf + * + * @param objectGraph The App Store dependency graph + * @param shelfUrl The url that this controller handled on a secondary fetch + * @param parameters The extracted parameters from the shelf url + */ + async secondaryShelfDataForShelfUrl(objectGraph, shelfUrl, shelfToken, parameters) { + return await GroupingShelfController.secondaryGroupingShelfDataForShelfUrl(objectGraph, shelfUrl, shelfToken, parameters); + } + /** + * For a given mediaApiData create an updated shelf token that contains all the additional data for this specific shelf type + * + * @param objectGraph The App Store dependency graph + * @param baseShelfToken The base grouping shelf token created by the grouping-controller + * @param mediaApiData The outer data object containing the FC properties and data + * @param groupingParseContext The parse context for the grouping page so far + */ + shelfTokenFromBaseTokenAndMediaApiData(objectGraph, mediaApiData, baseShelfToken, groupingParseContext) { + return baseShelfToken; + } + // endregion + // region Metrics + shouldImpressShelf() { + return false; + } + // endregion + // region Shelf Creation + /** + * + * @param objectGraph The App Store dependency graph + * @param shelfToken The shelf shelfToken for this current shelf creation request + * @param shelfData The media api shelfContents array for this shelf + * @param groupingParseContext The parse context used to generate the grouping page on the initial page load, + * this will be missing when this controller renders a secondary or incomplete shelf fetch. + */ + _createShelf(objectGraph, shelfToken, shelfData, groupingParseContext) { + const items = []; + for (const data of shelfData.shelfContents) { + if (serverData.isNull(data.attributes) || groupingShelfControllerCommon.shouldDefer(shelfToken)) { + shelfToken.isDeferring = true; + shelfToken.remainingItems.push(data); + continue; + } + const primaryContent = content.primaryContentForData(objectGraph, data); + if (breakoutsCommon.requiresPrimaryContent(objectGraph, data) && + !mediaAttributes.hasAttributes(primaryContent)) { + shelfToken.isDeferring = true; + shelfToken.remainingItems.push(data); + shelfToken.relationshipToFetch = "primary-content"; + continue; + } + const metricsOptions = { + targetType: "smallBreakout", + pageInformation: shelfToken.metricsPageInformation, + locationTracker: shelfToken.metricsLocationTracker, + recoMetricsData: mediaDataStructure.metricsFromMediaApiObject(data), + }; + // If this EI can and should show the expected release date of an arcade app, override the badge. + let badgeTitle; + const fallbackLabel = mediaAttributes.attributeAsString(data, "label"); + if (contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "showExpectedReleaseDate")) { + badgeTitle = objectGraph.loc.uppercased(content.dynamicPreorderDateFromData(objectGraph, primaryContent, fallbackLabel)); + } + else { + badgeTitle = fallbackLabel; + } + let badge = { type: "none" }; + if ((badgeTitle === null || badgeTitle === void 0 ? void 0 : badgeTitle.length) > 0) { + badge = { + type: "text", + title: badgeTitle, + }; + } + const title = content.editorialNotesFromData(objectGraph, data, "name") || + contentAttributes.contentAttributeAsString(objectGraph, primaryContent, "name"); + const description = content.editorialNotesFromData(objectGraph, data, "short") || + contentAttributes.contentAttributeAsString(objectGraph, primaryContent, "tagline"); + const artwork = content.iconFromData(objectGraph, primaryContent, { + useCase: 5 /* content.ArtworkUseCase.ArcadeSmallBreakout */, + withJoeColorPlaceholder: true, + }); + const attributePlatform = contentAttributes.bestAttributePlatformFromData(objectGraph, data); + const artworkData = mediaPlatformAttributes.platformAttributeAsDictionary(primaryContent, attributePlatform, "artwork"); + const backgroundColor = color.fromHex(serverData.asString(artworkData, "textColor4")) || (artwork === null || artwork === void 0 ? void 0 : artwork.backgroundColor); + const details = new models.BreakoutDetails(title, description, badge, null, breakoutsCommon.detailBackgroundStyleFromColor(objectGraph, backgroundColor), null); + const smallBreakout = new models.SmallBreakout(details, artwork, backgroundColor); + const impressionOptions = metricsHelpersImpressions.impressionOptions(objectGraph, data, smallBreakout.details.title, metricsOptions); + const productData = article.productDataFromArticle(objectGraph, data); + const isPreorder = contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, productData, "isPreorder"); + impressionOptions.isPreorder = isPreorder; + metricsHelpersImpressions.addImpressionFields(objectGraph, smallBreakout, impressionOptions); + // Push the breakout here so that the click action has the breakout in its location + // but we do not want to add it to the overall location tracker, so pop it right after adding it to the button + // action + // <rdar://problem/60883269> Metrics: Arcade: Container values requested in Location field + metricsHelpersLocation.pushContentLocation(objectGraph, impressionOptions, smallBreakout.details.title); + const breakoutAction = breakoutsCommon.actionFromData(objectGraph, data); + const breakoutClickOptions = { + pageInformation: shelfToken.metricsPageInformation, + locationTracker: shelfToken.metricsLocationTracker, + recoMetricsData: mediaDataStructure.metricsFromMediaApiObject(data), + targetType: "button", + id: data.id, + }; + metricsHelpersClicks.addClickEventToAction(objectGraph, breakoutAction, breakoutClickOptions); + smallBreakout.details.callToActionButtonAction = breakoutAction; + smallBreakout.clickAction = breakoutAction; + metricsHelpersLocation.popLocation(metricsOptions.locationTracker); + // Set flow preview actions + smallBreakout.flowPreviewActionsConfiguration = + flowPreview.flowPreviewActionsConfigurationForProductFromData(objectGraph, data, true, shelfToken === null || shelfToken === void 0 ? void 0 : shelfToken.clientIdentifierOverride, breakoutAction, metricsOptions, breakoutClickOptions); + items.push(smallBreakout); + } + const shelf = new models.Shelf("smallBreakout"); + shelf.isHorizontal = false; + shelf.items = items; + shelf.url = groupingShelfControllerCommon.createShelfTokenUrlIfNecessaryForShelf(objectGraph, shelf, shelfToken); + if (groupingParseContext.shelves.length === 0) { + shelf.presentationHints = { isFirstShelf: true }; + } + return shelf; + } +} +//# sourceMappingURL=grouping-small-breakout-shelf-controller.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/grouping/shelf-controllers/grouping-tag-brick-shelf-controller.js b/node_modules/@jet-app/app-store/tmp/src/common/grouping/shelf-controllers/grouping-tag-brick-shelf-controller.js new file mode 100644 index 0000000..66baba8 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/grouping/shelf-controllers/grouping-tag-brick-shelf-controller.js @@ -0,0 +1,246 @@ +import * as models from "../../../api/models"; +import * as serverData from "../../../foundation/json-parsing/server-data"; +import * as mediaAttributes from "../../../foundation/media/attributes"; +import * as mediaDataStructure from "../../../foundation/media/data-structure"; +import * as mediaRelationship from "../../../foundation/media/relationships"; +import * as content from "../../content/content"; +import * as flowPreview from "../../content/flow-preview"; +import * as lockupsEditorialContext from "../../lockups/editorial-context"; +import * as metricsHelpersClicks from "../../metrics/helpers/clicks"; +import * as metricsHelpersImpressions from "../../metrics/helpers/impressions"; +import * as metricsHelpersLocation from "../../metrics/helpers/location"; +import * as placeholders from "../../placeholders/placeholders"; +import { GroupingShelfController } from "./grouping-shelf-controller"; +import * as groupingShelfControllerCommon from "./grouping-shelf-controller-common"; +import { defaultLayoutSize } from "../../../foundation/media/data-fetching"; +import { isSome } from "@jet/environment"; +export class GroupingTagBrickShelfController extends GroupingShelfController { + // region Constructors + constructor() { + super("GroupingTagBrickShelfController"); + this.supportedFeaturedContentIds = new Set([ + 584 /* groupingTypes.FeaturedContentID.AppStore_TagsBrick */, + 587 /* groupingTypes.FeaturedContentID.AppStore_PersonalizedTagsBrick */, + ]); + } + // endregion + // region Shelf Creation Prerequisites + /** + * For a given mediaApiData extract the actual shelfContents array needed to render this shelf + * + * @param mediaApiData The outer shelfContents object containing the shelf contents + */ + initialShelfDataFromGroupingMediaApiData(objectGraph, mediaApiData) { + var _a; + if (isSome(mediaApiData)) { + const shelfContents = (_a = mediaRelationship.relationshipCollection(mediaApiData, "contents")) !== null && _a !== void 0 ? _a : null; + return { shelfContents: shelfContents }; + } + return { shelfContents: null }; + } + /** + * For a given url that this controller handles, we should return a promise that will result in the `ShelfData` + * needed to render this shelf + * + * @param objectGraph The App Store dependency graph + * @param shelfUrl The url that this controller handled on a secondary fetch + * @param parameters The extracted parameters from the shelf url + */ + async secondaryShelfDataForShelfUrl(objectGraph, shelfUrl, shelfToken, parameters) { + return await GroupingShelfController.secondaryGroupingShelfMediaApiData(objectGraph, shelfUrl, shelfToken, parameters).then((shelfData) => { + return { + shelfContents: shelfData.data, + }; + }); + } + /** + * For a given mediaApiData create an updated shelf token that contains all the additional data for this specific shelf type + * + * @param objectGraph The App Store dependency graph + * @param baseShelfToken The base grouping shelf token created by the grouping-controller + * @param mediaApiData The outer data object containing the FC properties and data + * @param groupingParseContext The parse context for the grouping page so far + */ + shelfTokenFromBaseTokenAndMediaApiData(objectGraph, mediaApiData, baseShelfToken, groupingParseContext) { + // If suppress text is not provided, default to hiding. + let suppressText = mediaAttributes.attributeAsBoolean(mediaApiData, "suppressText"); + if (serverData.isNull(suppressText)) { + suppressText = true; + } + const brickShelfToken = { + ...baseShelfToken, + showSupplementaryText: !suppressText, + }; + brickShelfToken.clientIdentifierOverride = lockupsEditorialContext.clientIdentifierForEditorialContextInData(objectGraph, mediaApiData); + return brickShelfToken; + } + // endregion + // region Shelf Creation + /** + * + * @param objectGraph The App Store dependency graph + * @param shelfToken The shelf shelfToken for this current shelf creation request + * @param shelfData The media api shelfContents array for this shelf + * @param groupingParseContext The parse context used to generate the grouping page on the initial page load, + * this will be missing when this controller renders a secondary or incomplete shelf fetch. + */ + _createShelf(objectGraph, shelfToken, shelfData, groupingParseContext) { + const items = []; + const remainingBricks = []; // Data where metadata was missing. + const displayStyle = serverData.asString(shelfToken.featuredContentData.attributes, "displayStyle"); + const isCategoryBrick = displayStyle === "small"; + const shelf = new models.Shelf("tagBrick"); + const layoutSize = serverData.asNumber(shelfToken.featuredContentData.attributes, "layoutStyle.layoutSize"); + shelf.rowsPerColumn = layoutSize !== null && layoutSize !== void 0 ? layoutSize : defaultLayoutSize(objectGraph); + shelf.isHorizontal = true; + if (isSome(shelfData.shelfContents)) { + for (const tagData of shelfData.shelfContents) { + const brickModel = GroupingTagBrickShelfController.createBrick(objectGraph, tagData, isCategoryBrick, shelfToken.metricsPageInformation, shelfToken.metricsLocationTracker, shelfToken, groupingParseContext); + if (!isSome(brickModel === null || brickModel === void 0 ? void 0 : brickModel.shortEditorialDescription)) { + shelfToken.remainingItems.push(tagData); + remainingBricks.push(tagData); + continue; + } + if (isSome(brickModel)) { + items.push(brickModel); + } + metricsHelpersLocation.nextPosition(shelfToken.metricsLocationTracker); + } + } + if (serverData.isDefinedNonNull(shelfToken.presentationHints)) { + shelf.presentationHints = shelfToken.presentationHints; + } + if (serverData.isDefinedNonNull(shelfToken.showSupplementaryText)) { + shelf.presentationHints = { + ...shelf.presentationHints, + showSupplementaryText: shelfToken.showSupplementaryText, + }; + } + // We don't need this in our incomplete shelf URL, so we'll preemptively remove it. + delete shelfToken.maxItemCount; + // Override `featuredContentData` to only have remaining bricks that must be fetched. + if (serverData.isDefinedNonNull(serverData.traverse(shelfToken.featuredContentData, "relationships.contents.data")) && + isSome(shelfToken.featuredContentData.relationships)) { + shelfToken.featuredContentData.relationships["contents"].data = remainingBricks; + } + // Set metadata + shelf.title = shelfToken.title; + shelf.subtitle = shelfToken.subtitle; + if (isCategoryBrick) { + const displayCount = serverData.asNumber(shelfToken.featuredContentData.attributes, "displayCount"); + shelf.items = items.slice(0, displayCount !== null && displayCount !== void 0 ? displayCount : items.length); + } + else { + shelf.items = items; + } + // See all + const hasSeeAll = serverData.asBooleanOrFalse(shelfToken.featuredContentData.attributes, "hasSeeAll"); + if (isCategoryBrick && hasSeeAll) { + // Setup shelf + const seeAllShelf = new models.Shelf("categoryBrick"); + seeAllShelf.items = items; + seeAllShelf.presentationHints = { isSeeAllContext: true }; + // Setup Page + const seeAllPage = new models.GenericPage([seeAllShelf]); + seeAllPage.title = shelfToken.title; + // Setup action + const seeAllAction = new models.FlowAction("page"); + seeAllAction.title = objectGraph.loc.string("ACTION_SEE_ALL"); + seeAllAction.pageData = seeAllPage; + // Connect action + shelf.seeAllAction = seeAllAction; + // Metrics + metricsHelpersClicks.addClickEventToSeeAllAction(objectGraph, seeAllAction, "", { + pageInformation: shelfToken.metricsPageInformation, + locationTracker: shelfToken.metricsLocationTracker, + }); + } + // If no items should we display placeholders for this shelf? + const willHydrateShelfLater = shelf && serverData.isNullOrEmpty(shelf.items) && shelfToken.isFirstRender; + if (willHydrateShelfLater && placeholders.placeholdersEnabled(objectGraph)) { + placeholders.insertPlaceholdersIntoGenericPageShelf(objectGraph, shelf, shelfToken, shelfToken.featuredContentId); + } + shelfToken.presentationHints = shelf.presentationHints; + shelf.url = groupingShelfControllerCommon.createShelfTokenUrlIfNecessaryForShelf(objectGraph, shelf, shelfToken); + return shelf; + } + // region Static Helpers + static createBrick(objectGraph, brickData, searchCategoryBricks, metricsPageInformation, metricsLocationTracker, shelfToken, groupingParseContext) { + var _a, _b, _c; + const metricsBrickData = (_a = mediaDataStructure.metricsFromMediaApiObject(brickData)) !== null && _a !== void 0 ? _a : undefined; + const metricsOptions = { + targetType: searchCategoryBricks ? "tile" : "brick", + pageInformation: metricsPageInformation, + locationTracker: metricsLocationTracker, + recoMetricsData: metricsBrickData, + }; + const metadata = groupingShelfControllerCommon.metadataForTag(objectGraph, brickData, shelfToken !== null && shelfToken !== void 0 ? shelfToken : null, metricsOptions); + if (!metadata) { + const brickModel = new models.Brick(); + brickModel.shortEditorialDescription = mediaAttributes.attributeAsString(brickData, "name"); + return brickModel; + } + const brickModel = new models.Brick(); + // Setup Artwork + const artworkOptions = { + useCase: 18 /* content.ArtworkUseCase.GroupingBrick */, + }; + const collectionIcons = groupingShelfControllerCommon.artworkForTags(objectGraph, brickData, 1060, 520, artworkOptions, metricsOptions); + if (collectionIcons.length > 0) { + const collectionIconBackgroundColor = collectionIcons[0].backgroundColor; + brickModel.collectionIcons = collectionIcons; + if (isSome(collectionIconBackgroundColor) && (collectionIconBackgroundColor === null || collectionIconBackgroundColor === void 0 ? void 0 : collectionIconBackgroundColor.type) === "rgb") { + brickModel.backgroundColor = + (_b = content.closestTagBackgroundColorForIcon(collectionIconBackgroundColor)) !== null && _b !== void 0 ? _b : undefined; + } + } + brickModel.accessibilityLabel = metadata.title; + // Set supplementary text. + brickModel.shortEditorialDescription = metadata.shortEditorialDescription; + // Set action + brickModel.clickAction = metadata.action; + // Set personalization + const brickFeaturedContentId = mediaAttributes.attributeAsNumber(brickData, "editorialElementKind"); + if (brickFeaturedContentId === 435 /* groupingTypes.FeaturedContentID.AppStore_MSOBrickMarker */) { + brickModel.personalizationStyle = "mso"; + } + // Set flow preview actions + const contentData = mediaRelationship.relationshipData(objectGraph, brickData, "contents"); + if (serverData.isDefinedNonNull(contentData)) { + const metricsClickOptions = metricsHelpersClicks.clickOptionsForLockup(objectGraph, contentData, metricsOptions); + const clientIdentifierOverride = (_c = shelfToken === null || shelfToken === void 0 ? void 0 : shelfToken.clientIdentifierOverride) !== null && _c !== void 0 ? _c : null; + brickModel.flowPreviewActionsConfiguration = flowPreview.flowPreviewActionsConfigurationForProductFromData(objectGraph, brickData, true, clientIdentifierOverride, brickModel.clickAction, metricsOptions, metricsClickOptions); + } + // Configure impressions + const impressionOptions = metricsHelpersImpressions.impressionOptions(objectGraph, brickData, metadata.title, metricsOptions); + metricsHelpersImpressions.addImpressionFields(objectGraph, brickModel, impressionOptions); + // Safe area + brickModel.artworkSafeArea = models.ChartOrCategorySafeArea.defaultTileArtworkSafeArea; + brickModel.textSafeArea = models.ChartOrCategorySafeArea.defaultTileTextSafeArea; + if (!brickModel.isValid()) { + return null; + } + return brickModel; + } + // endregion + // region Metrics + /** + * Return the shelf metrics options to use for this specific shelf. Using the base options from the grouping + * page controller + * + * @param objectGraph The App Store dependency graph + * @param shelfToken The shelf shelfToken for this current shelf creation request + * @param baseMetricsOptions The minimum set of metrics options for this shelf, created by the + * grouping page controller + */ + shelfMetricsOptionsFromBaseMetricsOptions(objectGraph, shelfToken, baseMetricsOptions) { + const shelfMetricsOptions = { ...baseMetricsOptions }; + const displayStyle = serverData.asString(shelfToken.featuredContentData.attributes, "displayStyle"); + // If this is a Category Bricks shelf, configure metrics title accordingly. + if (displayStyle === "small") { + shelfMetricsOptions.title = "Browse Categories"; + } + return shelfMetricsOptions; + } +} +//# sourceMappingURL=grouping-tag-brick-shelf-controller.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/grouping/shelf-controllers/grouping-tags-header-shelf-controller.js b/node_modules/@jet-app/app-store/tmp/src/common/grouping/shelf-controllers/grouping-tags-header-shelf-controller.js new file mode 100644 index 0000000..bb6a950 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/grouping/shelf-controllers/grouping-tags-header-shelf-controller.js @@ -0,0 +1,92 @@ +import * as models from "../../../api/models"; +import * as mediaRelationship from "../../../foundation/media/relationships"; +import { areAppTagsEnabled } from "../../util/app-tags-util"; +import { GroupingShelfController } from "./grouping-shelf-controller"; +import { attributeAsDictionary } from "@apple-media-services/media-api"; +import * as serverData from "../../../foundation/json-parsing/server-data"; +import { isSome } from "@jet/environment"; +import * as content from "../../content/content"; +import { artworkDictionaryFromData } from "../../arcade/breakouts-common"; +import * as color from "../../../foundation/util/color-util"; +/** + * This groupings controller adds the logic to display a header shelf on the groupings page. Th primary expereince + * is artwork and the secondary experience includes fallback icons. + */ +export class GroupingMediaPageHeaderShelfController extends GroupingShelfController { + // region Constructors + constructor() { + super("GroupingMediaPageHeaderShelfController"); + this.supportedFeaturedContentIds = new Set([585 /* groupingTypes.FeaturedContentID.AppStore_MediaPageHeader */]); + } + // endregion + // region Shelf Creation Prerequisites + /** + * For a given mediaApiData extract the actual shelfContents array needed to render this shelf + * + * @param mediaApiData The outer shelfContents object containing the shelf contents + */ + initialShelfDataFromGroupingMediaApiData(objectGraph, mediaApiData) { + return { shelfContents: mediaRelationship.relationshipCollection(mediaApiData, "contents") }; + } + /** + * For a given url that this controller handles, we should return a promise that will result in the `ShelfData` + * needed to render this shelf + * + * @param objectGraph The App Store dependency graph + * @param shelfUrl The url that this controller handled on a secondary fetch + * @param parameters The extracted parameters from the shelf url + */ + async secondaryShelfDataForShelfUrl(objectGraph, shelfUrl, shelfToken, parameters) { + return await GroupingShelfController.secondaryGroupingShelfDataForShelfUrl(objectGraph, shelfUrl, shelfToken, parameters); + } + /** + * For a given mediaApiData create an updated shelf token that contains all the additional data for this specific shelf type + * + * @param objectGraph The App Store dependency graph + * @param baseShelfToken The base grouping shelf token created by the grouping-controller + * @param mediaApiData The outer data object containing the FC properties and data + * @param groupingParseContext The parse context for the grouping page so far + */ + shelfTokenFromBaseTokenAndMediaApiData(objectGraph, mediaApiData, baseShelfToken, groupingParseContext) { + return baseShelfToken; + } + // endregion + // region Shelf Creation + /** + * + * @param objectGraph The App Store dependency graph + * @param shelfToken The shelf shelfToken for this current shelf creation request + * @param shelfData The media api shelfContents array for this shelf + * @param groupingParseContext The parse context used to generate the grouping page on the initial page load, + * this will be missing when this controller renders a secondary or incomplete shelf fetch. + */ + _createShelf(objectGraph, shelfToken, shelfData, groupingParseContext) { + var _a; + if (!areAppTagsEnabled(objectGraph, "grouping")) { + return null; + } + const headerData = shelfData.shelfContents[0]; + let title; + if (isSome(headerData)) { + const editorialNotes = attributeAsDictionary(headerData, "editorialNotes"); + title = (_a = serverData.asString(editorialNotes, "name")) !== null && _a !== void 0 ? _a : null; + const artworkDictionary = artworkDictionaryFromData(objectGraph, headerData); + const artworkData = serverData.asDictionary(artworkDictionary, "categoryDetailStatic16x9"); + const artworkFromData = content.artworkFromApiArtwork(objectGraph, artworkData, { + withJoeColorPlaceholder: true, + useCase: 0 /* content.ArtworkUseCase.Default */, + cropCode: "CDS.ApTCHM01", + }); + if (isSome(title) && isSome(artworkFromData)) { + const isArtworkDark = color.isDarkColor(artworkFromData.backgroundColor); + const pageHeader = new models.MediaPageHeader(null, title, null, artworkFromData, null, null, false, null, null, isArtworkDark ? "dark" : "light"); + const shelf = new models.Shelf("mediaPageHeader"); + shelf.items = [pageHeader]; + return shelf; + } + return null; + } + return null; + } +} +//# sourceMappingURL=grouping-tags-header-shelf-controller.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/linking/external-deep-link.js b/node_modules/@jet-app/app-store/tmp/src/common/linking/external-deep-link.js new file mode 100644 index 0000000..00031e4 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/linking/external-deep-link.js @@ -0,0 +1,73 @@ +import * as models from "../../api/models"; +import * as serverData from "../../foundation/json-parsing/server-data"; +import * as mediaAttributes from "../../foundation/media/attributes"; +import * as objects from "../../foundation/util/objects"; +import * as metricsHelpersClicks from "../metrics/helpers/clicks"; +/** + * "External Deep Link" feature description + * + * This feature is one where a particular offer will behave like a normal offer when the app is not openable, but in + * which the openable state for the offer actually deep links to the third party app in question. This deep link uses + * a universal link that is plumbed through by Media API; the deep link is submitted and approved through + * App Store Connect and will be interpreted by the third party app in accordance with the external action. + * + * This feature's name is prefixed by the term "external" to differentiate from deep linking into the App Store app + * itself (such as deep linking into the search tab via Siri or the user's purchases through a finance page). + */ +/// The query parameter name to use when attaching the external deep link to a product URL. +export const externalDeepLinkQueryParameter = "externalDeepLinkUrl"; +/// Query parameter name to use when attaching eligibility information for cpp deep links. +export const cppDeepLinkDisabledQueryParameter = "isCppDeepLinkDisabled"; +/// Query parameter name to use when attaching the aligned region deep link to a product URL. +export const alignedRegionDeepLinkQueryParameter = "alignedRegionDeepLinkUrl"; +/** + * Pulls out the external deep link url from the given data. + * @param {Data} data The data for the app that has the deep link. + * @return {string | null} The external deep link url, if it exists. + */ +export function deepLinkUrlFromData(objectGraph, data) { + if (!serverData.isDefinedNonNull(data)) { + return null; + } + return mediaAttributes.attributeAsString(data, "customUrl"); +} +/** + * Wraps an action in a state action that executes an external deep link when the app is openable. + * @param {Action} action The action for the product that is being deep linked into. + * @param {string | null} adamId The adam ID of the app. + * @param {string | null} bundleId The bundle ID of the app. + * @param {string | null} deepLinkUrl The url for the deep link action. + * @param {boolean} includeBetaApps Whether to include beta apps in the resulting action. + * @param metricsClickOptions The metrics click options for the action. + * @return {Action} The action to use for the app that is deep linked. + */ +export function deepLinkActionWrappingAction(objectGraph, action, adamId, bundleId, deepLinkUrl, includeBetaApps, metricsClickOptions) { + // Only wrap in OfferStateAction on devices that are supported when there's a deep link present. + if (!serverData.isDefinedNonNullNonEmpty(deepLinkUrl) && objectGraph.client.deviceType !== "mac") { + return action; + } + let openAction; + if (objectGraph.client.isiOS) { + const openAppAction = new models.OpenAppAction(adamId, "app"); + openAction = new models.AppLaunchTrampolineAction(bundleId, deepLinkUrl, openAppAction); + } + else { + openAction = new models.ExternalUrlAction(deepLinkUrl); + } + const clickOptions = objects.shallowCopyOf(metricsClickOptions); + clickOptions.actionType = "open"; + clickOptions.actionDetails = { actionUrl: deepLinkUrl }; + metricsHelpersClicks.addClickEventToAction(objectGraph, openAction, clickOptions, true, "button"); + if (action instanceof models.OfferStateAction) { + action.openAction = openAction; + action.includeBetaApps = includeBetaApps; + return action; + } + else { + const stateAction = new models.OfferStateAction(adamId, action); + stateAction.openAction = openAction; + stateAction.includeBetaApps = includeBetaApps; + return stateAction; + } +} +//# sourceMappingURL=external-deep-link.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/linking/os-update-links.js b/node_modules/@jet-app/app-store/tmp/src/common/linking/os-update-links.js new file mode 100644 index 0000000..cce8e9d --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/linking/os-update-links.js @@ -0,0 +1,42 @@ +/** + * Created by ls on 12/15/18. + * + * A home for building application URL links for jumping to other applications. + */ +// region System Preference: Updates +/** + * Build an URL that links to software update pref pane on given platform. + * Optionally, supply an major OS version to signal Preferences to initiate download for given version if that is supported by the platform. + * @param {DeviceType} deviceType Device type to request the update url for. + * @param {string | null} majorOSBundle Optional major OS bundle identifier, e.g. com.apple.InstallAssistant.Catalina + * @returns {string | null} URL that links into the "Software Updates" settings for given platform. `null` for platforms that we don't currently link into. + * @note Currently, only MAS is supported by this function as it is the only platform linking to Preference > OS Update. + */ +export function osUpdateUrl(deviceType, majorOSBundle = null) { + switch (deviceType) { + case "mac": + return macOSUpdateUrl(majorOSBundle); + default: + return null; + } +} +/** + * Build an URL that links to software update pref pane on macOS. + * Optionally, supply an major OS version to signal Preferences to initiate download for given version. + * @param {string | null} majorOSBundle Optional major OS bundle identifier, e.g. com.apple.InstallAssistant.Catalina + * @returns {string | null} URL that links into the "Software Updates" settings for macOS. + */ +function macOSUpdateUrl(majorOSBundle) { + /** + * Workaround for <rdar://problem/47124330> Parsing and building URLs should follow unified spec + * Manually build url. + */ + // Preference URLs have no authority component. + let updateUrl = `x-apple.systempreferences:com.apple.preferences.softwareupdate?client=AppStore&variant=CUSTOMER`; + if (majorOSBundle) { + updateUrl += `&installMajorOSBundle=${majorOSBundle}`; + } + return updateUrl; +} +// endregion +//# sourceMappingURL=os-update-links.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/locale.js b/node_modules/@jet-app/app-store/tmp/src/common/locale.js new file mode 100644 index 0000000..b829f75 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/locale.js @@ -0,0 +1,36 @@ +/** + * Retrieve the active {@linkcode NormalizedLocale} for the client + * + * This can be useful in cases where you need to create a `RoutableIntent` + * but do not have access to an existing `RoutableIntent` to read the locale + * information off of. + * + * Note: this should be used only as a last resort. Wherever possible, prefer to + * retrieve the {@linkcode NormalizedLocale} data off of an existing `RoutableIntent` + * instead of depending on the global state in the {@linkcode AppStoreObjectGraph}. + */ +export function getLocale(objectGraph) { + return { + storefront: objectGraph.locale.activeStorefront, + language: objectGraph.locale.activeLanguage, + }; +} +/** + * Determine how the `NormalizedStorefront` should be encoded into a URL + * + * This provides a means by which the app can determine how the storefront and language + * should be represented when creating a URL that includes locale information. + * + * This can be used, for example, to opt out of expressing an explicit language when it would + * be the default for the given storefront. + */ +export function deriveLocaleForUrl(objectGraph, storefront) { + return objectGraph.locale.deriveLocaleForUrl(storefront); +} +/** + * Turns an {@linkcode UnnormalizedLocale} into a {@linkcode NormalizedLocale} + */ +export function normalizeLocale(objectGraph, unnormalizedLocale) { + return objectGraph.locale.normalize(unnormalizedLocale); +} +//# sourceMappingURL=locale.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/lockups/ad-lockups.js b/node_modules/@jet-app/app-store/tmp/src/common/lockups/ad-lockups.js new file mode 100644 index 0000000..f668735 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/lockups/ad-lockups.js @@ -0,0 +1,439 @@ +/** + * Builder code specific to Ad Lockups + */ +import { isSome } from "@jet/environment"; +import { AdInteractionAction, AdTransparencyAction, CompoundAction, Screenshots, SearchAd, SearchAdOpportunity, } from "../../api/models"; +import { AdvertActionMetrics, } from "../../api/models/metrics/advert-action-metrics"; +import { isDefinedNonNull, isDefinedNonNullNonEmpty, isNull, asBooleanOrFalse, asArray, } from "../../foundation/json-parsing/server-data"; +import { attributeAsArrayOrEmpty, attributeAsBooleanOrFalse, attributeAsDictionary, attributeAsString, } from "../../foundation/media/attributes"; +import { advertInstanceIdForData } from "../ads/ad-common"; +import * as content from "../content/content"; +import { addClickEventsToAdLockup } from "../metrics/helpers/clicks"; +import { addImpressionsFieldsToAd, impressionOptionsForLockup } from "../metrics/helpers/impressions"; +import { metricsPlatformDisplayStyleFromData } from "../metrics/helpers/util"; +import { appDataHasVariant } from "../product-page/product-page-variants"; +import { adLogger } from "../search/search-ads"; +import { asString } from "@apple-media-services/media-api"; +// region Shared +/** + * Whether or not data is for ad. + * + * Note that this attribute *may not be initially part of the original response*. + * - Sponsored Search: Since Ad Rotation, `iad` attribute is populated from contents of `iads` dictionary. + * - Search Landing: `iad` attribute is populated from contents of `OnDeviceAdvert`. + * + * @param data Data to check if it is an ad. + * @see`iadAttributesForType` and `decorateiAdAttributeFromOnDeviceAd` + */ +export function isAdvert(objectGraph, data) { + const adDictionary = attributeAsDictionary(data, "iad"); + return isDefinedNonNullNonEmpty(adDictionary); +} +// endregion +// region Data Overrides +/** + * Populates Ad-specific customizations for given lockup to a **otherwise fully configured ad lockup** + * @param objectGraph The object graph. + * @param data Data that will provide the ad data. + * @param lockup The lockup to modify. + * @param metricsOptions The lockup metrics options. + * @param applyAdOfferDisplayProperties Whether to apply the default ad OfferDisplayProperties. Some callers of this function want to enforce their own offer styling. + */ +export function performAdOverridesforLockup(objectGraph, data, lockup, metricsOptions, applyAdOfferDisplayProperties = true) { + // If the lockup is not an ad, it's ad eligible. Just apply the metrics modifications and return early. + if (!metricsOptions.isAdvert) { + performMetricsOverridesForLockup(objectGraph, data, lockup, metricsOptions); + return; + } + let searchAd; + if (objectGraph.props.enabled("advertSlotReporting")) { + lockup.searchAdOpportunity = searchAdOpportunityFromData(objectGraph, data, metricsOptions.pageInformation); + searchAd = lockup.searchAdOpportunity.searchAd; + } + else { + lockup.searchAd = searchAdFromData(objectGraph, data, metricsOptions.pageInformation); + searchAd = lockup.searchAd; + } + /** + * Advert Action Metrics (AzulC+) + */ + const advertType = content.isArcadeSupported(objectGraph, data) + ? "arcadeApp" + : "standardApp"; + const reportingDestination = reportingDestinationFromMetricsOptions(objectGraph, metricsOptions.pageInformation); + const bundleId = attributeAsString(data, "platformAttributes.ios.bundleId"); + const isPreorder = attributeAsBooleanOrFalse(data, "isPreorder"); + const purchaseType = isPreorder ? "preorder" : "standard"; + const clickActionAdActionMetrics = new AdvertActionMetrics(searchAd.instanceId, data.id, bundleId, advertType, "advertPressed", purchaseType, reportingDestination); + lockup.clickAction = attachAdActionMetricsToAction(objectGraph, lockup.clickAction, clickActionAdActionMetrics); + const buttonActionAdActionMetrics = new AdvertActionMetrics(searchAd.instanceId, data.id, bundleId, advertType, "offerButtonPress", purchaseType, reportingDestination); + lockup.buttonAction = attachAdActionMetricsToAction(objectGraph, lockup.buttonAction, buttonActionAdActionMetrics); + lockup.itemBackground = objectGraph.props.enabled("insetAdItemBackground") ? "insetAd" : "ad"; + // Offer button style + if (lockup.offerDisplayProperties && applyAdOfferDisplayProperties) { + lockup.offerDisplayProperties = lockup.offerDisplayProperties.newOfferDisplayPropertiesChangingAppearance(false, "colored", "ad"); + } + // Rating + if (!attributeAsBooleanOrFalse(data, "iad.format.userRating")) { + lockup.rating = null; + lockup.ratingCount = null; + } + performMetricsOverridesForLockup(objectGraph, data, lockup, metricsOptions); +} +/** + * Whether or not an advert is eligible to use CPP deeplinks, regardless of whether or not one exists + * @param data Data that will provide the ad data. + */ +export function isCppDeeplinkEnabledForAdvert(data) { + const iAd = attributeAsDictionary(data, "iad"); + const searchAdCppDeepLinkEnabled = asBooleanOrFalse(iAd, "passthroughAdInfo.deepLinkEligible"); + const targetedAdCppDeeplinkingEnabled = asBooleanOrFalse(data, "meta.passthroughAdInfo.deepLinkEligible"); + return searchAdCppDeepLinkEnabled || targetedAdCppDeeplinkingEnabled; +} +/** + * Whether or not a custom creative advert is eligible to use CPP deeplinks, regardless of whether or not one exists + * @param data Data that will provide the ad data. + */ +export function isCustomCreativeDeeplinkEnabledForAdvert(data) { + const extraAdInfo = getExtraAdInfo(data); + if (isSome(extraAdInfo)) { + return asBooleanOrFalse(extraAdInfo, "passthroughAdInfo.deepLinkEligible"); + } + else { + return asBooleanOrFalse(data, "meta.passthroughAdInfo.deepLinkEligible"); + } +} +/** + * The custom creative ad deeplink url, if there is one. + * @param data Data that will provide the ad data. + */ +export function getCustomCreativeDeepLinkUrl(data) { + const extraAdInfo = getExtraAdInfo(data); + const creativeDetails = asArray(extraAdInfo, "creativeDetails"); + if (isSome(creativeDetails)) { + return asString(creativeDetails[0], "deepLink"); + } + else { + return asString(data, "meta.alignedRegionDetails.deepLink"); + } +} +/** + * The tapDestination of the custom creative advert which is also the cppId for the PP. + * @param data Data that will provide the ad data. + */ +export function getTapDestinationIdForAdvert(data) { + const extraAdInfo = getExtraAdInfo(data); + const creativeDetails = asArray(extraAdInfo, "creativeDetails"); + if (isSome(creativeDetails)) { + return asString(creativeDetails[0], "tapDestination"); + } + else { + return asString(data, "meta.alignedRegionDetails.tapDestination"); + } +} +/** + * Local function to extract the extraAdInfo from the meta in the data. + * @param data Data that will provide the ad data. + */ +function getExtraAdInfo(data) { + const extraAdInfoString = asString(data, "meta.extraAdInfo"); + if (isSome(extraAdInfoString)) { + return JSON.parse(extraAdInfoString); + } + return null; +} +/** + * Perform overrides for metrics for Ad Lockups + * @param data Data for ad lockup + * @param lockup Lockup to apply overrides to + * @param baseMetricsOptions Base metrics options to extend. + */ +function performMetricsOverridesForLockup(objectGraph, data, lockup, baseMetricsOptions) { + const platformDisplayStyle = metricsPlatformDisplayStyleFromData(objectGraph, data, lockup.icon, null); + const impressionsMetricOptions = impressionOptionsForLockup(objectGraph, data, lockup, platformDisplayStyle, baseMetricsOptions, true); + addClickEventsToAdLockup(objectGraph, lockup, impressionsMetricOptions); + addImpressionsFieldsToAd(objectGraph, lockup, impressionsMetricOptions, impressionsMetricOptions.pageInformation.iAdInfo); +} +/** + * Creates a template type string for the medium ad format + * @param lockup The lockup whose artwork to use for the template type string + * @returns The native ad template type for the medium format ad + */ +export function getTemplateTypeForMediumAdFromLockupWithScreenshots(screenshots) { + var _a; + const templateString = [ + "MEDRIVER_", + "U", + "I", + ((_a = screenshots === null || screenshots === void 0 ? void 0 : screenshots.artwork) !== null && _a !== void 0 ? _a : []).length, // Count of assets used. + ].join(""); + return templateString; +} +/** + * Creates a template type string for the medium ad format + * @param lockup The lockup whose artwork to use for the template type string + * @returns The native ad template type for the medium format ad + */ +export function getTemplateTypeForMediumAdFromLockupWithCustomCreative() { + const templateString = [ + "MEDRIVER_", + "U", + "I", + 1, + "_2x1", // Aspect Ratio used. + ].join(""); + return templateString; +} +/** + * Create an opportunity for an ad model + * @param objectGraph The object graph. + * @param data Data to build `SearchAd` for. + * @param pageInformation Metrics page information. + * @returns The search ad opportunity represented in the data + */ +export function searchAdOpportunityFromData(objectGraph, data, pageInformation) { + const searchAd = searchAdFromData(objectGraph, data, pageInformation); + const lifecycleEventPayloads = searchAdLifecycleEventPayloads(searchAd.instanceId, pageInformation); + return new SearchAdOpportunity(searchAd.instanceId, lifecycleEventPayloads, searchAd); +} +/** + * Create a missed opportunity for an ad model. This differentiates from `searchAdOpportunityFromData` because the + * opportunity is represented by an organic content that is in an ad eligible position. + * @param objectGraph The object graph. + * @param pageInformation Metrics page information. + * @returns The search ad opportunity for the missed opportunity + */ +export function searchAdMissedOpportunityFromId(objectGraph, pageInformation) { + let adInstanceId; + const placementType = pageInformation === null || pageInformation === void 0 ? void 0 : pageInformation.iAdInfo.placementType; + if (isDefinedNonNull(placementType) && objectGraph.props.enabled("advertSlotReporting")) { + try { + adInstanceId = objectGraph.ads.getIdentifierForMissedOpportunity(placementType); + } + catch { + adInstanceId = objectGraph.random.nextUUID(); + adLogger(objectGraph, `Error: getIdentifierForMissedOpportunity threw exception. Assigned ${adInstanceId}`); + } + } + else { + adInstanceId = objectGraph.random.nextUUID(); + adLogger(objectGraph, `Error: placementType was null or empty. Assigned ${adInstanceId}`); + } + const lifecycleEventPayloads = searchAdLifecycleEventPayloads(adInstanceId, pageInformation); + return new SearchAdOpportunity(adInstanceId, lifecycleEventPayloads); +} +/** + * Create the backing search ad model for any representation of ad. + * @param objectGraph The object graph. + * @param data Data to build `SearchAd` for. + * @param pageInformation Information of the page hosting the lockup + * @returns The search ad model + */ +function searchAdFromData(objectGraph, data, pageInformation) { + let instanceId = advertInstanceIdForData(objectGraph, data); + if (isNull(instanceId) || instanceId.length === 0) { + instanceId = objectGraph.random.nextUUID(); + adLogger(objectGraph, `Error: instanceId was null or empty. Assigned ${instanceId}`); + } + const iAd = attributeAsDictionary(data, "iad"); + const impressionId = attributeAsString(data, "iad.impressionId"); + const transparencyData = attributeAsString(data, "iad.privacy"); + const bundleId = attributeAsString(data, "platformAttributes.ios.bundleId"); + const transparencyAction = new AdTransparencyAction(transparencyData); + transparencyAction.title = objectGraph.adsLoc.string("IAD_PRIVACY_MARKER_BUTTON_TITLE"); + const advertType = content.isArcadeSupported(objectGraph, data) + ? "arcadeApp" + : "standardApp"; + const isPreorder = attributeAsBooleanOrFalse(data, "isPreorder"); + const purchaseType = isPreorder ? "preorder" : "standard"; + const reportingDestination = reportingDestinationFromMetricsOptions(objectGraph, pageInformation); + const adActionMetrics = new AdvertActionMetrics(instanceId, data.id, bundleId, advertType, "markerPress", purchaseType, reportingDestination); + const lifecycleEventPayloads = searchAdLifecycleEventPayloads(instanceId, pageInformation); + const action = attachAdActionMetricsToAction(objectGraph, transparencyAction, adActionMetrics); + return new SearchAd(instanceId, iAd, lifecycleEventPayloads, impressionId, action); +} +/** + * Create the backing metadata for any representation of ad opportunity. + * @param instanceId The identifier for the ad opportunity we're tracking + * @param pageInformation Information of the page hosting the lockup + * @returns Payloads to be sent to PromotedContent + */ +export function searchAdLifecycleEventPayloads(instanceId, pageInformation) { + const pageIdentifierRaw = pageInformation === null || pageInformation === void 0 ? void 0 : pageInformation.baseFields.pageId; + const pageIdentifier = typeof pageIdentifierRaw === "string" ? pageIdentifierRaw : "unknown"; + return { + placed: { + adInstanceId: instanceId, + pageIdentifier: pageIdentifier, + }, + pageEnter: { + pageIdentifier: pageIdentifier, + }, + pageExit: { + pageIdentifier: pageIdentifier, + }, + onScreen: { + adInstanceId: instanceId, + }, + offScreen: { + adInstanceId: instanceId, + }, + visible: { + adInstanceId: instanceId, + }, + completed: { + adInstanceId: instanceId, + }, + }; +} +/** + * Populates Ad-specific customizations for given card to an otherwise fully configured Today card + * @param objectGraph The object graph. + * @param data Data that will provide the ad data. + * @param card The Today card to modify. + * @param metricsOptions The card metrics options. + */ +export function performAdOverridesForCard(objectGraph, data, card, metricsOptions) { + let instanceId = advertInstanceIdForData(objectGraph, data); + if (isNull(instanceId) || instanceId.length === 0) { + instanceId = objectGraph.random.nextUUID(); + adLogger(objectGraph, `Error: instanceId was null or empty. Assigned ${instanceId}`); + } + const advertType = content.isArcadeSupported(objectGraph, data) + ? "arcadeApp" + : "standardApp"; + const reportingDestination = reportingDestinationFromMetricsOptions(objectGraph, metricsOptions.pageInformation); + const bundleId = attributeAsString(data, "platformAttributes.ios.bundleId"); + const isPreorder = attributeAsBooleanOrFalse(data, "isPreorder"); + const purchaseType = isPreorder ? "preorder" : "standard"; + const adActionMetrics = new AdvertActionMetrics(instanceId, data.id, bundleId, advertType, "advertPressed", purchaseType, reportingDestination); + card.clickAction = attachAdActionMetricsToAction(objectGraph, card.clickAction, adActionMetrics); +} +/** + * Attaches `AdActionMetrics` to an existing action enabling native ad interaction instrumentation. + * This works by wrapping the existing action, along with a new `AdInteractionAction`, in a `CompoundAction`, which are executed at the same time. + * + * @param objectGraph The Object Graph. + * @param existingAction The existing Action to wrap. + * @param adActionMetrics The adActionMetrics object for the `AdInteractionAction`. + * @returns The new Action containing the existing and new Actions. + */ +export function attachAdActionMetricsToAction(objectGraph, existingAction, adActionMetrics) { + const adInteractionAction = new AdInteractionAction(adActionMetrics); + const action = new CompoundAction([adInteractionAction, existingAction]); + // Re-use the title from the existing action. + action.title = existingAction.title; + return action; +} +// endregion +// region Asset Overrides +/** + * Performs iAd Asset overrides, where a field dictates which assets to use + * @param data The data to source overrides from + * @param lockup The lockup to modify + * @param metricsOptions The metrics options that this operation has side effects on. + */ +export function performAssetOverridesForMixedMediaAdLockupIfNeeded(objectGraph, data, lockup, metricsOptions) { + if (!shouldPerformAssetOverridesForData(objectGraph, data)) { + return; + } + const iAdAssetOverrides = attributeAsArrayOrEmpty(data, "iad.assetOverride"); + if (iAdAssetOverrides.length) { + const didApplyAssetOverrides = applyAssetOverridesToMixedMediaAdLockup(objectGraph, lockup, iAdAssetOverrides); + if (metricsOptions.pageInformation.iAdInfo && metricsOptions.pageInformation.iAdInfo.iAdIsPresent) { + if (preprocessor.CARRY_BUILD || preprocessor.DEBUG_BUILD) { + metricsOptions.pageInformation.iAdInfo.setSpecifiedAlignedRegionUsed(didApplyAssetOverrides); + } + } + } +} +/** + * Whether or not an asset override should occur. + * This is used to control specific scenarios when asset overrides should not occur, e.g. when it clashes with other features. + * + * @param data The apps resource to determine if asset overrides should be done for. + */ +function shouldPerformAssetOverridesForData(objectGraph, data) { + /** + * If an AB Test is being run on given app, don't accept the override. + */ + const appHasABTest = appDataHasVariant(objectGraph, data, "abExperiment"); + return !appHasABTest; +} +/** + * Apply asset overrides to the ad lock up, based on a given list of overrides + * @param lockup Lockup to modify + * @param iAdAssetOverrides Asset overrides specified + * @return {boolean} - Return whether or not the overrides were applied + */ +function applyAssetOverridesToMixedMediaAdLockup(objectGraph, lockup, iAdAssetOverrides) { + const unusedAssetOverrides = new Set(iAdAssetOverrides); + const videoOverrides = []; + const screenshotOverrides = []; + const isOverrideMedia = function (url, checksum) { + if (unusedAssetOverrides.size === 0) { + return false; + } + for (const assetOverride of iAdAssetOverrides) { + if (assetOverride === checksum || url.indexOf(assetOverride) !== -1) { + unusedAssetOverrides.delete(assetOverride); + return true; + } + } + return false; + }; + if (iAdAssetOverrides.length && (lockup.screenshots.length || lockup.trailers.length)) { + if (lockup.trailers.length) { + for (const video of lockup.trailers[0].videos) { + if (isOverrideMedia(video.videoUrl, video.preview.checksum)) { + videoOverrides.push(video); + } + } + } + if (lockup.screenshots.length) { + for (const screenshot of lockup.screenshots[0].artwork) { + if (isOverrideMedia(screenshot.template, screenshot.checksum)) { + screenshotOverrides.push(screenshot); + } + } + } + } + if (unusedAssetOverrides.size === 0 && (videoOverrides.length || screenshotOverrides.length)) { + if (lockup.trailers.length) { + lockup.trailers[0].videos = videoOverrides; + } + if (lockup.screenshots.length) { + lockup.screenshots[0] = new Screenshots(screenshotOverrides, lockup.screenshots[0].mediaPlatform); + } + return true; + } + else { + return false; + } +} +// endregion +// region Reporting Destionation +/** + * Determine the reporting destination based on metrics options + */ +export function reportingDestinationFromMetricsOptions(objectGraph, pageInformation) { + const iadInfo = pageInformation === null || pageInformation === void 0 ? void 0 : pageInformation.iAdInfo; + if (isNull(iadInfo)) { + return "undefined"; + } + const placementType = iadInfo.placementType; + switch (placementType) { + case "searchLanding": + return "promotedContent"; // SLP uses PC-backed Journey reporting + case "searchResults": + return "searchAds"; // SRP uses SA-backed Journey reporting + case "today": + case "productPageYMAL": + case "productPageYMALDuringDownload": + return "promotedContent"; // Chainlink uses PC-backed Journey reporting + default: + throw new Error(`This method should never be called with value: ${placementType}`); + } +} +// endregion +//# sourceMappingURL=ad-lockups.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/lockups/editorial-context.js b/node_modules/@jet-app/app-store/tmp/src/common/lockups/editorial-context.js new file mode 100644 index 0000000..7273a4f --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/lockups/editorial-context.js @@ -0,0 +1,25 @@ +import * as mediaAttributes from "../../foundation/media/attributes"; +/** + * Use to obtain an clientIdentifier for an editorial context. + * @param data The data from a media response + * @return {ClientIdentifier} A client identifier that corresponds to the given editorial context. + */ +export function clientIdentifierForEditorialContextInData(objectGraph, data) { + const rawEditorialContext = mediaAttributes.attributeAsString(data, "editorialContext"); + switch (rawEditorialContext) { + case "Watch": + return "com.apple.AppStore.BridgeStoreExtension" /* ClientIdentifier.AppStore_BridgeStoreExtension */; + case "Messages": + return "com.apple.MobileSMS" /* ClientIdentifier.MobileSMS */; + case "RealityDevice": + if (objectGraph.bag.enableDeviceDrivenDiscoveryContent) { + return "VisionAppStore" /* ClientIdentifier.VisionAppStore */; + } + else { + return null; + } + default: + return null; + } +} +//# sourceMappingURL=editorial-context.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/lockups/lockups.js b/node_modules/@jet-app/app-store/tmp/src/common/lockups/lockups.js new file mode 100644 index 0000000..1de013a --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/lockups/lockups.js @@ -0,0 +1,1920 @@ +// +// lockups.ts +// AppStoreKit +// +// Created by Jonathan Ellenbogen on 11/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 { makeBundlePageIntent } from "../../api/intents/bundle-page-intent"; +import { makeEditorialPageIntentByID } from "../../api/intents/editorial/editorial-page-intent"; +import { makeGroupingPageIntentByID } from "../../api/intents/grouping-page-intent"; +import { makeProductPageIntent } from "../../api/intents/product-page-intent"; +import { makeRoutableArticlePageIntent, } from "../../api/intents/routable-article-page-intent"; +import * as models from "../../api/models"; +import { AdvertActionMetrics, } from "../../api/models/metrics/advert-action-metrics"; +import * as productPageUtil from "../../common/product-page/product-page-util"; +import * as serverData from "../../foundation/json-parsing/server-data"; +import { editorialCardFromData } from "../../foundation/media/associations"; +import * as mediaAttributes from "../../foundation/media/attributes"; +import * as mediaPlatformAttributes from "../../foundation/media/platform-attributes"; +import { platformAttributeAsDictionary } from "../../foundation/media/platform-attributes"; +import * as mediaRelationship from "../../foundation/media/relationships"; +import * as mediaUrlBuilder from "../../foundation/media/url-builder"; +import { BuyParameters } from "../../foundation/metrics/buy-parameters"; +import * as http from "../../foundation/network/http"; +import { InAppPurchaseInstallPageParameters, Parameters, Path, ProductPageParameters, Protocol, } from "../../foundation/network/url-constants"; +import * as urls from "../../foundation/network/urls"; +import * as color from "../../foundation/util/color-util"; +import * as objects from "../../foundation/util/objects"; +import * as gameModels from "../../gameservicesui/src/api/data-models/game"; +import { makeClickMetrics } from "../../gameservicesui/src/utility/metrics"; +import * as adCommon from "../ads/ad-common"; +import * as appEvent from "../app-promotions/app-event"; +import * as appPromotionsCommon from "../app-promotions/app-promotions-common"; +import * as arcadeCommon from "../arcade/arcade-common"; +import * as arcadeUpsell from "../arcade/arcade-upsell"; +import * as mediaUrlMapping from "../builders/url-mapping"; +import * as ageRatings from "../content/age-ratings"; +import { createArtworkForResource } from "../content/artwork/artwork"; +import * as contentAttributes from "../content/attributes"; +import * as content from "../content/content"; +import * as flowPreview from "../content/flow-preview"; +import { isMediaDark } from "../editorial-pages/editorial-media-util"; +import * as editorialComponentMediaUtil from "../editorial-pages/editorial-page-component-media-util"; +import { makeEditorialPageURL } from "../editorial-pages/editorial-page-intent-controller-utils"; +import * as filtering from "../filtering"; +import { makeGroupingPageCanonicalURL } from "../grouping/grouping-page-url"; +import * as externalDeepLink from "../linking/external-deep-link"; +import { getLocale } from "../locale"; +import * as metricsHelpersClicks from "../metrics/helpers/clicks"; +import * as metricsHelpersImpressions from "../metrics/helpers/impressions"; +import * as metricsHelpersLocation from "../metrics/helpers/location"; +import * as metricsHelpersModels from "../metrics/helpers/models"; +import * as metricsUtil from "../metrics/helpers/util"; +import * as offerFormatting from "../offers/offer-formatting"; +import * as offers from "../offers/offers"; +import { getPlatform, inferPreviewPlatformFromDeviceFamilies } from "../preview-platform"; +import * as variants from "../product-page/product-page-variants"; +import * as reviews from "../product-page/reviews"; +import { customCreativeArtworkFromData, customCreativeVideoFromData } from "../search/custom-creative"; +import * as metadataRibbon from "../search/metadata-ribbon/metadata-ribbon"; +import * as searchTagsRibbon from "../search/metadata-ribbon/search-tags-ribbon"; +import * as sharing from "../sharing"; +import { makeRoutableArticlePageCanonicalUrl } from "../today/routable-article-page-url-utils"; +import * as adLockup from "./ad-lockups"; +import { reportingDestinationFromMetricsOptions } from "./ad-lockups"; +export const forTesting = { + copyDataIntoLockup, +}; +export function offerTypeForMediaType(objectGraph, type, supportsArcade) { + switch (type) { + case "in-apps": + return supportsArcade ? "arcade" : "inAppPurchase"; + default: + return supportsArcade ? "arcadeApp" : "app"; + } +} +function isArcadeLockupWordmarkSupported(objectGraph) { + if (objectGraph.client.isTV && objectGraph.host.clientIdentifier === "com.apple.Arcade") { + return false; + } + else { + return true; + } +} +/** + * Takes the metadata ribbon items type candidates from the `SearchExperimentData` and determines which should be shown in each slot, + * prioritizing the natural order from the candidates arrays + * @param objectGraph The ObjectGraph instance for the App Store + * @param data The data representing the mixed media lockup data + * @param lockup The lockup object the metadata object will be added to + * @param searchExperimentData The experiment data for the search experiments, to use its displayStyle object + */ +function copyMetadataRibbonInfoIntoLockup(objectGraph, data, lockup, searchExperimentDataForPage, options) { + var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l; + if (serverData.isNullOrEmpty(data)) { + return; + } + lockup.showMetadataInformationInLockup = objectGraph.bag.isLLMSearchTagsEnabled || objectGraph.client.isPad; + /// This needs to be guarded against iPhone as natively this will affect all iOS UI components, including iPad. + if (!objectGraph.client.isPhone) { + return; + } + /// If the individual lockup has a metadata ribbon field, that takes precedence over the page level metadata ribbon + const searchExperimentDataForLockup = serverData.asDictionary(data, "meta"); + const metadataRibbonItemSlots = (_b = (_a = searchExperimentDataForLockup === null || searchExperimentDataForLockup === void 0 ? void 0 : searchExperimentDataForLockup.displayStyle) === null || _a === void 0 ? void 0 : _a.metadataRibbon) !== null && _b !== void 0 ? _b : (_c = searchExperimentDataForPage === null || searchExperimentDataForPage === void 0 ? void 0 : searchExperimentDataForPage.displayStyle) === null || _c === void 0 ? void 0 : _c.metadataRibbon; + let llmMetadataRibbonItems = []; + if (objectGraph.bag.isLLMSearchTagsEnabled) { + llmMetadataRibbonItems = + (_g = (_e = (_d = searchExperimentDataForLockup === null || searchExperimentDataForLockup === void 0 ? void 0 : searchExperimentDataForLockup.displayStyle) === null || _d === void 0 ? void 0 : _d.llmRibbon) !== null && _e !== void 0 ? _e : (_f = searchExperimentDataForLockup === null || searchExperimentDataForLockup === void 0 ? void 0 : searchExperimentDataForLockup.displayStyle) === null || _f === void 0 ? void 0 : _f.metadataRibbon) !== null && _g !== void 0 ? _g : (_h = searchExperimentDataForPage === null || searchExperimentDataForPage === void 0 ? void 0 : searchExperimentDataForPage.displayStyle) === null || _h === void 0 ? void 0 : _h.metadataRibbon; + } + else { + llmMetadataRibbonItems = + (_k = (_j = searchExperimentDataForLockup === null || searchExperimentDataForLockup === void 0 ? void 0 : searchExperimentDataForLockup.displayStyle) === null || _j === void 0 ? void 0 : _j.metadataRibbon) !== null && _k !== void 0 ? _k : (_l = searchExperimentDataForPage === null || searchExperimentDataForPage === void 0 ? void 0 : searchExperimentDataForPage.displayStyle) === null || _l === void 0 ? void 0 : _l.metadataRibbon; + } + const metadataRibbonItems = metadataRibbon.createMetadataRibbonItemsForLockup(objectGraph, data, lockup, metadataRibbonItemSlots, options); + const searchTagsRibbonItems = searchTagsRibbon.createSearchTagsRibbonItemsForLockup(objectGraph, data, lockup, llmMetadataRibbonItems, options); + lockup.metadataRibbonItems = metadataRibbonItems; + lockup.searchTagRibbonItems = searchTagsRibbonItems; + lockup.shouldEvenlyDistributeRibbonItems = !objectGraph.bag.isLLMSearchTagsEnabled; +} +/** + * Configures all the data into the lockup object + * @param objectGraph The ObjectGraph instance for the App Store + * @param data The branch IAP used by the contingent offer + * @param lockup The lockup object the metadata object will be added to + * @param options Lockup configuration options + * @param isParentAppFree A flag used when creating an IAP lockup with missing parent app data in its relationships property. + */ +function copyDataIntoLockup(objectGraph, data, lockup, options, parentData, copyAdditionalDataIntoLockup) { + if (!data) { + return; + } + validation.context("copyDataIntoLockup", () => { + var _a, _b, _c, _d, _e, _f; + const isPreorder = mediaAttributes.attributeAsBooleanOrFalse(data, "isPreorder"); + // Drop this item if it's not a preorder, but contained in a pre-order exclusive shelf. + if (options.isContainedInPreorderExclusiveShelf && !isPreorder) { + return null; + } + const attributePlatform = (_a = options === null || options === void 0 ? void 0 : options.attributePlatformOverride) !== null && _a !== void 0 ? _a : contentAttributes.defaultAttributePlatform(objectGraph); + const productVariantData = variants.productVariantDataForData(objectGraph, data); + lockup.productVariantID = variants.productVariantIDForVariantData(productVariantData); + options.metricsOptions.productVariantData = productVariantData; + lockup.adamId = data.id; + const bundleId = contentAttributes.contentAttributeAsString(objectGraph, data, "bundleId", options === null || options === void 0 ? void 0 : options.attributePlatformOverride); + lockup.bundleId = bundleId; + lockup.decorations = []; + let clientIdentifierOverride = null; + if (options && options.clientIdentifierOverride) { + clientIdentifierOverride = options.clientIdentifierOverride; + } + lockup.icon = content.iconFromData(objectGraph, data, { + useCase: options.artworkUseCase, + withJoeColorPlaceholder: options.useJoeColorIconPlaceholder, + joeColorPlaceholderSelectionLogic: options.joeColorPlaceholderSelectionLogic, + overrideTextColorKey: options.overrideArtworkTextColorKey, + }, clientIdentifierOverride, productVariantData, options.attributePlatformOverride); + if (options && options.titleObjectPath) { + lockup.title = contentAttributes.contentAttributeAsString(objectGraph, data, options.titleObjectPath, options === null || options === void 0 ? void 0 : options.attributePlatformOverride); + } + else { + lockup.title = mediaAttributes.attributeAsString(data, "name"); + } + if (objectGraph.client.isWeb) { + lockup.isIOSBinaryMacOSCompatible = mediaAttributes.attributeAsBooleanOrFalse(data, "isIOSBinaryMacOSCompatible"); + } + // Only use an ad override language if this is an ad. + lockup.useAdsLocale = options.metricsOptions.isAdvert && isSome(objectGraph.bag.adsOverrideLanguage); + // Don't display "Apple Arcade" on a lockup if it's not an arcade app or we're running in the + // apple arcade app (tvOS) + const isArcadeLockup = content.isArcadeSupported(objectGraph, data, options === null || options === void 0 ? void 0 : options.attributePlatformOverride); + if (isArcadeLockup && isArcadeLockupWordmarkSupported(objectGraph) && !options.shouldHideArcadeHeader) { + // tvOS uses a 'pill' decoration over artwork to display apple arcade + // while everywhere else just uses the heading text slot to display a string + if (objectGraph.client.isTV) { + lockup.decorations.push("arcade"); + } + else { + lockup.heading = options.metricsOptions.isAdvert + ? objectGraph.adsLoc.string("Lockup.Heading.Arcade") + : objectGraph.loc.string("Lockup.Heading.Arcade"); + } + } + if (options.shouldShowFriendsPlayingShowcase) { + lockup.decorations.push("showcaseFriendsPlaying"); + } + // Subtitle + const allowMultilineTertiaryLabel = !isArcadeLockup && !isPreorder && ((_b = options.isMultilineTertiaryTitleAllowed) !== null && _b !== void 0 ? _b : true); + if (!options.isSubtitleHidden && !isBadgeMultilineFromData(objectGraph, data, allowMultilineTertiaryLabel)) { + lockup.subtitle = subtitleFromData(objectGraph, data, options); + } + // Tertiary badge + lockup.tertiaryTitle = badgeFromData(objectGraph, data, allowMultilineTertiaryLabel, options.hideCompatibilityBadge); + lockup.tertiaryTitleAction = badgeActionFromData(objectGraph, data); + lockup.tertiaryTitleArtwork = badgeArtworkFromData(objectGraph, data); + lockup.developerTagline = contentAttributes.contentAttributeAsString(objectGraph, data, "subtitle", options === null || options === void 0 ? void 0 : options.attributePlatformOverride); + lockup.editorialTagline = content.notesFromData(objectGraph, data, "tagline", false, options === null || options === void 0 ? void 0 : options.attributePlatformOverride); + lockup.editorialDescription = content.notesFromData(objectGraph, data, "standard", false, options === null || options === void 0 ? void 0 : options.attributePlatformOverride); + lockup.shortEditorialDescription = content.notesFromData(objectGraph, data, "short", false, options === null || options === void 0 ? void 0 : options.attributePlatformOverride); + lockup.ageRating = ageRatings.name(objectGraph, data, true); + lockup.productDescription = contentAttributes.contentAttributeAsString(objectGraph, data, "description.standard", options === null || options === void 0 ? void 0 : options.attributePlatformOverride); + if ((_c = options === null || options === void 0 ? void 0 : options.shouldShowSupportedPlatformLabel) !== null && _c !== void 0 ? _c : false) { + copySupportedPlatformLabelIntoLockup(objectGraph, data, lockup, options); + } + if (!reviews.shouldSuppressReviews(objectGraph, data)) { + const ratingCount = mediaAttributes.attributeAsNumber(data, "userRating.ratingCount"); + if (ratingCount > 0 || !(options && options.hideZeroRatings)) { + lockup.rating = mediaAttributes.attributeAsNumber(data, "userRating.value"); + const count = mediaAttributes.attributeAsNumber(data, "userRating.ratingCount"); + const adsOverrideLanguage = options.metricsOptions.isAdvert + ? objectGraph.bag.adsOverrideLanguage + : null; + lockup.ratingCount = objectGraph.loc.formattedCountForPreferredLocale(objectGraph, count, adsOverrideLanguage); + } + } + const metricsClickOptions = metricsHelpersClicks.clickOptionsForLockup(objectGraph, data, options.metricsOptions, options.metricsClickOptions); + metricsHelpersLocation.pushContentLocation(objectGraph, metricsClickOptions, lockup.title); + const offerData = offers.offerDataFromData(objectGraph, data, options === null || options === void 0 ? void 0 : options.attributePlatformOverride); + const includeBetaApps = (_d = options === null || options === void 0 ? void 0 : options.includeBetaApps) !== null && _d !== void 0 ? _d : false; + const metricsPlatformDisplayStyle = metricsUtil.metricsPlatformDisplayStyleFromData(objectGraph, data, lockup.icon, clientIdentifierOverride); + const offerButtonMetricsClickOptions = objects.shallowCopyOf(metricsClickOptions); + const offerAction = offers.offerActionFromOfferData(objectGraph, offerData, data, isPreorder, includeBetaApps, metricsPlatformDisplayStyle, offerButtonMetricsClickOptions, "default", options === null || options === void 0 ? void 0 : options.referrerData); + const allOfferDiscountData = serverData.asArrayOrEmpty(offerData, "discounts"); + let buttonAction = offers.wrapOfferActionIfNeeded(objectGraph, offerAction, data, isPreorder, offerButtonMetricsClickOptions, "default", clientIdentifierOverride, options.shouldNavigateToProductPage); + const cppDeeplinkUrl = contentAttributes.customAttributeAsString(objectGraph, data, productVariantData, "customDeepLink", attributePlatform); + const isAdvert = adLockup.isAdvert(objectGraph, data); + // CPP deeplinks used for ads have a requirement to not be used when targeting criteria is too narrow. + const isCppDeeplinkingEnabled = !isAdvert || adLockup.isCppDeeplinkEnabledForAdvert(data); + const hasCppDeepLink = isCppDeeplinkingEnabled && (cppDeeplinkUrl === null || cppDeeplinkUrl === void 0 ? void 0 : cppDeeplinkUrl.length) > 0; + const hasExternalDeepLink = ((_e = options === null || options === void 0 ? void 0 : options.externalDeepLinkUrl) === null || _e === void 0 ? void 0 : _e.length) > 0; + let deepLinkUrl; + if (preprocessor.CARRY_BUILD || preprocessor.DEBUG_BUILD) { + if (objectGraph.featureFlags.isEnabled("aligned_region_artwork_2025A")) { + const customCreativeDeepLinkUrl = adLockup.getCustomCreativeDeepLinkUrl(data); + const isCustomCreativeDeeplinkingEnabled = isAdvert && adLockup.isCustomCreativeDeeplinkEnabledForAdvert(data); + const hasCustomCreativeDeepLink = isCustomCreativeDeeplinkingEnabled && (customCreativeDeepLinkUrl === null || customCreativeDeepLinkUrl === void 0 ? void 0 : customCreativeDeepLinkUrl.length) > 0; + if (hasCustomCreativeDeepLink) { + deepLinkUrl = customCreativeDeepLinkUrl; + } + else if (hasCppDeepLink) { + deepLinkUrl = cppDeeplinkUrl; + } + else if (hasExternalDeepLink) { + deepLinkUrl = options.externalDeepLinkUrl; + } + } + else if (hasCppDeepLink || hasExternalDeepLink) { + deepLinkUrl = hasCppDeepLink ? cppDeeplinkUrl : options.externalDeepLinkUrl; + } + } + else if (hasCppDeepLink || hasExternalDeepLink) { + deepLinkUrl = hasCppDeepLink ? cppDeeplinkUrl : options.externalDeepLinkUrl; + } + if (isSome(deepLinkUrl)) { + // Configure cross link as well as deep link action. + buttonAction = externalDeepLink.deepLinkActionWrappingAction(objectGraph, buttonAction, offerAction.adamId, bundleId, deepLinkUrl, includeBetaApps, offerButtonMetricsClickOptions); + // Configure cross link title and subtitle. + if (((_f = options.crossLinkSubtitle) === null || _f === void 0 ? void 0 : _f.length) > 0) { + lockup.crossLinkTitle = objectGraph.loc.uppercased(mediaAttributes.attributeAsString(data, "name")); + lockup.crossLinkSubtitle = options.crossLinkSubtitle; + } + } + lockup.buttonAction = buttonAction; + // Beta apps + lockup.includeBetaApps = includeBetaApps; + lockup.developerName = mediaAttributes.attributeAsString(data, "artistName"); + if (isNothing(lockup.developerName)) { + // Some newer APIs use `developerName` instead of `artistName`. + lockup.developerName = mediaAttributes.attributeAsString(data, "developerName"); + } + // Ensure we grab its children. + lockup.children = childrenFromLockupData(objectGraph, data, options); + if (isSome(copyAdditionalDataIntoLockup)) { + copyAdditionalDataIntoLockup(); + } + metricsHelpersLocation.popLocation(options.metricsOptions.locationTracker); + const shareSheetData = sharing.shareSheetDataForProductFromProductData(objectGraph, data, clientIdentifierOverride); + if (shareSheetData) { + const shareMenuAction = new models.BlankAction(); + const shareMetricsOptions = objects.shallowCopyOf(metricsClickOptions); + shareMetricsOptions.actionType = "share"; + shareMetricsOptions.targetType = "lockup"; + metricsHelpersClicks.addClickEventToAction(objectGraph, shareMenuAction, shareMetricsOptions); + const shareMenuData = new models.LockupContextMenuData(); + shareMenuData.shareAction = shareMenuAction; + shareMenuData.shareSheetData = shareSheetData; + lockup.contextMenuData = shareMenuData; + } + const resolvedParentData = parentData !== null && parentData !== void 0 ? parentData : parentDataFromInAppData(objectGraph, data); + let isParentAppFree = false; + if (resolvedParentData) { + const parentOfferData = offers.offerDataFromData(objectGraph, resolvedParentData); + const parentPrice = offers.priceFromOfferData(objectGraph, parentOfferData); + isParentAppFree = !(parentPrice > 0); + } + const offerType = offerTypeForMediaType(objectGraph, data.type, isArcadeLockup); + if (options) { + lockup.offerDisplayProperties = offers.displayPropertiesFromOfferAction(objectGraph, offerAction, offerType, data, isPreorder, options.isContainedInPreorderExclusiveShelf, options.offerStyle, options.offerEnvironment, allOfferDiscountData[0], isParentAppFree, "default", options.shouldNavigateToProductPage, options.metricsOptions.isAdvert, null, options.parentAppData, options.isBuyDisallowed); + } + else { + lockup.offerDisplayProperties = offers.displayPropertiesFromOfferAction(objectGraph, offerAction, offerType, data, isPreorder, options.isContainedInPreorderExclusiveShelf, null, null, allOfferDiscountData[0], isParentAppFree, "default"); + } + if (!options || !options.skipDefaultClickAction) { + if (preprocessor.CARRY_BUILD || preprocessor.DEBUG_BUILD) { + lockup.clickAction = productActionFromData(objectGraph, data, metricsClickOptions, { + clientIdentifierOverride: clientIdentifierOverride, + productVariantData: productVariantData, + alignedRegionDeepLinkUrl: adLockup.getCustomCreativeDeepLinkUrl(data), + isCppDeepLinkEligible: isCppDeeplinkingEnabled, + }); + } + else { + lockup.clickAction = productActionFromData(objectGraph, data, metricsClickOptions, { + clientIdentifierOverride: clientIdentifierOverride, + productVariantData: productVariantData, + isCppDeepLinkEligible: isCppDeeplinkingEnabled, + }); + } + } + if (options && options.ordinal) { + lockup.ordinal = options.ordinal; + } + const editorialBadgeInfo = contentAttributes.contentAttributeAsDictionary(objectGraph, data, "editorialBadgeInfo", options === null || options === void 0 ? void 0 : options.attributePlatformOverride); + const supportsVisionOSCompatibleIOSBinary = content.supportsVisionOSCompatibleIOSBinaryFromData(objectGraph, data); + if (editorialBadgeInfo && !supportsVisionOSCompatibleIOSBinary) { + const badgeType = serverData.asString(editorialBadgeInfo, "editorialBadgeType"); + const hasEditorsChoiceBadge = badgeType && badgeType === "editorialPriority"; + lockup.isEditorsChoice = hasEditorsChoiceBadge; + } + // Flow preview actions + if (!isAdvert) { + lockup.flowPreviewActionsConfiguration = flowPreview.flowPreviewActionsConfigurationForProductFromData(objectGraph, data, false, clientIdentifierOverride, lockup.clickAction, options.metricsOptions, metricsClickOptions, options.externalDeepLinkUrl, lockup.subtitle, lockup.title); + } + const metricsImpressionOptions = metricsHelpersImpressions.impressionOptionsForLockup(objectGraph, data, lockup, metricsPlatformDisplayStyle, options.metricsOptions, options.canDisplayArcadeOfferButton); + metricsHelpersImpressions.addImpressionFields(objectGraph, lockup, metricsImpressionOptions); + }); +} +/** + * Configures the tertiary icons and label for showing unsupported apps in a bundle. + * @param objectGraph The ObjectGraph instance for the App Store + * @param data The branch IAP used by the contingent offer + * @param lockup The lockup object the metadata object will be added to + * @param options Lockup configuration options + */ +function copySupportedPlatformLabelIntoLockup(objectGraph, data, lockup, options) { + const appPlatforms = content.supportedAppPlatformsFromData(objectGraph, data); + const supportsMacOSCompatibleIOSBinary = content.supportsMacOSCompatibleIOSBinaryFromData(objectGraph, data, objectGraph.client.isMac); + const supportsVisionOSCompatibleBinary = content.supportsVisionOSCompatibleIOSBinaryFromData(objectGraph, data); + const isBuyable = content.buyableOnDevice(objectGraph, data, appPlatforms, objectGraph.client.deviceType, supportsMacOSCompatibleIOSBinary, supportsVisionOSCompatibleBinary); + /// If its buyable no need to display compatibility label + if (isBuyable) { + return; + } + const additionalPlatforms = appPlatforms.filter((platform) => platform !== objectGraph.client.deviceType); + if (additionalPlatforms.length === 0) { + return; + } + lockup.tertiaryTitleIcons = additionalPlatforms.map((platform) => content.systemImageNameForAppPlatform(platform)); + if (additionalPlatforms.length === 1) { + const platformTitle = content.appPlatformTitle(objectGraph, additionalPlatforms[0]); + lockup.tertiaryTitle = objectGraph.loc + .string("AppStore.Bundles.ProductPage.OnlyAvailable.Message") + .replace("@@platform@@", platformTitle); + } + else { + // We only support showing up to 2 platforms. Grab the first 2 + const platformTitle1 = content.appPlatformTitle(objectGraph, additionalPlatforms[0]); + const platformTitle2 = content.appPlatformTitle(objectGraph, additionalPlatforms[1]); + lockup.tertiaryTitle = objectGraph.loc + .string("AppStore.Bundles.ProductPage.AvailableOnTwo.Message") + .replace("@@platform1@@", platformTitle1) + .replace("@@platform2@@", platformTitle2); + } +} +export function childrenFromLockupData(objectGraph, data, options) { + const childrenRelationship = mediaRelationship.relationship(data, "apps"); + if (childrenRelationship) { + const listOptions = { + lockupOptions: { + ...options, + shouldCreateScreenshotsLockup: options === null || options === void 0 ? void 0 : options.shouldIncludeScreenshotsForChildren, + }, + filter: 0 /* filtering.Filter.None */, + }; + return lockupsFromDataContainer(objectGraph, childrenRelationship, listOptions); + } + return null; +} +function copyDataIntoInAppPurchaseLockup(objectGraph, data, lockup, options) { + if (!data) { + return; + } + validation.context("copyDataIntoInAppPurchaseLockup", () => { + var _a; + const parentData = (_a = parentDataFromInAppData(objectGraph, data)) !== null && _a !== void 0 ? _a : options.parentAppData; + const isStreamlinedBuy = mediaAttributes.attributeAsBooleanOrFalse(parentData, "supportsStreamlinedBuy"); + const iapData = iapDataFromData(objectGraph, data); + data = iapData; + copyDataIntoLockup(objectGraph, data, lockup, options, parentData); + lockup.productIdentifier = mediaAttributes.attributeAsString(data, "offerName"); + lockup.parent = lockupFromData(objectGraph, parentData, options); + lockup.description = mediaAttributes.attributeAsString(data, "description.standard"); + lockup.isVisibleByDefault = mediaAttributes.attributeAsBooleanOrFalse(data, "isMerchandisedVisibleByDefault"); + lockup.isSubscription = mediaAttributes.attributeAsBooleanOrFalse(data, "isSubscription"); + const offerData = offers.offerDataFromData(objectGraph, data); + const discountOfferData = serverData.asArrayOrEmpty(offerData, "discounts"); + lockup.offerDisplayProperties.hasDiscount = discountOfferData.length > 0; + lockup.offerDisplayProperties.subscriptionFamilyId = mediaAttributes.attributeAsString(data, "subscriptionFamilyId"); + // Action for App Install (needed in case the parent app is not installed) + const installRequiredAction = new models.FlowAction("inAppPurchaseInstall"); + installRequiredAction.presentationContext = "presentModalFormSheet"; + const installRequiredActionUrl = configureIAPInstallPageUrl(objectGraph, lockup.adamId, parentData.id); + installRequiredAction.pageUrl = installRequiredActionUrl; + // Install Page + const sidepackInstallPage = new models.InAppPurchaseInstallPage(); + sidepackInstallPage.parentLockup = objects.shallowCopyOf(lockup.parent); + sidepackInstallPage.lockup = objects.shallowCopyOf(lockup); + sidepackInstallPage.preInstallOfferDescription = offerFormatting.installPagePreInstallTrialDescription(objectGraph, offerData); + installRequiredAction.pageData = sidepackInstallPage; + const productIdentifier = mediaAttributes.attributeAsString(data, "offerName"); + const parentBundleId = contentAttributes.contentAttributeAsString(objectGraph, parentData, "bundleId"); + const firstVersionSupportingMerchIAP = mediaAttributes.attributeAsString(parentData, "firstVersionSupportingInAppPurchaseApi"); + const hasDiscountedOffer = serverData.isDefinedNonNullNonEmpty(discountedOfferFromData(data)); + const metricsOptions = metricsHelpersImpressions.impressionOptionsForLockup(objectGraph, data, lockup, "iap", options.metricsOptions, options.canDisplayArcadeOfferButton); + metricsHelpersLocation.pushContentLocation(objectGraph, metricsOptions, lockup.title); + if (isStreamlinedBuy && hasDiscountedOffer) { + const inAppPurchaseAction = new models.InAppPurchaseAction(productIdentifier, parentData.id, parentBundleId, lockup.parent.buttonAction); + if (lockup.parent) { + inAppPurchaseAction.appTitle = lockup.parent.title; + } + inAppPurchaseAction.productTitle = lockup.title; + inAppPurchaseAction.streamlineBuyAction = streamlineBuyActionForIAP(objectGraph, data, parentData, lockup, options); + lockup.buttonAction = inAppPurchaseAction; + lockup.subtitle = mediaAttributes.attributeAsString(parentData, "name"); + } + else if (firstVersionSupportingMerchIAP) { + const inAppPurchaseAction = new models.InAppPurchaseAction(productIdentifier, parentData.id, parentBundleId, installRequiredAction, firstVersionSupportingMerchIAP); + if (lockup.parent) { + inAppPurchaseAction.appTitle = lockup.parent.title; + } + inAppPurchaseAction.productTitle = lockup.title; + const clickOptions = { + ...options.metricsOptions, + id: lockup.adamId, + idType: "its_id", + actionDetails: { parentAdamId: parentData.id }, + }; + metricsHelpersClicks.addClickEventToAction(objectGraph, inAppPurchaseAction, clickOptions); + lockup.buttonAction = inAppPurchaseAction; + } + else { + const alert = new models.AlertAction("default"); + alert.title = objectGraph.loc.string("SEED_IN_APP_UNSUPPORTED_MESSAGE_OPTION_1"); + alert.message = ""; + alert.isCancelable = true; + lockup.buttonAction = alert; + } + metricsHelpersLocation.popLocation(options.metricsOptions.locationTracker); + // Click action to the product page + if (!options || !options.skipDefaultClickAction) { + const clickAction = iAPActionFromData(objectGraph, data, metricsOptions); + lockup.clickAction = clickAction; + lockup.productAction = clickAction; + } + metricsHelpersImpressions.addImpressionFieldsToInAppPurchaseLockup(objectGraph, lockup, metricsOptions); + }, "item.offer.buyParams"); +} +/** + * Creates the discounted buy action for the streamline buy Action + * @param objectGraph The ObjectGraph instance for the App Store + * @param data The branch IAP used by the contingent offer + * @param parentData The branch app used by the contingent offer + * @param lockup The lockup object the metadata object will be added to + * @param options Lockup configuration options + */ +function streamlineBuyActionForIAP(objectGraph, data, parentData, lockup, options) { + var _a; + const clickOptions = { + ...options.metricsOptions, + id: parentData.id, + targetId: parentData.id, + idType: "its_id", + actionDetails: { parentAdamId: parentData.id }, + }; + const metricsPlatformDisplayStyle = metricsUtil.metricsPlatformDisplayStyleFromData(objectGraph, data, lockup.icon, options.clientIdentifierOverride); + const offerButtonMetricsClickOptions = objects.shallowCopyOf(clickOptions); + const discountedOfferData = discountedOfferFromData(data); + const offerData = offers.offerDataFromData(objectGraph, data); + const appOfferData = offers.offerDataFromData(objectGraph, parentData); + const appBuyParams = new BuyParameters(serverData.asString(appOfferData, "buyParams")); + let currentBuyParams = (_a = serverData.asString(discountedOfferData, "buyParams")) !== null && _a !== void 0 ? _a : serverData.asString(offerData, "buyParams"); + currentBuyParams += `&appAdamId=${serverData.asString(parentData, "id")}`; + currentBuyParams += `&appExtVrsId=${appBuyParams.get("appExtVrsId", "")}`; + currentBuyParams += `&bid=${contentAttributes.contentAttributeAsString(objectGraph, parentData, "bundleId")}`; + currentBuyParams += `&bvrs=1.0`; + currentBuyParams += `&offerName=${contentAttributes.contentAttributeAsString(objectGraph, data, "offerName")}`; + const offerId = serverData.asString(discountedOfferData, "offerId"); + if (serverData.isDefinedNonNullNonEmpty(offerId)) { + currentBuyParams += `&adHocOfferId=${offerId}`; + } + offerData["buyParams"] = currentBuyParams; + const subscribeAction = offers.offerActionFromOfferData(objectGraph, offerData, data, false, false, metricsPlatformDisplayStyle, offerButtonMetricsClickOptions, "default", options === null || options === void 0 ? void 0 : options.referrerData, false, parentData.id); + return subscribeAction; +} +function copyDataIntoTrailersLockup(objectGraph, data, lockup, videoConfiguration, options) { + if (!data) { + return; + } + validation.context("copyDataIntoTrailersLockup", () => { + copyDataIntoLockup(objectGraph, data, lockup, options); + lockup.trailers = content.trailersFromData(objectGraph, data, videoConfiguration, options.metricsOptions, lockup.adamId); + }); +} +function copyMediaIntoMixedMediaLockup(objectGraph, data, lockup, videoConfiguration, options, cropCode) { + var _a; + if (!data) { + return; + } + if (options.isNetworkConstrained) { + return; + } + const isAd = (_a = options.metricsOptions.isAdvert) !== null && _a !== void 0 ? _a : false; + validation.context("copyMediaIntoMixedMediaLockup", () => { + if (preprocessor.CARRY_BUILD || preprocessor.DEBUG_BUILD) { + if (objectGraph.featureFlags.isEnabled("aligned_region_artwork_2025A")) { + const customCreativeData = platformAttributeAsDictionary(data, contentAttributes.bestAttributePlatformFromData(objectGraph, data), "creativeAttributes"); + if (isSome(customCreativeData)) { + lockup.alignedRegionArtwork = customCreativeArtworkFromData(objectGraph, data, customCreativeData, cropCode); + lockup.alignedRegionVideo = customCreativeVideoFromData(objectGraph, data, customCreativeData, videoConfiguration, cropCode); + } + } + } + lockup.screenshots = content.screenshotsFromData(objectGraph, data, 4 /* content.ArtworkUseCase.LockupScreenshots */, null, options.clientIdentifierOverride, null, isAd, cropCode); + const firstScreenshots = lockup.screenshots[0]; + lockup.trailers = []; + const trailers = content.trailersFromData(objectGraph, data, videoConfiguration, options.metricsOptions, lockup.adamId, isAd, cropCode); + if (serverData.isDefinedNonNull(trailers)) { + if (serverData.isNullOrEmpty(firstScreenshots) || + trailers.mediaPlatform.isEqualTo(firstScreenshots.mediaPlatform)) { + lockup.trailers.push(trailers); + } + } + }); +} +/** + * Copy the data contained in a platform response into a `ScreenshotsLockup`. + * @param data The store platform response data to read from. + * @param lockup The ScreenshotsLockup data model to copy store platform data to + * @param options Options customizing what data is needed. + */ +function copyDataIntoScreenshotsLockup(objectGraph, data, lockup, options) { + if (!data) { + return; + } + validation.context("copyDataIntoScreenshotsLockup", () => { + copyDataIntoLockup(objectGraph, data, lockup, options); + lockup.screenshots = content.screenshotsFromData(objectGraph, data, 4 /* content.ArtworkUseCase.LockupScreenshots */, null, options.clientIdentifierOverride); + }); +} +/** + * Copy the data contained in a platform response into a `PosterLockup`. + * @param data The store platform response data to read from. + * @param lockup The PosterLockup data model to copy store platform data to + * @param options Options customizing what data is needed. + */ +function copyDataIntoPosterLockup(objectGraph, data, lockup, options) { + if (!data) { + return; + } + validation.context("copyDataIntoPosterLockup", () => { + copyDataIntoLockup(objectGraph, data, lockup, options); + lockup.epicHeading = content.posterEpicHeadingArtworkFromData(objectGraph, data); + lockup.posterArtwork = content.posterArtworkFromData(objectGraph, data); + lockup.posterVideo = content.posterEditorialVideoFromData(objectGraph, data, 19 /* content.ArtworkUseCase.GroupingHero */); + if (lockup.offerDisplayProperties) { + lockup.offerDisplayProperties = lockup.offerDisplayProperties.newOfferDisplayPropertiesChangingAppearance(false, "white", "lightOverArtwork"); + } + if (lockup.posterVideo) { + lockup.isDark = color.isDarkColor(lockup.posterVideo.preview.backgroundColor); + } + else if (lockup.posterArtwork) { + lockup.isDark = color.isDarkColor(lockup.posterArtwork.backgroundColor); + } + else { + lockup.isDark = false; + } + const supportsArcade = contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "supportsArcade"); + const isPreorder = contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "isPreorder"); + if (isPreorder) { + const fallbackString = supportsArcade ? objectGraph.loc.string("Offer.Label.ComingSoon") : null; + let preorderDate; + if (objectGraph.client.isVision || objectGraph.client.isWeb) { + preorderDate = content.dynamicPreorderDateFromData(objectGraph, data, fallbackString); + } + else { + preorderDate = objectGraph.loc.uppercased(content.dynamicPreorderDateFromData(objectGraph, data, fallbackString)); + } + if (isSome(preorderDate)) { + lockup.footerText = preorderDate; + } + } + }); +} +export function subtitleFromData(objectGraph, data, lockupOptions = null) { + if (isNothing(data)) { + return null; + } + return validation.context("subtitleFromData", () => { + let subtitle; + if (lockupOptions && lockupOptions.subtitleObjectPath) { + subtitle = contentAttributes.contentAttributeAsString(objectGraph, data, lockupOptions.subtitleObjectPath, lockupOptions === null || lockupOptions === void 0 ? void 0 : lockupOptions.attributePlatformOverride); + } + if (serverData.isNullOrEmpty(subtitle)) { + subtitle = contentAttributes.contentAttributeAsString(objectGraph, data, "subtitle", lockupOptions === null || lockupOptions === void 0 ? void 0 : lockupOptions.attributePlatformOverride); + } + if (subtitle) { + return subtitle; + } + else { + return categoryFromData(objectGraph, data, lockupOptions); + } + }); +} +/** + * Creates the compability badge text for showing on a lockup. + * + * @param objectGraph Current object graph + * @param data Product data + * @returns Built badge text, or null + */ +export function badgeFromData(objectGraph, data, allowMultiline = false, hideCompatibilityBadge) { + return validation.context("badgeFromData", () => { + if (hideCompatibilityBadge) { + return null; + } + const doesClientSupportMacOSCompatibleIOSBinary = objectGraph.client.isMac || objectGraph.client.isWeb; + const supportsMacOSCompatibleIOSBinary = content.supportsMacOSCompatibleIOSBinaryFromData(objectGraph, data, doesClientSupportMacOSCompatibleIOSBinary); + const supportsVisionOSCompatibleBinary = content.supportsVisionOSCompatibleIOSBinaryFromData(objectGraph, data); + if (supportsMacOSCompatibleIOSBinary || supportsVisionOSCompatibleBinary) { + // 1. Build loc string base. + let locKey = ""; + const appPlatforms = content.supportedAppPlatformsFromData(objectGraph, data); + if (content.supportsPlatform(appPlatforms, "pad")) { + locKey = "Platform.DesignedForPad"; + } + else if (content.supportsPlatform(appPlatforms, "phone")) { + locKey = "Platform.DesignedForPhone"; + } + if ((locKey === null || locKey === void 0 ? void 0 : locKey.length) > 0) { + if (supportsMacOSCompatibleIOSBinary) { + // 2. Check if app is verified. + const isVerifiedForAppleSiliconMac = isVerifiedForAppleSiliconMacFromData(objectGraph, data); + if (!isVerifiedForAppleSiliconMac) { + locKey += ".NotVerified"; + // 3. Use expanded two-line not verified badge. + if (allowMultiline) { + locKey += ".Expanded"; + } + } + } + return objectGraph.loc.string(locKey); + } + } + return null; + }); +} +/** + * Creates the compability badge artwork for showing on a lockup. + * + * @param objectGraph Current object graph + * @param data Product data + * @returns An iPhone/iPad symbol artwork, or null + */ +export function badgeArtworkFromData(objectGraph, data) { + const supportsVisionOSCompatibleBinary = content.supportsVisionOSCompatibleIOSBinaryFromData(objectGraph, data); + if (supportsVisionOSCompatibleBinary && objectGraph.client.isVision) { + const appPlatforms = content.supportedAppPlatformsFromData(objectGraph, data); + if (content.supportsPlatform(appPlatforms, "pad")) { + return createArtworkForResource(objectGraph, "systemimage://ipad.landscape"); + } + else if (content.supportsPlatform(appPlatforms, "phone")) { + return createArtworkForResource(objectGraph, "systemimage://iphone"); + } + } + return null; +} +export function badgeActionFromData(objectGraph, data) { + return validation.context("badgeActionFromData", () => { + // Add action for unverified macOS apps. + const supportsMacOSCompatibleIOSBinary = content.supportsMacOSCompatibleIOSBinaryFromData(objectGraph, data, objectGraph.client.isMac); + if (!supportsMacOSCompatibleIOSBinary) { + return null; + } + const isVerifiedForAppleSiliconMac = isVerifiedForAppleSiliconMacFromData(objectGraph, data); + if (isVerifiedForAppleSiliconMac) { + return null; + } + const appPlatforms = content.supportedAppPlatformsFromData(objectGraph, data); + if (!content.supportsPlatform(appPlatforms, "pad") && !content.supportsPlatform(appPlatforms, "phone")) { + return null; + } + const linkAction = new models.FlowAction("article"); + linkAction.pageUrl = `https://apps.apple.com/story/id${objectGraph.bag.appleSiliconMacUnverifiedBadgeEditorialItemId}`; + return linkAction; + }); +} +/// Does the badge require multiple lines? +export function isBadgeMultilineFromData(objectGraph, data, allowMultiline) { + return validation.context("isBadgeMultilineFromData", () => { + if (!allowMultiline) { + return false; + } + const supportsMacOSCompatibleIOSBinary = content.supportsMacOSCompatibleIOSBinaryFromData(objectGraph, data, objectGraph.client.isMac); + if (!supportsMacOSCompatibleIOSBinary) { + return false; + } + const isVerifiedForAppleSiliconMac = isVerifiedForAppleSiliconMacFromData(objectGraph, data); + if (isVerifiedForAppleSiliconMac) { + return false; + } + const appPlatforms = content.supportedAppPlatformsFromData(objectGraph, data); + return content.supportsPlatform(appPlatforms, "pad") || content.supportsPlatform(appPlatforms, "phone"); + }); +} +function isVerifiedForAppleSiliconMacFromData(objectGraph, data) { + // Don't badge unverified apps when Apple Silicon support is not enabled. + if (!objectGraph.appleSilicon.isSupportEnabled) { + return true; + } + // Check response. + const isVerifiedForAppleSiliconMac = contentAttributes.contentAttributeAsBoolean(objectGraph, data, "isVerifiedForAppleSiliconMac", "ios"); + if (serverData.isDefinedNonNull(isVerifiedForAppleSiliconMac)) { + return isVerifiedForAppleSiliconMac; + } + return false; +} +export function categoryFromData(objectGraph, data, lockupOptions = null) { + return validation.context("categoryFromData", () => { + // genreDisplayName is the preferred field to use for the determining the category name + // It should always be available, but we have fallbacks below just in case. + const genreName = contentAttributes.contentAttributeAsString(objectGraph, data, "genreDisplayName", lockupOptions === null || lockupOptions === void 0 ? void 0 : lockupOptions.attributePlatformOverride); + if ((genreName === null || genreName === void 0 ? void 0 : genreName.length) > 0) { + return genreName; + } + // Fallback to interating over the genres list + const genres = mediaRelationship.relationshipCollection(data, "genres"); + if (genres.length > 0) { + let candidateGenre = genres[0]; + // If the primary genre is Games, search for the first sub-genre of games + const gamesId = 6014 /* constants.GenreIds.Games */.toString(); + if (candidateGenre.id === gamesId) { + for (const genre of genres) { + const parentGenreId = mediaAttributes.attributeAsString(genre, "parentId"); + if (genre.id !== gamesId && parentGenreId === gamesId) { + candidateGenre = genre; + break; + } + } + } + return mediaAttributes.attributeAsString(candidateGenre, "name"); + } + else { + // Fallback to genreNames + const genreNames = mediaAttributes.attributeAsArrayOrEmpty(data, "genreNames"); + return genreNames.length > 0 ? genreNames[0] : null; + } + }); +} +/** + * Configures an internal URL for use as the pageUrl of an 'install' FlowAction, + * using the provided parameters. + * @param inAppPurchaseAdamId The adamId for the lockup to appear in the header. + * @param parentAdamId The adamId for the lockup that is being offered. + * @returns A fully configured internal URL for fetching an app install page. + */ +export function configureIAPInstallPageUrl(objectGraph, inAppPurchaseAdamId, parentAdamId) { + const parameters = new http.FormBuilder() + .param(Parameters.id, parentAdamId) + .param(InAppPurchaseInstallPageParameters.inAppPurchaseId, inAppPurchaseAdamId) + .build(); + return `${Protocol.internal}:/${Path.product}/${Path.install}/?${parameters}`; +} +/** + * Configures an internal url to be used as the url for a clickAction from an IAP + * lockup to a product page. + * @param productUrl The URL for the product. + * @param inAppPurchaseAdamId The adamId for the lockup from which the click occurs. + * @param inAppPurchaseType The type for the in-app purchase. + * @returns A fully configured internal URL for a product click action via IAP. + */ +export function configureProductUrlFromInAppPurchase(objectGraph, productUrl, inAppProductIdentifier, isSubscription) { + const parameters = new http.FormBuilder() + .param(ProductPageParameters.url, productUrl) + .param(Parameters.offerName, inAppProductIdentifier) + .param(ProductPageParameters.isSubscription, isSubscription.toString()) + .build(); + return `${Protocol.internal}:/${Path.product}/${Path.lookup}/?${parameters}`; +} +export function lockupFromData(objectGraph, data, options) { + return validation.context("lockupFromData", () => { + var _a, _b, _c, _d, _e, _f, _g, _h, _j; + if (!data) { + validation.unexpectedNull("ignoredValue", "data"); + return null; + } + // Must setup iAdInfo before any builder methods. + const isAd = adLockup.isAdvert(objectGraph, data); + options.metricsOptions.isAdvert = isAd; + const isAdEligible = adCommon.isAdEligible((_b = (_a = options.metricsOptions.pageInformation) === null || _a === void 0 ? void 0 : _a.iAdInfo) === null || _b === void 0 ? void 0 : _b.placementType, options.metricsOptions.locationTracker); + options.metricsOptions.isAdEligible = isAdEligible; + if (isAd || isAdEligible) { + (_d = (_c = options.metricsOptions.pageInformation) === null || _c === void 0 ? void 0 : _c.iAdInfo) === null || _d === void 0 ? void 0 : _d.apply(objectGraph, data); + } + if (isAd) { + (_f = (_e = options.metricsOptions.pageInformation) === null || _e === void 0 ? void 0 : _e.iAdInfo) === null || _f === void 0 ? void 0 : _f.setTemplateType("APPLOCKUP"); + } + if (!mediaAttributes.hasAttributes(data)) { + return null; + } + switch (data.type) { + case "in-apps": + options.offerEnvironment = "widthConstrainedLockup"; + return inAppPurchaseLockupFromData(objectGraph, data, options); + case "app-events": + const parentAppData = mediaRelationship.relationshipData(objectGraph, data, "app"); + if (serverData.isNullOrEmpty(parentAppData)) { + return null; + } + const appLockup = new models.Lockup(); + copyDataIntoLockup(objectGraph, parentAppData, appLockup, options); + return appLockup; + case "contingent-items": + case "offer-items": + return appPromotionOfferLockupFromData(objectGraph, data, options); + default: + const lockup = new models.Lockup(); + copyDataIntoLockup(objectGraph, data, lockup, options); + if (isSome((_g = options.metricsOptions.pageInformation) === null || _g === void 0 ? void 0 : _g.iAdInfo)) { + if (isAd || isAdEligible) { + adLockup.performAdOverridesforLockup(objectGraph, data, lockup, options.metricsOptions); + } + if (isAd) { + if (objectGraph.props.enabled("advertSlotReporting")) { + (_h = lockup.searchAdOpportunity) === null || _h === void 0 ? void 0 : _h.setTemplateType("APPLOCKUP"); + } + else { + (_j = lockup.searchAd) === null || _j === void 0 ? void 0 : _j.setTemplateType("APPLOCKUP"); + } + } + } + return lockup; + } + }); +} +/** + * Configures a app promotion lockup with a contingent-items or offer-items object + * @param objectGraph The App Store Object Graph. + * @param data The data for the app to go into the lockup. + * @param options A set of options customising the lockup. + * @returns A Lockup with the desired configuration. + */ +export function appPromotionOfferLockupFromData(objectGraph, data, options) { + return validation.context("appPromotionOfferLockupFromData", () => { + var _a, _b, _c; + const parentData = (_a = parentDataFromInAppData(objectGraph, data)) !== null && _a !== void 0 ? _a : options.parentAppData; + const iapData = iapDataFromData(objectGraph, data); + const supportsStreamlinedBuy = mediaAttributes.attributeAsBooleanOrFalse(parentData, "supportsStreamlinedBuy"); + if (supportsStreamlinedBuy) { + const lockup = inAppPurchaseLockupFromData(objectGraph, data, options); + lockup.offerDisplayProperties.titles["standard"] = objectGraph.loc.string("OfferButton.Title.Subscribe"); + lockup.offerDisplayProperties.isStreamlinedBuy = true; + // Override any discounts to force showing "Subscribe" title on the offer button. + lockup.offerDisplayProperties.hasDiscount = false; + // Setup the artwork + const rawArtwork = mediaAttributes.attributeAsDictionary(iapData, "artwork"); + const backupArtwork = appPromotionsCommon.artworkFromPlatformData(objectGraph, parentData, "artwork"); + const iapArtwork = content.artworkFromApiArtwork(objectGraph, rawArtwork, { + useCase: options.artworkUseCase, + withJoeColorPlaceholder: options.useJoeColorIconPlaceholder, + style: "iap", + overrideTextColorKey: options.overrideArtworkTextColorKey, + }); + lockup.icon = iapArtwork !== null && iapArtwork !== void 0 ? iapArtwork : backupArtwork; + return lockup; + } + else { + const lockup = new models.Lockup(); + copyDataIntoLockup(objectGraph, parentData, lockup, options); + const metricsClickOptions = metricsHelpersClicks.clickOptionsForLockup(objectGraph, data, options.metricsOptions, options.metricsClickOptions); + metricsHelpersLocation.pushContentLocation(objectGraph, metricsClickOptions, lockup.title); + const parentBundleId = contentAttributes.contentAttributeAsString(objectGraph, parentData, "bundleId"); + const productIdentifier = mediaAttributes.attributeAsString(iapData, "offerName"); + const firstVersionSupportingMerchIAP = mediaAttributes.attributeAsString(parentData, "firstVersionSupportingInAppPurchaseApi"); + const offerAction = new models.InAppPurchaseAction(productIdentifier, parentData.id, parentBundleId, lockup.buttonAction, firstVersionSupportingMerchIAP); + offerAction.appTitle = (_b = mediaAttributes.attributeAsString(parentData, "name")) !== null && _b !== void 0 ? _b : ""; + offerAction.productTitle = (_c = mediaAttributes.attributeAsString(iapData, "name")) !== null && _c !== void 0 ? _c : ""; + if (data.type === "offer-items") { + const discountedOffer = discountedOfferFromData(iapData); + const offerId = serverData.asString(discountedOffer, "offerId"); + if (isSome(offerId) && offerId.length > 0) { + offerAction.additionalBuyParams = "adHocOfferId=" + offerId; + } + } + else { + // eslint-disable-next-line @typescript-eslint/restrict-plus-operands + offerAction.additionalBuyParams = "contingentItemId=" + data.id; + } + lockup.buttonAction = offerAction; + metricsHelpersLocation.popLocation(options.metricsOptions.locationTracker); + return lockup; + } + }); +} +export function inAppPurchaseLockupFromData(objectGraph, data, options) { + return validation.context("inAppPurchaseLockupFromData", () => { + const lockup = new models.InAppPurchaseLockup(); + copyDataIntoInAppPurchaseLockup(objectGraph, data, lockup, options); + return lockup; + }); +} +/** + * Create a new `ScreenshotsLockup` from a platform response data blob. + * @param data The platform response data to read from. + * @param options Options customizing what data the returned store item will contain. + * @returns A new `Lockup` object. + */ +export function screenshotsLockupFromData(objectGraph, data, options) { + return validation.context("screenshotsLockupFromData", () => { + const lockup = new models.ScreenshotsLockup(); + copyDataIntoScreenshotsLockup(objectGraph, data, lockup, options); + return lockup; + }); +} +/** + * Create a new `PosterLockup` from a platform response data blob. + * @param data The platform response data to read from. + * @param options Options customizing what data the returned store item will contain. + * @returns A new `Lockup` object. + */ +export function posterLockupFromData(objectGraph, data, options) { + return validation.context("posterLockupFromData", () => { + const lockup = new models.PosterLockup(); + copyDataIntoPosterLockup(objectGraph, data, lockup, options); + return lockup; + }); +} +/** + * + * @param data + * @param options + * @param videoConfiguration + * @returns {TrailersLockup} + */ +export function trailersLockupFromData(objectGraph, data, options, videoConfiguration) { + return validation.context("trailersLockupFromData", () => { + const lockup = new models.TrailersLockup(); + copyDataIntoTrailersLockup(objectGraph, data, lockup, videoConfiguration, options); + return lockup; + }); +} +/** + *Build a mixed media lockup from given data + * @param objectGraph + * @param data + * @param options + * @param videoConfiguration + * @param searchExperimentsData + * @param cropCode The crop code to use for the media + * @returns {MixedMediaLockup} + */ +export function mixedMediaLockupFromData(objectGraph, data, options, videoConfiguration, searchExperimentsData = null, cropCode) { + return validation.context("mixedMediaLockupFromData", () => { + const lockup = new models.MixedMediaLockup(); + copyDataIntoLockup(objectGraph, data, lockup, options, null, () => { + copyMetadataRibbonInfoIntoLockup(objectGraph, data, lockup, searchExperimentsData, options); + copyMediaIntoMixedMediaLockup(objectGraph, data, lockup, videoConfiguration, options, cropCode); + copyScreenshotsDisplayStyleIntoMixedMediaLockup(objectGraph, data, lockup, searchExperimentsData); + }); + return lockup; + }); +} +/** + * Create am image lockup for shelfContents to display within a grouping shelf + * @param objectGraph + * @param mediaApiData shelfContents to create lockup for. + * @param lockupOptions The options needed to customize this lockup + * @param collectionShelfDisplayStyle + */ +export function imageLockupFromData(objectGraph, mediaApiData, lockupOptions, collectionDisplayStyle) { + const isEditorialApiData = mediaApiData.type === "editorial-items"; + const lockupData = isEditorialApiData + ? mediaRelationship.relationshipData(objectGraph, mediaApiData, "primary-content") + : mediaApiData; + const lockup = lockupFromData(objectGraph, lockupData, lockupOptions); + // Determine which artwork to use, giving priority to an attached EI first, and then falling back + // to the lockup artwork. Currently we only use the attached EI on visionOS. + const attributePlatform = contentAttributes.bestAttributePlatformFromData(objectGraph, lockupData); + const lockupEditorialMediaData = editorialComponentMediaUtil.editorialMediaDataFromData(objectGraph, lockupData, collectionDisplayStyle); + const articleEditorialMediaData = editorialComponentMediaUtil.editorialMediaDataFromData(objectGraph, mediaApiData, collectionDisplayStyle); + const lockupArtwork = lockupEditorialMediaData === null || lockupEditorialMediaData === void 0 ? void 0 : lockupEditorialMediaData.artwork; + const articleArtwork = articleEditorialMediaData === null || articleEditorialMediaData === void 0 ? void 0 : articleEditorialMediaData.artwork; + let artwork; + let isDark; + if (isSome(articleArtwork) && objectGraph.client.isVision) { + isDark = isMediaDark(objectGraph, articleEditorialMediaData); + artwork = articleArtwork; + } + else if (isSome(lockupArtwork)) { + isDark = isMediaDark(objectGraph, lockupEditorialMediaData); + artwork = lockupArtwork; + } + if (isSome(artwork) && isSome(lockup)) { + const imageLockup = new models.ImageLockup(artwork, lockup, null, null, isDark); + imageLockup.caption = mediaPlatformAttributes.platformAttributeAsString(lockupData, attributePlatform, "editorialNotes.badge"); + if (isSome(imageLockup.caption) && objectGraph.client.isVision) { + imageLockup.caption = objectGraph.loc.uppercased(imageLockup.caption); + } + imageLockup.title = + mediaPlatformAttributes.platformAttributeAsString(lockupData, attributePlatform, "editorialNotes.tagline") || mediaAttributes.attributeAsString(lockupData, "genreDisplayName"); + imageLockup.impressionMetrics = lockup.impressionMetrics; + return imageLockup; + } + else { + return null; + } +} +function copyScreenshotsDisplayStyleIntoMixedMediaLockup(objectGraph, data, lockup, searchExperimentsData) { + var _a; + if (!objectGraph.client.isPhone) { + return; + } + const displayStyle = serverData.asString(data.meta, "imageLockupFromData"); + if (serverData.isDefinedNonNull(displayStyle)) { + lockup.screenshotsDisplayStyle = displayStyle; + } + else if (serverData.isDefinedNonNull((_a = searchExperimentsData === null || searchExperimentsData === void 0 ? void 0 : searchExperimentsData.displayStyle) === null || _a === void 0 ? void 0 : _a.screenshots)) { + lockup.screenshotsDisplayStyle = searchExperimentsData.displayStyle.screenshots; + } +} +/** + * Create a mixed media lockup, with some specific ad-related modifications, from the supplied data. + * @param objectGraph The App Store Object Graph. + * @param data The data for the app to go into the lockup. + * @param options A set of options customising the lockup. + * @param videoConfiguration A configuration object for any videos in the lockup. + * @param searchExperimentsData Data for any search results experiments being run currently. + * @param applyAdOfferDisplayProperties Whether to apply the default ad OfferDisplayProperties. Some callers of this function want to enforce their own offer styling. + * @returns A MixedMediaLockup with the desired configuration. + */ +export function mixedMediaAdLockupFromData(objectGraph, data, options, videoConfiguration, searchExperimentsData, applyAdOfferDisplayProperties = true) { + return validation.context("mixedMediaAdLockupFromData", () => { + const lockup = new models.MixedMediaLockup(); + if (!mediaAttributes.attributeAsBooleanOrFalse(data, "iad.format.images")) { + copyDataIntoLockup(objectGraph, data, lockup, options, null, () => { + copyMetadataRibbonInfoIntoLockup(objectGraph, data, lockup, searchExperimentsData, options); + }); + lockup.screenshots = []; + } + else { + copyMediaIntoMixedMediaLockup(objectGraph, data, lockup, videoConfiguration, options); + adLockup.performAssetOverridesForMixedMediaAdLockupIfNeeded(objectGraph, data, lockup, options.metricsOptions); + copyDataIntoLockup(objectGraph, data, lockup, options, null, () => { + copyScreenshotsDisplayStyleIntoMixedMediaLockup(objectGraph, data, lockup, searchExperimentsData); + copyMetadataRibbonInfoIntoLockup(objectGraph, data, lockup, searchExperimentsData, options); + }); + } + adLockup.performAdOverridesforLockup(objectGraph, data, lockup, options.metricsOptions, applyAdOfferDisplayProperties); + return lockup; + }); +} +/** + * Create an lockup that describes the arcade service lockup from upsell data. + * This lockup: + * - Has a title, e.g. "Apple Arcade". + * - May have a subtitle text, e.g. "Play 100+ games". + * - Does *NOT* have a `clickAction` on itself (There is no Arcade product page). + * - Has two actions for each subscription state. + */ +export function arcadeLockupFromData(objectGraph, upsellData, metricsOptions, context, offerStyle, offerEnvironment) { + return validation.context("arcadeLockupFromData", () => { + const data = upsellData.marketingItemData; + const lockup = new models.ArcadeLockup(); + lockup.title = objectGraph.loc.string("ARCADE_LOCKUP_TITLE", "Apple Arcade"); + const marketingItemData = upsellData.marketingItemData; + metricsOptions = { + ...metricsOptions, + mercuryMetricsData: metricsUtil.marketingItemTopLevelBaseFieldsFromData(objectGraph, marketingItemData), + }; + // This `name` attribute contains strings like "Some Games. \nAll you can play.". Weird. + // We trim newlines for arcade service footer lockup since the multiline text looks ugly. + let subtitle = arcadeUpsell.descriptionFromData(objectGraph, data); + if ((subtitle === null || subtitle === void 0 ? void 0 : subtitle.length) > 0) { + // This subtitle string on the upsell data blob is shared by many different views. However, editorial has a tendency to program explicit newlines, since some views look better with them (notably sheets). + // For the arcade lockup on some platforms, e.g. iOS, the string appears in a vertically constrained space and we ideally want to fit in 1 line if possible, We'll trim for now... Long term we need an editorial notes key + // for strings w/o newlines for this use case. + const platformIgnoresSubtitleNewlines = objectGraph.host.isiOS; + subtitle = platformIgnoresSubtitleNewlines ? subtitle.replace(/\n/g, " ") : subtitle; + lockup.nonsubscribedSubtitle = subtitle; + lockup.subscribedSubtitle = subtitle; + } + // Unsubscribed state action + let unsubscribedAction; + const unsubscribedActionTitle = arcadeUpsell.callToActionLabelFromData(objectGraph, data); + const arcadeLockupInNavBarEnabled = objectGraph.host.isiOS || objectGraph.host.isMac || objectGraph.client.isVision; + if (arcadeLockupInNavBarEnabled) { + // This is configured as an `ArcadeAction` directly if the pricing token is present. + unsubscribedAction = arcadeUpsell.arcadeOfferButtonActionFromData(objectGraph, upsellData.marketingItemData, context, metricsOptions); + unsubscribedAction.title = unsubscribedActionTitle; + } + else if ((unsubscribedActionTitle === null || unsubscribedActionTitle === void 0 ? void 0 : unsubscribedActionTitle.length) > 0) { + unsubscribedAction = arcadeCommon.arcadeSubscribePageFlowAction(objectGraph, models.marketingItemContextFromString("editorialItemCanvas"), null, null, { + ...metricsOptions, + id: data.id, + }); + unsubscribedAction.title = unsubscribedActionTitle; + } + else { + // If Upsell data is misconfigured and missing description, default to opening Arcade app for unsubscribed state. + unsubscribedAction = arcadeCommon.openArcadeMainAction(objectGraph, metricsOptions.pageInformation, metricsOptions.locationTracker); + if (preprocessor.GAMES_TARGET) { + unsubscribedAction.title = objectGraph.loc.string("OfferButton.Arcade.Title.Explore"); + } + else { + unsubscribedAction.title = objectGraph.loc.string("ARCADE_ACTION_TITLE_EXPLORE", "EXPLORE"); + } + } + lockup.unsubscribedButtonAction = unsubscribedAction; + // Subscribed state action + const subscribedAction = arcadeCommon.openArcadeMainAction(objectGraph, metricsOptions.pageInformation, metricsOptions.locationTracker); + if (preprocessor.GAMES_TARGET) { + subscribedAction.title = objectGraph.loc.string("OfferButton.Arcade.Title.Explore"); + } + else { + subscribedAction.title = objectGraph.loc.string("ARCADE_ACTION_TITLE_EXPLORE", "EXPLORE"); + } + lockup.subscribedButtonAction = subscribedAction; + // Impressions: + const metricsImpressionOptions = metricsHelpersImpressions.impressionOptions(objectGraph, upsellData.marketingItemData, lockup.title, metricsOptions); + metricsImpressionOptions.displaysArcadeUpsell = true; + // If no targetType is provided, set the correct value for the platform. + if (serverData.isNullOrEmpty(metricsImpressionOptions.targetType)) { + metricsImpressionOptions.targetType = objectGraph.client.isVision ? "lockupSmall" : "lockup"; + } + metricsHelpersImpressions.addImpressionFields(objectGraph, lockup, metricsImpressionOptions); + const displayProperties = new models.OfferDisplayProperties("arcade", objectGraph.bag.arcadeAppAdamId, null, offerStyle, null, offerEnvironment, null, null, null, null, null, null, null, null, null, null, null, null, objectGraph.bag.arcadeProductFamilyId); + if (preprocessor.GAMES_TARGET) { + displayProperties.titles["subscribed"] = objectGraph.loc.string("OfferButton.Arcade.Title.Explore"); + } + else { + displayProperties.titles["subscribed"] = objectGraph.loc.string("ARCADE_ACTION_TITLE_EXPLORE", "EXPLORE"); + } + lockup.offerDisplayProperties = displayProperties; + return lockup; + }); +} +export function lockupsFromDataContainer(objectGraph, dataContainer, options) { + if (serverData.isNull(dataContainer)) { + return []; + } + return lockupsFromData(objectGraph, dataContainer.data, options); +} +export function lockupsFromData(objectGraph, dataArray, options) { + return validation.context("lockupsFromData", () => { + var _a; + if (!dataArray) { + return []; + } + const items = []; + let isDeferring = false; + for (let index = 0; index < dataArray.length; index++) { + if (isDeferring) { + break; + } + const lockupData = dataArray[index]; + if (!mediaAttributes.hasAttributes(lockupData)) { + if (options.contentUnavailable) { + isDeferring = options.contentUnavailable(index, lockupData); + } + continue; + } + const lockupOptions = options.lockupOptions; + let filter = 80894 /* filtering.Filter.All */; + if (options.includeOrdinals) { + const ordinal = options.ordinalDirection === "descending" ? dataArray.length - index : index + 1; + lockupOptions.ordinal = objectGraph.loc.decimal(ordinal).toString(); + } + if (serverData.isDefinedNonNull(options.filter)) { + filter = options.filter; + } + if (filtering.shouldFilter(objectGraph, lockupData, filter) && !options.shouldShowOnUnsupportedPlatform) { + continue; + } + const lockup = ((_a = options.lockupOptions.shouldCreateScreenshotsLockup) !== null && _a !== void 0 ? _a : false) + ? screenshotsLockupFromData(objectGraph, lockupData, lockupOptions) + : lockupFromData(objectGraph, lockupData, lockupOptions); + if (serverData.isNull(lockup) || !lockup.isValid()) { + continue; + } + items.push(lockup); + metricsHelpersLocation.nextPosition(options.lockupOptions.metricsOptions.locationTracker); + } + return items; + }); +} +export function screenshotsLockupsFromData(objectGraph, dataArray, options) { + return validation.context("screenshotsLockupsFromData", () => { + if (!dataArray) { + return []; + } + const items = []; + for (let index = 0; index < dataArray.length; index++) { + const lockupData = dataArray[index]; + if (serverData.isNull(lockupData.attributes)) { + if (options.contentUnavailable) { + options.contentUnavailable(index, lockupData); + } + continue; + } + const lockupOptions = options.lockupOptions; + let filter = 80894 /* filtering.Filter.All */; + if (serverData.isDefinedNonNull(options.filter)) { + filter = options.filter; + } + if (filtering.shouldFilter(objectGraph, lockupData, filter)) { + continue; + } + const lockup = screenshotsLockupFromData(objectGraph, lockupData, lockupOptions); + if (!lockup.isValid()) { + continue; + } + items.push(lockup); + metricsHelpersLocation.nextPosition(options.lockupOptions.metricsOptions.locationTracker); + } + return items; + }); +} +/** + * Create an action for the provided data + * @param objectGraph The App Store Object Graph. + * @param data The data to create the action for. + * @param metricsOptions The metrics options to use for the action. + * @param clientIdentifierOverride A client identifier override, if any. + * @param externalDeepLinkUrl A custom deeplink URL, if any. + * @param isCppDeepLinkEligible Whether the action should be eligible for CPP Deep Links, if any. Used for restricting deep links on ads. + * @returns A configured action for the provided data. + */ +export function actionFromData(objectGraph, data, metricsOptions, clientIdentifierOverride, externalDeepLinkUrl = null, isCppDeepLinkEligible) { + return validation.context(`actionFromData: ${data.type}`, () => { + switch (data.type) { + case "apps": + case "app-bundles": { + return productActionFromData(objectGraph, data, metricsOptions, { + clientIdentifierOverride: clientIdentifierOverride, + externalDeepLinkUrl: externalDeepLinkUrl, + isCppDeepLinkEligible: isCppDeepLinkEligible, + }); + } + case "in-apps": { + return iAPActionFromData(objectGraph, data, metricsOptions); + } + case "editorial-items": { + return editorialItemActionFromData(objectGraph, data, metricsOptions, clientIdentifierOverride); + } + case "tags": + case "editorial-pages": { + return editorialPageActionFromData(objectGraph, data, metricsOptions); + } + case "multiple-system-operators": + return msoActionFromData(objectGraph, data, metricsOptions); + case "groupings": + return groupingActionFromData(objectGraph, data, metricsOptions); + case "developers": + default: { + return genericActionFromData(objectGraph, data, metricsOptions); + } + } + }); +} +function iAPActionFromData(objectGraph, data, options) { + return validation.context("iAPActionFromData", () => { + var _a; + const parentData = parentDataFromInAppData(objectGraph, data); + if (!parentData) { + return null; + } + const clickAction = new models.FlowAction("product"); + const parentUrl = urls.URL.from(mediaAttributes.attributeAsString(parentData, "url")); + // Attach the productVariantID/cppId/ppid to the parent URL, if it exists. + const productVariantData = (_a = options.productVariantData) !== null && _a !== void 0 ? _a : variants.productVariantDataForData(objectGraph, parentData); + const productVariantID = variants.productVariantIDForVariantData(productVariantData); + if (serverData.isDefinedNonNull(productVariantID)) { + parentUrl.param(Parameters.productVariantID, productVariantID); + } + const parentUrlString = parentUrl.toString(); + const inAppProductIdentifier = mediaAttributes.attributeAsString(data, "offerName"); + const isSubscription = mediaAttributes.attributeAsBooleanOrFalse(data, "isSubscription"); + clickAction.pageUrl = configureProductUrlFromInAppPurchase(objectGraph, parentUrlString, inAppProductIdentifier, isSubscription); + clickAction.title = mediaAttributes.attributeAsString(data, "name"); + metricsHelpersClicks.addClickEventToAction(objectGraph, clickAction, options); + return clickAction; + }); +} +export function editorialItemActionTypeFromData(objectGraph, data) { + if (serverData.isNullOrEmpty(data)) { + return 0 /* EditorialItemActionType.Unknown */; + } + return validation.context("editorialItemActionFromData", () => { + const link = mediaAttributes.attributeAsDictionary(data, "link"); + if (linkIsExternal(link)) { + return 1 /* EditorialItemActionType.ExternalLink */; + } + const substyle = mediaAttributes.attributeAsString(data, "displaySubStyle"); + const isListArticle = substyle === "List" || substyle === "NumberedList"; + if (mediaAttributes.attributeAsBooleanOrFalse(data, "isCanvasAvailable") || isListArticle) { + return 2 /* EditorialItemActionType.Article */; + } + // This is a bit of a workaround to get the product data from `primary-content`, if it's available, for app events + // as this is where it's currently made available for an EI. + const primaryContent = mediaRelationship.relationshipCollection(data, "primary-content"); + const cardContents = mediaRelationship.relationshipCollection(data, "card-contents"); + const isPrimaryContentProduct = serverData.isDefinedNonNullNonEmpty(primaryContent) && primaryContent.length === 1; + const isProduct = serverData.isDefinedNonNullNonEmpty(cardContents) && cardContents.length === 1; + if ((isProduct && cardContents[0].type === "app-events") || + (isPrimaryContentProduct && primaryContent[0].type === "app-events")) { + return 4 /* EditorialItemActionType.AppEvent */; + } + if (isProduct) { + return 3 /* EditorialItemActionType.Product */; + } + if ((objectGraph.client.isVision || preprocessor.GAMES_TARGET) && isSome(data.href) && data.href.length > 0) { + return 5 /* EditorialItemActionType.Href */; + } + return 0 /* EditorialItemActionType.Unknown */; + }); +} +/** + * Create an action for an editorial story + * @param objectGraph The App Store Object Graph. + * @param data The data for the editorial story + * @param options A set of options customising the lockup. + * @param clientIdentifierOverride A client identifier override. + * @param articleRecoMetricsData The recommendation metrics data for the editorial item + * @param todayCardConfig The config options for the editorial Item + * @param canvasFilter The canvas filter for this editorial item, used to fetch a specific story + * @returns A click action for an editorial item + */ +export function editorialItemActionFromData(objectGraph, data, options, clientIdentifierOverride, articleRecoMetricsData, todayCardConfig) { + if (serverData.isNullOrEmpty(data)) { + return null; + } + return validation.context("editorialItemActionFromData", () => { + var _a; + let flowDestination; + let pageUrlString; + let destinationIntent; + let presentation; + switch (editorialItemActionTypeFromData(objectGraph, data)) { + case 1 /* EditorialItemActionType.ExternalLink */: + return editorialItemExternalLinkActionFromData(objectGraph, data, options); + case 2 /* EditorialItemActionType.Article */: + flowDestination = "article"; + const pageUrl = urls.URL.from(mediaAttributes.attributeAsString(data, "url")); + if (serverData.isDefinedNonNull(articleRecoMetricsData)) { + pageUrl.param(Parameters.recoMetrics, JSON.stringify(articleRecoMetricsData)); + } + if (objectGraph.client.isiOS && isSome(todayCardConfig) && !todayCardConfig.isHorizontalShelfContext) { + pageUrl.param(Parameters.todayCardConfig, JSON.stringify(todayCardConfig)); + } + const editorialCardId = (_a = editorialCardFromData(data)) === null || _a === void 0 ? void 0 : _a.id; + if (isSome(editorialCardId)) { + pageUrl.param(Parameters.editorialCardId, editorialCardId); + } + pageUrlString = pageUrl.build(); + if (objectGraph.client.isWeb) { + destinationIntent = makeRoutableArticlePageIntent({ + ...getLocale(objectGraph), + ...getPlatform(objectGraph), + id: data.id, + }); + pageUrlString = makeRoutableArticlePageCanonicalUrl(objectGraph, destinationIntent); + } + if (preprocessor.GAMES_TARGET) { + // For Luck Seed1 we always show stories as modal across all platform, + // in the future we should follow App Store and push them on Mac, + // according to the specs https://quip-apple.com/CEplADSB1MJR + // Need to support proper dual column layout first: + // rdar://141251194 ([Seed 3] [Stories] Dynamic page layout (single and dual column)) + if (objectGraph.host.isMac) { + presentation = "stackPush"; + } + else { + presentation = "sheetPresent"; + } + } + break; + case 3 /* EditorialItemActionType.Product */: + const productData = mediaRelationship.relationshipCollection(data, "card-contents")[0]; + return actionFromData(objectGraph, productData, options, clientIdentifierOverride); + case 4 /* EditorialItemActionType.AppEvent */: + // This is a bit of a workaround to get the product data from `primary-content`, if it's available, for app events + // as this is where it's currently made available for an EI. + const primaryContent = mediaRelationship.relationshipCollection(data, "primary-content"); + const isPrimaryContentProduct = serverData.isDefinedNonNullNonEmpty(primaryContent) && primaryContent.length === 1; + const appEventData = isPrimaryContentProduct + ? primaryContent[0] + : mediaRelationship.relationshipCollection(data, "card-contents")[0]; + const eventProductData = mediaRelationship.relationshipData(objectGraph, appEventData, "app"); + if (serverData.isNullOrEmpty(eventProductData)) { + return null; + } + const appEventOrDate = appEvent.appEventOrPromotionStartDateFromData(objectGraph, appEventData, eventProductData, false, false, "dark", "infer", false, options, false, true, null, false, false); + // Return early if we received a Date, as this means the App Event shouldn't be accessible yet. + if (serverData.isNull(appEventOrDate) || appEventOrDate instanceof Date) { + return null; + } + return appPromotionsCommon.detailPageClickActionFromData(objectGraph, appEventData, eventProductData, appEventOrDate, options, true); + case 5 /* EditorialItemActionType.Href */: + flowDestination = "page"; + pageUrlString = mediaUrlMapping.hrefToRoutableUrl(objectGraph, data.href); + break; + default: + flowDestination = "unknown"; + const link = mediaAttributes.attributeAsDictionary(data, "link"); + if (objectGraph.client.isWeb) { + destinationIntent = makeEditorialPageIntentByID({ + ...getLocale(objectGraph), + ...getPlatform(objectGraph), + id: data.id, + }); + pageUrlString = makeEditorialPageURL(objectGraph, destinationIntent); + } + else { + pageUrlString = serverData.asString(link, "url"); + } + } + if (isNothing(pageUrlString)) { + return null; + } + const action = new models.FlowAction(flowDestination); + action.pageUrl = pageUrlString; + if (isSome(presentation)) { + action.presentation = presentation; + } + let title = content.notesFromData(objectGraph, data, "name"); + if (serverData.isNull(title)) { + title = serverData.asString(data, "label"); + } + action.title = title; + if (destinationIntent) { + action.destination = destinationIntent; + } + metricsHelpersClicks.addClickEventToAction(objectGraph, action, options); + return action; + }); +} +/** + * Creates a flow action to display a product page. + * @param objectGraph Dependency soup + * @param data The media api data to create the action from. + * @param metricsOptions Metrics dependencies + * @param options.clientIdentifierOverride The preferred client identifier to use for the product page. + * @param options.externalDeepLinkUrl An external deep link for the propuct to be used when opening the app, if any. + * @param options.isCppDeepLinkEligible Whether a CPP deep link can be used for this lockup, if available. + * @param options.productVariantData A variant to use. This can be populated as an optimization to avoid re-resolving the same variant data, e.g. in a product page. + * @returns A `FlowAction` object pointing to a product page. + */ +function productActionFromData(objectGraph, data, metricsOptions, options) { + var _a, _b, _c, _d; + if (!data) { + return null; + } + const clientIdentifierOverride = (_a = options.clientIdentifierOverride) !== null && _a !== void 0 ? _a : null; + const externalDeepLinkUrl = (_b = options.externalDeepLinkUrl) !== null && _b !== void 0 ? _b : null; + const isCppDeepLinkEligible = (_c = options.isCppDeepLinkEligible) !== null && _c !== void 0 ? _c : false; + const isCppDeepLinkDisabled = !isCppDeepLinkEligible; + const productVariantData = (_d = options.productVariantData) !== null && _d !== void 0 ? _d : variants.productVariantDataForData(objectGraph, data); + return validation.context("productActionFromData", () => { + var _a, _b, _c; + let productUrl = mediaAttributes.attributeAsString(data, "url"); + if (!productUrl) { + validation.unexpectedNull("ignoredValue", "string", "url"); + return null; + } + let productPageOptions = {}; + const url = new urls.URL(productUrl); + if (metricsOptions.isAdvert) { + const lineItem = serverData.asString(data, "iad.lineItem"); + if (lineItem !== null && lineItem.length > 0) { + url.param(metricsHelpersModels.iAdURLLineItemParameterStringToken, lineItem); + } + const iAdClickFields = (_a = metricsOptions.pageInformation.iAdInfo) === null || _a === void 0 ? void 0 : _a.clickFields; + url.param(metricsHelpersModels.iAdURLParameterStringToken, JSON.stringify(iAdClickFields)); + productPageOptions = { + iAdClickFields: serverData.asJSONData(iAdClickFields), + iAdLineItem: lineItem, + }; + const instanceId = adCommon.advertInstanceIdForData(objectGraph, data); + if (isSome(instanceId)) { + const advertType = content.isArcadeSupported(objectGraph, data) + ? "arcadeApp" + : "standardApp"; + const reportingDestination = reportingDestinationFromMetricsOptions(objectGraph, metricsOptions.pageInformation); + const bundleId = mediaAttributes.attributeAsString(data, "platformAttributes.ios.bundleId"); + const isPreorder = mediaAttributes.attributeAsBooleanOrFalse(data, "isPreorder"); + const purchaseType = isPreorder ? "preorder" : "standard"; + const dismissAdActionMetrics = new AdvertActionMetrics(instanceId, data.id, bundleId, advertType, "productPageDismissed", purchaseType, reportingDestination); + url.param(metricsHelpersModels.iAdDismissAdActionMetricsParameterStringToken, JSON.stringify(dismissAdActionMetrics)); + productPageOptions.iAdDismissAdActionMetrics = serverData.asJSONData(dismissAdActionMetrics); + } + } + const productVariantID = variants.productVariantIDForVariantData(productVariantData); + if (serverData.isDefinedNonNull(productVariantID)) { + url.param(Parameters.productVariantID, productVariantID); + } + if (preprocessor.CARRY_BUILD || preprocessor.DEBUG_BUILD) { + if (objectGraph.featureFlags.isEnabled("aligned_region_artwork_2025A")) { + const alignedRegionDeeplinkUrl = adLockup.getCustomCreativeDeepLinkUrl(data); + if (isSome(alignedRegionDeeplinkUrl)) { + url.param(externalDeepLink.alignedRegionDeepLinkQueryParameter, alignedRegionDeeplinkUrl); + } + else { + const tapDestinationCppId = adLockup.getTapDestinationIdForAdvert(data); + if (isSome(tapDestinationCppId)) { + url.param(Parameters.productVariantID, tapDestinationCppId); + } + } + } + } + productPageOptions.externalDeepLinkUrl = externalDeepLinkUrl; + if (serverData.isDefinedNonNull(externalDeepLinkUrl)) { + url.param(externalDeepLink.externalDeepLinkQueryParameter, externalDeepLinkUrl); + } + const platformFromIntent = getPlatform(objectGraph).platform; + const platformInferredFromData = safelyInferPlatformFromData(objectGraph, data); + if (objectGraph.client.isWeb) { + // For the web client, we add the platform query parameter if the active intent's platform + // differs from the "default" platform inferred from the app's data. + if (platformFromIntent && platformFromIntent !== platformInferredFromData) { + url.param("platform", platformFromIntent); + } + } + else { + // For non-web clients, propagate CPP, search term, and client identifier parms, which are not needed for web. + productPageOptions.isCppDeepLinkDisabled = isCppDeepLinkDisabled; + url.param(externalDeepLink.cppDeepLinkDisabledQueryParameter, isCppDeepLinkDisabled.toString()); + // Add a searchTerm to the product URL to propagate to purchases on the subsequent product page. + const searchTerm = (_c = (_b = metricsOptions.pageInformation) === null || _b === void 0 ? void 0 : _b.searchTermContext) === null || _c === void 0 ? void 0 : _c.term; + if (isSome(searchTerm)) { + url.param("searchTerm", searchTerm); + } + productPageOptions.clientIdentifierOverride = clientIdentifierOverride; + if ((clientIdentifierOverride === null || clientIdentifierOverride === void 0 ? void 0 : clientIdentifierOverride.length) > 0) { + url.param("clientIdentifierOverride", clientIdentifierOverride); + } + } + productUrl = url.toString(); + if (preprocessor.GAMES_TARGET) { + return gameModels.viewGameActionWithMediaAPIData(data, objectGraph, makeClickMetrics(objectGraph, data.id, "lockup", "navigate", [data.id])); + } + const action = new models.FlowAction("product"); + action.pageUrl = productUrl; + action.pageData = productPageUtil.createProductPageSidePackFromResponse(objectGraph, data, productPageOptions); + action.title = mediaAttributes.attributeAsString(data, "name"); + if (metricsOptions && metricsOptions.pageInformation) { + action.referrerUrl = metricsOptions.pageInformation.pageUrl; + } + // Extract and add additional options for pre-orders + const isPreorder = mediaAttributes.attributeAsBooleanOrFalse(data, "isPreorder"); + if (isPreorder) { + metricsHelpersClicks.addClickEventToAction(objectGraph, action, { + ...metricsOptions, + offerType: "preorder", + offerReleaseDate: offers.expectedReleaseDateFromData(objectGraph, data), + }, true); + } + else { + metricsHelpersClicks.addClickEventToAction(objectGraph, action, metricsOptions, true); + } + const productId = serverData.asString(data, "id"); + if (isSome(productId)) { + action.destination = + data.type === "app-bundles" + ? makeBundlePageIntent({ + ...getLocale(objectGraph), + id: productId, + }) + : makeProductPageIntent({ + ...getLocale(objectGraph), + platform: inferPreviewPlatform(objectGraph, data), + id: productId, + }); + } + return action; + }); +} +/** + * Safely infers a preview platform from media data, handling any potential errors. + */ +function safelyInferPlatformFromData(objectGraph, data) { + try { + return inferPreviewPlatformFromDeviceFamilies(objectGraph, { data: [data] }); + } + catch (error) { + objectGraph.console.error(`Error inferring preview platform from data: ${error}`); + return undefined; + } +} +function inferPreviewPlatform(objectGraph, data) { + const platformFromIntent = getPlatform(objectGraph).platform; + if (platformFromIntent) { + return platformFromIntent; + } + return safelyInferPlatformFromData(objectGraph, data); +} +/** + * Creates a flow action to display a grouping page + * @param data The media api data to create the action from. + * @returns A `FlowAction` object pointing to a grouping page. + */ +function groupingActionFromData(objectGraph, data, options) { + if (!data) { + return null; + } + return validation.context("groupingActionFromData", () => { + if (!data.href) { + validation.unexpectedNull("ignoredValue", "string", "href"); + return null; + } + const action = new models.FlowAction("page"); + if (objectGraph.client.isWeb) { + const destination = makeGroupingPageIntentByID({ + ...getLocale(objectGraph), + ...getPlatform(objectGraph), + id: data.id, + }); + action.destination = destination; + action.pageUrl = makeGroupingPageCanonicalURL(objectGraph, destination); + } + else { + action.pageUrl = mediaUrlBuilder + .buildURLFromRequest(objectGraph, mediaUrlMapping.mediaApiGroupingURLFromHref(objectGraph, data.href)) + .toString(); + } + action.title = nameFromGroupingData(objectGraph, data); + metricsHelpersClicks.addClickEventToAction(objectGraph, action, options); + return action; + }); +} +/** + * Creates a flow action to display a mso-page. + * @param data The media api data to create the action from. + * @returns A `FlowAction` object pointing to a mso page. + */ +function msoActionFromData(objectGraph, data, options) { + if (!data) { + return null; + } + return validation.context("msoActionFromPlatformData", () => { + const roomUrl = mediaAttributes.attributeAsString(data, "url"); + if (!roomUrl) { + validation.unexpectedNull("ignoredValue", "string", "url"); + return null; + } + const action = new models.FlowAction("mso"); + action.pageUrl = roomUrl; + action.title = mediaAttributes.attributeAsString(data, "name"); + metricsHelpersClicks.addClickEventToAction(objectGraph, action, options); + return action; + }); +} +/** + * Creates a flow action to display an editorial page. + * + * editorial-pages asset type does not have a url attribute, so we have to synthesize one + * - The "web" client uses the URL defined by the `EditorialPageIntentController` + * - Other clients use the `href` attribute + * + * @param data The media api data to create the action from. + * @param options The required options passed to the lockup creation method. + * @returns A `FlowAction` object pointing to a product page. + */ +function editorialPageActionFromData(objectGraph, data, options) { + if (!data) { + return null; + } + return validation.context("editorialPageActionFromData", () => { + const action = new models.FlowAction("page"); + if (objectGraph.client.isWeb) { + const editorialPageIntent = makeEditorialPageIntentByID({ + ...getLocale(objectGraph), + ...getPlatform(objectGraph), + id: data.id, + }); + action.destination = editorialPageIntent; + action.pageUrl = makeEditorialPageURL(objectGraph, editorialPageIntent); + } + else { + const href = data.href; + if (serverData.isNullOrEmpty(href)) { + validation.unexpectedNull("ignoredValue", "string", "href"); + return null; + } + action.pageUrl = mediaUrlMapping.hrefToRoutableUrl(objectGraph, href); + } + action.title = content.editorialNotesFromData(objectGraph, data, "name"); + if (serverData.isNullOrEmpty(action.title)) { + action.title = mediaAttributes.attributeAsString(data, "name"); + } + metricsHelpersClicks.addClickEventToAction(objectGraph, action, options); + return action; + }); +} +/** + * Creates a flow action to display a generic page + * @param data The media api data to create the action from. + * @returns A `FlowAction` object pointing to a product page. + */ +function genericActionFromData(objectGraph, data, options) { + if (!data) { + return null; + } + return validation.context("genericActionFromData", () => { + const type = serverData.asString(data, "type"); + const url = mediaAttributes.attributeAsString(data, "url"); + if (!url) { + validation.unexpectedNull("ignoredValue", "string", "url"); + return null; + } + const action = new models.FlowAction("page"); + action.pageUrl = url; + if (type === "groupings") { + action.title = nameFromGroupingData(objectGraph, data); + } + else { + action.title = mediaAttributes.attributeAsString(data, "name"); + } + metricsHelpersClicks.addClickEventToAction(objectGraph, action, options); + return action; + }); +} +/** + * Whether or not link blob is to external url. + * @param link JSON blob for 'link' + * @returns boolean whether url is external link. + */ +function linkIsExternal(link) { + const target = serverData.asString(link, "target"); + return target && target === "external"; +} +/** + * Create an `ExternalUrlAction` for an External Link card. + * The title of the action is either the url domain or provided short description. + * @param data Media Api data + * @returns A configured `ExternalUrlAction` for card. + */ +export function editorialItemExternalLinkActionFromData(objectGraph, data, options) { + return validation.context("editorialItemExternalLinkActionFromData", () => { + const link = mediaAttributes.attributeAsDictionary(data, "link"); + const urlString = serverData.asString(link, "url"); + const action = new models.ExternalUrlAction(urlString); + // Title is short description or url domain + const linkDescription = content.notesFromData(objectGraph, data, "short"); + if (linkDescription) { + action.title = linkDescription; + } + else { + const url = new urls.URL(urlString); + action.title = url.host; + } + metricsHelpersClicks.addClickEventToAction(objectGraph, action, options); + return action; + }); +} +/** + * Creates a shallow copy of the provided lockups array, overriding any of the properties + * supplied. This function makes sure to override the properties non-destructively; that + * is, the overrides are only applied to the copied array's lockups. + * @param lockups The lockups to copy. + * @param overrideStyle The style to apply for the copied lockups' offerDisplayProperties. + * @param filterAds Whether to filter ads. In some places where we add ads to a set of lockups, showing that ad in a copied location isn't supported. + * @returns {Lockup[]} + */ +export function shallowCopyLockupsOverridingProperties(objectGraph, lockups, overrideStyle, filterAds = false) { + var _a, _b; + if (!lockups) { + return null; + } + // Store a count of the removed ads so we know how many items we've filtered from the beginning of the list of lockups. + let removedAdCount = 0; + const copies = []; + for (const lockup of lockups) { + if (filterAds && serverData.isDefinedNonNull((_a = lockup.searchAd) !== null && _a !== void 0 ? _a : (_b = lockup.searchAdOpportunity) === null || _b === void 0 ? void 0 : _b.searchAd)) { + removedAdCount += 1; + continue; + } + const copy = objects.shallowCopyOf(lockup); + if (overrideStyle && copy.offerDisplayProperties) { + copy.offerDisplayProperties = copy.offerDisplayProperties.newOfferDisplayPropertiesChangingAppearance(false, overrideStyle); + } + /// Adjust the impression index of the copy by the number of ads that have been removed prior to this item. + if (removedAdCount > 0) { + // Take a copy of the impressionMetrics.fields, as the copy is only shallow and we don't want to affect the original. + const impressionMetricsFields = objects.shallowCopyOf(copy.impressionMetrics.fields); + const impressionIndex = serverData.asNumber(impressionMetricsFields.impressionIndex); + if (isSome(impressionIndex)) { + impressionMetricsFields.impressionIndex = impressionIndex - removedAdCount; + copy.impressionMetrics = new models.ImpressionMetrics(impressionMetricsFields, copy.impressionMetrics.id, copy.impressionMetrics.custom); + } + } + copies.push(copy); + } + return copies; +} +/** + * Generates a name from a grouping media api resource + * @param data The grouping data from a genre response + * @return {string} A string of the grouping page's name + */ +export function nameFromGroupingData(objectGraph, data) { + const genreNames = mediaAttributes.attributeAsArrayOrEmpty(data, "genreNames"); + if (serverData.isDefinedNonNullNonEmpty(genreNames)) { + return genreNames[0]; + } + else { + return mediaAttributes.attributeAsString(data, "name"); + } +} +export function deviceHasCapabilitiesFromData(objectGraph, data) { + if (!data) { + return false; + } + if (objectGraph.client.isWatch || objectGraph.client.isWeb || objectGraph.client.isCompanionVisionApp) { + // For the "watch" client, opt out of capability checks until we land <rdar://47322408>. + // For the "web" client, we treat it like it can run anything, since we can't infer actual capabilites. + // For the companion app, we can't call down to the remote device to check its capabilities, + // so we defer this check to later on. + return true; + } + const requiredCapabilitiesString = content.requiredCapabilitiesFromData(objectGraph, data, objectGraph.appleSilicon.isSupportEnabled); + if (serverData.isNullOrEmpty(requiredCapabilitiesString)) { + // If we don't have any capabilities, this is vacuously true. + return true; + } + const splitCapabilities = requiredCapabilitiesString.split(" "); + const supportsVisionOSCompatibleIOSBinary = content.supportsVisionOSCompatibleIOSBinaryFromData(objectGraph, data); + return objectGraph.client.deviceHasCapabilitiesIncludingCompatibilityCheckIsVisionOSCompatibleIOSApp(splitCapabilities, supportsVisionOSCompatibleIOSBinary); +} +/** + * Finds the IAP data from an app promotion + * @param data The app promotion data + * @returns data of the IAP + */ +export function iapDataFromData(objectGraph, data) { + if (!data) { + return null; + } + switch (data.type) { + case "contingent-items": + return mediaRelationship.relationshipData(objectGraph, data, "branch"); + case "offer-items": + return mediaRelationship.relationshipData(objectGraph, data, "salables"); + default: + break; + } + return data; +} +/** + * Finds the discounted offer data from an IAP. + * @param data The IAP data + * @returns the discounted offer + */ +export function discountedOfferFromData(data) { + if (!data) { + return null; + } + const contingentOffer = serverData.asDictionary(data, "meta.contingentItemOffer"); + if (serverData.isDefinedNonNullNonEmpty(contingentOffer)) { + return contingentOffer; + } + const winbackOffer = serverData.asDictionary(data, "meta.discountOffer"); + if (serverData.isDefinedNonNullNonEmpty(winbackOffer)) { + return winbackOffer; + } + return null; +} +/** + * Finds the parent app data from a app promotion. + * @param data The contingent-offer or offer-item data + * @returns the parent app data + */ +export function parentDataFromInAppData(objectGraph, data) { + if (!data) { + return null; + } + switch (data.type) { + case "contingent-items": + return mediaRelationship.relationshipData(objectGraph, data, "branch-app"); + case "offer-items": + const iapData = mediaRelationship.relationshipData(objectGraph, data, "salables"); + return mediaRelationship.relationshipData(objectGraph, iapData, "app"); + default: + break; + } + return mediaRelationship.relationshipData(objectGraph, data, "app"); +} +export function cleanupArcadeDownloadPackLockupMetricsIfNeeded(lockup, objectGraph) { + if (!objectGraph.bag.arcadeDownloadPacksMetricsEventsEnabled) { + lockup.clickAction.actionMetrics.clearAll(); + lockup.buttonAction.actionMetrics.clearAll(); + if (lockup.buttonAction instanceof models.OfferStateAction) { + lockup.buttonAction.defaultAction.actionMetrics.clearAll(); + } + } + if (!objectGraph.bag.arcadeDownloadPacksImpressionEventsEnabled) { + lockup.impressionMetrics = null; + } +} +//# sourceMappingURL=lockups.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/metrics/builder.js b/node_modules/@jet-app/app-store/tmp/src/common/metrics/builder.js new file mode 100644 index 0000000..ffb205f --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/metrics/builder.js @@ -0,0 +1,257 @@ +import { isSome } from "@jet/environment"; +import { isNothing } from "@jet/environment/types/optional"; +import { AppStoreMetricsData, } from "../../api/models/metrics/metrics"; +import * as serverData from "../../foundation/json-parsing/server-data"; +import * as client from "../../foundation/wrappers/client"; +import { EventLinter } from "./event-linter"; +import { optOutOfLegacyMetricsIdFieldsProvider } from "./helpers/legacy-metrics-identifier-fields-opt-out"; +export function createMetricsClickData(objectGraph, targetId, targetType, eventFields, additionalIncludingFields, isDefaultBrowser) { + const fields = {}; + Object.assign(fields, eventFields); + fields["eventType"] = "click"; + fields["targetType"] = targetType; + fields["targetId"] = targetId; + const include = ["impressionsSnapshot", "pageFields"]; + if (!preprocessor.GAMES_TARGET) { + include.push("contentRestrictionReasons"); + } + if (additionalIncludingFields) { + include.push(...additionalIncludingFields); + } + addWebClientEventFields(objectGraph, fields); + addAltAb2DataToEventFields(objectGraph, fields); + return optOutOfLegacyMetricsIdFieldsProvider(objectGraph, new AppStoreMetricsData(fields, include, [], topicFromEventFields(objectGraph, fields), shouldFlushFromEventFields(objectGraph, fields, null, isDefaultBrowser))); +} +export function createMetricsBackClickData(objectGraph, eventFields) { + const fields = {}; + Object.assign(fields, eventFields); + fields["actionType"] = "back"; + const clickData = createMetricsClickData(objectGraph, "back", "button", fields); + return clickData; +} +export function createMetricsPageData(objectGraph, isReferralEligible, isCrossfireReferralCandidate, timingMetrics, eventFields, isDefaultBrowser) { + const fields = {}; + Object.assign(fields, eventFields); + fields["eventType"] = "page"; + if (timingMetrics) { + fields["clientCorrelationKey"] = timingMetrics.clientCorrelationKey; + fields["requestStartTime"] = timingMetrics.requestStartTime; + fields["responseStartTime"] = timingMetrics.responseStartTime; + fields["responseEndTime"] = timingMetrics.responseEndTime; + } + const include = ["pageFields", "pageReferrer"]; + if (!preprocessor.GAMES_TARGET) { + include.push("userContentRestriction"); + } + if (isReferralEligible) { + include.push("crossfireReferral"); + } + else if (isCrossfireReferralCandidate) { + include.push("crossfireReferralCandidate"); // Only possible when not crossfire eligible. + } + addAltAb2DataToEventFields(objectGraph, fields); + addWebClientEventFields(objectGraph, fields); + return optOutOfLegacyMetricsIdFieldsProvider(objectGraph, new AppStoreMetricsData(fields, include, [], topicFromEventFields(objectGraph, fields), shouldFlushFromEventFields(objectGraph, fields, null, isDefaultBrowser))); +} +export function createMetricsSearchData(objectGraph, term, target, actionType, actionUrl, eventFields, additionalIncludingFields) { + const fields = {}; + Object.assign(fields, eventFields); + fields["term"] = term; + fields["targetType"] = target; + fields["actionType"] = actionType; + if (actionUrl) { + fields["actionUrl"] = actionUrl; // actionUrl is defined for `hints` but not for searches fired from elsewhere. + } + fields["eventType"] = "search"; + const include = ["pageReferrer"]; + if (additionalIncludingFields) { + include.push(...additionalIncludingFields); + } + addWebClientEventFields(objectGraph, fields); + addAltAb2DataToEventFields(objectGraph, fields); + return optOutOfLegacyMetricsIdFieldsProvider(objectGraph, new AppStoreMetricsData(fields, include, [], topicFromEventFields(objectGraph, fields), shouldFlushFromEventFields(objectGraph, fields))); +} +export function createMetricsPageRenderFields(objectGraph, timingMetrics, eventFields) { + const fields = {}; + Object.assign(fields, eventFields); + fields["eventType"] = "pageRender"; + if (timingMetrics) { + if (!fields["pageUrl"]) { + fields["pageUrl"] = timingMetrics.pageURL; + } + fields["clientCorrelationKey"] = timingMetrics.clientCorrelationKey; + fields["platformRequestStartTime"] = timingMetrics.requestStartTime; + fields["platformResponseStartTime"] = timingMetrics.responseStartTime; + fields["platformResponseEndTime"] = timingMetrics.responseEndTime; + fields["platformResponseWasCached"] = timingMetrics.responseWasCached; + fields["platformJsonParseStartTime"] = timingMetrics.parseStartTime; + fields["platformJsonParseEndTime"] = timingMetrics.parseEndTime; + } + addWebClientEventFields(objectGraph, fields); + addAltAb2DataToEventFields(objectGraph, fields); + return fields; +} +/** + * Create a metrics data for regular impressions. + * @param objectGraph The dependency graph for the App Store. + * @param eventFields Base fields to build off on. + * @param shouldIncludeAdFieldsForPad Whether or not metrics data should include iPad related fields for adverts. + * @param includeAdRotationFields Whether or not metrics data should include advert related fields. + * @param hasImpressionsAppendix Whether condensed format search results should track appendix data. + */ +export function createMetricsImpressionsData(objectGraph, eventFields, shouldIncludeAdFieldsForPad, includeAdRotationFields, hasImpressionsAppendix) { + const fields = {}; + Object.assign(fields, eventFields); + fields["eventType"] = "impressions"; + fields["impressionQueue"] = "data-metrics"; + fields["eventVersion"] = 4; + const include = ["impressions", "pageFields", "pageReferrer"]; + if (!preprocessor.GAMES_TARGET) { + include.push("contentRestrictionReasons"); + } + if (shouldIncludeAdFieldsForPad) { + include.push("advertDeviceWindow"); + } + if (includeAdRotationFields) { + include.push("advertRotation"); + } + if (hasImpressionsAppendix) { + include.push("impressionsAppendix"); + } + addWebClientEventFields(objectGraph, fields); + addAltAb2DataToEventFields(objectGraph, fields); + return optOutOfLegacyMetricsIdFieldsProvider(objectGraph, new AppStoreMetricsData(fields, include, ["eventVersion"], topicFromEventFields(objectGraph, fields), shouldFlushFromEventFields(objectGraph, fields))); +} +/** + * Create a metrics data for fast impressions, a special type for SearchAds on Search Page. + * Included fields assume fields are for search page w/ Ads. + * @param baseFields Base fields to build off on. + */ +export function createMetricsFastImpressionsData(objectGraph, baseFields, pageInformation) { + var _a, _b; + const shouldIncludeAdFieldsForPad = serverData.isDefinedNonNull(pageInformation.iAdInfo) && + objectGraph.client.isPad && + (isNothing(pageInformation.iAdInfo.missedOpportunityReason) || + pageInformation.iAdInfo.missedOpportunityReason.length === 0); + const shouldIncludeAdRotationFields = (_b = (_a = pageInformation.iAdInfo) === null || _a === void 0 ? void 0 : _a.shouldIncludeAdRotationFields) !== null && _b !== void 0 ? _b : false; + const exclude = []; + const fields = createMetricsImpressionsData(objectGraph, baseFields, shouldIncludeAdFieldsForPad, shouldIncludeAdRotationFields, false).fields; + fields["impressionQueue"] = "data-metrics-impressions-low-latency"; + if (pageInformation !== null && serverData.isDefinedNonNull(pageInformation.iAdInfo)) { + const eventVersion = pageInformation.iAdInfo.fastImpressionsEventVersion; + fields["eventVersion"] = eventVersion; + exclude.push("eventVersion"); + // Make some manual adjustments to events for v5. + if (eventVersion === 5) { + // Indicates the viewable area where elements can be impressed excludes the tab bar + fields["viewableArea"] = "excludingTabBar"; + // Remove the `iAdPlacementId` for v5. It's added for compatibility with v4 impressions. + delete fields["iAdPlacementId"]; + } + } + const include = ["fastImpressions", "pageFields", "pageReferrer"]; + if (shouldIncludeAdFieldsForPad) { + include.push("advertDeviceWindow"); + } + if (shouldIncludeAdRotationFields) { + include.push("advertRotation"); + } + return optOutOfLegacyMetricsIdFieldsProvider(objectGraph, new AppStoreMetricsData(fields, include, exclude, topicFromEventFields(objectGraph, fields), shouldFlushFromEventFields(objectGraph, fields, pageInformation))); +} +export function createMetricsMediaData(objectGraph, eventFields) { + const fields = {}; + Object.assign(fields, eventFields); + fields["eventType"] = "media"; + addWebClientEventFields(objectGraph, fields); + addAltAb2DataToEventFields(objectGraph, fields); + return optOutOfLegacyMetricsIdFieldsProvider(objectGraph, new AppStoreMetricsData(fields, [], [], topicFromEventFields(objectGraph, fields), shouldFlushFromEventFields(objectGraph, fields))); +} +export function createMetricsMediaClickData(objectGraph, targetId, targetType, eventFields) { + const fields = {}; + Object.assign(fields, eventFields); + fields["eventType"] = "click"; + fields["targetType"] = targetType; + fields["targetId"] = targetId; + const include = ["pageFields"]; + addWebClientEventFields(objectGraph, fields); + addAltAb2DataToEventFields(objectGraph, fields); + return optOutOfLegacyMetricsIdFieldsProvider(objectGraph, new AppStoreMetricsData(fields, include, [], topicFromEventFields(objectGraph, fields), shouldFlushFromEventFields(objectGraph, fields))); +} +function shouldFlushFromEventFields(objectGraph, eventFields, pageInformation = null, isDefaultBrowser) { + var _a, _b; + const eventType = eventFields["eventType"]; + let shouldFlush = false; + if (!serverData.isDefinedNonNullNonEmpty(eventType)) { + return shouldFlush; + } + const isDefaultBrowserContext = isDefaultBrowser !== null && isDefaultBrowser !== void 0 ? isDefaultBrowser : false; + switch (eventType) { + case "click": + shouldFlush = + serverData.asBooleanOrFalse(eventFields, EventLinter.hasIAdData) || + isDefaultBrowserContext; + break; + case "exit": + shouldFlush = true; + break; + case "impressions": + shouldFlush = serverData.asBooleanOrFalse(eventFields, EventLinter.hasIAdData); + // Not my best work here. + // For most ad placements on a page, it's fine to check the `hasIAdData` as an indicator for whether we should flush after a given event. + // The challenge for the product page placements is that they're fetched asynchronously, and we don't create an entirely new impressions + // event when that happens (we just update fields within the event via the `pageChange` mechanism). To ensure that fast impression events on + // the product page are actually fast, we have to force the events to flush if they meet the below conditions that effectively guarantee + // we will be impressing something on the low latency queue. + if (eventFields["impressionQueue"] === "data-metrics-impressions-low-latency" && + (((_a = pageInformation === null || pageInformation === void 0 ? void 0 : pageInformation.iAdInfo) === null || _a === void 0 ? void 0 : _a.placementType) === "productPageYMAL" || + ((_b = pageInformation === null || pageInformation === void 0 ? void 0 : pageInformation.iAdInfo) === null || _b === void 0 ? void 0 : _b.placementType) === "productPageYMALDuringDownload")) { + shouldFlush = true; + } + break; + case "page": + // rdar://98487650 (CL seed: Product Page observed lower % of events coming in <2 mins ( page change + page events )) + // In order to resolve issues with flushing timeliness in the PPE: rdar://93127678 ([Metrics] Dismissing store sheet does not fire clickstream events) + // we allow page events with ad data in them to cause a flush on product pages in the PPE. + // We also pre-emptively flush for page events where it's the default browser flow in order to "prewarm" the metrics pipeline, ensuring flushes can occur + // if and when the user taps "Open" on an installed app. + const isProductPageExtension = objectGraph.host.clientIdentifier === client.productPageExtensionIdentifier; + shouldFlush = + isProductPageExtension && + (serverData.asBooleanOrFalse(eventFields, EventLinter.hasIAdData) || + isDefaultBrowserContext); + break; + default: + break; + } + const isSubscribePageExtensionEnterExitEventsEnabled = objectGraph.host.isiOS; + if (objectGraph.host.clientIdentifier === client.subscribePageExtensionIdentifier && + !isSubscribePageExtensionEnterExitEventsEnabled) { + // <rdar://problem/55865604> Metrics: missing metrics events for arcade upsell from springboard app open on unsubscribed user + // Until a native fix is in to address enter/exit events in SPE (subscribePageExtensionEnterExitEvents), we + // need to force events in the SubscribePageExtension to trigger a flush. + shouldFlush = true; + } + return shouldFlush; +} +function topicFromEventFields(objectGraph, eventFields) { + const topic = eventFields["topic"] || objectGraph.bag.metricsTopic; + return topic; +} +function addAltAb2DataToEventFields(objectGraph, eventFields) { + if (objectGraph.bag.isMetricsAb2DataFallbackEnabled && isSome(objectGraph.experimentCache)) { + eventFields["alt_ab2_data"] = JSON.stringify(objectGraph.experimentCache.createAb2Data()); + } +} +/** + * Attach expected metrics fields specific to the "web" client, if necessary + */ +function addWebClientEventFields(objectGraph, eventFields) { + var _a; + if (!objectGraph.client.isWeb) { + return; + } + // The `platformContext` field is expected to reflect the "platform" that the user is browsing + // at the time of the event + eventFields["platformContext"] = (_a = objectGraph.activeIntent) === null || _a === void 0 ? void 0 : _a.previewPlatform; +} +//# sourceMappingURL=builder.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/metrics/event-linter.js b/node_modules/@jet-app/app-store/tmp/src/common/metrics/event-linter.js new file mode 100644 index 0000000..ad9706c --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/metrics/event-linter.js @@ -0,0 +1,563 @@ +import { isNothing, isSome } from "@jet/environment/types/optional"; +import { LintedMetricsEvent } from "../../api/models/metrics/metrics"; +import * as serverData from "../../foundation/json-parsing/server-data"; +import { BuyParameters } from "../../foundation/metrics/buy-parameters"; +import { cookiesOf } from "../../foundation/metrics/cookies"; +import { MetricsIdentifierType } from "../../foundation/metrics/metrics-identifiers-cache"; +import { URL } from "../../foundation/network/urls"; +import { reduceSignificantDigits } from "../../foundation/util/math-util"; +import * as constants from "./helpers/constants"; +import { stripUniqueImpressionIdsIfNecessary } from "./helpers/impressions"; +import { iAdDismissAdActionMetricsParameterStringToken, iAdURLLineItemParameterStringToken, iAdURLParameterStringToken, } from "./helpers/models"; +import * as searchResultImpressions from "./helpers/search-result-impressions"; +import * as searchFocusImpressions from "./helpers/search-focus-impressions"; +import * as util from "./helpers/util"; +import { MetricsReferralContext } from "./metrics-referral-context"; +/** + * A type which applies App Store business rules to metrics fields + * and generates events which are ready for posting to figaro. + */ +export class EventLinter { + /** + * Create an event linter. + * + * @param options The options which specify various behaviors of the new linter. + * This object will be frozen. + */ + constructor(options) { + this._options = Object.freeze(options); + } + // endsection + // section Public Properties + /** + * Topic to use if an event fields blob does not specify one. + */ + get defaultTopic() { + return this._options.defaultTopic; + } + // endsection + // section Utilities + /** + * Reduce the accuracy of fields in a blob according to + * a given array of rules found in a metrics objectGraph.bag. + * + * @param eventFields The fields of an event to reduce the accuracy of. + * @param rules An array of rules from a metrics objectGraph.bag. + */ + _reduceFieldAccuracy(eventFields, rules) { + for (const rule of rules) { + const fieldName = serverData.asString(rule, "fieldName"); + if (serverData.isNull(fieldName)) { + continue; + } + const value = serverData.asNumber(eventFields, fieldName); + if (serverData.isNull(value)) { + continue; + } + let magnitude = serverData.asNumber(rule, "magnitude"); + if (serverData.isNull(magnitude)) { + magnitude = 1024 * 1024; + } + let significantDigits = serverData.asNumber(rule, "significantDigits"); + if (serverData.isNull(significantDigits)) { + significantDigits = 2; + } + if (magnitude <= 0.0 || significantDigits < 0.0) { + // This is the failure mode from MetricsKit. + eventFields[fieldName] = Number.NaN; + continue; + } + const scaledValue = value / magnitude; + eventFields[fieldName] = reduceSignificantDigits(scaledValue, significantDigits); + } + } + /** + * Returns a new URL by scrubbing any ad fields we've inserted into URLs to pass + * ad attribution information between pages. + * @param urlString The original URL to be scrubbed + * @returns A scrubbed URL. + */ + _urlScrubbingAdParameters(urlString) { + const url = new URL(urlString); + url.removeParam(iAdURLParameterStringToken); + url.removeParam(iAdURLLineItemParameterStringToken); + url.removeParam(iAdDismissAdActionMetricsParameterStringToken); + return url.build(); + } + /** + * Returns a new URL by scrubbing everything but the protocol, domain, and port (e.g. host). + * @param urlString The original URL to be scrubbed + * @returns A scrubbed URL. + */ + _urlScrubbingExtRefUrl(urlString) { + const url = new URL(urlString); + url.username = ""; + url.password = ""; + url.pathname = undefined; + url.query = undefined; + url.hash = undefined; + return url.build(); + } + // endsection + // section Fast Impressions Deresolution + _derezFastImpressions(eventFields) { + const impressionQueue = serverData.asString(eventFields, "impressionQueue"); + const eventVersion = serverData.asNumber(eventFields, "eventVersion"); + if (impressionQueue !== "data-metrics-impressions-low-latency") { + return; // Only scrub data going to the AP low latency queue. + } + // Scrub `viewedInfo` of a v4 event. + if (eventVersion === 4) { + const impressions = serverData.asArrayOrEmpty(eventFields, "impressions"); + eventFields["impressions"] = impressions.map((impression) => { + if (isNothing(impression)) { + return impression; + } + const viewedInfo = serverData.asArrayOrEmpty(impression, "viewedInfo"); + if (viewedInfo.length === 0) { + return impression; // V3. No modification. + } + /** + * <rdar://problem/64497066> Metrics: iAd: JS must clear impression start times, and derezz duration to 2 sig figs + * - start time to 0 (strange ask - we already send this as part of `impressionTimes`) + * - duration to 2 sig fig. + */ + impression["viewedInfo"] = viewedInfo.map((interval) => { + if (isNothing(interval)) { + return interval; + } + const duration = serverData.asNumber(interval, "d"); + interval["s"] = 0; + if (isSome(duration)) { + interval["d"] = reduceSignificantDigits(duration, 2); + } + return interval; + }); + return impression; + }); + } + // Scrub `viewedInfoDetailed` of a v5 event. + if (eventVersion === 5) { + const impressions = serverData.asArrayOrEmpty(eventFields, "impressions"); + eventFields["impressions"] = impressions.map((impression) => { + if (isNothing(impression)) { + return impression; + } + // Impression items in a low latency impressions event shouldn't have a `cardType` field. + delete impression["cardType"]; + if (serverData.isNullOrEmpty(serverData.asString(impression, "iAdMetadata")) || + serverData.isNullOrEmpty(serverData.asString(impression, "iAdImpressionId"))) { + // If the iAdMetadata or iAdImpressionId for this impression on the fast queue is null, + // it's an organic result that has added instrumentation as it's in a defined ad slot. + // For these items, we have to scrub the adamId for privacy reasons. + delete impression["id"]; + } + // Get the dictionary of values, formatted like so: + // {0: [{s: 11636658562756, d: 23281}], 50: [{s: 1636658559350, d: 378}]} + const viewedInfoDetailed = serverData.asDictionary(impression, "viewedInfoDetailed"); + if (isNothing(viewedInfoDetailed) || serverData.isNullOrEmpty(viewedInfoDetailed)) { + return impression; // Nothing to modify. + } + /** + * rdar://89785026 (Chainlink: v5 Impressions ViewedInfoDetailed - Fixes for startTime and duration) + * - match scrubbing "s" value and de-rezing "d" value as per above v4 implementation. + */ + Object.entries(viewedInfoDetailed).forEach(([key, value]) => { + // For each key/value pair, grab the array of traditional "viewedInfo". + const viewedInfo = serverData.asArrayOrEmpty(value); + // Iterate over the array of values, scrubbing the required children. + viewedInfoDetailed[key] = viewedInfo.map((interval) => { + if (isNothing(interval)) { + return interval; + } + const duration = serverData.asNumber(interval, "d"); + interval["s"] = 0; + if (isSome(duration)) { + interval["d"] = reduceSignificantDigits(duration, 2); + } + return interval; + }); + }); + // Re-set the modified dictionary on the `impression` object. + impression["viewedInfoDetailed"] = viewedInfoDetailed; + return impression; + }); + } + } + _stripContentRatingImpressionFields(eventFields) { + const impressions = serverData.asArrayOrEmpty(eventFields, "impressions"); + eventFields["impressions"] = impressions.map((impression) => { + if (isSome(impression)) { + // "contentRating" and "bundleId" are used by native to + // determine the value of "contentRestrictionReasons" (whether a + // lockup's offer is disabled due to content restrictions). + // However we don't want these fields in the final impression. + delete impression["contentRating"]; + delete impression["bundleId"]; + } + return impression; + }); + } + // endsection + // section Search Results Page Impressions + /** + * Decorate `impressions` field for click and impressions events on SRP. + */ + _decorateSearchResultImpressions(eventFields) { + const pageType = serverData.asString(eventFields, "pageType"); + const pageId = serverData.asString(eventFields, "pageId"); + /** + * Only run on SRP. + */ + const isSearchResultsPage = pageType === "Search" && pageId !== "hints"; + if (isSearchResultsPage) { + searchResultImpressions.decorateImpressionParentId(eventFields); + } + } + // endsection + // section Search Focus Page Impressions + /** + * Decorate `impressions` field for click and impressions events on SFP. + */ + _decorateSearchFocusImpressions(eventFields) { + const pageType = serverData.asString(eventFields, "pageType"); + const pageId = serverData.asString(eventFields, "pageId"); + /** + * Only run on SFP. + */ + if (pageType === "SearchFocus" && pageId === "Focus") { + searchFocusImpressions.decorateImpressionParentId(eventFields); + } + } + // endsection + // section Rules + /** + * Apply the rules which are universal to all metrics events + * to a given metrics fields linter. + * + * @param eventFields The fields which will be used to construct a built event. + * @param topic The topic the built event will be submitted to. + */ + _decorateAll(objectGraph, eventFields, topic) { + var _a, _b, _c; + const getBagValue = this._options.bagProvider; + // - Metrics base fields + const metricsBase = getBagValue("metricsBase", topic); + if (!serverData.isNull(metricsBase) && typeof metricsBase === "object") { + Object.assign(eventFields, metricsBase); + } + // - Universal basic fields + eventFields["clientBuildType"] = this._options.buildType; + eventFields["resourceRevNum"] = this._options.jsVersion; + eventFields["xpSendMethod"] = "jet-js"; + this._options.buyDecorator.useApp(serverData.asString(eventFields, "app")); + // - Universal scrubbing + delete eventFields[constants.contextualAdamIdKey]; + // - Cookie-based fields + const cookies = cookiesOf(serverData.asString(eventFields, "cookie")); + for (const cookie of cookies) { + if (cookie.key === "xp_ci") { + // Always update buy decorator. + this._options.buyDecorator.useClientId(cookie.value); + break; + } + } + delete eventFields["cookie"]; + const clientIdFields = (_b = (_a = objectGraph.metricsIdentifiersCache) === null || _a === void 0 ? void 0 : _a.getMetricsFieldsForTypes([MetricsIdentifierType.client])) !== null && _b !== void 0 ? _b : {}; + Object.assign(eventFields, clientIdFields); + delete eventFields["clientGeneratedId"]; + // - page + const pageType = serverData.asString(eventFields, "pageType"); + const pageId = serverData.asString(eventFields, "pageId"); + if (!serverData.isNull(pageType) && !serverData.isNull(pageId)) { + const separator = serverData.asString(getBagValue("compoundSeparator", topic)) || "_"; + eventFields["page"] = `${pageType}${separator}${pageId}`; + } + // - Field value resolution reduction + const rules = serverData.asArrayOrEmpty(getBagValue("deResFields", topic)); + this._reduceFieldAccuracy(eventFields, rules); + // Scrub sensitive urls in event from data that should not leave the device. + // At times we insert ad metadata into URLs in order to pass ad attribution information between pages. + // Additionally, scrub extRefUrl of everything but the origin. + const urlFieldsToScrub = ["pageUrl", "actionUrl", "extRefUrl", "refUrl", "url", "parentPageUrl"]; + for (const urlField of urlFieldsToScrub) { + const urlString = serverData.asString(eventFields, urlField); + if (isSome(urlString) && urlString.length > 0) { + eventFields[urlField] = + urlField === "extRefUrl" + ? this._urlScrubbingExtRefUrl(urlString) + : this._urlScrubbingAdParameters(urlString); + } + } + // Where an `overridePageContext` has been provided, use it as the `pageContext` value. + const overridePageContext = serverData.asString(eventFields, "overridePageContext"); + if (isSome(overridePageContext)) { + delete eventFields["overridePageContext"]; + eventFields["pageContext"] = overridePageContext; + } + if (objectGraph.bag.isMetricsUserIdFallbackEnabled) { + const existingUserId = serverData.asString(eventFields, "userId"); + let metricsUserDSID = null; + if (isNothing(existingUserId) || + existingUserId.length === 0 || + existingUserId.length === EventLinter.clientGeneratedUserIdLength) { + metricsUserDSID = (_c = objectGraph.user.dsid) !== null && _c !== void 0 ? _c : null; + } + if (isSome(metricsUserDSID) && metricsUserDSID.length > 0) { + eventFields["dsId"] = metricsUserDSID; + } + } + } + _decorateClick(eventFields) { + util.adjustGhostHintFieldsForClick(eventFields); + // clicks have snapshot impressions field. + this._decorateSearchResultImpressions(eventFields); + this._decorateSearchFocusImpressions(eventFields); + MetricsReferralContext.shared.addReferralDataToEventIfNecessary(eventFields); + this._filterBuyParams(eventFields); + /// rdar://118948967 (Disable snapshot impressions on iOS 17) + const pageType = serverData.asString(eventFields, "pageType"); + if (isNothing(pageType) || !pageType.toLowerCase().includes("search")) { + delete eventFields["impressions"]; + } + stripUniqueImpressionIdsIfNecessary(eventFields); + } + /** + * Apply the rules specific to the `impression` event. + * + * @param objectGraph Current object graph + * @param eventFields The fields which will be used to construct a built event. + * @returns Whether or not the impressions event is valid and non-empty + */ + _decorateImpressions(objectGraph, eventFields) { + if (serverData.isNullOrEmpty(eventFields["impressions"])) { + return false; + } + this._derezFastImpressions(eventFields); + this._decorateSearchResultImpressions(eventFields); + this._stripContentRatingImpressionFields(eventFields); + const refUrl = serverData.asString(eventFields, "refUrl"); + if (isSome(refUrl) && refUrl.length > 0) { + eventFields["searchTerm"] = util.searchTermFromRefURL(refUrl); + delete eventFields["refUrl"]; + } + if (objectGraph.client.isVision) { + const pageUrl = serverData.asString(eventFields, "pageUrl"); + if (isSome(pageUrl) && isNothing(eventFields["searchTerm"])) { + const searchTerm = util.searchTermFromProductURL(pageUrl); + if (isSome(searchTerm)) { + eventFields["searchTerm"] = searchTerm; + } + } + } + stripUniqueImpressionIdsIfNecessary(eventFields); + // We need the impressionQueue for _derezFastImpressions, so we know which events to derez, but then we need to + // remove it from the event + // rdar://144333694 ([Ad Platforms][CrystalE][iOS]Seeing 'Impression Queue' as a top level + // field for impressions which was removed earlier) + delete eventFields["impressionQueue"]; + return true; + } + /** + * Apply the rules specific to the `media` event. + * + * @param eventFields The fields which will be used to construct a built event. + */ + _decorateMedia(eventFields) { + const position = serverData.asNumber(eventFields, "position"); + if (!serverData.isNull(position)) { + eventFields["position"] = Math.round(position); + } + } + /** + * Apply the rules specific to the `buyparams` . + * + * @param eventFields The fields which will be used to construct a built event. + */ + _filterBuyParams(eventFields) { + const buyParamsString = serverData.asString(eventFields, "actionDetails.buyParams"); + if (isSome(buyParamsString) && buyParamsString.length > 0) { + const buyParams = new BuyParameters(buyParamsString); + const disallowedFields = ["ownerDsid"]; + disallowedFields.forEach((key) => { + buyParams.set(key, null, null); + }); + if (isSome(eventFields["actionDetails"])) { + eventFields["actionDetails"]["buyParams"] = buyParams.toString(); + } + } + } + /** + * Apply the rules specific to the `page` event. + * + * @param objectGraph Current object graph + * @param eventFields The fields which will be used to construct a built event. + */ + _decoratePage(objectGraph, eventFields) { + const page = serverData.asString(eventFields, "page"); + if (!serverData.isNull(page)) { + eventFields["pageHistory"] = this._options.buyDecorator.getPageHistoryFor(page); + } + MetricsReferralContext.shared.setReferralDataForProductPageExtensionIfNecessary(eventFields); + MetricsReferralContext.shared.beginReferralContextForPageIfNecessary(eventFields); + MetricsReferralContext.shared.addReferralDataToEventIfNecessary(eventFields); + // Make sure to add the referral data before checking for valid refUrl + const refUrl = serverData.asString(eventFields, "refUrl"); + if (!serverData.isNull(refUrl)) { + const refApp = util.extractSiriRefAppFromRefURL(refUrl); + const searchTerm = util.searchTermFromRefURL(refUrl); + if (refApp !== null && refApp.length > 0) { + eventFields["refApp"] = refApp; + } + if (searchTerm !== null && searchTerm.length > 0) { + eventFields["searchTerm"] = searchTerm; + } + } + if (objectGraph.client.isVision) { + const pageUrl = serverData.asString(eventFields, "pageUrl"); + if (isSome(pageUrl) && isNothing(eventFields["searchTerm"])) { + const searchTerm = util.searchTermFromProductURL(pageUrl); + if (isSome(searchTerm)) { + eventFields["searchTerm"] = searchTerm; + } + } + } + } + /** + * Apply the rules specific to the `pageChange` event. The pageChange should have the same treatment + * as page event. + * + * @param eventFields The fields which will be used to construct a built event. + */ + _decoratePageChange(objectGraph, eventFields) { + this._decoratePage(objectGraph, eventFields); + } + /** + * Apply the rules specific to the `search` event. + * + * @param objectGraph Current object graph + * @param eventFields The fields which will be used to construct a built event. + */ + _decorateSearch(eventFields) { + eventFields["eventVersion"] = 3; + util.adjustGhostHintFieldsForSearch(eventFields); + } + /** + * Apply the rules specific to the `pageExit` event. + * + * @param eventFields The fields which will be used to construct a built event. + */ + _decoratePageExit(eventFields) { + MetricsReferralContext.shared.endReferralContextIfNecessaryForPageEvent(eventFields); + } + // endsection + // section Filter + _filterExtraneous(eventFields) { + util.removeExtraGhostHintFields(eventFields); + MetricsReferralContext.shared.removeReferralContextInfoFromMetricsEvent(eventFields); + } + // endsection + // section Building Events + /** + * Create a metrics event by applying the business rules of App Store to a given fields blob. + * + * @param eventFields The fields to use to construct an event. + * @returns A built event ready for posting. + */ + makeEvent(objectGraph, eventFields) { + var _a, _b; + if (preprocessor.GAMES_TARGET) { + // Until we get further with Privacy/Legal, the decision has been made to disable instrumentation + // for Game Overlay. the pre-consent fields provider is only active for Game Overlay. Other options + // for more fine-tuned control (_gameCenterPreConsent and _crossUsePreConsent) have been added to + // provide JS flexibility. + const isPreConsentFieldsProviderEnabled = eventFields["_isPreConsentFieldsProviderEnabled"]; + const gameCenterPreConsent = eventFields["_gameCenterPreConsent"]; + const crossUsePreConsent = eventFields["_crossUsePreConsent"]; + if (isPreConsentFieldsProviderEnabled === true || + gameCenterPreConsent === true || + crossUsePreConsent === true) { + return new LintedMetricsEvent({}); + } + delete eventFields["_isPreConsentFieldsProviderEnabled"]; + delete eventFields["_gameCenterPreConsent"]; + delete eventFields["_crossUsePreConsent"]; + } + const eventType = serverData.asString(eventFields, "eventType"); + if (this._options.isLoggingEnabled) { + objectGraph.console.log(`Building event for topic: ${eventType}`); + } + // rdar://135738684 (TLF: Off Store Lockups Removal) + // Currently all events from App Store Components are suppressed. + // Delete all fields to leave an empty event. + const app = eventFields["app"]; + if (app === "com.apple.appstorecomponentsd") { + return new LintedMetricsEvent({}); + } + const topic = serverData.asString(eventFields, "topic") || this._options.defaultTopic; + this._decorateAll(objectGraph, eventFields, topic); + // rdar://148554411 (Metrics: Disable PII collection from U13 accounts) + let isUnderThirteenAccount; + if (preprocessor.GAMES_TARGET && objectGraph.props.enabled("157263806-add-playerBridge-askGlobal")) { + isUnderThirteenAccount = (_b = (_a = objectGraph.player) === null || _a === void 0 ? void 0 : _a.isUnderThirteen) !== null && _b !== void 0 ? _b : false; + } + else { + isUnderThirteenAccount = objectGraph.user.isUnderThirteen; + } + if (isUnderThirteenAccount) { + delete eventFields["dsId"]; + delete eventFields["userId"]; + delete eventFields["canonicalAccountIdentifierOverride"]; + } + const extRefUrl = eventFields["extRefUrl"]; + if (extRefUrl && extRefUrl === "") { + delete eventFields["extRefUrl"]; + } + switch (eventType) { + case "click": + this._decorateClick(eventFields); + break; + case "exit": + break; + case "impressions": + const isValidEvent = this._decorateImpressions(objectGraph, eventFields); + if (!isValidEvent) { + // We want to filter out empty impressions events, + // and passing an empty event will contractually drop it from the metrics recorder + return new LintedMetricsEvent({}); + } + break; + case "media": + this._decorateMedia(eventFields); + break; + case "page": + this._decoratePage(objectGraph, eventFields); + break; + case "pageChange": + this._decoratePageChange(objectGraph, eventFields); + break; + case "pageExit": + this._decoratePageExit(eventFields); + break; + case "search": + this._decorateSearch(eventFields); + break; + default: + break; + } + this._filterExtraneous(eventFields); + if (objectGraph.bag.metricsIdMigrationEnabled) { + util.removeDSIDFields(eventFields); + } + return new LintedMetricsEvent(eventFields); + } +} +/** + * The length of a client generated user ID. + */ +EventLinter.clientGeneratedUserIdLength = 24; +/** + * Key whose value indicates if event fields contain iAd data. + */ +EventLinter.hasIAdData = "hasiAdData"; +//# sourceMappingURL=event-linter.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/buy.js b/node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/buy.js new file mode 100644 index 0000000..b39f9cf --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/buy.js @@ -0,0 +1,331 @@ +import { isNothing, isSome } from "@jet/environment/types/optional"; +import * as serverData from "../../../foundation/json-parsing/server-data"; +import { BuyParameters } from "../../../foundation/metrics/buy-parameters"; +import { URL } from "../../../foundation/network/urls"; +import * as productVariants from "../../product-page/product-page-variants"; +import { MetricsReferralContext } from "../metrics-referral-context"; +import * as metricsPosting from "../posting"; +import * as misc from "./misc"; +import { IAdSearchInformation } from "./models"; +import * as metricsUtil from "./util"; +//* ************************* +//* Buy Metrics +//* ************************* +/** + * Adds bag driven metrics fields. + * @param objectGraph + * @returns + */ +function addBagMetricsToBuyParams(objectGraph, baseBuyParams) { + const buyParams = new BuyParameters(baseBuyParams); + const metricsConfiguration = objectGraph.bag.metricsConfiguration; + const language = serverData.asString(metricsConfiguration, "metricsBase.language"); + buyParams.set("languageId", language); + return buyParams.toString(); +} +export function addPageMetricsToBuyParams(objectGraph, baseBuyParams, adamId, pageInformation, targetType, kind, metricsPlatformDisplayStyle, productVariantData, inAppEventId, excludeCrossfireAttribution, extRefApp2, extRefUrl2) { + const buyParams = new BuyParameters(baseBuyParams); + addPageMetricsToBuyParamsObject(objectGraph, buyParams, adamId, pageInformation, targetType, kind, metricsPlatformDisplayStyle, productVariantData, inAppEventId, excludeCrossfireAttribution, extRefApp2, extRefUrl2); + addExtraInfoToBuyParamsObject(objectGraph, buyParams, pageInformation); + return buyParams.toString(); +} +export function addPageMetricsToBuyParamsObject(objectGraph, buyParams, adamId, pageInformation, targetType, kind, metricsPlatformDisplayStyle, productVariantData, inAppEventId, excludeCrossfireAttribution, extRefApp2, extRefUrl2) { + var _a, _b, _c; + const fields = misc.fieldsFromPageInformation(pageInformation); + if (targetType) { + buyParams.set("impressionType", targetType); // impressionType == targetType == locationType. + } + if (kind) { + buyParams.set("kind", kind); + } + const pageId = serverData.asString(serverData.asJSONData(fields), "pageId"); + buyParams.set("pageId", pageId); + const pageType = serverData.asString(serverData.asJSONData(fields), "pageType"); + buyParams.set("pageType", pageType); + // Ad container id + const iAdContainerId = serverData.asString(serverData.asJSONData(fields), "iAdContainerId"); + if (isSome(iAdContainerId) && iAdContainerId.length > 0) { + buyParams.set(adBuyParamKeys.containerId, iAdContainerId, null); + } + // we add search terms to the page if the page is for the item we are buying, or if + // its for a streamlined contingent offer + const pageIds = (_a = pageId === null || pageId === void 0 ? void 0 : pageId.split("_")) !== null && _a !== void 0 ? _a : []; + const pageMatchesProduct = pageIds.includes(adamId); + const isProductPage = pageType === "Software"; + const isStreamlinedContingentOffer = ((_b = buyParams.get("contingentItemId", null)) === null || _b === void 0 ? void 0 : _b.length) > 0; + if (!isProductPage || pageMatchesProduct || isStreamlinedContingentOffer) { + // If there is a native search term, it will overwrite this + // initial value below in `addNativeMetricsToBuyParams`. + let searchTerm = serverData.asString(serverData.asJSONData(pageInformation), "searchTermContext.resultsTerm"); + // A search term may be attached to the product URL as a means of associating buys with searches. + if (serverData.isNullOrEmpty(searchTerm)) { + searchTerm = metricsUtil.searchTermFromProductURL(pageInformation === null || pageInformation === void 0 ? void 0 : pageInformation.pageUrl); + } + if (!serverData.isNull(searchTerm)) { + buyParams.set("searchTerm", searchTerm); + } + } + if (isProductPage && isSome(pageInformation) && isSome(pageInformation.pageUrl)) { + const pageUrl = new URL(pageInformation.pageUrl); + if (((_c = pageUrl.query) === null || _c === void 0 ? void 0 : _c["context"]) === "browserChoice") { + buyParams.set("prevPage", "BrowserChoice"); + buyParams.set("browserChoiceScreenBuy", "1", null); + } + } + productVariants.addProductPageVariantMetricsToBuyParams(buyParams, adamId, pageInformation === null || pageInformation === void 0 ? void 0 : pageInformation.productVariantData, productVariantData !== null && productVariantData !== void 0 ? productVariantData : undefined); + if (!serverData.isNull(metricsPlatformDisplayStyle) && metricsPlatformDisplayStyle.length > 0) { + buyParams.set("platformDisplayStyle", metricsPlatformDisplayStyle); + } + // App Event ID + buyParams.set("inAppEventId", inAppEventId); + // Referrer + if (!excludeCrossfireAttribution) { + if (serverData.isDefinedNonNull(MetricsReferralContext.shared.activeReferralData)) { + buyParams.set("extRefApp2", MetricsReferralContext.shared.activeReferralData.extRefApp2, null); + buyParams.set("extRefUrl2", MetricsReferralContext.shared.activeReferralData.extRefUrl2, null); + if (isSome(MetricsReferralContext.shared.activeReferralData.kind)) { + const extRefAppKindName = MetricsReferralContext.shared.activeReferralData.kind.name; + if (extRefAppKindName === "clip" || extRefAppKindName === "appClip") { + buyParams.set("hostApp", "com.apple.AppStore.clipOverlay"); + } + } + } + else { + buyParams.set("extRefApp2", extRefApp2, null); + buyParams.set("extRefUrl2", extRefUrl2, null); + } + } +} +function addExtraInfoToBuyParamsObject(objectGraph, buyParams, pageInformation) { + var _a, _b; + if (isNothing(pageInformation)) { + return; + } + const extraInfo = []; + const hasiAdData = isSome((_a = pageInformation.iAdInfo) === null || _a === void 0 ? void 0 : _a.clickFields["hasiAdData"]); + if (hasiAdData) { + const iAdExtraInfoKey = isIAdFromCurrentPage(objectGraph, pageInformation) ? "iAdSponsored" : "iAdOriginated"; + extraInfo.push({ key: iAdExtraInfoKey, value: "true" }); + } + if (serverData.isDefinedNonNullNonEmpty(extraInfo)) { + const extraInfoParamValue = extraInfo + .map((extraInfoValue) => `${extraInfoValue.key}=${extraInfoValue.value}`) + .join(";"); + const encodedValue = (_b = objectGraph.cryptography) === null || _b === void 0 ? void 0 : _b.base64Encode(extraInfoParamValue); + if (isSome(encodedValue)) { + buyParams.set("extraInfo", encodedValue); + } + } +} +/** + * Determines if an iAd is from the current page by comparing the iAd's placement type with the page type. + * + * This function validates whether an advertisement is correctly placed on the appropriate page type. + * Different ad placement types are designed for specific page contexts, and this function ensures + * that the placement matches the current page context. + * + * @param objectGraph - The AppStore object graph providing access to app services and utilities + * @param pageInformation - Information about the current page, including its type and iAd information + * @returns True if the iAd's placement type matches the current page type, false otherwise + * or if pageInformation or iAdInfo is missing + */ +function isIAdFromCurrentPage(objectGraph, pageInformation) { + const iAdInfo = pageInformation === null || pageInformation === void 0 ? void 0 : pageInformation.iAdInfo; + if (isNothing(pageInformation) || isNothing(iAdInfo)) { + return false; + } + const pageType = pageInformation.baseFields["pageType"]; + const iAdPlacementType = IAdSearchInformation.placementTypeFromPlacementId(objectGraph, iAdInfo.placementId); + switch (iAdPlacementType) { + case "searchLanding": + return pageType === "SearchLanding"; + case "today": + return pageType === "Today"; + case "productPageYMAL": + case "productPageYMALDuringDownload": + return pageType === "Software" || pageType === "SoftwareBundle"; + default: + return pageType === "Search"; + } +} +/** + * Returns a copy of the specified buy parameters enriched with native metrics fields. + * + * @param baseBuyParams The buy parameters of an app which have previously been + * decorated with page metrics. Passing buy parameters that were not decorated with page + * metrics can result in incorrect search terms being reported. + * @param adamId The identifier of the app whose buy parameters are being decorated. + * @param excludeCrossfireAttribution Specifies whether crossfire attribution should be + * excluded from the decorated buy params. + * @param nativeMetrics Metrics fields provided by native code. + * @param osMetrics Metrics fields describing the device's OS provided by native code. + * @returns A copy of `baseBuyParams` enriched with native metrics fields. + */ +export function addNativeValuesToBuyParams(objectGraph, baseBuyParams, adamId, excludeCrossfireAttribution, isAppInstalled, nativeMetrics, osMetrics) { + const baseBuyParamsObject = new BuyParameters(baseBuyParams); + addNativeValuesToBuyParamsObject(objectGraph, baseBuyParamsObject, adamId, excludeCrossfireAttribution, isAppInstalled, nativeMetrics, osMetrics); + return baseBuyParamsObject.toString(); +} +/** + * Returns a copy of the specified buy parameters enriched with native metrics fields. + * + * @param baseBuyParams The buy parameters of an app which have previously been + * decorated with page metrics. Passing buy parameters that were not decorated with page + * metrics can result in incorrect search terms being reported. + * @param adamId The identifier of the app whose buy parameters are being decorated. + * @param excludeCrossfireAttribution Specifies whether crossfire attribution should be + * excluded from the decorated buy params. + * @param nativeMetrics Metrics fields provided by native code. + * @param osMetrics Metrics fields describing the device's OS provided by native code. + * @returns A copy of `baseBuyParams` enriched with native metrics fields. + */ +export function addNativeValuesToBuyParamsObject(objectGraph, buyParams, adamId, excludeCrossfireAttribution, isAppInstalled, nativeMetrics, osMetrics) { + var _a, _b; + const pageContext = serverData.asString(nativeMetrics, "pageContext"); + buyParams.set("pageContext", pageContext); + const paymentTopic = objectGraph.props.enabled("paymentTopicFromBag") + ? objectGraph.bag.metricsPaymentTopic + : undefined; + buyParams.set("topic", paymentTopic !== null && paymentTopic !== void 0 ? paymentTopic : objectGraph.bag.metricsTopic); + metricsPosting.buyDecorator.useNativeValues(nativeMetrics); + const decoratorParams = metricsPosting.buyDecorator.params; + for (const key of Object.keys(decoratorParams)) { + if (key === "prevPage" && isSome(buyParams.get("prevPage"))) { + // We may have added a `prevPage` param earlier, specifically don't override this value if we have. + continue; + } + const value = serverData.asString(decoratorParams, key); + buyParams.set(key, value); + } + if (!serverData.isNull(osMetrics)) { + for (const key of Object.keys(osMetrics)) { + const value = serverData.asString(osMetrics, key); + buyParams.set(key, value); + buyParams.set(key, value, null); + } + } + if (!nativeMetrics) { + // ^^ Is this actually needed? + buyParams.set("searchTerm", null); + buyParams.set("platformDisplayStyle", null); + return; + } + const hostApp = serverData.asString(nativeMetrics, "hostApp"); + if (isSome(hostApp) && hostApp.length > 0) { + buyParams.set("hostApp", hostApp); + } + const isContingentOffer = ((_a = buyParams.get("contingentItemId", null)) === null || _a === void 0 ? void 0 : _a.length) > 0; + const app = serverData.asString(nativeMetrics, "app"); + if (isContingentOffer) { + // Contingent Offer Streamline buy app install flag + buyParams.set("app", objectGraph.host.clientIdentifier); + } + else if (isSome(app) && app.length > 0) { + buyParams.set("app", app); + } + if (!excludeCrossfireAttribution && !MetricsReferralContext.shared.shouldUseJSReferralData) { + const extRefUrl = serverData.asString(nativeMetrics, "extRefUrl2"); + const extractedSiriRefApp = metricsUtil.extractSiriRefAppFromRefURL(extRefUrl); + if (extRefUrl && extractedSiriRefApp) { + nativeMetrics["refApp"] = extractedSiriRefApp; + } + // ^^ Is this actually needed? + const usageContext = serverData.asString(nativeMetrics, "usageContext"); + if (isSome(usageContext)) { + switch (usageContext) { + case "overlay": + buyParams.set("hostApp", "com.apple.AppStore.overlay"); + break; + case "overlayClip": + buyParams.set("hostApp", "com.apple.AppStore.clipOverlay"); + break; + default: + break; + } + buyParams.set("extRefApp2", hostApp, null); + } + else { + const extRefApp2 = serverData.asString(nativeMetrics, "extRefApp2"); + buyParams.set("extRefApp2", extRefApp2, null); + const extRefUrl2 = serverData.asString(nativeMetrics, "extRefUrl2"); + buyParams.set("extRefUrl2", extRefUrl2, null); + const extRefAppType = serverData.asString(nativeMetrics, "extRefAppType"); + if (extRefAppType === "clip") { + buyParams.set("hostApp", "com.apple.AppStore.clipOverlay"); + } + } + } + // we add search terms to the page if the page is for the item we are buying + const pageId = buyParams.get("pageId"); + const pageType = buyParams.get("pageType"); + const pageIds = (_b = pageId === null || pageId === void 0 ? void 0 : pageId.split("_")) !== null && _b !== void 0 ? _b : []; + const pageMatchesProduct = pageIds.includes(adamId); + const isProductPage = pageType === "Software"; + if (!isProductPage || pageMatchesProduct) { + const searchTerm = metricsUtil.searchTermFromRefURL(serverData.asString(nativeMetrics, "refUrl")); + if (serverData.isDefinedNonNull(searchTerm)) { + buyParams.set("searchTerm", searchTerm); + } + } + // Remove ownerDsid from buyParams + buyParams.set("ownerDsid", null, null); +} +/** + * + * @param adamId The identifier of the app whose buy parameters are being decorated. + * @param buyParams The buy parameters of an app from a Media API response. + * @param pageInformation The purchase configuration page metrics configuration + * from an offer action. + * @param excludeCrossfireAttribution Specifies whether crossfire attribution should be + * excluded from the decorated buy params. + * @param targetType The target type that buy occured on + * @param kind The Kind that buy occured on, if any. + * @param metricsPlatformDisplayStyle The platform display style from an offer action. + * @param nativeMetrics Metrics fields provided by native code. + * @param osMetrics Metrics fields describing the device's OS provided by native code. + * @returns A copy of `buyParams` decorated with all metrics. + */ +export function addMetricsToBuyParams(objectGraph, adamId, buyParams, pageInformation, excludeCrossfireAttribution, isAppInstalled, targetType, kind, metricsPlatformDisplayStyle, nativeMetrics, osMetrics, productVariantData, inAppEventId, extRefApp2, extRefUrl2) { + // Future: Build BuyParameters once, instead of creating it per modification. + const bagMetricsBuyParams = addBagMetricsToBuyParams(objectGraph, buyParams); + const pageMetricsBuyParams = addPageMetricsToBuyParams(objectGraph, bagMetricsBuyParams, adamId, pageInformation, targetType, kind, metricsPlatformDisplayStyle, productVariantData, inAppEventId, excludeCrossfireAttribution, extRefApp2, extRefUrl2); + const nativeMetricsBuyParams = addNativeValuesToBuyParams(objectGraph, pageMetricsBuyParams, adamId, excludeCrossfireAttribution, isAppInstalled, nativeMetrics, osMetrics); + return nativeMetricsBuyParams; +} +/** + * Keys used for ad-related buy params. + */ +export const adBuyParamKeys = { + containerId: "mtContainerId", + placementId: "mtIadPlacementId", + templateType: "mtIadTemplateType", +}; +/** + * Copy ad-related buy params to the override buy params. + * Override buy params are used for updated and redownloads, and in those cases we lose iAd download attribution. + * If the original buy params had ad fields, we should copy them over here. + * @param originalBuyParamsString The original buy params attached to the offerAction. + * @param overrideBuyParamsString The override buy params from the purchase. + * @returns An updated buy params string with any ad-related fields copied over. + */ +export function copyAdBuyParamsToOverrideBuyParams(originalBuyParamsString, overrideBuyParamsString) { + const originalBuyParams = new BuyParameters(originalBuyParamsString); + const overrideBuyParams = new BuyParameters(overrideBuyParamsString); + const originalPlacementId = originalBuyParams.get(adBuyParamKeys.placementId, null); + if ((originalPlacementId === null || originalPlacementId === void 0 ? void 0 : originalPlacementId.length) > 0 && + serverData.isNullOrEmpty(overrideBuyParams.get(adBuyParamKeys.placementId, null))) { + overrideBuyParams.set(adBuyParamKeys.placementId, originalPlacementId, null); + } + const originalContainerId = originalBuyParams.get(adBuyParamKeys.containerId, null); + if ((originalContainerId === null || originalContainerId === void 0 ? void 0 : originalContainerId.length) > 0 && + serverData.isNullOrEmpty(overrideBuyParams.get(adBuyParamKeys.containerId, null))) { + overrideBuyParams.set(adBuyParamKeys.containerId, originalContainerId, null); + } + const originalTemplateType = originalBuyParams.get(adBuyParamKeys.templateType, null); + if ((originalTemplateType === null || originalTemplateType === void 0 ? void 0 : originalTemplateType.length) > 0 && + serverData.isNullOrEmpty(overrideBuyParams.get(adBuyParamKeys.templateType, null))) { + overrideBuyParams.set(adBuyParamKeys.templateType, originalTemplateType, null); + } + return overrideBuyParams.toString(); +} +//# sourceMappingURL=buy.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/clicks.js b/node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/clicks.js new file mode 100644 index 0000000..cc7b6f2 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/clicks.js @@ -0,0 +1,458 @@ +import * as metricsBuilder from "../builder"; +import * as metricsConstants from "./constants"; +import * as metricsLocation from "./location"; +import * as metricsModels from "./models"; +import * as metricsUtil from "./util"; +import * as metricsMisc from "./misc"; +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"; +//* ************************* +//* Click Metrics +//* ************************* +export function clickOptionsForLockup(objectGraph, data, baseOptions, clickOptions) { + return validation.context("clickOptionsForLockup", () => { + const contextualAdamId = data.id.slice(); + let id = data.id; + if (baseOptions.anonymizationOptions !== undefined && + baseOptions.anonymizationOptions.anonymizationString.length > 0) { + id = baseOptions.anonymizationOptions.anonymizationString; + } + const metricsOptions = { + ...baseOptions, + ...clickOptions, + id: id, + contextualAdamId: contextualAdamId, + softwareType: metricsUtil.softwareTypeForData(objectGraph, data), + }; + if (serverData.isNullOrEmpty(metricsOptions.targetType)) { + metricsOptions.targetType = objectGraph.client.isVision ? "lockupSmall" : "lockup"; + } + // There's a delicate false split from `baseOptions` and click options interface. Some paths pre-populate an unexpected `kind`, but we don't want to change that suddenly. + // Derive `kind` from data if missing. + if (serverData.isNull(metricsOptions.kind)) { + metricsOptions.kind = metricsUtil.metricsKindFromData(objectGraph, data); + } + // Include offerType for pre-order impressions + const isPreorder = mediaAttributes.attributeAsBoolean(data, "isPreorder"); + if (isPreorder) { + metricsOptions.offerType = "preorder"; + } + return metricsOptions; + }); +} +export function addBuyEventToOfferActionOnPage(objectGraph, action, options, isPreorder, isDefaultBrowser) { + var _a, _b, _c, _d, _e; + const pageInformation = options.pageInformation; + const buttonMetricsOptions = { + ...options, + targetType: "button", + }; + const metricsLocations = metricsLocation.createContentLocation(objectGraph, buttonMetricsOptions, (_a = action.title) !== null && _a !== void 0 ? _a : ""); + let targetId = (_c = (_b = options.anonymizationOptions) === null || _b === void 0 ? void 0 : _b.anonymizationString) !== null && _c !== void 0 ? _c : action.adamId; + if (isSome(options.targetId) && ((_d = options.targetId) === null || _d === void 0 ? void 0 : _d.length) > 0) { + targetId = options.targetId; + } + addBuyEventToOfferAction(objectGraph, action, targetId, isPreorder, pageInformation, metricsLocations, (_e = options.isAdvert) !== null && _e !== void 0 ? _e : false, options.recoMetricsData, isDefaultBrowser); +} +export function addBuyEventToOfferActionInheritingMetrics(objectGraph, newAction, originalAction, isPreorder) { + const originalPageInformation = originalAction.purchaseConfiguration.pageInformation; + // Find some event to inherit location event field from. + let metricsLocations; + if (isSome(originalAction.actionMetrics)) { + for (const event of originalAction.actionMetrics.data) { + metricsLocations = serverData.asArrayOrEmpty(serverData.asJSONValue(event.fields), "location"); + if (metricsLocations) { + break; + } + } + } + // Fine for now - CMC doesn't use anonymization. + addBuyEventToOfferAction(objectGraph, newAction, newAction.adamId, isPreorder, originalPageInformation, metricsLocations, false); +} +function addBuyEventToOfferAction(objectGraph, action, targetId, isPreorder, pageInformation, metricsLocations, isAdvert, recoMetricsData, isDefaultBrowser) { + var _a, _b, _c, _d, _e; + const eventFields = {}; + if (pageInformation) { + // We can't always check that `pageInformation` is an `instanceof MetricsPageInformation` here, because sometimes + // `pageInformation` is reconstituted from JSON which means it doesn't get the underlying type information. + // Instead, cast it and check for the existence of individual properties on the cast object. + const metricsPageInformation = pageInformation; + if (isAdvert && + ((_c = (_b = (_a = metricsPageInformation.iAdInfo) === null || _a === void 0 ? void 0 : _a.iAdAdamId) === null || _b === void 0 ? void 0 : _b.length) !== null && _c !== void 0 ? _c : 0) > 0 && + isSome(metricsPageInformation.iAdInfo) && + metricsPageInformation.iAdInfo.iAdAdamId === action.adamId) { + Object.assign(eventFields, metricsPageInformation.iAdInfo.clickFields); + } + if (serverData.isDefinedNonNullNonEmpty(metricsPageInformation.searchTermContext)) { + eventFields["searchTerm"] = metricsPageInformation.searchTermContext.term; + } + } + // Add the reco metrics data if available + if (isSome(recoMetricsData)) { + Object.assign(eventFields, recoMetricsData); + } + eventFields["actionDetails"] = { buyParams: action.purchaseConfiguration.buyParams }; + if (metricsLocations !== undefined) { + eventFields["location"] = metricsLocations; + } + // This hack where we add the adamId to the eventFields can be removed when `ActionDispatcher` + // is fully enabled and old ActionRunner code is removed. The custom data below replaces this. + eventFields[metricsConstants.contextualAdamIdKey] = action.adamId; + action.actionMetrics.custom[metricsConstants.contextualAdamIdKey] = action.adamId; + // Add Pre-order fields + if (isPreorder) { + eventFields["offerType"] = "preorder"; + if (serverData.isDefinedNonNull(action.expectedReleaseDate)) { + eventFields["offerReleaseDate"] = dateUtil.millisecondsToUTCMidnightFromLocalDate(action.expectedReleaseDate); + } + } + const clickEvent = metricsBuilder.createMetricsClickData(objectGraph, targetId, "button", eventFields, undefined, isDefaultBrowser); + // This hack where we add the adamId to the eventFields can be removed when `ActionDispatcher` + // is fully enabled and old ActionRunner code is removed. The custom data below replaces this. + eventFields[metricsConstants.contextualAdamIdKey] = action.adamId; // needed for `appState` + action.actionMetrics.custom[metricsConstants.contextualAdamIdKey] = action.adamId; + clickEvent.includingFields.push("appState"); + if (action.purchaseConfiguration.isArcadeApp) { + // Include button names in arcade buy action metrics + clickEvent.includingFields.push("buttonName"); + } + const shouldIncludeAdRotationFields = (_e = (_d = pageInformation === null || pageInformation === void 0 ? void 0 : pageInformation.iAdInfo) === null || _d === void 0 ? void 0 : _d.shouldIncludeAdRotationFields) !== null && _e !== void 0 ? _e : false; + if (isAdvert && shouldIncludeAdRotationFields) { + clickEvent.includingFields.push("advertRotation"); + } + const shouldIncludeAdWindowFields = isAdvert && objectGraph.client.isPad; + if (shouldIncludeAdWindowFields) { + clickEvent.includingFields.push("advertDeviceWindow"); + } + action.actionMetrics.addMetricsData(clickEvent); +} +export function addClickEventToArcadeBuyInitiateAction(objectGraph, action, options) { + var _a; + addClickEventToAction(objectGraph, action, { + ...options, + actionType: "buyInitiate", + subscriptionSKU: (_a = objectGraph.bag.arcadeProductId) !== null && _a !== void 0 ? _a : undefined, + actionContext: "Arcade", + targetType: "button", + }); +} +export function addClickEventToAction(objectGraph, action, options, addIAdFields = false, targetType) { + var _a, _b, _c, _d, _e, _f, _g; + let actionType = options.actionType; + if (!actionType) { + actionType = "navigate"; + } + const eventFields = { + actionType: actionType, + }; + // Ad click events will be wrapped in a CompoundAction. We still want to handle attaching `actionUrl` properly + // below if a `FlowAction` or `ExternalUrlAction` are inside the `CompoundAction`, so we create an array of actions + // to check. + let actions; + if (action instanceof models.CompoundAction) { + actions = action.actions; + } + else { + actions = [action]; + } + actions.forEach((subAction) => { + // Set the action URL if appropriate + if (subAction instanceof models.FlowAction) { + const flowAction = subAction; + eventFields["actionUrl"] = flowAction.pageUrl; + } + else if (subAction instanceof models.ExternalUrlAction) { + const flowAction = subAction; + eventFields["actionUrl"] = flowAction.url; + } + }); + if (options.actionDetails) { + eventFields["actionDetails"] = options.actionDetails; + } + if (options.actionContext) { + eventFields["actionContext"] = options.actionContext; + } + // Add offer type for pre-orders + if (serverData.isDefinedNonNull(options.offerType)) { + eventFields["offerType"] = options.offerType; + } + // Add release date for pre-orders + if (serverData.isDefinedNonNull(options.offerReleaseDate)) { + eventFields["offerReleaseDate"] = dateUtil.millisecondsToUTCMidnightFromLocalDate(options.offerReleaseDate); + } + const title = (_c = (_b = (_a = options.anonymizationOptions) === null || _a === void 0 ? void 0 : _a.anonymizationString) !== null && _b !== void 0 ? _b : action === null || action === void 0 ? void 0 : action.title) !== null && _c !== void 0 ? _c : ""; + eventFields["location"] = metricsLocation.createContentLocation(objectGraph, options, title); + // Search Term + if (options.pageInformation && options.pageInformation.searchTermContext) { + eventFields["searchTerm"] = options.pageInformation.searchTermContext.term; + } + if (serverData.isDefinedNonNull(options.softwareType)) { + eventFields["softwareType"] = options.softwareType; + } + // Advert Metrics + let additionalIncludingFields; + // Do we need both `options.isAdvert` and `addIAdFields`? + if ((options.isAdvert || options.isAdEligible) && addIAdFields && ((_d = options.pageInformation) === null || _d === void 0 ? void 0 : _d.iAdInfo)) { + Object.assign(eventFields, options.pageInformation.iAdInfo.clickFields); + if (objectGraph.client.isPad) { + additionalIncludingFields = ["advertDeviceWindow"]; + } + } + if (options.mercuryMetricsData) { + Object.assign(eventFields, options.mercuryMetricsData); + } + if (isSome(options.subjectIds)) { + eventFields["subjectIds"] = options.subjectIds; + } + const event = metricsBuilder.createMetricsClickData(objectGraph, options.id, targetType !== null && targetType !== void 0 ? targetType : metricsUtil.targetTypeForMetricsOptions(objectGraph, options), eventFields, additionalIncludingFields); + // Include button names in arcade buy action metrics + const isArcadeBuyAction = options.actionContext === "Arcade" && (options.actionType === "buy" || options.actionType === "buyInitiate"); + if (isArcadeBuyAction) { + event.includingFields.push("buttonName"); + // This hack where we add the adamId to the eventFields can be removed when `ActionDispatcher` + // is fully enabled and old ActionRunner code is removed. The custom data below replaces this. + event.fields[metricsConstants.contextualAdamIdKey] = options.contextualAdamId; + if (isSome(action.adamId)) { + action.actionMetrics.custom[metricsConstants.contextualAdamIdKey] = action.adamId; + } + } + // Include ad rotation metrics and click events if enabled for placement. + const shouldIncludeAdRotationFields = (_g = (_f = (_e = options.pageInformation) === null || _e === void 0 ? void 0 : _e.iAdInfo) === null || _f === void 0 ? void 0 : _f.shouldIncludeAdRotationFields) !== null && _g !== void 0 ? _g : false; + if (options.isAdvert && shouldIncludeAdRotationFields) { + event.includingFields.push("advertRotation"); + } + action.actionMetrics.addMetricsData(event); +} +/** + * + * @param objectGraph The App Store Object Graph + * @param action The search cancel or dismiss action + * @param options The metrics click options + * @param targetType The metrics click target type + * @param searchTerm The current search term + */ +export function addClickEventToSearchCancelOrDismissAction(objectGraph, action, options, targetType, searchTerm) { + const eventFields = { + searchTerm: searchTerm, + actionType: options.actionType, + }; + if (options.actionDetails) { + eventFields["actionDetails"] = options.actionDetails; + } + if (options.actionContext) { + eventFields["actionContext"] = options.actionContext; + } + // Search Term + if (searchTerm) { + eventFields["searchTerm"] = searchTerm; + } + if (serverData.isDefinedNonNull(options.softwareType)) { + eventFields["softwareType"] = options.softwareType; + } + if (options.mercuryMetricsData) { + Object.assign(eventFields, options.mercuryMetricsData); + } + const event = metricsBuilder.createMetricsClickData(objectGraph, options.id, targetType !== null && targetType !== void 0 ? targetType : metricsUtil.targetTypeForMetricsOptions(objectGraph, options), eventFields); + action.actionMetrics.addMetricsData(event); +} +export function addClickEventsToAdLockup(objectGraph, lockup, clickOptions) { + var _a, _b, _c, _d, _e; + const searchAd = (_b = (_a = lockup.searchAdOpportunity) === null || _a === void 0 ? void 0 : _a.searchAd) !== null && _b !== void 0 ? _b : lockup.searchAd; + if (serverData.isNull(searchAd)) { + return; + } + // Theoretically, we would probably be fine to just overwrite the click event action metrics below in `addClickEventToAction`. + // But we'll keep the call to clear the metrics just to provide continuity with older code that ran this full path. + (_c = lockup.clickAction) === null || _c === void 0 ? void 0 : _c.actionMetrics.clearAll(); + if (lockup.clickAction) { + addClickEventToAction(objectGraph, lockup.clickAction, clickOptions, true); + } + const pageInformation = clickOptions.pageInformation; + const eventFields = { + actionType: "ad_transparency", + }; + if (pageInformation === null || pageInformation === void 0 ? void 0 : pageInformation.iAdInfo) { + Object.assign(eventFields, pageInformation.iAdInfo.clickFields); + } + const figaroEvent = metricsBuilder.createMetricsClickData(objectGraph, lockup.adamId, "button", eventFields); + const shouldIncludeAdRotationFields = (_e = (_d = pageInformation === null || pageInformation === void 0 ? void 0 : pageInformation.iAdInfo) === null || _d === void 0 ? void 0 : _d.shouldIncludeAdRotationFields) !== null && _e !== void 0 ? _e : false; + if (shouldIncludeAdRotationFields) { + figaroEvent.includingFields.push("advertRotation"); + } + searchAd.transparencyAction.actionMetrics.addMetricsData(figaroEvent); +} +export function addClickEventToSeeAllAction(objectGraph, action, url, options) { + const eventFields = {}; + if (serverData.isDefinedNonNull(url)) { + eventFields["actionUrl"] = url; + } + if (!options.targetType) { + options.targetType = "button"; + } + eventFields["location"] = metricsLocation.createBasicLocation(objectGraph, options, action.title); + eventFields["actionType"] = "navigate"; + eventFields["target"] = "button_See All"; + const event = metricsBuilder.createMetricsClickData(objectGraph, "See All", "button", eventFields); + action.actionMetrics.addMetricsData(event); +} +export function addClickEventToClearSearchHistoryAction(objectGraph, clearAction) { + // MAINTAINER'S NOTE: + // We intentionally use an unlocalized targetId, instead of localized `clearAction.title`, + // so metrics are consistent regardless of language. + const clickMetrics = metricsBuilder.createMetricsClickData(objectGraph, "Clear Searches", "button", { + actionType: "confirm", + }); + clearAction.actionMetrics.addMetricsData(clickMetrics); +} +export function addClickEventToActivityFeedMetrics(objectGraph, actionMetrics, title, targetId, options) { + const eventFields = { + actionType: "navigate", + id: targetId, + idType: "static", + location: metricsLocation.createBasicLocation(objectGraph, options, title), + }; + const clickEvent = metricsBuilder.createMetricsClickData(objectGraph, targetId, "link", eventFields); + actionMetrics.addMetricsData(clickEvent); +} +export function addClickEventToPageFacetsChangeAction(objectGraph, action, filterParameter) { + const eventFields = {}; + eventFields["actionType"] = "filter"; + const event = metricsBuilder.createMetricsClickData(objectGraph, `filter_${filterParameter}`, "button", eventFields); + event.includingFields.push("selectedPageFacets"); + action.actionMetrics.addMetricsData(event); +} +//* ************************* +//* Search Metrics +//* ************************* +/** + * Adds a given `SearchAction` with the metrics data based on the Search it will fire, i.e data for: + * - Click Event + * - Search Event + * + * @param action Action to add metrics to + * @param target Target of this action. This should correspond to the UI this action is attached to. + * @param locationTracker Location tracker. + */ +export function addEventsToSearchAction(objectGraph, action, target, locationTracker, pageInformation) { + var _a, _b, _c, _d; + const actionType = metricsActionTypeForSearchOrigin(action.origin); + if (isNothing(pageInformation)) { + pageInformation = new metricsModels.MetricsPageInformation({ + page: "Search", + pageType: "Search", + pageId: "Search", + pageDetails: "Apps", // Legacy. Note sure why this is Apps. + }); + } + const options = { + pageInformation: pageInformation, + locationTracker: locationTracker, + targetType: target, + }; + // Click-Only Fields + const clickFields = { + ...metricsMisc.fieldsFromPageInformation(pageInformation), + actionType: actionType, + actionUrl: metricsUtil.emptyStringIfNullOrUndefined(action.url), + location: metricsLocation.createBasicLocation(objectGraph, options, action.term), + searchTerm: action.term, + }; + // Search-Only Fields + const searchFields = { + targetId: action.term, + }; + const searchActionDetails = {}; + if ((_a = action.prefixTerm) === null || _a === void 0 ? void 0 : _a.length) { + // Keep `searchPrefix` in `actionDetails` for search events + searchActionDetails["searchPrefix"] = action.prefixTerm; + } + if ((_b = action.entity) === null || _b === void 0 ? void 0 : _b.length) { + searchActionDetails["hintsEntity"] = action.entity; + } + if (serverData.isDefinedNonNullNonEmpty(searchActionDetails)) { + searchFields["actionDetails"] = searchActionDetails; + } + // Shared Fields + if ((_c = action.originatingTerm) === null || _c === void 0 ? void 0 : _c.length) { + clickFields["searchOriginatingTerm"] = action.originatingTerm; + searchFields["searchOriginatingTerm"] = action.originatingTerm; + } + // SSS: Clicks must be before Search + const clickData = metricsBuilder.createMetricsClickData(objectGraph, action.term, target, clickFields, [ + "searchGhostHint", + ]); + action.actionMetrics.addMetricsData(clickData); + const searchData = metricsBuilder.createMetricsSearchData(objectGraph, action.term, target, actionType, (_d = action.url) !== null && _d !== void 0 ? _d : null, searchFields, ["searchGhostHint"]); + action.actionMetrics.addMetricsData(searchData); +} +/** + * Returns the mapped action type for given search origin. + * @param origin Origin to resolve action type for + */ +function metricsActionTypeForSearchOrigin(origin) { + // actionType based on search origin. + switch (origin) { + case "trending": + return "trending"; + case "suggested": + return "suggested"; + case "recents": + return "recentQuery"; + case "hints": + return "hint"; + case "undoSpellCorrection": + return "searchInsteadFor"; + case "applySpellCorrection": + return "didYouMean"; + case "userTypedHint": + return "userTypedHint"; + // It is unexpected to see other search origins here. These are built in native: + default: + return "submit"; + } +} +// region Segmented Search +/** + * Add click metrics to segment change action + * @param action Action to add click event to. + * @param locationTracker Location tracker + */ +export function addEventsToSegmentChangeAction(objectGraph, action, targetId, locationTracker) { + // Click-Only Fields + const targetType = "link"; + const clickFields = { + actionType: "navigate", + location: metricsLocation.createBasicLocation(objectGraph, { + pageInformation: null, + locationTracker: locationTracker, + targetType: targetType, + }, action.title), + }; + const clickData = metricsBuilder.createMetricsClickData(objectGraph, targetId, targetType, clickFields); + action.actionMetrics.addMetricsData(clickData); +} +/** + * Add click metrics to segment change action + * @param action Action to add click event to. + * @param locationTracker Location tracker + */ +export function addClickEventToSearchPageSegmentChangeAction(objectGraph, action, targetId, locationTracker) { + // Click-Only Fields + const targetType = "SearchResults"; + const clickFields = { + actionType: "navigate", + location: metricsLocation.createBasicLocation(objectGraph, { + pageInformation: null, + locationTracker: locationTracker, + targetType: targetType, + }, "searchPageSegmentChange"), + }; + const clickData = metricsBuilder.createMetricsClickData(objectGraph, targetId, targetType, clickFields); + action.actionMetrics.addMetricsData(clickData); +} +// endregion +//# sourceMappingURL=clicks.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/constants.js b/node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/constants.js new file mode 100644 index 0000000..c23084b --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/constants.js @@ -0,0 +1,11 @@ +/** + * HACK: <rdar://problem/65687333> Tech Debt: Metrics: Adam ID attribution should be explicit + * This is an incremental step out of a hack that used `targetId` as `adamId` for decorating adamId-related fields for a given `MetricsData`. + * It should be leveraged for paths that use field providers that have an adam id dependency. + * + * As of 2022E we are using this key in the `custom` field of `actionMetrics`, which is a recommended method of sending down + * additional data to augment metrics events but shouldn't be included in the event itself. `custom` fields are automatically + * added to the `MetricsFieldsContext` by `ActionDispatcher`, allowing `MetricsFieldsProvider`s to access and use this value. + */ +export const contextualAdamIdKey = "jet_adamId"; +//# sourceMappingURL=constants.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/impressions.js b/node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/impressions.js new file mode 100644 index 0000000..edc1d96 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/impressions.js @@ -0,0 +1,419 @@ +import * as metricsLocation from "./location"; +import * as metricsUtil from "./util"; +import { isNothing, isSome } from "@jet/environment"; +import * as validation from "@jet/environment/json/validation"; +import * as models from "../../../api/models"; +import * as serverData from "../../../foundation/json-parsing/server-data"; +import * as contentAttributes from "../../content/attributes"; +import * as content from "../../content/content"; +import * as productVariants from "../../product-page/product-page-variants"; +import * as lockups from "../../../common/lockups/lockups"; +//* ************************* +//* Impression Metrics +//* ************************* +function generateImpressionFields(objectGraph, options) { + /// please note this is not the metrics event itself but the fields for the items that impress + /// there is a very strict spec for these items so please do not modify this unless you know + /// exactly what you are doing. + var _a, _b, _c, _d; + let id = options.id; + let title = options.title; + if (serverData.isDefinedNonNullNonEmpty(options.anonymizationOptions)) { + const anonymizationString = (_b = (_a = options.anonymizationOptions) === null || _a === void 0 ? void 0 : _a.anonymizationString) !== null && _b !== void 0 ? _b : "ANONYMOUS"; + id = anonymizationString; + title = anonymizationString; + } + else if (id && options.createUniqueImpressionId && !objectGraph.client.isWatch) { + id = createUniqueImpressionId(objectGraph, id); + } + const impressionData = { + id: metricsUtil.emptyStringIfNullOrUndefined(id), + name: metricsUtil.emptyStringIfNullOrUndefined(title), + impressionType: metricsUtil.targetTypeForMetricsOptions(objectGraph, options), + }; + const idType = metricsUtil.idTypeForMetricsOptions(options); + if (isSome(idType)) { + impressionData["idType"] = idType; + } + if (options && options.kind) { + impressionData["kind"] = options.kind; + } + if (options && options.softwareType) { + impressionData["softwareType"] = options.softwareType; + } + if (options && options.recoMetricsData) { + Object.assign(impressionData, options.recoMetricsData); + } + if (options && options.mercuryMetricsData) { + Object.assign(impressionData, options.mercuryMetricsData); + } + if (options && options.lockupDisplayStyle) { + impressionData["platformDisplayStyle"] = options.lockupDisplayStyle; + } + const shouldOmitImpressionIndex = (_c = options.shouldOmitImpressionIndex) !== null && _c !== void 0 ? _c : false; + if (options && options.locationTracker && !shouldOmitImpressionIndex) { + const currentPosition = metricsLocation.currentPosition(options.locationTracker); + impressionData["impressionIndex"] = currentPosition; + if (impressionData.id === "") { + impressionData.id = currentPosition.toString(); + impressionData["idType"] = "sequential"; + } + } + if (options && options.modelSource) { + impressionData["modelSource"] = options.modelSource; + } + // Add offerType if available + if (serverData.isDefinedNonNull(options.offerType)) { + impressionData["offerType"] = options.offerType; + } + // Arcade Upsell Tracking + if (options && serverData.isDefinedNonNull(options.displaysArcadeUpsell)) { + impressionData["displaysArcadeUpsell"] = options.displaysArcadeUpsell; + } + // Preorder Tracking + if (options && serverData.isDefinedNonNull(options.isPreorder)) { + impressionData["isPreorder"] = options.isPreorder; + } + // Add adamId if available + if (serverData.isDefinedNonNull(options.adamId) && serverData.isNullOrEmpty(options.anonymizationOptions)) { + impressionData["adamId"] = options.adamId; + } + // Badges + if (options && serverData.isDefinedNonNull(options.badges)) { + impressionData["badges"] = options.badges; + } + // In App Event ID + if (options && serverData.isDefinedNonNull(options.inAppEventId)) { + impressionData["inAppEventId"] = options.inAppEventId; + } + // Related subject IDs + if (options && serverData.isDefinedNonNull(options.relatedSubjectIds)) { + impressionData["relatedSubjectIds"] = options.relatedSubjectIds; + } + /// Hints entity + if ((_d = options === null || options === void 0 ? void 0 : options.hintsEntity) === null || _d === void 0 ? void 0 : _d.length) { + impressionData["hintsEntity"] = options.hintsEntity; + } + // autoAdvanceInterval for auto scrolling Views + if (options && serverData.isDefinedNonNull(options.autoAdvanceInterval)) { + impressionData["autoAdvanceInterval"] = options.autoAdvanceInterval; + } + // Add fcKind if available + if (serverData.isDefinedNonNull(options === null || options === void 0 ? void 0 : options.fcKind)) { + impressionData["fcKind"] = options.fcKind; + } + if (serverData.isDefinedNonNull(options === null || options === void 0 ? void 0 : options.canonicalId)) { + impressionData["canonicalId"] = options.canonicalId; + } + if (serverData.isDefinedNonNull(options === null || options === void 0 ? void 0 : options.displayStyle)) { + impressionData["displayStyle"] = options.displayStyle; + } + // Add product variant fields if available + if (serverData.isDefinedNonNull(options.productVariantData)) { + Object.assign(impressionData, productVariants.contentFieldsForProductVariantData(options.productVariantData)); + } + if (isSome(options.contentRating)) { + impressionData["contentRating"] = options.contentRating; + } + if (isSome(options.bundleId)) { + impressionData["bundleId"] = options.bundleId; + } + if (impressionData.id === "") { + objectGraph.console.log(`impressionId missing. Tracking broken for ${impressionData.name} of ${impressionData.impressionType}`); + } + return impressionData; +} +export function addImpressionFields(objectGraph, impressionable, options) { + if (!impressionable) { + return; + } + impressionable.impressionMetrics = new models.ImpressionMetrics(generateImpressionFields(objectGraph, options)); +} +export function addImpressionFieldsToTagRoomHeader(objectGraph, impressionable, options) { + if (!impressionable) { + return; + } + const impressionMetrics = new models.ImpressionMetrics(generateImpressionFields(objectGraph, options)); + delete impressionMetrics.fields.impressionIndex; + impressionable.impressionMetrics = impressionMetrics; +} +/** + * Adds impression fields to the search metadata ribbon item. + * + * @param objectGraph - The object graph. + * @param metadataRibbonItem - The metadata ribbon item. + * @param options - The metrics impression options. + */ +export function addImpressionFieldsToSearchMetadataRibbonItem(objectGraph, metadataRibbonItem, options) { + var _a; + if (!metadataRibbonItem) { + return; + } + addImpressionFields(objectGraph, metadataRibbonItem, options); + if ((options.isAdvert || options.isAdEligible) && + ((_a = options.pageInformation) === null || _a === void 0 ? void 0 : _a.iAdInfo) && + metadataRibbonItem.impressionMetrics) { + metadataRibbonItem.impressionMetrics = new models.FastImpressionMetrics(metadataRibbonItem.impressionMetrics, true); + } +} +/** + * Add impressions fields to a Today card. + * @param objectGraph The object graph. + * @param card The Today card to apply the impressions fields to. + * @param options A set of metrics options to gather data from. + * @param franchise The card franchise. + * @param cardType The card type. + * @param isOnboardingCard Whether the card is an onboarding card. + * @param coerceNullToEmptyStrings Whether `null` values for `franchise` and `cardType` should be coerced into empty strings. This + * was a behaviour that was always enabled by default, but as of the implementation of Chainlink ads, we don't necessarily want empty strings. + * Defaults to true to maintain legacy behaviour for other cards that don't want to opt out. + * @returns + */ +export function addImpressionsFieldsToTodayCard(objectGraph, card, options, franchise, cardType, isOnboardingCard, coerceNullToEmptyStrings = true) { + var _a, _b, _c, _d, _e, _f; + if (!card) { + return; + } + const impressionData = generateImpressionFields(objectGraph, options); + if (coerceNullToEmptyStrings) { + impressionData["franchise"] = metricsUtil.emptyStringIfNullOrUndefined(franchise); + impressionData["cardType"] = metricsUtil.emptyStringIfNullOrUndefined(cardType); + } + else { + if (franchise) { + impressionData["franchise"] = franchise; + } + if (cardType) { + impressionData["cardType"] = cardType; + } + } + if (isOnboardingCard) { + impressionData["isOnboardingCard"] = isOnboardingCard; + } + if (((_b = (_a = options === null || options === void 0 ? void 0 : options.optimizationEntityId) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0) > 0) { + impressionData["optimizationEntityId"] = options.optimizationEntityId; + } + if (((_d = (_c = options === null || options === void 0 ? void 0 : options.optimizationId) === null || _c === void 0 ? void 0 : _c.length) !== null && _d !== void 0 ? _d : 0) > 0) { + impressionData["optimizationId"] = options.optimizationId; + } + if (isSome(options === null || options === void 0 ? void 0 : options.rowIndex)) { + impressionData["rowIndex"] = options.rowIndex; + } + card.impressionMetrics = new models.ImpressionMetrics(impressionData); + if ((options.isAdvert || options.isAdEligible) && ((_e = options.pageInformation) === null || _e === void 0 ? void 0 : _e.iAdInfo)) { + const sanitizedIAdData = metricsUtil.sanitizedMetricsDictionary(options.pageInformation.iAdInfo.fastImpressionsFieldsForCurrentItem(options.locationTracker, options.adSlotOverride)); + Object.assign(card.impressionMetrics.fields, sanitizedIAdData); + card.impressionMetrics = new models.FastImpressionMetrics(card.impressionMetrics, true); + if (options.isAdvert) { + (_f = card.impressionMetrics) === null || _f === void 0 ? true : delete _f.fields["cardType"]; + } + } +} +export function addImpressionsFieldsToAd(objectGraph, impressionable, options, iAdData) { + if (!impressionable || !iAdData) { + return; + } + addImpressionFields(objectGraph, impressionable, options); + const sanitizedIAdData = metricsUtil.sanitizedMetricsDictionary(iAdData.fastImpressionsFieldsForCurrentItem(options.locationTracker, options.adSlotOverride)); + if (isSome(impressionable.impressionMetrics)) { + Object.assign(impressionable.impressionMetrics.fields, sanitizedIAdData); + const disableFastImpressions = serverData.asBooleanOrFalse(options.disableFastImpressionsForAds); + impressionable.impressionMetrics = new models.FastImpressionMetrics(impressionable.impressionMetrics, !disableFastImpressions); + } + /** + * This is an longstanding hack to prevent ad and organic impression showing same content (i.e. AdamId) + * to properly have nonclashing identifiers, and have separate impression objects. + * Even with impression item ids' hasing on `impressionIndex` - this is still needed in case the Nth rotated ad is the same as Nth organic. + * This is orthogonal to "ad_container" in location stack when building ads for ad-rotation, which is a separate metrics trick. + */ + if (isSome(impressionable.impressionMetrics)) { + impressionable.impressionMetrics.fields["parentId"] = "ad_container"; + } +} +export function addImpressionFieldsToInAppPurchaseLockup(objectGraph, lockup, options) { + if (!lockup) { + return; + } + // The code that calls this method currently does this indirectly, + // this is just to guard against that code changing in the future. + if (!lockup.impressionMetrics) { + addImpressionFields(objectGraph, lockup, options); + } + if (lockup.parent && lockup.parent.adamId && isSome(lockup.impressionMetrics)) { + lockup.impressionMetrics.fields["parentAdamId"] = metricsUtil.emptyStringIfNullOrUndefined(lockup.parent.adamId); + } +} +export function impressionOptionsForLockup(objectGraph, data, lockup, displayStyle, baseOptions, canDisplayArcadeOfferButton, attributePlatformOverride = undefined) { + var _a; + const options = impressionOptions(objectGraph, data, lockup.title, baseOptions); + options.lockupDisplayStyle = displayStyle; + options.contentRating = (_a = lockup.offerDisplayProperties) === null || _a === void 0 ? void 0 : _a.contentRating; + options.bundleId = lockup.bundleId; + // If no targetType is provided, set the correct value for the platform. + if (serverData.isNullOrEmpty(options.targetType)) { + options.targetType = objectGraph.client.isVision ? "lockupSmall" : "lockup"; + } + if (canDisplayArcadeOfferButton && content.isArcadeSupported(objectGraph, data)) { + options.displaysArcadeUpsell = true; + } + // If it has a discounted offer then use the options already set + const parentID = baseOptions["id"]; + if (serverData.isDefinedNonNullNonEmpty(lockups.discountedOfferFromData(data)) && + serverData.isDefinedNonNull(parentID) && + parentID.length > 0) { + options.id = parentID; + } + return options; +} +export function impressionOptions(objectGraph, data, title, baseOptions, attributePlatformOverride = undefined) { + return validation.context("impressionOptions", () => { + const kind = metricsUtil.metricsKindFromData(objectGraph, data); + const softwareType = metricsUtil.softwareTypeForData(objectGraph, data); + const metricsOptions = { + ...baseOptions, + kind: kind, + softwareType: softwareType, + title: title, + id: data.id, + }; + // Include offerType for pre-order impressions + // NOTE: Even though metricsOptions.isPreorder may be true, we don't key off that here because + // offerType implies an offer exists, which is not always true (e.g. an Arcade Coming Soon breakout). + const containsPreorderOffer = contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "isPreorder", attributePlatformOverride); + if (containsPreorderOffer) { + metricsOptions.offerType = "preorder"; + } + return metricsOptions; + }); +} +/// Returns impression options for Arcade See All Games ribbon item. +export function impressionOptionsForArcadeSeeAllGamesRibbonItem(baseOptions) { + return validation.context("impressionOptionsForArcadeSeeAllGamesRibbonItem", () => { + const metricsOptions = { + ...baseOptions, + id: "AllGames", + idType: "none", + kind: null, + softwareType: null, + title: "All Games", + }; + return metricsOptions; + }); +} +/// Returns impression options for tag ribbon item on product page. +export function impressionOptionsForTagRibbonItem(objectGraph, data, title, baseOptions) { + return validation.context("impressionOptions", () => { + const kind = metricsUtil.metricsKindFromData(objectGraph, data); + const softwareType = metricsUtil.softwareTypeForData(objectGraph, data); + const metricsOptions = { + ...baseOptions, + kind: kind, + softwareType: softwareType, + title: title, + id: data.id, + idType: "its_id", + displayStyle: "textOnly", + }; + return metricsOptions; + }); +} +/// Returns impression options for tag ribbon item on product page. +export function impressionOptionsForTagHeader(objectGraph, data, title, baseOptions) { + return validation.context("impressionOptions", () => { + const kind = metricsUtil.metricsKindFromData(objectGraph, data); + const softwareType = metricsUtil.softwareTypeForData(objectGraph, data); + const metricsOptions = { + ...baseOptions, + kind: kind, + softwareType: softwareType, + title: title, + id: data.id, + idType: "its_contentId", + targetType: "tagHeader", + }; + return metricsOptions; + }); +} +/// Returns impression options for Arcade Choose Your Favorites brick. +export function impressionOptionsForArcadeChooseYourFavoritesBrick(baseOptions) { + return validation.context("impressionOptionsForArcadeChooseYourFavoritesBrick", () => { + const metricsOptions = { + ...baseOptions, + id: "", + kind: null, + softwareType: null, + title: "choose_your_games_brick", + }; + return metricsOptions; + }); +} +export function impressionOptionsForMetadataRibbonItem(baseOptions, id, name, idType) { + return validation.context("impressionOptionsForMetadataRibbonItem", () => { + const metricsOptions = { + ...baseOptions, + id: id, + kind: null, + softwareType: null, + title: name, + idType: idType, + targetType: "tag", + }; + return metricsOptions; + }); +} +// region Hints Impressions +export function impressionOptionsForSearchHint(objectGraph, hintTerm, baseOptions, searchEntity, hintSource) { + return validation.context("impressionOptionsForSearchHint", () => { + const metricsOptions = { + ...baseOptions, + id: "", + kind: null, + softwareType: null, + title: hintTerm, + hintsEntity: searchEntity, + modelSource: hintSource, + }; + return metricsOptions; + }); +} +export function addImpressionMetricsToHintsSearchAction(objectGraph, searchAction, metricsOptions) { + const options = impressionOptionsForSearchHint(objectGraph, searchAction.term, metricsOptions, searchAction.entity, searchAction.source); + const impressionFields = generateImpressionFields(objectGraph, options); + searchAction.impressionMetrics = new models.ImpressionMetrics(impressionFields); +} +const uniqueIdDelimiter = "::"; +function createUniqueImpressionId(objectGraph, baseId) { + return `${baseId}${uniqueIdDelimiter}${objectGraph.random.nextUUID()}`; +} +/** + * Strips the unique impression ID from the event fields if necessary. + * + * @param eventFields - The event fields containing impressions. + */ +export function stripUniqueImpressionIdsIfNecessary(eventFields) { + const impressions = serverData.asArrayOrEmpty(eventFields, "impressions"); + for (const impression of impressions) { + const impressionId = serverData.asString(impression, "id"); + if (isNothing(impressionId)) { + continue; + } + impression["id"] = stripUniqueImpressionIdIfNecessary(impressionId); + } +} +/** + * Strips the unique impression ID from the given impression ID if necessary. + * If the impression ID contains a unique ID delimiter, it splits the string + * and returns the part before the delimiter. Otherwise, it returns the original + * impression ID. + * + * @param impressionId - The impression ID to process. + * @returns The processed impression ID. + */ +function stripUniqueImpressionIdIfNecessary(impressionId) { + if (impressionId.includes(uniqueIdDelimiter)) { + return impressionId.split(uniqueIdDelimiter)[0]; + } + return impressionId; +} +// endregion +//# sourceMappingURL=impressions.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/legacy-metrics-identifier-fields-opt-out.js b/node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/legacy-metrics-identifier-fields-opt-out.js new file mode 100644 index 0000000..4232a33 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/legacy-metrics-identifier-fields-opt-out.js @@ -0,0 +1,18 @@ +/** + * Hack for injecting clientId and metrics data if needed. + */ +import { AppStoreMetricsData } from "../../../api/models"; +/** + * Opt out of legacy metrics id fields provider, the `AMSMetricsIdentifierFieldsProvider` instead + * we'll rely solely on `MetricsIdFieldsProvider` added for Katana + * @param objectGraph - The object graph. + * @param metricsData - The metrics data. + * @returns The metrics data, with the `amsMetricsID` field excluded. + */ +export function optOutOfLegacyMetricsIdFieldsProvider(objectGraph, metricsData) { + var _a; + const excludingFields = (_a = metricsData.excludingFields) !== null && _a !== void 0 ? _a : []; + excludingFields.push("amsMetricsID"); + return new AppStoreMetricsData(metricsData.fields, metricsData.includingFields, excludingFields, metricsData.topic, metricsData.shouldFlush); +} +//# sourceMappingURL=legacy-metrics-identifier-fields-opt-out.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/location.js b/node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/location.js new file mode 100644 index 0000000..f7ffd4e --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/location.js @@ -0,0 +1,188 @@ +import * as validation from "@jet/environment/json/validation"; +import { isNothing, isSome } from "@jet/environment/types/optional"; +import { asJSONData } from "../../../foundation/json-parsing/server-data"; +import * as productVariant from "../../product-page/product-page-variants"; +import * as metricsUtil from "./util"; +class MetricsLocationStackItem { + constructor() { + this.position = 0; + } +} +export function newLocationTracker() { + return { + rootPosition: 0, + locationStack: [], + }; +} +/** + * @param locationTracker The location tracker to copy + * @returns A copy of the location tracker + */ +export function createLocationTrackerCopy(locationTracker) { + const locationStackCopy = []; + for (const locationStackEntry of locationTracker.locationStack) { + locationStackCopy.push({ + ...locationStackEntry, + }); + } + return { + rootPosition: locationTracker.rootPosition, + locationStack: locationStackCopy, + }; +} +export function createContentLocation(objectGraph, options, title) { + const locations = stackItemsToLocationStack(options.locationTracker); + const contentLocation = newContentLocation(objectGraph, options, title); + return [contentLocation, ...locations]; +} +export function createBasicLocation(objectGraph, options, title) { + const locations = stackItemsToLocationStack(options.locationTracker); + const basicLocation = newBasicLocation(objectGraph, options, title); + return [basicLocation, ...locations]; +} +export function pushContentLocation(objectGraph, options, title) { + const stackItem = new MetricsLocationStackItem(); + stackItem.location = newContentLocation(objectGraph, options, title); + options.locationTracker.locationStack.unshift(stackItem); +} +export function pushBasicLocation(objectGraph, options, title) { + const stackItem = new MetricsLocationStackItem(); + stackItem.location = newBasicLocation(objectGraph, options, title); + options.locationTracker.locationStack.unshift(stackItem); +} +export function popLocation(tracker) { + if (tracker.locationStack.length === 0) { + validation.unexpectedType("ignoredValue", "non-empty location stack", "empty location stack"); + return; + } + tracker.locationStack.shift(); +} +export function currentPosition(tracker) { + const stackItem = lastStackItemAdded(tracker); + if (stackItem) { + return stackItem.position; + } + else { + return tracker.rootPosition; + } +} +export function previousPosition(tracker) { + if (tracker.locationStack.length < 2) { + return null; + } + return tracker.locationStack[1].position; +} +export function currentLocation(tracker) { + const stackItem = lastStackItemAdded(tracker); + if (stackItem) { + return stackItem.location; + } + else { + return null; + } +} +/** + * Set current position of tracker. This is necessary when large today modules are broken apart into multipart shelves. + * We need to preserve the position of content within server-response, not our logical shelves. + * @param tracker Tracker update position of. + * @param position Position to set to. + */ +export function setCurrentPosition(tracker, position) { + const stackItem = lastStackItemAdded(tracker); + if (stackItem) { + stackItem.position = position; + } + else { + tracker.rootPosition = position; + } +} +export function nextPosition(tracker) { + const stackItem = lastStackItemAdded(tracker); + if (stackItem) { + stackItem.position++; + } + else { + tracker.rootPosition++; + } +} +function newContentLocation(objectGraph, options, title) { + var _a; + const base = newBasicLocation(objectGraph, options, title); + // Use the location tracker if there is no id override + if (!options.id && options.locationTracker) { + base.idType = "sequential"; + base.id = currentPosition(options.locationTracker).toString(); + } + else { + // If there is a id specified, use that + const idType = metricsUtil.idTypeForMetricsOptions(options); + if (isSome(idType)) { + base.idType = idType; + } + let id = options.id; + if ((_a = options.anonymizationOptions) === null || _a === void 0 ? void 0 : _a.anonymizationString) { + id = options.anonymizationOptions.anonymizationString; + } + base.id = isNothing(id) ? "" : id; + } + if (options.fcKind) { + base.fcKind = options.fcKind; + } + if (options.displayStyle) { + base.displayStyle = options.displayStyle; + } + if (options.inAppEventId) { + base.inAppEventId = options.inAppEventId; + } + if (options.relatedSubjectIds) { + base.relatedSubjectIds = options.relatedSubjectIds; + } + if (options.canonicalId) { + base.canonicalId = options.canonicalId; + } + if (options.optimizationEntityId) { + base.optimizationEntityId = options.optimizationEntityId; + } + if (options.optimizationId) { + base.optimizationId = options.optimizationId; + } + if (isSome(options.rowIndex)) { + base.rowIndex = options.rowIndex; + } + if (options.productVariantData) { + Object.assign(base, productVariant.contentFieldsForProductVariantData(options.productVariantData)); + } + return base; +} +function newBasicLocation(objectGraph, options, title) { + var _a, _b; + let name = title; + if ((_a = options.anonymizationOptions) === null || _a === void 0 ? void 0 : _a.anonymizationString) { + name = options.anonymizationOptions.anonymizationString; + } + const location = { + locationPosition: currentPosition(options.locationTracker), + locationType: metricsUtil.targetTypeForMetricsOptions(objectGraph, options), + name: isNothing(name) ? "" : name, + }; + if (isSome(options.badges)) { + location.badges = (_b = asJSONData(options.badges)) !== null && _b !== void 0 ? _b : undefined; + } + if (options.recoMetricsData) { + Object.assign(location, options.recoMetricsData); + } + return location; +} +function stackItemsToLocationStack(tracker) { + return tracker.locationStack.map((stackItem) => { + return stackItem.location; + }); +} +function lastStackItemAdded(tracker) { + const length = tracker.locationStack.length; + if (length === 0) { + return null; + } + return tracker.locationStack[0]; +} +//# sourceMappingURL=location.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/media.js b/node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/media.js new file mode 100644 index 0000000..0b12cf9 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/media.js @@ -0,0 +1,34 @@ +import { isNothing, isSome } from "@jet/environment"; +import * as objects from "../../../foundation/util/objects"; +import * as metricsBuilder from "../builder"; +import * as metricsLocation from "./location"; +import * as misc from "./misc"; +import * as metricsUtil from "./util"; +//* ************************* +//* Media Metrics +//* ************************* +export function addMetricsEventsToVideo(objectGraph, video, options) { + if (isNothing(video)) { + return; + } + const mediaEventFields = misc.fieldsFromPageInformation(options.pageInformation); + if (mediaEventFields === null) { + return; + } + mediaEventFields["id"] = metricsUtil.emptyStringIfNullOrUndefined(options.id); + const idType = metricsUtil.idTypeForMetricsOptions(options); + if (isSome(idType)) { + mediaEventFields["idType"] = idType; + } + mediaEventFields["type"] = "video"; + mediaEventFields["typeDetails"] = "iTunesStoreContent"; + mediaEventFields["location"] = metricsLocation.createContentLocation(objectGraph, options, ""); + if (options.actionDetails) { + mediaEventFields["actionDetails"] = options.actionDetails; + } + video.templateMediaEvent = metricsBuilder.createMetricsMediaData(objectGraph, mediaEventFields); + const clickEventFields = objects.shallowCopyOf(mediaEventFields); + clickEventFields["actionUrl"] = video.videoUrl; + video.templateClickEvent = metricsBuilder.createMetricsMediaClickData(objectGraph, null, "button", clickEventFields); +} +//# sourceMappingURL=media.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/misc.js b/node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/misc.js new file mode 100644 index 0000000..ffb0b77 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/misc.js @@ -0,0 +1,46 @@ +import { isDefinedNonNull } from "../../../foundation/json-parsing/server-data"; +import { pageFieldsForPageInfoProductVariantData } from "../../product-page/product-page-variants"; +import { EventLinter } from "../event-linter"; +import * as metricsUtil from "./util"; +// region Page Information +/** + * Returns a set of pageFields from a given `pageInformation` - i.e. fields that are included on metrics data with `pageField` IncludingFields, and buyParams. + * It is not recommended to use this function for instrumentation that involves field providers, as these page fields will overwrite them. + * @param pageInformation Page information to create page fields for. + */ +export function fieldsFromPageInformation(pageInformation) { + var _a; + const fields = {}; + if (!pageInformation) { + return fields; + } + Object.assign(fields, pageInformation.baseFields); + if (pageInformation.pageUrl) { + fields["pageUrl"] = pageInformation.pageUrl; + } + else if (pageInformation.timingMetrics && pageInformation.timingMetrics.pageURL) { + fields["pageUrl"] = pageInformation.timingMetrics.pageURL; + } + if (pageInformation.recoMetricsData) { + Object.assign(fields, pageInformation.recoMetricsData); + } + if (pageInformation.mercuryMetricsData) { + Object.assign(fields, pageInformation.mercuryMetricsData); + } + if (pageInformation.productVariantData) { + Object.assign(fields, pageFieldsForPageInfoProductVariantData(pageInformation.productVariantData)); + } + if (pageInformation.iAdInfo && isDefinedNonNull(pageInformation.iAdInfo.pageFields[EventLinter.hasIAdData])) { + fields[EventLinter.hasIAdData] = pageInformation.iAdInfo.pageFields[EventLinter.hasIAdData]; + } + const iAdId = (_a = pageInformation.iAdInfo) === null || _a === void 0 ? void 0 : _a.pageFields["iAdId"]; + if (isDefinedNonNull(iAdId)) { + fields["iAdId"] = iAdId; + } + return metricsUtil.sanitizedMetricsDictionary(fields); +} +// endregion +// region Network Performance +// @see `JSNetworkPerformanceMetrics.metrics(fromResult:)` in native. +// endregion +//# sourceMappingURL=misc.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/models.js b/node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/models.js new file mode 100644 index 0000000..3456e9e --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/models.js @@ -0,0 +1,671 @@ +import * as metricsLocation from "./location"; +import * as metricsUtil from "./util"; +import { isNothing, isSome } from "@jet/environment/types/optional"; +import * as serverData from "../../../foundation/json-parsing/server-data"; +import { attributeAsDictionary } from "../../../foundation/media/attributes"; +import { shallowCopyOf } from "../../../foundation/util/objects"; +import { productVariantDataForData, productVariantDataHasVariant } from "../../product-page/product-page-variants"; +import { EventLinter } from "../event-linter"; +import { eligibleSlotPositionsForAdPlacement } from "../../ads/ad-common"; +import { FlattenedTodayItemType } from "../../today/today-parse-util"; +import { getSelectedCustomCreativeId } from "../../search/custom-creative"; +export const iAdURLParameterStringToken = "X-AppStore-iAdClickToken"; +export const iAdURLLineItemParameterStringToken = "X-AppStore-iAdLineItem"; +export const iAdDismissAdActionMetricsParameterStringToken = "X-AppStore-iAdDismissAdActionMetrics"; +// User defined type guard for determining if an object conforms to ContentMetricsOptions interface. +export function isContentMetricsOptions(object) { + return object && Object.prototype.hasOwnProperty.call(object, "id"); +} +export class IAdSearchInformation { + /** + * Initialise a new `IAdSearchInformation` + * @param objectGraph The Object Graph. + * @param placementType The placement type for the ad this object is tracking. + * @param baseSlotInformation The initial list of slotInfos for this ad placement + * @param iAdId The unique id for the ad instance. + * @param appStoreClientRequestId The unique id for the client requesting the ad. + * @param wasOdmlSuccessful Whether native ODML processing was successful. + * @param positionInfo The position info data describing the requested position of this ad. + */ + constructor(objectGraph, placementType, baseSlotInformation, iAdId, appStoreClientRequestId, wasOdmlSuccessful, positionInfo) { + this.placementType = placementType; + this.placementId = placementType === null ? null : this.placementIdFromType(placementType); + this.pageFields = {}; + this.clickFields = {}; + this.impressionsFields = {}; + this.fastImpressionFields = {}; + this.iAdClickEventFields = {}; + this._iAdApplied = false; + this._iAdAdamId = undefined; + this.positionInfo = positionInfo; + this.slotInfo = baseSlotInformation; + this.setInitialAdData(objectGraph, iAdId, appStoreClientRequestId); + if (serverData.isDefinedNonNull(wasOdmlSuccessful)) { + this.pageFields["iAdOdmlSuccess"] = wasOdmlSuccessful; + } + this.fastImpressionFields["iAdEligible"] = true; + } + /** + * Construct an IAdSearchInformation from the given JSON representation. + * This is necessary over and above standard JSON parsing to preserve our ability to call functions on this object. + * @param objectGraph The Object Graph. + * @param json The JSON representation of the object. + * @returns A constructed IAdSearchInformation from the JSON. + */ + static from(objectGraph, json) { + var _a, _b, _c, _d; + const iAdInfo = new IAdSearchInformation(objectGraph, serverData.asString(json.placementType), serverData.asArrayOrEmpty(json.slotInfo), (_a = serverData.asString(json.iAdId)) !== null && _a !== void 0 ? _a : undefined, (_b = serverData.asString(json.appStoreClientRequestId)) !== null && _b !== void 0 ? _b : undefined, (_c = serverData.asBoolean(json.wasOdmlSuccessful)) !== null && _c !== void 0 ? _c : undefined, serverData.asInterface(json.positionInfo)); + iAdInfo._iAdApplied = serverData.asBooleanOrFalse(json._iAdApplied); + iAdInfo._iAdAdamId = (_d = serverData.asString(json._iAdAdamId)) !== null && _d !== void 0 ? _d : undefined; + Object.assign(iAdInfo.pageFields, json.pageFields); + Object.assign(iAdInfo.clickFields, json.clickFields); + Object.assign(iAdInfo.impressionsFields, json.impressionsFields); + Object.assign(iAdInfo.fastImpressionFields, json.fastImpressionFields); + Object.assign(iAdInfo.iAdClickEventFields, json.iAdClickEventFields); + iAdInfo.updateContainerId(serverData.asString(json.containerId)); + return iAdInfo; + } + /** + * Create an array of `IAdSlotInformation` objects based on the current ad information available. + * This isn't ideal, but we need to understand the available ad slots so we can report + * on all slots, whether they have ads in them or not. + */ + static createInitialSlotInfos(objectGraph, placementType, positionInfo, flattenedTodayFeed) { + var _a; + switch (placementType) { + case "productPageYMAL": + case "productPageYMALDuringDownload": + const productPageContainerId = IAdSearchInformation.containerIdFromType(placementType); + const slotIndex = (_a = positionInfo === null || positionInfo === void 0 ? void 0 : positionInfo.slot) !== null && _a !== void 0 ? _a : 0; + const productPageSlot = { + slotId: `${productPageContainerId}_${slotIndex}`, + slotIndex: slotIndex, + hasAdData: false, + }; + return [ + { + containerId: productPageContainerId, + slots: [productPageSlot], + }, + ]; + break; + case "today": + const placementEligibleSlotPositions = eligibleSlotPositionsForAdPlacement(objectGraph, placementType); + const eligibleAdSlots = isNothing(placementEligibleSlotPositions) + ? [] + : placementEligibleSlotPositions.map((slotPosition) => slotPosition.slot); + const containerSlotInfoMapping = {}; + /// iAd provides slots that are not zero based, adjust the slot + const adjustedAdSlot = isSome(positionInfo) ? positionInfo.slot - 1 : null; + let adPositionEncountered = false; + eligibleAdSlots.forEach((eligibleIndex) => { + var _a; + // If we've reached the ad we need to subtract 1 from the index, to act as if the ad ad was + // actually part of the feed. + const augmentedFeedIndexIndex = adPositionEncountered ? eligibleIndex - 1 : eligibleIndex; + const todayItem = flattenedTodayFeed === null || flattenedTodayFeed === void 0 ? void 0 : flattenedTodayFeed.find((item) => item.containedAdSlots.includes(augmentedFeedIndexIndex)); + const isAdSlotIndex = adjustedAdSlot === eligibleIndex; + const todayContainerId = iAdContainerIdForSlotInTodayItem(augmentedFeedIndexIndex, isAdSlotIndex, todayItem); + const containerSlotInformation = (_a = containerSlotInfoMapping[todayContainerId]) !== null && _a !== void 0 ? _a : { + containerId: todayContainerId, + slots: [], + }; + containerSlotInfoMapping[todayContainerId] = containerSlotInformation; + const todaySlot = { + slotId: `${todayContainerId}_${eligibleIndex}`, + slotIndex: eligibleIndex, + hasAdData: false, + }; + containerSlotInformation.slots.push(todaySlot); + adPositionEncountered = adPositionEncountered || isAdSlotIndex; + }); + return Object.values(containerSlotInfoMapping); + default: + return null; + } + } + get iAdIsPresent() { + return this._iAdApplied; + } + get iAdAdamId() { + return this._iAdAdamId; + } + /** + * Update our information with a new ad response. + * This is primarily used for asynchronous ad requests, where we need to update the metrics info subsequent to the initial page load. + * @param objectGraph The Object Graph. + * @param adResponse An ad response to use to update some fields. + */ + updateForAdResponse(objectGraph, adResponse) { + var _a; + if (serverData.isNull(adResponse)) { + return; + } + this.placementType = adResponse.placementType; + this.placementId = this.placementIdFromType(this.placementType); + this.positionInfo = (_a = adResponse.onDeviceAd) === null || _a === void 0 ? void 0 : _a.positionInfo; + this.setInitialAdData(objectGraph, adResponse.iAdId, adResponse.clientRequestId); + } + /** + * Set the initial ad data, if available. + * This function requires that an ad fetch has been made, and can be called from two paths: + * 1. Via the constructor, where the ad fetch is made at page load time, or + * 2. Via `updateForAdResponse`, where the ad fetch was made asynchronously after the page load. + * @param objectGraph The Object Graph. + * @param iAdId The unique id provided for the ad instance. + * @param appStoreClientRequestId The unique id representing the App Store ad request client. + */ + setInitialAdData(objectGraph, iAdId, appStoreClientRequestId) { + if (isNothing(appStoreClientRequestId)) { + return; + } + // ToroID Suppression expects this to be replaced with -1 for figaro events + const iAdIdValue = isNothing(iAdId) ? "-1" : iAdId; + this.pageFields[EventLinter.hasIAdData] = true; + switch (this.placementType) { + case "today": + case "productPageYMAL": + case "productPageYMALDuringDownload": + // Only set `hasIAdData` on impressions fields initially if it's a Chainlink placement. + // This covers the "ad eligible" cases where we still want to check ad metrics when ads are not present. + this.impressionsFields[EventLinter.hasIAdData] = true; + break; + default: + break; + } + this.pageFields["iAdAppStoreClientRequestId"] = appStoreClientRequestId; + switch (this.placementType) { + case "today": + case "productPageYMAL": + case "productPageYMALDuringDownload": + this.clickFields["iAdAppStoreClientRequestId"] = appStoreClientRequestId; + this.impressionsFields["iAdAppStoreClientRequestId"] = appStoreClientRequestId; + break; + default: + break; + } + this.pageFields["iAdId"] = iAdIdValue; + this.impressionsFields["iAdId"] = iAdIdValue; + this.clickFields["iAdId"] = iAdIdValue; + this.updateContainerId(null); + // Update slot info with data + this.updateSlotInfo(); + if (serverData.isDefinedNonNullNonEmpty(this.slotInfo)) { + this.pageFields["iAdSlotInfo"] = this.slotInfo; + this.clickFields["iAdSlotInfo"] = this.slotInfo; + this.impressionsFields["iAdSlotInfo"] = this.slotInfo; + } + if (this.placementId !== null && this.placementId.length > 0) { + this.pageFields["iAdPlacementId"] = this.placementId; + this.clickFields["iAdPlacementId"] = this.placementId; + // Attach the `iAdPlacementId` to top-level impressions for v4 impressions. + this.impressionsFields["iAdPlacementId"] = this.placementId; + // For Chainlink placements, the `iAdPlacementId` sits within the impressions array. + // We manually remove `iAdPlacementId` from the top-level fields for v5 impressions in `createMetricsFastImpressionsData`. + switch (this.placementType) { + case "today": + case "productPageYMAL": + case "productPageYMALDuringDownload": + this.fastImpressionFields["iAdPlacementId"] = this.placementId; + break; + default: + break; + } + } + } + /** + * Update the containerId based on the current placementType. + */ + updateContainerId(containerId) { + if (this.placementType === "today") { + this.containerId = containerId !== null && containerId !== void 0 ? containerId : null; + if (serverData.isDefinedNonNull(this.containerId)) { + this.clickFields["iAdContainerId"] = this.containerId; + this.fastImpressionFields["iAdContainerId"] = this.containerId; + } + } + else { + this.containerId = + this.placementType === null ? null : IAdSearchInformation.containerIdFromType(this.placementType); + if (serverData.isDefinedNonNull(this.containerId)) { + this.pageFields["iAdContainerId"] = this.containerId; + this.clickFields["iAdContainerId"] = this.containerId; + this.fastImpressionFields["iAdContainerId"] = this.containerId; + } + } + } + /** + * @param slotIndex The slot index to look for a container Id for + * @returns The container Id for the given slot index, based off the slot infos + */ + containerIdForSlotIndex(slotIndex) { + if (isNothing(slotIndex) || isNothing(this.slotInfo)) { + return null; + } + for (const slotInfo of this.slotInfo) { + for (const slot of slotInfo.slots) { + if (slot.slotIndex === slotIndex) { + return slotInfo.containerId; + } + } + } + return this.containerId; + } + apply(objectGraph, adData) { + if (isNothing(adData) || serverData.isNullOrEmpty(adData)) { + return; + } + const iAdAdamId = adData.id; + const iAdConfigurationDictionary = attributeAsDictionary(adData, "iad"); + this._iAdAdamId = iAdAdamId; + if (iAdConfigurationDictionary) { + this.impressionsFields[EventLinter.hasIAdData] = true; + this.clickFields[EventLinter.hasIAdData] = true; + const impressionId = metricsUtil.emptyStringIfNullOrUndefined(iAdConfigurationDictionary["impressionId"]); + this.fastImpressionFields["iAdImpressionId"] = impressionId; + this.clickFields["iAdImpressionId"] = impressionId; + const metadata = metricsUtil.emptyStringIfNullOrUndefined(iAdConfigurationDictionary["metadata"]); + this.clickFields["iAdMetadata"] = metadata; + this.fastImpressionFields["iAdMetadata"] = metadata; + // Ads boldly populate `adamId` on pages. This is correct. + this.pageFields["adamId"] = iAdAdamId; + this.pageFields["iAd"] = { + iAdFormat: metricsUtil.sanitizedMetricsDictionary(serverData.asInterface(serverData.asJSONValue(iAdConfigurationDictionary), "format")), + iAdAlgoId: metricsUtil.emptyStringIfNullOrUndefined(iAdConfigurationDictionary["algoId"]), + iAdImpressionId: metricsUtil.emptyStringIfNullOrUndefined(iAdConfigurationDictionary["impressionId"]), + iAdMetadata: metricsUtil.emptyStringIfNullOrUndefined(iAdConfigurationDictionary["metadata"]), + }; + const productVariantData = productVariantDataForData(objectGraph, adData); + this.updateIAdMetricsFieldsForProductVariantData(productVariantData, this.clickFields); + if (preprocessor.CARRY_BUILD || preprocessor.DEBUG_BUILD) { + if (objectGraph.featureFlags.isEnabled("aligned_region_artwork_2025A")) { + const selectedCustomCreativeId = getSelectedCustomCreativeId(adData); + if (this.placementType === "today") { + this.updateIAdMetricsFieldsForAlignedRegion(selectedCustomCreativeId, this.fastImpressionFields); + } + else { + this.updateIAdMetricsFieldsForAlignedRegion(selectedCustomCreativeId, this.impressionsFields); + } + this.updateIAdMetricsFieldsForAlignedRegion(selectedCustomCreativeId, this.clickFields); + } + } + Object.assign(this.iAdClickEventFields, iAdConfigurationDictionary); + this._iAdApplied = true; + // Clear out any prior missed opportunity reason if we have an ad. + this.setMissedOpportunity(objectGraph, undefined, this.placementType); + } + // Update slot info after ad is applied. + this.updateSlotInfo(); + if (serverData.isDefinedNonNullNonEmpty(this.slotInfo)) { + this.pageFields["iAdSlotInfo"] = this.slotInfo; + this.clickFields["iAdSlotInfo"] = this.slotInfo; + this.impressionsFields["iAdSlotInfo"] = this.slotInfo; + } + } + /** + * Apply the click fields that were attached to the page request we're following. + * This will generally be following on from click on a lockup with ad data attached, where we want to pull + * some of the click fields from ad to be applied to this new page data. + * + * The goal here is to attach enough metadata into the incoming page that we can attribute any offer action + * to the ad that was tapped on to reach this page. Whilst doing this we don't want to attach too much + * ad data to the page, as this can start to interfere with metrics for other ad placements. This results in + * us manually inserting and removing fields to achieve the right balance. + * @param iAdAdamId The adamId for the ad. + * @param fields The set of fields to apply to our clickFields. + */ + applyClickFieldsFromPageRequest(iAdAdamId, fields) { + this._iAdApplied = true; + this._iAdAdamId = iAdAdamId; + Object.assign(this.clickFields, fields); + // We don't want to assign any of the fields to the page event, as this looks like an ad is being shown + // on the page we're navigating to. + Object.keys(this.pageFields).forEach((field) => delete this.pageFields[field]); + } + setSpecifiedAlignedRegionUsed(didUseSpecifiedCreative) { + this.fastImpressionFields["iAdIsSpecifiedCreativeUsed"] = didUseSpecifiedCreative; + this.clickFields["iAdIsSpecifiedCreativeUsed"] = didUseSpecifiedCreative; + } + /** + * Set the template type for the page. + */ + setTemplateType(templateType) { + this.pageFields["iAdTemplateType"] = templateType; + this.impressionsFields["iAdTemplateType"] = templateType; + this.clickFields["iAdTemplateType"] = templateType; + } + setMissedOpportunity(objectGraph, reason, placementType) { + this.missedOpportunityReason = reason; + // Only set in page and impressions fields if reason is non-null. + if (serverData.isDefinedNonNull(reason)) { + this.clickFields["iAdMissedOpportunityReason"] = reason; + // Chainlink placements don't expect "iAdMissedOpportunityReason" at the top level of impressions or page events. + switch (this.placementType) { + case "today": + case "productPageYMAL": + case "productPageYMALDuringDownload": + break; + default: + this.pageFields["iAdMissedOpportunityReason"] = reason; + this.impressionsFields["iAdMissedOpportunityReason"] = reason; + break; + } + } + else { + delete this.clickFields["iAdMissedOpportunityReason"]; + // Generally, we just want to remove the missed opportunity reason if one isn't set. + // The 'productPageYMALDuringDownload' placement is a special case where it's placed on the page + // subsequent to a previous placement, and metrics is updated via the pageChange metrics. + // This means that we need to send `null` as the value for this placement, in order to clear + // any possible missed opportunity value from the placement it's replacing. + switch (placementType) { + case "productPageYMALDuringDownload": + this.pageFields["iAdMissedOpportunityReason"] = null; + this.impressionsFields["iAdMissedOpportunityReason"] = null; + break; + default: + delete this.pageFields["iAdMissedOpportunityReason"]; + delete this.impressionsFields["iAdMissedOpportunityReason"]; + break; + } + } + // Only set in page and impressions fields if reason is non-null. + if (serverData.isDefinedNonNull(reason)) { + this.pageFields["iAdMissedOpportunityReason"] = reason; + this.impressionsFields["iAdMissedOpportunityReason"] = reason; + } + else { + delete this.pageFields["iAdMissedOpportunityReason"]; + delete this.impressionsFields["iAdMissedOpportunityReason"]; + } + // Update slotInfo with missed opportunity reason. + this.updateSlotInfo(); + if (serverData.isDefinedNonNullNonEmpty(this.slotInfo)) { + this.pageFields["iAdSlotInfo"] = this.slotInfo; + this.clickFields["iAdSlotInfo"] = this.slotInfo; + this.impressionsFields["iAdSlotInfo"] = this.slotInfo; + } + } + placementIdFromType(type) { + switch (type) { + case "searchLanding": + return "APPSTORE_SEARCH_LANDING_PAGE"; + case "searchResults": + return "APPSTORE_SEARCH_RESULT_PAGE"; + case "today": + return "APPSTORE_TODAY_TAB"; + case "productPageYMAL": + return "APPSTORE_PRODUCT_PAGE"; + case "productPageYMALDuringDownload": + return "APPSTORE_PRODUCT_PAGE_DOWNLOAD"; + default: + throw new Error(`This method should never be called with value: ${type}`); + } + } + static placementTypeFromPlacementId(objectGraph, id) { + switch (id) { + case "APPSTORE_SEARCH_LANDING_PAGE": + return "searchLanding"; + case "APPSTORE_SEARCH_RESULT_PAGE": + return "searchResults"; + case "APPSTORE_TODAY_TAB": + return "today"; + case "APPSTORE_PRODUCT_PAGE": + return "productPageYMAL"; + case "APPSTORE_PRODUCT_PAGE_DOWNLOAD": + return "productPageYMALDuringDownload"; + default: + objectGraph.console.log(`Failed to get placementType from placementId ${id}. Defaulting to searchResults`); + // For legacy reasons we fall back to `seachResults` when nothing is provided. + // Being the first ad slot, in the past this has been assumed as the "default". + return "searchResults"; + } + } + /** + * Get a containerId value for a given placement type to attach to the ad's metrics. + * @param type The AdPlacementType for the current ad. + * @returns A string value for containerId, if the placement has one. Otherwise null. + */ + static containerIdFromType(type) { + switch (type) { + case "productPageYMAL": + return "customers-also-bought-apps"; + case "productPageYMALDuringDownload": + return "customers-also-bought-apps-download"; + case "today": + return null; // Today page has variable containerIds that are updated programatically + default: + return null; + } + } + /** + * Return the fast impressions metrics fields for an item using the location tracker. + * + * `fastImpressionFields` are the items inside the impressions array of metrics events. Although some fields are stable, + * these are unique to a given position, so we must use that position information to generate the right fields. + * + * @param locationTracker A LocationTracker used to identify the relevant information to attach to the metrics fields. + * @param adSlotOverride The ad slot to use for the metrics fields. If not provided, the ad slot will be inferred from + * the location tracker. This is needed for today where the ad can span different shelves so the position is going to be 0 for multiple ads + * @returns Relevant metrics fields for the item in an impressions array. + */ + fastImpressionsFieldsForCurrentItem(locationTracker, adSlotOverride) { + switch (this.placementType) { + case "productPageYMAL": + case "productPageYMALDuringDownload": + case "today": + let position; + if (isSome(adSlotOverride)) { + position = adSlotOverride; + } + else { + const location = metricsLocation.currentLocation(locationTracker); + // If the current location is within a todayCard, we want to extract the location of the card, not the location + // of this lockup within the card. + if (location !== null && location.locationType === "todayCard") { + position = metricsLocation.previousPosition(locationTracker); + } + else { + position = metricsLocation.currentPosition(locationTracker); + } + } + const sharedFields = shallowCopyOf(this.fastImpressionFields); + sharedFields["iAdSlotId"] = `${this.containerIdForSlotIndex(position)}_${position}`; + if (position !== this.adjustedSlotIndex) { + // If the current position is not where the ad is, we only want a subset of the tracked fields. + const allowedFields = ["iAdEligible", "iAdPlacementId", "iAdContainerId", "iAdSlotId"]; + Object.keys(sharedFields).forEach((key) => { + if (!allowedFields.includes(key)) { + delete sharedFields[key]; + } + }); + } + return sharedFields; + default: + return this.fastImpressionFields; + } + } + /** + * Get the adjusted slot index of the ad we're tracking. + * This is zero-based - Ad Platforms returns slot info to us one-based but we always adjust it before using it anywhere. + */ + get adjustedSlotIndex() { + var _a; + const positionInfoSlotIndex = (_a = this.positionInfo) === null || _a === void 0 ? void 0 : _a.slot; + // The slot as provided by ad platforms is one-based - adjust it so we're working with zero-based numbers. + if (serverData.isDefinedNonNull(positionInfoSlotIndex)) { + return positionInfoSlotIndex - 1; + } + return null; + } + /** + * Update the existing slot information now that new data has been applied. + */ + updateSlotInfo() { + if (isNothing(this.slotInfo)) { + return; + } + switch (this.placementType) { + case "productPageYMAL": + case "productPageYMALDuringDownload": + for (const containerSlotInfo of this.slotInfo) { + for (const slot of containerSlotInfo.slots) { + slot.hasAdData = this.iAdIsPresent; + if (serverData.isDefinedNonNull(this.missedOpportunityReason)) { + slot.missedOpportunityReason = this.missedOpportunityReason; + } + } + } + break; + case "today": + const adjustedSlotIndex = this.adjustedSlotIndex; + for (const containerSlotInfo of this.slotInfo) { + for (const slot of containerSlotInfo.slots) { + // Check whether the current index matches our slot information. + // If so, an ad is intended to be placed here. + const isAdInCurrentIndex = adjustedSlotIndex === slot.slotIndex; + // In order to understand whether a given slot has ad data, we ensure that a usable ad is present, + // and the current slot is the one an ad is placed in. + const hasAdData = this.iAdIsPresent && isAdInCurrentIndex; + // By default, use the missed opportunity reason, if any, stored here. + let missedOpportunityReason = this.missedOpportunityReason; + // If: + // - the ad is not in the current index we're building slot info for, and + // - there is a valid slot index + // we override the reason to 'NOAD', as it means another eligible slot was filled. + if (!isAdInCurrentIndex && serverData.isDefinedNonNull(adjustedSlotIndex)) { + missedOpportunityReason = "NOAD"; + } + slot.hasAdData = hasAdData; + if (serverData.isDefinedNonNull(missedOpportunityReason)) { + slot.missedOpportunityReason = missedOpportunityReason; + } + } + } + break; + default: + break; + } + } + /** + * Modifies the provided MetricsFields for the ProductVariantData specific to fields required for iAds. + * @param productVariantData The variant data of impressionable data to apply. + * @param metricsFields An existing set of MetricsFields to modify. + */ + updateIAdMetricsFieldsForProductVariantData(productVariantData, metricsFields) { + if (isSome(productVariantData) && productVariantDataHasVariant(productVariantData, "customProductPage")) { + metricsFields["iAdPageCustomId"] = productVariantData.productPageId; + } + else { + // If a custom product page doesn't exist, we remove the fields. + delete metricsFields["iAdPageCustomId"]; + } + } + /** + * Modifies the provided MetricsFields to include the custom id for iAds. + * @param selectedCustomCreativeId The id for the custom creative that we want to set as custom id. + * @param metricsFields An existing set of MetricsFields to modify. + */ + updateIAdMetricsFieldsForAlignedRegion(selectedCustomCreativeId, metricsFields) { + if (preprocessor.CARRY_BUILD || preprocessor.DEBUG_BUILD) { + if (isSome(selectedCustomCreativeId)) { + metricsFields["iAdCustomId"] = selectedCustomCreativeId; + } + else { + delete metricsFields["iAdCustomId"]; + } + } + } + /** + * Return the event version for fast impressions based on the placement of the ad. + * Impressions v5 is being rolled out first for Chainlink. Once all impressions have adopted it, this can be removed. + */ + get fastImpressionsEventVersion() { + switch (this.placementType) { + case "productPageYMAL": + case "productPageYMALDuringDownload": + case "today": + return 5; + default: + return 4; + } + } + /** + * Return whether ad rotation fields should be included on metrics events, based on the current placement type. + */ + get shouldIncludeAdRotationFields() { + switch (this.placementType) { + case "productPageYMAL": + case "productPageYMALDuringDownload": + case "today": + return false; + case "searchLanding": + case "searchResults": + return true; + default: + return true; + } + } +} +/** + * Return the container id to use for a today feed item + * @param slotIndex The slot index that we found a TodayItem for + * @param isAdSlot Whether this slotIndex is the slot the ad is in + * @param todayItem The today item that this card is contained in, this could be a single item or a story group item + * @returns The configuration to use when parsing a today card + */ +export function iAdContainerIdForSlotInTodayItem(slotIndex, isAdSlot, todayItem) { + if (isNothing(todayItem)) { + return "0"; + } + switch (todayItem.type) { + case FlattenedTodayItemType.EditorialItemGroup: + const storyGroupHasMultipleItems = todayItem.containedAdSlots.length > 1; + const slotIndexIsFirstOrLastInStoryGroup = !storyGroupHasMultipleItems || + slotIndex === todayItem.containedAdSlots[0] || + slotIndex === todayItem.containedAdSlots[todayItem.containedAdSlots.length - 1]; + if (isAdSlot && slotIndexIsFirstOrLastInStoryGroup) { + // If the story group has fewer than 2 items, that means theres not valid slot **in** the actual story group + // to place the ad, so its going to be at the top level. Or if there are multiple items, but the slot is the + // first or last item there is no reason to pull the ad into the story group, it should be top level. + return "0"; + } + else { + // Using the `data` from the todayItem here and not the todayCardData, because + // this will be the data for the containing story gorup + return "0"; + } + default: + return "0"; + } +} +/** + * Create the iAdInformation for a given page if necessary + * @param objectGraph The dependency graph for the app store + * @param pageId The id of the page this iAd info is being used for + * @param response The MAPI response for this page + * @param positionInfo The position information on where the ad will attempted to be inserted + * @param flattenedTodayFeed The flattened to today feed if this is for a today page + * @returns The iAdInformation to be used in a metrics page information object + */ +export function iAdInformationFromMediaApiResponse(objectGraph, pageId, response, positionInfo = null, flattenedTodayFeed = null) { + var _a; + /** <rdar://problem/33764430> Toro: iAd is missing AD_OPEN figaro event when tapping on ad and pressing OPEN through the product page */ + const iAdClickInfoString = serverData.asString(response, iAdURLParameterStringToken); + if (isNothing(iAdClickInfoString)) { + return null; + } + const iAdClickInfo = JSON.parse(iAdClickInfoString); + // rdar://72607206 (Gibraltar: Tech Debt: Population of iAd fields in Product Page) + const placementType = IAdSearchInformation.placementTypeFromPlacementId(objectGraph, serverData.asString(iAdClickInfo, "iAdPlacementId")); + const iAdInfo = new IAdSearchInformation(objectGraph, placementType, IAdSearchInformation.createInitialSlotInfos(objectGraph, placementType, positionInfo, flattenedTodayFeed), (_a = serverData.asString(iAdClickInfo, "iAdId")) !== null && _a !== void 0 ? _a : undefined, undefined, undefined, positionInfo); + iAdInfo.applyClickFieldsFromPageRequest(pageId !== null && pageId !== void 0 ? pageId : undefined, iAdClickInfo); + return iAdInfo; +} +/** @public */ +export class MetricsPageInformation { + constructor(base = {}) { + this.baseFields = base; + } +} +//# sourceMappingURL=models.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/page.js b/node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/page.js new file mode 100644 index 0000000..0c088b1 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/page.js @@ -0,0 +1,482 @@ +import { PageInvocationPoint } from "@jet/environment/types/metrics"; +import { isSome } from "@jet/environment/types/optional"; +import * as models from "../../../api/models"; +import * as serverData from "../../../foundation/json-parsing/server-data"; +import * as mediaAttributes from "../../../foundation/media/attributes"; +import * as mediaDataStructure from "../../../foundation/media/data-structure"; +import { ResponseMetadata } from "../../../foundation/network/network"; +import { Parameters } from "../../../foundation/network/url-constants"; +import * as urls from "../../../foundation/network/urls"; +import * as dateUtil from "../../../foundation/util/date-util"; +import * as objects from "../../../foundation/util/objects"; +import * as content from "../../content/content"; +import { productVariantDataForData } from "../../product-page/product-page-variants"; +import * as guidedSearch from "../../search/guided-search/guided-search-metrics"; +import * as searchAds from "../../search/search-ads"; +import * as metricsBuilder from "../builder"; +import * as misc from "./misc"; +import * as metricsModels from "./models"; +import * as metricsUtil from "./util"; +import { searchMetricsDataSetID } from "../../search/search-common"; +//* ************************* +//* Page Metrics +//* ************************* +export function metricsPageInformationFromMarketingItemMediaApiResponse(objectGraph, pageType, pageId, marketingItemData) { + var _a; + const pageInformation = metricsPageInformationFromMediaApiResponse(objectGraph, pageType, pageId, marketingItemData); + if (serverData.isDefinedNonNull(marketingItemData)) { + pageInformation.mercuryMetricsData = + (_a = metricsUtil.marketingItemTopLevelBaseFieldsFromData(objectGraph, marketingItemData)) !== null && _a !== void 0 ? _a : undefined; + } + return pageInformation; +} +export function metricsPageInformationFromMediaApiResponse(objectGraph, pageType, pageId, response, pageDetails, overrideIAdInfo) { + var _a, _b, _c; + const pageInformation = (_a = serverData.asInterface(response, ResponseMetadata.pageInformation, {})) !== null && _a !== void 0 ? _a : {}; + const timingMetrics = serverData.asInterface(response, ResponseMetadata.timingValues); + const pageUrl = serverData.asString(response, ResponseMetadata.requestedUrl); + let productVariantData; + const responsePageBaseFields = pageInformation; + responsePageBaseFields.pageType = pageType; + responsePageBaseFields.pageId = pageId; + if (pageDetails) { + responsePageBaseFields["pageDetails"] = pageDetails; + } + if (pageType === "Software") { + const softwareData = mediaDataStructure.dataFromDataContainer(objectGraph, response); + if (isSome(softwareData)) { + const name = mediaAttributes.attributeAsString(softwareData, "name"); + const artistName = mediaAttributes.attributeAsString(softwareData, "artistName"); + responsePageBaseFields["pageDetails"] = `${artistName}_${name}`; + // To distinguish normal apps from Arcade apps + if (content.isArcadeSupported(objectGraph, softwareData)) { + responsePageBaseFields["softwareType"] = "Arcade"; + } + productVariantData = productVariantDataForData(objectGraph, softwareData); + } + } + else if (pageType === "Genre") { + responsePageBaseFields["pageDetails"] = `${pageType}_${pageId}`; + } + else if (pageType === "Search") { + responsePageBaseFields["pageDetails"] = "Apps"; + } + else if (pageType === "SearchLanding" && pageId === "SearchLanding") { + responsePageBaseFields["pageDetails"] = `${pageType}_${pageId}`; + } + const pageInfo = new metricsModels.MetricsPageInformation((_b = metricsUtil.sanitizedMetricsDictionary(responsePageBaseFields)) !== null && _b !== void 0 ? _b : {}); + if (timingMetrics !== null) { + pageInfo.timingMetrics = timingMetrics; + if (isSome(pageUrl)) { + pageInfo.pageUrl = pageUrl; + } + } + // For product pages, every shelf is creating it's a new page information. There should be one top-level one, shared for page. + // Revisit in: rdar://77227964 (Metrics: ProductPage: Shelves should share page information instead of building it per-shelf.) + if (serverData.isDefinedNonNull(productVariantData)) { + pageInfo.productVariantData = productVariantData; + } + const iAdInfo = overrideIAdInfo !== null && overrideIAdInfo !== void 0 ? overrideIAdInfo : metricsModels.iAdInformationFromMediaApiResponse(objectGraph, pageId, response); + if (isSome(iAdInfo)) { + pageInfo.iAdInfo = iAdInfo; + } + pageInfo.recoMetricsData = (_c = mediaDataStructure.metricsFromMediaApiObject(response)) !== null && _c !== void 0 ? _c : undefined; + return pageInfo; +} +export function fakeMetricsPageInformation(objectGraph, pageType, pageId, pageDetails, iAdClickInfo) { + var _a; + const page = new metricsModels.MetricsPageInformation({ + pageType: pageType, + pageId: pageId, + page: `${pageType}_${pageId}`, + pageDetails: pageDetails, + }); + if (iAdClickInfo) { + // rdar://72607206 (Gibraltar: Tech Debt: Population of iAd fields in Product Page) + const placementType = metricsModels.IAdSearchInformation.placementTypeFromPlacementId(objectGraph, serverData.asString(iAdClickInfo, "iAdPlacementId")); + page.iAdInfo = new metricsModels.IAdSearchInformation(objectGraph, placementType, metricsModels.IAdSearchInformation.createInitialSlotInfos(objectGraph, placementType, null, null), (_a = serverData.asString(iAdClickInfo, "iAdId")) !== null && _a !== void 0 ? _a : undefined); + page.iAdInfo.applyClickFieldsFromPageRequest(pageId, iAdClickInfo); + } + return page; +} +export function addMetricsEventsToPageWithInformation(objectGraph, page, pageInformation, fieldsModifier, isDefaultBrowser) { + if (serverData.isNull(pageInformation)) { + return; + } + page.pageMetrics.pageFields = misc.fieldsFromPageInformation(pageInformation); + page.pageMetrics.addManyInstructions(impressionsInstructionsFromPageData(objectGraph, pageInformation, fieldsModifier)); + page.pageMetrics.addData(pageEventFromPageData(objectGraph, pageInformation, fieldsModifier, isDefaultBrowser), [ + PageInvocationPoint.pageEnter, + ]); + page.pageMetrics.addData(pageExitEventFromPageData(objectGraph, pageInformation, fieldsModifier), [ + PageInvocationPoint.pageExit, + ]); + page.pageMetrics.pageRenderFields = pageRenderFromPageData(objectGraph, pageInformation, fieldsModifier); + page.pageRenderMetrics = pageRenderFromPageData(objectGraph, pageInformation, fieldsModifier); + if (pageRequiresBackEvent(page)) { + page.pageMetrics.addData(backEventFromPageData(objectGraph, pageInformation, fieldsModifier), [ + PageInvocationPoint.backButton, + ]); + } + const fetchTimingMetricsBuilder = objectGraph.fetchTimingMetricsBuilder; + if (isSome(fetchTimingMetricsBuilder)) { + fetchTimingMetricsBuilder.decorate(page); + } +} +/** + * @param page The page to check + * @returns Whether this page requires a back event to be sent when the user navigates back from it. + */ +function pageRequiresBackEvent(page) { + const isSearchResultsPage = page instanceof models.SearchResults; + const isContingentOfferDetailPage = page instanceof models.ContingentOfferDetailPage; + const isWinbackOfferDetailPage = page instanceof models.OfferItemDetailPage; + return !isSearchResultsPage && !isContingentOfferDetailPage && !isWinbackOfferDetailPage; +} +export function copyMetricsEventsIntoSidepackedPagewithInformation(objectGraph, page, pageInformation) { + if (serverData.isNull(pageInformation)) { + return; + } + page.pageMetrics.addData(backEventFromPageData(objectGraph, pageInformation, undefined), [ + PageInvocationPoint.backButton, + ]); + if (serverData.isNull(page.pageMetrics.pageFields)) { + page.pageMetrics.pageFields = {}; + } +} +/** + * Constructs metrics page information for a page navigated to from the action links on the product page. + * @param productId The adamId of the product for which these action links are displayed. + * @param pageType The metrics pageType for the page the links navigates to. + */ +export function pageInformationForActionLinkPage(objectGraph, productId, pageType) { + const base = { + pageId: productId || "", + pageType: pageType, + }; + return new metricsModels.MetricsPageInformation(base); +} +/** + * Create metrics page information for rooms that may not have an its id. + * @param pageId The identifier to differentiate room + */ +export function pageInformationForRoom(objectGraph, pageId) { + const pageType = "Room"; + const page = new metricsModels.MetricsPageInformation({ + pageType: pageType, + pageId: pageId, + page: `${pageType}_${pageId}`, + }); + return page; +} +/** + * Constructs empty page information for search hints 'page' for given prefix term and optional dataSetId. + */ +export function pageInformationForSearchHintsPage(objectGraph, prefixTerm, pageUrl, dataSetId) { + const base = { + pageId: "hints", + pageType: "Search", + }; + if (dataSetId) { + base[searchMetricsDataSetID] = dataSetId; + } + const pageInformation = new metricsModels.MetricsPageInformation(base); + pageInformation.pageUrl = pageUrl; + return pageInformation; +} +export function pageInformationForIAPPage(objectGraph, parentId, iapId) { + const base = { + pageId: iapId || "", + pageType: "IAPInstallPage", + parentId: metricsUtil.emptyStringIfNullOrUndefined(parentId), + }; + const pageInformation = new metricsModels.MetricsPageInformation(base); + return pageInformation; +} +/** + * Build the pageInformation to use for search pages. + */ +export function pageInformationForSearchPage(objectGraph, request, response, termContext, searchRequestUrl, pageId, sponsoredSearchRequestData, wasOdmlSuccessful, guidedSearchData) { + const pageInformation = metricsPageInformationFromMediaApiResponse(objectGraph, "Search", pageId, response); + pageInformation.searchTermContext = termContext; + pageInformation.pageUrl = searchRequestUrl; // For Search, pageUrl is request url + if (guidedSearchData) { + pageInformation.guidedSearch = guidedSearch.guidedSearchPageInformationFields(objectGraph, request, guidedSearchData); + } + if (searchAds.platformSupportsAdverts(objectGraph) && sponsoredSearchRequestData != null) { + pageInformation.iAdInfo = new metricsModels.IAdSearchInformation(objectGraph, "searchResults", metricsModels.IAdSearchInformation.createInitialSlotInfos(objectGraph, "searchResults", null, null), sponsoredSearchRequestData.iAdId, sponsoredSearchRequestData.appStoreClientRequestId, wasOdmlSuccessful); + } + return pageInformation; +} +export function pageInformationForSegmentedSearchPage(objectGraph, response, termContext, searchRequestUrl, groupId) { + const pageInformation = metricsPageInformationFromMediaApiResponse(objectGraph, "Search", groupId, response); + pageInformation.searchTermContext = termContext; + pageInformation.pageUrl = searchRequestUrl; + return pageInformation; +} +export function pageInformationForAppPromotionDetailPage(objectGraph, appPromotionType, appPromotionId, parentAppAdamId, referrerData, recoMetricsData) { + let pageId = ""; + let pageType = ""; + switch (appPromotionType) { + case models.AppPromotionType.AppEvent: + pageId = `${appPromotionId}_${parentAppAdamId}`; + pageType = "EventDetails"; + break; + case models.AppPromotionType.ContingentOffer: + case models.AppPromotionType.OfferItem: + pageId = `${appPromotionId}`; + pageType = "OfferDetails"; + break; + default: + break; + } + const base = { + pageId: pageId, + pageType: pageType, + }; + if (serverData.isDefinedNonNull(referrerData)) { + base["refApp"] = referrerData["app"]; + base["extRefUrl"] = referrerData["externalUrl"]; + } + const pageInformation = new metricsModels.MetricsPageInformation(base); + pageInformation.recoMetricsData = recoMetricsData !== null && recoMetricsData !== void 0 ? recoMetricsData : undefined; + return pageInformation; +} +export function addPageEventsToInAppPurchasePage(objectGraph, page, pageInformation) { + addMetricsEventsToPageWithInformation(objectGraph, page, pageInformation); +} +export function makePageReferralEligible(objectGraph, page) { + if (serverData.isNull(page) || serverData.isNull(page.pageMetrics)) { + return; + } + const pageInstructions = page.pageMetrics.instructions; + if (!serverData.isNull(pageInstructions)) { + for (const instruction of pageInstructions) { + // We shoudl only be requesting crossfire information for page events. + // if we dont, and add it to all of the instructions then we end up clearing out + // the referral information on the native side during any other event that happens on this page + // before a buy can happen, for instance an impressions event that occurs on page exit when + // displaying an upsell sheet. + if (instruction.data.fields["eventType"] !== "page") { + continue; + } + instruction.data.includingFields.push("crossfireReferral"); + } + } + // Ensure that for referral eligible pages, the attribution is included in the purchase configuration + // <rdar://problem/45420867> Crossfire: Metrics: extRefApp2/extRefUrl2 buyparam are being persisted too strongly, being sent on buys of other app pages + let productLockup = null; + if (page instanceof models.ProductPage) { + productLockup = page; + } + else if (page instanceof models.ShelfBasedProductPage) { + productLockup = page.lockup; + } + if (productLockup) { + const eligibleActions = []; + if (productLockup.buttonAction instanceof models.OfferAction) { + eligibleActions.push(productLockup.buttonAction); + } + else if (productLockup.buttonAction instanceof models.OfferConfirmationAction && + productLockup.buttonAction.buyAction instanceof models.OfferAction) { + eligibleActions.push(productLockup.buttonAction.buyAction); + } + else if (productLockup.buttonAction instanceof models.OfferAlertAction && + productLockup.buttonAction.completionAction instanceof models.OfferAction) { + eligibleActions.push(productLockup.buttonAction.completionAction); + } + else if (productLockup.buttonAction instanceof models.OfferStateAction) { + if (productLockup.buttonAction.buyAction instanceof models.OfferAction) { + eligibleActions.push(productLockup.buttonAction.buyAction); + } + if (productLockup.buttonAction.defaultAction instanceof models.OfferAction) { + eligibleActions.push(productLockup.buttonAction.defaultAction); + } + if (productLockup.buttonAction.openAction instanceof models.OfferAction) { + eligibleActions.push(productLockup.buttonAction.openAction); + } + if (productLockup.buttonAction.subscribePageAction instanceof models.FlowAction && + productLockup.buttonAction.subscribePageAction.page === "arcadeSubscribe" && + isSome(productLockup.buttonAction.subscribePageAction.pageUrl) && + productLockup.buttonAction.subscribePageAction.pageUrl.length > 0) { + const pageUrl = urls.URL.from(productLockup.buttonAction.subscribePageAction.pageUrl); + pageUrl.param(Parameters.includePostSubscribeAttribution, "true"); + productLockup.buttonAction.subscribePageAction.pageUrl = pageUrl.build(); + } + if (productLockup.buttonAction.subscribePageAction instanceof models.FlowAction && + productLockup.buttonAction.subscribePageAction.pageData instanceof models.MarketingItemRequestInfo && + productLockup.buttonAction.subscribePageAction.pageData.purchaseSuccessAction instanceof + models.OfferAction) { + eligibleActions.push(productLockup.buttonAction.subscribePageAction.pageData.purchaseSuccessAction); + } + } + for (const eligibleAction of eligibleActions) { + if (eligibleAction.purchaseConfiguration) { + eligibleAction.purchaseConfiguration.excludeAttribution = false; + } + } + } +} +/** + * Add an updated set of page metrics to the shelf that will be used to update the existing page metrics, + * as well as be sent as a `pageChange` event. + * @param objectGraph The object graph. + * @param shelf The shelf to attach the pageChange fields to + * @param pageInformation The modified page information for the containing page. + * @param fieldsModifier A field modifier, if required. + * @returns + */ +export function addPageChangeFieldsToShelfWithInformation(objectGraph, shelf, pageInformation, fieldsModifier) { + if (serverData.isNull(pageInformation)) { + return; + } + let updatedEvents = []; + // Page + const pageFields = misc.fieldsFromPageInformation(pageInformation); + const pageEvent = pageEventFromPageData(objectGraph, pageInformation, fieldsModifier); + updatedEvents.push(pageEvent); + // Impressions + const impressionsInstructions = impressionsInstructionsFromPageData(objectGraph, pageInformation, fieldsModifier); + const impressionsEvents = impressionsInstructions.map((instruction) => instruction.data); + updatedEvents = updatedEvents.concat(impressionsEvents); + filterPageMetricsFieldsForPageChange(pageFields); + updatedEvents.forEach((event) => filterPageMetricsFieldsForPageChange(event.fields)); + shelf.pageChangeMetrics = { + pageFields: pageFields, + updatedEvents: updatedEvents, + }; +} +/// An allow list for the fields that can change in a `pageChange` event. +const allowedPageChangeKeys = new Set([ + "iAdAppStoreClientRequestId", + "iAdId", + "iAdSlotInfo", + "iAdOdmlSuccess", + "iAdEligible", + "iAdContainerId", + "iAdImpressionId", + "iAdMetadata", + "adamId", + "iAd", + "iAdPlacementId", + "iAdMissedOpportunityReason", + "hasiAdData", + "iAdTemplateType", + // These are required to merge with existing events + "eventType", + "impressionQueue", +]); +/** + * Filter some fields for the allowed content for a pageChange event. + * @param metricsFields The MetricsFields to filter. + */ +function filterPageMetricsFieldsForPageChange(metricsFields) { + Object.keys(metricsFields) + .filter((key) => !allowedPageChangeKeys.has(key)) + .forEach((key) => delete metricsFields[key]); +} +function pageEventFromPageData(objectGraph, pageInformation, fieldsModifier, isDefaultBrowser) { + var _a, _b, _c, _d, _e; + const base = baseFieldsForPageEventsFromPageInformation(pageInformation, fieldsModifier); + if (pageInformation.iAdInfo) { + // Always copy this, even if no ad is present. + Object.assign(base, pageInformation.iAdInfo.pageFields); + } + // Include pre-order release date for page events. + if (serverData.isDefinedNonNull(pageInformation.offerReleaseDate)) { + base["offerReleaseDate"] = dateUtil.millisecondsToUTCMidnightFromLocalDate(pageInformation.offerReleaseDate); + } + const searchTermContext = pageInformation.searchTermContext; + if (searchTermContext) { + base["searchTerm"] = searchTermContext.term; + if (searchTermContext.suggestedTerm) { + base["searchSuggestedTerm"] = searchTermContext.suggestedTerm; + } + if (searchTermContext.correctedTerm) { + base["searchCorrectedTerm"] = searchTermContext.correctedTerm; + } + if (searchTermContext.originatingTerm) { + base["searchOriginatingTerm"] = searchTermContext.originatingTerm; + } + } + if (pageInformation.guidedSearch) { + Object.assign(base, pageInformation.guidedSearch); + } + const pageEvent = metricsBuilder.createMetricsPageData(objectGraph, false, (_a = pageInformation.isCrossfireReferralCandidate) !== null && _a !== void 0 ? _a : false, pageInformation.timingMetrics, base, isDefaultBrowser); + const hasAdverts = (_c = (_b = pageInformation.iAdInfo) === null || _b === void 0 ? void 0 : _b.iAdIsPresent) !== null && _c !== void 0 ? _c : false; + const shouldIncludeAdRotationFields = (_e = (_d = pageInformation.iAdInfo) === null || _d === void 0 ? void 0 : _d.shouldIncludeAdRotationFields) !== null && _e !== void 0 ? _e : false; + if (hasAdverts && shouldIncludeAdRotationFields) { + pageEvent.includingFields.push("advertRotation"); + } + return pageEvent; +} +/** + * Creates a pageExitEvent which has all of the same fields as a pageEvent but with an eventType of pageExit. + * Noted this event is an event that is primarily meant to be used so the JS can get a hook into when a page is + * left so we can clear out associated referralContexts + */ +function pageExitEventFromPageData(objectGraph, pageInformation, fieldsModifier) { + const pageExitEvent = pageEventFromPageData(objectGraph, pageInformation, fieldsModifier); + pageExitEvent.fields["eventType"] = "pageExit"; + return pageExitEvent; +} +function backEventFromPageData(objectGraph, pageInformation, fieldsModifier) { + const base = baseFieldsForPageEventsFromPageInformation(pageInformation, fieldsModifier); + return metricsBuilder.createMetricsBackClickData(objectGraph, base); +} +function pageRenderFromPageData(objectGraph, pageInformation, fieldsModifier) { + const base = baseFieldsForPageEventsFromPageInformation(pageInformation, fieldsModifier); + if (pageInformation.searchTermContext) { + base["searchTerm"] = pageInformation.searchTermContext.term; + } + // Splice in page fields until all native clients have fix for <rdar://problem/68879825> + if (pageInformation.baseFields) { + Object.assign(base, pageInformation.baseFields); + } + return metricsBuilder.createMetricsPageRenderFields(objectGraph, pageInformation.timingMetrics, base); +} +function impressionsInstructionsFromPageData(objectGraph, pageInformation, fieldsModifier) { + var _a, _b, _c; + const base = baseFieldsForPageEventsFromPageInformation(pageInformation, fieldsModifier); + if (pageInformation.searchTermContext) { + base["searchTerm"] = pageInformation.searchTermContext.term; + } + const impressionBase = objects.shallowCopyOf(base); + if (pageInformation.iAdInfo) { + Object.assign(impressionBase, pageInformation.iAdInfo.impressionsFields); + } + if (pageInformation.guidedSearch) { + Object.assign(impressionBase, pageInformation.guidedSearch); + } + // Regular Impressions + const shouldIncludeAdMetrics = serverData.isDefinedNonNull(pageInformation.iAdInfo); + const iAdEligibleForWindowCollection = serverData.isNullOrEmpty((_a = pageInformation.iAdInfo) === null || _a === void 0 ? void 0 : _a.missedOpportunityReason) && objectGraph.client.isPad; + const shouldIncludeAdRotationFields = (_c = (_b = pageInformation.iAdInfo) === null || _b === void 0 ? void 0 : _b.shouldIncludeAdRotationFields) !== null && _c !== void 0 ? _c : false; + const impressionsData = metricsBuilder.createMetricsImpressionsData(objectGraph, impressionBase, shouldIncludeAdMetrics && iAdEligibleForWindowCollection, shouldIncludeAdRotationFields, true); + const instruction = { + data: impressionsData, + invocationPoints: [PageInvocationPoint.appExit, PageInvocationPoint.pageExit], + }; + // Fast Impressions + const impressionsArray = [instruction]; + if (shouldIncludeAdMetrics) { + const fastImpressions = metricsBuilder.createMetricsFastImpressionsData(objectGraph, impressionBase, pageInformation); + impressionsArray.push({ + data: fastImpressions, + invocationPoints: [PageInvocationPoint.appExit, PageInvocationPoint.pageExit, PageInvocationPoint.timer], + }); + } + return impressionsArray; +} +function baseFieldsForPageEventsFromPageInformation(pageInformation, fieldsModifier) { + const base = {}; + // Add offer type for pre-orders (this should be added to everything, including impressions) + if (serverData.isDefinedNonNull(pageInformation.offerType)) { + base["offerType"] = pageInformation.offerType; + } + if (fieldsModifier !== undefined && base) { + fieldsModifier(base); + } + return base; +} +//# sourceMappingURL=page.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/search-focus-impressions.js b/node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/search-focus-impressions.js new file mode 100644 index 0000000..0bd030a --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/search-focus-impressions.js @@ -0,0 +1,57 @@ +import * as serverData from "../../../foundation/json-parsing/server-data"; +/** + * This is a workaround for Search Focus Page based on existing metrics tech debt in Search Results Page. + * + * # Context + * - In CrystalB, impressions for SFP now includes a recent searches shelf which needs to update dynamically as searches are performed. + * + * # What is the Workaround? + * The JetEngine Metrics API is designed to work s.t.: + * 1. JS populates `parentId`, which JetEngine Metric APIs uses **internally** to refer to between `ImpressionMetrics`. + * 2. Native generates `impressionParentId`, which is refers `impressionId` of parent assigned during serialization - + * + * Here, we instead: + * 1. JS populates `parentId` as normal + * 2. `event-linter` iterates through the impressions, finds the parent containers, and adds parent impression ids. + * 3. Native generates `impressionParentId` as normal where possible. + * + * # Why Workaround? + * 1. There are existing hacks for impression parents, e.g. fake `ad_container` and native child trackers w/ parent ID attribution. + * 2. AppStore is stuck between old ASK metrics and new JE metrics APIs. Our JS builders and existing native metrics are intertwined in a way that makes using JS defined `parentId` attribution nontrivial. + * 3. We need to dynamically update the recent searches shelf every time a search is performed. + * + * See `search-results-impressions` for more background. + */ +/** + * Update the `impression` field, attributing impressionParentIds per workaround above. + * @param eventFields Event fields to modify **in place**. + */ +export function decorateImpressionParentId(eventFields) { + var _a; + const impressions = serverData.asArrayOrEmpty(eventFields, "impressions"); + // Find result parent id. + let recentsParentImpressionId; + for (const impression of impressions) { + const canonicalId = serverData.asString(impression, "canonicalId"); + if (canonicalId === "R8804") { + recentsParentImpressionId = (_a = serverData.asString(impression, "impressionId")) !== null && _a !== void 0 ? _a : undefined; // *NOT* id. + break; + } + } + if (!recentsParentImpressionId) { + return; + } + // Update impressions for search results + eventFields["impressions"] = impressions.map((impression) => { + const canonicalId = serverData.asString(impression, "canonicalId"); + const impressionType = serverData.asString(impression, "impressionType"); + if (serverData.isNullOrEmpty(canonicalId) && + impressionType === "link" && + impression != null && + serverData.asString(impression, "impressionParentId") == null) { + impression["impressionParentId"] = recentsParentImpressionId; + } + return impression; + }); +} +//# sourceMappingURL=search-focus-impressions.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/search-result-impressions.js b/node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/search-result-impressions.js new file mode 100644 index 0000000..6874724 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/search-result-impressions.js @@ -0,0 +1,56 @@ +import { isSome } from "@jet/environment/types/optional"; +import * as serverData from "../../../foundation/json-parsing/server-data"; +/** + * This is a workaround for existing metrics tech debt in Search Results Page. + * + * # Context + * - In AzulC, impressions for SRP now includes 'containers' for search results and guided search tokens. + * - The containers were added so tokens don't affect impression index, which search uses as a signal for ranking. + * + * # What is the Workaround? + * The JetEngine Metrics API is designed to work s.t.: + * 1. JS populates `parentId`, which JetEngine Metric APIs uses **internally** to refer to between `ImpressionMetrics`. + * 2. Native generates `impressionParentId`, which is refers `impressionId` of parent assigned during serialization - + * + * Here, we instead: + * 1. JS *doesn't* populate `parentId` + * 2. `event-linter` iterates through the impressions, finds the parent containers, and adds parent impression ids. + * + * # Why Workaround? + * 1. There are existing hacks for impression parents, e.g. fake `ad_container` and native child trackers w/ parent ID attribution. + * 2. AppStore is stuck between old ASK metrics and new JE metrics APIs. Our JS builders and existing native metrics are intertwined in a way that makes using JS defined `parentId` attribution nontrivial. + * + * Note that we **don't** use this for attributing parent id workaround for `search-revisions`, i.e. guided search tokens. + * This is because that codepath began from a clean slate, and uses JE metrics as-designed + */ +/** + * Update the `impression` field, attributing impressionParentIds per workaround above. + * @param eventFields Event fields to modify **in place**. + */ +export function decorateImpressionParentId(eventFields) { + const impressions = serverData.asArrayOrEmpty(eventFields, "impressions"); + // Find result parent id. + let resultsParentImpressionId; + for (const impression of impressions) { + const impressionType = serverData.asString(impression, "impressionType"); + if (isSome(impression) && impressionType === "SearchResults") { + resultsParentImpressionId = impression["impressionId"]; // *NOT* id. + break; + } + } + if (!resultsParentImpressionId) { + return; + } + // Update impressions for search results + eventFields["impressions"] = impressions.map((impression) => { + const impressionType = serverData.asString(impression, "impressionType"); + const isCardResult = impressionType === "card"; + const isEventModuleResult = impressionType === "eventModule"; + const isSearchResult = isCardResult || isEventModuleResult; // search results are cards and app events. + if (isSome(impression) && isSearchResult) { + impression["impressionParentId"] = resultsParentImpressionId; + } + return impression; + }); +} +//# sourceMappingURL=search-result-impressions.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/search/search-shelves.js b/node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/search/search-shelves.js new file mode 100644 index 0000000..486fc9d --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/search/search-shelves.js @@ -0,0 +1,98 @@ +import * as mediaDataStructure from "../../../../foundation/media/data-structure"; +import * as models from "../../../../api/models"; +import * as searchShelves from "../../../search/content/search-shelves"; +import { impressionOptions } from "../impressions"; +import { asString } from "../../../../foundation/json-parsing/server-data"; +import { relationship } from "../../../../foundation/media/relationships"; +import { isNothing } from "@jet/environment/types/optional"; +/** + * Generates the metrics impressions options for the search shelf + * @param objectGraph The App Store Object Graph + * @param data The shelf data object + * @param shelfAttributes The shelf's attributes + * @param searchPageContext The context for the page containing the shelf + * @returns The metrics options for the shelf + */ +export function createMetricsOptionsForGenericSearchPageShelf(objectGraph, data, shelfAttributes, searchPageContext) { + var _a, _b, _c; + /// On shelves, the actual reco metrics are on the content and not the data blob itself + const recoMetricsDataContainer = relationship(data, "contents"); + const recoMetricsData = recoMetricsDataContainer === null + ? undefined + : (_a = mediaDataStructure.metricsFromMediaApiObject(recoMetricsDataContainer)) !== null && _a !== void 0 ? _a : undefined; + let impressionsIdType = "its_contentId"; + if (searchPageContext.pageType === searchShelves.SearchPageType.ChartsAndCategories) { + impressionsIdType = "static"; + } + const shelfMetricsOptions = { + id: shelfAttributes.id, + kind: null, + softwareType: null, + targetType: "swoosh", + title: (_b = shelfAttributes.title) !== null && _b !== void 0 ? _b : "", + pageInformation: searchPageContext.metricsPageInformation, + locationTracker: searchPageContext.metricsLocationTracker, + idType: impressionsIdType, + fcKind: undefined, + canonicalId: (_c = asString(data.meta, "canonicalId")) !== null && _c !== void 0 ? _c : undefined, + recoMetricsData: recoMetricsData, + }; + return shelfMetricsOptions; +} +/** + * Generates the impressions metrics options for the search chart or category + * @param objectGraph The App Store Object Graph + * @param model The chart or category model + * @param modelData The chart or category model data object + * @param searchShelfContext The context for the shelf containing the chart or category + * @returns The metrics options for the chart or category model + */ +export function createMetricsOptionsForChartOrCategory(objectGraph, model, modelData, searchShelfContext) { + var _a; + const chartModelMetricsOptions = { + ...searchShelfContext.metricsOptions, + ...impressionOptions(objectGraph, modelData, model.title, searchShelfContext.metricsOptions), + recoMetricsData: (_a = mediaDataStructure.metricsFromMediaApiObject(modelData)) !== null && _a !== void 0 ? _a : undefined, + targetType: metricsTargetTypeForChartOrCategory(model.density), + idType: "its_id", + }; + return chartModelMetricsOptions; +} +/** + * Generates the click metrics options for the search chart or category + * @param objectGraph The App Store Object Graph + * @param model The chart or category model + * @param modelData The chart or category model data object + * @param searchShelfContext The context for the shelf containing the chart or category + * @returns The metrics options for the chart or category model + */ +export function createClickMetricsOptionsForChartOrCategory(objectGraph, modelDensity, modelData, searchShelfContext) { + var _a; + const chartModelMetricsOptions = { + pageInformation: searchShelfContext.metricsOptions.pageInformation, + locationTracker: searchShelfContext.metricsOptions.locationTracker, + recoMetricsData: (_a = mediaDataStructure.metricsFromMediaApiObject(modelData)) !== null && _a !== void 0 ? _a : undefined, + targetType: metricsTargetTypeForChartOrCategory(modelDensity), + id: modelData.id, + }; + return chartModelMetricsOptions; +} +/** + * Gets the matching metrics target type for the chart or category model + * @param model The model we want the target type for + * @returns The target type for the model + */ +function metricsTargetTypeForChartOrCategory(modelDensity) { + if (isNothing(modelDensity)) { + return "tile"; + } + switch (modelDensity) { + case models.GenericSearchPageShelfDisplayStyleDensity.Density1: + return "tile"; + case models.GenericSearchPageShelfDisplayStyleDensity.Density2: + return "pill"; + default: + return "tile"; + } +} +//# sourceMappingURL=search-shelves.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/util.js b/node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/util.js new file mode 100644 index 0000000..8be872f --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/metrics/helpers/util.js @@ -0,0 +1,407 @@ +import { isNothing, isSome } from "@jet/environment"; +import * as serverData from "../../../foundation/json-parsing/server-data"; +import * as urls from "../../../foundation/network/urls"; +import * as errors from "../../../foundation/util/errors"; +import * as objects from "../../../foundation/util/objects"; +import * as content from "../../content/content"; +import * as deviceFamily from "../../content/device-family"; +export function targetTypeForMetricsOptions(objectGraph, options) { + let type = options.targetType; + if (!type) { + type = objectGraph.client.isVision ? "lockupSmall" : "lockup"; + } + return type; +} +export function idTypeForMetricsOptions(options) { + let type = options.idType; + if (type === "none") { + type = null; + } + else if (!type) { + type = "its_id"; + } + return type; +} +export function softwareTypeForData(objectGraph, data) { + return content.isArcadeSupported(objectGraph, data) ? "Arcade" : null; +} +export function metricsKindFromData(objectGraph, data, attributePlatformOverride = undefined) { + const type = serverData.asString(data, "type"); + const isMacType = deviceFamily.dataHasDeviceFamily(objectGraph, data, "mac", true); + const isOnlyMacType = deviceFamily.dataOnlyHasDeviceFamily(objectGraph, data, "mac", true); + const isIOSType = deviceFamily.dataHasAnyDeviceFamilies(objectGraph, data, ["iphone", "ipad", "ipod", "tvos", "watch"], true); + const isOnlyIOSType = deviceFamily.dataOnlyHasDeviceFamilies(objectGraph, data, ["iphone", "ipad", "ipod", "tvos", "watch"], true); + const isIOSDeviceType = objectGraph.client.isiOS || objectGraph.client.isTV || objectGraph.client.isWatch; + const isAppleSiliconDeviceType = objectGraph.client.isMac && objectGraph.appleSilicon.isSupportEnabled; + const isVisionDeviceType = objectGraph.client.isVision; + const isVisionType = deviceFamily.dataHasDeviceFamily(objectGraph, data, "realityDevice", true); + const isOnlyVisionType = deviceFamily.dataOnlyHasDeviceFamily(objectGraph, data, "realityDevice", true); + // If: + // - this is a mac only app, or + // - it has multiple types but we're currently on the Mac, or + // - it has multiple types and the plaform override is for Mac, + // use the 'macSoftware' or 'macSoftwareBundle' kinds + if (isOnlyMacType || + (isMacType && objectGraph.client.isMac) || + (isMacType && attributePlatformOverride === "osx")) { + switch (type) { + case "apps": + return "macSoftware"; + case "app-bundles": + return "macSoftwareBundle"; + default: + break; + } + } + // If: + // - this is a Vision only app, or + // - it has multiple types but we're currently on a Vision device, or + // - it has multiple types and the platform override is for Vision, + // use the 'visionSoftware' kind. + if (isOnlyVisionType || + (isVisionType && objectGraph.client.isVision) || + (isVisionType && attributePlatformOverride === "xros")) { + switch (type) { + case "apps": + return "visionSoftware"; + case "app-bundles": + // To add if/when vision supports app bundles. + break; + default: + break; + } + } + // If this is an iOS only app or it has multiple types but we're currently: + // - on an iOS device, or + // - on an Apple Silicon Mac, or + // - on a Vision device, or + // - the platform override is for an iOS-like device + // use the 'iosSoftware' or 'mobileSoftwareBundle' kinds + if (isOnlyIOSType || + (isIOSType && isIOSDeviceType) || + (isIOSType && isAppleSiliconDeviceType) || + (isIOSType && isVisionDeviceType) || + (isIOSType && attributePlatformOverride === "ios") || + (isIOSType && attributePlatformOverride === "watch") || + (isIOSType && attributePlatformOverride === "appletvos")) { + switch (type) { + case "apps": + return "iosSoftware"; + case "app-bundles": + return "mobileSoftwareBundle"; + default: + break; + } + } + switch (type) { + case "in-apps": + return "softwareAddOn"; + case "groupings": + return "grouping"; + case "editorial-elements": + case "editorial-items": + return "editorialItem"; + case "developers": + return "artist"; + default: + return null; + } +} +export function emptyStringIfNullOrUndefined(object) { + if (object === null || object === undefined) { + return ""; + } + return object; +} +export function extractSiriRefAppFromRefURL(urlString) { + if (!urlString) { + return null; + } + const refUrl = new urls.URL(urlString); + let extracteRefApp = null; + const query = refUrl.query; + if (isSome(query)) { + for (const key of Object.keys(query)) { + if (key === "referrer") { + if (query[key] === "siri") { + extracteRefApp = "com.apple.siri"; + } + break; + } + } + } + return extracteRefApp; +} +export function sanitizedMetricsDictionary(dict) { + var _a; + if (isNothing(dict)) { + return {}; + } + return (_a = serverData.asInterface(sanitizeJson(serverData.asJSONData(dict)))) !== null && _a !== void 0 ? _a : {}; +} +function sanitizeJson(json) { + if (serverData.isNull(json)) { + return null; + } + else if (json instanceof Array) { + const arrayCopy = []; + for (const value of json) { + const sanitizedValue = sanitizeJson(value); + if (serverData.isDefinedNonNull(sanitizedValue)) { + arrayCopy.push(sanitizedValue); + } + } + return arrayCopy; + } + else if (json instanceof Object) { + const objectCopy = {}; + Object.keys(json).forEach((key, index, array) => { + const value = json[key]; + const sanitizedValue = sanitizeJson(value); + if (serverData.isDefinedNonNull(sanitizedValue)) { + objectCopy[key] = sanitizedValue; + } + }); + return objectCopy; + } + return json; +} +export function searchTermFromRefURL(refUrlString) { + if (!refUrlString) { + return null; + } + const refUrl = new urls.URL(refUrlString); + const queryItems = refUrl.query; + const searchTerm = queryItems === null || queryItems === void 0 ? void 0 : queryItems["term"]; + const path = refUrl.pathname; + if (serverData.isNull(searchTerm) || serverData.isNull(path)) { + return null; + } + if (!path.endsWith("/search")) { + return null; + } + // the url object has already urldecoded this query parameter + const plainTerm = searchTerm; + return plainTerm; +} +/** + * Get a search term from a product URL, if one has been added. + * @param productUrlString The URL of a product + * @returns A string, if a search term exists. + */ +export function searchTermFromProductURL(productUrlString) { + if (isNothing(productUrlString)) { + return null; + } + const productUrl = new urls.URL(productUrlString); + const queryItems = productUrl.query; + const searchTerm = queryItems === null || queryItems === void 0 ? void 0 : queryItems["searchTerm"]; + const path = productUrl.pathname; + if (isNothing(searchTerm) || isNothing(path)) { + return null; + } + if (!path.includes("/app")) { + return null; + } + const plainTerm = searchTerm; + return plainTerm; +} +/** + * Convert a product's data `type` and top lockup icon into a `MetricsPlatformDisplayStyle` object. + * This is used to determine how an app icon is presented (i.e. as watch, as atv) for metrics. + * @param objectGraph Current object graph + * @param data Server data for the app + * @param artwork The product's top lockup icon. + * @param clientIdentifierOverride The preferred client identifier, if any. + * @returns a MetricsPlatformDisplayStyle object. + */ +// <rdar://problem/47715014> Metrics: Send editorial intent buy param to finance for watch apps +export function metricsPlatformDisplayStyleFromData(objectGraph, data, artwork, clientIdentifierOverride) { + if (!data || !artwork) { + return "unknown"; + } + if (data.type === "app-bundles") { + return "bundle"; + } + const artworkStyle = artwork.style; + if (isNothing(artworkStyle)) { + return "unknown"; + } + switch (artworkStyle) { + case "roundedRect": + case "roundedRectPrerendered": { + return "ios"; + } + case "unadorned": { + return "mac"; + } + case "tvRect": { + return "tv"; + } + case "round": + case "roundPrerendered": { + const attributePlatform = content.iconAttributePlatform(objectGraph, data, clientIdentifierOverride !== null && clientIdentifierOverride !== void 0 ? clientIdentifierOverride : undefined); + if (attributePlatform === "xros") { + return "vision"; + } + else { + return "watch"; + } + } + case "pill": { + return "messages"; + } + case "iap": { + return "iap"; + } + default: { + errors.unreachable(artworkStyle); + return "unknown"; + } + } +} +// region Search GhostHint +/** + * Move ghostHint fields for click events + * @param eventFields Fields of event to modify in place. + */ +export function adjustGhostHintFieldsForClick(eventFields) { + /** + * Copy `searchGhostHintPrefix` to `searchPrefix` if no prefix is present. + * - JS-built search actions specify searchPrefix (matches prefixTerm of hint request). + * - Native-built search actions don't specify searchPrefix (dynamic). + */ + const existingSearchPrefix = serverData.asString(eventFields, "searchPrefix"); + const ghostHintPrefix = serverData.asString(eventFields, "searchGhostHintPrefix"); + if (serverData.isNull(existingSearchPrefix) && isSome(ghostHintPrefix) && (ghostHintPrefix === null || ghostHintPrefix === void 0 ? void 0 : ghostHintPrefix.length) > 0) { + eventFields["searchPrefix"] = ghostHintPrefix; + } + /** + * Delete `searchGhostHintTerm` if phase is pending (i.e. only send if displayed or rejected) per POR. + */ + const ghostHintTermPhase = serverData.asString(eventFields, "searchGhostHintTermPhase"); + if (ghostHintTermPhase === "pending") { + delete eventFields["searchGhostHintTerm"]; + } +} +/** + * Move ghostHint fields for seach events + * @param eventFields Fields of event to modify in place. + */ +export function adjustGhostHintFieldsForSearch(eventFields) { + var _a; + /** + * Copy `searchGhostHintPrefix` to `searchPrefix` if no prefix is present. + * - JS-built search actions specify actionDetails.searchPrefix (matches prefixTerm of hint request). + * - Native-built search actions don't specify actionDetails.searchPrefix (dynamic). + */ + const actionDetails = (_a = eventFields["actionDetails"]) !== null && _a !== void 0 ? _a : {}; + const existingSearchPrefix = actionDetails["searchPrefix"]; + const ghostHintPrefix = serverData.asString(eventFields, "searchGhostHintPrefix"); + if (serverData.isNull(existingSearchPrefix) && isSome(ghostHintPrefix) && (ghostHintPrefix === null || ghostHintPrefix === void 0 ? void 0 : ghostHintPrefix.length) > 0) { + actionDetails["searchPrefix"] = ghostHintPrefix; + eventFields["actionDetails"] = actionDetails; + } + /** + * Delete `searchGhostHintTerm` if phase is pending (i.e. only send if displayed or rejected) per POR. + */ + const ghostHintTermPhase = serverData.asString(eventFields, "searchGhostHintTermPhase"); + if (ghostHintTermPhase === "pending") { + delete eventFields["searchGhostHintTerm"]; + } + /** + * Prune `searchGhostHintTerm` if `actionType` is `input`, i.e. is from hints page loading. + * This is a side-effect of when the event is fired, and otherwise doesn't belong there. + */ + if (eventFields["actionType"] === "input") { + delete eventFields["searchGhostHintTerm"]; + } +} +/** + * Clean up extraneous generated fields. These extra fields are speculative + * to allow some additional JS customization if needed for SSS. + * @param eventFields Event fields to modify in place. + */ +export function removeExtraGhostHintFields(eventFields) { + // Prune prefix annotation. + delete eventFields["searchGhostHintPrefix"]; + // Prune phase annotation. + delete eventFields["searchGhostHintTermPhase"]; + // Prune historical annotation. + delete eventFields["searchGhostHintTermLastDisplayed"]; +} +// endregion +// region Arcade Upsell Marketing Items +/** + * Returns a dictionary of fields pulled out from the meta.metrics dictionary associated with a marketing item response. + * This data comes from Mercury, and we simply pull out relevant fields to be hoisted into the top-level base field on + * metrics events (page/impression/click). + * @param marketingItemData The marketing item response data. + */ +export function marketingItemTopLevelBaseFieldsFromData(objectGraph, marketingItemData) { + if (!serverData.isDefinedNonNull(marketingItemData)) { + return null; + } + const fieldsData = {}; + const marketingDictionary = serverData.asDictionary(marketingItemData, "meta.metrics"); + if (!serverData.isDefinedNonNullNonEmpty(marketingDictionary)) { + return null; + } + const channelPartner = serverData.asString(marketingDictionary, "channelPartner"); + if (isSome(channelPartner) && (channelPartner === null || channelPartner === void 0 ? void 0 : channelPartner.length) > 0) { + fieldsData["channelPartner"] = channelPartner; + } + const eligibilityType = serverData.asString(marketingDictionary, "eligibilityType"); + if (isSome(eligibilityType) && (eligibilityType === null || eligibilityType === void 0 ? void 0 : eligibilityType.length) > 0) { + fieldsData["eligibilityType"] = eligibilityType; + } + const upsellScenario = serverData.asString(marketingDictionary, "upsellScenario"); + if (isSome(upsellScenario) && (upsellScenario === null || upsellScenario === void 0 ? void 0 : upsellScenario.length) > 0) { + fieldsData["upsellScenario"] = upsellScenario; + } + fieldsData["marketing"] = { + marketingItemId: marketingItemData.id, + }; + return fieldsData; +} +// endregion +// region On Device Personalization +/** + * Merges the provided reco metrics data with the on device personalization metrics data. + * @param metricsData The input reco metrics data + * @param onDevicePersonalizationProcessingType The type of processing that occurred on the data + * @param onDevicePersonalizationMetricsData The metrics data provided by the on device personalization framework + * @returns Reco metrics data, or null + */ +export function combinedRecoMetricsDataFromMetricsData(metricsData, onDevicePersonalizationProcessingType, onDevicePersonalizationMetricsData) { + let combinedMetricsData = null; + if (serverData.isDefinedNonNull(metricsData)) { + combinedMetricsData = objects.shallowCopyOf(metricsData); + } + if (serverData.isDefinedNonNull(onDevicePersonalizationProcessingType)) { + if (serverData.isNull(combinedMetricsData)) { + combinedMetricsData = {}; + } + combinedMetricsData["odpModuleUpdate"] = onDevicePersonalizationProcessingType.toString(); + } + if (serverData.isDefinedNonNullNonEmpty(onDevicePersonalizationMetricsData)) { + if (serverData.isNull(combinedMetricsData)) { + combinedMetricsData = {}; + } + combinedMetricsData["userSegment"] = onDevicePersonalizationMetricsData; + } + return combinedMetricsData; +} +// endregion +// region Metrics ID +/** + * Clean up dsid fields. We want this as a last line of defense for removing dsid. + * @param eventFields Event fields to modify in place. + */ +export function removeDSIDFields(eventFields) { + // Prune dsid + delete eventFields["dsid"]; + delete eventFields["DSID"]; +} +// endregion +//# sourceMappingURL=util.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/metrics/metrics-referral-context.js b/node_modules/@jet-app/app-store/tmp/src/common/metrics/metrics-referral-context.js new file mode 100644 index 0000000..2e0a81f --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/metrics/metrics-referral-context.js @@ -0,0 +1,370 @@ +import { isNothing } from "@jet/environment"; +import { asInterface, asJSONData, asString, isDefinedNonNull, isDefinedNonNullNonEmpty, isNull, traverse, } from "../../foundation/json-parsing/server-data"; +/** + * This class is used as a singleton to track whether we should use the crossfire attribution from the JS purchase + * configuration, this is to remedy the following issue: + * + * rdar://96291594 ([Sydney][Regression][App Store][Clickstream][iTMS11] RefApp missing from page event - (iOS) Sydney20A312a) + * + * The flow of how this object is used is as follows: + * + * 1. Deeplink comes in and is routed to the product page controller, containing the crossfire referrer data + * 2. We build the product page model and pass the referrer data along + * 3. As we're building the page model for this product page, we check to see if we have valid referrer data. If we do + * we call into `MetricsReferralContext.beginReferralContextForProduct(appProductId)`, which will create store a unique + * referral context identifier for this product page. This identifier will be used to make sure we're only every dealing + * with the original product page. + * 4. When we create the pageEvent for this product page we modify all the event fields to include unique referral context id, + * using `MetricsReferralContext.addReferralContextToMetricsFieldsIfNecessary(metricsFields)` + * 5. The referrer data is then added to the purchase configuration for the product lockup's buy button, which comes back to us + * at time of purchase decoration + * 6. Later when the user taps ont the product buy button and we enter the purchase decoration flow, we check to see if we should + * skip adding the native referrer data to the purchase buy params, which would clear out the referrer data since the page change, + * event has cleared it out natively. We call `MetricsReferralContext.shouldUseJSReferralData` for this check. + * 7. If any page event comes in to the metrics event linter, we check to see if it has a referral context id that is not the + * current one, if it does we clear the current metrics referrer contents using `MetricsReferralContext.endReferralContextIfNecessaryForPageExitEvent(pageEvent)` + * 8. Additionally in the event linter we always call into `MetricsReferralContext.removeReferralContextInfoFromMetricsEvent(metricsFields)` + * which deletes the added referralContextId field. + */ +export class MetricsReferralContext { + static createSharedMetricsReferralContext(objectGraph) { + if (MetricsReferralContext.shared) { + return; + } + MetricsReferralContext.shared = new MetricsReferralContext(objectGraph); + } + /** + * Initializers + */ + /** + * @param objectGraph The object graph to use to determine if the referral context is needed. + */ + constructor(objectGraph) { + /** + * Properties + */ + /** + * Identifier denoting that we should use the extRefUrl2 and extRefApp2 from the original purchase configuration + * found in the purchase token, rather than using the native metrics data which would clear out the ref data + * due to pageChange events. + */ + this.currentReferral = null; + if (objectGraph.host.isiOS) { + this.isMetricsReferralContextRequired = true; + this.isEventDetailClickEventOverrideNecessary = !objectGraph.host.isOSAtLeast(16, 2, 0); + } + else if (objectGraph.host.isMac) { + this.isMetricsReferralContextRequired = objectGraph.host.isOSAtLeast(13, 0, 0); + this.isEventDetailClickEventOverrideNecessary = false; + } + else { + this.isMetricsReferralContextRequired = false; + this.isEventDetailClickEventOverrideNecessary = false; + } + } + /** + * Returns whether we should use the JS referral data or not. + */ + get shouldUseJSReferralData() { + return this.isMetricsReferralContextRequired && isDefinedNonNull(this.currentReferral); + } + /** + * Returns the current referral data for the active context + */ + get activeReferralData() { + if (!this.shouldUseJSReferralData) { + return null; + } + if (this.currentReferral === null || !this.currentReferral.isActive) { + return null; + } + return this.currentReferral.data; + } + /** + * Setting Referral Data + */ + /** + * Called when we get a deep link into the product page and need to make sure we track + * the referral data for this page. + * + * @param productId The id of the product the referral context is for. + * @param referrerData The referral data for this product page. + */ + setReferralDataForProduct(productId, referrerData) { + var _a, _b, _c; + if (!this.isMetricsReferralContextRequired || isNull(referrerData)) { + return; + } + const extRefApp2 = (_a = asString(referrerData, "app")) !== null && _a !== void 0 ? _a : null; + const extRefUrl2 = (_b = asString(referrerData, "externalUrl")) !== null && _b !== void 0 ? _b : null; + const kind = (_c = asInterface(referrerData, "kind")) !== null && _c !== void 0 ? _c : null; + this.currentReferral = { + id: `${productId}_${Date.now()}`, + data: { + extRefApp2, + extRefUrl2, + kind, + refUrl: null, + }, + isActive: false, + productPageExtensionInfo: null, + }; + } + /** + * Called when we are linting a page event, if this page event is for a product page extension, and + * we don't yet have an active crossfire referral context, we need to make sure we start one, since there + * should always be one in this case. The fact that there is not means that the pageChange event cleared it out + * natively. + * + * Additionally we're going to add the referral context id to the page event so we can track it later, and know + * when to end the referral context. + * + * @param pageEvent The page event that is currently being linted, so we can check to see if we're on a product page, + * in the product page extension. + */ + setReferralDataForProductPageExtensionIfNecessary(pageEvent) { + var _a, _b; + if (!this.isMetricsReferralContextRequired) { + return; + } + const productId = asString(pageEvent, "pageId"); + const refApp = asString(pageEvent, "refApp"); + if (!MetricsReferralContextUtil.isProductPageExtension(pageEvent) || + !MetricsReferralContextUtil.isValidPageEvent(pageEvent) || + isNull(productId) || + isNull(refApp)) { + return; + } + const extRefUrl = (_a = asString(pageEvent, "extRefUrl")) !== null && _a !== void 0 ? _a : null; + const refAppKindName = asString(pageEvent, "refAppType"); + let refAppKindContext; + switch (refAppKindName) { + case "trampoline": + refAppKindContext = asJSONData(traverse(pageEvent, "trampolineContext")); + break; + case "widget": + refAppKindContext = asJSONData(traverse(pageEvent, "widgetContext")); + break; + default: + refAppKindContext = {}; + } + const refUrl = (_b = asString(pageEvent, "refUrl")) !== null && _b !== void 0 ? _b : null; + this.currentReferral = { + id: `${productId}_${Date.now()}`, + data: { + extRefApp2: refApp, + extRefUrl2: extRefUrl, + refUrl: refUrl, + kind: { + name: refAppKindName, + context: refAppKindContext, + }, + }, + isActive: false, + productPageExtensionInfo: { + productId, + }, + }; + this.addReferralContextToMetricsFieldsIfNecessary(pageEvent); + } + /** + * Begin / End Metrics Referral Context + */ + /** + * Called when we get a deep link into the product page and need to make sure we track + * whether the referral data should be used from the js configuration. + * + * @param pageEvent Some page event that may be associated with the current referral context. + */ + beginReferralContextForPageIfNecessary(pageEvent) { + if (!this.isMetricsReferralContextRequired || !MetricsReferralContextUtil.isValidPageEvent(pageEvent)) { + return; + } + if (!MetricsReferralContextUtil.isReferralForEvent(this.currentReferral, pageEvent)) { + return; + } + if (this.currentReferral !== null) { + this.currentReferral.isActive = true; + } + } + /** + * Called when we get a pageExit event after the page event for the current deeplinked + * product page, if there is one. This will reset the flag to use the native metrics. This should + * always be the next pageExit event after the page enter event + */ + endReferralContextIfNecessaryForPageEvent(pageExitEvent) { + if (!this.isMetricsReferralContextRequired || !MetricsReferralContextUtil.isValidPageEvent(pageExitEvent)) { + return; + } + if (!MetricsReferralContextUtil.isReferralForEvent(this.currentReferral, pageExitEvent)) { + return; + } + this.currentReferral = null; + } + /** + * Setting / Clearing Page Fields + */ + /** + * Called when we're building the metrics events for a product page, this way we can tag the events with the current + * referral context id if there is one. + * + * @param pageMetricsFields The page event fields we can modify to track the current product page. + */ + addReferralContextToMetricsFieldsIfNecessary(pageMetricsFields) { + var _a; + if (!this.isMetricsReferralContextRequired) { + return; + } + pageMetricsFields[MetricsReferralContext.referralContextEventField] = (_a = this.currentReferral) === null || _a === void 0 ? void 0 : _a.id; + } + /** + * Called when linting our metrics events so we can make sure to remove the referral context id, so its not sent to the server + * + * @param metricsEvent The metrics event we're currently linting. + */ + removeReferralContextInfoFromMetricsEvent(metricsEvent) { + if (!this.isMetricsReferralContextRequired) { + return; + } + delete metricsEvent[MetricsReferralContext.referralContextEventField]; + } + /** + * Event Attribution + */ + /** + * If we have an active referral context, we need to make sure we add the referral data to the event. + * + * @param metricsEvent The metrics event we're currently linting. + */ + addReferralDataToEventIfNecessary(metricsEvent) { + if (isNull(this.activeReferralData)) { + return; + } + if (!MetricsReferralContextUtil.shouldAddReferralDataToEvent(metricsEvent)) { + return; + } + if (MetricsReferralContextUtil.isEventDetailsClickEvent(metricsEvent) && + !this.isEventDetailClickEventOverrideNecessary) { + return; + } + if (MetricsReferralContextUtil.isEventDetailsClickEvent(metricsEvent)) { + // Correct the `pageType` of this event for rdar://101302008 ([Sydney][App Store] [Clickstream][iTMS11] click event on EventDetails page from an app referral has incorrect pageType) + // Then continue on and apply referral data. + metricsEvent["pageType"] = "EventDetails"; + } + metricsEvent["refApp"] = this.activeReferralData.extRefApp2; + metricsEvent["extRefUrl"] = this.activeReferralData.extRefUrl2; + if (isDefinedNonNullNonEmpty(this.activeReferralData.refUrl)) { + metricsEvent["refUrl"] = this.activeReferralData.refUrl; + } + if (this.activeReferralData !== null && this.activeReferralData.kind !== null) { + metricsEvent["refAppType"] = this.activeReferralData.kind.name; + switch (metricsEvent["refAppType"]) { + case "trampoline": + metricsEvent["trampolineContext"] = this.activeReferralData.kind.context; + break; + case "widget": + metricsEvent["widgetContext"] = this.activeReferralData.kind.context; + break; + default: + break; + } + } + } +} +/** + * They event field to use on a page event so we can determine later if this page event belongs to + * the same deeplinked product page. + */ +MetricsReferralContext.referralContextEventField = "referralContextId"; +// eslint-disable-next-line @typescript-eslint/no-extraneous-class +class MetricsReferralContextUtil { + /** + * Check to see if the pageEvent is within the ProductPageExtension + * + * @param pageEvent The page event we're checking to see if its in an extension. + * @returns Whether "app" for this event is a valid type + */ + static isProductPageExtension(pageEvent) { + const app = asString(pageEvent, "app"); + return app === MetricsReferralContextUtil.productPageExtensionAppId; + } + /** + * Check to see if the current page event is for a product page. + * + * @param pageEvent The page event we're checking to see if its a product page. + * @returns Whether "pageType" for this event is a valid type + */ + static isValidPageEvent(pageEvent) { + const pageType = asString(pageEvent, "pageType"); + if (isNothing(pageType)) { + return false; + } + return MetricsReferralContextUtil.validPageEventTypes.has(pageType); + } + /** + * This method will check the `referralContextEventField` to see if it matches the current referral. + * + * @param referral The current metrics referral taken from the referral context + * @param event Some event to test whether there is an associated referral context. And if so + * if that referral context matches. + */ + static isReferralForEvent(referral, event) { + var _a; + if (isNull(referral)) { + return false; + } + const referralContextId = event[MetricsReferralContext.referralContextEventField]; + const productId = asString(event, "pageId"); + if (isDefinedNonNull(referralContextId)) { + return referralContextId === referral.id; + } + else if (MetricsReferralContextUtil.isProductPageExtension(event) && isDefinedNonNull(productId)) { + // For product page extensions we do not get a chance to add the referralContextId to the + // pageExit event so we need to check the productId to see if it matches the current referral. + return productId === ((_a = referral === null || referral === void 0 ? void 0 : referral.productPageExtensionInfo) === null || _a === void 0 ? void 0 : _a.productId); + } + else { + return false; + } + } + static shouldAddReferralDataToEvent(event) { + // Generally, we don't want to force referral data onto click events, but this is not true for In-App Events (IAE): + // rdar://101399254 ([Sydney] [App Store][Clickstream][iTMS11] Missing refApp and extRefURL on IAE Click Open events through App/Web Referrals) + // This only applies prior to SydneyC, as this was fixed natively there. + if (event.eventType === "click") { + return this.isEventDetailsClickEvent(event); + } + return true; + } + /** + * Check whether the event is for a click on an In-App Events (IAE) page. + * + * @param event The event we're checking to see if it's on a an IAE page. + */ + static isEventDetailsClickEvent(event) { + if (event.eventType !== "click") { + return false; + } + const location = event.location; + const currentLocation = location === null || location === void 0 ? void 0 : location[0]; + return isDefinedNonNull(currentLocation) && currentLocation.locationType === "EventDetails"; + } +} +/** + * The identifier for the product page extension in a metrics page event + */ +MetricsReferralContextUtil.productPageExtensionAppId = "com.apple.AppStore.ProductPageExtension"; +/** + * The identifier used for the pageType field of an app event page event. + */ +MetricsReferralContextUtil.eventDetailsPageType = "EventDetails"; +/** + * The set of valid page types for a product page, page event + */ +MetricsReferralContextUtil.validPageEventTypes = new Set([ + "Software", + "SoftwareBundle", + MetricsReferralContextUtil.eventDetailsPageType, +]); +//# sourceMappingURL=metrics-referral-context.js.map
\ No newline at end of file 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 diff --git a/node_modules/@jet-app/app-store/tmp/src/common/personalization/on-device-impression-demotion.js b/node_modules/@jet-app/app-store/tmp/src/common/personalization/on-device-impression-demotion.js new file mode 100644 index 0000000..f2430e9 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/personalization/on-device-impression-demotion.js @@ -0,0 +1,73 @@ +import * as serverData from "../../foundation/json-parsing/server-data"; +import { demoteByEngagements } from "@amp/amd-apps"; +// The AMD key used to retrieve the engagement events from the app store. +export const AMSEngagementAppStoreEventKey = "appStore.getEngagementEvents"; +/** + * personalize the dataItems based on the impression data for that shelf. + * + * @param objectGraph The object graph. + * @param dataItems The data items to personalize. + * @param impressionData The impression data to use for personalization. + * @returns The personalized data items. + */ +export function personalizeDataItems(dataItems, impressionData, shelfRecoMetricsData) { + const shelfAlgoId = shelfRecoMetricsData["reco_algoId"]; + if (serverData.isNullOrEmpty(shelfAlgoId) || + serverData.isNullOrEmpty(dataItems) || + serverData.isNullOrEmpty(impressionData[shelfAlgoId])) { + return dataItems; + } + // First create Candidate objects from the data items. + const candidates = dataItems.map((dataItem) => { + var _a; + const adamId = serverData.asNumber(dataItem.id); + const score = (_a = serverData.asNumber(dataItem, "meta.personalizationData.score")) !== null && _a !== void 0 ? _a : 0; + const candidate = { identifier: adamId, score: score }; + return candidate; + }); + // Demote the candidates based on the impression data. + const shelfRecoData = impressionData[shelfAlgoId]; + const rerankedCandidates = demoteByEngagements(candidates, shelfRecoData); + // Create a lookup map from dataItems to allow faster rearranging + const dictionary = dataItems.reduce((acc, item) => ({ ...acc, [item.id]: item }), {}); + // create a new array based on the rearranged Candidate array + const rearranged = rerankedCandidates.map((candidate) => dictionary[candidate.identifier.toString()]); + return rearranged; +} +/** + * Takes the AMD response and creates a map of EngagementEvent per shelf. + * + * @param objectGraph The object graph. + * @param data The data to convert. + * @returns The map of EngagementEvent per shelf. + */ +export function impressionEventsFromData(objectGraph, data) { + // go through all the impressionData and return the EgagementData + if (!serverData.isDefinedNonNullNonEmpty(data)) { + return {}; + } + // Impression data is a map of shelf ids to impression arrays. + // We want to keep this relationship so only impressions for a given shelf are returned. + const impressionData = serverData.asDictionary(data, "data.engagementEvents.impression"); + const convertedMap = {}; + // Iterate over each key in the original map and convert the data into EngagementEvents which can be passed to the demoteByEngagements function. + for (const key in impressionData) { + if (serverData.isDefinedNonNullNonEmpty(key)) { + const impressions = serverData.asArrayOrEmpty(impressionData, key); + convertedMap[key] = impressions.map((impression) => { + const adamId = serverData.asNumber(impression["adamId"]); + const timestamp = serverData.asNumber(impression["eventTimeMillis"]); + const event = { adamIdentifier: adamId, timestamp: timestamp }; + return event; + }); + } + } + return convertedMap; +} +/** + * Convenience function for determining if data personalization is available. + */ +export function isImpressionDemotionAvailable(objectGraph) { + return objectGraph.client.isiOS && objectGraph.bag.enableRecoOnDeviceReordering; +} +//# sourceMappingURL=on-device-impression-demotion.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/personalization/on-device-personalization-processing.js b/node_modules/@jet-app/app-store/tmp/src/common/personalization/on-device-personalization-processing.js new file mode 100644 index 0000000..538e708 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/personalization/on-device-personalization-processing.js @@ -0,0 +1,370 @@ +import { isNothing } from "@jet/environment"; +import * as serverData from "../../foundation/json-parsing/server-data"; +import { diversifyDataItems, getOrderedAppIds, getUpdatedScoreAfterBoosting, PersonalizedData, } from "./on-device-recommendations-common"; +/** + * This utility class simplifies processing the raw data, by decorating with some key properties. + * */ +class PersonalizedDataDefault extends PersonalizedData { + constructor(rawData) { + super(); + this.rawData = rawData; + this.isExactMatch = false; + this.isWildcardMatch = false; + this.isUnpersonalizedMatch = false; + this.isFallbackMatch = false; + this.appId = null; + this.groupId = null; + this.score = 0; + this.modifiedScore = 0; + this.onDeviceScore = 0; + } +} +// Represents a "match all" wildcard segment. Any data items that have this segment are always considered a match. +const alwaysMatchUserSegment = "-1"; +/** + * Converts a list of raw data blobs into a list that has been personalized for the user, based upon on device personalization data. + * + * If use_segment_scores is true, the rules we follow here are: + * 1. Choose the data items that have personalization segments which match the user + * 2. Remove some data items so that there is only one per group + * 3. Bring any data items where the user exactly matches the personalization segment to the front of the list + * + * If needed, we may also include fallback results to reach a preferred number of results. For any group where no matches are found, the last + * item in that group can be used as a fallback. We can only ever have one item per group, so it may not always be possible to reach the + * preferred number of results. + * + * If use_signals is true, we rerank content using the on-device scores + * + * @param dataItems The raw data blobs. + * @param onDevicePersonalizationDataContainer The on device personalization data container for the user, used for matching segments against the dataItems. + * @param includeItemsWithNoPersonalizationData Whether dataItems without any valid personalization data should always be included in the results. + * @param allowUnmatchedFallbackResults Whether to allow fallback results to be included in the results. This will only be utilised in order to reach a preferredResultCount. + * @param preferredResultCount The preferred number of items to be included in the results. + * @param parentAppId An optional appID, which is the parent for all the dataItems. Currently only used for search. + * @param diversify An optional flag that determines if we should diverse the personalized results on the basis of server side apps ranking + * @returns The personalized set of data. This will be a subset (or all) of the original dataItems, and metrics data. + */ +export function personalizeDataItems(objectGraph, dataItems, onDevicePersonalizationDataContainer, includeItemsWithNoPersonalizationData, allowUnmatchedFallbackResults, preferredResultCount, parentAppId, diversify) { + var _a; + let sortResult = { sortedDataItems: [], processingType: 0 }; + const useSignals = (_a = onDevicePersonalizationDataContainer === null || onDevicePersonalizationDataContainer === void 0 ? void 0 : onDevicePersonalizationDataContainer.metricsData["use_signals"]) !== null && _a !== void 0 ? _a : false; + if (!useSignals) { + // First decorate our raw dataItems with segment and group information + const personalizedDataItems = personalizedDataItemsFromDataItems(objectGraph, dataItems, onDevicePersonalizationDataContainer === null || onDevicePersonalizationDataContainer === void 0 ? void 0 : onDevicePersonalizationDataContainer.personalizationData, includeItemsWithNoPersonalizationData, parentAppId); + // Get server side ordering of app Ids to be used for diversification + const serverSideAppIdsOrdering = getOrderedAppIds(personalizedDataItems); + // Now iterate through the list of personalizedDataItems, and choose one per group + const matchedDataItemsIncludingFallback = filterDataItemsIntoOnePerGroup(objectGraph, personalizedDataItems); + // Now sort the data items, respecting our preferredResultCount if needed + sortResult = sortDataItems(objectGraph, matchedDataItemsIncludingFallback, allowUnmatchedFallbackResults, serverSideAppIdsOrdering, preferredResultCount, diversify); + } + else { + // First decorate our raw dataItems with frequency, recency, usage information + const personalizedDataItems = personalizedDataItemsFromDataItemsOnDeviceSignals(objectGraph, dataItems, onDevicePersonalizationDataContainer === null || onDevicePersonalizationDataContainer === void 0 ? void 0 : onDevicePersonalizationDataContainer.personalizationData, includeItemsWithNoPersonalizationData, parentAppId); + // Now sort the data items + const sortedDataItems = getUpdatedScoreAfterBoosting(personalizedDataItems, onDevicePersonalizationDataContainer === null || onDevicePersonalizationDataContainer === void 0 ? void 0 : onDevicePersonalizationDataContainer.metricsData); + const orderWasNotChanged = personalizedDataItems.every((dataItem, index) => { + return dataItem === sortedDataItems[index]; + }); + sortResult = { + sortedDataItems: sortedDataItems, + processingType: orderWasNotChanged + ? 0 /* onDevicePersonalization.ProcessingType.contentsNotChanged */ + : 2 /* onDevicePersonalization.ProcessingType.contentsSorted */, + }; + if (serverData.isDefinedNonNull(preferredResultCount) && + sortResult.sortedDataItems.length >= preferredResultCount) { + sortResult.sortedDataItems = sortResult.sortedDataItems.slice(0, preferredResultCount); + } + } + // We only need to return the raw data blobs, so remove the personalization decoration + const finalDataItems = sortResult.sortedDataItems.map((personalizedDataItem) => personalizedDataItem.rawData); + // Generate the processing type value + const filterType = dataItems.length !== finalDataItems.length + ? 1 /* onDevicePersonalization.ProcessingType.contentsFiltered */ + : 0 /* onDevicePersonalization.ProcessingType.contentsNotChanged */; + const processingType = filterType + sortResult.processingType; + return { + personalizedData: finalDataItems, + processingType: processingType, + }; +} +/** + * Creates a list of `PersonalizedData` objects, based on the input raw data items. + * + * @param dataItems The raw data blobs. + * @param onDevicePersonalizationData The on device personalization data, used for matching personalization segments against the dataItems. + * @param includeItemsWithNoPersonalizationData Whether dataItems without any valid personalization data should be included in the results. + * @param parentAppId An optional appID, which is the parent for all the dataItems. Currently only used for search. + * @returns A list of PersonalizedData objects. + */ +function personalizedDataItemsFromDataItemsOnDeviceSignals(objectGraph, dataItems, onDevicePersonalizationData, includeItemsWithNoPersonalizationData, parentAppId) { + const personalizedDataItems = []; + for (const data of dataItems) { + const personalizedData = new PersonalizedDataDefault(data); + // Filter out invalid data + const score = serverData.asNumber(data, "meta.personalizationData.score"); + let appId = serverData.asString(data, "meta.personalizationData.appId"); + if ((isNothing(appId) || appId.length === 0) && (parentAppId === null || parentAppId === void 0 ? void 0 : parentAppId.length) > 0) { + // If we have a parentAppId this means we are coming from search, where `appId` is not provided. + appId = parentAppId; + } + if (isNothing(appId) || appId.length === 0) { + // Personalization data is missing or invalid. This may sometimes be valid, eg. evergreen today stories for when reco times out. + if (includeItemsWithNoPersonalizationData) { + personalizedData.isUnpersonalizedMatch = true; + personalizedDataItems.push(personalizedData); + } + continue; + } + if (serverData.isDefinedNonNull(onDevicePersonalizationData)) { + const onDevicePersonalizationDataForApp = onDevicePersonalizationData[appId]; + if (serverData.isDefinedNonNull(onDevicePersonalizationDataForApp) && + serverData.isDefinedNonNull(onDevicePersonalizationDataForApp.onDeviceSignals)) { + personalizedData.onDeviceScore = +onDevicePersonalizationDataForApp.onDeviceSignals; + } + } + personalizedData.appId = appId; + personalizedData.score = score !== null && score !== void 0 ? score : 0; + personalizedDataItems.push(personalizedData); + } + return personalizedDataItems; +} +/** + * Creates a list of `PersonalizedData` objects, based on the input raw data items. + * + * @param dataItems The raw data blobs. + * @param onDevicePersonalizationData The on device personalization data, used for matching personalization segments against the dataItems. + * @param includeItemsWithNoPersonalizationData Whether dataItems without any valid personalization data should be included in the results. + * @param parentAppId An optional appID, which is the parent for all the dataItems. Currently only used for search. + * @returns A list of PersonalizedData objects. + */ +function personalizedDataItemsFromDataItems(objectGraph, dataItems, onDevicePersonalizationData, includeItemsWithNoPersonalizationData, parentAppId) { + const personalizedDataItems = []; + for (const data of dataItems) { + const personalizedData = new PersonalizedDataDefault(data); + // Filter out invalid data + const rawDataUserSegments = serverData.asString(data, "meta.personalizationData.segId"); + let appId = serverData.asString(data, "meta.personalizationData.appId"); + let groupId = serverData.asString(data, "meta.personalizationData.grpId"); + if ((isNothing(appId) || appId.length === 0) && (parentAppId === null || parentAppId === void 0 ? void 0 : parentAppId.length) > 0) { + // If we have a parentAppId this means we are coming from search, where `appId` and `grpId` are not provided. + // Normally we filter our data items to only allow one item per group, so in this case we allocate a random + // group ID, so that none of the data items get filtered out for that reason. Later on as part of search + // results processing we will pick the first (valid) result, but only after ODP has finished. + appId = parentAppId; + groupId = objectGraph.random.nextUUID(); + } + if (serverData.isNullOrEmpty(rawDataUserSegments) || + serverData.isNullOrEmpty(appId) || + serverData.isNullOrEmpty(groupId)) { + // Personalization data is missing or invalid. This may sometimes be valid, eg. evergreen today stories for when reco times out. + if (includeItemsWithNoPersonalizationData) { + personalizedData.isUnpersonalizedMatch = true; + personalizedDataItems.push(personalizedData); + } + continue; + } + // Check if the data has the match all user segment + const dataUserSegments = rawDataUserSegments.split(","); + if (dataUserSegments.includes(alwaysMatchUserSegment)) { + personalizedData.isWildcardMatch = true; + } + // Check if any of the data segments match with the on device personalization data + if (serverData.isDefinedNonNull(onDevicePersonalizationData)) { + const onDevicePersonalizationDataForApp = onDevicePersonalizationData[appId]; + if (serverData.isDefinedNonNull(onDevicePersonalizationDataForApp)) { + for (const dataUserSegment of dataUserSegments) { + if (onDevicePersonalizationDataForApp.userSegments.includes(dataUserSegment)) { + personalizedData.isExactMatch = true; + break; + } + } + } + } + personalizedData.appId = appId; + personalizedData.groupId = groupId; + personalizedDataItems.push(personalizedData); + } + return personalizedDataItems; +} +/** + * Iterates through the list of given data items, and ensures we only have one per group. + * + * @param dataItems The data items to processed. + * @returns A subset of dataItems, with only one dataItem per group. + */ +function filterDataItemsIntoOnePerGroup(objectGraph, dataItems) { + var _a; + const filledGroupIds = new Set(); + const matchedDataItemsIncludingMultipleFallbacksPerGroup = []; + // Determine which groups have any exact matches + const groupIdsWithExactMatchesArray = dataItems + .filter((dataItem) => { + return dataItem.isExactMatch; + }) + .map((dataItem) => { + return dataItem.groupId; + }); + const groupIdsWithExactMatches = new Set(groupIdsWithExactMatchesArray); + // Now iterate through our data items, and filter out any we don't need + dataItems.forEach((dataItem, index) => { + // If an item has no group, we always include it. This would only happen for + // data which is missing valid personalization metadata, and we have specifically + // opted in to including these items in the results. + if (serverData.isNullOrEmpty(dataItem.groupId)) { + matchedDataItemsIncludingMultipleFallbacksPerGroup.push(dataItem); + return; + } + // We already have a match for this group, so move onto the next item + if (filledGroupIds.has(dataItem.groupId)) { + return; + } + // This item is an unpersonalized match, which will only occur if we permit this. + // These are always added to the result set. + if (dataItem.isUnpersonalizedMatch) { + matchedDataItemsIncludingMultipleFallbacksPerGroup.push(dataItem); + return; + } + // This item is the first exact match for this group, so add it into our result set + if (dataItem.isExactMatch) { + filledGroupIds.add(dataItem.groupId); + matchedDataItemsIncludingMultipleFallbacksPerGroup.push(dataItem); + return; + } + // If we know we have an exact match somewhere else for this group, we can just + // continue on to the next item, as the exact match will be picked later. + if (groupIdsWithExactMatches.has(dataItem.groupId)) { + return; + } + // We have no exact matches for this group, so we can now take wildcard matches. + if (dataItem.isWildcardMatch) { + filledGroupIds.add(dataItem.groupId); + matchedDataItemsIncludingMultipleFallbacksPerGroup.push(dataItem); + return; + } + // This item is not a match. As we don't have any matches for this group yet, + // we can mark it as a fallback. This does not necessarily mean it will be used, + // but it does mean it becomes available for use. groupIDs are not necessarily in + // sequential order, so we mark all of these as fallbacks, and filter them further below. + dataItem.isFallbackMatch = true; + matchedDataItemsIncludingMultipleFallbacksPerGroup.push(dataItem); + }); + // We now need to remove all the fallback items except for the last one in each group, so iterate + // through in reverse order and filter out any duplicates + const matchedDataItemsWithOneFallbackPerGroup = []; + const reversedMatchedDataItems = matchedDataItemsIncludingMultipleFallbacksPerGroup.slice().reverse(); + for (const dataItem of reversedMatchedDataItems) { + if (dataItem.isFallbackMatch) { + if (filledGroupIds.has(dataItem.groupId)) { + continue; + } + } + matchedDataItemsWithOneFallbackPerGroup.push(dataItem); + if (((_a = dataItem.groupId) === null || _a === void 0 ? void 0 : _a.length) > 0) { + filledGroupIds.add(dataItem.groupId); + } + } + // Return to our original order + matchedDataItemsWithOneFallbackPerGroup.reverse(); + return matchedDataItemsWithOneFallbackPerGroup; +} +/** + * Sorts the given list of data items, and optionally restricts the list to a specified number of results. + * + * @param dataItems The data items to process. + * @param allowUnmatchedFallbackResults Whether to allow fallback results to be included in the results. This will only be utilised in order to reach a preferredResultCount. + * @param preferredResultCount? The preferrd number of results. + * @param serverSideAppIdsOrdering List of ordered app ids from server side + * @param diversify An optional flag that determines if we should diverse the personalized results on the basis of server side apps ranking + * @returns The sorted list of dataItems, optionally restricted in length, + */ +function sortDataItems(objectGraph, dataItems, allowUnmatchedFallbackResults, serverSideAppIdsOrdering, preferredResultCount, diversify) { + let sortResult; + // Excluding fallback results is the preferred route, but if the number of results is less than our preferredResultCount, we will need to use the fallback results. + const dataItemsWithoutFallback = dataItems.filter((data) => data.isExactMatch || data.isWildcardMatch || data.isUnpersonalizedMatch || serverData.isNull(data.groupId)); + if (serverData.isNull(preferredResultCount)) { + // There is no preferred number of results, so simply perform our final sort and then return + sortResult = sortAndDiversify(dataItemsWithoutFallback, serverSideAppIdsOrdering, diversify); + } + else if (dataItemsWithoutFallback.length >= preferredResultCount || !allowUnmatchedFallbackResults) { + // There is a preferred number of results, but we either have enough items without needing to utilise + // any fallback matches, or we don't allow fallback results. + sortResult = sortAndDiversify(dataItemsWithoutFallback, serverSideAppIdsOrdering, diversify); + sortResult.sortedDataItems = sortResult.sortedDataItems.slice(0, preferredResultCount); + } + else { + // There is a preferred number of results, and we need to use fallback matches in order to + // meet this number. We may still fall short, but this gets us as close as possible. + sortResult = sortAndDiversify(dataItems, serverSideAppIdsOrdering, diversify); + sortResult.sortedDataItems = sortResult.sortedDataItems.slice(0, preferredResultCount); + } + return sortResult; +} +/** + * Rearranges a list of dataItems, so that any where there is an exact segment match are moved to the front of the list. + * + * @param dataItems The data items to process. + * @param serverSideAppIdsOrdering List of ordered app ids from server side + * @param diversify An optional flag that determines if we should diverse the personalized results on the basis of server side apps ranking + * @returns The sorted list of data items. + */ +function sortAndDiversify(dataItems, serverSideAppIdsOrdering, diversify) { + const exactMatchDataItems = dataItems.filter((value) => value.isExactMatch); + let otherDataItems = dataItems.filter((value) => !value.isExactMatch); + if (serverData.isDefinedNonNull(diversify) && diversify) { + otherDataItems = diversifyDataItems(otherDataItems, serverSideAppIdsOrdering); + } + const sortedDataItems = exactMatchDataItems.concat(otherDataItems); + const orderWasNotChanged = dataItems.every((dataItem, index) => { + return dataItem === sortedDataItems[index]; + }); + return { + sortedDataItems: sortedDataItems, + processingType: orderWasNotChanged + ? 0 /* onDevicePersonalization.ProcessingType.contentsNotChanged */ + : 2 /* onDevicePersonalization.ProcessingType.contentsSorted */, + }; +} +/** + * Filters a list of raw data blobs into a list which only includes non-personalized data, or data that is set to "match all". + * + * @param dataItems The raw data blobs. + * @param preferredResultCount The preferred number of items to be included in the results. + * @returns The filtered set of data blobs. This will be a subset (or all) of the original dataItems. + */ +export function removePersonalizedDataItems(objectGraph, dataItems, preferredResultCount) { + let filteredDataItems = []; + const filledGroupIds = new Set(); + for (const data of dataItems) { + // If the personalization data is invalid or empty, we keep this in our result set. + const rawDataUserSegments = serverData.asString(data, "meta.personalizationData.segId"); + const appId = serverData.asString(data, "meta.personalizationData.appId"); + const groupId = serverData.asString(data, "meta.personalizationData.grpId"); + if (serverData.isNullOrEmpty(rawDataUserSegments) || + serverData.isNullOrEmpty(appId) || + serverData.isNullOrEmpty(groupId)) { + filteredDataItems.push(data); + continue; + } + // We already have a match for this group, so move onto the next item + if (filledGroupIds.has(groupId)) { + continue; + } + // If the data has a match all user segment, we keep this in our result set. + const dataUserSegments = rawDataUserSegments.split(","); + if (dataUserSegments.includes(alwaysMatchUserSegment)) { + filteredDataItems.push(data); + filledGroupIds.add(groupId); + } + } + // Finally, if we have a preferredResultCount which is smaller than our result set, trim our results down to this count + if (serverData.isDefinedNonNull(preferredResultCount) && filteredDataItems.length > preferredResultCount) { + filteredDataItems = filteredDataItems.slice(0, preferredResultCount); + } + return { + personalizedData: filteredDataItems, + processingType: null, + }; +} +//# sourceMappingURL=on-device-personalization-processing.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/personalization/on-device-personalization.js b/node_modules/@jet-app/app-store/tmp/src/common/personalization/on-device-personalization.js new file mode 100644 index 0000000..02003e9 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/personalization/on-device-personalization.js @@ -0,0 +1,134 @@ +import * as onDevicePersonalizationGroupingProcessing from "./on-device-personalization-grouping-processing"; +import * as onDevicePersonalizationProcessing from "./on-device-personalization-processing"; +import * as serverData from "../../foundation/json-parsing/server-data"; +/** + * Accepts an array of data blobs, and returns a subset of those original data blobs which is personalized to the user. + * In the case where personalization is disabled, or any personalized data blobs will instead be filtered out. + * + * @param placement Placement of the personalized items for on-device personalization + * @param dataItems The input list of data blobs. + * @param includeItemsWithNoPersonalizationData Whether to include data which no personalizationData meta is present. + * @param personalizationDataContainer The data container to use for personalizing the data. + * @param allowUnmatchedFallbackResults Whether to allow fallback results to be included in the results. This will only be utilised in order to reach a preferredResultCount. + * @param preferredResultCount The preferred number of items to be included in the results. + * @param parentAppId An optional appID, which is the parent for all the dataItems. Currently only used for search. + * @param diversify An optional flag that determines if we should diverse the personalized results on the basis of server side apps ranking + * @returns The relevant list of data blobs. + */ +export function personalizeDataItems(objectGraph, placement, dataItems, includeItemsWithNoPersonalizationData, personalizationDataContainer, allowUnmatchedFallbackResults = false, preferredResultCount, parentAppId, diversify) { + if (isPersonalizationAvailable(objectGraph)) { + switch (placement) { + case "groupingAppEvent": + return personalizeGroupingDataItems(objectGraph, dataItems, includeItemsWithNoPersonalizationData, personalizationDataContainer, diversify); + default: + return onDevicePersonalizationProcessing.personalizeDataItems(objectGraph, dataItems, personalizationDataContainer, includeItemsWithNoPersonalizationData, allowUnmatchedFallbackResults, preferredResultCount, parentAppId, diversify); + } + } + else { + return onDevicePersonalizationProcessing.removePersonalizedDataItems(objectGraph, dataItems, preferredResultCount); + } +} +/** + * Accepts an array of data blobs, and returns a subset of those original data blobs which is personalized to the user. + * In the case where personalization is disabled, or any personalized data blobs will instead be filtered out. + * + * @param dataItems The input list of data blobs. + * @param includeItemsWithNoPersonalizationData Whether to include data which no personalizationData meta is present. + * @param personalizationDataContainer The data container to use for personalizing the data. + * @param diversify An optional flag that determines if we should diverse the personalized results on the basis of server side apps ranking + * @returns The relevant list of data blobs. + */ +function personalizeGroupingDataItems(objectGraph, dataItems, includeItemsWithNoPersonalizationData, personalizationDataContainer, diversify) { + var _a, _b; + // We must filter out non app-event items in order to avoid moving their positions + // when contingent_offers_personalization is turned off + let appEventsOnlyDataItems = dataItems; + let wereItemsRemoved = false; + const nonAppEventIndexes = []; + if (!objectGraph.featureFlags.isEnabled("contingent_offers_personalization")) { + appEventsOnlyDataItems = dataItems.filter((item, index) => { + if (serverData.isDefinedNonNullNonEmpty(item.type) && item.type !== "app-events") { + nonAppEventIndexes.push(index); + return false; + } + return true; + }); + wereItemsRemoved = appEventsOnlyDataItems.length !== dataItems.length; + } + // We fetch the information regarding the segment optimizer flow from the personalization container + const personalizedMetricsData = personalizationDataContainer === null || personalizationDataContainer === void 0 ? void 0 : personalizationDataContainer.metricsData; + const useSegScores = (_a = personalizedMetricsData["use_segment_scores"]) !== null && _a !== void 0 ? _a : false; + const useOnDeviceSignals = (_b = personalizedMetricsData["use_signals"]) !== null && _b !== void 0 ? _b : false; + let personalizedResults; + if (useSegScores || useOnDeviceSignals) { + personalizedResults = onDevicePersonalizationGroupingProcessing.personalizeDataItems(objectGraph, appEventsOnlyDataItems, personalizationDataContainer, diversify); + } + else { + personalizedResults = onDevicePersonalizationProcessing.personalizeDataItems(objectGraph, appEventsOnlyDataItems, personalizationDataContainer, includeItemsWithNoPersonalizationData, null, null, null, diversify); + } + // We re-add non app-event items back into their original positions + if (wereItemsRemoved) { + const resultsArray = personalizedResults.personalizedData; + nonAppEventIndexes.forEach((index) => { + const item = dataItems[index]; + if (index < resultsArray.length) { + resultsArray.splice(index, 0, item); + } + else { + resultsArray.push(item); + } + }); + personalizedResults = { + personalizedData: resultsArray, + processingType: personalizedResults.processingType, + }; + } + return personalizedResults; +} +/** + * Convenience function for determining if data personalization is available. + */ +export function isPersonalizationAvailable(objectGraph) { + return (objectGraph.client.isiOS && + objectGraph.user.isOnDevicePersonalizationEnabled && + objectGraph.bag.enableOnDevicePersonalization); +} +/** + * Reaches down to the native client to return the current set of on device personalization data, + * restricted to a set of app IDs. + * + * @param appIds A set of appIds to restrict the personalization data to. + * @returns The relevant set of personalization data + */ +export function personalizationDataContainerForAppIds(objectGraph, appIds) { + if (!isPersonalizationAvailable(objectGraph)) { + return null; + } + if (objectGraph.host.platform === "iOS") { + return objectGraph.user.onDevicePersonalizationDataContainerForAppIds(Array.from(appIds)); + } + else { + return { + personalizationData: {}, + metricsData: null, + }; + } + return null; +} +/** + * Reaches down to the native client to return the current metrics data. + * + * @returns The current AMDClient metrics data + */ +export function metricsData(objectGraph) { + if (!isPersonalizationAvailable(objectGraph)) { + return null; + } + if (objectGraph.host.platform === "iOS") { + return objectGraph.user.onDevicePersonalizationDataContainerForAppIds([]).metricsData; + } + else { + return null; + } +} +//# sourceMappingURL=on-device-personalization.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/personalization/on-device-recommendations-common.js b/node_modules/@jet-app/app-store/tmp/src/common/personalization/on-device-recommendations-common.js new file mode 100644 index 0000000..c03b729 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/personalization/on-device-recommendations-common.js @@ -0,0 +1,190 @@ +import * as validation from "@jet/environment/json/validation"; +import * as serverData from "../../foundation/json-parsing/server-data"; +import * as mediaDataFetching from "../../foundation/media/data-fetching"; +import * as mediaNetwork from "../../foundation/media/network"; +import * as groupingShelfControllerCommon from "../grouping/shelf-controllers/grouping-shelf-controller-common"; +import { Parameters } from "../../foundation/network/url-constants"; +export class PersonalizedData { +} +export async function recommendedAppsForUseCase(objectGraph, useCase, displayContext) { + const displayContextLogString = displayContext === "shelf" ? "OnDeviceRecommendationsShelfController" : "OnDeviceRecommendationsPageController"; + return await new Promise((resolve, reject) => { + if (!objectGraph.host.isiOS) { + const errorMessage = `${displayContextLogString}: On device personalization is only enabled on iOS devices.`; + validation.unexpectedType("defaultValue", errorMessage, null); + reject(new Error(errorMessage)); + return; + } + if (serverData.isNullOrEmpty(objectGraph.user.dsid)) { + const errorMessage = `${displayContextLogString}: User is currently not signed in.`; + validation.unexpectedType("defaultValue", errorMessage, null); + reject(new Error(errorMessage)); + return; + } + if (serverData.isNullOrEmpty(useCase)) { + const errorMessage = `${displayContextLogString}: Missing valid useCase for ODP: ${useCase}`; + validation.unexpectedType("defaultValue", errorMessage, null); + reject(new Error(errorMessage)); + return; + } + objectGraph.onDeviceRecommendationsManager + .performRequest({ + type: "fetchRecommendations", + dsId: objectGraph.user.dsid, + useCase: useCase, + }) + .then((recoResponse) => { + const candidates = serverData.asArrayOrEmpty(recoResponse["candidates"]); + const recoMetrics = serverData.asJSONData(recoResponse["metrics"]); + if (serverData.isNullOrEmpty(candidates)) { + const errorMessage = `${displayContextLogString}: ODP returned no candidate ids for useCase: ${useCase}`; + validation.unexpectedType("defaultValue", errorMessage, null); + reject(new NoODPCandidatesError(errorMessage)); + return; + } + if (serverData.isNullOrEmpty(recoMetrics)) { + const errorMessage = `${displayContextLogString}: ODP returned no metrics for useCase: ${useCase}`; + validation.unexpectedType("defaultValue", errorMessage, null); + reject(new Error(errorMessage)); + return; + } + const candidatesData = []; + for (const candidateId of candidates) { + if (serverData.isDefinedNonNullNonEmpty(candidateId)) { + candidatesData.push({ + id: candidateId, + type: "apps", + }); + } + } + // MAPI Request + const mediaApiRequest = new mediaDataFetching.Request(objectGraph, candidatesData) + .withFilter("apps:recommendable", "true") + .addingQuery(Parameters.onDevicePersonalizationUseCase, useCase); + groupingShelfControllerCommon.prepareGroupingShelfRequest(objectGraph, mediaApiRequest); + mediaNetwork + .fetchData(objectGraph, mediaApiRequest) + .then((recoDataContainer) => { + resolve({ + candidates: candidates, + recoMetrics: recoMetrics, + dataContainer: recoDataContainer, + }); + }) + .catch((error) => { + const errorMessage = `${displayContextLogString}: Failed to fetch Media API data for candidates: ${candidates}`; + validation.unexpectedType("defaultValue", errorMessage, null); + reject(new Error(errorMessage)); + }); + }) + .catch((error) => { + const errorMessage = `${displayContextLogString}: Failed to perform ODP for useCase: ${useCase}, ${error}`; + validation.unexpectedType("defaultValue", errorMessage, null); + reject(new Error(errorMessage)); + }); + }); +} +/*** + * Gives the list of app ids in the order in which they appear in the passed response + * @param personalizedDataItems The data items to be processed + * @return List of ordered app ids + */ +export function getOrderedAppIds(personalizedDataItems) { + const seenAppIds = new Set(); + const orderedAppIds = []; + personalizedDataItems.forEach((dataItem, index) => { + if (!seenAppIds.has(dataItem.appId)) { + orderedAppIds.push(dataItem.appId); + seenAppIds.add(dataItem.appId); + } + }); + return orderedAppIds; +} +/*** + * This function reranks events based on a calculated on-device score + * that can factor in recency, frequency, or usage of an app + * @param personalizedDataItems List of personalized data items + * @param metricsData Metrics data with hyperparameter values to use + * @return Personalized data items after reranking using on-device signals + */ +export function getUpdatedScoreAfterBoosting(personalizedDataItems, metricsData) { + var _a; + const weightParam = Number((_a = metricsData["weight_parameter"]) !== null && _a !== void 0 ? _a : 0.0); + for (const personalizedDataItem of personalizedDataItems) { + const serverScore = personalizedDataItem.score; + const onDeviceScore = personalizedDataItem.onDeviceScore; + personalizedDataItem.modifiedScore = weightParam * onDeviceScore + (1 - weightParam) * serverScore; + } + personalizedDataItems.sort((a, b) => { + return b.modifiedScore - a.modifiedScore; + }); + return personalizedDataItems; +} +/*** + * This function will diversify the personalized items on the basis of ordering of apps passed. + * In case there are pinned items, we will remove them from diversification bucket and keep them in their position and add diversified items in order + * This can lead to duplicates next to the pinned items. But the pinning and duplicate app events case scenario is rare so we avoid complicated logic here. + * Explanation -> + * If the original data items list contains appId in this order -> [1(a), 1(b), 2(a), 3(a), 2(b), 2(c), 3(b), 4(a), 1(c), 4(b)] + * and the ordering of the apps used for relative ranking is -> [1, 2, 3, 4] + * Then the updated ordering of the data items should look like -> [1(a), 2(a), 3(a), 4(a), 1(b), 2(b), 3(b), 4(b), 1(c), 2(c)] + * @param personalizedDataItems List of personalized data items for diversification + * @param orderedAppIds List of ordered app ids used for relative ordering between in-app events + * @return The diversified list of data items. + */ +export function diversifyDataItems(personalizedDataItems, orderedAppIds) { + const pinnedItems = personalizedDataItems.filter((item) => serverData.isDefinedNonNull(item.pinnedPosition)); + personalizedDataItems = personalizedDataItems.filter((item) => serverData.isNull(item.pinnedPosition)); + const appIdGroups = new Map(); + // Insert the elements in a map in reverse order of their appearance in dataItems so that we can later use pop() instead of shift() + // 1 -> [1(c), 1(b), 1(a)], 2 -> [2(c), 2(b), 2(a)], 3 -> [3(b), 3(a)], 4-> [4(b), 4(a)] + personalizedDataItems.reverse(); + personalizedDataItems.forEach((dataItem, index) => { + if (dataItem.appId in appIdGroups) { + appIdGroups[dataItem.appId].push(dataItem); + } + else { + appIdGroups[dataItem.appId] = [dataItem]; + } + }); + const diversifiedDataItems = []; + // We find the max number of appItems for our appIds, to determine the iteration count + const maxAppEventsForAppId = Math.max(...Object.values(appIdGroups).map((a) => a.length)); + for (let index = 0; index < maxAppEventsForAppId; index++) { + const reducedServerSideAppIdsOrdering = []; + orderedAppIds.forEach((appId) => { + if (appId in appIdGroups && appIdGroups[appId].length > 0) { + diversifiedDataItems.push(appIdGroups[appId].pop()); + if (appIdGroups[appId].length !== 0) { + reducedServerSideAppIdsOrdering.push(appId); + } + } + }); + orderedAppIds = reducedServerSideAppIdsOrdering; + } + // Merge the pinned items and diversified items + const finalizedResponse = new Array(pinnedItems.length + diversifiedDataItems.length); + // Sort pinned items by pinned position to be able to handle the pinned items that exceed the final list length + // For all the pinned items that have position greater than input list size, we just add them to end of the list + pinnedItems.sort((a, b) => a.pinnedPosition - b.pinnedPosition); + for (const dataItem of pinnedItems) { + if (dataItem.pinnedPosition < finalizedResponse.length) { + // Possible edgecase - Sanity check in case filtering reduces the length of items + finalizedResponse[dataItem.pinnedPosition] = dataItem; + } + else { + // Extremely rare case scenario: Push these items to diversifiedDataItemsList + diversifiedDataItems.push(dataItem); + } + } + diversifiedDataItems.reverse(); // To allow popping for first element fetch + for (const [index, finalizedItem] of finalizedResponse.entries()) { + if (serverData.isNull(finalizedItem) && diversifiedDataItems.length) { + finalizedResponse[index] = diversifiedDataItems.pop(); + } + } + return finalizedResponse; +} +export class NoODPCandidatesError extends Error { +} +//# sourceMappingURL=on-device-recommendations-common.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/personalization/on-device-recommendations-today.js b/node_modules/@jet-app/app-store/tmp/src/common/personalization/on-device-recommendations-today.js new file mode 100644 index 0000000..df2fa73 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/personalization/on-device-recommendations-today.js @@ -0,0 +1,329 @@ +import { isNothing, isSome } from "@jet/environment"; +import * as validation from "@jet/environment/json/validation"; +import * as serverData from "../../foundation/json-parsing/server-data"; +import * as mediaDataFetching from "../../foundation/media/data-fetching"; +import * as mediaNetwork from "../../foundation/media/network"; +import { Parameters } from "../../foundation/network/url-constants"; +import { allOptional } from "../../foundation/util/promise-util"; +import * as groupingShelfControllerCommon from "../grouping/shelf-controllers/grouping-shelf-controller-common"; +import * as lottery from "../util/lottery"; +import { startPromiseWithAdditionalTimeout } from "../util/timeout-manager-util"; +import { isPersonalizationAvailable } from "./on-device-personalization"; +export const todayTabODPTimeoutUseCase = "todayTabPersonalization"; +const displayContextLogString = "OnDeviceRecommendationsTodayShelfController"; +/** + * Convenience function for determining if Today tab Arcade personalization is available. + */ +export function isTodayTabArcadePersonalizationAvailable(objectGraph) { + const isDataPersonalizationAvailable = isPersonalizationAvailable(objectGraph); + const isiOS = objectGraph.client.isiOS; + const isFeatureEnabledForCurrentUser = lottery.isFeatureEnabledForCurrentUser(objectGraph, objectGraph.bag.todayTabArcadePersonalizationRate); + // Personalization is enabled only IF + // - Data personalization is available AND + // - Client is iOS AND + // - Feature is enabled for current user + return isDataPersonalizationAvailable && isiOS && isFeatureEnabledForCurrentUser; +} +/** + * Fetches and returns Today recommendations result using on-device recommendations manager with timeout. + * @param timeout: Optional timeout in seconds. + * @returns Promise<Opt<TodayRecommendationsResult>>: Today recommendations result. + */ +export async function fetchTodayRecommendationsWithTimeout(objectGraph, timeout) { + return await startPromiseWithAdditionalTimeout(objectGraph, fetchTodayRecommendations(objectGraph), timeout, todayTabODPTimeoutUseCase); +} +/** + * Fetches and returns Today recommendations result using on-device recommendations manager. + * @returns Promise<Opt<TodayRecommendationsResult>>: Today recommendations result. + */ +async function fetchTodayRecommendations(objectGraph) { + try { + const useCases = await fetchUseCasesForTab("today", objectGraph); + const recommendationPromises = useCases.map(async (useCase) => await fetchTodayRecommendationForUseCase(objectGraph, useCase)); + const recommendationPromiseResults = await allOptional(recommendationPromises); + const recommendations = recommendationPromiseResults + .map((promiseResult) => { + if (promiseResult.success) { + return promiseResult.value; + } + else { + return undefined; + } + }) + .filter(isSome); + return new TodayRecommendationsResult(recommendations); + } + catch (error) { + objectGraph.console.log(`${displayContextLogString}: Failed to perform ODP for Today recommendations: ${error}`); + return undefined; + } +} +/** + * Fetches Today Recommendation for a use case using on-device recommendations manager. + * - useCase: On-device personalization use case. + * @returns Promise<TodayRecommendation | undefined>: Today recommendation. + */ +async function fetchTodayRecommendationForUseCase(objectGraph, useCase) { + try { + const odrResponse = await fetchTodayRecommendation(useCase, objectGraph); + const recommendedCandidatesAndMetrics = await makeTodayRecommendedCandidatesAndMetrics(useCase, odrResponse); + const recoMetrics = recommendedCandidatesAndMetrics.metrics; + const recommendedCandidates = recommendedCandidatesAndMetrics.candidates; + if (recommendedCandidates.length === 0) { + return undefined; + } + // Select the first candidate as we support only one candidate for each use case. + const recommendedCandidate = recommendedCandidates[0]; + const todayRecommendationPromise = await makeTodayRecommendation(useCase, recommendedCandidate, recoMetrics, objectGraph); + return todayRecommendationPromise; + } + catch (error) { + objectGraph.console.log(`${displayContextLogString}: Failed to perform ODP Today recommendation for useCase: ${useCase}, with error: ${error}`); + return undefined; + } +} +/** + * Fetches and returns use cases for given tab using on-device recommendations manager. + * - tab: Navigation tab. + * @returns Promise<string[]>: Use cases for given tab. + */ +export async function fetchUseCasesForTab(tab, objectGraph) { + if (serverData.isNullOrEmpty(objectGraph.user.dsid)) { + const errorMessage = `${displayContextLogString}: User is currently not signed in.`; + validation.unexpectedType("defaultValue", errorMessage, null); + throw new Error(errorMessage); + } + try { + const odrResponse = await objectGraph.onDeviceRecommendationsManager.performRequest({ + type: "fetchUseCases", + tab: tab, + dsId: objectGraph.user.dsid, + }); + const useCases = serverData.asArrayOrEmpty(odrResponse["useCases"]); + if (serverData.isNullOrEmpty(useCases)) { + const errorMessage = `${displayContextLogString}: ODP returned no use cases for tab: ${tab}`; + validation.unexpectedType("defaultValue", errorMessage, null); + throw new Error(errorMessage); + } + return useCases; + } + catch (error) { + const errorMessage = `${displayContextLogString}: Failed to fetch ODP use cases for tab: ${tab}, with error: ${error}`; + validation.unexpectedType("defaultValue", errorMessage, null); + throw new Error(errorMessage); + } +} +/** + * Fetches and returns Today recommendation response for given use case using on-device recommendations manager. + * - useCase: Use case. + * @returns Promise<JSONData>: Today recommendation response. + */ +export async function fetchTodayRecommendation(useCase, objectGraph) { + if (serverData.isNullOrEmpty(objectGraph.user.dsid)) { + const errorMessage = `${displayContextLogString}: User is currently not signed in.`; + throw new Error(errorMessage); + } + try { + return await objectGraph.onDeviceRecommendationsManager.performRequest({ + type: "fetchRecommendations", + dsId: objectGraph.user.dsid, + useCase: useCase, + }); + } + catch (error) { + const errorMessage = `${displayContextLogString}: Failed to perform ODP Today recommendation for useCase: ${useCase}, with error: ${error}`; + validation.unexpectedType("defaultValue", errorMessage, null); + throw new Error(errorMessage); + } +} +/** + * Makes and returns Today recommended candidates and metrics from given useCase and on-device recommendation response. + * - useCase: Use case. + * - odrResponse: On-device recommendation response. + * @returns { candidates: TodayRecommendedCandidate[]; metrics: JSONData }: Today recommended candidates and reco metrics. + */ +export async function makeTodayRecommendedCandidatesAndMetrics(useCase, odrResponse) { + const recoCandidates = serverData.asArrayOrEmpty(odrResponse["candidates"]); + if (serverData.isNullOrEmpty(recoCandidates)) { + const errorMessage = `${displayContextLogString}: ODP returned no candidates for useCase: ${useCase}`; + validation.unexpectedType("defaultValue", errorMessage, null); + throw new Error(errorMessage); + } + const recoMetrics = serverData.asJSONData(odrResponse["metrics"]); + const recommendedCandidates = recoCandidates.map((candidate) => makeRecommendedCandidate(candidate)).filter(isSome); + if (serverData.isNull(recoMetrics) || serverData.isNullOrEmpty(recommendedCandidates)) { + const errorMessage = `${displayContextLogString}: ODP candidates could not be parsed for useCase: ${useCase}`; + validation.unexpectedType("defaultValue", errorMessage, null); + throw new Error(errorMessage); + } + return { candidates: recommendedCandidates, metrics: recoMetrics }; +} +/** + * Makes and returns Today recommendation for given use case and candidates using MAPI response. + * - useCase: Use case. + * - recommendedCandidate: Recommended candidate. + * - recoMetrics: Reco metrics. + * @returns Promise<TodayRecommendation>: Today recommendation. + */ +export async function makeTodayRecommendation(useCase, recommendedCandidate, recoMetrics, objectGraph) { + const mediaApiRequest = new mediaDataFetching.Request(objectGraph, recommendedCandidate.data, true) + .addingQuery(Parameters.onDevicePersonalizationUseCase, useCase) + .addingQuery(Parameters.filterRecommendable, "true"); + groupingShelfControllerCommon.prepareGroupingShelfRequest(objectGraph, mediaApiRequest); + try { + const mapiResponse = await mediaNetwork.fetchData(objectGraph, mediaApiRequest); + return new TodayRecommendation(useCase, [recommendedCandidate], recoMetrics, mapiResponse); + } + catch (error) { + const errorMessage = `${displayContextLogString}: Failed to fetch Media API data for: ${recommendedCandidate.data}, with error: ${error}`; + validation.unexpectedType("defaultValue", errorMessage, null); + throw new Error(errorMessage); + } +} +/** + * Makes and returns Today recommended candidate using given candidate data. + * - candidate: Candidate data. + * @returns TodayRecommendedCandidate: Today recommended candidate. + */ +export function makeRecommendedCandidate(candidate) { + // Extract candidate ID and type from candidate data. + const candidateID = serverData.asString(candidate.id); + const candidateType = serverData.asString(candidate.type); + if (serverData.isNull(candidateID)) { + return undefined; + } + let storyIDs = []; + let mediaType; + switch (candidateType) { + case "editorialItemGroup": + // Exract story candidates to use within the story group. + const storyCandidates = serverData.asArrayOrEmpty(candidate.candidates); + // Create story IDs from story candidates. + storyIDs = storyCandidates + .map((storyCandidate) => serverData.asString(storyCandidate.id)) + .filter((storyID) => isSome(storyID)); + mediaType = "editorial-item-groups"; + break; + case "editorialItem": + mediaType = "editorial-items"; + break; + default: + return undefined; + } + let candidatesData = []; + candidatesData.push({ + id: candidateID, + type: mediaType, + }); + // If there are any story IDs, add story candidates to candidates data. + if (serverData.isDefinedNonNullNonEmpty(storyIDs)) { + const storyCandidates = storyIDs.map((storyID) => ({ + id: storyID, + type: "editorial-items", + })); + candidatesData = candidatesData.concat(storyCandidates); + } + return new TodayRecommendedCandidate(candidateID, mediaType, storyIDs, candidatesData); +} +/** + * A container for Today recommendations that also makes story data and story group data. + */ +export class TodayRecommendationsResult { + /** + * Initializes today recommendations results. + * @param recommendations - Today recommendations. + */ + constructor(recommendations) { + this.recommendations = recommendations; + } + /** + * Returns story data from recommendations that has the given use case. + * @param useCase - Use case that is used to match a story. + * @returns Story data for given use case and type, or null if not found. + */ + storyData(useCase) { + var _a; + const recommendation = this.recommendationForUseCase(useCase); + const candidate = recommendation === null || recommendation === void 0 ? void 0 : recommendation.candidate("editorial-items"); + if (isNothing(recommendation) || isNothing(candidate)) { + return undefined; + } + return (_a = recommendation === null || recommendation === void 0 ? void 0 : recommendation.dataContainer) === null || _a === void 0 ? void 0 : _a.data.find((item) => item.id === candidate.id); + } + /** + * Returns story group data from recommendations that has the given use case. + * @param useCase - Use case that is used to match a story group. + * @returns Story group data for given use case and type, or null if not found. + */ + storyGroupData(useCase) { + var _a, _b; + const recommendation = this.recommendationForUseCase(useCase); + const candidate = recommendation === null || recommendation === void 0 ? void 0 : recommendation.candidate("editorial-item-groups"); + if (isNothing(recommendation) || isNothing(candidate)) { + return undefined; + } + const storyGroupData = (_a = recommendation === null || recommendation === void 0 ? void 0 : recommendation.dataContainer) === null || _a === void 0 ? void 0 : _a.data.find((item) => item.id === (candidate === null || candidate === void 0 ? void 0 : candidate.id)); + const storiesData = (_b = recommendation === null || recommendation === void 0 ? void 0 : recommendation.dataContainer) === null || _b === void 0 ? void 0 : _b.data.filter((item) => candidate.candidateIDs.includes(item.id)); + if (isNothing(storyGroupData) || isNothing(storiesData)) { + return undefined; + } + storyGroupData["meta"] = { + associations: { + recommendations: { + data: storiesData, + }, + }, + }; + return storyGroupData; + } + /** + * Returns the first recommendation that has the given use case. + * @param useCase - Use case that is used to match a recommendation. + * @returns Recommendation for given use case or null if not found. + */ + recommendationForUseCase(useCase) { + return this.recommendations.find((recommendation) => recommendation.useCase === useCase); + } +} +/** + * Today recommendation with a use case, candidates, metrics and a data container. + */ +export class TodayRecommendation { + /** + * Initializes today recommendation. + * @param useCase - Use case that is used to match recommendations. + * @param candidates - Recommended candidates to use as replacements. + * @param recoMetrics - Metrics for given candidates. + * @param dataContainer - Data returned from MAPI for recommended candidates. + */ + constructor(useCase, candidates, recoMetrics, dataContainer) { + this.useCase = useCase; + this.candidates = candidates; + this.recoMetrics = recoMetrics; + this.dataContainer = dataContainer; + } + /// Returns the first candidate that has the given type. + candidate(type) { + return this.candidates.find((candidate) => candidate.type === type); + } +} +/** + * Today recommended candidate with its id, type, data + * and candidate IDs (only if it is a candidate for a story group). + */ +export class TodayRecommendedCandidate { + /** + * Initializes today recommended candidate. + * @param id - Candidate id i.e. story ID or story group ID. + * @param type - Candidate type i.e. "editorial-items" or "editorial-item-groups". + * @param candidateIDs - Candidate IDs. + * @param data - Data to use while fetching from MAPI. + */ + constructor(id, type, candidateIDs, data) { + this.id = id; + this.type = type; + this.candidateIDs = candidateIDs; + this.data = data; + } +} +//# sourceMappingURL=on-device-recommendations-today.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/placeholders/placeholders.js b/node_modules/@jet-app/app-store/tmp/src/common/placeholders/placeholders.js new file mode 100644 index 0000000..8110a4a --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/placeholders/placeholders.js @@ -0,0 +1,229 @@ +import * as models from "../../api/models"; +import * as serverData from "../../foundation/json-parsing/server-data"; +import * as types from "../grouping/grouping-types"; +import { isSome } from "@jet/environment/types/optional"; +/** + * Determines if placeholders are enabled for the current device + * @param objectGraph Current object graph + * @returns True if the current device supports placeholders + */ +export function placeholdersEnabled(objectGraph) { + return objectGraph.client.isiOS || objectGraph.client.isVision; +} +/** + * Whether a given shelf content type supports placeholders + * @param objectGraph Current object graph + * @param contentType Content type for the shelf + * @returns True if the content type supports placeholders + */ +function contentTypeSupportsPlaceholders(objectGraph, contentType) { + if (preprocessor.GAMES_TARGET) { + switch (contentType) { + case "smallLockup": + case "mediumLockup": + case "mediumImageLockup": + case "largeImageLockup": + case "smallStoryCard": + case "mediumStoryCard": + case "largeStoryCard": + case "miniTodayCard": + return true; + default: + return false; + } + } + if (objectGraph.client.isVision) { + switch (contentType) { + case "smallBrick": + case "brick": + case "largeBrick": + case "smallLockup": + case "mediumLockup": + case "largeLockup": + case "smallVerticalLockup": + case "mediumVerticalLockup": + case "posterLockup": + case "action": + case "smallImageLockup": + case "mediumImageLockup": + case "largeImageLockup": + case "smallStoryCard": + case "mediumStoryCard": + case "largeHeroBreakout": + return true; + default: + return false; + } + } + else { + switch (contentType) { + case "smallLockup": + case "mediumLockup": + case "largeLockup": + case "brick": + case "categoryBrick": + case "videoCard": + case "posterLockup": + case "appTrailerLockup": + case "screenshotsLockup": + case "appPromotion": + case "appEvent": + case "smallStoryCard": + case "tagBrick": + case "miniTodayCard": + return true; + default: + return false; + } + } +} +/** + * The number of placeholders to use, if the hydration count is unknown + * @param objectGraph Current object graph + * @param itemCount The number of items + * @param featuredContentID An optional FCID + * @returns The number of placeholds to use + */ +function placeholderCountForItemCount(objectGraph, itemCount, featuredContentID) { + if (itemCount > 0) { + return itemCount; + } + // In some cases mediaAPI can return is a shelf with no items. This is valid for personalised shelves but not for + // editorially controlled shelves and something is probably wrong with the configuration of that shelf shelf. + // In that case we return 0 placeholders so the shelf doesn't get modified (and placeholders don't get inserted), staying hidden from the user + if (isSome(featuredContentID) && !types.isRecommendationsShelf(featuredContentID)) { + return 0; + } + if (objectGraph.client.isPad || objectGraph.client.isVision) { + return 15; + } + else { + return 6; + } +} +/** + * Inserts placeholder items into the given product page shelf + * @param objectGraph Current object graph + * @param shelf The input shelf + * @param token The current shelf token + * @param featuredContentId An optional FCID + */ +export function insertPlaceholdersIntoProductPageShelfIfRequired(objectGraph, shelf, token) { + if (objectGraph.client.isiOS && + objectGraph.bag.isOnDemandShelfFetchingEnabled && + placeholdersEnabled(objectGraph) && + isSome(shelf) && + serverData.isNullOrEmpty(shelf.items) && + token.isFirstRender) { + token.showingPlaceholders = insertPlaceholdersIntoShelf(objectGraph, shelf, token.remainingItems.length); + } +} +/** + * Inserts placeholder items into the given shelf, if we pass some pre-flight checks + * @param objectGraph Current object graph + * @param shelf The input shelf + * @param shelfToken The current shelf token + * @param featuredContentId An optional FCID + */ +export function insertPlaceholdersIntoShelfIfRequired(objectGraph, shelf, shelfToken, displayLimit = undefined, featuredContentId) { + if (placeholdersEnabled(objectGraph) && + isSome(shelf) && + isSome(shelf.url) && + serverData.isNullOrEmpty(shelf.items) && + shelfToken.isFirstRender) { + shelfToken.showingPlaceholders = insertPlaceholdersIntoShelf(objectGraph, shelf, displayLimit !== null && displayLimit !== void 0 ? displayLimit : shelfToken.remainingItems.length, shelfToken.isSearchLandingPage, shelfToken.isArcadePage, featuredContentId); + } +} +/** + * Inserts placeholder items into the given generic page shelf + * @param objectGraph Current object graph + * @param shelf The input shelf + * @param token The current shelf token + * @param featuredContentId An optional FCID + */ +export function insertPlaceholdersIntoGenericPageShelf(objectGraph, shelf, token, featuredContentId) { + token.showingPlaceholders = insertPlaceholdersIntoShelf(objectGraph, shelf, token.remainingItems.length, token.isSearchLandingPage, token.isArcadePage, featuredContentId); +} +/** + * @param objectGraph The current object graph + * @param shelf The shelf to insert placeholders into + * @param remainaingItemCount The number of items remaining to load + * @param isSearchLandingPage Whether the current page is a search landing page + * @param isArcadePage Whether the current page is an arcade page + * @param featuredContentId The optional FCID + * @returns Whether placeholders were inserted + */ +function insertPlaceholdersIntoShelf(objectGraph, shelf, remainaingItemCount, isSearchLandingPage = false, isArcadePage = false, featuredContentId = null) { + // Return if the shelf already has items do not insert placeholders + if (serverData.isDefinedNonNullNonEmpty(shelf.items)) { + return false; + } + // Return if not a supported placeholder type or grouping is search + if (!contentTypeSupportsPlaceholders(objectGraph, shelf.contentType) || isSearchLandingPage) { + return false; + } + const placeholderCount = placeholderCountForItemCount(objectGraph, remainaingItemCount, featuredContentId); + // Generate placeholder items + const placeholderItems = new Array(placeholderCount); + for (let i = 0; i < placeholderCount; i += 1) { + placeholderItems[i] = new models.Placeholder(); + } + if (serverData.isNullOrEmpty(placeholderItems)) { + // If we didn't generate any items then don't modify the shelf and just return + return false; + } + const rowsPerColumn = rowsPerColumnForShelf(objectGraph, shelf); + if (isSome(rowsPerColumn)) { + shelf.rowsPerColumn = rowsPerColumn; + } + // Swap the real content type to the placeholder type and assign the items + shelf.placeholderContentType = shelf.contentType; + shelf.contentType = "placeholder"; + shelf.items = placeholderItems; + if (serverData.isDefinedNonNullNonEmpty(isArcadePage)) { + shelf.presentationHints = { ...shelf.presentationHints, isAppleArcadeContext: isArcadePage }; + } + // If we're showing placeholders we never want the shelf to be hidden + shelf.isHidden = false; + return true; +} +export function isPlaceholderShelf(shelf) { + return shelf.contentType === "placeholder"; +} +/** + * The preferred number of rows to use per column, for a given shelf + * @param objectGraph Current object graph + * @param shelf The shelf we're calculating for + * @returns The number of rows to use + */ +function rowsPerColumnForShelf(objectGraph, shelf) { + if (objectGraph.client.isVision) { + switch (shelf.contentType) { + case "smallLockup": + return 3; + case "mediumLockup": + return 2; + default: + return 1; + } + } + else if (isSome(shelf.rowsPerColumn)) { + return shelf.rowsPerColumn; + } + else { + switch (shelf.contentType) { + case "smallLockup": + return 3; + case "mediumLockup": + return 2; + case "largeLockup": + return 1; + case "categoryBrick": + // Do not default for Category Brick + return null; + default: + return 1; + } + } +} +//# sourceMappingURL=placeholders.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/preview-platform.js b/node_modules/@jet-app/app-store/tmp/src/common/preview-platform.js new file mode 100644 index 0000000..bba4d5e --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/preview-platform.js @@ -0,0 +1,87 @@ +import { unwrapOptional as unwrap } from "@jet/environment/types/optional"; +import { attributeAsArrayOrEmpty } from "../foundation/media/attributes"; +import { dataFromDataContainer } from "../foundation/media/data-structure"; +import { deviceFamiliesFromData } from "./content/device-family"; +/** + * Retreive the current `PreviewPlatform`, in the format used to create a {@linkcode WithPlatform} `Intent` + * + * @example + * makeIntentWithPlatform({ + * ...getPlatform(objectGraph) + * ...otherProps + * }); + */ +export function getPlatform(objectGraph) { + var _a; + return { + platform: (_a = objectGraph.activeIntent) === null || _a === void 0 ? void 0 : _a.previewPlatform, + }; +} +/** + * Determine the most relevant {@linkcode PreviewPlatform} from a Media API response that + * contains "device families", like an `app` or `app-bundle` + */ +export function inferPreviewPlatformFromDeviceFamilies(objectGraph, response) { + const data = unwrap(dataFromDataContainer(objectGraph, response)); + const deviceFamilies = new Set(deviceFamiliesFromData(objectGraph, data, true)); + if (deviceFamilies.has("iphone")) { + return "iphone"; + } + if (deviceFamilies.has("ipad")) { + return "ipad"; + } + if (deviceFamilies.has("mac")) { + return "mac"; + } + if (deviceFamilies.has("realityDevice")) { + return "vision"; + } + if (deviceFamilies.has("tvos")) { + return "tv"; + } + if (deviceFamilies.has("watch")) { + return "watch"; + } + throw new Error("Could not infer platform from device families"); +} +/** + * Determine the most relevant {@linkcode PreviewPlatform} from a Media API response that + * contains "editorial platforms"", like `editorial-items` + */ +export function inferPreviewPlatformFromEditorialPlatforms(objectGraph, container) { + const data = unwrap(dataFromDataContainer(objectGraph, container)); + const editorialPlatforms = new Set(attributeAsArrayOrEmpty(data, "editorialPlatforms")); + if (editorialPlatforms.has("iphone")) { + return "iphone"; + } + if (editorialPlatforms.has("ipad")) { + return "ipad"; + } + if (editorialPlatforms.has("desktop")) { + return "mac"; + } + if (editorialPlatforms.has("realitydevice")) { + return "vision"; + } + if (editorialPlatforms.has("watch")) { + return "watch"; + } + if (editorialPlatforms.has("appletv")) { + return "tv"; + } + throw new Error("Could not infer preview platform from editorial platforms"); +} +/** + * Set the `previewPlatform` query param on the {@linkcode request}, if necessary + * + * This should only be used with requests being made for the following resources: + * - "Editorial" (Grouping Page, Editorial Page, Today, etc) + * - "Search" + */ +export function setPreviewPlatform(objectGraph, request) { + var _a; + return ((_a = objectGraph.activeIntent) === null || _a === void 0 ? void 0 : _a.previewPlatform) + ? request.addingQuery("previewPlatform", objectGraph.activeIntent.platform).addingQuery("platform", "web") + : request; +} +//# sourceMappingURL=preview-platform.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/privacy/privacy-footer-shelf.js b/node_modules/@jet-app/app-store/tmp/src/common/privacy/privacy-footer-shelf.js new file mode 100644 index 0000000..809b042 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/privacy/privacy-footer-shelf.js @@ -0,0 +1,87 @@ +import * as validation from "@jet/environment/json/validation"; +import * as models from "../../api/models"; +import * as serverData from "../../foundation/json-parsing/server-data"; +import * as privacyHeaderShelf from "./privacy-header-shelf"; +/** + * Builder for the privacy footer shelf. + */ +export function create(objectGraph, data, pageInformation, locationTracker) { + return validation.context("create", () => { + if (serverData.isNullOrEmpty(data)) { + return null; + } + if (objectGraph.client.isWatch) { + return null; + } + const privacyTypes = privacyHeaderShelf.privacyTypesFromData(objectGraph, data, false, "detailPage", pageInformation, locationTracker); + const privacyDataNotProvided = (privacyTypes.length === 1 && privacyTypes[0].identifier === "DATA_NOT_PROVIDED") || + privacyTypes.length === 0; + if (privacyDataNotProvided && objectGraph.client.deviceType !== "tv") { + return null; + } + const shelf = new models.Shelf("privacyFooter"); + const privacyFooter = privacyFooterFromData(objectGraph, data, pageInformation, locationTracker); + shelf.items = [privacyFooter]; + return shelf; + }); +} +/** + * Creates a privacy footer object. + * @param data The data blob + */ +export function privacyFooterFromData(objectGraph, data, pageInformation, locationTracker) { + return validation.context("privacyFooterFromData", () => { + const bodyText = bodyTextFromData(objectGraph, data, pageInformation, locationTracker); + const actions = actionsFromData(objectGraph, data, pageInformation, locationTracker); + let privacyTypes = []; + if (objectGraph.client.isTV) { + privacyTypes = privacyHeaderShelf.privacyTypesFromData(objectGraph, data, false, "productPage", pageInformation, locationTracker); + } + return new models.PrivacyFooter(bodyText, actions, privacyTypes.length); + }); +} +/** + * Creates the main body text for the footer. + * @param data The data blob + */ +function bodyTextFromData(objectGraph, data, pageInformation, locationTracker) { + let text; + const learnMoreLinkText = objectGraph.loc.string("PRODUCT_PRIVACY_LEARN_MORE_LINK"); + const linkedSubstrings = {}; + const learnMoreAction = privacyHeaderShelf.createLearnMoreAction(objectGraph, pageInformation, locationTracker); + if (objectGraph.client.isTV || serverData.isNull(learnMoreAction)) { + text = objectGraph.loc.string("PRODUCT_PRIVACY_LEARN_MORE_NO_LINK"); + } + else { + text = objectGraph.loc.string("PRODUCT_PRIVACY_LEARN_MORE_TEMPLATE"); + text = text.replace("{learnMoreLink}", learnMoreLinkText); + if (serverData.isNull(learnMoreAction)) { + text = objectGraph.loc.string("PRODUCT_PRIVACY_LEARN_MORE_NO_LINK"); + } + else { + linkedSubstrings[learnMoreLinkText] = learnMoreAction; + } + } + const textType = "text/plain"; + const styledText = new models.StyledText(text, textType); + return new models.LinkableText(styledText, linkedSubstrings); +} +/** + * Creates the actions for displaying in the privacy footer. + */ +function actionsFromData(objectGraph, data, pageInformation, locationTracker) { + if (objectGraph.client.deviceType !== "tv") { + return []; + } + const actions = []; + const learnMoreAction = privacyHeaderShelf.createLearnMoreAction(objectGraph, pageInformation, locationTracker); + if (serverData.isDefinedNonNull(learnMoreAction)) { + actions.push(learnMoreAction); + } + const privacyDefinitionsAction = privacyHeaderShelf.createPrivacyDefinitionsAction(objectGraph, pageInformation, locationTracker); + if (serverData.isDefinedNonNull(privacyDefinitionsAction)) { + actions.push(privacyDefinitionsAction); + } + return actions; +} +//# sourceMappingURL=privacy-footer-shelf.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/privacy/privacy-header-shelf.js b/node_modules/@jet-app/app-store/tmp/src/common/privacy/privacy-header-shelf.js new file mode 100644 index 0000000..b911d94 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/privacy/privacy-header-shelf.js @@ -0,0 +1,427 @@ +import * as validation from "@jet/environment/json/validation"; +import { isNothing } 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 mediaRelationships from "../../foundation/media/relationships"; +import { Path, Protocol } from "../../foundation/network/url-constants"; +import * as urls from "../../foundation/network/urls"; +import * as constants from "../../foundation/util/constants"; +import * as contentAttributes from "../content/attributes"; +import * as metricsHelpersClicks from "../metrics/helpers/clicks"; +import * as linksShelf from "../product-page/shelves/links-shelf"; +import * as privacyTypesShelf from "./privacy-types-shelf"; +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"; +/** + * Builder for the privacy header shelf. This shelf is currently used + * on both the product page, and the privacy detail page. + */ +export function create(objectGraph, data, pageInformation, locationTracker) { + return validation.context("privacyShelf", () => { + if (serverData.isNullOrEmpty(data)) { + return null; + } + const shelf = new models.Shelf("privacyHeader"); + shelf.title = objectGraph.loc.string("PRODUCT_PRIVACY_TITLE"); + const privacyHeader = privacyHeaderFromData(objectGraph, data, false, false, pageInformation, locationTracker); + shelf.items = [privacyHeader]; + if (objectGraph.client.deviceType !== "watch" && objectGraph.client.deviceType !== "tv") { + shelf.seeAllAction = privacyDetailActionFromData(objectGraph, data, "detailPage", pageInformation, locationTracker, null); + } + return shelf; + }); +} +/** + * Creates a privacy header object. + * @param data The data blob + * @param isDetailHeader Whether this header is intended to be displayed on the detail page + * @param isDetailData Whether the included data is for the detail page + */ +export function privacyHeaderFromData(objectGraph, data, isDetailHeader, isDetailData, pageInformation, locationTracker) { + return validation.context("createPrivacyHeaderFromData", () => { + const bodyText = bodyTextFromData(objectGraph, data, isDetailHeader, isDetailData, pageInformation, locationTracker); + let seeDetailsAction; + let privacyPolicyAction; + const privacyDefinitionsText = privacyDefinitionsTextFromData(objectGraph, data, isDetailHeader, pageInformation, locationTracker); + let privacyDefinitionsAction; + let learnMoreText; + let learnMoreAction; + if (objectGraph.client.isTV || objectGraph.client.isWatch) { + if (!isDetailHeader) { + const destinationPrivacyTypeStyle = privacyTypesShelf.isIntermediateDetailPageEnabled(objectGraph) + ? "intermediateDetailPage" + : "detailPage"; + seeDetailsAction = privacyDetailActionFromData(objectGraph, data, destinationPrivacyTypeStyle, pageInformation, locationTracker, null); + } + privacyPolicyAction = privacyPolicyActionFromData(objectGraph, data, pageInformation, locationTracker); + } + if (isDetailHeader) { + if (objectGraph.client.isWatch || objectGraph.client.isTV) { + privacyDefinitionsAction = createPrivacyDefinitionsAction(objectGraph, pageInformation, locationTracker); + } + learnMoreText = learnMoreTextFromData(objectGraph, data, isDetailHeader, pageInformation, locationTracker); + if (serverData.isDefinedNonNullNonEmpty(learnMoreText)) { + learnMoreAction = createLearnMoreAction(objectGraph, pageInformation, locationTracker); + } + } + const supplementaryItems = []; + if (serverData.isDefinedNonNull(privacyDefinitionsText)) { + const supplementaryItem = new models.PrivacyHeaderSupplementaryItem(privacyDefinitionsText, privacyDefinitionsAction); + supplementaryItems.push(supplementaryItem); + } + if (serverData.isDefinedNonNull(learnMoreText)) { + const supplementaryItem = new models.PrivacyHeaderSupplementaryItem(learnMoreText, learnMoreAction); + supplementaryItems.push(supplementaryItem); + } + let privacyTypes = []; + if ((objectGraph.client.isWatch || objectGraph.client.isTV) && !isDetailHeader) { + privacyTypes = privacyTypesFromData(objectGraph, data, isDetailData, "productPage", pageInformation, locationTracker); + } + const bodyActions = []; + if (objectGraph.client.isTV) { + if (serverData.isDefinedNonNull(seeDetailsAction)) { + bodyActions.push(seeDetailsAction); + } + if (serverData.isDefinedNonNull(privacyPolicyAction)) { + bodyActions.push(privacyPolicyAction); + } + } + if (objectGraph.client.isWatch) { + if (serverData.isDefinedNonNull(privacyPolicyAction)) { + bodyActions.push(privacyPolicyAction); + } + } + return new models.PrivacyHeader(bodyText, isDetailHeader, privacyTypes, bodyActions, supplementaryItems, seeDetailsAction); + }); +} +/** + * Creates the main body text for the header. + * @param data The data blob + * @param isDetailHeader Whether this header is intended to be displayed on the detail page + * @param isDetailData Whether the included data is for the detail page + */ +function bodyTextFromData(objectGraph, data, isDetailHeader, isDetailData, pageInformation, locationTracker) { + let text; + let textType = "text/x-apple-as3-nqml"; + const developer = mediaRelationships.relationshipData(objectGraph, data, "developer"); + const isAppleOwnedDeveloperId = serverData.isDefinedNonNullNonEmpty(developer) && constants.appleOwnedDeveloperIds.indexOf(developer.id) > -1; + if (isDetailHeader && !isAppleOwnedDeveloperId) { + text = objectGraph.loc.string("PRODUCT_PRIVACY_DETAIL_HEADER_TEMPLATE"); + } + else { + text = objectGraph.loc.string("PRODUCT_PRIVACY_HEADER_TEMPLATE"); + } + const privacyTypes = privacyTypesFromData(objectGraph, data, isDetailData, "detailPage", pageInformation, locationTracker); + const privacyDataNotProvided = (privacyTypes.length === 1 && privacyTypes[0].identifier === "DATA_NOT_PROVIDED") || privacyTypes.length === 0; + if (privacyDataNotProvided) { + text = objectGraph.loc.string("PRODUCT_PRIVACY_HEADER_NO_DETAILS_TEMPLATE"); + } + // Developer name + const developerName = mediaAttributes.attributeAsString(data, "artistName"); + if (serverData.isDefinedNonNull(developerName)) { + text = text.replace("{developerName}", "<b>" + developerName + "</b>"); + } + else { + // This shouldn't happen, but just in case, fallback to the text with no developer name placeholder + if (privacyDataNotProvided) { + text = objectGraph.loc.string("PRODUCT_PRIVACY_FALLBACK_HEADER_NO_DETAILS_TEMPLATE"); + } + else { + if (isDetailHeader) { + text = objectGraph.loc.string("PRODUCT_PRIVACY_FALLBACK_DETAIL_HEADER_TEMPLATE"); + } + else { + text = objectGraph.loc.string("PRODUCT_PRIVACY_FALLBACK_HEADER_TEMPLATE"); + } + } + textType = "text/plain"; + } + // Privacy policy link + const privacyPolicyLinkText = objectGraph.loc.string("PRODUCT_PRIVACY_SUMMARY_PRIVACY_POLICY_LINK"); + text = text.replace("{privacyPolicyLink}", privacyPolicyLinkText); + const privacyPolicyAction = privacyPolicyActionFromData(objectGraph, data, pageInformation, locationTracker); + const linkedSubstrings = {}; + if (serverData.isDefinedNonNull(privacyPolicyAction)) { + linkedSubstrings[privacyPolicyLinkText] = privacyPolicyAction; + } + // Manage choices + if (isDetailHeader) { + if (objectGraph.client.isiOS || + objectGraph.client.isMac || + objectGraph.client.isVision || + objectGraph.client.isWeb) { + const managePrivacyChoicesAction = managePrivacyChoicesActionFromData(objectGraph, data, pageInformation, locationTracker); + if (serverData.isDefinedNonNull(managePrivacyChoicesAction)) { + const managePrivacyChoicesLink = objectGraph.loc.string("PRODUCT_PRIVACY_MANAGE_CHOICES_LINK"); + text += "<br><br>"; + text += objectGraph.loc + .string("PRODUCT_PRIVACY_MANAGE_CHOICES_TEMPLATE") + .replace("{manageChoicesLink}", managePrivacyChoicesLink); + managePrivacyChoicesAction.title = managePrivacyChoicesLink; + linkedSubstrings[managePrivacyChoicesLink] = managePrivacyChoicesAction; + } + } + else { + text += "<br><br>"; + text += objectGraph.loc.string("PRODUCT_PRIVACY_MANAGE_CHOICES_NO_LINK"); + } + } + const styledText = new models.StyledText(text, textType); + return new models.LinkableText(styledText, linkedSubstrings); +} +/** + * Creates the action for linking to the developer's privacy policy. + */ +function privacyPolicyActionFromData(objectGraph, data, pageInformation, locationTracker) { + let privacyPolicyLinkAction; + if (objectGraph.client.isTV) { + const hasPrivacyPolicy = contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "hasPrivacyPolicyText"); + if (!hasPrivacyPolicy) { + return null; + } + const url = linksShelf.privacyPolicyUrlFromData(objectGraph, data); + if (serverData.isNull(url)) { + return null; + } + const action = new models.FlowAction("unknown"); + action.pageUrl = url; + privacyPolicyLinkAction = action; + } + else { + const privacyPolicyUrl = contentAttributes.contentAttributeAsString(objectGraph, data, "privacyPolicyUrl"); + if (isNothing(privacyPolicyUrl) || serverData.isNullOrEmpty(privacyPolicyUrl)) { + return null; + } + privacyPolicyLinkAction = new models.ExternalUrlAction(privacyPolicyUrl, false); + } + privacyPolicyLinkAction.title = objectGraph.loc.string("PRODUCT_PRIVACY_SUMMARY_PRIVACY_POLICY_BUTTON_TITLE"); + metricsHelpersClicks.addClickEventToAction(objectGraph, privacyPolicyLinkAction, { + targetType: "link", + id: "privacyPolicy", + pageInformation: pageInformation, + locationTracker: locationTracker, + }); + return privacyPolicyLinkAction; +} +/** + * Creates the action for linking to the developer's manage privacy choices URL. + */ +function managePrivacyChoicesActionFromData(objectGraph, data, pageInformation, locationTracker) { + const privacyDetailsData = mediaAttributes.attributeAsDictionary(data, "privacyDetails"); + const managePrivacyChoicesUrl = serverData.asString(privacyDetailsData, "managePrivacyChoicesUrl"); + if (isNothing(managePrivacyChoicesUrl) || serverData.isNullOrEmpty(managePrivacyChoicesUrl)) { + return null; + } + const managePrivacyChoicesAction = new models.ExternalUrlAction(managePrivacyChoicesUrl, false); + metricsHelpersClicks.addClickEventToAction(objectGraph, managePrivacyChoicesAction, { + targetType: "link", + id: "managePrivacyChoices", + pageInformation: pageInformation, + locationTracker: locationTracker, + }); + return managePrivacyChoicesAction; +} +/** + * Creates the text for linking to the learn more page. + * @param data The data blob + * @param isDetailHeader Whether this header is intended to be displayed on the detail page + */ +function learnMoreTextFromData(objectGraph, data, isDetailHeader, pageInformation, locationTracker) { + if (!isDetailHeader || objectGraph.client.isTV) { + return null; + } + const learnMoreLink = objectGraph.loc.string("PRODUCT_PRIVACY_LEARN_MORE_LINK"); + const learnMoreAction = createLearnMoreAction(objectGraph, pageInformation, locationTracker); + let text; + const linkedSubstrings = {}; + if (serverData.isNull(learnMoreAction) || objectGraph.client.isWatch) { + text = objectGraph.loc.string("PRODUCT_PRIVACY_LEARN_MORE_NO_LINK"); + } + else { + text = objectGraph.loc.string("PRODUCT_PRIVACY_LEARN_MORE_TEMPLATE").replace("{learnMoreLink}", learnMoreLink); + learnMoreAction.title = learnMoreLink; + linkedSubstrings[learnMoreLink] = learnMoreAction; + } + const styledText = new models.StyledText(text, "text/plain"); + return new models.LinkableText(styledText, linkedSubstrings); +} +/** + * Creates the action for linking to the learn more URL. + */ +export function createLearnMoreAction(objectGraph, pageInformation, locationTracker) { + const editorialItemId = objectGraph.bag.appPrivacyLearnMoreEditorialItemId; + if (isNothing(editorialItemId) || editorialItemId.length === 0) { + return null; + } + const learnMoreAction = new models.FlowAction("article"); + learnMoreAction.title = objectGraph.loc.string("PRODUCT_PRIVACY_LEARN_MORE_LINK"); + learnMoreAction.pageUrl = `https://apps.apple.com/story/id${editorialItemId}`; + metricsHelpersClicks.addClickEventToAction(objectGraph, learnMoreAction, { + targetType: "button", + id: "privacyLearnMore", + pageInformation: pageInformation, + locationTracker: locationTracker, + }); + if (objectGraph.client.isVision) { + learnMoreAction.presentation = "sheetPresent"; + } + if (objectGraph.client.isWeb) { + const destination = makeRoutableArticlePageIntent({ + ...getLocale(objectGraph), + ...getPlatform(objectGraph), + id: editorialItemId, + }); + const pageUrlString = makeRoutableArticlePageCanonicalUrl(objectGraph, destination); + learnMoreAction.pageUrl = pageUrlString; + learnMoreAction.destination = destination; + } + return learnMoreAction; +} +/** + * Creates the text for linking to the privacy definitions EI. + * @param data The data blob + * @param isDetailHeader Whether this header is intended to be displayed on the detail page + */ +function privacyDefinitionsTextFromData(objectGraph, data, isDetailHeader, pageInformation, locationTracker) { + if (!isDetailHeader) { + return null; + } + const privacyDefinitionsLink = objectGraph.loc.string("PRODUCT_PRIVACY_DEFINITIONS_LINK"); + const text = objectGraph.loc + .string("PRODUCT_PRIVACY_DEFINITIONS_LINK_TEMPLATE") + .replace("{privacyDefinitionsLink}", privacyDefinitionsLink); + const privacyDefinitionsAction = createPrivacyDefinitionsAction(objectGraph, pageInformation, locationTracker); + if (serverData.isNull(privacyDefinitionsAction)) { + return null; + } + privacyDefinitionsAction.title = privacyDefinitionsLink; + const linkedSubstrings = {}; + linkedSubstrings[privacyDefinitionsLink] = privacyDefinitionsAction; + const styledText = new models.StyledText(text, "text/plain"); + return new models.LinkableText(styledText, linkedSubstrings); +} +/** + * Creates the action for linking to the privacy definitions page. + */ +export function createPrivacyDefinitionsAction(objectGraph, pageInformation, locationTracker) { + const editorialItemId = objectGraph.bag.appPrivacyDefinitionsEditorialItemId; + if (isNothing(editorialItemId) || editorialItemId.length === 0) { + return null; + } + const privacyDefinitionsAction = new models.FlowAction("article"); + privacyDefinitionsAction.title = objectGraph.loc.string("PRODUCT_PRIVACY_DEFINITIONS_LINK"); + privacyDefinitionsAction.pageUrl = `https://apps.apple.com/story/id${editorialItemId}`; + if (objectGraph.client.isWeb) { + const destination = makeRoutableArticlePageIntent({ + ...getLocale(objectGraph), + ...getPlatform(objectGraph), + id: editorialItemId, + }); + const pageUrlString = makeRoutableArticlePageCanonicalUrl(objectGraph, destination); + privacyDefinitionsAction.pageUrl = pageUrlString; + privacyDefinitionsAction.destination = destination; + } + metricsHelpersClicks.addClickEventToAction(objectGraph, privacyDefinitionsAction, { + targetType: "button", + id: "privacyDefinitions", + pageInformation: pageInformation, + locationTracker: locationTracker, + }); + return privacyDefinitionsAction; +} +/** + * Creates a list of privacy types, suitable for associating with the header. + * In practice, this means that the categories will only be included when necessary, + * to keep the data size as small as possible. + */ +export function privacyTypesFromData(objectGraph, data, isDetailData, destinationPrivacyTypeStyle, pageInformation, locationTracker) { + var _a; + let privacyTypes = []; + const privacyDataKey = isDetailData ? "privacyDetails" : "privacy"; + const privacyData = mediaAttributes.attributeAsDictionary(data, privacyDataKey); + if (serverData.isDefinedNonNullNonEmpty(privacyData)) { + const includeCategories = objectGraph.client.deviceType !== "watch" || destinationPrivacyTypeStyle === "intermediateDetailPage"; + privacyTypes = + (_a = privacyTypesShelf.privacyTypesFromData(objectGraph, privacyData, data, destinationPrivacyTypeStyle, includeCategories, pageInformation, locationTracker)) !== null && _a !== void 0 ? _a : []; + // If we only have a single privacy type, with no categories, force it to display in `productPage` style + // even on the intermediate detail page, as it has a nicer appearance for this case. + if (privacyTypes.length === 1 && privacyTypes[0].categories.length === 0) { + privacyTypes[0].style = "productPage"; + } + } + return privacyTypes; +} +/** + * Creates an incomplete privacy detail page, sidepacking the privacy header shelf. + * On watchOS, when creating the `intermediateDetailPage`, we also sidepack the privacy types themselves, + * resulting in a complete (intermediate) detail page. + */ +export function privacyDetailSidepackPageFromData(objectGraph, data, destinationPrivacyTypeStyle, pageInformation, locationTracker) { + const shelves = []; + // Always sidepack the header, unless this will be the detail page for the watch, + // which does not display the header + if (objectGraph.client.deviceType !== "watch" || destinationPrivacyTypeStyle !== "detailPage") { + const headerShelf = new models.Shelf("privacyHeader"); + const privacyHeader = privacyHeaderFromData(objectGraph, data, true, false, pageInformation, locationTracker); + headerShelf.items = [privacyHeader]; + headerShelf.presentationHints = { isFirstShelf: true }; + shelves.push(headerShelf); + } + if ( + // On the watch, we want to sidepack the privacy types if coming from the product page + // as we already have all the information we need, and we can avoid having an incomplete page + (objectGraph.client.isWatch && destinationPrivacyTypeStyle === "intermediateDetailPage") || + // The "detailPage" on the "web" client is presented as a modal, so we want to sidepack all + // of the data so it can display is immediately + (objectGraph.client.isWeb && destinationPrivacyTypeStyle === "detailPage")) { + const privacyTypes = privacyTypesFromData(objectGraph, data, objectGraph.client.isWeb, destinationPrivacyTypeStyle, pageInformation, locationTracker); + const shelf = new models.Shelf("privacyType"); + if (privacyTypes.length > 0) { + shelf.items = privacyTypes; + shelves.push(shelf); + } + } + const page = new models.GenericPage(shelves); + if (objectGraph.client.deviceType !== "watch" || destinationPrivacyTypeStyle === "detailPage") { + page.isIncomplete = true; + } + page.title = objectGraph.loc.string("PRODUCT_PRIVACY_TITLE"); + if (objectGraph.client.isMac) { + page.presentationOptions = ["prefersLargeTitle"]; + } + return page; +} +/** + * Creates the action for linking to the privacy detail page. + */ +export function privacyDetailActionFromData(objectGraph, data, destinationPrivacyTypeStyle, pageInformation, locationTracker, scrollFocusPrivacyTypeId) { + if (serverData.isNull(data.id)) { + return null; + } + const seeDetailsAction = new models.FlowAction("privacyDetail"); + seeDetailsAction.title = objectGraph.loc.string("ACTION_SEE_DETAILS"); + seeDetailsAction.pageData = privacyDetailSidepackPageFromData(objectGraph, data, destinationPrivacyTypeStyle, pageInformation, locationTracker); + const productType = data.type === "app-bundles" ? Path.productBundle : Path.product; + let query; + if (serverData.isDefinedNonNullNonEmpty(scrollFocusPrivacyTypeId)) { + query = { privacyTypeId: scrollFocusPrivacyTypeId }; + } + const pageUrl = urls.URL.fromComponents(Protocol.internal, null, `/${Path.privacyDetail}/${productType}/${data.id}`, query); + seeDetailsAction.pageUrl = pageUrl.build(); + const seeDetailsClickOptions = { + targetType: "button", + id: "seeDetails", + pageInformation: pageInformation, + locationTracker: locationTracker, + }; + if (serverData.isDefinedNonNull(scrollFocusPrivacyTypeId)) { + seeDetailsClickOptions.targetType = "privacyCard"; + seeDetailsClickOptions.id = scrollFocusPrivacyTypeId; + } + metricsHelpersClicks.addClickEventToAction(objectGraph, seeDetailsAction, seeDetailsClickOptions); + return seeDetailsAction; +} +//# sourceMappingURL=privacy-header-shelf.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/privacy/privacy-suppression.js b/node_modules/@jet-app/app-store/tmp/src/common/privacy/privacy-suppression.js new file mode 100644 index 0000000..f43144f --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/privacy/privacy-suppression.js @@ -0,0 +1,34 @@ +import { isNothing } from "@jet/environment/types/optional"; +import * as serverData from "../../foundation/json-parsing/server-data"; +let suppressedPrivacyApps = null; +function initialize(objectGraph) { + if (suppressedPrivacyApps !== null) { + return; + } + suppressedPrivacyApps = new Set(); + for (const appId of objectGraph.bag.suppressedPrivacyAppIds) { + suppressedPrivacyApps.add(appId); + } +} +export function shouldSuppressPrivacyInformationForAdamId(objectGraph, adamId) { + initialize(objectGraph); + if (isNothing(suppressedPrivacyApps) || serverData.isNullOrEmpty(adamId)) { + return false; + } + return suppressedPrivacyApps.has(adamId); +} +export function shouldSuppressPrivacyInformationForBundleId(objectGraph, bundleId) { + initialize(objectGraph); + if (isNothing(suppressedPrivacyApps) || serverData.isNullOrEmpty(bundleId)) { + return false; + } + /// This handles a special case wildcard entry for macOS installers, to avoid the churn of adding new bundleIDs + /// for each macOS release. We specifically do not want to treat all entries in `suppressedPrivacyApps` as a regex, + /// as there is no expectation that any other entry will be a regex, and treating them as such may lead to false matches. + if (suppressedPrivacyApps.has("com.apple.InstallAssistant.*") && + bundleId.startsWith("com.apple.InstallAssistant.")) { + return true; + } + return suppressedPrivacyApps.has(bundleId); +} +//# sourceMappingURL=privacy-suppression.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/privacy/privacy-types-shelf.js b/node_modules/@jet-app/app-store/tmp/src/common/privacy/privacy-types-shelf.js new file mode 100644 index 0000000..11b58b6 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/privacy/privacy-types-shelf.js @@ -0,0 +1,322 @@ +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 contentArtwork from "../content/artwork/artwork"; +import * as content from "../content/content"; +import * as metricsHelpersImpressions from "../metrics/helpers/impressions"; +import * as metricsHelpersLocation from "../metrics/helpers/location"; +import * as privacyHeaderShelf from "./privacy-header-shelf"; +/** + * Builder for the privacy types shelf. This shelf is currently used + * on both the product page, and the privacy detail page. + */ +export function create(objectGraph, data, shelfMetrics) { + return validation.context("create", () => { + if (serverData.isNullOrEmpty(data) || objectGraph.client.isWatch) { + return null; + } + const privacyData = mediaAttributes.attributeAsDictionary(data, "privacy"); + if (isNothing(privacyData) || serverData.isNullOrEmpty(privacyData)) { + return null; + } + const shelf = new models.Shelf("privacyType"); + const title = objectGraph.loc.string("PRODUCT_PRIVACY_TITLE"); + metricsHelpersLocation.pushContentLocation(objectGraph, { + pageInformation: shelfMetrics.metricsPageInformation, + locationTracker: shelfMetrics.locationTracker, + targetType: "privacyCard", + id: `${shelfMetrics.getSequenceId()}`, + idType: "sequential", + }, title); + const privacyTypes = privacyTypesFromData(objectGraph, privacyData, data, "productPage", true, shelfMetrics.metricsPageInformation, shelfMetrics.locationTracker); + if (privacyTypes === null || privacyTypes.length === 0) { + return null; + } + shelf.items = privacyTypes; + if (privacyTypes.length <= 2) { + shelf.presentationHints = { isLowDensity: true }; + } + // We don't actually want a shelf title, but we populate it temporarily here + // so that impressions have the appropriate title without causing too much churn. + shelf.title = title; + shelfMetrics.addImpressionsToShelf(objectGraph, shelf, "appPrivacy"); + metricsHelpersLocation.popLocation(shelfMetrics.locationTracker); + metricsHelpersLocation.nextPosition(shelfMetrics.locationTracker); + shelf.title = null; + return shelf; + }); +} +/** + * Creates a list of privacy types from the given data. + * @param privacyData The privacy data blob + * @param dataContainer The response data + * @param style The style that the privacy types should be created with + * @param includeCategories Whether the privacy categories should be included or not. + */ +export function privacyTypesFromData(objectGraph, privacyData, dataContainer, style, includeCategories, pageInformation, locationTracker) { + const types = serverData.asArrayOrEmpty(privacyData, "privacyTypes"); + const privacyTypes = []; + for (const type of types) { + const typeData = serverData.asJSONData(type); + if (serverData.isDefinedNonNullNonEmpty(typeData)) { + const privacyType = privacyTypeFromData(objectGraph, typeData, dataContainer, style, includeCategories, pageInformation, locationTracker); + if (serverData.isDefinedNonNull(privacyType)) { + privacyTypes.push(privacyType); + } + } + } + if (privacyTypes.length === 0) { + const noDetailsPrivacyType = noDetailsProvidedPrivacyTypeFromDataContainer(objectGraph, dataContainer, style, pageInformation, locationTracker); + privacyTypes.push(noDetailsPrivacyType); + } + for (const privacyType of privacyTypes) { + const options = { + id: null, + kind: null, + softwareType: null, + title: privacyType.title, + pageInformation: pageInformation, + locationTracker: locationTracker, + targetType: "privacyCard", + }; + metricsHelpersImpressions.addImpressionFields(objectGraph, privacyType, options); + metricsHelpersLocation.nextPosition(locationTracker); + } + return privacyTypes; +} +/** + * Creates a privacy type from the given data. + * @param data The privacy types data blob + * @param dataContainer The response data + * @param style The style that the privacy types should have. + * @param includeCategories Whether the privacy categories should be included or not. + */ +function privacyTypeFromData(objectGraph, data, dataContainer, style, includeCategories, pageInformation, locationTracker) { + const identifier = serverData.asString(data, "identifier"); + const title = serverData.asString(data, "privacyType"); + const detail = serverData.asString(data, "description"); + if (isNothing(identifier) || + serverData.isNullOrEmpty(identifier) || + isNothing(title) || + serverData.isNullOrEmpty(title) || + isNothing(detail) || + serverData.isNullOrEmpty(detail)) { + return null; + } + const artworkResourceUrl = artworkResourceUrlForPrivacyTypeIdentifier(objectGraph, identifier); + const artwork = artworkFromData(objectGraph, data, artworkResourceUrl); + const categories = includeCategories ? privacyCategoriesFromData(objectGraph, data, style) : []; + const purposes = privacyPurposesFromData(objectGraph, data, style); + const clickAction = privacyTypeClickActionFromDataContainer(objectGraph, dataContainer, identifier, style, pageInformation, locationTracker); + return new models.PrivacyType(identifier, title, detail, artwork, style, purposes, categories, clickAction); +} +/** + * Creates a suitable click action for a privacy type. + * @param dataContainer The response data + * @param identifier The identifier for the privacy type + * @param style The style that the privacy types should have. + */ +function privacyTypeClickActionFromDataContainer(objectGraph, dataContainer, identifier, style, pageInformation, locationTracker) { + let clickAction = null; + if (style === "productPage" || style === "intermediateDetailPage") { + const destinationStyle = isIntermediateDetailPageEnabled(objectGraph) && style === "productPage" + ? "intermediateDetailPage" + : "detailPage"; + clickAction = privacyHeaderShelf.privacyDetailActionFromData(objectGraph, dataContainer, destinationStyle, pageInformation, locationTracker, identifier); + } + return clickAction; +} +/** + * Creates a privacy type object to represent the case where the developer has not yet provided any privacy details. + * + * @param dataContainer The response data + * @param style The style that the privacy types should have. + */ +function noDetailsProvidedPrivacyTypeFromDataContainer(objectGraph, dataContainer, style, pageInformation, locationTracker) { + const identifier = "DATA_NOT_PROVIDED"; + const title = objectGraph.loc.string("PRODUCT_PRIVACY_NO_DETAILS_PROVIDED_TITLE"); + const detail = objectGraph.loc.string("PRODUCT_PRIVACY_NO_DETAILS_PROVIDED_BODY"); + const artwork = contentArtwork.createArtworkForResource(objectGraph, "systemimage://exclamationmark.triangle"); + const clickAction = privacyTypeClickActionFromDataContainer(objectGraph, dataContainer, identifier, style, pageInformation, locationTracker); + return new models.PrivacyType(identifier, title, detail, artwork, style, [], [], clickAction); +} +/** + * Creates a list of privacy categories from the given data. + * @param data The privacy data blob + * @param privacyTypeStyle The style that the parent privacy type style will have. + */ +function privacyCategoriesFromData(objectGraph, data, privacyTypeStyle) { + const categories = serverData.asArrayOrEmpty(data, "dataCategories"); + const privacyCategories = []; + for (const category of categories) { + const categoryData = serverData.asJSONData(category); + if (serverData.isDefinedNonNullNonEmpty(categoryData)) { + const style = privacyTypeStyle === "productPage" ? "productPage" : "detailPage"; + const privacyCategory = privacyCategoryFromData(objectGraph, categoryData, style); + if (serverData.isDefinedNonNull(privacyCategory)) { + privacyCategories.push(privacyCategory); + } + } + } + return privacyCategories; +} +/** + * Creates a privacy category from the given data. + * @param data The privacy data blob + * @param style The style that the privacy category should have. + */ +function privacyCategoryFromData(objectGraph, data, style) { + const identifier = serverData.asString(data, "identifier"); + const title = serverData.asString(data, "dataCategory"); + if (isNothing(identifier) || + serverData.isNullOrEmpty(identifier) || + isNothing(title) || + serverData.isNullOrEmpty(title)) { + return null; + } + const artworkResourceUrl = artworkResourceUrlForPrivacyCategoryIdentifier(objectGraph, identifier); + const artwork = artworkFromData(objectGraph, data, artworkResourceUrl); + if (serverData.isNull(artwork)) { + return null; + } + let dataTypes = []; + if (style === "detailPage") { + dataTypes = serverData.asArrayOrEmpty(data, "dataTypes"); + } + const privacyCategory = new models.PrivacyCategory(identifier, title, artwork, style, dataTypes); + if (identifier === "USAGE_DATA") { + privacyCategory.prefersSmallArtwork = true; + } + return privacyCategory; +} +/** + * Creates a list of privacy purposes from the given data. + * @param data The privacy data blob + * @param privacyTypeStyle The style that the parent privacy type style will have. + */ +function privacyPurposesFromData(objectGraph, data, privacyTypeStyle) { + const purposes = serverData.asArrayOrEmpty(data, "purposes"); + const privacyPurposes = []; + for (const purpose of purposes) { + const purposeData = serverData.asJSONData(purpose); + if (serverData.isDefinedNonNullNonEmpty(purposeData)) { + const privacyPurpose = privacyPurposeFromData(objectGraph, purposeData, privacyTypeStyle); + if (serverData.isDefinedNonNull(privacyPurpose)) { + privacyPurposes.push(privacyPurpose); + } + } + } + return privacyPurposes; +} +/** + * Creates a privacy purpose from the given data. + * @param data The privacy data blob + * @param privacyTypeStyle The style that the parent privacy type style will have. + */ +function privacyPurposeFromData(objectGraph, data, privacyTypeStyle) { + const identifier = serverData.asString(data, "identifier"); + const title = serverData.asString(data, "purpose"); + const categories = privacyCategoriesFromData(objectGraph, data, privacyTypeStyle); + if (isNothing(identifier) || + serverData.isNullOrEmpty(identifier) || + isNothing(title) || + serverData.isNullOrEmpty(title) || + categories.length === 0) { + return null; + } + return new models.PrivacyPurpose(identifier, title, categories); +} +/** + * Creates a suitable artwork object for a privacy object, from the given data blob or artwork resource url + * @param data The privacy object data blob + * @param artworkResourceUrl An optional artwork resource url + */ +function artworkFromData(objectGraph, data, artworkResourceUrl) { + let artwork = null; + if (isSome(artworkResourceUrl) && (artworkResourceUrl === null || artworkResourceUrl === void 0 ? void 0 : artworkResourceUrl.length) > 0) { + artwork = contentArtwork.createArtworkForResource(objectGraph, artworkResourceUrl); + } + if (serverData.isNull(artwork)) { + const artworkData = serverData.asDictionary(data, "artwork"); + if (serverData.isDefinedNonNull(artworkData)) { + artwork = content.artworkFromApiArtwork(objectGraph, artworkData, { + useCase: 0 /* content.ArtworkUseCase.Default */, + allowingTransparency: true, + }); + } + } + return artwork; +} +/** + * Maps a privacy type identifier to a known (SF symbol) artwork resource. + * @param privacyTypeIdentifier the given privacy type identifier. + */ +function artworkResourceUrlForPrivacyTypeIdentifier(objectGraph, privacyTypeIdentifier) { + switch (privacyTypeIdentifier) { + case "DATA_NOT_LINKED_TO_YOU": + return "resource://person.circle.slash"; + case "DATA_USED_TO_TRACK_YOU": + return "systemimage://person.fill.viewfinder"; + case "DATA_NOT_COLLECTED": + return "systemimage://checkmark.circle"; + case "DATA_LINKED_TO_YOU": + return "systemimage://person.circle"; + default: + return null; + } +} +/** + * Maps a privacy category identifier to a known (SF symbol) artwork resource. + * @param privacyCategoryIdentifier the given privacy category identifier. + */ +function artworkResourceUrlForPrivacyCategoryIdentifier(objectGraph, privacyCategoryIdentifier) { + switch (privacyCategoryIdentifier) { + case "FINANCIAL_INFO": + return "systemimage://creditcard.fill"; + case "CONTACT_INFO": + return "systemimage://info.circle.fill"; + case "OTHER": + return "systemimage://ellipsis.circle.fill"; + case "SENSITIVE_INFO": + return "systemimage://eye.fill"; + case "USAGE_DATA": + return "systemimage://chart.bar.fill"; + case "CONTACTS": + return "systemimage://person.circle"; + case "PURCHASES": + return "systemimage://bag.fill"; + case "LOCATION": + return "systemimage://location.fill"; + case "HEALTH_AND_FITNESS": + return "systemimage://heart.circle.fill"; + case "IDENTIFIERS": + return "resource://person.crop.rectangle.line.fill"; + case "USER_CONTENT": + return "systemimage://photo.fill.on.rectangle.fill"; + case "BROWSING_HISTORY": + return "systemimage://clock.fill"; + case "DIAGNOSTICS": + return "systemimage://gearshape.fill"; + case "SEARCH_HISTORY": + return "systemimage://magnifyingglass.circle.fill"; + case "BODY": + return "systemimage://figure"; + case "SURROUNDING": + return "systemimage://arkit"; + default: + return null; + } +} +/** + * Determines whether the intermediate detail privacy page is enabled. + */ +export function isIntermediateDetailPageEnabled(objectGraph) { + // Only watchOS has an intermediate detail page + if (objectGraph.client.deviceType !== "watch") { + return false; + } + return true; +} +//# sourceMappingURL=privacy-types-shelf.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/product-page/badges/badges-common.js b/node_modules/@jet-app/app-store/tmp/src/common/product-page/badges/badges-common.js new file mode 100644 index 0000000..475110a --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/product-page/badges/badges-common.js @@ -0,0 +1,123 @@ +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 content from "../../content/content"; +import * as metricsHelpersClicks from "../../metrics/helpers/clicks"; +import * as productPageUtil from "../product-page-util"; +export function badgeArtwork(objectGraph, artworkData, cropCode) { + // Filter out this badge if no artwork exists + if (!serverData.isDefinedNonNullNonEmpty(artworkData)) { + return null; + } + let cropCodeToUse = null; + if (objectGraph.client.isVision) { + cropCodeToUse = "bb"; + } + else if (isNothing(cropCode)) { + cropCodeToUse = "sr"; + } + else { + cropCodeToUse = cropCode; + } + // Filter out this badge if we can't create an artwork model + const artwork = content.artworkFromApiArtwork(objectGraph, artworkData, { + cropCode: cropCodeToUse, + useCase: 12 /* content.ArtworkUseCase.ProductPageBadge */, + allowingTransparency: true, + }); + if (serverData.isNullOrEmpty(artwork)) { + return null; + } + return artwork; +} +/** + * Contains metadata for an action on a badge. This is useful when the configuration of a badge action is dependent on + * data in models that might be configured at a later time. + */ +export class BadgeActionMetadata { + setShelfId(shelfId, index) { + this.shelfId = shelfId; + this.index = index; + if (models.legacyProductPageKnownShelfIds.has(shelfId)) { + this.section = new models.ProductPageSection("shelf", shelfId); + } + } +} +/** + * Certain parameters on the actions for some badges depend on product page configuration done at a later point + * in the product page building. This function allows metadata configured at those various parts of the product page + * build process to be applied. + * + * Example: We configure the product page badges in the sidepack, but the action depends on there being an + * Information section, which is not part of the sidepack. We need the index within the Information section to inform + * scroll actions exactly which index to scroll to (iOS/macOS), or we need the Information item itself in order to + * display it (visionOS). + * @param badges The badges to apply the metadata to. + * @param badgeActionMetadataByKey The map of badge keys to their respective metadata, if any. + * @param shelfMetrics The metrics to + */ +export function applyActionMetadataToBadges(objectGraph, badges, badgeActionMetadataByKey, shelfMetrics) { + var _a, _b, _c, _d, _e; + let index = 0; + for (const badge of badges) { + const metadata = badgeActionMetadataByKey[badge.key]; + if (metadata) { + let clickAction; + if (objectGraph.client.isVision) { + if (isSome(metadata.annotation)) { + // We only want to allow presenting an expanded annotation from a ribbon badge if there + // is useful extra information to show. This somewhat duplicates logic we have on the + // native side, but that logic is specific to annotations in the Information section, + // and has some additional checks. + const hasSummary = isSome(metadata.annotation.summary) || isSome((_a = metadata.annotation.items[0]) === null || _a === void 0 ? void 0 : _a.text); + const hasMultipleItems = metadata.annotation.items.length > 1; + const summaryIsDifferentToItem = isSome(metadata.annotation.summary) && + ((_b = metadata.annotation.items[0]) === null || _b === void 0 ? void 0 : _b.text) !== metadata.annotation.summary; + const itemHasTextPairs = isSome(((_e = (_d = (_c = metadata.annotation.items[0]) === null || _c === void 0 ? void 0 : _c.textPairs) === null || _d === void 0 ? void 0 : _d.length) !== null && _e !== void 0 ? _e : 0) > 0); + const hasLink = isSome(metadata.annotation.linkAction); + if (!hasSummary || hasMultipleItems || summaryIsDifferentToItem || itemHasTextPairs || hasLink) { + clickAction = metadata.annotation.expandAction; + } + } + } + else if (productPageUtil.isShelfBased(objectGraph)) { + clickAction = new models.ShelfBasedPageScrollAction(metadata.shelfId, null, null, null, metadata.index, true); + } + else if (isSome(metadata.section)) { + const action = new models.ProductPageScrollAction(metadata.section, true); + action.index = metadata.index; + clickAction = action; + } + badge.clickAction = clickAction; + } + let actionType; + switch (badge.key) { + case "contentRating": + case "editorsChoice": + case "rating": + case "languages": + case "size": + case "controller": + case "category": + actionType = "select"; + break; + default: + actionType = "navigate"; + } + if (badge.clickAction) { + metricsHelpersClicks.addClickEventToAction(objectGraph, badge.clickAction, { + targetType: "badge", + id: badge.key, + idType: "sequential", + actionType: actionType, + actionDetails: { + position: index, + }, + pageInformation: shelfMetrics.metricsPageInformation, + locationTracker: shelfMetrics.locationTracker, + }); + } + index += 1; + } +} +//# sourceMappingURL=badges-common.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/product-page/badges/badges.js b/node_modules/@jet-app/app-store/tmp/src/common/product-page/badges/badges.js new file mode 100644 index 0000000..2e57c73 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/product-page/badges/badges.js @@ -0,0 +1,153 @@ +import * as validation from "@jet/environment/json/validation"; +import * as mediaAttributes from "../../../foundation/media/attributes"; +import * as category from "./category-badge"; +import * as chart from "./chart-badge"; +import * as compatibility from "./compatibility-badge"; +import * as contentRating from "./content-rating-badge"; +import * as controller from "./controller-badge"; +import * as developer from "./developer-badge"; +import * as editorsChoice from "./editors-choice-badge"; +import * as friendsPlaying from "./friends-playing-badge"; +import * as highMotion from "./high-motion-badge"; +import * as language from "./language-badge"; +import * as multiplayer from "./multiplayer-badge"; +import * as preorder from "./preorder-badge"; +import * as rating from "./rating-badge"; +import * as size from "./size-badge"; +import * as spatialController from "./spatial-controller-badge"; +import * as storefrontContentRating from "./storefront-content-rating-badge"; +const preorderBadgeList = [ + contentRating.createBadge, + storefrontContentRating.createBadge, + category.createBadge, + multiplayer.createBadge, + developer.createBadge, + controller.createBadge, + language.createBadge, +]; +const standardBadgeList = [ + rating.createBadge, + editorsChoice.createBadge, + chart.createBadge, + contentRating.createBadge, + storefrontContentRating.createBadge, +]; +const ribbonBadgeList = [ + rating.createBadge, + contentRating.createBadge, + storefrontContentRating.createBadge, + controller.createHighPriorityBadge, + friendsPlaying.createBadge, + chart.createBadge, + category.createBadge, + multiplayer.createBadge, + developer.createBadge, + controller.createLowPriorityBadge, + language.createBadge, + size.createBadge, +]; +const gamesBadgeList = [ + rating.createBadge, + contentRating.createBadge, + category.createBadge, + controller.createBadge, + developer.createBadge, +]; +const visionRibbonBadgeList = [ + preorder.createBadge, + compatibility.createBadge, + rating.createBadge, + spatialController.createRequiredBadge, + controller.createHighPriorityBadge, + spatialController.createHighPrioritySupportedBadge, + editorsChoice.createBadge, + contentRating.createBadge, + storefrontContentRating.createBadge, + highMotion.createBadge, + category.createBadge, + multiplayer.createBadge, + controller.createLowPriorityBadge, + spatialController.createLowPrioritySupportedBadge, + developer.createBadge, +]; +const visionPreorderBadgeList = [ + preorder.createBadge, + compatibility.createBadge, + editorsChoice.createBadge, + category.createBadge, + contentRating.createBadge, + storefrontContentRating.createBadge, + highMotion.createBadge, + multiplayer.createBadge, + controller.createBadge, + spatialController.createBadge, + developer.createBadge, +]; +const tvBadgeList = [ + rating.createBadge, + editorsChoice.createBadge, + controller.createBadge, + chart.createBadge, + contentRating.createBadge, + storefrontContentRating.createBadge, + category.createBadge, + multiplayer.createBadge, + developer.createBadge, +]; +function createBadges(objectGraph, badgeList, data, embeddedInRibbon, excludeCategoryIfChartExists, metricsContext) { + const badges = []; + for (const f of badgeList) { + const badge = f(objectGraph, data, embeddedInRibbon, metricsContext); + if (badge) { + badges.push(badge); + } + } + if (excludeCategoryIfChartExists && badges.some((badge) => badge.key === "chartPosition")) { + // Category and chart badges are mutually exclusive, with chart taking priority + const categoryIndex = badges.findIndex((badge) => badge.key === "category"); + if (categoryIndex >= 0) { + badges.splice(categoryIndex, 1); + } + } + return badges; +} +export function badgesFromResponse(objectGraph, data, embeddedInRibbon, metricsContext) { + if (!data) { + return []; + } + return validation.context("badgesFromLookupResponse", () => { + let badgeList; + let excludeCategoryIfChartExists = false; + const isPreorder = mediaAttributes.attributeAsBooleanOrFalse(data, "isPreorder"); + if (preprocessor.GAMES_TARGET) { + badgeList = gamesBadgeList; + } + else if (isPreorder) { + if (objectGraph.client.isVision) { + badgeList = visionPreorderBadgeList; + } + else { + badgeList = preorderBadgeList; + } + } + else if (objectGraph.client.isTV) { + badgeList = tvBadgeList; + } + else if (objectGraph.client.isVision) { + badgeList = visionRibbonBadgeList; + } + else if (embeddedInRibbon) { + badgeList = ribbonBadgeList; + const ribbonSupportedOnPlatform = objectGraph.host.isiOS || objectGraph.host.isMac || objectGraph.client.isWeb; + if (ribbonSupportedOnPlatform && badgeList.indexOf(editorsChoice.createBadge) === -1) { + badgeList.splice(1, 0, editorsChoice.createBadge); + } + excludeCategoryIfChartExists = true; + } + else { + badgeList = standardBadgeList; + } + return createBadges(objectGraph, badgeList, data, embeddedInRibbon, excludeCategoryIfChartExists, metricsContext); + }); +} +//# sourceMappingURL=badges.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/product-page/badges/category-badge.js b/node_modules/@jet-app/app-store/tmp/src/common/product-page/badges/category-badge.js new file mode 100644 index 0000000..33d31c6 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/product-page/badges/category-badge.js @@ -0,0 +1,29 @@ +import * as models from "../../../api/models"; +import * as serverData from "../../../foundation/json-parsing/server-data"; +import * as mediaAttributes from "../../../foundation/media/attributes"; +import { categoryArtworkData } from "../../categories"; +import * as lockups from "../../lockups/lockups"; +import * as badgesCommon from "./badges-common"; +export function createBadge(objectGraph, data, embeddedInRibbon, metricsContext) { + const categoryName = lockups.categoryFromData(objectGraph, data); + if (serverData.isNullOrEmpty(categoryName)) { + return null; + } + const badgeContent = {}; + const heading = objectGraph.loc.string("BADGE_CATEGORY_HEADING"); + const style = objectGraph.client.isTV ? "annotation" : "standard"; + const badgeType = objectGraph.client.isTV ? "custom" : "artwork"; + const badge = new models.Badge(badgeType, "category", badgeContent, style, heading, categoryName, null, categoryName, heading, "infer"); + const action = new models.FlowAction("page"); + action.title = mediaAttributes.attributeAsString(data, "artistName"); + if (embeddedInRibbon && objectGraph.client.deviceType !== "tv") { + const artworkData = categoryArtworkData(objectGraph, data, true); + // Artwork is required for artwork badges; if it's missing, hide the badge completely. + if (badgeType === "artwork" && serverData.isNull(artworkData)) { + return null; + } + badge.artwork = badgesCommon.badgeArtwork(objectGraph, artworkData, "bb"); + } + return badge; +} +//# sourceMappingURL=category-badge.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/product-page/badges/chart-badge.js b/node_modules/@jet-app/app-store/tmp/src/common/product-page/badges/chart-badge.js new file mode 100644 index 0000000..e70411d --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/product-page/badges/chart-badge.js @@ -0,0 +1,37 @@ +import { isSome } from "@jet/environment/types/optional"; +import * as models from "../../../api/models"; +import * as serverData from "../../../foundation/json-parsing/server-data"; +import * as contentAttributes from "../../content/attributes"; +import * as content from "../../content/content"; +export function createBadge(objectGraph, data, embeddedInRibbon, metricsContext) { + const chartDataByStore = contentAttributes.contentAttributeAsDictionary(objectGraph, data, "chartPositions"); + if (chartDataByStore) { + const storeChartKey = content.badgeChartKeyForClientIdentifier(objectGraph, objectGraph.host.clientIdentifier); + if (storeChartKey) { + const chartData = serverData.asDictionary(chartDataByStore, storeChartKey); + if (chartData) { + const position = serverData.asNumber(chartData, "position"); + const genreName = serverData.asString(chartData, "genreName"); + const genre = serverData.asString(chartData, "genre"); + const chart = serverData.asString(chartData, "chart"); + const heading = objectGraph.loc.string("BADGE_CHART_POSITION_HEADING"); + const accessibilityTitle = objectGraph.loc + .string("PRODUCT_ACCESSIBILITY_BADGE_CHART_POSITION_TITLE") + .replace("{chartPosition}", position.toString()); + const badge = new models.Badge("chartPosition", "chartPosition", { + position: objectGraph.loc.decimal(position), + }, "standard", heading, genreName, null, accessibilityTitle, genreName, "view"); + const chartUrl = content.chartUrlFromData(objectGraph, genre, chart); + // Charts are not currently supported for visionOS + if (isSome(chartUrl) && !objectGraph.client.isVision) { + const action = new models.FlowAction("topCharts"); + action.pageUrl = chartUrl; + badge.clickAction = action; + } + return badge; + } + } + } + return null; +} +//# sourceMappingURL=chart-badge.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/product-page/badges/content-rating-badge.js b/node_modules/@jet-app/app-store/tmp/src/common/product-page/badges/content-rating-badge.js new file mode 100644 index 0000000..83def78 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/product-page/badges/content-rating-badge.js @@ -0,0 +1,76 @@ +import { isNothing, isSome } from "@jet/environment"; +import * as models from "../../../api/models"; +import * as mediaAttributes from "../../../foundation/media/attributes"; +import * as ageRatings from "../../content/age-ratings"; +import * as content from "../../content/content"; +import { artworkTemplateForBundleImage, createArtworkForResource } from "../../content/artwork/artwork"; +/** + * Builds a `Badge` model representing the Apple content rating. Additionally + * includes the Brazil age rating if user is in the BR storefront. + */ +export function createBadge(objectGraph, data, embeddedInRibbon, metricsContext) { + var _a; + return ((_a = createModernBadge(objectGraph, data, embeddedInRibbon, metricsContext)) !== null && _a !== void 0 ? _a : createLegacyBadge(objectGraph, data, embeddedInRibbon, metricsContext)); +} +function createModernBadge(objectGraph, data, embeddedInRibbon, metricsContext) { + const name = ageRatings.name(objectGraph, data); + if (isNothing(name)) { + return null; + } + let caption; + let longCaption; + if (preprocessor.GAMES_TARGET) { + caption = undefined; + } + else if (embeddedInRibbon && !objectGraph.client.isVision) { + if (ageRatings.hasInAppControls(objectGraph, data)) { + caption = objectGraph.loc.string("ProductPage.Badge.AgeRating.Caption.Ribbon.InAppControls"); + } + else { + caption = objectGraph.loc.string("ProductPage.Badge.AgeRating.Caption.Ribbon"); + } + } + else { + caption = objectGraph.loc.string("ProductPage.Badge.AgeRating.Caption"); + } + const heading = preprocessor.GAMES_TARGET + ? undefined + : objectGraph.loc.string("ProductPage.Badge.AgeRating.Heading"); + const accessibilityCaption = objectGraph.loc.string("ProductPage.Badge.AgeRating.AX.Caption"); + const pictogramResource = ageRatings.pictogramResource(objectGraph, data); + const badgeType = isSome(pictogramResource) ? "artwork" : "contentRating"; + const style = objectGraph.client.isTV ? "annotation" : "standard"; + const badge = new models.Badge(badgeType, "contentRating", { contentRating: name }, style, heading, caption, longCaption, name, accessibilityCaption, "view"); + if (isSome(pictogramResource)) { + badge.isMonochrome = false; + badge.artwork = createArtworkForResource(objectGraph, artworkTemplateForBundleImage(pictogramResource), 26, 26); + } + return badge; +} +function createLegacyBadge(objectGraph, data, embeddedInRibbon, metricsContext) { + const contentRating = mediaAttributes.attributeAsString(data, "contentRatingsBySystem.appsApple.name"); + if (isNothing(contentRating)) { + return null; + } + const badgeContent = { contentRating: contentRating }; + const brazilRank = mediaAttributes.attributeAsNumber(data, "contentRatingsBySystem.appsBrazil.rank"); + const contentRatingResource = content.storefrontContentRatingResourceForRank(objectGraph, brazilRank !== null && brazilRank !== void 0 ? brazilRank : undefined); + if (contentRatingResource) { + badgeContent.contentRatingResource = contentRatingResource; + } + let title; + if (preprocessor.GAMES_TARGET) { + title = undefined; + } + else if (embeddedInRibbon && !objectGraph.client.isVision) { + title = objectGraph.loc.string("BADGE_AGE_RATING_YEARS"); + } + else { + title = objectGraph.loc.string("BADGE_AGE_RATING"); + } + const heading = preprocessor.GAMES_TARGET ? undefined : objectGraph.loc.string("BADGE_AGE_RATING_HEADING"); + const accessibilityCaption = objectGraph.loc.string("PRODUCT_ACCESSIBILITY_BADGE_AGE_RATING_TITLE"); + const style = objectGraph.client.isTV ? "annotation" : "standard"; + return new models.Badge("contentRating", "contentRating", badgeContent, style, heading, title, undefined, contentRating, accessibilityCaption, "view"); +} +//# sourceMappingURL=content-rating-badge.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/product-page/badges/controller-badge.js b/node_modules/@jet-app/app-store/tmp/src/common/product-page/badges/controller-badge.js new file mode 100644 index 0000000..439448e --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/product-page/badges/controller-badge.js @@ -0,0 +1,178 @@ +import * as models from "../../../api/models"; +import * as artwork from "../../content/artwork/artwork"; +import * as productPageUtil from "../product-page-util"; +import * as gameController from "./../../content/game-controller"; +export function createBadge(objectGraph, data, embeddedInRibbon, metricsContext) { + var _a; + if (objectGraph.host.isTV) { + return createBadgeForTV(objectGraph, data, embeddedInRibbon, metricsContext); + } + else { + return ((_a = createHighPriorityBadge(objectGraph, data, embeddedInRibbon, metricsContext)) !== null && _a !== void 0 ? _a : createLowPriorityBadge(objectGraph, data, embeddedInRibbon, metricsContext)); + } +} +/** + * Returns a badge that should displayed as higher priority in badge list + * - In iOS: returns value if badge is `CONTROLLER_RECOMMENDED` + * - In visionOS: returns value if badge is `CONTROLLER_REQUIRED` + * + * This is mutually exclusive with `createLowPriorityBadge` + */ +export function createHighPriorityBadge(objectGraph, data, embeddedInRibbon, metricsContext) { + if (objectGraph.client.isiOS) { + return createRecommendedControllerBadgeForIOS(objectGraph, data, embeddedInRibbon, metricsContext); + } + else if (objectGraph.client.isVision) { + return createRequiredControllerBadgeForVisionOS(objectGraph, data, embeddedInRibbon, metricsContext); + } + else { + return null; + } +} +/** + * Returns a badge that should displayed as lower priority in badge list (eg: `CONTROLLER_OPTIONAL`) + * + * This is mutually exclusive with `createHighPriorityBadge` + */ +export function createLowPriorityBadge(objectGraph, data, embeddedInRibbon, metricsContext) { + return createSupportedBadge(objectGraph, data, embeddedInRibbon, metricsContext); +} +// MARK: - Private methods +function createBadgeForTV(objectGraph, data, embeddedInRibbon, metricsContext) { + if (objectGraph.host.isTV) { + let badge = null; + const controllerRequirement = gameController.controllerRequirementFromData(objectGraph, data); + let heading = null; + let caption = null; + switch (controllerRequirement) { + case "SIRI_REMOTE_OR_CONTROLLER_REQUIRED": + heading = objectGraph.loc.string("BADGE_SIRI_REMOTE_REQUIRED_OR_CONTROLLER_OPTIONAL_HEADING"); + caption = objectGraph.loc.string("BADGE_MFI_SUPPORTED"); + badge = new models.Badge("siriRemoteOrControllerRequired", "siriRemoteOrControllerRequired", {}, "standard", heading, caption, undefined, undefined, caption, "view"); + break; + case "SIRI_REMOTE_REQUIRED": + heading = objectGraph.loc.string("BADGE_SIRI_REMOTE_REQUIRED_HEADING"); + caption = objectGraph.loc.string("BADGE_MFI_SUPPORTED"); + badge = new models.Badge("siriRemoteRequired", "siriRemoteRequired", {}, "standard", heading, caption, undefined, undefined, caption, "view"); + break; + case "CONTROLLER_REQUIRED": + heading = objectGraph.loc.string("BADGE_MFI_REQUIRED_HEADING"); + caption = objectGraph.loc.string("BADGE_MFI_SUPPORTED"); + badge = new models.Badge("controllerRequired", "controllerRequired", {}, "standard", heading, caption, undefined, undefined, caption, "view"); + break; + case "CONTROLLER_OPTIONAL": + heading = objectGraph.loc.string("BADGE_MFI_HEADING"); + caption = objectGraph.loc.string("BADGE_MFI_SUPPORTED"); + badge = new models.Badge("controllerOptional", "controllerOptional", {}, "standard", heading, caption, undefined, undefined, caption, "view"); + break; + case "NO_BADGE": + break; + default: + break; + } + if (badge) { + badge.clickAction = productPageUtil.isShelfBased(objectGraph) + ? new models.ShelfBasedPageScrollAction("capabilities") + : new models.ProductPageScrollAction(new models.ProductPageSection("shelf", "capabilities")); + } + return badge; + } + else { + return null; + } +} +/// This creates a badge only if the controller type is "CONTROLLER_RECOMMENDED", otherwise no badge will be vended. +/// Should use for iOS only as only iOS have `CONTROLLER_RECOMMENDED` +function createRecommendedControllerBadgeForIOS(objectGraph, data, embeddedInRibbon, metricsContext) { + if (gameController.isGameControllerRecommended(objectGraph, data)) { + let heading; + let caption; + if (!preprocessor.GAMES_TARGET) { + heading = objectGraph.loc.string("BADGE_MFI_HEADING"); + caption = objectGraph.loc.string("ProductPage.Badge.GameController.Recommended"); + } + const badge = new models.Badge(createBadgeType(objectGraph), "controller", {}, "standard", heading, caption, undefined, undefined, caption, "view"); + if (preprocessor.GAMES_TARGET) { + badge.artwork = artwork.createArtworkForResource(objectGraph, "systemimage://gamecontroller.fill"); + } + else { + badge.artwork = createAppStoreControllerArtwork(objectGraph); + } + badge.clickAction = productPageUtil.isShelfBased(objectGraph) + ? new models.ShelfBasedPageScrollAction("capabilities") + : new models.ProductPageScrollAction(new models.ProductPageSection("shelf", "capabilities")); + return badge; + } + else { + return null; + } +} +/// This creates a badge only if the controller type is required, otherwise no badge will be vended. +/// Should use for visionOS only. +function createRequiredControllerBadgeForVisionOS(objectGraph, data, embeddedInRibbon, metricsContext) { + if (gameController.isGameControllerRequired(objectGraph, data)) { + if (gameController.isSpatialControllerRequired(objectGraph, data)) { + return null; + } + let heading; + let caption; + if (!preprocessor.GAMES_TARGET) { + heading = objectGraph.loc.string("BADGE_MFI_HEADING"); + caption = objectGraph.loc.string("ProductPage.Badge.GameController.Required.Caption.v2"); + } + const badge = new models.Badge(createBadgeType(objectGraph), "controller", {}, "standard", heading, caption, undefined, undefined, caption, "view"); + badge.artwork = artwork.createArtworkForResource(objectGraph, "systemimage://gamecontroller.fill"); + badge.clickAction = new models.ShelfBasedPageScrollAction("capabilities"); + return badge; + } + else { + return null; + } +} +/** + * Creates a badge if a controller is supported (but does not consider the recommended or required case). + * + * In iOS, if the feature flag `gameControllerRecommendedRolloutRate` is off, `CONTROLLER_RECOMMENDED` will be consider as supported controller. + */ +function createSupportedBadge(objectGraph, data, embeddedInRibbon, metricsContext) { + if (gameController.isGameControllerSupported(objectGraph, data) && + !gameController.isGameControllerRequired(objectGraph, data) && + !gameController.isGameControllerRecommended(objectGraph, data) && + !gameController.isSpatialControllerRequired(objectGraph, data)) { + if (objectGraph.client.isVision && gameController.isSpatialControllerRequired(objectGraph, data)) { + return null; + } + let heading; + let caption; + if (!preprocessor.GAMES_TARGET) { + heading = objectGraph.loc.string("BADGE_MFI_HEADING"); + caption = objectGraph.client.isVision + ? objectGraph.loc.string("ProductPage.Badge.GameController.Supported.Caption.v2") + : objectGraph.loc.string("BADGE_MFI_SUPPORTED"); + } + const badge = new models.Badge(createBadgeType(objectGraph), "controller", {}, "standard", heading, caption, undefined, undefined, caption, "view"); + if (objectGraph.client.isVision || preprocessor.GAMES_TARGET) { + badge.artwork = artwork.createArtworkForResource(objectGraph, "systemimage://gamecontroller.fill"); + } + else { + badge.artwork = createAppStoreControllerArtwork(objectGraph); + } + badge.clickAction = productPageUtil.isShelfBased(objectGraph) + ? new models.ShelfBasedPageScrollAction("capabilities") + : new models.ProductPageScrollAction(new models.ProductPageSection("shelf", "capabilities")); + return badge; + } + else { + return null; + } +} +function createBadgeType(objectGraph) { + // Be warned, when `artworkControllerBadge` is enabled (as of LuckB), the client no longer supports "controller" as a badge type + return objectGraph.props.enabled("artworkControllerBadge") ? "artwork" : "controller"; +} +function createAppStoreControllerArtwork(objectGraph) { + const controllerIcon = artwork.createArtworkForResource(objectGraph, "systemimage://gamecontroller.fill"); + controllerIcon.imageScale = "small"; + return controllerIcon; +} +//# sourceMappingURL=controller-badge.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/product-page/badges/developer-badge.js b/node_modules/@jet-app/app-store/tmp/src/common/product-page/badges/developer-badge.js new file mode 100644 index 0000000..8f8992d --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/product-page/badges/developer-badge.js @@ -0,0 +1,56 @@ +import * as models from "../../../api/models"; +import * as serverData from "../../../foundation/json-parsing/server-data"; +import * as mediaAttributes from "../../../foundation/media/attributes"; +import * as mediaRelationship from "../../../foundation/media/relationships"; +import { categoryArtworkData } from "../../categories"; +import { makeDeveloperPageIntent } from "../../../api/intents/developer-page-intent"; +import { getLocale } from "../../locale"; +import * as artwork from "../../content/artwork/artwork"; +import * as content from "../../content/content"; +import * as metricsHelpersClicks from "../../metrics/helpers/clicks"; +import * as badgesCommon from "./badges-common"; +export function createBadge(objectGraph, data, embeddedInRibbon, metricsContext) { + const developer = mediaRelationship.relationshipData(objectGraph, data, "developer"); + const heading = objectGraph.loc.string("BADGE_DEVELOPER_HEADING"); + const caption = mediaAttributes.attributeAsString(data, "artistName"); + const style = objectGraph.client.isTV ? "annotation" : "standard"; + const badgeType = objectGraph.client.isTV ? "custom" : "artwork"; + const badge = new models.Badge(badgeType, "developer", {}, style, heading, caption, null, caption, heading, "infer"); + if (embeddedInRibbon && objectGraph.client.deviceType !== "tv") { + const artworkData = categoryArtworkData(objectGraph, developer, true); + // Artwork is required for artwork badges; if it's missing, default to a generic image + if (badgeType === "artwork" && serverData.isNull(artworkData)) { + if (objectGraph.client.isVision) { + badge.artwork = artwork.createArtworkForResource(objectGraph, "systemimage://person.fill"); + } + else { + badge.artwork = artwork.createArtworkForResource(objectGraph, "systemimage://person.crop.square", 26, 26); + } + } + else { + badge.artwork = badgesCommon.badgeArtwork(objectGraph, artworkData); + } + } + const url = content.developerUrlFromDeveloperData(objectGraph, developer); + if (serverData.isDefinedNonNull(url)) { + const action = new models.FlowAction("page"); + action.title = mediaAttributes.attributeAsString(data, "artistName"); + action.pageUrl = url; + if (developer) { + action.destination = makeDeveloperPageIntent({ + ...getLocale(objectGraph), + id: developer.id, + }); + } + metricsHelpersClicks.addClickEventToAction(objectGraph, action, { + targetType: "informationRibbon", + id: developer.id, + actionType: "navigate", + pageInformation: metricsContext.pageInformation, + locationTracker: metricsContext.locationTracker, + }); + badge.clickAction = action; + } + return badge; +} +//# sourceMappingURL=developer-badge.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/product-page/badges/editors-choice-badge.js b/node_modules/@jet-app/app-store/tmp/src/common/product-page/badges/editors-choice-badge.js new file mode 100644 index 0000000..eaf7445 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/product-page/badges/editors-choice-badge.js @@ -0,0 +1,55 @@ +import * as models from "../../../api/models"; +import * as serverData from "../../../foundation/json-parsing/server-data"; +import * as mediaAttributes from "../../../foundation/media/attributes"; +import { createArtworkForResource } from "../../content/artwork/artwork"; +import { supportsVisionOSCompatibleIOSBinaryFromData } from "../../content/content"; +import * as contentAttributes from "../../content/attributes"; +import * as productPageUtil from "../product-page-util"; +export function createBadge(objectGraph, data, embeddedInRibbon, metricsContext) { + const supportsVisionOSCompatibleIOSBinary = supportsVisionOSCompatibleIOSBinaryFromData(objectGraph, data); + if (supportsVisionOSCompatibleIOSBinary) { + return null; + } + const editorialBadgeInfo = contentAttributes.contentAttributeAsDictionary(objectGraph, data, "editorialBadgeInfo"); + if (editorialBadgeInfo) { + const badgeType = serverData.asString(editorialBadgeInfo, "editorialBadgeType"); + if (badgeType && badgeType === "editorialPriority") { + if (objectGraph.client.isVision) { + const caption = objectGraph.loc.string("EDITORS_CHOICE_SINGLE_LINE"); + const badge = new models.Badge("artwork", "editorsChoice", {}, "standard", null, caption, null, null, caption); + badge.artwork = createArtworkForResource(objectGraph, "systemimage://laurel.leading"); + badge.trailingArtwork = createArtworkForResource(objectGraph, "systemimage://laurel.trailing"); + badge.clickAction = new models.ShelfBasedPageScrollAction("productRatings", "notPurchasedRatingsAndReviews", "purchasedRatingsAndReviews", data.id); + return badge; + } + else { + let caption; + const genreNames = mediaAttributes.attributeAsArrayOrEmpty(data, "genreNames"); + if (genreNames[0] === "Games") { + caption = objectGraph.loc.string("EDITORIAL_BADGE_GAMES"); + } + else { + caption = objectGraph.loc.string("EDITORIAL_BADGE_APPS"); + } + let heading; + if (embeddedInRibbon) { + heading = objectGraph.loc.string("EDITORS_CHOICE_RIBBON_HEADING"); + } + else { + heading = objectGraph.loc.string("EDITORS_CHOICE_SINGLE_LINE"); + } + const accessibilityTitle = objectGraph.loc.string("PRODUCT_ACCESSIBILITY_BADGE_EDITORS_CHOICE"); + const badge = new models.Badge("editorsChoice", "editorsChoice", {}, "standard", heading, caption, null, accessibilityTitle, caption); + if (productPageUtil.isShelfBased(objectGraph)) { + badge.clickAction = new models.ShelfBasedPageScrollAction(objectGraph.client.isiOS ? "editorsChoice" : "productRatings"); + } + else { + badge.clickAction = new models.ProductPageScrollAction(new models.ProductPageSection("shelf", "reviews")); + } + return badge; + } + } + } + return null; +} +//# sourceMappingURL=editors-choice-badge.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/product-page/badges/friends-playing-badge.js b/node_modules/@jet-app/app-store/tmp/src/common/product-page/badges/friends-playing-badge.js new file mode 100644 index 0000000..fbb660a --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/product-page/badges/friends-playing-badge.js @@ -0,0 +1,23 @@ +import * as models from "../../../api/models"; +import * as contentAttributes from "../../content/attributes"; +export function createBadge(objectGraph, data, embeddedInRibbon, metricsContext) { + if (objectGraph.client.isTV || objectGraph.client.isVision) { + return null; + } + const isGameCenterEnabled = contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "isGameCenterEnabled"); + if (!isGameCenterEnabled) { + return null; + } + const heading = objectGraph.loc.string("BADGE_FRIENDS_HEADING", "Friends"); + const caption = ""; + const style = "standard"; + const badgeType = "friendsPlaying"; + const badgeKey = "friendsPlaying"; + const badgeContent = { + maxNumberOfAvatarsToDisplay: 3, + maxNumberOfFriendsBeforeTruncation: 99, + adamId: data.id, + }; + return new models.Badge(badgeType, badgeKey, badgeContent, style, heading, caption, null, caption, heading, "infer"); +} +//# sourceMappingURL=friends-playing-badge.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/product-page/badges/language-badge.js b/node_modules/@jet-app/app-store/tmp/src/common/product-page/badges/language-badge.js new file mode 100644 index 0000000..a9e65ef --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/product-page/badges/language-badge.js @@ -0,0 +1,27 @@ +import * as models from "../../../api/models"; +import * as contentAttributes from "../../content/attributes"; +import { primaryLanguageLocaleFromLocales } from "../../content/content"; +export function createBadge(objectGraph, data, embeddedInRibbon, metricsContext) { + const supportedLocales = contentAttributes.contentAttributeAsArrayOrEmpty(objectGraph, data, "supportedLocales"); + const primaryLocale = primaryLanguageLocaleFromLocales(objectGraph, supportedLocales); + if (!primaryLocale) { + return null; + } + const badgeContent = { + paragraphText: primaryLocale.tag, + }; + let caption; + const additionalLanguagesCount = supportedLocales.length - 1; + if (additionalLanguagesCount > 0) { + caption = objectGraph.loc + .stringWithCount("ProductPage.Badge.Language.Caption", additionalLanguagesCount) + .replace("{number_other_languages}", objectGraph.loc.formattedCount(additionalLanguagesCount)); + } + else { + caption = primaryLocale.name; + } + const heading = objectGraph.loc.string("BADGE_LANGUAGE_HEADING"); + const badge = new models.Badge("paragraph", "languages", badgeContent, "standard", heading, caption, null, null, caption, "view"); + return badge; +} +//# sourceMappingURL=language-badge.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/product-page/badges/multiplayer-badge.js b/node_modules/@jet-app/app-store/tmp/src/common/product-page/badges/multiplayer-badge.js new file mode 100644 index 0000000..cbf28f5 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/product-page/badges/multiplayer-badge.js @@ -0,0 +1,63 @@ +import * as models from "../../../api/models"; +import * as serverData from "../../../foundation/json-parsing/server-data"; +import * as contentAttributes from "../../content/attributes"; +export function createBadge(objectGraph, data, embeddedInRibbon, metricsContext) { + if (objectGraph.host.isWatch) { + return null; + } + const minPlayers = contentAttributes.contentAttributeAsNumber(objectGraph, data, "minPlayers"); + const maxPlayers = contentAttributes.contentAttributeAsNumber(objectGraph, data, "maxPlayers"); + if (!serverData.isDefinedNonNull(minPlayers) || !serverData.isDefinedNonNull(maxPlayers)) { + return null; + } + // This can happen if the data is empty or corrupt. + if (minPlayers <= 0 || maxPlayers <= 0 || minPlayers > maxPlayers) { + return null; + } + const heading = objectGraph.loc.string("BADGE_PLAYERS_HEADING"); + const caption = multiplayerBadgeCaptionText(objectGraph, minPlayers, maxPlayers, false); + const accessibilityHeading = multiplayerBadgeCaptionText(objectGraph, minPlayers, maxPlayers, true); + const accessibilityCaption = objectGraph.loc.string("BADGE_PLAYERS_HEADING"); + const badgeContent = { + paragraphText: multiplayerBadgeParagraphText(objectGraph, minPlayers, maxPlayers), + }; + const style = objectGraph.client.isTV ? "annotation" : "standard"; + return new models.Badge("paragraph", "multiplayer", badgeContent, style, heading, caption, null, accessibilityHeading, accessibilityCaption, "view"); +} +export function multiplayerBadgeParagraphText(objectGraph, minPlayers, maxPlayers) { + const maxPlayersToShow = 32; + if (minPlayers === maxPlayers) { + if (minPlayers <= maxPlayersToShow) { + return objectGraph.loc.formattedCount(minPlayers); + } + return objectGraph.loc + .string("BADGE_PLAYERS_CAPTION_MULTIPLAYER_INDEFINITE_MIN_AND_MAX_LIMIT") + .replace("{minPlayers}", objectGraph.loc.formattedCount(maxPlayersToShow)); + } + if (maxPlayers <= maxPlayersToShow) { + // This game can be single player, but also have up to 32 players + return objectGraph.loc + .string("BADGE_PLAYERS_CAPTION_MULTIPLAYER_DEFINITE_LIMIT") + .replace("{minPlayers}", objectGraph.loc.formattedCount(minPlayers)) + .replace("{maxPlayers}", objectGraph.loc.formattedCount(maxPlayers)); + } + // This game can be single player, but also have up to 32 players + return objectGraph.loc + .string("BADGE_PLAYERS_CAPTION_MULTIPLAYER_INDEFINITE_MAX_LIMIT") + .replace("{minPlayers}", objectGraph.loc.formattedCount(minPlayers)) + .replace("{maxPlayers}", objectGraph.loc.formattedCount(maxPlayersToShow)); +} +export function multiplayerBadgeCaptionText(objectGraph, minPlayers, maxPlayers, isAccessibilityCaption) { + if (minPlayers === 1 && maxPlayers === 1) { + return objectGraph.client.isVision + ? objectGraph.loc.string("BADGE_PLAYERS_CAPTION_PLAYER") + : objectGraph.loc.string("BADGE_PLAYERS_CAPTION_SINGLE"); + } + if (minPlayers === 1 || isAccessibilityCaption) { + return objectGraph.client.isVision + ? objectGraph.loc.string("BADGE_PLAYERS_CAPTION_PLAYERS") + : objectGraph.loc.string("BADGE_PLAYERS_CAPTION_MULTIPLAYER"); + } + return objectGraph.loc.string("BADGE_PLAYERS_CAPTION_REQUIRED"); +} +//# sourceMappingURL=multiplayer-badge.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/product-page/badges/rating-badge.js b/node_modules/@jet-app/app-store/tmp/src/common/product-page/badges/rating-badge.js new file mode 100644 index 0000000..b598407 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/product-page/badges/rating-badge.js @@ -0,0 +1,72 @@ +import * as models from "../../../api/models"; +import * as serverData from "../../../foundation/json-parsing/server-data"; +import * as mediaAttributes from "../../../foundation/media/attributes"; +import * as productPageUtil from "../product-page-util"; +import * as reviews from "../reviews"; +export function createBadge(objectGraph, data, embeddedInRibbon, metricsContext) { + // Only show this badge for the ribbon if: + // - the property "ribbonReviewBadge" is enabled (YukonC+) + // - all TV clients (at the time of writing, `embeddedInRibbon` is always `false` for tv, but we have this check here incase that ever changes). + if (embeddedInRibbon && objectGraph.host.isWatch) { + return null; + } + const userRating = mediaAttributes.attributeAsDictionary(data, "userRating"); + let badge = null; + const shouldSuppressReviews = reviews.shouldSuppressReviews(objectGraph, data); + if (!shouldSuppressReviews && userRating && userRating.value && userRating.ratingCount) { + const userRatingValue = serverData.asNumber(userRating, "value"); + const roundedRatingValue = Math.round(userRatingValue * 10) / 10; + const ratingFormatted = objectGraph.loc.decimal(roundedRatingValue, 1); + const ratingCount = serverData.asNumber(userRating, "ratingCount"); + const ratingCountFormatted = objectGraph.loc.formattedCount(ratingCount); + let caption; + if (objectGraph.client.isVision || preprocessor.GAMES_TARGET) { + caption = ratingCountFormatted; + } + else { + caption = objectGraph.loc + .stringWithCount("ProductPage.BadgeRating.Caption.ShortTemplate", ratingCount) + .replace("{count}", ratingCountFormatted); + } + const longCaption = objectGraph.loc + .stringWithCount("ProductPage.BadgeRating.Caption.LongTemplate", ratingCount) + .replace("{count}", ratingCountFormatted) + .replace("{rating}", ratingFormatted); + const accessibilityTitle = objectGraph.loc + .string("PRODUCT_ACCESSIBILITY_BADGE_STAR_RATING") + .replace("{starCount}", ratingFormatted); + badge = new models.Badge("rating", "rating", { + rating: roundedRatingValue, + ratingFormatted: ratingFormatted, + }, "standard", caption, caption, longCaption, accessibilityTitle, caption); + } + else { + // Prevent this item from appearing in the ribbon if there are not enough ratings. + if (embeddedInRibbon) { + return null; + } + const noRatingsCaption = objectGraph.loc.string("BADGE_NO_RATINGS_CAPTION"); + if (shouldSuppressReviews && noRatingsCaption !== "BADGE_NO_RATINGS_CAPTION") { + badge = new models.Badge("rating", "rating", {}, "standard", null, noRatingsCaption, null, noRatingsCaption); + } + else if (!shouldSuppressReviews) { + const caption = objectGraph.loc.string("BADGE_NOT_ENOUGH_RATINGS_CAPTION"); + badge = new models.Badge("rating", "rating", {}, "standard", null, caption, null, caption); + } + } + if (badge) { + let shelfBasedPageScrollAction; + if (objectGraph.client.isVision) { + shelfBasedPageScrollAction = new models.ShelfBasedPageScrollAction("productRatings", "notPurchasedRatingsAndReviews", "purchasedRatingsAndReviews", data.id); + } + else { + shelfBasedPageScrollAction = new models.ShelfBasedPageScrollAction("productRatings"); + } + badge.clickAction = productPageUtil.isShelfBased(objectGraph) + ? shelfBasedPageScrollAction + : new models.ProductPageScrollAction(new models.ProductPageSection("shelf", "reviews")); + return badge; + } + return null; +} +//# sourceMappingURL=rating-badge.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/product-page/badges/size-badge.js b/node_modules/@jet-app/app-store/tmp/src/common/product-page/badges/size-badge.js new file mode 100644 index 0000000..20a7d6b --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/product-page/badges/size-badge.js @@ -0,0 +1,25 @@ +import * as models from "../../../api/models"; +import * as serverData from "../../../foundation/json-parsing/server-data"; +import * as content from "../../content/content"; +import * as sad from "../../content/sad"; +export function createBadge(objectGraph, data, embeddedInRibbon, metricsContext) { + // rdar://63384336 (Size should not appear in SAD apps) + if (sad.systemApps(objectGraph).isSystemAppFromData(data)) { + return null; + } + const combinedFileSize = content.combinedFileSizeFromData(objectGraph, data); + if (serverData.isNull(combinedFileSize) || serverData.isNull(combinedFileSize.fileSizeByDevice)) { + return null; + } + const fileSizeAndUnit = content.fileSizeAndUnitFromCombinedFileSize(objectGraph, combinedFileSize); + if (!fileSizeAndUnit) { + return null; + } + const badgeContent = { + paragraphText: fileSizeAndUnit.size, + }; + const caption = fileSizeAndUnit.unit; + const heading = objectGraph.loc.string("BADGE_SIZE_HEADING"); + return new models.Badge("paragraph", "size", badgeContent, "standard", heading, caption, null, null, caption, "view"); +} +//# sourceMappingURL=size-badge.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/product-page/badges/storefront-content-rating-badge.js b/node_modules/@jet-app/app-store/tmp/src/common/product-page/badges/storefront-content-rating-badge.js new file mode 100644 index 0000000..7567ac2 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/product-page/badges/storefront-content-rating-badge.js @@ -0,0 +1,75 @@ +import { isNothing, isSome } from "@jet/environment"; +import { Badge } from "../../../api/models"; +import { storefrontContentRatingNameForRank, storefrontContentRatingResourceForRank, } from "../../../common/content/content"; +import * as mediaAttributes from "../../../foundation/media/attributes"; +import { createArtworkForResource } from "../../content/artwork/artwork"; +/** + * Creates a storefront-specific content badge for storefronts requiring one. + */ +export function createBadge(objectGraph, data, embeddedInRibbon, metricsContext) { + if (isSome(mediaAttributes.attributeAsDictionary(data, "ageRating"))) { + // rdar://148625156 (Age Ratings & Assurance: iTMS11 shows double Age Rating badges in ribbon for 10791449607) + // This is a temporary crutch while iTMS11 is sometimes returning both + // the old and new age rating models. Sometime after Luck, this whole + // badge can be removed. + return null; + } + // Australia + const auContentRank = mediaAttributes.attributeAsNumber(data, "contentRatingsBySystem.appsAustralia.rank"); + const auContentRating = mediaAttributes.attributeAsString(data, "contentRatingsBySystem.appsAustralia.name"); + if (isSome(auContentRank) && isSome(auContentRating)) { + return createStorefrontContentRatingBadge(objectGraph, embeddedInRibbon, "AU", auContentRank, auContentRating, false); + } + // France + const frContentRank = mediaAttributes.attributeAsNumber(data, "contentRatingsBySystem.appsFrance.rank"); + const frContentRating = mediaAttributes.attributeAsString(data, "contentRatingsBySystem.appsFrance.name"); + if (isSome(frContentRank) && isSome(frContentRating)) { + return createStorefrontContentRatingBadge(objectGraph, embeddedInRibbon, "FR", frContentRank, frContentRating, false); + } + // Korea + const krContentRank = mediaAttributes.attributeAsNumber(data, "contentRatingsBySystem.appsKorea.rank"); + const krContentRating = mediaAttributes.attributeAsString(data, "contentRatingsBySystem.appsKorea.name"); + if (isSome(krContentRank) && isSome(krContentRating)) { + return createStorefrontContentRatingBadge(objectGraph, embeddedInRibbon, "KR", krContentRank, krContentRating, true); + } + return null; +} +function createStorefrontContentRatingBadge(objectGraph, embeddedInRibbon, storefrontCode, storefrontContentRank, storefrontContentRating, needsFullColor) { + const pictogramName = storefrontContentRatingNameForRank(objectGraph, storefrontContentRank); + const pictogramResource = storefrontContentRatingResourceForRank(objectGraph, storefrontContentRank); + if (isNothing(pictogramResource) || isNothing(pictogramName)) { + // Return early if there is no artwork to display for the badge. This + // is the case for the Korea 19+ ranking, or when `rank` is invalid. + return null; + } + const type = "artwork"; + const key = "storefrontContentRating"; + const content = { contentRating: pictogramName }; + const style = objectGraph.client.isTV ? "annotation" : "standard"; + const heading = objectGraph.loc.string(`ProductPage.Badge.AgeRating.${storefrontCode}.Heading`); + let caption; + if (embeddedInRibbon && !objectGraph.client.isVision) { + caption = objectGraph.loc.string("BADGE_AGE_RATING_YEARS"); + } + else if (objectGraph.client.isVision) { + caption = objectGraph.loc.string(`ProductPage.Badge.AgeRating.${storefrontCode}.Heading`); + } + else { + caption = objectGraph.loc.string("BADGE_AGE_RATING"); + } + let axTitle; + if (objectGraph.client.isTV) { + // We just want to show the Korea pictogram on TV with nothing after it. + axTitle = undefined; + } + else { + axTitle = storefrontContentRating; + } + const axCaption = objectGraph.loc.string(`ProductPage.Badge.AgeRating.${storefrontCode}.AX.Caption`); + const badge = new Badge(type, key, content, style, heading, caption, undefined, axTitle, axCaption, "view"); + // Unlike most badges, the Korea pictogram artwork needs to render in full color. + badge.isMonochrome = !needsFullColor; + badge.artwork = createArtworkForResource(objectGraph, `resource://${pictogramResource}`, 26, 26); + return badge; +} +//# sourceMappingURL=storefront-content-rating-badge.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/product-page/banner.js b/node_modules/@jet-app/app-store/tmp/src/common/product-page/banner.js new file mode 100644 index 0000000..3fd9547 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/product-page/banner.js @@ -0,0 +1,460 @@ +import { isSome } from "@jet/environment/types/optional"; +import * as models from "../../api/models"; +import * as serverData from "../../foundation/json-parsing/server-data"; +import * as mediaAttributes from "../../foundation/media/attributes"; +import * as mediaPlatformAttributes from "../../foundation/media/platform-attributes"; +import * as color from "../../foundation/util/color-util"; +import * as contentDeviceFamily from "../content/device-family"; +import * as client from "../../foundation/wrappers/client"; +import * as contentArtwork from "../content/artwork/artwork"; +import * as contentAttributes from "../content/attributes"; +import * as content from "../content/content"; +import * as filtering from "../filtering"; +import * as lockups from "../lockups/lockups"; +import * as metricsHelpersClicks from "../metrics/helpers/clicks"; +import { externalPurchasesLearnMoreAction, externalPurchasesPlacementIsEnabled, hasExternalPurchasesForData, } from "../offers/external-purchases"; +import * as productPageUtil from "../product-page/product-page-util"; +import { createExpandActionForAnnotation } from "./shelves/annotations/annotations"; +import * as compatibilityAnnotation from "./shelves/annotations/compatibility-annotation"; +import { appPlatformTitle } from "../../gameservicesui/src/common/media-preview-platform"; +import { hasRemoteDownloadIdentifiers, supportsVisionPlatformForVisionCompanion } from "../offers/offers"; +/** + * Create a product page banner. + * + * @param data The raw data response for a product page JSON fetch. + * @param bannerContext A collection of any other variables used when creating this banner. + * @returns A banner. + */ +export function create(objectGraph, data, bannerContext) { + let message = null; + let focusedMessage = null; + let action = null; + let fullProductAction = null; + let leadingArtwork = null; + let leadingArtworkTintColor = null; + let includeBackgroundBorder = false; + const bannerKind = null; + // 32-bit only app (disables buy button) + // `requires32bit` is used only for macOS apps, which is why we have two similar flags here + const is32bitOnly = contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "is32bitOnly"); + const requires32bit = contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "requires32bit"); + if (is32bitOnly || requires32bit) { + if (objectGraph.client.isMac) { + message = objectGraph.loc.string("ProductPage.Banner.UpdateRequired.macOS"); + } + else if (objectGraph.client.isVision) { + message = objectGraph.loc.string("ProductPage.Banner.UpdateRequired.Vision"); + } + else { + message = objectGraph.loc.string("ProductPage.Banner.UpdateRequired.iOS"); + } + bannerContext.offerButtonShouldBeDisabled = true; + } + // Is SAD watch-only app + if (message === null && filtering.shouldFilter(objectGraph, data, 1024 /* filtering.Filter.SADWatchApps */)) { + message = objectGraph.loc.string("OFFER_WATCH_ONLY_BANNER"); + bannerContext.offerButtonShouldBeDisabled = true; + } + // Required capabilities mismatch + if (message === null && !lockups.deviceHasCapabilitiesFromData(objectGraph, data)) { + message = objectGraph.loc.string("NOT_COMPATIBLE_BANNER"); + if (preprocessor.GAMES_TARGET) { + message = objectGraph.loc.string("GameDetails.Page.Banner.NotCompatible.Message"); + } + else if (objectGraph.client.isTV) { + // We don't support product page scrolling for this particular banner on TV + message = objectGraph.loc.string("NOT_COMPATIBLE_BANNER_NO_LINK"); + } + else if (objectGraph.client.isVision) { + // For visionOS, we show a sheet rather than scrolling + const annotation = compatibilityAnnotation.createAnnotation(objectGraph, data, false, false, false, null); + if (isSome(annotation)) { + action = createExpandActionForAnnotation(objectGraph, annotation); + if (isSome(action)) { + action.title = objectGraph.loc.string("NOT_COMPATIBLE_BANNER_LINK_TITLE"); + message = objectGraph.loc + .string("NOT_COMPATIBLE_BANNER_TEMPLATE") + .replace("{linkTitle}", action.title); + } + } + bannerContext.offerButtonShouldBeDisabled = true; + } + else if (objectGraph.client.isiOS || objectGraph.client.isMac) { + // For iOS / macOS, add an action that allows users to scroll to the Compatibility item + if (productPageUtil.isShelfBased(objectGraph)) { + const scrollAction = new models.ShelfBasedPageScrollAction("information"); + scrollAction.title = objectGraph.loc.string("NOT_COMPATIBLE_BANNER_LINK_TITLE"); + scrollAction.indexId = "compatibilityAnnotation"; + // Here we use the `fullProductAction` action rather than just `action`, + // as our `ProductPageScrollAction` will only be valid once the full product page has loaded + fullProductAction = scrollAction; + } + else { + const informationSection = new models.ProductPageSection("shelf", "information"); + const scrollAction = new models.ProductPageScrollAction(informationSection); + scrollAction.title = objectGraph.loc.string("NOT_COMPATIBLE_BANNER_LINK_TITLE"); + scrollAction.indexId = "compatibilityAnnotation"; + // Here we use the `fullProductAction` action rather than just `action`, + // as our `ProductPageScrollAction` will only be valid once the full product page has loaded + fullProductAction = scrollAction; + } + message = objectGraph.loc + .string("NOT_COMPATIBLE_BANNER_TEMPLATE") + .replace("{linkTitle}", fullProductAction.title); + bannerContext.offerButtonShouldBeDisabled = true; + } + } + // Filtered system & legacy apps + if (message === null && + filtering.shouldFilter(objectGraph, data, 4 /* filtering.Filter.UnsupportedSystemDeletableApps */ | 32 /* filtering.Filter.LegacyApps */)) { + message = objectGraph.loc.string("UNSUPPORTED_CAPABILITIES"); + bannerContext.offerButtonShouldBeDisabled = true; + } + // App is supported on this OS version + if (filtering.shouldFilter(objectGraph, data, 512 /* filtering.Filter.MinimumOSRequirement */)) { + const minOSVersion = content.minimumOSVersionFromData(objectGraph, data, objectGraph.appleSilicon.isSupportEnabled); + switch (objectGraph.client.deviceType) { + case "mac": + message = objectGraph.loc.string("UNSUPPORTED_MACOS_VERSION").replace("{osVersion}", minOSVersion); + break; + case "phone": + case "pad": + message = objectGraph.loc.string("UNSUPPORTED_IOS_VERSION").replace("{osVersion}", minOSVersion); + break; + case "tv": + message = objectGraph.loc.string("UNSUPPORTED_TVOS_VERSION").replace("{osVersion}", minOSVersion); + break; + case "watch": + const minWatchOSVersion = contentAttributes.contentAttributeAsString(objectGraph, data, "minimumWatchOSVersion"); + message = objectGraph.loc + .string("UNSUPPORTED_WATCHOS_VERSION") + .replace("{osVersion}", minWatchOSVersion); + break; + case "vision": + message = objectGraph.loc.string("UNSUPPORTED_VISIONOS_VERSION").replace("{osVersion}", minOSVersion); + break; + default: + message = objectGraph.loc.string("UNSUPPORTED_CAPABILITIES"); + break; + } + // If the device is a phone but the app is watch-only, use the unsupported watch message. + if (objectGraph.client.isPhone && + contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "isStandaloneForWatchOS")) { + const minWatchOSVersion = contentAttributes.contentAttributeAsString(objectGraph, data, "minimumWatchOSVersion"); + message = objectGraph.loc.string("UNSUPPORTED_WATCHOS_VERSION").replace("{osVersion}", minWatchOSVersion); + } + } + // App is supported on companion's OS version + if (objectGraph.client.isWatch && + filtering.shouldFilter(objectGraph, data, 2048 /* filtering.Filter.MinimumCompanionOSRequirement */)) { + const minOSVersion = mediaPlatformAttributes.platformAttributeAsString(data, contentAttributes.bestAttributePlatformFromData(objectGraph, data), "minimumOSVersion"); + message = objectGraph.loc.string("UNSUPPORTED_IOS_VERSION").replace("{osVersion}", minOSVersion); + } + // If the current device is not supported + if (filtering.shouldFilter(objectGraph, data, 128 /* filtering.Filter.UnsupportedPlatform */)) { + bannerContext.offerButtonShouldBeDisabled = true; + message = objectGraph.loc.string("NOT_COMPATIBLE_BANNER"); + if (preprocessor.GAMES_TARGET) { + const supportedPlatforms = content.supportedAppPlatformsFromData(objectGraph, data); + if (supportedPlatforms.length === 1) { + const supportedPlatform = supportedPlatforms[0]; + const platformTitle = appPlatformTitle(objectGraph, supportedPlatform); + message = objectGraph.localizer.string("GameDetails.Page.Banner.OnlyAvailable.Message", { + platform: platformTitle, + }); + const systemImageName = content.systemImageNameForAppPlatform(supportedPlatform); + leadingArtwork = contentArtwork.createArtworkForResource(objectGraph, `systemimage://${systemImageName}`); + } + else { + message = objectGraph.loc.string("GameDetails.Page.Banner.NotCompatible.Message"); + } + } + else if (objectGraph.client.isTV) { + // We don't support product page scrolling for this particular banner on TV + message = objectGraph.loc.string("NOT_COMPATIBLE_BANNER_NO_LINK"); + } + else if (objectGraph.client.isVision) { + // For visionOS, we show a sheet rather than scrolling + const annotation = compatibilityAnnotation.createAnnotation(objectGraph, data, false, false, false, null); + if (isSome(annotation)) { + action = createExpandActionForAnnotation(objectGraph, annotation); + if (isSome(action)) { + action.title = objectGraph.loc.string("NOT_COMPATIBLE_BANNER_LINK_TITLE"); + message = objectGraph.loc + .string("NOT_COMPATIBLE_BANNER_TEMPLATE") + .replace("{linkTitle}", action.title); + } + } + bannerContext.offerButtonShouldBeDisabled = true; + } + else if (objectGraph.client.isiOS || objectGraph.client.isMac) { + if (productPageUtil.isShelfBased(objectGraph)) { + const scrollAction = new models.ShelfBasedPageScrollAction("information"); + scrollAction.title = objectGraph.loc.string("NOT_COMPATIBLE_BANNER_LINK_TITLE"); + scrollAction.indexId = "compatibilityAnnotation"; + // Here we use the `fullProductAction` action rather than just `action`, + // as our `ProductPageScrollAction` will only be valid once the full product page has loaded + fullProductAction = scrollAction; + message = objectGraph.loc + .string("NOT_COMPATIBLE_BANNER_TEMPLATE") + .replace("{linkTitle}", scrollAction.title); + } + else { + // For iOS / macOS, add an action that allows users to scroll to the Compatibility item + const informationSection = new models.ProductPageSection("shelf", "information"); + const scrollAction = new models.ProductPageScrollAction(informationSection); + scrollAction.title = objectGraph.loc.string("NOT_COMPATIBLE_BANNER_LINK_TITLE"); + scrollAction.indexId = "compatibilityAnnotation"; + // Here we use the `fullProductAction` action rather than just `action`, + // as our `ProductPageScrollAction` will only be valid once the full product page has loaded + fullProductAction = scrollAction; + message = objectGraph.loc + .string("NOT_COMPATIBLE_BANNER_TEMPLATE") + .replace("{linkTitle}", scrollAction.title); + } + } + } + // Perform Rosetta filtering for macOS. + if (filtering.shouldFilter(objectGraph, data, 8192 /* filtering.Filter.MacOSRosetta */)) { + if (productPageUtil.isShelfBased(objectGraph)) { + const scrollAction = new models.ShelfBasedPageScrollAction("information"); + scrollAction.title = objectGraph.loc.string("NOT_COMPATIBLE_BANNER_LINK_TITLE"); + scrollAction.indexId = "compatibilityAnnotation"; + // Here we use the `fullProductAction` action rather than just `action`, + // as our `ProductPageScrollAction` will only be valid once the full product page has loaded + fullProductAction = scrollAction; + } + else { + const informationSection = new models.ProductPageSection("shelf", "information"); + const scrollAction = new models.ProductPageScrollAction(informationSection); + scrollAction.title = objectGraph.loc.string("NOT_COMPATIBLE_BANNER_LINK_TITLE"); + scrollAction.indexId = "compatibilityAnnotation"; + // Here we use the `fullProductAction` action rather than just `action`, + // as our `ProductPageScrollAction` will only be valid once the full product page has loaded + fullProductAction = scrollAction; + } + message = objectGraph.loc + .string("NOT_COMPATIBLE_BANNER_TEMPLATE") + .replace("{linkTitle}", fullProductAction.title); + } + // App is not supported by the current companion, eg. dynamic duo app on a Tinker watch + const isPreorder = mediaAttributes.attributeAsBooleanOrFalse(data, "isPreorder"); + if (content.isUnsupportedByCurrentCompanion(objectGraph, data)) { + if (isPreorder) { + if (objectGraph.host.isWatch) { + message = objectGraph.loc.string("UNSUPPORTED_COMPANION_CONFIGURATION_PREORDER"); + } + else { + message = objectGraph.loc.string("ProductPage.WatchOS.PreOrderRequiresiPhone"); + } + } + else { + message = objectGraph.loc.string("UNSUPPORTED_COMPANION_CONFIGURATION", "Requires iPhone"); + } + bannerContext.offerButtonShouldBeDisabled = true; + } + // App is not supported on paired watch OS version + const minimumWatchOSVersionString = contentAttributes.contentAttributeAsString(objectGraph, data, "minimumWatchOSVersion"); + if (message === null && + bannerContext.clientIdentifierOverride === client.watchIdentifier && + content.isActivePairedWatchOSBelowVersion(objectGraph, minimumWatchOSVersionString)) { + message = objectGraph.loc + .string("ProductPage.Banner.PairedWatchOSVersionBelowMinimum") + .replace("{osVersion}", minimumWatchOSVersionString); + } + /// App requires a visionOS device + const isVisionOnlyApp = contentDeviceFamily.dataOnlyHasDeviceFamily(objectGraph, data, "realityDevice"); + if (isVisionOnlyApp && + !objectGraph.client.isVision && + !objectGraph.client.isWeb && + !preprocessor.GAMES_TARGET && + !objectGraph.client.isCompanionVisionApp) { + const learnMoreTitle = objectGraph.loc.string("BANNER_VISION_ONLY_APP_LEARN_MORE_LINK"); + action = visionOnlyAppLearnMoreAction(objectGraph, learnMoreTitle, bannerContext.metricsPageInformation, bannerContext.metricsLocationTracker); + message = visionOnlyAppMessage(objectGraph, learnMoreTitle, action); + fullProductAction = null; + leadingArtwork = contentArtwork.createArtworkForResource(objectGraph, "systemimage://visionpro"); + leadingArtworkTintColor = color.named("secondaryText"); + includeBackgroundBorder = true; + } + if (supportsVisionPlatformForVisionCompanion(objectGraph, data) && !hasRemoteDownloadIdentifiers(objectGraph)) { + // This case is where we support the download to a visionOS device, but we don't have anything to download to. + bannerContext.offerButtonShouldBeDisabled = true; + message = objectGraph.loc.string("ProductPage.Banner.Companion.VisionDeviceRequired"); + } + else if (objectGraph.client.isCompanionVisionApp && + !supportsVisionPlatformForVisionCompanion(objectGraph, data)) { + // This case is the app doesn't support remote downloads, show this banner. + bannerContext.offerButtonShouldBeDisabled = true; + message = objectGraph.loc.string("ProductPage.Banner.Companion.RemoteDownloadUnavailable"); + } + // External purchases + // If we have no banner, or the offer button is not disabled, give priority to the external purchases banner + const hasExternalPurchases = hasExternalPurchasesForData(objectGraph, data); + const externalPurchasesEnabled = externalPurchasesPlacementIsEnabled(objectGraph, "product-page-banner"); + if ((message === null || !bannerContext.offerButtonShouldBeDisabled) && + hasExternalPurchases && + externalPurchasesEnabled) { + const learnMoreTitle = objectGraph.loc.string("ProductPage.ExternalPurchasesBanner.LearnMore"); + action = externalPurchasesLearnMoreAction(objectGraph, learnMoreTitle, bannerContext.metricsPageInformation, bannerContext.metricsLocationTracker); + message = externalPurchasesMessage(objectGraph, learnMoreTitle, action); + fullProductAction = null; + if (objectGraph.bag.externalPurchasesIncludeProductPageBannerIcon) { + if (objectGraph.bag.externalPurchasesProductPageBannerIconVariant === "secondaryInfoCircle") { + leadingArtwork = contentArtwork.createArtworkForResource(objectGraph, "systemimage://info.circle"); + leadingArtworkTintColor = color.named("secondaryText"); + } + else { + leadingArtwork = contentArtwork.createArtworkForResource(objectGraph, "systemimage://exclamationmark.triangle"); + if (objectGraph.client.isVision) { + leadingArtworkTintColor = color.named("secondaryText"); + } + else { + leadingArtworkTintColor = color.named("systemRed"); + } + } + } + else { + leadingArtwork = null; + leadingArtworkTintColor = null; + } + if (objectGraph.client.isTV) { + if (isSome(objectGraph.bag.externalPurchasesProductPageBannerTextVariant)) { + const key = `ProductPage.ExternalPurchasesBanner.Focused.Variant${objectGraph.bag.externalPurchasesProductPageBannerTextVariant}`; + focusedMessage = objectGraph.loc.string(key); + } + else { + focusedMessage = objectGraph.loc.string("ProductPage.ExternalPurchasesBanner.Focused"); + } + } + includeBackgroundBorder = true; + } + if ((message === null || message === void 0 ? void 0 : message.length) > 0) { + return new models.Banner(message, focusedMessage, action, fullProductAction, leadingArtwork, leadingArtworkTintColor, includeBackgroundBorder, bannerKind); + } + else if (!bannerContext.offerButtonShouldBeDisabled) { + // If no banner or disabled button for above reasons, show an AppState based Banner if needed + const appCapabilities = content.appBinaryTraitsFromData(objectGraph, data); + if (serverData.isDefinedNonNullNonEmpty(appCapabilities)) { + return createExternalBrowserCompatibilityBanner(objectGraph, appCapabilities); + } + } + return null; +} +function createExternalBrowserBanner(appAction, appCapabilities, message) { + return new models.Banner(message, undefined, undefined, undefined, undefined, undefined, undefined, undefined, { + type: "canPerformAppAction", + appAction: appAction, + appCapabilities: appCapabilities, + }); +} +function createExternalBrowserCompatibilityBanner(objectGraph, appCapabilities) { + const message = objectGraph.loc.string("ProductPage.Banner.ExternalBrowser.Message"); + let unknownMessage = message; + let buyMessage = message; + let downloadMessage = message; + let updateMessage = message; + let openMessage = message; + // Debug builds append current app state to banner text for testing. + if (["debug"].includes(objectGraph.client.buildType)) { + unknownMessage += "\n(Internal: unknown)"; + buyMessage += "\n(Internal: buyOrGet)"; + downloadMessage += "\n(Internal: redownload)"; + updateMessage += "\n(Internal: update)"; + openMessage += "\n(Internal: open)"; + } + return new models.AppStateBanner( + /* unknownBanner */ createExternalBrowserBanner("install", appCapabilities, unknownMessage), + /* buyBanner */ createExternalBrowserBanner("install", appCapabilities, buyMessage), + /* downloadBanner */ createExternalBrowserBanner("install", appCapabilities, downloadMessage), + /* updateBanner */ createExternalBrowserBanner("update", appCapabilities, updateMessage), + /* openBanner */ createExternalBrowserBanner("launch", appCapabilities, openMessage)); +} +/** + * Creates the banner message to display for external purchases. + * + * @param objectGraph Current object graph + * @param learnMoreTitle The title for a "Learn More" link + * @param learnMoreAction The action associated with the "Learn More" link, if any + * @returns The built message, which may or ma + */ +function externalPurchasesMessage(objectGraph, learnMoreTitle, learnMoreAction) { + if (learnMoreTitle && + learnMoreAction && + !objectGraph.client.isTV && + !objectGraph.client.isWatch && + !objectGraph.client.isWeb) { + if (objectGraph.client.isMac || objectGraph.client.isVision) { + if (isSome(objectGraph.bag.externalPurchasesProductPageBannerTextVariant)) { + const key = `ProductPage.ExternalPurchasesBanner.WithLink_WithoutNewline.Variant${objectGraph.bag.externalPurchasesProductPageBannerTextVariant}`; + return objectGraph.loc.string(key).replace("{learnMoreLink}", learnMoreTitle); + } + else { + return objectGraph.loc + .string("ProductPage.ExternalPurchasesBanner.WithLink_WithoutNewline") + .replace("{learnMoreLink}", learnMoreTitle); + } + } + else { + if (isSome(objectGraph.bag.externalPurchasesProductPageBannerTextVariant)) { + const key = `ProductPage.ExternalPurchasesBanner.WithLink.Variant${objectGraph.bag.externalPurchasesProductPageBannerTextVariant}`; + return objectGraph.loc.string(key).replace("{learnMoreLink}", learnMoreTitle); + } + else { + return objectGraph.loc + .string("ProductPage.ExternalPurchasesBanner.WithLink") + .replace("{learnMoreLink}", learnMoreTitle); + } + } + } + else { + if (isSome(objectGraph.bag.externalPurchasesProductPageBannerTextVariant)) { + const key = `ProductPage.ExternalPurchasesBanner.NoLink.Variant${objectGraph.bag.externalPurchasesProductPageBannerTextVariant}`; + return objectGraph.loc.string(key); + } + else { + return objectGraph.loc.string("ProductPage.ExternalPurchasesBanner.NoLink"); + } + } +} +/** + * The message to use when viewing a visionOS only app on other platforms. + * @param objectGraph Current object graph + * @param learnMoreTitle Title for the learn more action + * @param learnMoreAction An optional learn more action + * @returns A message string + */ +function visionOnlyAppMessage(objectGraph, learnMoreTitle, learnMoreAction) { + if (learnMoreTitle && learnMoreAction) { + return objectGraph.loc.string("BANNER_VISION_ONLY_APP_WITH_LINK").replace("{learnMoreLink}", learnMoreTitle); + } + else { + return objectGraph.loc.string("BANNER_VISION_ONLY_APP_NO_LINK"); + } +} +/** + * Creates a learn more action, for when viewing a visionOS only app on other platforms. + * @param objectGraph Current object graph + * @param title The title to assign to the action + * @param metricsPageInformation Metrics page info + * @param metricsLocationTracker Metrics location tracker + * @returns A built action + */ +export function visionOnlyAppLearnMoreAction(objectGraph, title, metricsPageInformation, metricsLocationTracker) { + const editorialItemId = objectGraph.bag.visionOnlyAppLearnMoreEditorialItemId; + if (serverData.isNullOrEmpty(editorialItemId)) { + return null; + } + const action = new models.FlowAction("article"); + action.title = title; + action.pageUrl = `https://apps.apple.com/story/id${editorialItemId}`; + metricsHelpersClicks.addClickEventToAction(objectGraph, action, { + id: "LearnMore", + targetType: "link", + actionType: "navigate", + pageInformation: metricsPageInformation, + locationTracker: metricsLocationTracker, + }); + return action; +} +//# sourceMappingURL=banner.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/product-page/bundle-page-common.js b/node_modules/@jet-app/app-store/tmp/src/common/product-page/bundle-page-common.js new file mode 100644 index 0000000..a0852c6 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/product-page/bundle-page-common.js @@ -0,0 +1,19 @@ +import { defaultAdditionalPlatformsForClient, Request } from "../../foundation/media/data-fetching"; +/** + * Generates a {@linkcode Request} to the `app-bundles` Media API endpoint + */ +export function makeBundlePageRequest(objectGraph, intent) { + const mediaApiRequest = new Request(objectGraph) + .withIdOfType(intent.id, "app-bundles") + .includingAdditionalPlatforms(defaultAdditionalPlatformsForClient(objectGraph)) + .includingRelationships(["genres", "developer", "apps", "developer-other-apps", "reviews"]); + if (objectGraph.client.isWeb) { + // Ensure that the "web" client fully hydrates all PDP shelves + mediaApiRequest.addingQuery("sparseLimit[developer-other-apps]", "15"); + if (objectGraph.bag.enableUpdatedAgeRatings) { + mediaApiRequest.includingAttributes(["ageRating"]); + } + } + return mediaApiRequest; +} +//# sourceMappingURL=bundle-page-common.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/product-page/intent-controller-routing.js b/node_modules/@jet-app/app-store/tmp/src/common/product-page/intent-controller-routing.js new file mode 100644 index 0000000..83c76a1 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/product-page/intent-controller-routing.js @@ -0,0 +1,81 @@ +import { makeProductPageIntent as baseMakeProductPageIntent, } from "../../api/intents/product-page-intent"; +import { makeAppEventPageIntent } from "../../api/intents/app-event-page-intent"; +import { generateRoutes } from "../util/generate-routes"; +import { makeSeeAllPageIntent, SEE_ALL_TYPES, } from "../../api/intents/see-all-page-intent"; +import { Path } from "../../foundation/network/url-constants"; +import { normalizePreviewPlaform } from "../../api/models/preview-platform"; +/// MARK: Common Routing Definitions +const PRODUCT_PAGE_URL_PATTERN_WITH_APPNAME = "/app/{appName}/{id}"; +const PRODUCT_PAGE_URL_PATTERN_WITHOUT_APPNAME = "/app/{id}"; +const PRODUCT_PAGE_OPTIONAL_QUERY_PARAMS = { + optionalQuery: ["platform", "ppid", "lic"], +}; +function makeProductPageIntent(opts) { + const { ...clone } = opts; + // The `{appName}` dynamic segment is part of the URL for SEO purposes, but is not actually part + // of the `Intent` since it is not necessary for data-fetching. This data must not end up as + // part of the `Intent` as the value cannot be consistently normalized, which can break important + // caching behavior within the `web` client + delete clone["appName"]; + return baseMakeProductPageIntent(clone); +} +const { routes: productPageRoutesWithAppNameSlug } = generateRoutes( +// @ts-expect-error `generateRoutes` does not handle a typed `Platform` value correctly +makeProductPageIntent, PRODUCT_PAGE_URL_PATTERN_WITH_APPNAME, [], PRODUCT_PAGE_OPTIONAL_QUERY_PARAMS); +const { routes: productPageRoutesWithoutAppNameSlug, makeCanonicalUrl: makeProductPageURLWithoutAppSlug } = generateRoutes(baseMakeProductPageIntent, PRODUCT_PAGE_URL_PATTERN_WITHOUT_APPNAME, [], +// @ts-expect-error `generateRoutes` does not handle a typed `Platform` value correctly +PRODUCT_PAGE_OPTIONAL_QUERY_PARAMS); +// If an "App Clip Demo" URL is hit, we redirect to the product page via the setting of the `canonicalUrl` on `page` +const { routes: appClipDemoPageRoutes } = generateRoutes( +// @ts-expect-error `generateRoutes` does not handle a typed `Platform` value correctly +makeProductPageIntent, "/demo/{id}", [], PRODUCT_PAGE_OPTIONAL_QUERY_PARAMS); +export const productPageRoutes = (objectGraph) => [ + ...productPageRoutesWithAppNameSlug(objectGraph), + ...productPageRoutesWithoutAppNameSlug(objectGraph), + ...appClipDemoPageRoutes(objectGraph), +]; +export { makeProductPageURLWithoutAppSlug }; +function makeAppEventPageIntentFromURLParams(options) { + const { eventid: id, platform, storefront, language } = options; + return makeAppEventPageIntent({ + storefront, + language, + platform, + id, + }); +} +const { routes: appEventPageRoutesWithAppNameSlug } = generateRoutes( +// @ts-expect-error working around type-safety issues in `generateRoutes` +makeAppEventPageIntentFromURLParams, PRODUCT_PAGE_URL_PATTERN_WITH_APPNAME, ["eventid"], PRODUCT_PAGE_OPTIONAL_QUERY_PARAMS); +const { routes: appEventPageRoutesWithoutAppNameSlug } = generateRoutes( +// @ts-expect-error working around type-safety issues in `generateRoutes` +makeAppEventPageIntentFromURLParams, PRODUCT_PAGE_URL_PATTERN_WITHOUT_APPNAME, ["eventid"], PRODUCT_PAGE_OPTIONAL_QUERY_PARAMS); +export const appEventPageRoutes = (objectGraph) => [ + ...appEventPageRoutesWithAppNameSlug(objectGraph), + ...appEventPageRoutesWithoutAppNameSlug(objectGraph), +]; +function makeSeeAllPageIntentFromURL(params) { + const { id, storefront, language, platform } = params; + return makeSeeAllPageIntent({ + id, + storefront, + language, + "platform": normalizePreviewPlaform(platform), + "see-all": params["see-all"], + }); +} +const optionalQueryRuleParams = PRODUCT_PAGE_OPTIONAL_QUERY_PARAMS.optionalQuery.map((p) => `${p}?`); +const allSeeAllRules = []; +SEE_ALL_TYPES.forEach((seeAllType) => { + const queryWithSeeAll = [`see-all=${seeAllType}`, ...optionalQueryRuleParams]; + [PRODUCT_PAGE_URL_PATTERN_WITH_APPNAME, PRODUCT_PAGE_URL_PATTERN_WITHOUT_APPNAME].forEach((pattern) => { + allSeeAllRules.push({ path: pattern, query: queryWithSeeAll }); + allSeeAllRules.push({ path: `/{${Path.storeFront}}${pattern}`, query: queryWithSeeAll }); + }); +}); +const { routes: seeAllPageRoutes, makeCanonicalUrl: makeSeeAllPageURL } = generateRoutes(makeSeeAllPageIntentFromURL, PRODUCT_PAGE_URL_PATTERN_WITHOUT_APPNAME, ["see-all"], { + extraRules: allSeeAllRules, + optionalQuery: ["platform"], +}); +export { seeAllPageRoutes, makeSeeAllPageURL }; +//# sourceMappingURL=intent-controller-routing.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/product-page/product-page-common.js b/node_modules/@jet-app/app-store/tmp/src/common/product-page/product-page-common.js new file mode 100644 index 0000000..d3bc1f2 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/product-page/product-page-common.js @@ -0,0 +1,604 @@ +import * as models from "../../api/models"; +import * as serverData from "../../foundation/json-parsing/server-data"; +import { Request, defaultAdditionalPlatformsForClient } from "../../foundation/media/data-fetching"; +import * as mediaDataStructure from "../../foundation/media/data-structure"; +import { Host, Parameters, Path, Protocol, ShelfRefreshType } from "../../foundation/network/url-constants"; +import * as urls from "../../foundation/network/urls"; +import * as routingComponents from "../../foundation/routing/routing-components"; +import * as color from "../../foundation/util/color-util"; +import * as validation from "@jet/environment/json/validation"; +import * as todayHorizontalCardUtil from "../../common/today/today-horizontal-card-util"; +import * as client from "../../foundation/wrappers/client"; +import * as content from "../content/content"; +import * as lockups from "../lockups/lockups"; +import { addClickEventToSeeAllAction } from "../metrics/helpers/clicks"; +import * as metricsHelpersModels from "../metrics/helpers/models"; +import { addMetricsEventsToPageWithInformation, addPageChangeFieldsToShelfWithInformation, } from "../metrics/helpers/page"; +import { ProductPageShelfMetrics } from "./product-page-shelf-metrics"; +import * as moreByDeveloperShelf from "./shelves/more-by-developer-shelf"; +import * as similarItemsShelf from "./shelves/similar-items-shelf"; +import * as smallStoryCardShelf from "./shelves/small-story-card-shelf"; +// MARK: - Constants +/// Write review action query parameter value +export const writeReviewAction = "write-review"; +/// Reviews action query parameter value +export const reviewsAction = "reviews"; +/// Key for request / response for App Promotion (App Event or Winback offer) data. +export const appPromotionRequirementKey = "appPromotionRequirementKey"; +/// The color to use for shelves that have gray backgrounds. +export const grayShelfBackgroundColor = color.dynamicWith(color.fromHex("F0F0F8"), color.fromHex("303031")); +/** + * The `ProductPageOnDemandShelfType` enum is used to specify the type of shelf that is being rendered via + * secondary lookup + */ +export var ProductPageOnDemandShelfType; +(function (ProductPageOnDemandShelfType) { + ProductPageOnDemandShelfType["MoreByDeveloper"] = "moreByDeveloper"; + ProductPageOnDemandShelfType["SimilarItems"] = "similarItems"; + ProductPageOnDemandShelfType["SmallStory"] = "smallStory"; +})(ProductPageOnDemandShelfType || (ProductPageOnDemandShelfType = {})); +/** + * The `ProductPageShelfToken` type is responsible for plumbing through all + * the data needed to render an incomplete shelf (a shelf that requires a + * lookup) on the product page. + */ +export class ProductPageShelfToken { + // endregion + constructor(productId, remainingItems, title, shouldInferSeeAllFromFetchedItems, capabilities, contentType, offerStyle, spotlightInAppProductIdentifier, refreshUrl, recoMetricsData, supportsArcade, onDemandShelfType) { + //* ************************* + //* Secondary Fetch Properties + //* ************************* + // Whether this is the first render of the shelf. + this.isFirstRender = true; + //* ************************* + //* Bundle Properties + //* ************************* + this.isBundleShelf = false; + //* ************************* + //* Placeholder Properties + //* ************************* + // Whether this shelf is currently showing placeholder cells + // if this is the case we generally use this flag to tell the shelf not to + // merge after the additional fetch + this.showingPlaceholders = false; + this.productId = productId; + this.onDemandShelfType = onDemandShelfType; + this.remainingItems = remainingItems; + this.title = title; + this.shouldInferSeeAllFromFetchesItems = shouldInferSeeAllFromFetchedItems; + this.capabilities = capabilities; + this.contentType = contentType; + this.offerStyle = offerStyle; + this.spotlightInAppProductIdentifier = spotlightInAppProductIdentifier; + this.refreshUrl = refreshUrl; + this.recoMetricsData = recoMetricsData; + this.supportsArcade = supportsArcade; + } +} +/** + * Returns the shelf content URL for a given array of adamIds. + * @param token + * @param sourcePageInformation + * @returns A string containing a URL. + */ +export function shelfContentUrl(objectGraph, token, shelfMetrics, destinationPageInformation) { + if (serverData.isNullOrEmpty(token.remainingItems)) { + return null; + } + return (`${Protocol.internal}:/${Path.product}/${Path.shelf}/` + + encodedShelfToken(token, shelfMetrics, destinationPageInformation)); +} +export function encodedShelfToken(token, shelfMetrics, destinationPageInformation) { + token.sourceSequenceId = shelfMetrics.getSequenceId(); + token.sourcePageInformation = shelfMetrics.metricsPageInformation; + token.sourceLocationTracker = shelfMetrics.locationTracker; + token.destinationPageInformation = destinationPageInformation; + return encodeURIComponent(JSON.stringify(token)); +} +export function lockupsFromDataContainer(objectGraph, dataContainer, shelfMetrics, artworkUseCase, shelfContentType, offerStyle, filter, recoMetricsData, shouldShowOnUnsupportedPlatform) { + return lockupsFromData(objectGraph, dataContainer.data, shelfMetrics, artworkUseCase, shelfContentType, offerStyle, filter, recoMetricsData, shouldShowOnUnsupportedPlatform); +} +export function lockupsFromData(objectGraph, data, shelfMetrics, artworkUseCase, shelfContentType, offerStyle, filter, recoMetricsData, shouldShowOnUnsupportedPlatform) { + if (serverData.isNullOrEmpty(data)) { + return null; + } + const remainingItems = []; + const baseLockupOptions = { + metricsOptions: { + pageInformation: shelfMetrics.metricsPageInformation, + locationTracker: shelfMetrics.locationTracker, + recoMetricsData: recoMetricsData, + }, + artworkUseCase: artworkUseCase, + canDisplayArcadeOfferButton: content.shelfContentTypeCanDisplayArcadeOfferButtons(objectGraph, shelfContentType), + }; + const bundleLockupOptions = { shouldShowSupportedPlatformLabel: shouldShowOnUnsupportedPlatform !== null && shouldShowOnUnsupportedPlatform !== void 0 ? shouldShowOnUnsupportedPlatform : false }; + const styleLockupOptions = serverData.isNull(offerStyle) ? {} : { offerStyle: offerStyle }; + const lockupListOptions = { + shouldShowOnUnsupportedPlatform: shouldShowOnUnsupportedPlatform !== null && shouldShowOnUnsupportedPlatform !== void 0 ? shouldShowOnUnsupportedPlatform : false, + lockupOptions: { ...baseLockupOptions, ...styleLockupOptions, ...bundleLockupOptions }, + shouldConsiderDataPastLastAvailable: true, + contentUnavailable: (_index, dataItem) => { + remainingItems.push(dataItem); + return false; + }, + filter: filter, + }; + /// Maximum number of items allowed in shelves on watchOS. + const watchItemLimit = 3; + const items = lockups.lockupsFromData(objectGraph, data, lockupListOptions); + if (objectGraph.client.isWatch) { + const trimmedItems = items.slice(0, watchItemLimit); + const trimmedRemainingItems = trimmedItems.length < watchItemLimit ? remainingItems.slice(0, watchItemLimit - trimmedItems.length) : []; + return { + items: trimmedItems, + remainingItems: trimmedRemainingItems, + }; + } + else { + return { + items, + remainingItems, + }; + } +} +/** + * Moves the item with the specified id to the front of the lockup items. + * @param id The id (adamId or iAP product identifier) for the lockup to move to the front. + * @param items The list of items in which this lockup may appear. + */ +export function moveLockupToFront(objectGraph, id, items) { + if (!items) { + return; + } + let oldIndex = -1; + let spotlightLockup = null; + items.forEach((item, index) => { + const lockup = item; + const iapLockup = lockup; + const lockupMatchesSpotlightAdamId = lockup && lockup.adamId === id; + const inAppPurchaseMatchesSpotlightIdentifier = iapLockup && iapLockup.productIdentifier === id; + if (lockupMatchesSpotlightAdamId || inAppPurchaseMatchesSpotlightIdentifier) { + oldIndex = index; + spotlightLockup = lockup; + // Set the theme if an iAP + if (iapLockup) { + iapLockup.theme = "spotlight"; + iapLockup.offerDisplayProperties = + iapLockup.offerDisplayProperties.newOfferDisplayPropertiesChangingAppearance(false, "colored", "ad", { type: "blue" }); + } + } + }); + if (oldIndex !== -1) { + items.splice(oldIndex, 1); + items.splice(0, 0, spotlightLockup); + } +} +const supportedCardMediaKinds = ["brandedSingleApp", "grid", "artwork", "video"]; +/** + * Determines whether or not to include the today card on a shelf on the product page. + * + * @param card The today card in question. + * @return `true` if-and-only-if the card should be included on the product page's shelf. + */ +export function includeTodayCardOnProductPage(card) { + if (supportedCardMediaKinds.indexOf(card.media.kind) === -1) { + return false; + } + const flowAction = card.clickAction; + if (!flowAction) { + return true; + } + const urlString = flowAction.pageUrl; + if (!urlString) { + return true; + } + // We drop cards that link back to the same product page. + const url = new urls.URL(urlString); + const productPageRoute = allPageRoutes(); + for (const ruleDefinition of productPageRoute) { + const productPageRule = new routingComponents.UrlRule(ruleDefinition); + if (productPageRule.matches(url)) { + return false; + } + } + return true; +} +export function allPageRoutes() { + return [ + { + protocol: Protocol.https, + path: `/{countryCode}/${Path.product}/{appName}/{id}`, + query: [ + `${Parameters.action}?`, + `${Parameters.offerName}?`, + `${Parameters.isPPT}?`, + `${Parameters.appEventId}?-caseInsensitive`, + `${Parameters.offerItemId}?`, + `${Parameters.isPreloading}?`, + `${Parameters.isViewOnly}?`, + `${Parameters.includeUnlistedApps}?`, + `${Parameters.webBrowser}?`, + ], + }, + { + protocol: Protocol.https, + path: `/{countryCode}/${Path.productBundle}/{appName}/{id}`, + query: [ + `${Parameters.action}?`, + `${Parameters.offerName}?`, + `${Parameters.isPreloading}?`, + `${Parameters.isViewOnly}?`, + `${Parameters.includeUnlistedApps}?`, + ], + }, + { + protocol: Protocol.https, + path: `/{countryCode}/${Path.product}/{id}`, + query: [ + `${Parameters.action}?`, + `${Parameters.offerName}?`, + `${Parameters.appEventId}?-caseInsensitive`, + `${Parameters.offerItemId}?`, + `${Parameters.isPreloading}?`, + `${Parameters.isViewOnly}?`, + `${Parameters.includeUnlistedApps}?`, + `${Parameters.webBrowser}?`, + ], + }, + { + protocol: Protocol.https, + path: `/{countryCode}/${Path.productBundle}/{id}`, + query: [ + `${Parameters.action}?`, + `${Parameters.offerName}?`, + `${Parameters.isPreloading}?`, + `${Parameters.isViewOnly}?`, + `${Parameters.includeUnlistedApps}?`, + ], + }, + { + protocol: Protocol.https, + path: `/${Path.product}/{appName}/{id}`, + query: [ + `${Parameters.action}?`, + `${Parameters.offerName}?`, + `${Parameters.appEventId}?-caseInsensitive`, + `${Parameters.offerItemId}?`, + `${Parameters.isPreloading}?`, + `${Parameters.isViewOnly}?`, + `${Parameters.includeUnlistedApps}?`, + `${Parameters.webBrowser}?`, + ], + }, + { + protocol: Protocol.https, + path: `/${Path.productBundle}/{appName}/{id}`, + query: [ + `${Parameters.action}?`, + `${Parameters.offerName}?`, + `${Parameters.isPreloading}?`, + `${Parameters.isViewOnly}?`, + `${Parameters.includeUnlistedApps}?`, + ], + }, + { + protocol: Protocol.https, + path: `/${Path.product}/{id}`, + query: [ + `${Parameters.action}?`, + `${Parameters.offerName}?`, + `${Parameters.appEventId}?-caseInsensitive`, + `${Parameters.offerItemId}?`, + `${Parameters.isPreloading}?`, + `${Parameters.isViewOnly}?`, + `${Parameters.includeUnlistedApps}?`, + `${Parameters.webBrowser}?`, + ], + }, + { + protocol: Protocol.https, + path: `/${Path.product}/{id}`, + query: [ + `${Parameters.v0}?`, + `${Parameters.metrics}?`, + `${Parameters.isPreloading}?`, + `${Parameters.isViewOnly}?`, + `${Parameters.includeUnlistedApps}?`, + `${Parameters.webBrowser}?`, + ], + }, + { + protocol: Protocol.https, + path: `/${Path.productBundle}/{id}`, + query: [ + `${Parameters.action}?`, + `${Parameters.offerName}?`, + `${Parameters.isPreloading}?`, + `${Parameters.isViewOnly}?`, + `${Parameters.includeUnlistedApps}?`, + ], + }, + { + protocol: Protocol.https, + query: [ + Parameters.bundleIdentifier, + `${Parameters.isPreloading}?`, + `${Parameters.isViewOnly}?`, + `${Parameters.includeUnlistedApps}?`, + `${Parameters.webBrowser}?`, + ], + }, + { + protocol: Protocol.https, + query: [ + Parameters.action, + Parameters.ids, + `${Parameters.isPurchasesApp}?`, + `${Parameters.isPreloading}?`, + `${Parameters.isViewOnly}?`, + `${Parameters.includeUnlistedApps}?`, + ], + }, + { + protocol: Protocol.https, + path: `WebObjects/MZStorePlatform.woa/ra/{apiVersion}/{realm}/catalog/{countryCode}/apps/{id}`, + query: [`${Parameters.isPreloading}?`, `${Parameters.isViewOnly}?`, `${Parameters.includeUnlistedApps}?`], + }, + { + protocol: Protocol.https, + path: `WebObjects/MZStorePlatform.woa/ra/{apiVersion}/{realm}/catalog/{countryCode}/app-bundles/{id}`, + query: [], + }, + { + protocol: Protocol.https, + path: `{apiVersion}/catalog/{countryCode}/apps/{id}`, + query: [ + `${Parameters.isPreloading}?`, + `${Parameters.isViewOnly}?`, + `${Parameters.includeUnlistedApps}?`, + `${Parameters.offerItemId}?`, + ], + }, + { + protocol: Protocol.https, + path: `{apiVersion}/catalog/{countryCode}/app-bundles/{id}`, + query: [`${Parameters.isPreloading}?`, `${Parameters.isViewOnly}?`, `${Parameters.includeUnlistedApps}?`], + }, + { + protocol: Protocol.https, + hostName: `${Host.product}`, + path: `/${Path.siri}/{id}`, + query: [`${Parameters.isPreloading}?`, `${Parameters.isViewOnly}?`, `${Parameters.includeUnlistedApps}?`], + }, + { + protocol: Protocol.https, + path: `/${Path.store}/${Path.viewSoftware}`, + query: [ + Parameters.id, + `${Parameters.v0}?`, + `${Parameters.isPreloading}?`, + `${Parameters.isViewOnly}?`, + `${Parameters.includeUnlistedApps}?`, + ], + }, + { + protocol: Protocol.internal, + path: `/${Path.product}/{id}`, + query: [ + Parameters.invalidateWidgetsOnFailure, + `${Parameters.metrics}?`, + `${Parameters.offerItemId}?`, + `${Parameters.appEventId}?`, + `${Parameters.isPreloading}?`, + `${Parameters.isViewOnly}?`, + `${Parameters.includeUnlistedApps}?`, + `${Parameters.webBrowser}?`, + ], + }, + ]; +} +export function generateShelfRequest(objectGraph, url, parameters) { + const tokenJson = parameters["token"]; + const token = JSON.parse(tokenJson); + // Determine the resource type appropriate for our shelf. + let resourceType; + if (token.isBundleShelf) { + resourceType = "app-bundles"; + } + else { + switch (token.contentType) { + case "smallStoryCard": + case "todayBrick": + case "miniTodayCard": + resourceType = "editorial-items"; + break; + case "inAppPurchaseLockup": + resourceType = "in-apps"; + break; + default: + resourceType = "apps"; + } + } + const attributes = ["editorialArtwork", "editorialVideo", "minimumOSVersion"]; + if (objectGraph.appleSilicon.isSupportEnabled) { + attributes.push("macRequiredCapabilities"); + } + if (objectGraph.client.isMac) { + attributes.push("hasMacIPAPackage"); + } + if (objectGraph.client.isVision) { + attributes.push("compatibilityControllerRequirement"); + } + if (objectGraph.bag.enableUpdatedAgeRatings) { + attributes.push("ageRating"); + } + if (content.shouldUsePrerenderedIconArtwork(objectGraph)) { + attributes.push("iconArtwork"); + } + return new Request(objectGraph) + .withIdsOfType(token.remainingItems.map((item) => item.id), resourceType) + .includingAdditionalPlatforms(defaultAdditionalPlatformsForClient(objectGraph)) + .includingAttributes(resourceType === "in-apps" ? [] : attributes); +} +/** + * Render a shelf for the product page. + * @param objectGraph The object graph. + * @param data The Data received for the shelf. + * @param parameters The Parameters for the shelf. + * @param adResponse A response for the provided ad fetch, if any. + * @param addPageChangeMetrics Whether to attach pageChange metrics to the created shelf. + * @returns + */ +export async function renderShelf(objectGraph, data, parameters, adResponse, addPageChangeMetrics = false) { + const tokenJson = parameters["token"]; + // Parse the shelf token. + // The `iAdInfo` object is constructed manually so that it's a proper `IAdSearchInformation` with callable functions. + const token = JSON.parse(tokenJson, (key, value) => { + if (typeof value === "object" && key === "iAdInfo" && serverData.isDefinedNonNullNonEmpty(value)) { + const iAdInfo = metricsHelpersModels.IAdSearchInformation.from(objectGraph, value); + return iAdInfo; + } + return value; + }); + token.isFirstRender = false; + // Check if the data has reco metrics data. Prefer this over what's in the token, as it's more + // relevant to what we're building. + const recoMetricsData = mediaDataStructure.metricsFromMediaApiObject(data); + token.recoMetricsData = recoMetricsData !== null && recoMetricsData !== void 0 ? recoMetricsData : token.recoMetricsData; + if (parameters[Parameters.shelfRefreshType] === ShelfRefreshType.productPageSimilarItems) { + ProductPageShelfMetrics.resetLocationTrackerForSimilarItemsDuringDownloadShelf(objectGraph, token); + } + const shelf = shelfFromDataContainer(objectGraph, data, token, adResponse); + // TODO: <rdar://problem/39439657> Media API: Product Page Incomplete Shelf Metrics + // shelf.networkTimingMetrics = response[metricsHelpers.timingValues]; + if (addPageChangeMetrics) { + addPageChangeFieldsToShelfWithInformation(objectGraph, shelf, token.sourcePageInformation); + } + // Ensure the refreshUrl is maintained on the completed shelf. + shelf.refreshUrl = token.refreshUrl; + if (parameters[Parameters.shelfRefreshType] === ShelfRefreshType.productPageSimilarItems) { + ProductPageShelfMetrics.addImpressionsFieldsToSimilarItemsDuringDownloadShelf(objectGraph, shelf, token); + } + // Configure 'See All' using fetched items + if (token.shouldInferSeeAllFromFetchesItems) { + const seeAllAction = new models.FlowAction("page"); + seeAllAction.title = objectGraph.loc.string("ACTION_SEE_ALL"); + const seeAllShelf = new models.Shelf(shelf.contentType); + seeAllShelf.items = lockups.shallowCopyLockupsOverridingProperties(objectGraph, shelf.items, "infer", true); + const seeAllPage = new models.GenericPage([seeAllShelf]); + addMetricsEventsToPageWithInformation(objectGraph, seeAllPage, token.destinationPageInformation); + seeAllPage.title = token.title; + seeAllAction.pageData = seeAllPage; + addClickEventToSeeAllAction(objectGraph, seeAllAction, null, { + pageInformation: token.sourcePageInformation, + locationTracker: token.sourceLocationTracker, + }); + shelf.seeAllAction = seeAllAction; + } + if (serverData.isNullOrEmpty(shelf.items) && !token.hasExistingContent) { + shelf.isHidden = true; + } + return shelf; +} +/** + * Configures a shelf from a lookup response. + * + * This function is to be used by 'incomplete' shelves, i.e. shelves that have no + * items but which require a fetch using their URL. + * + * @param dataContainer Data container array with app data. + * @param token Token containing page context. + * @param adResponse A response for the provided ad fetch, if any. + * @return The fully-configured shelf. + */ +function shelfFromDataContainer(objectGraph, dataContainer, token, adResponse) { + return validation.context("shelfFromLookupResponse", () => { + switch (token.onDemandShelfType) { + case ProductPageOnDemandShelfType.SimilarItems: + return similarItemsShelf.createSecondaryShelf(objectGraph, dataContainer.data, token, adResponse); + case ProductPageOnDemandShelfType.MoreByDeveloper: + return moreByDeveloperShelf.createSecondaryShelf(objectGraph, dataContainer.data, token); + case ProductPageOnDemandShelfType.SmallStory: + return smallStoryCardShelf.createSecondaryShelf(objectGraph, dataContainer.data, token); + default: + break; + } + const shelf = new models.Shelf(token.contentType); + // Check if the dataContainer has reco metrics data. Prefer this over what's in the token, as it's more + // relevant to what we're building. + const recoMetricsData = mediaDataStructure.metricsFromMediaApiObject(dataContainer); + const metricsOptions = { + pageInformation: token.sourcePageInformation, + locationTracker: token.sourceLocationTracker, + excludeAttribution: true, + recoMetricsData: recoMetricsData !== null && recoMetricsData !== void 0 ? recoMetricsData : token.recoMetricsData, + }; + let shelfModels; + switch (token.contentType) { + case "todayBrick": + // TODO: <rdar://problem/37505151> Media Api: TODO: Product page inline editorial item shelf + const inlineCards = todayHorizontalCardUtil.featuredInTodayCardsFromData(objectGraph, dataContainer.data, metricsOptions.pageInformation, metricsOptions.locationTracker, includeTodayCardOnProductPage); + if (serverData.isDefinedNonNull(inlineCards)) { + shelfModels = [inlineCards]; + } + break; + default: + const lockupOptions = { + metricsOptions: metricsOptions, + offerStyle: token.offerStyle, + artworkUseCase: content.artworkUseCaseFromShelfStyle(objectGraph, token.contentType), + canDisplayArcadeOfferButton: content.shelfContentTypeCanDisplayArcadeOfferButtons(objectGraph, token.contentType), + }; + if (token.contentType === "inAppPurchaseLockup") { + lockupOptions.skipDefaultClickAction = true; + } + const options = { lockupOptions: lockupOptions }; + if (token.isBundleShelf) { + options.filter = 0 /* filtering.Filter.None */; + } + const lockupModels = lockups.lockupsFromDataContainer(objectGraph, dataContainer, options); + const sortableClients = { [client.watchIdentifier]: "round", [client.messagesIdentifier]: "pill" }; + const sortableStyle = sortableClients[objectGraph.host.clientIdentifier]; + if (sortableStyle) { + lockupModels.sort((a, b) => { + const aIsSortableStyle = a.icon.style === sortableStyle; + const bIsSortableStyle = b.icon.style === sortableStyle; + if (aIsSortableStyle && bIsSortableStyle) { + return 0; + } + else if (aIsSortableStyle && !bIsSortableStyle) { + return -1; + } + else { + return 1; + } + }); + } + const spotlightId = token.spotlightInAppProductIdentifier; + if (spotlightId) { + moveLockupToFront(objectGraph, spotlightId, lockupModels); + } + shelfModels = lockupModels; + break; + } + shelf.items = shelfModels; + shelf.mergeWhenFetched = true; + return shelf; + }); +} +/** + * Returns the *unsupported* set of media kinds for small story card shelf on given platform. + * @param platform Platform to find unsupported media kinds for. + */ +export function filteredMediaCardKindsForSmallStoryCardOnPlatform(platform) { + const filteredMediaKinds = new Set(["appIcon"]); // Policy: No app icon. + if (platform === "macOS") { + filteredMediaKinds.add("brandedSingleApp"); + filteredMediaKinds.add("list"); + filteredMediaKinds.add("inAppPurchase"); + filteredMediaKinds.add("river"); + } + return filteredMediaKinds; +} +//# sourceMappingURL=product-page-common.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/product-page/product-page-shelf-metrics.js b/node_modules/@jet-app/app-store/tmp/src/common/product-page/product-page-shelf-metrics.js new file mode 100644 index 0000000..7e97587 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/product-page/product-page-shelf-metrics.js @@ -0,0 +1,82 @@ +import * as metricsHelpersImpressions from "../metrics/helpers/impressions"; +import * as metricsHelpersLocation from "../metrics/helpers/location"; +export class ProductPageShelfMetrics { + constructor(metricsPageInformation, locationTracker, sequenceId = 0) { + this.sequenceId = 0; + this.metricsPageInformation = metricsPageInformation; + this.locationTracker = locationTracker; + this.sequenceId = sequenceId; + } + addImpressionsToShelf(objectGraph, shelf, targetType, overrideId, overrideIdType, recoMetricsData, title) { + metricsHelpersImpressions.addImpressionFields(objectGraph, shelf, { + id: overrideId !== null && overrideId !== void 0 ? overrideId : `${this.sequenceId}`, + idType: overrideIdType !== null && overrideIdType !== void 0 ? overrideIdType : "sequential", + kind: null, + softwareType: null, + targetType: targetType, + title: title !== null && title !== void 0 ? title : shelf.title, + pageInformation: this.metricsPageInformation, + locationTracker: this.locationTracker, + recoMetricsData: recoMetricsData, + }); + this.sequenceId++; + } + getSequenceId() { + return this.sequenceId; + } + // rdar://55462137 (Metrics: Swoosh "You May Also Like" instrumentation) + addImpressionsFieldsToSimilarItemsShelf(objectGraph, shelf, targetType, idType) { + if (!shelf) { + return; + } + const options = { + id: ProductPageShelfMetrics.similarItemsShelfId, + kind: null, + softwareType: null, + targetType: targetType, + title: shelf.title, + pageInformation: this.metricsPageInformation, + locationTracker: this.locationTracker, + idType: idType, + }; + metricsHelpersImpressions.addImpressionFields(objectGraph, shelf, options); + this.sequenceId++; + } + /** + * Reset the location tracker before creating the YMAL During Download shelf. + * @param objectGraph The object graph. + * @param shelfToken The shelf token being used to build the shelf. + */ + static resetLocationTrackerForSimilarItemsDuringDownloadShelf(objectGraph, shelfToken) { + // Reset the position of the location tracker, as it's based on the old position + const locationTracker = shelfToken.sourceLocationTracker; + metricsHelpersLocation.setCurrentPosition(locationTracker, 0); + metricsHelpersLocation.currentLocation(locationTracker).id = + ProductPageShelfMetrics.similarItemsDuringDownloadShelfId; + } + /** + * Add impressions fields to the YMAL During Download shelf. + * @param objectGraph The object graph. + * @param shelf The created shelf to add impressions fields to. + * @param shelfToken The shelf token used to build the shelf. + */ + static addImpressionsFieldsToSimilarItemsDuringDownloadShelf(objectGraph, shelf, shelfToken) { + if (!shelf) { + return; + } + const options = { + id: ProductPageShelfMetrics.similarItemsDuringDownloadShelfId, + kind: null, + softwareType: null, + targetType: "similarItems", + title: shelfToken.title, + pageInformation: shelfToken.sourcePageInformation, + locationTracker: shelfToken.sourceLocationTracker, + idType: "relationship", + }; + metricsHelpersImpressions.addImpressionFields(objectGraph, shelf, options); + } +} +ProductPageShelfMetrics.similarItemsShelfId = "customers-also-bought-apps"; +ProductPageShelfMetrics.similarItemsDuringDownloadShelfId = "customers-also-bought-apps-download"; +//# sourceMappingURL=product-page-shelf-metrics.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/product-page/product-page-util.js b/node_modules/@jet-app/app-store/tmp/src/common/product-page/product-page-util.js new file mode 100644 index 0000000..3199e81 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/product-page/product-page-util.js @@ -0,0 +1,65 @@ +import * as serverData from "../../foundation/json-parsing/server-data"; +import * as mediaDataStructure from "../../foundation/media/data-structure"; +import * as content from "../content/content"; +import * as productPage from "./product-page"; +import * as shelfBasedProductPage from "./shelf-based/shelf-based-product-page"; +import { Parameters } from "../../foundation/network/url-constants"; +export function isShelfBased(objectGraph) { + return (objectGraph.featureFlags.isEnabled("shelves_2_0_product") || + objectGraph.client.isiOS || + objectGraph.client.isTV || + objectGraph.client.isVision || + objectGraph.client.isWeb || + preprocessor.GAMES_TARGET); +} +export async function createProductPageFromResponse(objectGraph, response, options = {}, additionalPageRequirements = {}, referrerData = null, isForPreloading = false, isViewOnly = false) { + const productData = mediaDataStructure.dataFromDataContainer(objectGraph, response); + const supportsArcade = content.isArcadeSupported(objectGraph, productData); + const shelfBasedPage = await shelfBasedProductPage.createProductPageFromResponse(objectGraph, response, options, additionalPageRequirements, referrerData, isViewOnly); + // In SkyE for PPE we are calling directly into fetchPage and not fetchShelfBasedPage which expects a non-shelf-based + // product page so in the preloading case we should return the non-shelf-based version + // rdar://90971491 (Error: [object Object] is not expected type XD - JS fix) + return isShelfBased(objectGraph) && !isForPreloading + ? shelfBasedPage + : productPage.createProductPageFromShelfBasedProductPage(objectGraph, shelfBasedPage, supportsArcade, options); +} +export function createProductPageSidePackFromResponse(objectGraph, response, options = {}) { + if (!objectGraph.client.isSidepackingEnabled) { + return null; + } + const supportsArcade = content.isArcadeSupported(objectGraph, response); + const shelfBasedPage = shelfBasedProductPage.createProductPageSidePackFromResponse(objectGraph, response, options); + return isShelfBased(objectGraph) + ? shelfBasedPage + : productPage.createProductPageFromShelfBasedProductPage(objectGraph, shelfBasedPage, supportsArcade, options); +} +export function badgesFromInfoRibbonShelf(objectGraph, shelf) { + if (serverData.isNullOrEmpty(shelf === null || shelf === void 0 ? void 0 : shelf.items)) { + return []; + } + if (isShelfBased(objectGraph)) { + return shelf.items; + } + else { + const informationRibbonItems = shelf.items; + if (informationRibbonItems.length > 0) { + return informationRibbonItems[0].badges; + } + else { + return []; + } + } +} +/** + * @param parameters The query params from a page request + * @returns The app event id if it exists + */ +export function appEventIdFromParameters(parameters) { + for (const [paramKey, paramValue] of Object.entries(parameters)) { + if (paramKey.toLocaleLowerCase() === Parameters.appEventId.toLocaleLowerCase()) { + return paramValue; + } + } + return null; +} +//# sourceMappingURL=product-page-util.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/product-page/product-page-variants.js b/node_modules/@jet-app/app-store/tmp/src/common/product-page/product-page-variants.js new file mode 100644 index 0000000..71b4935 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/product-page/product-page-variants.js @@ -0,0 +1,499 @@ +/** + * Builds objects for Product Page Variants (PPV) + * + * PPV has two components: + * - Custom Product Pages: Special versions of product pages with different metadata (e.g. artwork). + * - Product Page Treatments: A/B testing treatments for icon, artwork, etc. + * + * At a high level: + * - Multiple versions of product pages now exist (Default and Custom Pages) + * - Each version of product page **may** have a special A/B testing treatment determined by xp_ab testing cookie. + * + * Example page configurations are: + * - Default page with no treatment. + * - Default page with treatment B + * - Custom page A with treatment C. + * + * There are two signals that determine which variant to show: + * - `meta.cppData`: Describes which CPP to show / link to. May be explicitly requested via `ppid` param, or automagically programmed. + * - `meta.experimentData`: Describes which experiments are running, so client can select the correct treatment. + * + * Based on contents of above, we choose which asset to use for given model. As a rule, we use assets in the following order: + * 1. Assets for CPP (if any) + * 2. Assets for Treatment (if any) + * 3. Default Assets + * i.e. CPP overrides Treatments overrides Default + * + * For 2021, scope is limited to: + * - iOS only + * - CPPs don't have treatments (though we don't care at our layer) + */ +import { asString, isDefinedNonNull, isDefinedNonNullNonEmpty, isNull, isNullOrEmpty, traverse, } from "../../foundation/json-parsing/server-data"; +import { unreachable } from "../../foundation/util/errors"; +import { contentAttributeAsDictionary } from "../content/attributes"; +import { productVariantTreatmentId } from "../../foundation/experimentation/product-page-experiments"; +import { isNothing, isSome } from "@jet/environment/types/optional"; +// region API +/** + * Whether or not custom attributes data should be fetched. This is effectively a feature gate. + * Downstream builder code should parameterize based presence of custom attributes within response, not this flag. + */ +export function shouldFetchCustomAttributes(objectGraph) { + // Custom attributes are used on web or for PPV (iOS Only) + return (objectGraph.client.isWeb || objectGraph.host.isiOS) && objectGraph.bag.enableProductPageVariants; +} +/** + * Returns the app variant data for given `data` and `treatmentGroupId` + * @param data The data for app resource + * @param treatmentGroupId The treatment group to retrieve variant for. (For testing) + */ +export function productVariantDataForData(objectGraph, data, treatmentGroupId) { + if (treatmentGroupId === undefined) { + treatmentGroupId = getClientTreatmentGroupId(objectGraph); + } + if (isNullOrEmpty(data.id)) { + return undefined; + } + const variantData = { + adamID: data.id, + productPageId: defaultIdentifier, + treatmentPageIdMap: { [defaultIdentifier]: defaultIdentifier }, + experimentIdMap: {}, + experimentLocaleMap: {}, + }; + if (isNullOrEmpty(traverse(data, "meta", null))) { + return variantData; // skip if `meta` is empty. + } + /** + * Determine productPageId (CPP or default) + */ + copyCustomProductPageData(objectGraph, variantData, data); + /** + * Determine treatmentPageId or `default` pageId + * + * # Why default, and not `variantData.productPageId` (which can be a CPP id?) + * - At product-level, CPPs DON'T support AB Testing Treatments today. + */ + copyExperimentPageData(objectGraph, variantData, defaultIdentifier, treatmentGroupId, data); + if (!preprocessor.PRODUCTION_BUILD && objectGraph.client.isiOS) { + if (objectGraph.bag.enableAdditionalLoggingForPPV) { + objectGraph.console.log(`[PPV] productVariantDataForData: id: ${data.id} productPageId: ${variantData.productPageId} treatmentPageId[default]: ${variantData.treatmentPageIdMap[defaultIdentifier]}`); + } + } + return variantData; +} +/** + * Copy custom product page data into `ProductVariantData` for given MAPI apps resource. + * @param objectGraph Object graph + * @param variantData Variant data to copy CPP data to + * @param data Apps resource data to source CPP data from + */ +function copyCustomProductPageData(objectGraph, variantData, data) { + const cppData = traverse(data, "meta.cppData", null); + if (isNullOrEmpty(cppData)) { + return; + } + // MAPI checks the validity of the ppid provided, so if it exists, always use it. + const customProductPageId = asString(cppData, "ppid"); + if (isDefinedNonNullNonEmpty(customProductPageId)) { + variantData.productPageId = customProductPageId; + } +} +/** + * Copy treatment page data into `ProductVariantData` for a specified `pageId`. + * @param objectGraph Object graph + * @param variantData Variant data to copy experiment data to + * @param pageId The page id to determine treatment for. (Treatments are unique to each page id) + * @param data Apps resource data to source experiment data from + */ +function copyExperimentPageData(objectGraph, variantData, pageId, treatmentGroupId, data) { + const experimentData = traverse(data, "meta.experimentData", null); + if (isNullOrEmpty(experimentData)) { + return; + } + // Evaluate for specified `pageId`, which may **not** match `variantData.productPageId` + const pageExperimentData = traverse(experimentData, pageId, null); + if (isNullOrEmpty(pageExperimentData)) { + return; + } + const experimentId = asString(pageExperimentData, "experimentId"); + const experimentLocale = asString(pageExperimentData, "experimentLocale"); + let treatmentPageId = null; + const trafficAllocation = traverse(pageExperimentData, "trafficAllocation", null); + if (isDefinedNonNullNonEmpty(trafficAllocation)) { + // Nonpersonalized endpoint. Resolve treatment from traffic allocation + treatmentPageId = matchingTreatmentPageIdFromTrafficAllocation(objectGraph, trafficAllocation, treatmentGroupId); + } + else { + // Personalized endpoint. Infer treatment from thinned variation. + treatmentPageId = matchingTreatmentPageIdFromThinnedCustomAttributes(objectGraph, data, pageId); + } + if (isDefinedNonNullNonEmpty(treatmentPageId) && isDefinedNonNullNonEmpty(experimentId)) { + variantData.treatmentPageIdMap[pageId] = treatmentPageId; + variantData.experimentIdMap[pageId] = experimentId; + if (isDefinedNonNullNonEmpty(experimentLocale)) { + variantData.experimentLocaleMap[pageId] = experimentLocale; + } + } +} +/** + * Selects the variant custom attribute for given key in custom attributes json. + * + * For a given attribute key, e.g. `customArtwork`, and a variant data specifying which product page id and treatment id, + * it looks in the following locations in priority order: + * - customAttributes.{productPageId}.{treatmentPageId} + * - customAttributes.{productPageId}.default + * - customAttributes.default.{treatmentPageId} + * - customAttributes.default.default. + * + * This effectively means: + * - Custom Product Page w/ AB Treatment. + * - Custom Product Page w/ default treament + * - Default Product Page w/ AB Treatment. + * - Default Product Page w/ default treament + * + * @param objectGraph Dependency cocktail + * @param customAttributes A `customAttributes` JSON attribute dict. This is a field in a specific platform attribute, e.g. `platformAttributes.ios.customAttributes` + * @param productVariantData Specifies product page id and treatment id to use. + * @param attributeKey The attribute to find + * @param allowNondefaultTreatmentInNondefaultPage Whether or not to use nondefault treatment can be fetched for nondefault page. Used to limit AB Testing effects on CPPs + * @returns `JSONValue` containing attribute for given key, or `null` + */ +export function variantAttributeForKey(objectGraph, customAttributes, productVariantData, attributeKey, allowNondefaultTreatmentInNondefaultPage) { + if (isNullOrEmpty(customAttributes)) { + return null; + } + // Contains a set of search paths for custom attributes by priority + let searchPaths; + if (productVariantData.productPageId !== defaultIdentifier) { + /** + * `productPageId` is nondefault (CPP). Priorities are: + * - CPP Page Data + * - AB Treatment on Default Page (may be default treatment) IF attribute allows AB testing on CPP (allowNondefaultTreatmentInNondefaultPage). + * - Default Treatment on Default Page + * + * Note that treatment for `productPageId` is skipped today. + */ + const treatmentForDefault = productVariantData.treatmentPageIdMap[defaultIdentifier]; + if (allowNondefaultTreatmentInNondefaultPage) { + // Allow default.treatment after cpp search path. + searchPaths = [ + `${productVariantData.productPageId}.${defaultIdentifier}.${attributeKey}`, + `${defaultIdentifier}.${treatmentForDefault}.${attributeKey}`, + `${defaultIdentifier}.${defaultIdentifier}.${attributeKey}`, + ]; + } + else { + // Skip default.treatment after cpp search path. + searchPaths = [ + `${productVariantData.productPageId}.${defaultIdentifier}.${attributeKey}`, + `${defaultIdentifier}.${defaultIdentifier}.${attributeKey}`, + ]; + } + } + else { + /** + * `productPageId` is default. Priorities are: + * - AB Treatment on Default Page (may be default) + * - Default Treatment on Default Page + */ + const treatmentForDefault = productVariantData.treatmentPageIdMap[defaultIdentifier]; + searchPaths = [ + `${defaultIdentifier}.${treatmentForDefault}.${attributeKey}`, + `${defaultIdentifier}.${defaultIdentifier}.${attributeKey}`, + ]; + } + for (const path of searchPaths) { + const variantAttribute = traverse(customAttributes, path); + if (isDefinedNonNull(variantAttribute)) { + // `variantAttribute` can be an "empty" override, e.g. no screenshots or no video. + return variantAttribute; + } + } + return null; +} +/** + * Extract the product variant ID (a.k.a. `ppid`, a.k.a. Custom Product Page Identiier) from `ProductVariantData` + * + * # Product Variant ID v.s. `productPageId` + * Product Variant ID is the canonical value for `ppid` query param, used to **request** custom variants for apps resource. + * `productPageId` may be equal to product page id, except for when `productPageId` is default which indicates a lack of product variant id. + * + * @param productVariantData The data to get data for. + * @returns `string` or `null` for the product variant id + * @seealso customProductPageIdForData + */ +export function productVariantIDForVariantData(productVariantData) { + // Default is not a valid variant ID. + if (isNothing(productVariantData) || productVariantData.productPageId === defaultIdentifier) { + return null; + } + return productVariantData.productPageId; +} +/** + * Convenience function to retrieve the custom product page id for given `Data` directly + * @param objectGraph Object graph + * @param data The data to get custom product page id for. + * @seealso productVariantIDForVariantData + */ +export function customProductPageIdForData(objectGraph, data) { + const variantData = productVariantDataForData(objectGraph, data); + return productVariantIDForVariantData(variantData); +} +/** + * Extract all available product variant IDs (a.k.a. `ppId` or `cppId`) from the given app data. + * + * @param objectGraph Dependencies all the way down. + * @param data The app data from which to get the available product variant IDs. + * @returns `string[]` for the available product variant ids, or null if there are none. + */ +export function allProductVariantIdsForData(objectGraph, data) { + const customAttributes = contentAttributeAsDictionary(objectGraph, data, "customAttributes"); + if (customAttributes === null || isNullOrEmpty(customAttributes)) { + return null; + } + const keys = Object.keys(customAttributes); + const allProductVariantIds = keys.filter((key) => key !== defaultIdentifier); + return allProductVariantIds; +} +/** + * Determines the treatment of product page to use for given data from a trafficAllocation json. + * This is used on unpersonalized endpoints, where client must resolve traffic allocation manually. + * @param trafficAllocation The traffic allocation for a specific page. + * @param treatmentGroupId The treatment group to retrieve treatment page id for, e.g. "5". This is usually provided by a cookie. + */ +function matchingTreatmentPageIdFromTrafficAllocation(objectGraph, trafficAllocation, treatmentGroupId) { + if (treatmentGroupId === undefined || isNullOrEmpty(treatmentGroupId)) { + return defaultIdentifier; // Default if no AB bucket. + } + /** + * Iterate over traffic allocation map that looks like: + * "85b6a82c-43e6-11eb-b378-0242ac130002": ["1", "19", "51", ...] + * "43de034f-43e6-11eb-b378-0242ac130002": ["2", "34", "55", ...] + */ + for (const [treatmentPageId, includedTreatmentGroups] of Object.entries(trafficAllocation)) { + if (isDefinedNonNullNonEmpty(includedTreatmentGroups) && + includedTreatmentGroups.indexOf(treatmentGroupId) !== -1) { + return treatmentPageId; + } + } + return defaultIdentifier; +} +/** + * Determines the treatment of product page to use for given data based on thinned custom attributes. + * This is used on personalized endpoints, where server can thin the response. + * @param pageExperimentData The experiment data specific a page variant + * @param treatmentGroupId The treatment group to retrieve treatment page id for, e.g. "5". This is usually provided by a cookie. + */ +function matchingTreatmentPageIdFromThinnedCustomAttributes(objectGraph, data, pageId) { + // Traverse keys on `customAttributes` to find treatment page id. + const customAttributesForPage = contentAttributeAsDictionary(objectGraph, data, `customAttributes.${pageId}`); + if (isNullOrEmpty(customAttributesForPage)) { + return defaultIdentifier; + } + const treatmentPageId = Object.keys(customAttributesForPage)[0]; + if (isNullOrEmpty(treatmentPageId)) { + return defaultIdentifier; + } + return treatmentPageId; +} +// endregion +// region Treatment Group ID +/** + * Retrieve the treatment group id, which is a identifier for a traffic bucket the client belongs in. + */ +function getClientTreatmentGroupId(objectGraph) { + /** + * Use xp_ab cookie value + */ + const treatmentId = productVariantTreatmentId(objectGraph); + if (!preprocessor.PRODUCTION_BUILD) { + if (lastSeenClientTreatmentId !== treatmentId) { + objectGraph.console.log("[PPV] Treatment Group ID", treatmentId); + lastSeenClientTreatmentId = treatmentId; + } + } + return treatmentId; +} +// endregion +// region API - Metrics +/** + * Builds the metrics dictionary to add to page fields for a software page w/ product variant features (CPP, AB Testing) + * @param productVariantData The variant data of **page** to build page fields for. + */ +export function pageFieldsForPageInfoProductVariantData(productVariantData) { + const fields = {}; + /** + * Custom Product Page Fields + */ + if (productVariantDataHasVariant(productVariantData, "customProductPage")) { + fields["pageCustomId"] = productVariantData.productPageId; + } + /** + * AB Testing Page Fields. + * Always from "default" (instead of productVariantData.productPageId). + * CPPs don't support AB testing. + */ + const treatmentPageId = productVariantData.treatmentPageIdMap[defaultIdentifier]; + const experimentId = productVariantData.experimentIdMap[defaultIdentifier]; + const experimentLocale = productVariantData.experimentLocaleMap[defaultIdentifier]; + if (productVariantDataHasVariant(productVariantData, "abExperiment")) { + fields["pageVariantId"] = treatmentPageId; + fields["pageExperimentId"] = experimentId; + fields["pageExperimentLocale"] = experimentLocale; + } + return fields; +} +/** + * Builds content field included in impressions and locations metrics for a specific product variant. + * @param productVariantData The variant data of impressionable data to build impression fields for. + */ +export function contentFieldsForProductVariantData(productVariantData) { + const fields = {}; + /** + * Custom Product Page Content Fields. + * Used to describe the Custom Product Page data being presented in a lockup, + * and the Custom Product Page the lockup points to. + */ + if (productVariantDataHasVariant(productVariantData, "customProductPage")) { + fields["pageCustomId"] = productVariantData.productPageId; + } + /** + * AB Testing Content Fields. + * Always from "default" attributes (instead of productVariantData.productPageId). + * CPPs don't support AB testing. + */ + const treatmentPageId = productVariantData.treatmentPageIdMap[defaultIdentifier]; + const experimentId = productVariantData.experimentIdMap[defaultIdentifier]; + const experimentLocale = productVariantData.experimentLocaleMap[defaultIdentifier]; + if (productVariantDataHasVariant(productVariantData, "abExperiment")) { + fields["variantId"] = treatmentPageId; + fields["experimentId"] = experimentId; + fields["experimentLocale"] = experimentLocale; + } + return fields; +} +/** + * Update `buyParams` with PPV metrics fields from the page and content's product variant data + * + * # Why are there two product variant data? + * Consider the following scenario: + * - On Page for App A that has AB tests + * - The page features App B, which also has AB Tests. + * On the given page, there are two AB tests occuring in the page. In the `PurchaseConfiguration` for App A and App B, there are two product variant captured: + * - Page Product Variant Data for App A (Since we're on App A's Product Page) + * - Item Product Variant Data for App A or B, for App A and B respectively. + * + * @param buyParams The buyParam to add product page variant metrics to + * @param adamID The adam id of item being purchased. + * @param pageProductVariantData The product variant data for **PAGE** the purchase is occuring on. May be the same as `itemProductVariantData`. May be undefined. + * @param itemProductVariantData The product variant data for **ITEM** being purchased. May be undefined. + */ +export function addProductPageVariantMetricsToBuyParams(buyParams, adamID, pageProductVariantData, itemProductVariantData) { + /** + * Only add certain page / fields of fields if data is present, adam id matches, and information isn't redundant. + */ + const addPageFields = isDefinedNonNull(pageProductVariantData) && pageProductVariantData.adamID === adamID; + const addContentFields = isDefinedNonNull(itemProductVariantData) && itemProductVariantData.adamID === adamID && !addPageFields; // if we're adding page fields, don't add item variant fields + // Product variant data of **PAGE** to buy params. + if (addPageFields && isDefinedNonNull(pageProductVariantData)) { + const productVariantPageFields = pageFieldsForPageInfoProductVariantData(pageProductVariantData); + for (const key of Object.keys(productVariantPageFields)) { + const value = asString(productVariantPageFields, key); + if (isSome(value)) { + buyParams.set(key, value); + } + } + } + // Product variant data of **ITEM** within a page to buy params. + if (addContentFields && isDefinedNonNull(itemProductVariantData)) { + const productVariantFields = contentFieldsForProductVariantData(itemProductVariantData); + for (const key of Object.keys(productVariantFields)) { + const value = asString(productVariantFields, key); + if (isSome(value)) { + buyParams.set(key, value); + } + } + } +} +/** + * Whether or not product variant data has an custom product page / ab testing variants for it. + * While we always create a `ProductVariantData` to parse the attributes, this is used to determine if the data actually has + * an developer-supplied variant for custom product pages or ab testing experiment from a content perspective. + * + * @param productVariantData The product variant data to check. + * @param variantType The type of variation to query if product variant actually exist for. + * @returns Whether or not specified variant exists for content. + */ +export function productVariantDataHasVariant(productVariantData, variantType) { + if (isNull(productVariantData)) { + return false; + } + switch (variantType) { + case "customProductPage": + return (isDefinedNonNullNonEmpty(productVariantData.productPageId) && + productVariantData.productPageId !== defaultIdentifier); + case "abExperiment": + return isDefinedNonNullNonEmpty(productVariantData.experimentIdMap); + default: + unreachable(variantType); + } +} +/** + * Convenience API for `productVariantDataHasVariant` + * @param objectGraph Dependency soup + * @param data The apps resource to determine whether a variant is present for + * @param variantType The type of variation to query if product variant actually exist for. + * @returns Whether or not specified variant exists for content. + + */ +export function appDataHasVariant(objectGraph, data, variantType) { + const variantData = productVariantDataForData(objectGraph, data); + if (isNothing(variantData)) { + return null; + } + return productVariantDataHasVariant(variantData, variantType); +} +// endregion +// region Requests +/** + * Add PPV specific query parameters necessary to fetch the correct variant in subsequent requests for unhydrated items. + * Specifically this adds: + * &ppid[apps:<appid>]=<cppid> + * for every `Data` resource that had a non-default variant identifier. + * + * This should be called for every `Request` initializer that passes a `Data[]`, found via "new .*Request\(.*s\)" regex. + * + * ## Why does this not live in `Request` initializer? + * It would be ideal to handle this in the initializer for `Request` that accepts `Data[]` (and extracts `ids`), but + * `Request` lives in `foundation` and (tries) to not have feature specific logic, just URL abstractions. + * Adding CPP ID determining logic in `Request` constructor violates dependency rules. + * This can be solved by another layer of abstraction (see `BaseRequest`), but most requests use `Requests` directly today. + * + * @param request The request to modify. + * @param items Items to add PPV query params for. + */ +export function addVariantParametersToRequestForItems(objectGraph, request, items) { + /** + * For catalog requets for `Data[]`, we also need to specify custom variant if it was initially vended with one: + * so the same custom variant is fetched + */ + for (const item of items) { + const cppId = customProductPageIdForData(objectGraph, item); + if (isDefinedNonNull(cppId)) { + request.addingQuery(`ppid[apps:${item.id}]`, `${cppId}`); + } + } +} +// endregion +// region Constants +/** + * A constant to designate a `default` productPageId or treatmentPageId identifier. + */ +const defaultIdentifier = "default"; +/** + * Last seen client treatment group id for logging. + */ +let lastSeenClientTreatmentId; +// endregion Constants +//# sourceMappingURL=product-page-variants.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/product-page/product-page.js b/node_modules/@jet-app/app-store/tmp/src/common/product-page/product-page.js new file mode 100644 index 0000000..06e2851 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/product-page/product-page.js @@ -0,0 +1,225 @@ +// +// product-page.ts +// AppStoreKit +// +// Created by Kevin MacWhinnie on 8/17/16. +// Copyright (c) 2016 Apple Inc. All rights reserved. +// +import * as validation from "@jet/environment/json/validation"; +import * as models from "../../api/models"; +import * as serverData from "../../foundation/json-parsing/server-data"; +import * as productPageUtil from "./product-page-util"; +import { sectionTitleForPlatform } from "./reviews"; +import { legacyProductPageSectionsFromMappings } from "./shelf-based/product-page-section-mapping"; +import * as productOrdering from "./shelf-based/product-page-shelf-ordering/product-page-shelf-ordering"; +import * as productMediaShelf from "./shelves/product-media-shelf"; +/** + * Creates a non-ShelfBasedProductPage from a ShelfBased on + * @param objectGraph + * @param shelfBasedProductPageModel + * @param isArcadeApp + * @param options + */ +export function createProductPageFromShelfBasedProductPage(objectGraph, shelfBasedProductPageModel, isArcadeApp, options) { + return validation.context("createProductPageFromShelfBasedProductPage", () => { + var _a, _b, _c, _d; + const productPage = new models.ProductPage(); + // Lockup Level Properties + productPage.adamId = shelfBasedProductPageModel.lockup.adamId; + productPage.bundleId = shelfBasedProductPageModel.lockup.bundleId; + productPage.icon = shelfBasedProductPageModel.lockup.icon; + productPage.isEditorsChoice = shelfBasedProductPageModel.lockup.isEditorsChoice; + productPage.ordinal = shelfBasedProductPageModel.lockup.ordinal; + productPage.title = shelfBasedProductPageModel.lockup.title; + productPage.subtitle = shelfBasedProductPageModel.lockup.subtitle; + productPage.developerTagline = shelfBasedProductPageModel.lockup.developerTagline; + productPage.editorialTagline = shelfBasedProductPageModel.lockup.editorialTagline; + productPage.editorialDescription = shelfBasedProductPageModel.lockup.editorialDescription; + productPage.shortEditorialDescription = shelfBasedProductPageModel.lockup.shortEditorialDescription; + productPage.rating = shelfBasedProductPageModel.lockup.rating; + productPage.ratingCount = shelfBasedProductPageModel.lockup.ratingCount; + productPage.ageRating = shelfBasedProductPageModel.lockup.ageRating; + productPage.buttonAction = shelfBasedProductPageModel.lockup.buttonAction; + productPage.offerDisplayProperties = shelfBasedProductPageModel.lockup.offerDisplayProperties; + productPage.titleOfferDisplayProperties = shelfBasedProductPageModel.titleOfferDisplayProperties; + productPage.clickAction = shelfBasedProductPageModel.lockup.clickAction; + productPage.children = shelfBasedProductPageModel.lockup.children; + productPage.contextMenuData = shelfBasedProductPageModel.lockup.contextMenuData; + productPage.shelfBackground = shelfBasedProductPageModel.lockup.shelfBackground; + productPage.searchAd = shelfBasedProductPageModel.lockup.searchAd; + productPage.searchAdOpportunity = shelfBasedProductPageModel.lockup.searchAdOpportunity; + productPage.crossLinkTitle = shelfBasedProductPageModel.lockup.crossLinkTitle; + productPage.crossLinkSubtitle = shelfBasedProductPageModel.lockup.crossLinkSubtitle; + productPage.tertiaryTitle = shelfBasedProductPageModel.lockup.tertiaryTitle; + productPage.tertiaryTitleAction = shelfBasedProductPageModel.lockup.tertiaryTitleAction; + productPage.tertiaryTitleArtwork = shelfBasedProductPageModel.lockup.tertiaryTitleArtwork; + productPage.flowPreviewActionsConfiguration = shelfBasedProductPageModel.lockup.flowPreviewActionsConfiguration; + productPage.productDescription = shelfBasedProductPageModel.lockup.productDescription; + productPage.itemBackground = shelfBasedProductPageModel.lockup.itemBackground; + // Page Level Properties + productPage.developerAction = shelfBasedProductPageModel.developerAction; + productPage.ageRatingAction = shelfBasedProductPageModel.ageRatingAction; + productPage.logoArtwork = shelfBasedProductPageModel.logoArtwork; + productPage.navigationBarIconArtwork = shelfBasedProductPageModel.navigationBarIconArtwork; + productPage.uberArtwork = (_a = shelfBasedProductPageModel.uber) === null || _a === void 0 ? void 0 : _a.artwork; + productPage.uberArtworkForCompactDisplay = (_b = shelfBasedProductPageModel.uber) === null || _b === void 0 ? void 0 : _b.compactArtwork; + productPage.uberVideo = (_c = shelfBasedProductPageModel.uber) === null || _c === void 0 ? void 0 : _c.video; + productPage.uberStyle = (_d = shelfBasedProductPageModel.uber) === null || _d === void 0 ? void 0 : _d.style; + productPage.media = allProductMediaFromShelfBasedProductPage(shelfBasedProductPageModel); + if (serverData.isDefinedNonNull(shelfBasedProductPageModel.shelfMapping["informationRibbon"])) { + productPage.badges = productPageUtil.badgesFromInfoRibbonShelf(objectGraph, shelfBasedProductPageModel.shelfMapping["informationRibbon"]); + } + else if (objectGraph.host.isWatch || objectGraph.host.isTV) { + // copy sidepack badge for watch and tv. + productPage.badges = shelfBasedProductPageModel.badges; + } + productPage.shareAction = shelfBasedProductPageModel.shareAction; + productPage.pageMetrics = shelfBasedProductPageModel.pageMetrics; + productPage.pageRenderMetrics = shelfBasedProductPageModel.pageRenderMetrics; + productPage.isComplete = !shelfBasedProductPageModel.isIncomplete; + productPage.hasDarkUserInterfaceStyle = shelfBasedProductPageModel.hasDarkUserInterfaceStyle; + productPage.mediaSectionTitle = objectGraph.client.isTV + ? null + : objectGraph.loc.string("ProductPage.Section.ScreenshotsPreview.Title"); + productPage.expandedOfferDetails = shelfBasedProductPageModel.expandedOfferDetails; + productPage.regularPriceFormatted = shelfBasedProductPageModel.regularPriceFormatted; + productPage.theme = shelfBasedProductPageModel.theme; + productPage.externalVersionIdentifier = shelfBasedProductPageModel.externalVersionIdentifier; + productPage.updateBuyParams = shelfBasedProductPageModel.updateBuyParams; + productPage.appPlatforms = shelfBasedProductPageModel.appPlatforms; + productPage.descriptionHeader = shelfBasedProductPageModel.descriptionHeader; + productPage.description = shelfBasedProductPageModel.description; + productPage.banner = shelfBasedProductPageModel.banner; + productPage.secondaryBanner = shelfBasedProductPageModel.secondaryBanner; + productPage.fullProductFetchedAction = shelfBasedProductPageModel.fullProductFetchedAction; + productPage.appPromotionDetailPageFlowAction = shelfBasedProductPageModel.appPromotionDetailPageFlowAction; + productPage.pageRefreshPolicy = shelfBasedProductPageModel.pageRefreshPolicy; + productPage.purchasedOrdering = legacyProductPageSectionsFromMappings(objectGraph, productOrdering.sectionOrdering(objectGraph, true, objectGraph.host.platform, isArcadeApp, false, false, options)); + productPage.notPurchasedOrdering = legacyProductPageSectionsFromMappings(objectGraph, productOrdering.sectionOrdering(objectGraph, false, objectGraph.host.platform, isArcadeApp, false, false, options)); + productPage.shelfMapping = productShelfMappingFromShelfBasedProductPage(objectGraph, shelfBasedProductPageModel); + productPage.alwaysAllowReviews = shelfBasedProductPageModel.alwaysAllowReviews; + return productPage; + }); +} +function productShelfMappingFromShelfBasedProductPage(objectGraph, shelfBasedPage) { + const shelfMapping = {}; + for (const knownShelfId of models.legacyProductPageKnownShelfIds) { + let shelf; + if (knownShelfId === "reviews") { + shelf = productReviewsShelfFromShelfBasedProductPage(objectGraph, shelfBasedPage); + } + else { + shelf = shelfBasedPage.shelfMapping[knownShelfId]; + } + if (serverData.isDefinedNonNullNonEmpty(shelf)) { + shelfMapping[knownShelfId] = shelf; + } + } + return shelfMapping; +} +export function allProductMediaFromShelfBasedProductPage(shelfBasedPage) { + const productMedia = []; + for (const shelf of Object.values(shelfBasedPage.shelfMapping)) { + if (productMediaShelf.isProductMediaShelf(shelf)) { + const productMediaMetaData = shelf.contentsMetadata; + const productMediaItems = shelf.items; + productMedia.push(new models.ProductMedia(productMediaItems, productMediaMetaData.platform, productMediaMetaData.allPlatforms, productMediaMetaData.platformDescription, productMediaMetaData.allPlatformsDescription, productMediaMetaData.allPlatformsDescriptionPlacement)); + } + } + return productMedia; +} +export function mediaPreviewContentFromShelfBasedProductPage(shelfBasedPage) { + const content = []; + for (const shelf of Object.values(shelfBasedPage.shelfMapping)) { + if (productMediaShelf.isProductMediaShelf(shelf)) { + const productMediaMetaData = shelf.contentsMetadata; + const productMediaItems = shelf.items; + const media = new models.ProductMedia(productMediaItems, productMediaMetaData.platform, productMediaMetaData.allPlatforms, productMediaMetaData.platformDescription, productMediaMetaData.allPlatformsDescription, productMediaMetaData.allPlatformsDescriptionPlacement); + content.push({ + metaData: productMediaMetaData, + media: media, + }); + } + } + return content; +} +function productReviewsShelfFromShelfBasedProductPage(objectGraph, shelfBasedPage) { + const productRatingsShelf = shelfBasedPage.shelfMapping["productRatings"]; + const allProductReviewsShelf = shelfBasedPage.shelfMapping["allProductReviews"]; + const editorsChoiceProductReviewsShelf = objectGraph.client.isiOS + ? shelfBasedPage.shelfMapping["editorsChoice"] + : shelfBasedPage.shelfMapping["editorsChoiceProductReviews"]; + const tapToRateProductReviewActionShelf = shelfBasedPage.shelfMapping["tapToRateProductReviewAction"]; + const writeAReviewProductReviewActionShelf = shelfBasedPage.shelfMapping["writeAReviewProductReviewAction"]; + if (serverData.isNullOrEmpty(productRatingsShelf) && + serverData.isNullOrEmpty(allProductReviewsShelf) && + serverData.isNullOrEmpty(editorsChoiceProductReviewsShelf) && + serverData.isNullOrEmpty(tapToRateProductReviewActionShelf) && + serverData.isNullOrEmpty(writeAReviewProductReviewActionShelf)) { + return null; + } + let editorsChoiceReview = null; + let userReviews = []; + let ratings = null; + let tapToRate = null; + let writeReviewAction = null; + let supportAction = null; + let seeAllAction = null; + if (serverData.isDefinedNonNullNonEmpty(allProductReviewsShelf === null || allProductReviewsShelf === void 0 ? void 0 : allProductReviewsShelf.items)) { + for (const item of allProductReviewsShelf.items) { + const productReview = item; + switch (productReview.sourceType) { + case "editorsChoice": + editorsChoiceReview = productReview.review; + break; + case "user": + userReviews.push(productReview.review); + break; + default: + break; + } + } + } + else { + userReviews = null; + } + if (serverData.isDefinedNonNullNonEmpty(editorsChoiceProductReviewsShelf === null || editorsChoiceProductReviewsShelf === void 0 ? void 0 : editorsChoiceProductReviewsShelf.items)) { + if (objectGraph.client.isiOS) { + // On iOS, Editors' Choice is a separate shelf to reviews. On all other platforms, editors choice is embedded in the reviews list. + editorsChoiceReview = editorsChoiceProductReviewsShelf.items[0]; + } + else { + const productReview = editorsChoiceProductReviewsShelf.items[0]; + editorsChoiceReview = productReview.review; + } + } + if (serverData.isDefinedNonNullNonEmpty(tapToRateProductReviewActionShelf === null || tapToRateProductReviewActionShelf === void 0 ? void 0 : tapToRateProductReviewActionShelf.items)) { + const productReviewAction = tapToRateProductReviewActionShelf.items[0]; + tapToRate = productReviewAction.action; + } + if (serverData.isDefinedNonNullNonEmpty(writeAReviewProductReviewActionShelf === null || writeAReviewProductReviewActionShelf === void 0 ? void 0 : writeAReviewProductReviewActionShelf.items)) { + const productReviewAction = writeAReviewProductReviewActionShelf.items[0]; + const writeAReview = productReviewAction.action; + writeReviewAction = writeAReview.writeReviewAction; + supportAction = writeAReview.supportAction; + } + if (serverData.isDefinedNonNullNonEmpty(productRatingsShelf === null || productRatingsShelf === void 0 ? void 0 : productRatingsShelf.items)) { + ratings = productRatingsShelf.items[0]; + seeAllAction = productRatingsShelf === null || productRatingsShelf === void 0 ? void 0 : productRatingsShelf.seeAllAction; + } + const reviewsContainer = new models.ReviewsContainer(); + reviewsContainer.adamId = shelfBasedPage.lockup.adamId; + reviewsContainer.ratings = ratings; + reviewsContainer.tapToRate = tapToRate; + reviewsContainer.editorsChoice = editorsChoiceReview; + reviewsContainer.reviews = userReviews; + reviewsContainer.supportAction = supportAction; + reviewsContainer.writeReviewAction = writeReviewAction; + reviewsContainer.alwaysAllowReviews = shelfBasedPage.alwaysAllowReviews; + const reviewsShelf = new models.Shelf("reviewsContainer"); + reviewsShelf.title = sectionTitleForPlatform(objectGraph); + reviewsShelf.items = [reviewsContainer]; + reviewsShelf.seeAllAction = seeAllAction; + return reviewsShelf; +} +//# sourceMappingURL=product-page.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/product-page/reviews.js b/node_modules/@jet-app/app-store/tmp/src/common/product-page/reviews.js new file mode 100644 index 0000000..26ac212 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/product-page/reviews.js @@ -0,0 +1,1077 @@ +/** + * Created by km on 10/14/16. + */ +import * as validation from "@jet/environment/json/validation"; +import * as models from "../../api/models"; +import * as serverData from "../../foundation/json-parsing/server-data"; +import * as mediaAttributes from "../../foundation/media/attributes"; +import * as mediaDataStructure from "../../foundation/media/data-structure"; +import * as mediaRelationship from "../../foundation/media/relationships"; +import { Path, Protocol, Parameters } from "../../foundation/network/url-constants"; +import * as urls from "../../foundation/network/urls"; +import * as objects from "../../foundation/util/objects"; +import * as contentAttributes from "../content/attributes"; +import * as sad from "../content/sad"; +import * as metricsHelpersLocation from "../metrics/helpers/location"; +import * as metricsHelpersImpressions from "../metrics/helpers/impressions"; +import * as pagination from "../builders/pagination"; +import * as artworkBuilder from "../content/artwork/artwork"; +import { supportedAppPlatformsFromData } from "../content/content"; +import * as flowPreview from "../content/flow-preview"; +import * as productPageUtil from "./product-page-util"; +import { createProductReviewActions } from "./shelves/shelf-based-reviews-shelves"; +import { flowPreviewActionsConfigurationForReviewSummaryFromData } from "../content/flow-preview"; +import { isNothing, isSome, unwrapOptional } from "@jet/environment"; +import { createEditorsChoiceModel } from "./shelves/editors-choice-shelf"; +import { subtitleFromData } from "../lockups/lockups"; +import { makeProductPageIntent } from "../../api/intents/product-page-intent"; +import { getPlatform } from "../preview-platform"; +import { getLocale } from "../locale"; +import { actionFor } from "../../foundation/runtime/action-provider"; +import { makeSeeAllPageIntent } from "../../api/intents/see-all-page-intent"; +import { makeSeeAllPageURL } from "./intent-controller-routing"; +import * as metricsHelpersClicks from "../metrics/helpers/clicks"; +const displayableKindMobileSoftware = "11"; +const displayableKindMobileSoftwareBundle = "43"; +const displayableKindMacSoftware = "30"; +const displayableKindMacSoftwareBundle = "44"; +// Mapping of displayable-kind from software to bundle +const softwareToBundleKindMap = { + [displayableKindMobileSoftware]: displayableKindMobileSoftwareBundle, + [displayableKindMacSoftware]: displayableKindMacSoftwareBundle, +}; +/// The default sort id to use, `Most Helpful`. +const sortIdDefault = "helpful"; +/** + * Token used to construct a personalized reviews shelf. + */ +export class PersonalizedReviewsShelfToken { +} +/** + * Creates a pagination url for reviews pagination. + */ +export function reviewsPaginationUrl(objectGraph, adamId, sort) { + return `${Protocol.internal}:/${Path.reviews}/${Path.shelf}/${adamId}/${sort}`; +} +/** + * Creates a page url for changing sorting order. + */ +export function sortedReviewsPageUrl(objectGraph, adamId, sort, sortedReviewsPageToken = null) { + let url = `${Protocol.internal}:/${Path.reviews}/${adamId}/${sort}`; + if (serverData.isDefinedNonNullNonEmpty(sortedReviewsPageToken)) { + url = `${url}/?${Parameters.token}=${encodeURIComponent(JSON.stringify(sortedReviewsPageToken))}`; + } + return url; +} +/** + * Creates a pagination token. + */ +export function createReviewsPageToken(objectGraph, adamId, nextHref, sort, remainingContent) { + return { + url: reviewsPaginationUrl(objectGraph, adamId, sort), + remainingContent: remainingContent, + nextHref: nextHref, + profile: "lockup", + maxPerPage: pagination.suggestedMaxPerPage, + highestOrdinal: 0, + metricsPageInformation: null, + metricsLocationTracker: null, + }; +} +/** + * Whether reviews should be suppressed for the given app. + * + * @param {Data} data MAPI data blob describing the app. + * @returns {boolean} Returns `true` if reviews are restricted. + */ +export function shouldSuppressReviews(objectGraph, data) { + if (!serverData.isDefinedNonNull(data)) { + return false; + } + // If we have a system deletable and force-1st-party-app-review bag key + const allowsReviewsForSADApp = shouldAllowReviewsForSADApp(objectGraph, data); + if (allowsReviewsForSADApp) { + return false; + } + // Suppress reviews for first party apps + const isFirstPartyHideableApp = mediaAttributes.attributeAsBooleanOrFalse(data, "isFirstPartyHideableApp"); + if (isFirstPartyHideableApp) { + return true; + } + // Suppress reviews for restricted apps + const areReviewsRestricted = mediaAttributes.attributeAsBooleanOrFalse(data, "reviewsRestricted"); + if (areReviewsRestricted) { + return true; + } + return false; +} +/** + * Whether reviews should be allowed for the given app if it is a 1st party/SAD app. + * + * @param {Data} data MAPI data blob describing the app. + * @returns {boolean} Returns `true` if the app is a 1st party/SAD app and current bag has reviews enabled for 1st party/SAD apps. + */ +export function shouldAllowReviewsForSADApp(objectGraph, data) { + if (serverData.isNullOrEmpty(data) || !objectGraph.bag.enableSystemAppReviews) { + return false; + } + // Since the bag for watch and iOS have different apps included as SAD apps, it is possible that we are not showing reviews for the following scenario: + // The watch is direct linked to an app that is iOS only. This means that is should be SAD app from the iOS side, but is not a SAD app on the watch bag, + // so it doesn't qualify for reviews. However, these apps have the metadata tag of "isFirstPartyHideableApp" that we will use to circumvent this limitation. + // For watch, we will see if an app is a SAD app for the watch, or if an app is technically a SAD app for the iPhone we are viewing on the watch. + if (objectGraph.client.isWatch) { + return (sad.systemApps(objectGraph).isSystemAppFromData(data) || + mediaAttributes.attributeAsBooleanOrFalse(data, "isFirstPartyHideableApp")); + } + else { + return sad.systemApps(objectGraph).isSystemAppFromData(data); + } +} +/** + * Whether reviews should be allowed for a given app, this returns true for all standard apps, for system apps + * this will return true only if system app reviews are enabled. For non-shelf-based product pages we return `true` because + * this logic is handled natively. + * + * @param objectGraph + * @param {Data} data MAPI data blob describing the app. + * @returns {boolean} Returns `true` if the app is not a system app or if system app reviews are allowed. + */ +export function isAppReviewable(objectGraph, data) { + if (productPageUtil.isShelfBased(objectGraph)) { + return true; + } + if (serverData.isNullOrEmpty(data)) { + return false; + } + let isSystemApp; + // Since the bag for watch and iOS have different apps included as SAD apps, it is possible that we are not showing reviews for the following scenario: + // The watch is direct linked to an app that is iOS only. This means that is should be SAD app from the iOS side, but is not a SAD app on the watch bag, + // so it doesn't qualify for reviews. However, these apps have the metadata tag of "isFirstPartyHideableApp" that we will use to circumvent this limitation. + // For watch, we will see if an app is a SAD app for the watch, or if an app is technically a SAD app for the iPhone we are viewing on the watch. + if (objectGraph.client.isWatch) { + isSystemApp = + sad.systemApps(objectGraph).isSystemAppFromData(data) || + mediaAttributes.attributeAsBooleanOrFalse(data, "isFirstPartyHideableApp"); + } + else { + isSystemApp = sad.systemApps(objectGraph).isSystemAppFromData(data); + } + return !isSystemApp || objectGraph.bag.enableSystemAppReviews; +} +/** + * Generates a tap-to-rate endpoint URL for the given parameter. + * @param adamId The adam id of the product to get review data for. + * @param isBundle Whether this is a bundle adamId + * @returns A URL for posting tap-to-rate data. + */ +function assembleUserRateURL(objectGraph, adamId, isBundle) { + return createBaseURL(objectGraph.bag.userRateURL, isBundle).param("id", adamId).build(); +} +/** + * Generates a writeUserReview endpoint URL for the given parameter. + * @param adamId The adam id of the product to be reviewed. + * @param isBundle Whether this is a bundle adamId + * @returns A URL for posting write review data. + */ +function assembleWriteReviewURL(objectGraph, adamId, isBundle) { + return createBaseURL(objectGraph.bag.writeReviewURL, isBundle).param("id", adamId).build(); +} +function createBaseURL(baseURL, isBundle) { + const url = urls.URL.from(baseURL); + const displayableKind = serverData.asString(url.query, "displayable-kind"); + // Convert displayable-kind for bundles + if (isBundle && (displayableKind === null || displayableKind === void 0 ? void 0 : displayableKind.length) > 0) { + url.query["displayable-kind"] = softwareToBundleKindMap[displayableKind] || displayableKind; + } + return url; +} +/** + * Create and return a rate action for a given adamId. + * @param adamId The adamId of the app to rate + * @param isBundle Whether this is a bundle adamId + * @param appName the name of the app + * @returns A new rate action. + */ +export function userRateAction(objectGraph, adamId, isBundle, appName = null) { + const successToast = new models.AlertAction("toast"); + successToast.title = objectGraph.loc.string("TOAST_TAP_TO_RATE_TITLE"); + successToast.message = objectGraph.loc.string("TOAST_TAP_TO_RATE_DESCRIPTION"); + successToast.artwork = artworkBuilder.createArtworkForResource(objectGraph, "resource://ToastStar.png", 95.0, 90.0); + const action = new models.RateAction(assembleUserRateURL(objectGraph, adamId, isBundle)); + action.adamId = adamId; + action.method = "POST"; + action.isStoreRequest = true; + action.disableCache = true; + action.successAction = successToast; + const ratingParameter = new models.HttpTemplateParameter("rating", "urlQuery", "decimalPad"); + const appVersionParameter = new models.HttpTemplateParameter("version-to-review", "urlQuery", "decimalPad"); + action.parameters = [ratingParameter, appVersionParameter]; + if (objectGraph.client.isTV && (appName === null || appName === void 0 ? void 0 : appName.length) > 0) { + action.title = objectGraph.loc.string("TV_RATE_PRODUCT").replace("{title}", appName); + } + return action; +} +export function createProductReviewsList(objectGraph, ratingsData, reviewItems, includeMoreAction = false, editorsChoiceReview, reviewSummary, shelfMetrics) { + return validation.context("createProductReviewsList", () => { + const productReviews = []; + if (serverData.isDefinedNonNullNonEmpty(reviewSummary) && objectGraph.client.isiOS) { + if (isSome(shelfMetrics)) { + const shelfMetricsOptions = { + id: `${shelfMetrics.getSequenceId()}`, + kind: null, + softwareType: null, + targetType: "reviewSummary", + title: objectGraph.loc.string("ProductPage.ReviewSummary.Body.Title"), + pageInformation: shelfMetrics.metricsPageInformation, + locationTracker: shelfMetrics.locationTracker, + idType: "sequential", + }; + metricsHelpersLocation.pushContentLocation(objectGraph, { + pageInformation: shelfMetrics.metricsPageInformation, + locationTracker: shelfMetrics.locationTracker, + targetType: "reviewSummary", + id: `${shelfMetrics.getSequenceId()}`, + idType: "sequential", + }, objectGraph.loc.string("ProductPage.ReviewSummary.Body.Title")); + metricsHelpersImpressions.addImpressionFields(objectGraph, reviewSummary, shelfMetricsOptions); + metricsHelpersLocation.popLocation(shelfMetrics.locationTracker); + metricsHelpersLocation.nextPosition(shelfMetrics.locationTracker); + } + productReviews.push(reviewSummary); + } + if (serverData.isDefinedNonNullNonEmpty(editorsChoiceReview) && !objectGraph.client.isiOS) { + productReviews.push(editorsChoiceReview); + } + if (serverData.isDefinedNonNull(reviewItems) && reviewItems.length > 0) { + const isWatchStore = objectGraph.client.isWatch; + // We omit the actions on watchOS, they aren't used. + const includeActions = !isWatchStore; + // We limit the number of reviews to one on watchOS. That's all + // we can display inline, and we want to reduce memory usage. + const fixedUpReviewItems = isWatchStore ? reviewItems.slice(0, 1) : reviewItems; + const includeFeedbackActions = objectGraph.client.isVision; + const userReviews = createReviewItems(objectGraph, objectGraph.client.guid, ratingsData, fixedUpReviewItems, includeMoreAction && includeActions, includeFeedbackActions && includeActions, includeActions, shelfMetrics); + if (isSome(shelfMetrics)) { + metricsHelpersLocation.pushContentLocation(objectGraph, { + pageInformation: shelfMetrics.metricsPageInformation, + locationTracker: shelfMetrics.locationTracker, + targetType: "mostHelpfulReviews", + id: `${shelfMetrics.getSequenceId()}`, + idType: "sequential", + }, null); + } + userReviews.forEach((userReview, index) => { + const productReview = new models.ProductReview(); + productReview.sourceType = "user"; + productReview.review = userReview; + productReviews.push(productReview); + if (isSome(shelfMetrics)) { + const options = { + id: productReview.id, + idType: "its_id", + kind: null, + softwareType: null, + title: null, + pageInformation: shelfMetrics.metricsPageInformation, + locationTracker: shelfMetrics.locationTracker, + targetType: "helpfulReview", + }; + metricsHelpersImpressions.addImpressionFields(objectGraph, productReview, options); + metricsHelpersLocation.nextPosition(shelfMetrics.locationTracker); + } + }); + if (isSome(shelfMetrics)) { + metricsHelpersLocation.popLocation(shelfMetrics.locationTracker); + metricsHelpersLocation.nextPosition(shelfMetrics.locationTracker); + } + } + return productReviews; + }); +} +export function createEditorsChoiceReview(objectGraph, data) { + return validation.context("editorsChoiceItem", () => { + if (objectGraph.client.isiOS) { + // On iOS, Editors' Choice is a separate shelf to reviews. On all other platforms, editors choice is embedded in the reviews list. + return null; + } + const editorsChoice = createEditorsChoiceModel(objectGraph, data); + if (isSome(editorsChoice)) { + const productReview = new models.ProductReview(); + productReview.sourceType = "editorsChoice"; + productReview.review = editorsChoice; + if (objectGraph.client.isVision) { + productReview.review.clickAction = createEditorsChoiceDetailActionForEditorsChoice(objectGraph, productReview.review); + } + return productReview; + } + return null; + }); +} +/** + * Creates a flow action for navigating to the expanded view of an editors choice item. + * + * @param objectGraph The current object graph + * @param editorsChoice The editors choice item + * @returns A flow action + */ +function createEditorsChoiceDetailActionForEditorsChoice(objectGraph, editorsChoice) { + const flowAction = new models.FlowAction("editorsChoiceDetail"); + flowAction.pageData = objects.shallowCopyOf(editorsChoice); + return flowAction; +} +/** + * Converts an array of user review rows into an array of review objects. + * @param deviceId The UUID for the user's device. + * @param rows User review rows from an API response. + * @returns An array of zero or more review objects. + */ +export function createReviewItems(objectGraph, deviceId, ratingsData, reviewItems, includeMoreAction = false, includeFeedbackActions = false, includeFlowPreviewActionsConfiguration = true, shelfMetrics) { + return validation.context("createReviewItems", () => { + const reviewDateString = function (date, isEdited) { + if (isEdited) { + const now = new Date(); + const withinAnHour = (now.getTime() - date.getTime()) * 1000 < 60 * 60; + if (withinAnHour) { + return objectGraph.loc.string("TimeAgo.Edited.JustNow"); + } + else { + return objectGraph.loc + .string("TimeAgo.Edited.Time") + .replace("{time}", objectGraph.loc.timeAgoWithContext(date, "standalone")); + } + } + else { + return objectGraph.loc.timeAgoWithContext(date, "standalone"); + } + }; + return reviewItems.map((item) => { + var _a; + const review = new models.Review(); + review.id = serverData.asString(item, "id", "coercible"); + review.title = mediaAttributes.attributeAsString(item, "title"); + const rawDate = mediaAttributes.attributeAsString(item, "date"); + if (rawDate) { + review.date = new Date(rawDate); + review.dateText = reviewDateString(review.date, mediaAttributes.attributeAsBooleanOrFalse(item, "isEdited")); + } + review.contents = mediaAttributes.attributeAsString(item, "review"); + review.rating = mediaAttributes.attributeAsNumber(item, "rating"); + review.reviewerName = mediaAttributes.attributeAsString(item, "userName"); + review.dateAuthorText = objectGraph.loc + .string("ProductPage.Section.Reviews.DateAuthor") + .replace("{date}", review.dateText) + .replace("{author}", review.reviewerName); + let responseToReviewText = null; + const responseId = mediaAttributes.attributeAsString(item, "developerResponse.id"); + if ((responseId === null || responseId === void 0 ? void 0 : responseId.length) > 0) { + const response = new models.Response(); + response.id = responseId; + response.contents = mediaAttributes.attributeAsString(item, "developerResponse.body"); + const rawResponseDate = mediaAttributes.attributeAsString(item, "developerResponse.modified"); + if (rawResponseDate) { + response.date = new Date(rawResponseDate); + response.dateText = reviewDateString(response.date, false); + } + review.response = response; + responseToReviewText = review.response.contents; + } + if (includeFlowPreviewActionsConfiguration) { + const adamId = serverData.asString(ratingsData, "adamId"); + review.flowPreviewActionsConfiguration = flowPreview.flowPreviewActionsConfigurationForReviewFromData(objectGraph, item, deviceId, adamId, responseToReviewText); + } + if (includeFeedbackActions) { + const voteActions = [ + flowPreview.voteActionFromData(objectGraph, item, deviceId, true), + flowPreview.voteActionFromData(objectGraph, item, deviceId, false), + ]; + if (((_a = objectGraph.bag.reportConcernUrl) === null || _a === void 0 ? void 0 : _a.length) > 0) { + voteActions.push(flowPreview.reportConcernActionFromData(objectGraph, item, deviceId)); + } + review.voteActions = voteActions; + } + if (includeMoreAction) { + if (objectGraph.client.isVision) { + review.moreAction = createReviewDetailActionForReview(objectGraph, review); + // Clear out the voteActions, as we only need them in the copy of the review + // attached to the `moreAction`. + review.voteActions = null; + } + else { + review.moreAction = singleReviewPageActionFromReviewItem(objectGraph, deviceId, ratingsData, item, shelfMetrics); + } + } + return review; + }); + }); +} +/** + * Create a ratings object from the responses of the requests to get product reviews. + * @param deviceId The UUID for the user's device. + * @param ratingsData The basic review data response. + * @returns A customer reviews object. + */ +export function ratingsFromApiResponses(objectGraph, deviceId, context, ratingsData) { + if (!ratingsData) { + return null; + } + return validation.context("ratingsFromApiResponses", () => { + const ratings = new models.Ratings(); + ratings.productId = serverData.asString(ratingsData, "adamId", "coercible"); + ratings.ratingAverage = serverData.asNumber(ratingsData, "ratingAverage"); + ratings.totalNumberOfRatings = serverData.asNumber(ratingsData, "ratingCount"); + ratings.totalNumberOfReviews = serverData.asNumber(ratingsData, "totalNumberOfReviews"); + ratings.context = context; + // Translate rating counts for each star level to represent the number of ratings out of `totalNumberOfRatings`. + const absoluteRatingsCounts = serverData + .asArrayOrEmpty(ratingsData, "ratingCountList") + .slice() + .reverse(); + const totalAbsoluteRatingsCounts = absoluteRatingsCounts.reduce((a, b) => a + b, 0); + if (totalAbsoluteRatingsCounts > 0) { + ratings.ratingCounts = absoluteRatingsCounts.map((absoluteCount) => (absoluteCount / totalAbsoluteRatingsCounts) * ratings.totalNumberOfRatings); + } + else { + ratings.ratingCounts = absoluteRatingsCounts; + } + const hasRatings = ratings.ratingAverage > 0 && ratings.ratingCounts; + if (!hasRatings) { + const wasReset = serverData.asBooleanOrFalse(ratingsData, "wasReset"); + ratings.status = wasReset + ? objectGraph.loc.string("RATINGS_STATUS_DEVELOPER_RESET") + : objectGraph.loc.string("RATINGS_STATUS_NOT_ENOUGH_RATINGS"); + } + return ratings; + }); +} +export function createReviewSummaryProductReview(objectGraph, data, shelfMetrics) { + return validation.context("createReviewSummaryProductReview", () => { + const reviewSummary = reviewSummaryFromData(objectGraph, data, shelfMetrics); + if (isNothing(reviewSummary)) { + return null; + } + const reviewSummaryProductReview = new models.ProductReview(); + reviewSummaryProductReview.review = reviewSummary; + reviewSummaryProductReview.sourceType = "reviewSummary"; + return reviewSummaryProductReview; + }); +} +/** + * Create a review summary object from the product page data + * @param data Product page data + * @param objectGraph Current object graph + * @returns The review summary object + */ +export function reviewSummaryFromData(objectGraph, data, shelfMetrics) { + if (!isReviewSummaryEnabled(objectGraph)) { + return null; + } + const reviewSummaryRelationship = mediaRelationship.relationship(data, "review-summary"); + if (isNothing(reviewSummaryRelationship)) { + return null; + } + const reviewSummaryDataArray = reviewSummaryRelationship.data; + if (serverData.isNullOrEmpty(reviewSummaryDataArray)) { + return null; + } + const reviewSummaryData = reviewSummaryDataArray[0]; + const reviewSummaryId = serverData.asString(reviewSummaryData, "id", "coercible"); + const reviewSummaryBodyWithTitle = createReviewSummaryBody(objectGraph, reviewSummaryData, true); + const reviewSummaryBodyNoTitle = createReviewSummaryBody(objectGraph, reviewSummaryData, false); + const subtitle = objectGraph.loc.string("ProductPage.ReviewSummary.Subtitle"); + const reviewSummaryReportConcernData = objectGraph.bag.reviewSummaryReportConcernData; + if (isNothing(reviewSummaryBodyNoTitle) || isNothing(reviewSummaryBodyWithTitle)) { + return null; + } + const appName = mediaAttributes.attributeAsString(data, "name"); + const reviewSummaryText = contentAttributes.contentAttributeAsString(objectGraph, reviewSummaryData, "text"); + const reviewSummaryArtwork = artworkBuilder.createArtworkForResource(objectGraph, "systemimage://text.line.2.summary"); + const reviewSummary = new models.ReviewSummary(reviewSummaryBodyWithTitle, reviewSummaryBodyNoTitle, subtitle, reviewSummaryArtwork, "leading", "text/markdown", flowPreviewActionsConfigurationForReviewSummaryFromData(objectGraph, reviewSummaryReportConcernData, data.id, appName, reviewSummaryId, reviewSummaryText, objectGraph.client.guid)); + // Add metrics before serializing token for url + const shelfMetricsOptions = { + id: `${shelfMetrics.getSequenceId()}`, + kind: null, + softwareType: null, + targetType: "reviewSummary", + title: objectGraph.loc.string("ProductPage.ReviewSummary.Body.Title"), + pageInformation: shelfMetrics.metricsPageInformation, + locationTracker: shelfMetrics.locationTracker, + idType: "sequential", + }; + metricsHelpersLocation.pushContentLocation(objectGraph, { + pageInformation: shelfMetrics.metricsPageInformation, + locationTracker: shelfMetrics.locationTracker, + targetType: "reviewSummary", + id: `${shelfMetrics.getSequenceId()}`, + idType: "sequential", + }, reviewSummary.title); + metricsHelpersImpressions.addImpressionFields(objectGraph, reviewSummary, shelfMetricsOptions); + metricsHelpersLocation.popLocation(shelfMetrics.locationTracker); + metricsHelpersLocation.nextPosition(shelfMetrics.locationTracker); + return reviewSummary; +} +/** + * Checks if the review summary module should be enabled + * @param objectGraph + * @returns true if the feature flag and bag key are enabled and the platform is iOS + */ +export function isReviewSummaryEnabled(objectGraph) { + return objectGraph.client.isiOS && objectGraph.bag.enableReviewSummarization; +} +/** + * Builds the review summary text using the review summary text + * @param objectGraph The current object graph + * @param reviewSummaryData The review summary content attributes + * @returns The built review summary body text + */ +function createReviewSummaryBody(objectGraph, reviewSummaryData, withTitle) { + const reviewSummaryText = contentAttributes.contentAttributeAsString(objectGraph, reviewSummaryData, "text"); + if (isNothing(reviewSummaryText)) { + return null; + } + const reviewSummaryTitle = objectGraph.loc.string("ProductPage.ReviewSummary.Body.Title"); + const reviewSummaryTitleStyled = `^[${reviewSummaryTitle}](jetFont: 'reviewSummaryTitle')`; + const reviewSummaryTextStyled = `^[${reviewSummaryText}](jetFont: 'reviewSummaryText')`; + if (!withTitle) { + return reviewSummaryTextStyled; + } + const reviewSummaryBody = objectGraph.loc + .string("ProductPage.ReviewSummary.Body") + .replace("{styledTitle}", reviewSummaryTitleStyled) + .replace("{reviewSummary}", reviewSummaryTextStyled); + return reviewSummaryBody; +} +export function starRatingsFromRatings(ratings) { + if (!ratings) { + return null; + } + const starRatings = new models.ProductStarRatings(); + copyRatingsInfoIntoOther(ratings, starRatings); + return starRatings; +} +export function starRatingsHistogramFromRatings(ratings) { + if (!ratings) { + return null; + } + const starRatings = new models.ProductStarRatingsHistogram(); + copyRatingsInfoIntoOther(ratings, starRatings); + return starRatings; +} +export function noRatingsFromRatings(ratings) { + if (!ratings) { + return null; + } + const noRatings = new models.ProductNoRatings(); + copyRatingsInfoIntoOther(ratings, noRatings); + return noRatings; +} +function copyRatingsInfoIntoOther(original, other) { + other.ratingAverage = original.ratingAverage; + other.ratingCounts = original.ratingCounts; + other.totalNumberOfRatings = original.totalNumberOfRatings; + other.totalNumberOfReviews = original.totalNumberOfReviews; + other.status = original.status; + other.reviews = original.reviews; + other.actions = original.actions; + other.nextPage = original.nextPage; +} +/** + * Create a tap to rate model from the lookup response + * @param adamId The adamId of the app. + * @param appName The name of the app. + * @param isBundle Whether this is a bundle adamId + * @param rating The current rating + * @returns A single tap to rate model object + */ +export function tapToRateWithAdamId(objectGraph, adamId, isBundle, appName = null, rating = null) { + const tapToRate = new models.TapToRate(); + switch (objectGraph.client.deviceType) { + case "tv": + tapToRate.title = objectGraph.loc.string("TV_SELECT_TO_RATE"); + break; + case "mac": + tapToRate.title = objectGraph.loc.string("CLICK_TO_RATE"); + break; + default: + tapToRate.title = objectGraph.client.isiOS + ? objectGraph.loc.string("TAP_TO_RATE") + : objectGraph.loc.string("TAP_TO_RATE_LEGACY"); + break; + } + tapToRate.rating = rating; + tapToRate.rateAction = userRateAction(objectGraph, adamId, isBundle, appName); + return tapToRate; +} +/** + * Creates a platform-specific action to write a review. + * @param adamId The app's identifier. + * @param isBundle Whether this is a bundle adamId + * @param appIcon The app's icon. + * @param isRated Whether the app has an existing rating + * @returns Action to show the write a review sheet. + */ +export function createWriteReviewAction(objectGraph, adamId, isBundle, appIcon, isRated = false, lockupSubtitle, lockupTitle) { + return validation.context("createWriteReviewAction", () => { + const title = isRated ? objectGraph.loc.string("EDIT_REVIEW") : objectGraph.loc.string("WRITE_A_REVIEW"); + const writeReviewUrl = assembleWriteReviewURL(objectGraph, adamId, isBundle); + let appIconWithCrop; + if (isSome(appIcon)) { + // The artwork template is ultimately passed to AMS, and doesn't go through our pipeline, + // so we need to ensure the crop code and file type is already populated. + const variant = artworkBuilder.createArtworkVariantForClient(objectGraph, true, false, 1 /* ArtworkUseCase.LockupIconSmall */); + const template = appIcon.template + .replace("{c}", `${appIcon.crop}-${variant.quality}`) + .replace("{f}", variant.format); + appIconWithCrop = new models.Artwork(template, appIcon.width, appIcon.height, appIcon.variants); + appIconWithCrop.backgroundColor = appIcon.backgroundColor; + appIconWithCrop.textColor = appIcon.textColor; + appIconWithCrop.checksum = appIcon.checksum; + appIconWithCrop.style = appIcon.style; + appIconWithCrop.crop = appIcon.crop; + appIconWithCrop.contentMode = appIcon.contentMode; + appIconWithCrop.imageScale = appIcon.imageScale; + } + let writeReviewAction; + switch (objectGraph.client.deviceType) { + case "mac": { + const action = new models.WriteReviewAction(adamId, writeReviewUrl); + action.title = title; + action.appIcon = appIconWithCrop; + action.itemDescription = lockupSubtitle; + action.appName = lockupTitle; + writeReviewAction = action; + break; + } + default: { + if (objectGraph.featureFlags.isEnabled("review_composer_redesign")) { + const action = new models.WriteReviewAction(adamId, writeReviewUrl); + action.title = title; + action.appName = lockupTitle; + action.itemDescription = lockupSubtitle; + action.appIcon = appIconWithCrop; + action.artwork = artworkBuilder.createArtworkForResource(objectGraph, "systemimage://square.and.pencil"); + writeReviewAction = action; + break; + } + else { + const action = new models.FlowAction("writeReview"); + action.title = title; + action.pageUrl = writeReviewUrl; + action.pageData = adamId; + action.presentationContext = "presentModal"; + action.artwork = artworkBuilder.createArtworkForResource(objectGraph, "systemimage://square.and.pencil"); + writeReviewAction = action; + break; + } + } + } + return writeReviewAction; + }); +} +/** + * Creates and returns an action for writing a product review + * @param data The response data to read from. + * @returns An action or null + */ +export function writeReviewActionFromData(objectGraph, data, lockupSubtitle, lockupTitle, appIcon) { + return validation.context(`writeReviewActionFromData`, () => { + const adamId = data.id; + if ((adamId === null || adamId === void 0 ? void 0 : adamId.length) > 0) { + const isBundle = data.type === "app-bundles"; + const rating = mediaAttributes.attributeAsNumber(data, "rating"); + const isRated = serverData.isDefinedNonNull(rating) && rating > 0; + return createWriteReviewAction(objectGraph, adamId, isBundle, appIcon, isRated, lockupSubtitle, lockupTitle); + } + return null; + }); +} +/** + * Creates and returns an action for navigating to a product's ratings & reviews + * @param clickAction The flow action for viewing the product + * @returns An action or null + */ +export function seeRatingsAndReviewsActionFromClickAction(objectGraph, adamId, clickAction) { + return validation.context(`seeRatingsAndReviewsActionFromData`, () => { + const flowAction = objects.shallowCopyOf(clickAction); + if (clickAction.pageData instanceof models.ProductPage || + clickAction.pageData instanceof models.ShelfBasedProductPage) { + let shelfBasedPageScrollAction; + if (objectGraph.client.isVision) { + shelfBasedPageScrollAction = new models.ShelfBasedPageScrollAction("productRatings", "notPurchasedRatingsAndReviews", "purchasedRatingsAndReviews", adamId); + } + else { + shelfBasedPageScrollAction = new models.ShelfBasedPageScrollAction("productRatings"); + } + const scrollAction = productPageUtil.isShelfBased(objectGraph) + ? shelfBasedPageScrollAction + : new models.ProductPageScrollAction(new models.ProductPageSection("shelf", "reviews")); + const productPageData = objects.shallowCopyOf(clickAction.pageData); + productPageData.fullProductFetchedAction = scrollAction; + flowAction.pageData = productPageData; + } + return flowAction; + }); +} +/** + * Create a custom shelf for the reviews module + * @param deviceId The UUID for the user's device. + * @param reviewData The basic review data response. + * @param rows The review row data response. + * @returns A custom shelf with one ratings module + */ +export function reviewsShelfForReviewsData(objectGraph, deviceId, ratingsData, reviewItems, includeMoreAction = false, includeFeedbackActions = false, includeFlowPreviewActionsConfiguration = true, shelfMetrics) { + return validation.context("reviewsShelfForReviewsData", () => { + if (objectGraph.client.isiOS) { + const shelf = new models.Shelf("productReview"); + shelfMetrics === null || shelfMetrics === void 0 ? void 0 : shelfMetrics.addImpressionsToShelf(objectGraph, shelf, "ratingsDetails"); + shelf.items = createProductReviewsList(objectGraph, ratingsData, reviewItems, includeMoreAction, null, null, shelfMetrics); + return shelf; + } + else { + const shelf = new models.Shelf("reviews"); + shelfMetrics === null || shelfMetrics === void 0 ? void 0 : shelfMetrics.addImpressionsToShelf(objectGraph, shelf, "ratingsDetails"); + shelf.items = createReviewItems(objectGraph, deviceId, ratingsData, reviewItems, includeMoreAction, includeFeedbackActions, includeFlowPreviewActionsConfiguration, shelfMetrics); + return shelf; + } + }); +} +/** + * Create a reviews container shelf. Optionally pass in reviews to horizontally scroll them. + */ +export function reviewsContainerShelfForReviewsData(objectGraph, deviceId, ratingsData, reviewItems, appName, productData, appIcon, editorsChoice, shelfMetrics, nextPageHref, includeSeeAllAction = false, shouldIncludePersonalizationUrl = true, rating = null, tvOnlyApp = false) { + return validation.context("reviewsContainerShelfForReviewsData", () => { + const reviewsContainer = reviewsContainerForReviewsData(objectGraph, deviceId, ratingsData, reviewItems, productData, appName, appIcon, editorsChoice, shelfMetrics, rating, tvOnlyApp); + const shelf = new models.Shelf("reviewsContainer"); + shelf.title = sectionTitleForPlatform(objectGraph); + shelf.items = [reviewsContainer]; + shelfMetrics === null || shelfMetrics === void 0 ? void 0 : shelfMetrics.addImpressionsToShelf(objectGraph, shelf, "ratingsDetails"); + if (includeSeeAllAction && + serverData.isDefinedNonNull(reviewsContainer.reviews) && + reviewsContainer.reviews.length > 0) { + shelf.seeAllAction = reviewsPageActionFromReviewsData(objectGraph, deviceId, ratingsData, reviewItems, nextPageHref, appName, appIcon, false, false); + } + return shelf; + }); +} +/** + * Create a personalized reviews container shelf + */ +export function personalizedReviewsContainerShelf(objectGraph, deviceId, token, dataContainer) { + return validation.context("personalizedReviewsContainerShelf", () => { + const data = mediaDataStructure.dataFromDataContainer(objectGraph, dataContainer); + const rating = mediaAttributes.attributeAsNumber(data, "rating"); + return reviewsContainerShelfForReviewsData(objectGraph, deviceId, token.ratingsData, token.reviewItems, token.appName, data, token.appIcon, token.editorsChoice, token.shelfMetrics, token.nextPageHref, token.includeSeeAllAction, false, rating); + }); +} +/** + * Create a reviews container + */ +export function reviewsContainerForReviewsData(objectGraph, deviceId, ratingsData, reviewItems, productData, appName, appIcon, editorsChoice, shelfMetrics, rating = null, tvOnlyApp = false) { + return validation.context("reviewsContainerForReviewsData", () => { + const reviewsContainer = new models.ReviewsContainer(); + const adamId = serverData.asString(ratingsData, "adamId"); + const isBundle = serverData.asBooleanOrFalse(ratingsData, "isBundle"); + reviewsContainer.adamId = adamId; + // Ratings + reviewsContainer.ratings = ratingsFromApiResponses(objectGraph, deviceId, "details", ratingsData); + if (objectGraph.client.isiOS) { + const reviewSummary = reviewSummaryFromData(objectGraph, productData, shelfMetrics); + if (serverData.isDefinedNonNull(reviewSummary)) { + reviewsContainer.reviewSummary = reviewSummary; + } + } + // Tap to Rate + if (!tvOnlyApp || objectGraph.client.isTV) { + reviewsContainer.tapToRate = tapToRateWithAdamId(objectGraph, adamId, isBundle, appName, rating); + } + if (objectGraph.client.isWeb) { + const productPageIntent = makeProductPageIntent({ + ...getLocale(objectGraph), + ...getPlatform(objectGraph), + id: unwrapOptional(adamId), + }); + const productAction = actionFor(productPageIntent, objectGraph); + productAction.title = appName; + reviewsContainer.productAction = productAction; + } + // Reviews + if (serverData.isDefinedNonNull(reviewItems) && reviewItems.length > 0) { + const isWatchStore = objectGraph.client.isWatch; + // We omit the actions on watchOS, they aren't used. + const includeActions = !isWatchStore; + // We limit the number of reviews to one on watchOS. That's all + // we can display inline, and we want to reduce memory usage. + const fixedUpReviewItems = isWatchStore ? reviewItems.slice(0, 1) : reviewItems; + reviewsContainer.reviews = createReviewItems(objectGraph, deviceId, ratingsData, fixedUpReviewItems, includeActions, false, includeActions, shelfMetrics); + } + // No reviews affects ratings message + if (serverData.isNull(reviewsContainer.reviews) || reviewsContainer.reviews.length === 0) { + const hasRatings = reviewsContainer.ratings.ratingAverage > 0 && reviewsContainer.ratings.ratingCounts; + const wasReset = serverData.asBooleanOrFalse(ratingsData, "wasReset"); + if (!hasRatings && !wasReset) { + reviewsContainer.ratings.status = objectGraph.loc.string("RATINGS_STATUS_NOT_ENOUGH_RATINGS_OR_REVIEWS"); + } + } + // Editors choice + if (!serverData.isNull(editorsChoice)) { + reviewsContainer.editorsChoice = editorsChoice; + } + // Write a review action + if (!tvOnlyApp || objectGraph.client.isTV) { + const isRated = serverData.isDefinedNonNull(rating) && rating > 0; + reviewsContainer.writeReviewAction = createWriteReviewAction(objectGraph, adamId, isBundle, appIcon, isRated, subtitleFromData(objectGraph, productData), appName); + } + // Support action + const supportUrl = serverData.asString(ratingsData, "supportUrl"); + if (supportUrl) { + const supportAction = new models.ExternalUrlAction(supportUrl, false); + supportAction.title = objectGraph.loc.string("APP_SUPPORT"); + reviewsContainer.supportAction = supportAction; + } + let alwaysAllowReviews = false; + for (const item of reviewItems) { + if (shouldAllowReviewsForSADApp(objectGraph, item)) { + alwaysAllowReviews = true; + break; + } + } + reviewsContainer.alwaysAllowReviews = alwaysAllowReviews; + return reviewsContainer; + }); +} +function createSortOption(objectGraph, adamId, sortId, title, sortActionTitle, sortedReviewsPageToken = null) { + const url = sortedReviewsPageUrl(objectGraph, adamId, sortId, sortedReviewsPageToken); + return new models.ReviewsPageSortOption(sortId, title, sortActionTitle, url); +} +/** + * Create and return the learn more action on the Review Summary context menu + * @param objectGraph + * @returns a flow action for the learn more article page for review summary generation + */ +export function reviewSummaryLearnMoreAction(objectGraph) { + const editorialItemId = objectGraph.bag.reviewSummarizationLearnMoreEditorialItemId; + if (serverData.isNullOrEmpty(editorialItemId)) { + return null; + } + const title = objectGraph.loc.string("Action.LearnMore"); + const flowAction = new models.FlowAction("article"); + flowAction.title = title; + flowAction.pageUrl = `https://apps.apple.com/story/id${editorialItemId}`; + flowAction.artwork = artworkBuilder.createArtworkForResource(objectGraph, "systemimage://questionmark.circle"); + return flowAction; +} +/** + * Create a reviews page + * @param deviceId The UUID for the user's device. + * @param ratingsData The basic ratings response. + * @param reviewItems Reviews row data. + * @param isFirstPage Whether this is the first page. + * @param nextPageHref the Media API href for next page. + * @param appName The name of the app. + * @param appAdamId The adamId of the app. + * @param appIcon The app icon to use. + * @param sortId The sort identifier for this page. + * @param productData Product data for the app + * @returns A page object to be presented using a FlowAction + */ +function reviewsPageForReviewsData(objectGraph, deviceId, ratingsData, reviewItems, isFirstPage, nextPageHref = null, appAdamId = null, appName, appIcon = null, productData, shelfMetrics, includeMoreAction = false, includeFeedbackActions = false, sortId = sortIdDefault, sortedReviewsPageToken = null) { + const page = new models.ReviewsPage(); + page.shelves = []; + return validation.context("reviewsPageForReviewsData", () => { + const adamId = serverData.isNull(appAdamId) ? serverData.asString(ratingsData, "adamId") : appAdamId; + if (isFirstPage && + !serverData.isNull(ratingsData) && + !objectGraph.client.isiOS && + !objectGraph.client.isVision) { + // Reviews container shelf + const containerShelf = reviewsContainerShelfForReviewsData(objectGraph, deviceId, ratingsData, [], appName, productData, appIcon, null, shelfMetrics); + page.trailingNavBarAction = reviewSummaryLearnMoreAction(objectGraph); + page.shelves.push(containerShelf); + } + // Ratings & review actions + if (isFirstPage && objectGraph.client.isVision && serverData.isDefinedNonNull(productData)) { + const appPlatforms = supportedAppPlatformsFromData(objectGraph, productData); + const tvOnlyApp = appPlatforms.length === 1 && appPlatforms[0] === "tv"; + const isBundle = productData.type === "app-bundles"; + page.ratings = ratingsFromApiResponses(objectGraph, deviceId, "details", ratingsData); + page.productReviewActions = createProductReviewActions(objectGraph, productData, appName, ratingsData, isBundle, tvOnlyApp, appIcon); + } + const pageComponents = createReviewsPageComponents(objectGraph, deviceId, adamId, ratingsData, reviewItems, nextPageHref, includeMoreAction, includeFeedbackActions, sortId, sortedReviewsPageToken, shelfMetrics); + pageComponents.reviewsShelf.presentationHints = { + ...pageComponents.reviewsShelf.presentationHints, + isSortable: isFirstPage && pageComponents.reviewsShelf.presentationHints.isSortable, + }; + page.adamId = adamId; + page.shelves.push(pageComponents.reviewsShelf); + page.nextPage = pageComponents.paginationToken; + page.initialSortOptionIdentifier = pageComponents.initialSortId; + page.sortActionSheetTitle = pageComponents.sortActionSheetTitle; + page.sortOptions = pageComponents.sorts; + page.alwaysAllowReviews = shouldAllowReviewsForSADApp(objectGraph, productData); + return page; + }); +} +/// THe id used to concat the sort optin if there is one +const baseReviewsShelfId = "ReviewsPage.ShelfId"; +/// Create the known shelfId for the shelf that includes the reviews +export function reviewsShelfIdForSortId(sortOptionId) { + if (serverData.isNullOrEmpty(sortOptionId)) { + return baseReviewsShelfId; + } + return `${baseReviewsShelfId}.${sortOptionId}`; +} +/** + * @param objectGraph The dependency graph for the app. + * @param deviceId The id used when submitting reviews. + * @param ratingsData The ratings data for this app. + * @param reviewItems The actual reivews data + * @param nextPageHref The url to fetch more reviews. + * @param includeMoreAction Whether to include the more action on the reviews shelf. + * @param includeFeedbackActions Whether to include the feedback actions on the reviews shelf. + * @param sortId The sort identifier for this page. + * @returns The components needed to create a reviews page. + */ +export function createReviewsPageComponents(objectGraph, deviceId, adamId, ratingsData, reviewItems, nextPageHref = null, includeMoreAction = false, includeFeedbackActions = false, sortId = sortIdDefault, sortedReviewsPageToken = null, shelfMetrics) { + // Ensure an even number of items are on the page for two column layout paging + const shouldTruncateShelfItems = (nextPageHref === null || nextPageHref === void 0 ? void 0 : nextPageHref.length) > 0 && serverData.isDefinedNonNullNonEmpty(reviewItems) && reviewItems.length % 2 > 0; + const itemsForReviewsShelf = shouldTruncateShelfItems ? reviewItems.slice(0, reviewItems.length - 1) : reviewItems; + const remainingItems = shouldTruncateShelfItems + ? reviewItems.slice(reviewItems.length - 1) + : []; + const reviewsShelf = reviewsShelfForReviewsData(objectGraph, deviceId, ratingsData, itemsForReviewsShelf, includeMoreAction, includeFeedbackActions, true, shelfMetrics); + reviewsShelf.id = reviewsShelfIdForSortId(sortId); + const pageComponents = { + reviewsShelf, + }; + reviewsShelf.presentationHints = { + isSortable: reviewItems.length > 0 || (nextPageHref === null || nextPageHref === void 0 ? void 0 : nextPageHref.length) > 0, + isSeeAllContext: true, + }; + pageComponents.initialSortId = sortId; + pageComponents.sortActionSheetTitle = objectGraph.loc.string("REVIEWS_SORT_BY"); + pageComponents.sorts = [ + createSortOption(objectGraph, adamId, "helpful", objectGraph.loc.string("REVIEWS_MOST_HELPFUL"), objectGraph.loc.string(objectGraph.client.isiOS ? "REVIEWS_MOST_HELPFUL" : "REVIEWS_SORT_BY_MOST_HELPFUL"), sortedReviewsPageToken), + createSortOption(objectGraph, adamId, "favorable", objectGraph.loc.string("REVIEWS_MOST_FAVORABLE"), objectGraph.loc.string(objectGraph.client.isiOS ? "REVIEWS_MOST_FAVORABLE" : "REVIEWS_SORT_BY_MOST_FAVORABLE"), sortedReviewsPageToken), + createSortOption(objectGraph, adamId, "critical", objectGraph.loc.string("REVIEWS_MOST_CRITICAL"), objectGraph.loc.string(objectGraph.client.isiOS ? "REVIEWS_MOST_CRITICAL" : "REVIEWS_SORT_BY_MOST_CRITICAL"), sortedReviewsPageToken), + createSortOption(objectGraph, adamId, "recent", objectGraph.loc.string("REVIEWS_MOST_RECENT"), objectGraph.loc.string(objectGraph.client.isiOS ? "REVIEWS_MOST_RECENT" : "REVIEWS_SORT_BY_MOST_RECENT"), sortedReviewsPageToken), + ]; + if ((nextPageHref === null || nextPageHref === void 0 ? void 0 : nextPageHref.length) > 0) { + pageComponents.paginationToken = createReviewsPageToken(objectGraph, adamId, nextPageHref, sortId, remainingItems); + } + return pageComponents; +} +/** + * Create a partial reviews page from a reviews media api container + */ +export function partialReviewsPageForReviewsMediaContainer(objectGraph, deviceId, adamId, prependingItems, data, isFirstPage, sortId, sortedReviewsPageToken, shelfMetrics) { + return validation.context("reviewsPageForReviewsMediaContainer", () => { + let reviewItems; + if (serverData.isDefinedNonNullNonEmpty(data.data)) { + reviewItems = prependingItems.concat(data.data); + } + else { + reviewItems = prependingItems; + } + const includeMoreAction = objectGraph.client.isVision; + const includeFeedbackActions = objectGraph.client.isVision; + return reviewsPageForReviewsData(objectGraph, deviceId, null, reviewItems, isFirstPage, data.next, adamId, null, null, null, shelfMetrics, includeMoreAction, includeFeedbackActions, sortId, sortedReviewsPageToken); + }); +} +/** + * Creates a flow action to display a reviews page. + * @param deviceId The UUID for the user's device. + * @param ratingsData The ratings data response + * @param reviewsData The review row data response. + * @param nextPageHref The Media API href for the next page. + * @param appName The name of the app + * @param appIcon The app's icon. + * @param productData The data for the product + + * @returns A `FlowAction` object pointing to a reviews page. + */ +export function reviewsPageActionFromReviewsData(objectGraph, deviceId, ratingsData, reviewsData, nextPageHref = null, appName = null, appIcon = null, includeMoreAction, includeFeedbackActions, productData, shelfMetrics) { + if (!ratingsData) { + return null; + } + return validation.context("reviewsPageActionFromReviewsData", () => { + const pageData = reviewsPageForReviewsData(objectGraph, deviceId, ratingsData, reviewsData, true, nextPageHref, null, appName, appIcon, productData, shelfMetrics, includeMoreAction, includeFeedbackActions); + pageData.title = pageTitleForPlatform(objectGraph); + const action = new models.FlowAction("reviews"); + action.pageData = pageData; + action.title = objectGraph.loc.string("ACTION_SEE_ALL"); + if (objectGraph.client.isWeb) { + const destination = makeSeeAllPageIntent({ + ...getLocale(objectGraph), + ...getPlatform(objectGraph), + "id": pageData.adamId, + "see-all": "reviews", + }); + action.destination = destination; + action.pageUrl = makeSeeAllPageURL(objectGraph, destination); + const { metricsPageInformation, locationTracker } = shelfMetrics !== null && shelfMetrics !== void 0 ? shelfMetrics : {}; + metricsHelpersClicks.addClickEventToAction(objectGraph, action, { + id: "SeeAllReviews", + actionType: "navigate", + locationTracker: locationTracker, + pageInformation: metricsPageInformation, + }, false, "button"); + } + return action; + }); +} +/** + * Creates a flow action to display a single reviewreviewsPageUrl + * @param deviceId The UUID for the user's device. + * @param ratingsData The ratings data response + * @param item The review data from a single row data response. + * @returns A `FlowAction` object pointing to a reviews page. + */ +function singleReviewPageActionFromReviewItem(objectGraph, deviceId, ratingsData, item, shelfMetrics) { + if (!item) { + return null; + } + return validation.context("singleReviewActionFromReviewData", () => { + // Reviews shelf + const reviewsShelf = reviewsShelfForReviewsData(objectGraph, deviceId, ratingsData, [item], null, null, null, shelfMetrics); + const page = new models.ReviewsPage(); + page.adamId = serverData.asString(ratingsData, "adamId"); + page.targetReviewId = serverData.asString(item, "id", "coercible"); + page.shelves = [reviewsShelf]; + const action = new models.FlowAction("reviews"); + action.pageData = page; + action.title = sectionTitleForPlatform(objectGraph); + return action; + }); +} +export function pageTitleForPlatform(objectGraph) { + if (objectGraph.client.isWatch) { + return objectGraph.loc.string("ProductPage.Section.Reviews.Title"); + } + else { + return null; + } +} +export function sectionTitleForPlatform(objectGraph) { + switch (objectGraph.client.deviceType) { + case "tv": + return objectGraph.loc.string("TV_PRODUCT_SECTION_RATINGS"); + case "watch": + return null; + default: + return objectGraph.loc.string("PRODUCT_SECTION_REVIEWS"); + } +} +/** + * Creates a flow action for navigating to the expanded view of a review. + * + * @param objectGraph The current object graph + * @param review The source review + * @returns A flow action + */ +function createReviewDetailActionForReview(objectGraph, review) { + const flowAction = new models.FlowAction("reviewDetail"); + const detailReview = objects.shallowCopyOf(review); + detailReview.moreAction = null; + flowAction.pageData = detailReview; + return flowAction; +} +//# sourceMappingURL=reviews.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelf-based/product-page-section-mapping.js b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelf-based/product-page-section-mapping.js new file mode 100644 index 0000000..0605b10 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelf-based/product-page-section-mapping.js @@ -0,0 +1,40 @@ +import * as models from "../../../api/models/index"; +/** + * ProductPageShelfMapping is a type that allows us to consolidate the logic around shelf + * ordering between ShelfBasedProductPage and ProductPage for now until we move over + * to ShelfBased completely. + */ +export class ProductPageSectionMapping { + constructor(sectionType, shelfId, shelfBasedMappedIds) { + this.sectionType = sectionType; + this.shelfId = shelfId; + this.shelfBasedMappedIds = shelfBasedMappedIds; + } + /// Create the ProductPageSection for this mapping + createProductPageSection(objectGraph) { + return models.legacyProductPageNonShelfSections.has(this.sectionType) + ? new models.ProductPageSection(this.sectionType, null) + : new models.ProductPageSection(this.sectionType, this.shelfId); + } + /// Create the list of shelfIds to use for this section when in a ShelfBasedProductPage + createShelfIdList() { + return this.shelfBasedMappedIds || [this.shelfId]; + } +} +/// Create the list of ProductPageSections for use with the non-ShelfBasedProduct pages +export function legacyProductPageSectionsFromMappings(objectGraph, sectionMappings) { + return sectionMappings.map((mapping) => { + return mapping.createProductPageSection(objectGraph); + }); +} +/// Create the list of ProductPageShelfIds for use with the ShelfBasedProduct pages +export function productPageShelfIdsFromMappings(sectionMappings) { + return sectionMappings + .map((mapping) => { + return mapping.createShelfIdList(); + }) + .reduce((accumulator, currentValue) => { + return accumulator.concat(currentValue); + }); +} +//# sourceMappingURL=product-page-section-mapping.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelf-based/product-page-shelf-ordering/product-page-shelf-ordering-ios-arcade.js b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelf-based/product-page-shelf-ordering/product-page-shelf-ordering-ios-arcade.js new file mode 100644 index 0000000..e0e0451 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelf-based/product-page-shelf-ordering/product-page-shelf-ordering-ios-arcade.js @@ -0,0 +1,215 @@ +import { ProductPageSectionMapping } from "../product-page-section-mapping"; +/// The at which to place a section that is in the spotlight. +export const purchasedSpotlightIndex = 5; +export const notPurchasedSpotlightIndex = 5; +// MARK: - Installed +export const purchasedOrdering = [ + new ProductPageSectionMapping("header", "header"), + new ProductPageSectionMapping("topLockup", "topLockup"), + new ProductPageSectionMapping("shelf", "informationRibbon"), + new ProductPageSectionMapping("shelf", "arcadeProductPageInlineMessage"), + new ProductPageSectionMapping("shelf", "preorderDisclaimer"), + new ProductPageSectionMapping("shelf", "appEvents"), + new ProductPageSectionMapping("shelf", "mostRecentVersion"), + new ProductPageSectionMapping("shelf", "achievements"), + new ProductPageSectionMapping("shelf", "friendsPlaying"), + new ProductPageSectionMapping("shelf", "videos"), + new ProductPageSectionMapping("screenshots", "screenshots"), + new ProductPageSectionMapping("shelf", "description"), + new ProductPageSectionMapping("shelf", "editorialQuote"), + new ProductPageSectionMapping("shelf", "editorsChoice"), + new ProductPageSectionMapping("shelf", "reviews", [ + "productRatings", + "allProductReviews", + "allProductReviewActions", + "productReviewsFooter", + ]), + new ProductPageSectionMapping("shelf", "privacyHeader"), + new ProductPageSectionMapping("shelf", "privacyTypes"), + new ProductPageSectionMapping("shelf", "privacyFooter"), + new ProductPageSectionMapping("shelf", "accessibilityHeader"), + new ProductPageSectionMapping("shelf", "accessibilityFeatures"), + new ProductPageSectionMapping("shelf", "accessibilityDeveloperLink"), + new ProductPageSectionMapping("shelf", "information"), + new ProductPageSectionMapping("shelf", "purchasedLinks"), + new ProductPageSectionMapping("shelf", "capabilities"), + new ProductPageSectionMapping("shelf", "featuredIn"), + new ProductPageSectionMapping("shelf", "similarItems"), + new ProductPageSectionMapping("shelf", "moreByDeveloper"), +]; +export const purchasedOrderingCompact = [ + new ProductPageSectionMapping("header", "header"), + new ProductPageSectionMapping("topLockup", "topLockup"), + new ProductPageSectionMapping("shelf", "informationRibbon"), + new ProductPageSectionMapping("shelf", "arcadeProductPageInlineMessage"), + new ProductPageSectionMapping("shelf", "preorderDisclaimer"), + new ProductPageSectionMapping("shelf", "appEvents"), + new ProductPageSectionMapping("shelf", "mostRecentVersion"), + new ProductPageSectionMapping("shelf", "achievements"), + new ProductPageSectionMapping("shelf", "friendsPlaying"), + new ProductPageSectionMapping("shelf", "videos"), + new ProductPageSectionMapping("screenshots", "screenshots"), + new ProductPageSectionMapping("shelf", "description"), + new ProductPageSectionMapping("shelf", "editorialQuote"), + new ProductPageSectionMapping("shelf", "editorsChoice"), + new ProductPageSectionMapping("shelf", "reviews", [ + "productRatings", + "reviewSummary", + "userProductReviews", + "editorsChoiceProductReviews", + "productReviewsFooter", + "tapToRateProductReviewAction", + "writeAReviewProductReviewAction", + ]), + new ProductPageSectionMapping("shelf", "privacyHeader"), + new ProductPageSectionMapping("shelf", "privacyTypes"), + new ProductPageSectionMapping("shelf", "privacyFooter"), + new ProductPageSectionMapping("shelf", "accessibilityHeader"), + new ProductPageSectionMapping("shelf", "accessibilityFeatures"), + new ProductPageSectionMapping("shelf", "accessibilityDeveloperLink"), + new ProductPageSectionMapping("shelf", "information"), + new ProductPageSectionMapping("shelf", "purchasedLinks"), + new ProductPageSectionMapping("shelf", "capabilities"), + new ProductPageSectionMapping("shelf", "featuredIn"), + new ProductPageSectionMapping("shelf", "similarItems"), + new ProductPageSectionMapping("shelf", "moreByDeveloper"), +]; +// MARK: - Not Installed +/// The standard ordering to use for an app that is not installed. +export const notPurchasedOrdering = [ + new ProductPageSectionMapping("header", "header"), + new ProductPageSectionMapping("topLockup", "topLockup"), + new ProductPageSectionMapping("shelf", "informationRibbon"), + new ProductPageSectionMapping("shelf", "arcadeProductPageInlineMessage"), + new ProductPageSectionMapping("shelf", "preorderDisclaimer"), + new ProductPageSectionMapping("screenshots", "screenshots"), + new ProductPageSectionMapping("shelf", "description"), + new ProductPageSectionMapping("shelf", "editorialQuote"), + new ProductPageSectionMapping("shelf", "videos"), + new ProductPageSectionMapping("shelf", "editorsChoice"), + new ProductPageSectionMapping("shelf", "reviews", ["productRatings", "allProductReviews", "productReviewsFooter"]), + new ProductPageSectionMapping("shelf", "appEvents"), + new ProductPageSectionMapping("shelf", "achievements"), + new ProductPageSectionMapping("shelf", "friendsPlaying"), + new ProductPageSectionMapping("shelf", "mostRecentVersion"), + new ProductPageSectionMapping("shelf", "privacyHeader"), + new ProductPageSectionMapping("shelf", "privacyTypes"), + new ProductPageSectionMapping("shelf", "privacyFooter"), + new ProductPageSectionMapping("shelf", "accessibilityHeader"), + new ProductPageSectionMapping("shelf", "accessibilityFeatures"), + new ProductPageSectionMapping("shelf", "accessibilityDeveloperLink"), + new ProductPageSectionMapping("shelf", "information"), + new ProductPageSectionMapping("shelf", "notPurchasedLinks"), + new ProductPageSectionMapping("shelf", "capabilities"), + new ProductPageSectionMapping("shelf", "featuredIn"), + new ProductPageSectionMapping("shelf", "similarItems"), + new ProductPageSectionMapping("shelf", "moreByDeveloper"), +]; +export const notPurchasedOrderingCompact = [ + new ProductPageSectionMapping("header", "header"), + new ProductPageSectionMapping("topLockup", "topLockup"), + new ProductPageSectionMapping("shelf", "informationRibbon"), + new ProductPageSectionMapping("shelf", "arcadeProductPageInlineMessage"), + new ProductPageSectionMapping("shelf", "preorderDisclaimer"), + new ProductPageSectionMapping("screenshots", "screenshots"), + new ProductPageSectionMapping("shelf", "description"), + new ProductPageSectionMapping("shelf", "editorialQuote"), + new ProductPageSectionMapping("shelf", "videos"), + new ProductPageSectionMapping("shelf", "editorsChoice"), + new ProductPageSectionMapping("shelf", "reviews", [ + "productRatings", + "reviewSummary", + "userProductReviews", + "editorsChoiceProductReviews", + "productReviewsFooter", + ]), + new ProductPageSectionMapping("shelf", "appEvents"), + new ProductPageSectionMapping("shelf", "achievements"), + new ProductPageSectionMapping("shelf", "friendsPlaying"), + new ProductPageSectionMapping("shelf", "mostRecentVersion"), + new ProductPageSectionMapping("shelf", "privacyHeader"), + new ProductPageSectionMapping("shelf", "privacyTypes"), + new ProductPageSectionMapping("shelf", "privacyFooter"), + new ProductPageSectionMapping("shelf", "accessibilityHeader"), + new ProductPageSectionMapping("shelf", "accessibilityFeatures"), + new ProductPageSectionMapping("shelf", "accessibilityDeveloperLink"), + new ProductPageSectionMapping("shelf", "information"), + new ProductPageSectionMapping("shelf", "notPurchasedLinks"), + new ProductPageSectionMapping("shelf", "capabilities"), + new ProductPageSectionMapping("shelf", "featuredIn"), + new ProductPageSectionMapping("shelf", "similarItems"), + new ProductPageSectionMapping("shelf", "moreByDeveloper"), +]; +// MARK: - During Download +export const downloadingOrdering = [ + new ProductPageSectionMapping("header", "header"), + new ProductPageSectionMapping("topLockup", "topLockup"), + new ProductPageSectionMapping("shelf", "informationRibbon"), + new ProductPageSectionMapping("shelf", "arcadeProductPageInlineMessage"), + new ProductPageSectionMapping("shelf", "preorderDisclaimer"), + new ProductPageSectionMapping("shelf", "similarItems"), + new ProductPageSectionMapping("shelf", "appEvents"), + new ProductPageSectionMapping("shelf", "mostRecentVersion"), + new ProductPageSectionMapping("shelf", "achievements"), + new ProductPageSectionMapping("shelf", "friendsPlaying"), + new ProductPageSectionMapping("shelf", "videos"), + new ProductPageSectionMapping("screenshots", "screenshots"), + new ProductPageSectionMapping("shelf", "description"), + new ProductPageSectionMapping("shelf", "editorialQuote"), + new ProductPageSectionMapping("shelf", "editorsChoice"), + new ProductPageSectionMapping("shelf", "reviews", [ + "productRatings", + "allProductReviewActions", + "allProductReviews", + "productReviewsFooter", + ]), + new ProductPageSectionMapping("shelf", "privacyHeader"), + new ProductPageSectionMapping("shelf", "privacyTypes"), + new ProductPageSectionMapping("shelf", "privacyFooter"), + new ProductPageSectionMapping("shelf", "accessibilityHeader"), + new ProductPageSectionMapping("shelf", "accessibilityFeatures"), + new ProductPageSectionMapping("shelf", "accessibilityDeveloperLink"), + new ProductPageSectionMapping("shelf", "information"), + new ProductPageSectionMapping("shelf", "purchasedLinks"), + new ProductPageSectionMapping("shelf", "capabilities"), + new ProductPageSectionMapping("shelf", "featuredIn"), + new ProductPageSectionMapping("shelf", "moreByDeveloper"), +]; +export const downloadingOrderingCompact = [ + new ProductPageSectionMapping("header", "header"), + new ProductPageSectionMapping("topLockup", "topLockup"), + new ProductPageSectionMapping("shelf", "informationRibbon"), + new ProductPageSectionMapping("shelf", "arcadeProductPageInlineMessage"), + new ProductPageSectionMapping("shelf", "preorderDisclaimer"), + new ProductPageSectionMapping("shelf", "similarItems"), + new ProductPageSectionMapping("shelf", "appEvents"), + new ProductPageSectionMapping("shelf", "mostRecentVersion"), + new ProductPageSectionMapping("shelf", "achievements"), + new ProductPageSectionMapping("shelf", "friendsPlaying"), + new ProductPageSectionMapping("shelf", "videos"), + new ProductPageSectionMapping("screenshots", "screenshots"), + new ProductPageSectionMapping("shelf", "description"), + new ProductPageSectionMapping("shelf", "editorialQuote"), + new ProductPageSectionMapping("shelf", "editorsChoice"), + new ProductPageSectionMapping("shelf", "reviews", [ + "productRatings", + "tapToRateProductReviewAction", + "reviewSummary", + "userProductReviews", + "editorsChoiceProductReviews", + "productReviewsFooter", + "writeAReviewProductReviewAction", + ]), + new ProductPageSectionMapping("shelf", "privacyHeader"), + new ProductPageSectionMapping("shelf", "privacyTypes"), + new ProductPageSectionMapping("shelf", "privacyFooter"), + new ProductPageSectionMapping("shelf", "accessibilityHeader"), + new ProductPageSectionMapping("shelf", "accessibilityFeatures"), + new ProductPageSectionMapping("shelf", "accessibilityDeveloperLink"), + new ProductPageSectionMapping("shelf", "information"), + new ProductPageSectionMapping("shelf", "purchasedLinks"), + new ProductPageSectionMapping("shelf", "capabilities"), + new ProductPageSectionMapping("shelf", "featuredIn"), + new ProductPageSectionMapping("shelf", "moreByDeveloper"), +]; +//# sourceMappingURL=product-page-shelf-ordering-ios-arcade.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelf-based/product-page-shelf-ordering/product-page-shelf-ordering-ios.js b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelf-based/product-page-shelf-ordering/product-page-shelf-ordering-ios.js new file mode 100644 index 0000000..4ebf248 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelf-based/product-page-shelf-ordering/product-page-shelf-ordering-ios.js @@ -0,0 +1,247 @@ +import { ProductPageSectionMapping } from "../product-page-section-mapping"; +/// The at which to place a section that is in the spotlight. +export const purchasedSpotlightIndex = 5; +export const notPurchasedSpotlightIndex = 5; +// MARK: - Installed +export const purchasedOrdering = [ + new ProductPageSectionMapping("header", "header"), + new ProductPageSectionMapping("topLockup", "topLockup"), + new ProductPageSectionMapping("shelf", "informationRibbon"), + new ProductPageSectionMapping("shelf", "productPageInlineMessage"), + new ProductPageSectionMapping("shelf", "preorderDisclaimer"), + new ProductPageSectionMapping("shelf", "bundleChildren"), + new ProductPageSectionMapping("shelf", "bundleParents"), + new ProductPageSectionMapping("shelf", "appEvents"), + new ProductPageSectionMapping("shelf", "mostRecentVersion"), + new ProductPageSectionMapping("shelf", "subscriptions"), + new ProductPageSectionMapping("shelf", "inAppPurchases"), + new ProductPageSectionMapping("shelf", "achievements"), + new ProductPageSectionMapping("shelf", "friendsPlaying"), + new ProductPageSectionMapping("shelf", "videos"), + new ProductPageSectionMapping("screenshots", "screenshots"), + new ProductPageSectionMapping("shelf", "description"), + new ProductPageSectionMapping("shelf", "editorialQuote"), + new ProductPageSectionMapping("shelf", "editorsChoice"), + new ProductPageSectionMapping("shelf", "reviews", [ + "productRatings", + "allProductReviews", + "allProductReviewActions", + "productReviewsFooter", + ]), + new ProductPageSectionMapping("shelf", "privacyHeader"), + new ProductPageSectionMapping("shelf", "privacyTypes"), + new ProductPageSectionMapping("shelf", "privacyFooter"), + new ProductPageSectionMapping("shelf", "accessibilityHeader"), + new ProductPageSectionMapping("shelf", "accessibilityFeatures"), + new ProductPageSectionMapping("shelf", "accessibilityDeveloperLink"), + new ProductPageSectionMapping("shelf", "information"), + new ProductPageSectionMapping("shelf", "purchasedLinks"), + new ProductPageSectionMapping("shelf", "capabilities"), + new ProductPageSectionMapping("shelf", "featuredIn"), + new ProductPageSectionMapping("shelf", "moreByDeveloper"), + new ProductPageSectionMapping("shelf", "similarItems"), +]; +export const purchasedOrderingCompact = [ + new ProductPageSectionMapping("header", "header"), + new ProductPageSectionMapping("topLockup", "topLockup"), + new ProductPageSectionMapping("shelf", "informationRibbon"), + new ProductPageSectionMapping("shelf", "productPageInlineMessage"), + new ProductPageSectionMapping("shelf", "preorderDisclaimer"), + new ProductPageSectionMapping("shelf", "bundleChildren"), + new ProductPageSectionMapping("shelf", "bundleParents"), + new ProductPageSectionMapping("shelf", "appEvents"), + new ProductPageSectionMapping("shelf", "mostRecentVersion"), + new ProductPageSectionMapping("shelf", "subscriptions"), + new ProductPageSectionMapping("shelf", "inAppPurchases"), + new ProductPageSectionMapping("shelf", "achievements"), + new ProductPageSectionMapping("shelf", "friendsPlaying"), + new ProductPageSectionMapping("shelf", "videos"), + new ProductPageSectionMapping("screenshots", "screenshots"), + new ProductPageSectionMapping("shelf", "description"), + new ProductPageSectionMapping("shelf", "editorialQuote"), + new ProductPageSectionMapping("shelf", "editorsChoice"), + new ProductPageSectionMapping("shelf", "reviews", [ + "productRatings", + "reviewSummary", + "userProductReviews", + "editorsChoiceProductReviews", + "productReviewsFooter", + "tapToRateProductReviewAction", + "writeAReviewProductReviewAction", + ]), + new ProductPageSectionMapping("shelf", "tapToRateProductReviewAction"), + new ProductPageSectionMapping("shelf", "userProductReviews"), + new ProductPageSectionMapping("shelf", "editorsChoiceProductReviews"), + new ProductPageSectionMapping("shelf", "writeAReviewProductReviewAction"), + new ProductPageSectionMapping("shelf", "privacyHeader"), + new ProductPageSectionMapping("shelf", "privacyTypes"), + new ProductPageSectionMapping("shelf", "privacyFooter"), + new ProductPageSectionMapping("shelf", "accessibilityHeader"), + new ProductPageSectionMapping("shelf", "accessibilityFeatures"), + new ProductPageSectionMapping("shelf", "accessibilityDeveloperLink"), + new ProductPageSectionMapping("shelf", "information"), + new ProductPageSectionMapping("shelf", "purchasedLinks"), + new ProductPageSectionMapping("shelf", "capabilities"), + new ProductPageSectionMapping("shelf", "featuredIn"), + new ProductPageSectionMapping("shelf", "moreByDeveloper"), + new ProductPageSectionMapping("shelf", "similarItems"), +]; +// MARK: - Not Installed +/// The standard ordering to use for an app that is not installed. +export const notPurchasedOrdering = [ + new ProductPageSectionMapping("header", "header"), + new ProductPageSectionMapping("topLockup", "topLockup"), + new ProductPageSectionMapping("shelf", "informationRibbon"), + new ProductPageSectionMapping("shelf", "productPageInlineMessage"), + new ProductPageSectionMapping("shelf", "preorderDisclaimer"), + new ProductPageSectionMapping("shelf", "bundleChildren"), + new ProductPageSectionMapping("shelf", "bundleParents"), + new ProductPageSectionMapping("screenshots", "screenshots"), + new ProductPageSectionMapping("shelf", "description"), + new ProductPageSectionMapping("shelf", "appEvents"), + new ProductPageSectionMapping("shelf", "editorialQuote"), + new ProductPageSectionMapping("shelf", "videos"), + new ProductPageSectionMapping("shelf", "editorsChoice"), + new ProductPageSectionMapping("shelf", "reviews", ["productRatings", "allProductReviews", "productReviewsFooter"]), + new ProductPageSectionMapping("shelf", "achievements"), + new ProductPageSectionMapping("shelf", "friendsPlaying"), + new ProductPageSectionMapping("shelf", "mostRecentVersion"), + new ProductPageSectionMapping("shelf", "subscriptions"), + new ProductPageSectionMapping("shelf", "inAppPurchases"), + new ProductPageSectionMapping("shelf", "privacyHeader"), + new ProductPageSectionMapping("shelf", "privacyTypes"), + new ProductPageSectionMapping("shelf", "privacyFooter"), + new ProductPageSectionMapping("shelf", "accessibilityHeader"), + new ProductPageSectionMapping("shelf", "accessibilityFeatures"), + new ProductPageSectionMapping("shelf", "accessibilityDeveloperLink"), + new ProductPageSectionMapping("shelf", "information"), + new ProductPageSectionMapping("shelf", "notPurchasedLinks"), + new ProductPageSectionMapping("shelf", "capabilities"), + new ProductPageSectionMapping("shelf", "featuredIn"), + new ProductPageSectionMapping("shelf", "moreByDeveloper"), + new ProductPageSectionMapping("shelf", "similarItems"), +]; +export const notPurchasedOrderingCompact = [ + new ProductPageSectionMapping("header", "header"), + new ProductPageSectionMapping("topLockup", "topLockup"), + new ProductPageSectionMapping("shelf", "informationRibbon"), + new ProductPageSectionMapping("shelf", "productPageInlineMessage"), + new ProductPageSectionMapping("shelf", "preorderDisclaimer"), + new ProductPageSectionMapping("shelf", "bundleChildren"), + new ProductPageSectionMapping("shelf", "bundleParents"), + new ProductPageSectionMapping("screenshots", "screenshots"), + new ProductPageSectionMapping("shelf", "description"), + new ProductPageSectionMapping("shelf", "appEvents"), + new ProductPageSectionMapping("shelf", "editorialQuote"), + new ProductPageSectionMapping("shelf", "videos"), + new ProductPageSectionMapping("shelf", "editorsChoice"), + new ProductPageSectionMapping("shelf", "reviews", [ + "productRatings", + "reviewSummary", + "userProductReviews", + "editorsChoiceProductReviews", + "productReviewsFooter", + ]), + new ProductPageSectionMapping("shelf", "achievements"), + new ProductPageSectionMapping("shelf", "friendsPlaying"), + new ProductPageSectionMapping("shelf", "mostRecentVersion"), + new ProductPageSectionMapping("shelf", "subscriptions"), + new ProductPageSectionMapping("shelf", "inAppPurchases"), + new ProductPageSectionMapping("shelf", "privacyHeader"), + new ProductPageSectionMapping("shelf", "privacyTypes"), + new ProductPageSectionMapping("shelf", "privacyFooter"), + new ProductPageSectionMapping("shelf", "accessibilityHeader"), + new ProductPageSectionMapping("shelf", "accessibilityFeatures"), + new ProductPageSectionMapping("shelf", "accessibilityDeveloperLink"), + new ProductPageSectionMapping("shelf", "information"), + new ProductPageSectionMapping("shelf", "notPurchasedLinks"), + new ProductPageSectionMapping("shelf", "capabilities"), + new ProductPageSectionMapping("shelf", "featuredIn"), + new ProductPageSectionMapping("shelf", "moreByDeveloper"), + new ProductPageSectionMapping("shelf", "similarItems"), +]; +// MARK: - During Download +export const downloadingOrdering = [ + new ProductPageSectionMapping("header", "header"), + new ProductPageSectionMapping("topLockup", "topLockup"), + new ProductPageSectionMapping("shelf", "informationRibbon"), + new ProductPageSectionMapping("shelf", "productPageInlineMessage"), + new ProductPageSectionMapping("shelf", "preorderDisclaimer"), + new ProductPageSectionMapping("shelf", "similarItems"), + new ProductPageSectionMapping("shelf", "bundleChildren"), + new ProductPageSectionMapping("shelf", "bundleParents"), + new ProductPageSectionMapping("shelf", "appEvents"), + new ProductPageSectionMapping("shelf", "mostRecentVersion"), + new ProductPageSectionMapping("shelf", "subscriptions"), + new ProductPageSectionMapping("shelf", "inAppPurchases"), + new ProductPageSectionMapping("shelf", "achievements"), + new ProductPageSectionMapping("shelf", "friendsPlaying"), + new ProductPageSectionMapping("shelf", "videos"), + new ProductPageSectionMapping("screenshots", "screenshots"), + new ProductPageSectionMapping("shelf", "description"), + new ProductPageSectionMapping("shelf", "editorialQuote"), + new ProductPageSectionMapping("shelf", "editorsChoice"), + new ProductPageSectionMapping("shelf", "reviews", [ + "productRatings", + "allProductReviews", + "allProductReviewActions", + "productReviewsFooter", + ]), + new ProductPageSectionMapping("shelf", "privacyHeader"), + new ProductPageSectionMapping("shelf", "privacyTypes"), + new ProductPageSectionMapping("shelf", "privacyFooter"), + new ProductPageSectionMapping("shelf", "accessibilityHeader"), + new ProductPageSectionMapping("shelf", "accessibilityFeatures"), + new ProductPageSectionMapping("shelf", "accessibilityDeveloperLink"), + new ProductPageSectionMapping("shelf", "information"), + new ProductPageSectionMapping("shelf", "purchasedLinks"), + new ProductPageSectionMapping("shelf", "capabilities"), + new ProductPageSectionMapping("shelf", "featuredIn"), + new ProductPageSectionMapping("shelf", "moreByDeveloper"), +]; +export const downloadingOrderingCompact = [ + new ProductPageSectionMapping("header", "header"), + new ProductPageSectionMapping("topLockup", "topLockup"), + new ProductPageSectionMapping("shelf", "informationRibbon"), + new ProductPageSectionMapping("shelf", "productPageInlineMessage"), + new ProductPageSectionMapping("shelf", "preorderDisclaimer"), + new ProductPageSectionMapping("shelf", "similarItems"), + new ProductPageSectionMapping("shelf", "bundleChildren"), + new ProductPageSectionMapping("shelf", "bundleParents"), + new ProductPageSectionMapping("shelf", "appEvents"), + new ProductPageSectionMapping("shelf", "mostRecentVersion"), + new ProductPageSectionMapping("shelf", "subscriptions"), + new ProductPageSectionMapping("shelf", "inAppPurchases"), + new ProductPageSectionMapping("shelf", "achievements"), + new ProductPageSectionMapping("shelf", "friendsPlaying"), + new ProductPageSectionMapping("shelf", "videos"), + new ProductPageSectionMapping("screenshots", "screenshots"), + new ProductPageSectionMapping("shelf", "description"), + new ProductPageSectionMapping("shelf", "editorialQuote"), + new ProductPageSectionMapping("shelf", "editorsChoice"), + new ProductPageSectionMapping("shelf", "reviews", [ + "productRatings", + "reviewSummary", + "userProductReviews", + "editorsChoiceProductReviews", + "productReviewsFooter", + "tapToRateProductReviewAction", + "writeAReviewProductReviewAction", + ]), + new ProductPageSectionMapping("shelf", "tapToRateProductReviewAction"), + new ProductPageSectionMapping("shelf", "userProductReviews"), + new ProductPageSectionMapping("shelf", "editorsChoiceProductReviews"), + new ProductPageSectionMapping("shelf", "writeAReviewProductReviewAction"), + new ProductPageSectionMapping("shelf", "privacyHeader"), + new ProductPageSectionMapping("shelf", "privacyTypes"), + new ProductPageSectionMapping("shelf", "privacyFooter"), + new ProductPageSectionMapping("shelf", "accessibilityHeader"), + new ProductPageSectionMapping("shelf", "accessibilityFeatures"), + new ProductPageSectionMapping("shelf", "accessibilityDeveloperLink"), + new ProductPageSectionMapping("shelf", "information"), + new ProductPageSectionMapping("shelf", "purchasedLinks"), + new ProductPageSectionMapping("shelf", "capabilities"), + new ProductPageSectionMapping("shelf", "featuredIn"), + new ProductPageSectionMapping("shelf", "moreByDeveloper"), +]; +//# sourceMappingURL=product-page-shelf-ordering-ios.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelf-based/product-page-shelf-ordering/product-page-shelf-ordering-mac-arcade.js b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelf-based/product-page-shelf-ordering/product-page-shelf-ordering-mac-arcade.js new file mode 100644 index 0000000..39d0345 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelf-based/product-page-shelf-ordering/product-page-shelf-ordering-mac-arcade.js @@ -0,0 +1,62 @@ +import { ProductPageSectionMapping } from "../product-page-section-mapping"; +/// The at which to place a section that is in the spotlight. +export const purchasedSpotlightIndex = 5; +export const notPurchasedSpotlightIndex = 5; +// MARK: - Installed +export const purchasedOrdering = [ + new ProductPageSectionMapping("header", "header"), + new ProductPageSectionMapping("topLockup", "topLockup"), + new ProductPageSectionMapping("shelf", "informationRibbon"), + new ProductPageSectionMapping("shelf", "preorderDisclaimer"), + new ProductPageSectionMapping("shelf", "appEvents"), + new ProductPageSectionMapping("shelf", "mostRecentVersion"), + new ProductPageSectionMapping("shelf", "achievements"), + new ProductPageSectionMapping("shelf", "friendsPlaying"), + new ProductPageSectionMapping("shelf", "videos"), + new ProductPageSectionMapping("screenshots", "screenshots"), + new ProductPageSectionMapping("shelf", "description"), + new ProductPageSectionMapping("shelf", "editorialQuote"), + new ProductPageSectionMapping("shelf", "reviews"), + new ProductPageSectionMapping("shelf", "privacyHeader"), + new ProductPageSectionMapping("shelf", "privacyTypes"), + new ProductPageSectionMapping("shelf", "privacyFooter"), + new ProductPageSectionMapping("shelf", "accessibilityHeader"), + new ProductPageSectionMapping("shelf", "accessibilityFeatures"), + new ProductPageSectionMapping("shelf", "accessibilityDeveloperLink"), + new ProductPageSectionMapping("shelf", "information"), + new ProductPageSectionMapping("shelf", "purchasedLinks"), + new ProductPageSectionMapping("shelf", "capabilities"), + new ProductPageSectionMapping("shelf", "featuredIn"), + new ProductPageSectionMapping("shelf", "similarItems"), + new ProductPageSectionMapping("shelf", "moreByDeveloper"), +]; +// MARK: - Not Installed +/// The standard ordering to use for an app that is not installed. +export const notPurchasedOrdering = [ + new ProductPageSectionMapping("header", "header"), + new ProductPageSectionMapping("topLockup", "topLockup"), + new ProductPageSectionMapping("shelf", "informationRibbon"), + new ProductPageSectionMapping("shelf", "preorderDisclaimer"), + new ProductPageSectionMapping("screenshots", "screenshots"), + new ProductPageSectionMapping("shelf", "description"), + new ProductPageSectionMapping("shelf", "editorialQuote"), + new ProductPageSectionMapping("shelf", "videos"), + new ProductPageSectionMapping("shelf", "reviews"), + new ProductPageSectionMapping("shelf", "appEvents"), + new ProductPageSectionMapping("shelf", "achievements"), + new ProductPageSectionMapping("shelf", "friendsPlaying"), + new ProductPageSectionMapping("shelf", "mostRecentVersion"), + new ProductPageSectionMapping("shelf", "privacyHeader"), + new ProductPageSectionMapping("shelf", "privacyTypes"), + new ProductPageSectionMapping("shelf", "privacyFooter"), + new ProductPageSectionMapping("shelf", "accessibilityHeader"), + new ProductPageSectionMapping("shelf", "accessibilityFeatures"), + new ProductPageSectionMapping("shelf", "accessibilityDeveloperLink"), + new ProductPageSectionMapping("shelf", "information"), + new ProductPageSectionMapping("shelf", "notPurchasedLinks"), + new ProductPageSectionMapping("shelf", "capabilities"), + new ProductPageSectionMapping("shelf", "featuredIn"), + new ProductPageSectionMapping("shelf", "similarItems"), + new ProductPageSectionMapping("shelf", "moreByDeveloper"), +]; +//# sourceMappingURL=product-page-shelf-ordering-mac-arcade.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelf-based/product-page-shelf-ordering/product-page-shelf-ordering-mac.js b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelf-based/product-page-shelf-ordering/product-page-shelf-ordering-mac.js new file mode 100644 index 0000000..032cb9a --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelf-based/product-page-shelf-ordering/product-page-shelf-ordering-mac.js @@ -0,0 +1,70 @@ +import { ProductPageSectionMapping } from "../product-page-section-mapping"; +/// The at which to place a section that is in the spotlight. +export const purchasedSpotlightIndex = 5; +export const notPurchasedSpotlightIndex = 5; +// MARK: - Installed +export const purchasedOrdering = [ + new ProductPageSectionMapping("header", "header"), + new ProductPageSectionMapping("topLockup", "topLockup"), + new ProductPageSectionMapping("shelf", "informationRibbon"), + new ProductPageSectionMapping("shelf", "preorderDisclaimer"), + new ProductPageSectionMapping("shelf", "bundleChildren"), + new ProductPageSectionMapping("shelf", "bundleParents"), + new ProductPageSectionMapping("shelf", "appEvents"), + new ProductPageSectionMapping("shelf", "mostRecentVersion"), + new ProductPageSectionMapping("shelf", "subscriptions"), + new ProductPageSectionMapping("shelf", "inAppPurchases"), + new ProductPageSectionMapping("shelf", "achievements"), + new ProductPageSectionMapping("shelf", "friendsPlaying"), + new ProductPageSectionMapping("shelf", "videos"), + new ProductPageSectionMapping("screenshots", "screenshots"), + new ProductPageSectionMapping("shelf", "description"), + new ProductPageSectionMapping("shelf", "editorialQuote"), + new ProductPageSectionMapping("shelf", "reviews"), + new ProductPageSectionMapping("shelf", "privacyHeader"), + new ProductPageSectionMapping("shelf", "privacyTypes"), + new ProductPageSectionMapping("shelf", "privacyFooter"), + new ProductPageSectionMapping("shelf", "accessibilityHeader"), + new ProductPageSectionMapping("shelf", "accessibilityFeatures"), + new ProductPageSectionMapping("shelf", "accessibilityDeveloperLink"), + new ProductPageSectionMapping("shelf", "information"), + new ProductPageSectionMapping("shelf", "purchasedLinks"), + new ProductPageSectionMapping("shelf", "capabilities"), + new ProductPageSectionMapping("shelf", "featuredIn"), + new ProductPageSectionMapping("shelf", "moreByDeveloper"), + new ProductPageSectionMapping("shelf", "similarItems"), +]; +// MARK: - Not Installed +/// The standard ordering to use for an app that is not installed. +export const notPurchasedOrdering = [ + new ProductPageSectionMapping("header", "header"), + new ProductPageSectionMapping("topLockup", "topLockup"), + new ProductPageSectionMapping("shelf", "informationRibbon"), + new ProductPageSectionMapping("shelf", "preorderDisclaimer"), + new ProductPageSectionMapping("shelf", "bundleChildren"), + new ProductPageSectionMapping("shelf", "bundleParents"), + new ProductPageSectionMapping("screenshots", "screenshots"), + new ProductPageSectionMapping("shelf", "description"), + new ProductPageSectionMapping("shelf", "appEvents"), + new ProductPageSectionMapping("shelf", "editorialQuote"), + new ProductPageSectionMapping("shelf", "videos"), + new ProductPageSectionMapping("shelf", "reviews"), + new ProductPageSectionMapping("shelf", "achievements"), + new ProductPageSectionMapping("shelf", "friendsPlaying"), + new ProductPageSectionMapping("shelf", "mostRecentVersion"), + new ProductPageSectionMapping("shelf", "subscriptions"), + new ProductPageSectionMapping("shelf", "inAppPurchases"), + new ProductPageSectionMapping("shelf", "privacyHeader"), + new ProductPageSectionMapping("shelf", "privacyTypes"), + new ProductPageSectionMapping("shelf", "privacyFooter"), + new ProductPageSectionMapping("shelf", "accessibilityHeader"), + new ProductPageSectionMapping("shelf", "accessibilityFeatures"), + new ProductPageSectionMapping("shelf", "accessibilityDeveloperLink"), + new ProductPageSectionMapping("shelf", "information"), + new ProductPageSectionMapping("shelf", "notPurchasedLinks"), + new ProductPageSectionMapping("shelf", "capabilities"), + new ProductPageSectionMapping("shelf", "featuredIn"), + new ProductPageSectionMapping("shelf", "moreByDeveloper"), + new ProductPageSectionMapping("shelf", "similarItems"), +]; +//# sourceMappingURL=product-page-shelf-ordering-mac.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelf-based/product-page-shelf-ordering/product-page-shelf-ordering-tvos-arcade.js b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelf-based/product-page-shelf-ordering/product-page-shelf-ordering-tvos-arcade.js new file mode 100644 index 0000000..61536f4 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelf-based/product-page-shelf-ordering/product-page-shelf-ordering-tvos-arcade.js @@ -0,0 +1,50 @@ +import { ProductPageSectionMapping } from "../product-page-section-mapping"; +/// The at which to place a section that is in the spotlight. +export const purchasedSpotlightIndex = 5; +export const notPurchasedSpotlightIndex = 5; +// MARK: - Installed +export const purchasedOrdering = [ + new ProductPageSectionMapping("header", "header"), + new ProductPageSectionMapping("topLockup", "topLockup"), + new ProductPageSectionMapping("shelf", "videos"), + new ProductPageSectionMapping("screenshots", "screenshots"), + new ProductPageSectionMapping("shelf", "textCards"), + new ProductPageSectionMapping("shelf", "achievements"), + new ProductPageSectionMapping("shelf", "friendsPlaying"), + new ProductPageSectionMapping("shelf", "purchasedRatingsAndReviews"), + new ProductPageSectionMapping("shelf", "similarItems"), + new ProductPageSectionMapping("shelf", "moreByDeveloper"), + new ProductPageSectionMapping("shelf", "privacyHeader"), + new ProductPageSectionMapping("shelf", "privacyTypes"), + new ProductPageSectionMapping("shelf", "privacyFooter"), + new ProductPageSectionMapping("shelf", "accessibilityHeader"), + new ProductPageSectionMapping("shelf", "accessibilityFeatures"), + new ProductPageSectionMapping("shelf", "accessibilityDeveloperLink"), + new ProductPageSectionMapping("shelf", "information"), + new ProductPageSectionMapping("shelf", "purchasedLinks"), + new ProductPageSectionMapping("shelf", "textLinksShelf"), +]; +// MARK: - Not Installed +/// The standard ordering to use for an app that is not installed. +export const notPurchasedOrdering = [ + new ProductPageSectionMapping("header", "header"), + new ProductPageSectionMapping("topLockup", "topLockup"), + new ProductPageSectionMapping("screenshots", "screenshots"), + new ProductPageSectionMapping("shelf", "textCards"), + new ProductPageSectionMapping("shelf", "videos"), + new ProductPageSectionMapping("shelf", "notPurchasedRatingsAndReviews"), + new ProductPageSectionMapping("shelf", "achievements"), + new ProductPageSectionMapping("shelf", "friendsPlaying"), + new ProductPageSectionMapping("shelf", "similarItems"), + new ProductPageSectionMapping("shelf", "moreByDeveloper"), + new ProductPageSectionMapping("shelf", "privacyHeader"), + new ProductPageSectionMapping("shelf", "privacyTypes"), + new ProductPageSectionMapping("shelf", "privacyFooter"), + new ProductPageSectionMapping("shelf", "accessibilityHeader"), + new ProductPageSectionMapping("shelf", "accessibilityFeatures"), + new ProductPageSectionMapping("shelf", "accessibilityDeveloperLink"), + new ProductPageSectionMapping("shelf", "information"), + new ProductPageSectionMapping("shelf", "notPurchasedLinks"), + new ProductPageSectionMapping("shelf", "textLinksShelf"), +]; +//# sourceMappingURL=product-page-shelf-ordering-tvos-arcade.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelf-based/product-page-shelf-ordering/product-page-shelf-ordering-tvos.js b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelf-based/product-page-shelf-ordering/product-page-shelf-ordering-tvos.js new file mode 100644 index 0000000..f0067ef --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelf-based/product-page-shelf-ordering/product-page-shelf-ordering-tvos.js @@ -0,0 +1,50 @@ +import { ProductPageSectionMapping } from "../product-page-section-mapping"; +/// The at which to place a section that is in the spotlight. +export const purchasedSpotlightIndex = 5; +export const notPurchasedSpotlightIndex = 5; +// MARK: - Installed +export const purchasedOrdering = [ + new ProductPageSectionMapping("header", "header"), + new ProductPageSectionMapping("topLockup", "topLockup"), + new ProductPageSectionMapping("shelf", "videos"), + new ProductPageSectionMapping("screenshots", "screenshots"), + new ProductPageSectionMapping("shelf", "textCards"), + new ProductPageSectionMapping("shelf", "achievements"), + new ProductPageSectionMapping("shelf", "friendsPlaying"), + new ProductPageSectionMapping("shelf", "purchasedRatingsAndReviews"), + new ProductPageSectionMapping("shelf", "moreByDeveloper"), + new ProductPageSectionMapping("shelf", "similarItems"), + new ProductPageSectionMapping("shelf", "privacyHeader"), + new ProductPageSectionMapping("shelf", "privacyTypes"), + new ProductPageSectionMapping("shelf", "privacyFooter"), + new ProductPageSectionMapping("shelf", "accessibilityHeader"), + new ProductPageSectionMapping("shelf", "accessibilityFeatures"), + new ProductPageSectionMapping("shelf", "accessibilityDeveloperLink"), + new ProductPageSectionMapping("shelf", "information"), + new ProductPageSectionMapping("shelf", "purchasedLinks"), + new ProductPageSectionMapping("shelf", "textLinksShelf"), +]; +// MARK: - Not Installed +/// The standard ordering to use for an app that is not installed. +export const notPurchasedOrdering = [ + new ProductPageSectionMapping("header", "header"), + new ProductPageSectionMapping("topLockup", "topLockup"), + new ProductPageSectionMapping("screenshots", "screenshots"), + new ProductPageSectionMapping("shelf", "textCards"), + new ProductPageSectionMapping("shelf", "videos"), + new ProductPageSectionMapping("shelf", "notPurchasedRatingsAndReviews"), + new ProductPageSectionMapping("shelf", "achievements"), + new ProductPageSectionMapping("shelf", "friendsPlaying"), + new ProductPageSectionMapping("shelf", "moreByDeveloper"), + new ProductPageSectionMapping("shelf", "similarItems"), + new ProductPageSectionMapping("shelf", "privacyHeader"), + new ProductPageSectionMapping("shelf", "privacyTypes"), + new ProductPageSectionMapping("shelf", "privacyFooter"), + new ProductPageSectionMapping("shelf", "accessibilityHeader"), + new ProductPageSectionMapping("shelf", "accessibilityFeatures"), + new ProductPageSectionMapping("shelf", "accessibilityDeveloperLink"), + new ProductPageSectionMapping("shelf", "information"), + new ProductPageSectionMapping("shelf", "notPurchasedLinks"), + new ProductPageSectionMapping("shelf", "textLinksShelf"), +]; +//# sourceMappingURL=product-page-shelf-ordering-tvos.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelf-based/product-page-shelf-ordering/product-page-shelf-ordering-visionos-arcade.js b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelf-based/product-page-shelf-ordering/product-page-shelf-ordering-visionos-arcade.js new file mode 100644 index 0000000..38f85f2 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelf-based/product-page-shelf-ordering/product-page-shelf-ordering-visionos-arcade.js @@ -0,0 +1,54 @@ +import { ProductPageSectionMapping } from "../product-page-section-mapping"; +/// The index at which to place a section that is in the spotlight. +export const purchasedSpotlightIndex = 5; +export const notPurchasedSpotlightIndex = 5; +// MARK: - Installed +export const purchasedOrdering = [ + new ProductPageSectionMapping("header", "header"), + new ProductPageSectionMapping("topLockup", "topLockup"), + new ProductPageSectionMapping("shelf", "preorderDisclaimer"), + new ProductPageSectionMapping("shelf", "achievements"), + new ProductPageSectionMapping("shelf", "friendsPlaying"), + new ProductPageSectionMapping("shelf", "videos"), + new ProductPageSectionMapping("screenshots", "screenshots"), + new ProductPageSectionMapping("shelf", "textCards"), + new ProductPageSectionMapping("shelf", "productReviewsHeader"), + new ProductPageSectionMapping("shelf", "purchasedRatingsAndReviews"), + new ProductPageSectionMapping("shelf", "privacyHeader"), + new ProductPageSectionMapping("shelf", "privacyTypes"), + new ProductPageSectionMapping("shelf", "privacyFooter"), + new ProductPageSectionMapping("shelf", "accessibilityHeader"), + new ProductPageSectionMapping("shelf", "accessibilityFeatures"), + new ProductPageSectionMapping("shelf", "accessibilityDeveloperLink"), + new ProductPageSectionMapping("shelf", "information"), + new ProductPageSectionMapping("shelf", "purchasedLinks"), + new ProductPageSectionMapping("shelf", "capabilities"), + new ProductPageSectionMapping("shelf", "similarItems"), + new ProductPageSectionMapping("shelf", "moreByDeveloper"), +]; +// MARK: - Not Installed +/// The standard ordering to use for an app that is not installed. +export const notPurchasedOrdering = [ + new ProductPageSectionMapping("header", "header"), + new ProductPageSectionMapping("topLockup", "topLockup"), + new ProductPageSectionMapping("shelf", "preorderDisclaimer"), + new ProductPageSectionMapping("screenshots", "screenshots"), + new ProductPageSectionMapping("shelf", "textCards"), + new ProductPageSectionMapping("shelf", "videos"), + new ProductPageSectionMapping("shelf", "productReviewsHeader"), + new ProductPageSectionMapping("shelf", "notPurchasedRatingsAndReviews"), + new ProductPageSectionMapping("shelf", "achievements"), + new ProductPageSectionMapping("shelf", "friendsPlaying"), + new ProductPageSectionMapping("shelf", "privacyHeader"), + new ProductPageSectionMapping("shelf", "privacyTypes"), + new ProductPageSectionMapping("shelf", "privacyFooter"), + new ProductPageSectionMapping("shelf", "accessibilityHeader"), + new ProductPageSectionMapping("shelf", "accessibilityFeatures"), + new ProductPageSectionMapping("shelf", "accessibilityDeveloperLink"), + new ProductPageSectionMapping("shelf", "information"), + new ProductPageSectionMapping("shelf", "notPurchasedLinks"), + new ProductPageSectionMapping("shelf", "capabilities"), + new ProductPageSectionMapping("shelf", "similarItems"), + new ProductPageSectionMapping("shelf", "moreByDeveloper"), +]; +//# sourceMappingURL=product-page-shelf-ordering-visionos-arcade.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelf-based/product-page-shelf-ordering/product-page-shelf-ordering-visionos.js b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelf-based/product-page-shelf-ordering/product-page-shelf-ordering-visionos.js new file mode 100644 index 0000000..91ffa9d --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelf-based/product-page-shelf-ordering/product-page-shelf-ordering-visionos.js @@ -0,0 +1,50 @@ +import { ProductPageSectionMapping } from "../product-page-section-mapping"; +/// The index at which to place a section that is in the spotlight. +export const purchasedSpotlightIndex = 5; +export const notPurchasedSpotlightIndex = 5; +// MARK: - Installed +export const purchasedOrdering = [ + new ProductPageSectionMapping("header", "header"), + new ProductPageSectionMapping("topLockup", "topLockup"), + new ProductPageSectionMapping("shelf", "preorderDisclaimer"), + new ProductPageSectionMapping("shelf", "videos"), + new ProductPageSectionMapping("screenshots", "screenshots"), + new ProductPageSectionMapping("shelf", "textCards"), + new ProductPageSectionMapping("shelf", "productReviewsHeader"), + new ProductPageSectionMapping("shelf", "purchasedRatingsAndReviews"), + new ProductPageSectionMapping("shelf", "privacyHeader"), + new ProductPageSectionMapping("shelf", "privacyTypes"), + new ProductPageSectionMapping("shelf", "privacyFooter"), + new ProductPageSectionMapping("shelf", "accessibilityHeader"), + new ProductPageSectionMapping("shelf", "accessibilityFeatures"), + new ProductPageSectionMapping("shelf", "accessibilityDeveloperLink"), + new ProductPageSectionMapping("shelf", "information"), + new ProductPageSectionMapping("shelf", "purchasedLinks"), + new ProductPageSectionMapping("shelf", "capabilities"), + new ProductPageSectionMapping("shelf", "moreByDeveloper"), + new ProductPageSectionMapping("shelf", "similarItems"), +]; +// MARK: - Not Installed +/// The standard ordering to use for an app that is not installed. +export const notPurchasedOrdering = [ + new ProductPageSectionMapping("header", "header"), + new ProductPageSectionMapping("topLockup", "topLockup"), + new ProductPageSectionMapping("shelf", "preorderDisclaimer"), + new ProductPageSectionMapping("screenshots", "screenshots"), + new ProductPageSectionMapping("shelf", "textCards"), + new ProductPageSectionMapping("shelf", "videos"), + new ProductPageSectionMapping("shelf", "productReviewsHeader"), + new ProductPageSectionMapping("shelf", "notPurchasedRatingsAndReviews"), + new ProductPageSectionMapping("shelf", "privacyHeader"), + new ProductPageSectionMapping("shelf", "privacyTypes"), + new ProductPageSectionMapping("shelf", "privacyFooter"), + new ProductPageSectionMapping("shelf", "accessibilityHeader"), + new ProductPageSectionMapping("shelf", "accessibilityFeatures"), + new ProductPageSectionMapping("shelf", "accessibilityDeveloperLink"), + new ProductPageSectionMapping("shelf", "information"), + new ProductPageSectionMapping("shelf", "notPurchasedLinks"), + new ProductPageSectionMapping("shelf", "capabilities"), + new ProductPageSectionMapping("shelf", "moreByDeveloper"), + new ProductPageSectionMapping("shelf", "similarItems"), +]; +//# sourceMappingURL=product-page-shelf-ordering-visionos.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelf-based/product-page-shelf-ordering/product-page-shelf-ordering-watch.js b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelf-based/product-page-shelf-ordering/product-page-shelf-ordering-watch.js new file mode 100644 index 0000000..aeec356 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelf-based/product-page-shelf-ordering/product-page-shelf-ordering-watch.js @@ -0,0 +1,28 @@ +import { ProductPageSectionMapping } from "../product-page-section-mapping"; +/// The at which to place a section that is in the spotlight. +export const purchasedSpotlightIndex = 3; +export const notPurchasedSpotlightIndex = 3; +// MARK: - Installed +export const purchasedOrdering = [ + new ProductPageSectionMapping("header", "header"), + new ProductPageSectionMapping("topLockup", "topLockup"), + new ProductPageSectionMapping("shelf", "mostRecentVersion"), + new ProductPageSectionMapping("screenshots", "screenshots"), + new ProductPageSectionMapping("shelf", "description"), + new ProductPageSectionMapping("shelf", "privacyHeader"), + new ProductPageSectionMapping("shelf", "privacyTypes"), + new ProductPageSectionMapping("shelf", "actionLinks"), +]; +// MARK: - Not Installed +/// The standard ordering to use for an app that is not installed. +export const notPurchasedOrdering = [ + new ProductPageSectionMapping("header", "header"), + new ProductPageSectionMapping("topLockup", "topLockup"), + new ProductPageSectionMapping("screenshots", "screenshots"), + new ProductPageSectionMapping("shelf", "description"), + new ProductPageSectionMapping("shelf", "mostRecentVersion"), + new ProductPageSectionMapping("shelf", "privacyHeader"), + new ProductPageSectionMapping("shelf", "privacyTypes"), + new ProductPageSectionMapping("shelf", "actionLinks"), +]; +//# sourceMappingURL=product-page-shelf-ordering-watch.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelf-based/product-page-shelf-ordering/product-page-shelf-ordering-web-arcade.js b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelf-based/product-page-shelf-ordering/product-page-shelf-ordering-web-arcade.js new file mode 100644 index 0000000..205553a --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelf-based/product-page-shelf-ordering/product-page-shelf-ordering-web-arcade.js @@ -0,0 +1,15 @@ +import * as iosOrdering from "./product-page-shelf-ordering-ios-arcade"; +/// The index at which to place a section that is in the spotlight. +export const purchasedSpotlightIndex = iosOrdering.purchasedSpotlightIndex; +export const notPurchasedSpotlightIndex = iosOrdering.notPurchasedSpotlightIndex; +// MARK: - Installed +export const purchasedOrdering = [...iosOrdering.purchasedOrdering]; +export const purchasedOrderingCompact = [...iosOrdering.purchasedOrderingCompact]; +// MARK: - Not Installed +/// The standard ordering to use for an app that is not installed. +export const notPurchasedOrdering = [...iosOrdering.notPurchasedOrdering]; +export const notPurchasedOrderingCompact = [...iosOrdering.notPurchasedOrderingCompact]; +// MARK: - During Download +export const downloadingOrdering = [...iosOrdering.downloadingOrdering]; +export const downloadingOrderingCompact = [...iosOrdering.downloadingOrderingCompact]; +//# sourceMappingURL=product-page-shelf-ordering-web-arcade.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelf-based/product-page-shelf-ordering/product-page-shelf-ordering-web.js b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelf-based/product-page-shelf-ordering/product-page-shelf-ordering-web.js new file mode 100644 index 0000000..ee1a8fd --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelf-based/product-page-shelf-ordering/product-page-shelf-ordering-web.js @@ -0,0 +1,15 @@ +import * as iosOrdering from "./product-page-shelf-ordering-ios"; +/// The index at which to place a section that is in the spotlight. +export const purchasedSpotlightIndex = iosOrdering.purchasedSpotlightIndex; +export const notPurchasedSpotlightIndex = iosOrdering.notPurchasedSpotlightIndex; +// MARK: - Installed +export const purchasedOrdering = [...iosOrdering.purchasedOrdering]; +export const purchasedOrderingCompact = [...iosOrdering.purchasedOrderingCompact]; +// MARK: - Not Installed +/// The standard ordering to use for an app that is not installed. +export const notPurchasedOrdering = [...iosOrdering.notPurchasedOrdering]; +export const notPurchasedOrderingCompact = [...iosOrdering.notPurchasedOrderingCompact]; +// MARK: - During Download +export const downloadingOrdering = [...iosOrdering.downloadingOrdering]; +export const downloadingOrderingCompact = [...iosOrdering.downloadingOrderingCompact]; +//# sourceMappingURL=product-page-shelf-ordering-web.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelf-based/product-page-shelf-ordering/product-page-shelf-ordering.js b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelf-based/product-page-shelf-ordering/product-page-shelf-ordering.js new file mode 100644 index 0000000..ec5f39b --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelf-based/product-page-shelf-ordering/product-page-shelf-ordering.js @@ -0,0 +1,117 @@ +// +// product-page-shelf-ordering.ts +// AppStoreKit +// +// Created by Dersu Abolfathi on 2/15/17. +// Copyright (c) 2017 Apple Inc. All rights reserved. +// +import { ProductPageSectionMapping } from "../product-page-section-mapping"; +import * as iOSOrdering from "./product-page-shelf-ordering-ios"; +import * as iOSArcadeOrdering from "./product-page-shelf-ordering-ios-arcade"; +import * as macOrdering from "./product-page-shelf-ordering-mac"; +import * as macArcadeOrdering from "./product-page-shelf-ordering-mac-arcade"; +import * as tvOSOrdering from "./product-page-shelf-ordering-tvos"; +import * as tvOSArcadeOrdering from "./product-page-shelf-ordering-tvos-arcade"; +import * as watchOSOrdering from "./product-page-shelf-ordering-watch"; +import * as visionOSOrdering from "./product-page-shelf-ordering-visionos"; +import * as visionOSArcadeOrdering from "./product-page-shelf-ordering-visionos-arcade"; +import * as webOrdering from "./product-page-shelf-ordering-web"; +import * as webArcadeOrdering from "./product-page-shelf-ordering-web-arcade"; +export function orderingForPlatform(objectGraph, hostPlatform, supportsArcade) { + switch (hostPlatform) { + case "tvOS": + return supportsArcade ? tvOSArcadeOrdering : tvOSOrdering; + case "watchOS": + return watchOSOrdering; + case "iOS": + return supportsArcade ? iOSArcadeOrdering : iOSOrdering; + case "macOS": + return supportsArcade ? macArcadeOrdering : macOrdering; + case "xrOS": + return supportsArcade ? visionOSArcadeOrdering : visionOSOrdering; + case "web": + return supportsArcade ? webArcadeOrdering : webOrdering; + default: + return iOSOrdering; + } +} +/** + * Determines the overall section ordering for a product page. + * @param objectGraph + * @param isPurchased = Whether the requested ordering is for the purchased state. + * @param hostPlatform + * @param supportsArcade + * @param isCompact - ordering for compact display + * @param isDownloading Whether the section ordering is for the downloading order of a product page. + * @param options - The options to use for the product page configuration. + * note: - This function is backed by a small, straightforward set of human-readable + * arrays. Customization with regards to 'spotlighting' (placing a section at the + * top of the page, depending on how the product page was reached) is applied through + * the 'options' parameter. + * @return The ordering for the overall product page sections. + */ +export function sectionOrdering(objectGraph, isPurchased, hostPlatform, supportsArcade, isCompact, isDownloading, options) { + const ordering = orderingForPlatform(objectGraph, hostPlatform, supportsArcade); + // Create a shallow copy of the appropriate ordering + let sections = []; + if (isPurchased) { + sections = + isCompact && ordering.purchasedOrderingCompact + ? ordering.purchasedOrderingCompact.slice() + : ordering.purchasedOrdering.slice(); + } + else if (isDownloading && ordering.downloadingOrdering) { + sections = + isCompact && ordering.downloadingOrderingCompact + ? ordering.downloadingOrderingCompact.slice() + : ordering.downloadingOrdering.slice(); + } + else { + sections = + isCompact && ordering.notPurchasedOrderingCompact + ? ordering.notPurchasedOrderingCompact.slice() + : ordering.notPurchasedOrdering.slice(); + } + const spotlightIndex = isPurchased ? ordering.purchasedSpotlightIndex : ordering.notPurchasedSpotlightIndex; + // Case: Our options dictate spotlighting of a section + if (options && options.spotlightSection) { + // Re-Insert the section at the spotlight index + const index = indexOfSectionInOrdering(options.spotlightSection, sections); + if (index !== -1) { + sections.splice(index, 1); + sections.splice(spotlightIndex, 0, options.spotlightSection); + } + } + return sections; +} +/** + * Determines the index of a particular section within an ordering of sections. + * @param section The section in question. + * @param ordering The ordering in which the section's index is desired. + * @returns The index of the section, or -1 if it does not appear in the ordering. + */ +function indexOfSectionInOrdering(section, ordering) { + for (let index = 0; index < ordering.length; index++) { + const aSection = ordering[index]; + if (section.sectionType === aSection.sectionType && + section.shelfId === aSection.shelfId && + JSON.stringify(section.shelfBasedMappedIds) === JSON.stringify(aSection.shelfBasedMappedIds)) { + return index; + } + } + return -1; +} +/** + * Determines the appropriate product page section for the iAP type. + * @param isSubscription The type for the iAP product. + * @returns {ProductPageSection} The section appropriate for this iAP type. + */ +export function inAppPurchaseSection(isSubscription) { + if (isSubscription) { + return new ProductPageSectionMapping("shelf", "subscriptions"); + } + else { + return new ProductPageSectionMapping("shelf", "inAppPurchases"); + } +} +//# sourceMappingURL=product-page-shelf-ordering.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelf-based/shelf-based-product-page.js b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelf-based/shelf-based-product-page.js new file mode 100644 index 0000000..572e8b9 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelf-based/shelf-based-product-page.js @@ -0,0 +1,1059 @@ +// +// product-page.ts +// AppStoreKit +// +// Created by Kevin MacWhinnie on 8/17/16. +// Copyright (c) 2016 Apple Inc. All rights reserved. +// +import { isSome } from "@jet/environment"; +import * as validation from "@jet/environment/json/validation"; +import * as models from "../../../api/models"; +import { AdInteractionAction, SelectAppAction } from "../../../api/models"; +import { AdvertActionMetrics, } from "../../../api/models/metrics/advert-action-metrics"; +import * as serverData from "../../../foundation/json-parsing/server-data"; +import * as mediaAttributes from "../../../foundation/media/attributes"; +import * as mediaDataStructure from "../../../foundation/media/data-structure"; +import * as mediaRelationship from "../../../foundation/media/relationships"; +import { Parameters } from "../../../foundation/network/url-constants"; +import * as urls from "../../../foundation/network/urls"; +import * as color from "../../../foundation/util/color-util"; +import { isAdPlacementEnabled } from "../../ads/ad-common"; +import { isNothing } from "@jet/environment/types/optional"; +import { makeDeveloperPageIntent } from "../../../api/intents/developer-page-intent"; +import { makeGamesResponse } from "../../../gameservicesui/src/api/data-models/game-mapi"; +import { deviceFamilyForAccessibilityLabels } from "../../accessibility/accessibility-common"; +import * as appPromotion from "../../app-promotions/app-promotion"; +import * as appPromotionsShelf from "../../app-promotions/app-promotions-shelf"; +import * as contentAttributes from "../../content/attributes"; +import * as content from "../../content/content"; +import * as filtering from "../../filtering"; +import { shelfForUnifiedMessage } from "../../grouping/grouping-common"; +import * as externalDeepLink from "../../linking/external-deep-link"; +import { getLocale } from "../../locale"; +import { attachAdActionMetricsToAction } from "../../lockups/ad-lockups"; +import * as metricsHelpersClicks from "../../metrics/helpers/clicks"; +import * as metricsHelpersLocation from "../../metrics/helpers/location"; +import * as metricsHelpersModels from "../../metrics/helpers/models"; +import * as metricsHelpersPage from "../../metrics/helpers/page"; +import * as metricsUtil from "../../metrics/helpers/util"; +import { MetricsReferralContext } from "../../metrics/metrics-referral-context"; +import * as offers from "../../offers/offers"; +import * as privacyFooterShelf from "../../privacy/privacy-footer-shelf"; +import * as privacyHeaderShelf from "../../privacy/privacy-header-shelf"; +import * as privacySuppression from "../../privacy/privacy-suppression"; +import * as privacyTypesShelf from "../../privacy/privacy-types-shelf"; +import * as refresh from "../../refresh/page-refresh-controller"; +import * as sharing from "../../sharing"; +import * as productBadges from "../badges/badges"; +import * as productBadgesCommon from "../badges/badges-common"; +import * as banner from "../banner"; +import * as productPageCommon from "../product-page-common"; +import { ProductPageShelfMetrics } from "../product-page-shelf-metrics"; +import * as productPageUtil from "../product-page-util"; +import * as productPageVariants from "../product-page-variants"; +import * as reviews from "../reviews"; +import * as accessibilityShelves from "../shelves/accessibility-shelves"; +import * as achievementsShelf from "../shelves/achievements-shelf"; +import * as actionLinksShelf from "../shelves/action-links-shelf"; +import * as annotationsShelf from "../shelves/annotations/annotations"; +import * as categoryAnnotation from "../shelves/annotations/category-annotation"; +import * as compatibilityAnnotation from "../shelves/annotations/compatibility-annotation"; +import * as contentRatingAnnotation from "../shelves/annotations/content-rating-annotation"; +import * as highMotionAnnotation from "../shelves/annotations/high-motion-annotation"; +import * as languageAnnotation from "../shelves/annotations/languages-annotation"; +import * as sizeAnnotation from "../shelves/annotations/size-annotation"; +import * as bundleChildrenShelf from "../shelves/bundle-children-shelf"; +import * as bundleParentsShelf from "../shelves/bundle-parents-shelf"; +import * as capabilitiesShelf from "../shelves/capabilities-shelf"; +import * as descriptionShelf from "../shelves/description-shelf"; +import * as editorialQuoteShelf from "../shelves/editorial-quote-shelf"; +import { createEditorsChoiceShelf } from "../shelves/editors-choice-shelf"; +import * as featuredInShelf from "../shelves/featured-in-shelf"; +import * as friendsPlayingShelf from "../shelves/friends-playing-shelf"; +import * as inAppPurchasesShelf from "../shelves/in-app-purchases-shelf"; +import * as linksShelf from "../shelves/links-shelf"; +import * as moreByDeveloperShelf from "../shelves/more-by-developer-shelf"; +import * as productMediaShelf from "../shelves/product-media-shelf"; +import * as ribbonShelf from "../shelves/ribbon-shelf"; +import * as reviewsShelf from "../shelves/shelf-based-reviews-shelves"; +import * as similarItemsShelf from "../shelves/similar-items-shelf"; +import * as smallStoryCardShelf from "../shelves/small-story-card-shelf"; +import * as textLinksShelf from "../shelves/text-links-shelf"; +import * as textCardsShelf from "../shelves/textcard-shelf"; +import * as versionHistoryShelves from "../shelves/version-history-shelves"; +import * as lockups from "./../../lockups/lockups"; +import { productPageShelfIdsFromMappings } from "./product-page-section-mapping"; +import * as shelfBasedProductOrdering from "./product-page-shelf-ordering/product-page-shelf-ordering"; +import { tryAwait } from "../../../foundation/util/promise-util"; +import { withAsyncValidationContext } from "../../../foundation/util/validation-util"; +/** + * Create a side pack using product data. + * + * @param response The raw response for a product page. + * @param options The options to use for the product page configuration. + * @returns A product page side pack model object. + */ +export function createProductPageSidePackFromResponse(objectGraph, response, options) { + return validation.context("createProductPageSidePackFromResponse", () => { + if (!objectGraph.client.isSidepackingEnabled) { + return null; + } + const shelfMetrics = createSidePackShelfMetrics(objectGraph, response, options); + const sidePack = createProductPageSidePack(objectGraph, response, shelfMetrics, options); + // Apply back action for the side pack. + metricsHelpersPage.copyMetricsEventsIntoSidepackedPagewithInformation(objectGraph, sidePack, shelfMetrics.metricsPageInformation); + return sidePack; + }); +} +/** + * Creates a full product page using product data (i.e. a product side pack + all other details required to render a product page). + * + * @param objectGraph + * @param response The raw response for a product page. + * @param options The options to use for the product page configuration. + * @param additionalPageRequirements Optional additional page requirements for the product page response. + * @returns A product page model object. + */ +export async function createProductPageFromResponse(objectGraph, response, options, additionalPageRequirements, referrerData, isViewOnly = false) { + return await withAsyncValidationContext("createProductPageFromResponse", async () => { + var _a; + const productData = mediaDataStructure.dataFromDataContainer(objectGraph, response); + if (serverData.isNullOrEmpty(productData)) { + return null; + } + const isBundle = productData.type === "app-bundles"; + if (objectGraph.client.isWatch && isBundle) { + throw new models.ItemNotAvailableError("Bundles are not supported on watchOS"); + } + const appEventDataContainer = additionalPageRequirements === null || additionalPageRequirements === void 0 ? void 0 : additionalPageRequirements[productPageCommon.appPromotionRequirementKey]; + const appEventData = mediaDataStructure.dataFromDataContainer(objectGraph, appEventDataContainer); + // When we are rendering a product page with an AppEventDetails page on we need to make sure the referral goes + // to the app event not the product page. + const appEventReferrerData = serverData.isDefinedNonNullNonEmpty(appEventData) + ? referrerData + : null; + const productReferrerData = serverData.isDefinedNonNullNonEmpty(appEventData) + ? null + : referrerData; + const shelfMetrics = createProductPageShelfMetrics(objectGraph, response, options); + const productPage = createProductPageSidePack(objectGraph, productData, shelfMetrics, options, appEventData, productReferrerData, appEventReferrerData, isViewOnly); + shelfMetrics.metricsPageInformation.baseFields["platformDisplayStyle"] = + metricsUtil.metricsPlatformDisplayStyleFromData(objectGraph, productData, productPage.lockup.icon, options === null || options === void 0 ? void 0 : options.clientIdentifierOverride); + if (isAdPlacementEnabled(objectGraph, "productPageYMAL") || + isAdPlacementEnabled(objectGraph, "productPageYMALDuringDownload")) { + // Create a new empty IAdSearchInfo, as we won't yet have any ad data. + shelfMetrics.metricsPageInformation.iAdInfo = new metricsHelpersModels.IAdSearchInformation(objectGraph, "productPageYMAL", metricsHelpersModels.IAdSearchInformation.createInitialSlotInfos(objectGraph, "productPageYMAL", null, null)); + } + const appPlatforms = content.supportedAppPlatformsFromData(objectGraph, productData); + const isWebBrowserContext = (_a = options === null || options === void 0 ? void 0 : options.webBrowser) !== null && _a !== void 0 ? _a : false; + if (!isWebBrowserContext) { + // Add metrics events (including the back button event) for this full product page response. + metricsHelpersPage.addMetricsEventsToPageWithInformation(objectGraph, productPage, shelfMetrics.metricsPageInformation, (fields) => { + // <rdar://problem/43026165> Metrics: Add a deviceCompatibility field to the metrics events + const supportsMacOSCompatibleIOSBinary = content.supportsMacOSCompatibleIOSBinaryFromData(objectGraph, productData, objectGraph.appleSilicon.isSupportEnabled); + const supportsVisionOSCompatibleIOSBinary = content.supportsVisionOSCompatibleIOSBinaryFromData(objectGraph, productData); + const isMacOSAppBuyable = content.isMacOSAppBuyableAndRunnableFromData(objectGraph, productData, objectGraph.appleSilicon.isSupportEnabled, objectGraph.appleSilicon.isRosettaAvailable); + const isBuyable = content.buyableOnDevice(objectGraph, productData, appPlatforms, objectGraph.client.deviceType, supportsMacOSCompatibleIOSBinary, supportsVisionOSCompatibleIOSBinary, isMacOSAppBuyable); + const isPreorderable = content.preorderableOnDevice(objectGraph, appPlatforms, objectGraph.client.deviceType, supportsMacOSCompatibleIOSBinary, supportsVisionOSCompatibleIOSBinary); + const isPurchasable = appPlatforms && (isBuyable || isPreorderable); + const isRunnable = content.runnableOnDeviceWithData(objectGraph, productData, objectGraph.client.deviceType, objectGraph.appleSilicon.isSupportEnabled); + if (isPurchasable && isRunnable) { + fields["deviceCompatibility"] = "runnable"; + } + else if (isPurchasable) { + fields["deviceCompatibility"] = "purchasable"; + } + else { + fields["deviceCompatibility"] = "none"; + } + if (serverData.isDefinedNonNullNonEmpty(productReferrerData)) { + MetricsReferralContext.shared.addReferralContextToMetricsFieldsIfNecessary(fields); + } + }, isWebBrowserContext); + } + // Create refresh controller to gather information on when we may need to refresh. + const refreshController = refresh.newPageRefreshControllerFromResponse(response, true); + await addFullProductShelfMappingToProductPage(objectGraph, productPage, productData, shelfMetrics, options, refreshController, isViewOnly); + // Apply refresh policy to page. + productPage.pageRefreshPolicy = refresh.pageRefreshPolicyForController(objectGraph, refreshController); + productPage.isIncomplete = false; + productPage.alwaysAllowReviews = reviews.shouldAllowReviewsForSADApp(objectGraph, productData); + productPage.canonicalURL = mediaAttributes.attributeAsString(productData, "url"); + // Remove interactive elements from view-only product pages. + if (isViewOnly) { + productPage.shareAction = null; + productPage.titleOfferDisplayProperties = null; + productPage.expandedOfferDetails = null; + productPage.lockup.offerDisplayProperties = null; + productPage.banner = null; + } + return productPage; + }); +} +/** + * Creates a side pack model object using JSON data. + * @param objectGraph + * @param data The data representing the product page side pack. + * @param shelfMetrics The metrics that describe the containing shelf. + * @param options Any options to consider whilst creating this model object. + * @param appPromotionData The data representing a deep linked app promotion (App Event or Winback Offer). + * @param productReferrerData The referrerData for the product page + * @param appEventReferrerData The referrer data for the app event details page + */ +function createProductPageSidePack(objectGraph, data, shelfMetrics, options, appPromotionData, productReferrerData, appEventReferrerData, isViewOnly = false) { + return validation.context("createProductPageSidePack", () => { + var _a; + let clientIdentifierOverride; + if (serverData.isDefinedNonNull(options)) { + clientIdentifierOverride = options.clientIdentifierOverride; + } + const id = data.id; + const name = mediaAttributes.attributeAsString(data, "name"); + const isFirstPartyHideableApp = mediaAttributes.attributeAsBooleanOrFalse(data, "isFirstPartyHideableApp"); + const shouldSuppressReviews = reviews.shouldSuppressReviews(objectGraph, data); + const supportsArcade = content.isArcadeSupported(objectGraph, data); + const metricsOptions = { + pageInformation: shelfMetrics.metricsPageInformation, + locationTracker: shelfMetrics.locationTracker, + id: id, + isAdvert: shelfMetrics.metricsPageInformation.iAdInfo + ? shelfMetrics.metricsPageInformation.iAdInfo.iAdIsPresent + : false, + }; + const productPage = new models.ShelfBasedProductPage(); + const productLockup = new models.Lockup(); + // Add metrics information + const isPreorder = mediaAttributes.attributeAsBoolean(data, "isPreorder"); + if (isPreorder && + serverData.isDefinedNonNull(shelfMetrics) && + serverData.isDefinedNonNull(shelfMetrics.metricsPageInformation)) { + shelfMetrics.metricsPageInformation.offerType = "preorder"; + shelfMetrics.metricsPageInformation.offerReleaseDate = offers.expectedReleaseDateFromData(objectGraph, data); + } + productLockup.adamId = id; + productLockup.bundleId = contentAttributes.contentAttributeAsString(objectGraph, data, "bundleId"); + productPage.uber = createUber(objectGraph, data, clientIdentifierOverride); + const useInlineUberStyle = productPage.uber && productPage.uber.style === "inline"; + productLockup.icon = content.iconFromData(objectGraph, data, { + useCase: 10 /* content.ArtworkUseCase.ProductPageIcon */, + withJoeColorPlaceholder: preprocessor.GAMES_TARGET || undefined, + }, clientIdentifierOverride); + productPage.title = name; + productPage.isIncomplete = true; + productLockup.title = name; + productLockup.subtitle = lockups.subtitleFromData(objectGraph, data); + productLockup.developerTagline = contentAttributes.contentAttributeAsString(objectGraph, data, "subtitle"); + productPage.regularPriceFormatted = mediaAttributes.attributeAsString(data, "regularPriceFormatted"); + productPage.logoArtwork = content.productLogoArtworkFromData(objectGraph, data); + productPage.navigationBarIconArtwork = createNavigationBarIconArtwork(objectGraph, data, clientIdentifierOverride); + if (!shouldSuppressReviews) { + productLockup.rating = mediaAttributes.attributeAsNumber(data, "userRating.value"); + } + // For the web client, we show an "Open in Mac App Store" button, which is shown on by basis of whether + // the app's binary is compatible with macOS, so we need to pas this attribute. + if (objectGraph.client.isWeb) { + productLockup.isIOSBinaryMacOSCompatible = mediaAttributes.attributeAsBooleanOrFalse(data, "isIOSBinaryMacOSCompatible"); + productLockup.clickAction = lockups.actionFromData(objectGraph, data, metricsOptions, null); + } + // Add primary text color for tvOS + if (objectGraph.client.isTV && serverData.isDefinedNonNullNonEmpty(productPage.uber)) { + let backgroundColor; + if (serverData.isDefinedNonNull(productPage.uber.video) && + serverData.isDefinedNonNull(productPage.uber.video.preview)) { + backgroundColor = productPage.uber.video.preview.backgroundColor; + } + else if (serverData.isDefinedNonNull(productPage.uber.artwork)) { + backgroundColor = productPage.uber.artwork.backgroundColor; + } + else if (serverData.isDefinedNonNull(productPage.uber.iconArtwork)) { + backgroundColor = productPage.uber.iconArtwork.backgroundColor; + } + productPage.hasDarkUserInterfaceStyle = serverData.isDefinedNonNull(backgroundColor) + ? color.isDarkColor(backgroundColor, 50) + : false; + } + productLockup.children = lockups.childrenFromLockupData(objectGraph, data, { + metricsOptions: metricsOptions, + artworkUseCase: 1 /* content.ArtworkUseCase.LockupIconSmall */, + }); + const productButtonAction = createProductLockupButtonAction(objectGraph, data, productLockup.icon, shelfMetrics, metricsOptions, options, productReferrerData); + productLockup.buttonAction = productButtonAction.buttonAction; + // Release date and pre-order treatment + if (isPreorder) { + // Apply theme + productPage.theme = "blue"; + // Create expanded offer + let expandedOfferTitle; + let expandedOfferSubtitle = null; + if (supportsArcade) { + expandedOfferTitle = ""; + expandedOfferSubtitle = objectGraph.loc.uppercased(content.dynamicPreorderDateFromData(objectGraph, data, objectGraph.loc.string("Offer.Expanded.Title.ComingSoon"))); + } + else { + expandedOfferTitle = objectGraph.loc.string("Offer.Expanded.Title.ComingSoon"); + if (serverData.isDefinedNonNull(productButtonAction.offerAction.expectedReleaseDate)) { + const dateFormat = "MMM d, yyyy"; + const formattedReleaseDate = objectGraph.loc.formatDate(dateFormat, productButtonAction.offerAction.expectedReleaseDate); + const fallbackLabel = objectGraph.loc + .string("PREORDER_EXPANDED_OFFER_SUBTITLE") + .replace("{releaseDate}", formattedReleaseDate); + expandedOfferSubtitle = content.dynamicPreorderDateFromData(objectGraph, data, fallbackLabel); + } + else if (objectGraph.client.isTV) { + // The top lockup for tvOS does not display the expandedOfferTitle on the product page, + // so populate the subtitle with `Coming Soon` instead if there is no expected release date. + expandedOfferSubtitle = objectGraph.loc.string("Offer.Expanded.Title.ComingSoon"); + } + } + productPage.expandedOfferDetails = new models.ProductPageExpandedOfferDetails(expandedOfferTitle, expandedOfferSubtitle); + } + if (useInlineUberStyle && supportsArcade) { + productPage.theme = "white"; + } + // Buy Offer + let offerStyle; + let offerEnvironment; + if (useInlineUberStyle && supportsArcade) { + offerStyle = "white"; + offerEnvironment = "arcadeProductPage"; + } + else { + offerStyle = "colored"; + offerEnvironment = "productPage"; + } + const titleOfferEnvironment = objectGraph.client.isVision || objectGraph.client.isiOS ? "navigationBar" : "productPage"; + const titleOfferStyle = "colored"; + const offerType = supportsArcade ? "arcadeApp" : "app"; + const offerContext = ((_a = options === null || options === void 0 ? void 0 : options.webBrowser) !== null && _a !== void 0 ? _a : false) ? "productPageBrowserChoice" : "productPage"; + productLockup.offerDisplayProperties = offers.displayPropertiesFromOfferAction(objectGraph, productButtonAction.offerAction, offerType, data, isPreorder, false, offerStyle, offerEnvironment, null, null, offerContext, false, false, clientIdentifierOverride); + productPage.titleOfferDisplayProperties = offers.displayPropertiesFromOfferAction(objectGraph, productButtonAction.offerAction, offerType, data, isPreorder, false, titleOfferStyle, titleOfferEnvironment, null, null, offerContext); + // Developer + let developerAction; + const developer = mediaRelationship.relationshipData(objectGraph, data, "developer"); + const developerUrl = content.developerUrlFromDeveloperData(objectGraph, developer); + if (serverData.isDefinedNonNull(developerUrl)) { + developerAction = new models.FlowAction("page"); + developerAction.title = mediaAttributes.attributeAsString(data, "artistName"); + developerAction.pageUrl = developerUrl; + metricsHelpersClicks.addClickEventToAction(objectGraph, developerAction, { + targetType: "button", + id: developer.id, + actionType: "navigate", + pageInformation: shelfMetrics.metricsPageInformation, + locationTracker: shelfMetrics.locationTracker, + }); + if (objectGraph.client.isWeb) { + developerAction.destination = makeDeveloperPageIntent({ + ...getLocale(objectGraph), + id: developer.id, + }); + } + } + productPage.developerAction = developerAction; + // Add share action + productPage.shareAction = sharing.shareProductActionFromData(objectGraph, data, shelfMetrics.metricsPageInformation, shelfMetrics.locationTracker, clientIdentifierOverride); + // Tertiary badge + productLockup.tertiaryTitle = lockups.badgeFromData(objectGraph, data); + productLockup.tertiaryTitleAction = lockups.badgeActionFromData(objectGraph, data); + productLockup.tertiaryTitleArtwork = lockups.badgeArtworkFromData(objectGraph, data); + // Banner Context + const appPlatforms = content.supportedAppPlatformsFromData(objectGraph, data); + const bannerContext = { + appPlatforms: appPlatforms, + clientIdentifierOverride: clientIdentifierOverride, + offerButtonShouldBeDisabled: false, + metricsPageInformation: shelfMetrics.metricsPageInformation, + metricsLocationTracker: shelfMetrics.locationTracker, + webBrowser: options === null || options === void 0 ? void 0 : options.webBrowser, + }; + // Show Banner if present, otherwise show AppStateBanner. + const bannerToDisplay = banner.create(objectGraph, data, bannerContext); + if (bannerToDisplay instanceof models.Banner) { + productPage.banner = bannerToDisplay; + } + else if (bannerToDisplay instanceof models.AppStateBanner) { + productPage.appStateBanner = bannerToDisplay; + } + const offerDisplayProperties = productLockup.offerDisplayProperties; + if (bannerContext.offerButtonShouldBeDisabled && serverData.isDefinedNonNullNonEmpty(offerDisplayProperties)) { + productLockup.offerDisplayProperties = offerDisplayProperties.newOfferDisplayPropertiesChangingAppearance(false, "disabled"); + } + // Configure in-line updates for product page + const isMininimumOSRequirementMet = !filtering.shouldFilter(objectGraph, data, 512 /* filtering.Filter.MinimumOSRequirement */); + if (!isFirstPartyHideableApp && !bannerContext.offerButtonShouldBeDisabled && isMininimumOSRequirementMet) { + const updateOfferData = offers.updateOfferDataFromData(objectGraph, data); + productPage.updateBuyParams = serverData.asString(updateOfferData, "buyParams"); + productPage.externalVersionIdentifier = contentAttributes.contentAttributeAsNumber(objectGraph, data, "externalVersionId"); + } + // Description header + if (objectGraph.client.isTV) { + if (productLockup.developerTagline) { + productPage.descriptionHeader = objectGraph.loc + .string("PRODUCT_DESCRIPTION_HEADER") + .replace("{title}", productPage.title) + .replace("{subtitle}", productLockup.developerTagline); + } + else { + productPage.descriptionHeader = productPage.title; + } + productPage.description = descriptionShelf.paragraphFromData(objectGraph, data); + } + if (supportsArcade) { + productLockup.editorialTagline = objectGraph.loc.string("APPLE_ARCADE"); + } + // Badges + if (objectGraph.client.isTV || objectGraph.client.isWatch || objectGraph.client.isVision) { + const embeddedInRibbon = objectGraph.client.isVision; + productPage.badges = productBadges.badgesFromResponse(objectGraph, data, embeddedInRibbon, { + locationTracker: shelfMetrics.locationTracker, + pageInformation: shelfMetrics.metricsPageInformation, + }); + } + productPage.lockup = productLockup; + productPage.appPlatforms = content.supportedAppPlatformsFromData(objectGraph, data); + // App Promotion + if (serverData.isDefinedNonNull(appPromotionData)) { + const appPromotionOrDate = appPromotion.appPromotionOrDateFromData(objectGraph, appPromotionData, data, false, false, "dark", "white", true, metricsOptions, true, false, null, false, false); + if (serverData.isDefinedNonNull(appPromotionData) && appPromotionOrDate instanceof models.AppPromotion) { + const appPromotionItem = appPromotionOrDate; + productPage.appPromotionDetailPageFlowAction = appPromotion.detailPageFlowActionFromData(objectGraph, appPromotionData, data, appPromotionItem, metricsOptions, "never", false, appEventReferrerData); + } + } + addSidePackShelfMappingToProductPage(objectGraph, productPage, data, shelfMetrics, options, isViewOnly); + if (productPage.uber && !objectGraph.client.isVision) { + productPage.presentationOptions.push("prefersOverlayedPageHeader"); + productPage.presentationOptions.push("prefersNonStandardBackButton"); + } + productPage.alwaysAllowReviews = reviews.shouldAllowReviewsForSADApp(objectGraph, data); + /** + * Adverts: Apply metrics to the lockup action and the back button + */ + const iAdDismissAdActionMetrics = options === null || options === void 0 ? void 0 : options.iAdDismissAdActionMetrics; + if (isSome(iAdDismissAdActionMetrics)) { + const dismissActionMetrics = new AdvertActionMetrics(serverData.asString(iAdDismissAdActionMetrics.instanceId), serverData.asString(iAdDismissAdActionMetrics.adamId), serverData.asString(iAdDismissAdActionMetrics.bundleId), serverData.asString(iAdDismissAdActionMetrics.advertType), serverData.asString(iAdDismissAdActionMetrics.invocation), serverData.asString(iAdDismissAdActionMetrics.purchaseType), serverData.asString(iAdDismissAdActionMetrics.reportingDestination)); + const adInteractionAction = new AdInteractionAction(dismissActionMetrics); + productPage.pageDisappearedAction = adInteractionAction; + const buyActionMetrics = new AdvertActionMetrics(serverData.asString(iAdDismissAdActionMetrics.instanceId), serverData.asString(iAdDismissAdActionMetrics.adamId), serverData.asString(iAdDismissAdActionMetrics.bundleId), serverData.asString(iAdDismissAdActionMetrics.advertType), "offerButtonPress", serverData.asString(iAdDismissAdActionMetrics.purchaseType), serverData.asString(iAdDismissAdActionMetrics.reportingDestination)); + productPage.lockup.buttonAction = attachAdActionMetricsToAction(objectGraph, productPage.lockup.buttonAction, buyActionMetrics); + } + return productPage; + }); +} +/** + * Creates the shelf metrics for a side pack. + * @param data The data describing the side pack. + * @param options The side pack options. + */ +function createSidePackShelfMetrics(objectGraph, data, options) { + const productUrl = mediaAttributes.attributeAsString(data, "url"); + const name = mediaAttributes.attributeAsString(data, "name"); + const artistName = mediaAttributes.attributeAsString(data, "artistName"); + let iAdClickInfo = null; + if (options) { + iAdClickInfo = options.iAdClickFields; + } + const pageInformation = metricsHelpersPage.fakeMetricsPageInformation(objectGraph, "Software", data.id, `${artistName}_${name}`, iAdClickInfo); + pageInformation.pageUrl = productUrl; + const locationTracker = metricsHelpersLocation.newLocationTracker(); + return new ProductPageShelfMetrics(pageInformation, locationTracker); +} +/** + * Creates the shelf metrics for a product page response. + * @param response The product page response. + * @param options The product page options. + */ +function createProductPageShelfMetrics(objectGraph, response, options) { + var _a; + const productData = mediaDataStructure.dataFromDataContainer(objectGraph, response); + const isBundle = productData.type === "app-bundles"; + // Add the toro data for the click to the page data + if (serverData.isDefinedNonNull(options)) { + if (options.iAdClickFields) { + response[metricsHelpersModels.iAdURLParameterStringToken] = JSON.stringify(options.iAdClickFields); + } + if (options.iAdLineItem) { + response[metricsHelpersModels.iAdURLLineItemParameterStringToken] = options.iAdLineItem; + } + } + // <rdar://problem/33764430> Toro: iAd is missing AD_OPEN figaro event when tapping on ad and pressing OPEN through the product page + const pageType = isBundle ? "SoftwareBundle" : "Software"; + const pageInformation = metricsHelpersPage.metricsPageInformationFromMediaApiResponse(objectGraph, pageType, productData.id, response); + const pageUrl = new urls.URL(pageInformation.pageUrl); + const isWebBrowserContext = (_a = options === null || options === void 0 ? void 0 : options.webBrowser) !== null && _a !== void 0 ? _a : false; + if (isWebBrowserContext) { + // If this is the web browser context, swap the `webBrowser=1` param with `context=browserChoice`. + pageUrl.param("context", "browserChoice"); + pageUrl.removeParam("webBrowser"); + pageInformation.pageUrl = pageUrl.build(); + } + const locationTracker = metricsHelpersLocation.newLocationTracker(); + return new ProductPageShelfMetrics(pageInformation, locationTracker); +} +/** + * When configuring a product page, it might be the case that we are doing so with + * the intention of 'spotlighting' a particular section. This essentially means + * that the context of the product page presentation dictates a particular section + * appear at the top (just underneath the top lockup). This function applies these + * spotlighting options to the product page. + * @param options The options for the product page. + * @param productPage + */ +function applySpotlightOptions(objectGraph, options, productPage) { + if (options && options.spotlightSection) { + const spotlightId = options.spotlightInAppProductIdentifier; + if (spotlightId && options.spotlightSection) { + const shelf = productPage.shelfMapping[options.spotlightSection.shelfId]; + if (!shelf) { + return; + } + // Apply Presentation Hints + shelf.presentationHints = { isInProductPageSpotlight: true }; + // Shelf Items + productPageCommon.moveLockupToFront(objectGraph, spotlightId, shelf.items); + // Shelf URL + if (shelf.url) { + const url = new urls.URL(shelf.url); + if (url) { + shelf.url = url.param(Parameters.offerName, spotlightId).build(); + } + } + } + } +} +function addSidePackShelfMappingToProductPage(objectGraph, productPage, data, shelfMetrics, options, isViewOnly) { + productPage.shelfMapping["topLockup"] = new models.Shelf("marker", "productTopLockup"); + if (objectGraph.client.deviceType !== "tv" && !objectGraph.client.isVision) { + const descriptionShelfContext = { + developerAction: productPage.developerAction, + }; + productPage.shelfMapping["description"] = descriptionShelf.create(objectGraph, data, shelfMetrics, descriptionShelfContext); + } + // Badges + if (objectGraph.client.isWeb || objectGraph.client.isiOS || objectGraph.client.isMac) { + const ribbonShelfContext = { + useInlineUberStyle: productPage.uber && productPage.uber.style === "inline", + }; + productPage.shelfMapping["informationRibbon"] = ribbonShelf.create(objectGraph, data, shelfMetrics, ribbonShelfContext); + } + addShelfOrderingToProductPage(objectGraph, productPage, [], content.isArcadeSupported(objectGraph, data), options); +} +async function addFullProductShelfMappingToProductPage(objectGraph, productPage, data, shelfMetrics, options, refreshController, isViewOnly) { + var _a, _b, _c; + const appPlatforms = content.supportedAppPlatformsFromData(objectGraph, data); + const isBundle = data.type === "app-bundles"; + const isPreorder = mediaAttributes.attributeAsBoolean(data, "isPreorder"); + const macOnlyApp = appPlatforms.length === 1 && appPlatforms[0] === "mac"; + const shouldShowMacOnlyReviews = !macOnlyApp || objectGraph.client.isMac; + const shouldSuppressReviews = reviews.shouldSuppressReviews(objectGraph, data); + const shouldShowRatingsAndReviews = (shouldShowMacOnlyReviews || objectGraph.client.isWeb) && !isPreorder && !shouldSuppressReviews; + const clientIdentifierOverride = serverData.isDefinedNonNullNonEmpty(options) + ? options.clientIdentifierOverride + : null; + const isFirstPartyHideableApp = mediaAttributes.attributeAsBooleanOrFalse(data, "isFirstPartyHideableApp"); + const offerDisplayProperties = productPage.lockup.offerDisplayProperties; + const isFreeProduct = serverData.isDefinedNonNull(offerDisplayProperties) ? offerDisplayProperties.isFree : true; + // Bundle Children + let bundleChildren; + if (isBundle) { + const childrenRelationship = mediaRelationship.relationship(data, "apps"); + bundleChildren = bundleChildrenShelf.create(objectGraph, data, childrenRelationship, shelfMetrics); + } + if (bundleChildren) { + productPage.shelfMapping["bundleChildren"] = bundleChildren; + } + // Bundles This App Is Part Of + const bundleParentRelationship = mediaRelationship.relationship(data, "app-bundles"); + const bundleParents = bundleParentsShelf.create(objectGraph, data, bundleParentRelationship, shelfMetrics); + if (bundleParents) { + productPage.shelfMapping["bundleParents"] = bundleParents; + } + if (preprocessor.GAMES_TARGET) { + productPage.gamesResponse = makeGamesResponse(objectGraph, data); + } + const ratingsAndReviewsShelfContext = { + productTitle: productPage.title, + shouldShowRatingsAndReviews: shouldShowRatingsAndReviews, + isFirstPartyHideableApp: isFirstPartyHideableApp, + isBundle: isBundle, + isPreorder: isPreorder, + }; + const ratingsAndReviewsShelves = reviewsShelf.createReviewsShelves(objectGraph, data, shelfMetrics, ratingsAndReviewsShelfContext, productPage.lockup.subtitle); + if (serverData.isDefinedNonNullNonEmpty(ratingsAndReviewsShelves)) { + productPage.shelfMapping["purchasedRatingsAndReviews"] = + ratingsAndReviewsShelves.purchasedRatingsAndReviewsComponentShelf; + productPage.shelfMapping["notPurchasedRatingsAndReviews"] = + ratingsAndReviewsShelves.notPurchasedRatingsAndReviewsComponentShelf; + productPage.shelfMapping["productRatings"] = ratingsAndReviewsShelves.ratingsShelf; + productPage.shelfMapping["allProductReviewActions"] = ratingsAndReviewsShelves.allReviewActionsShelf; + productPage.shelfMapping["allProductReviews"] = ratingsAndReviewsShelves.allProductReviewsShelf; + productPage.shelfMapping["userProductReviews"] = ratingsAndReviewsShelves.userProductReviewsShelf; + if (objectGraph.client.isiOS) { + productPage.shelfMapping["editorsChoice"] = createEditorsChoiceShelf(objectGraph, data); + } + else { + productPage.shelfMapping["editorsChoiceProductReviews"] = + ratingsAndReviewsShelves.editorsChoiceProductReviewsShelf; + } + productPage.shelfMapping["tapToRateProductReviewAction"] = ratingsAndReviewsShelves.tapToRateActionsShelf; + productPage.shelfMapping["writeAReviewProductReviewAction"] = ratingsAndReviewsShelves.writeAReviewActionsShelf; + productPage.shelfMapping["productReviewsHeader"] = ratingsAndReviewsShelves.productReviewsHeader; + } + if (objectGraph.client.isiOS) { + const productReviewsFooter = reviewsShelf.createProductReviewsLearnMoreShelf(objectGraph); + if (isSome(productReviewsFooter)) { + productPage.shelfMapping["productReviewsFooter"] = productReviewsFooter; + } + } + const reviewSummary = reviewsShelf.createReviewSummary(objectGraph, data, shelfMetrics); + if (isSome(reviewSummary) && objectGraph.client.isiOS) { + productPage.shelfMapping["reviewSummary"] = reviewSummary; + } + // Most Recent Version + const versionHistoryShelfContext = { + isFirstPartyHideableApp: isFirstPartyHideableApp, + isBundle: isBundle, + isPreorder: isPreorder, + }; + const mostRecentVersion = versionHistoryShelves.create(objectGraph, data, shelfMetrics, versionHistoryShelfContext); + if (mostRecentVersion) { + productPage.shelfMapping["mostRecentVersion"] = mostRecentVersion; + } + productPage.shelfMapping["textCards"] = textCardsShelf.create(objectGraph, data, shelfMetrics); + // Privacy + const bundleId = contentAttributes.contentAttributeAsString(objectGraph, data, "bundleId"); + if (objectGraph.bag.enablePrivacyNutritionLabels && + !privacySuppression.shouldSuppressPrivacyInformationForAdamId(objectGraph, data.id) && + !privacySuppression.shouldSuppressPrivacyInformationForBundleId(objectGraph, bundleId)) { + const privacyHeader = privacyHeaderShelf.create(objectGraph, data, shelfMetrics.metricsPageInformation, shelfMetrics.locationTracker); + const privacyTypes = privacyTypesShelf.create(objectGraph, data, shelfMetrics); + if (serverData.isDefinedNonNull(privacyHeader) && + (serverData.isDefinedNonNull(privacyTypes) || objectGraph.client.isWatch)) { + productPage.shelfMapping["privacyHeader"] = privacyHeader; + if (serverData.isDefinedNonNull(privacyTypes)) { + productPage.shelfMapping["privacyTypes"] = privacyTypes; + } + const privacyFooter = privacyFooterShelf.create(objectGraph, data, shelfMetrics.metricsPageInformation, shelfMetrics.locationTracker); + if (serverData.isDefinedNonNull(privacyFooter)) { + productPage.shelfMapping["privacyFooter"] = privacyFooter; + } + } + } + // Information + const ageRatingBadgeActionMetadata = new productBadgesCommon.BadgeActionMetadata(); + const languageBadgeActionMetadata = new productBadgesCommon.BadgeActionMetadata(); + const sizeBadgeActionMetadata = new productBadgesCommon.BadgeActionMetadata(); + const categoryBadgeActionMetadata = new productBadgesCommon.BadgeActionMetadata(); + const highMotionBadgeActionMetadata = new productBadgesCommon.BadgeActionMetadata(); + const metadataHandler = (factory, annotation, currentIndex) => { + var _a, _b; + if (factory === contentRatingAnnotation.createAnnotation) { + ageRatingBadgeActionMetadata.setShelfId("information", currentIndex); + ageRatingBadgeActionMetadata.annotation = annotation; + productPage.ageRatingAction = new models.ShelfBasedPageScrollAction("information", null, null, null, currentIndex, true); + productPage.ageRatingAction.title = objectGraph.loc.string("ProductPage.Banner.AskForException.Review.ViewAgeRatingLink"); + } + // If we have a product page banner which needs to scroll to a particular information section index, populate the index here + if (factory === compatibilityAnnotation.createAnnotation && serverData.isDefinedNonNull(productPage.banner)) { + // This banner has a variant that mentions the Support (Information) section. Since we now have a Support section, + // overwrite the original `action` with the `fullProductAction` + if (serverData.isDefinedNonNull(productPage.banner.fullProductAction)) { + productPage.banner.action = productPage.banner.fullProductAction; + } + const action = productPage.banner.action; + if (serverData.isDefinedNonNull(action)) { + if (productPageUtil.isShelfBased(objectGraph)) { + if (action instanceof models.ShelfBasedPageScrollAction && + ((_a = action.shelfId) === null || _a === void 0 ? void 0 : _a.length) > 0 && + action.shelfId === "information" && + action.indexId === "compatibilityAnnotation") { + action.index = currentIndex; + } + } + else { + if (action instanceof models.ProductPageScrollAction && + ((_b = action.section.shelfId) === null || _b === void 0 ? void 0 : _b.length) > 0 && + action.section.shelfId === "information" && + action.indexId === "compatibilityAnnotation") { + action.index = currentIndex; + } + } + } + } + if (factory === languageAnnotation.createAnnotation) { + languageBadgeActionMetadata.setShelfId("information", currentIndex); + languageBadgeActionMetadata.annotation = annotation; + } + if (factory === sizeAnnotation.createAnnotation) { + sizeBadgeActionMetadata.setShelfId("information", currentIndex); + sizeBadgeActionMetadata.annotation = annotation; + } + if (factory === categoryAnnotation.createAnnotation) { + categoryBadgeActionMetadata.setShelfId("information", currentIndex); + categoryBadgeActionMetadata.annotation = annotation; + } + if (factory === highMotionAnnotation.createAnnotation) { + highMotionBadgeActionMetadata.setShelfId("information", currentIndex); + highMotionBadgeActionMetadata.annotation = annotation; + } + }; + const supportsArcade = content.isArcadeSupported(objectGraph, data); + const information = annotationsShelf.create(objectGraph, data, isFirstPartyHideableApp, shelfMetrics, metadataHandler, supportsArcade, isFreeProduct, false); + if (information) { + productPage.shelfMapping["information"] = information; + } + // App Events + const appEvents = appPromotionsShelf.appPromotionsShelfForProductPage(objectGraph, data, shelfMetrics, refreshController); + if (serverData.isDefinedNonNull(appEvents)) { + productPage.shelfMapping["appEvents"] = appEvents; + } + // Editorial Quote + if (supportsArcade) { + const editorialQuote = editorialQuoteShelf.create(objectGraph, data, shelfMetrics); + if (editorialQuote) { + productPage.shelfMapping["editorialQuote"] = editorialQuote; + } + } + // Links + // If the app is purchased, we will show the link for report a problem + productPage.shelfMapping["notPurchasedLinks"] = linksShelf.create(objectGraph, data, shelfMetrics, false, isFreeProduct, supportsArcade); + productPage.shelfMapping["purchasedLinks"] = linksShelf.create(objectGraph, data, shelfMetrics, true, isFreeProduct, supportsArcade); + productPage.shelfMapping["textLinksShelf"] = textLinksShelf.create(objectGraph, data, shelfMetrics); + // Capabilities + const capabilities = capabilitiesShelf.create(objectGraph, data, isFreeProduct, shelfMetrics); + if (capabilities) { + productPage.shelfMapping["capabilities"] = capabilities; + } + const isWebBrowserContext = (_a = options === null || options === void 0 ? void 0 : options.webBrowser) !== null && _a !== void 0 ? _a : false; + // Featured In + if (!objectGraph.client.isVision && !isWebBrowserContext) { + const featuredInContainer = mediaRelationship.relationship(data, "related-editorial-items"); + if (serverData.isDefinedNonNullNonEmpty(featuredInContainer)) { + const isSmallStoryCardsSupported = objectGraph.host.isiOS || objectGraph.host.isMac || objectGraph.host.isWeb; + const featuredIn = isSmallStoryCardsSupported + ? smallStoryCardShelf.createInitialShelf(objectGraph, data, featuredInContainer.data, shelfMetrics) + : featuredInShelf.create(objectGraph, data, featuredInContainer, shelfMetrics); + if (featuredIn) { + productPage.shelfMapping["featuredIn"] = featuredIn; + } + } + } + // Friends Playing + const friendsPlayingShelfContext = { + isPreorder: isPreorder, + }; + const friendsPlayingBadgeActionMetadata = new productBadgesCommon.BadgeActionMetadata(); + const friendsPlaying = friendsPlayingShelf.create(objectGraph, data, shelfMetrics, friendsPlayingShelfContext); + if (friendsPlaying) { + productPage.shelfMapping["friendsPlaying"] = friendsPlaying; + friendsPlayingBadgeActionMetadata.setShelfId("friendsPlaying"); + } + // Achievements + const achievementsShelfContext = { + isPreorder: isPreorder, + }; + const achievements = achievementsShelf.create(objectGraph, data, shelfMetrics, achievementsShelfContext); + if (achievements) { + productPage.shelfMapping["achievements"] = achievements; + } + // More by Developer + if (!isViewOnly && !isWebBrowserContext) { + const moreByDeveloper = moreByDeveloperShelf.createInitialShelf(objectGraph, data, shelfMetrics); + if (isSome(moreByDeveloper)) { + productPage.shelfMapping["moreByDeveloper"] = moreByDeveloper; + } + } + // Similar Items + if (!isViewOnly && !isWebBrowserContext) { + const similarItems = similarItemsShelf.createInitialShelf(objectGraph, data, shelfMetrics); + if (isSome(similarItems)) { + productPage.shelfMapping["similarItems"] = similarItems; + } + } + // Subscriptions + const subscriptionsShelfContext = { + isForSubscriptions: true, + options: options, + }; + const subscriptionsResult = await tryAwait(inAppPurchasesShelf.create(objectGraph, data, shelfMetrics, subscriptionsShelfContext)); + if (subscriptionsResult.success && isSome(subscriptionsResult.value)) { + productPage.shelfMapping["subscriptions"] = subscriptionsResult.value; + } + // In-App Purchases + const inAppPurchaseShelfContext = { + isForSubscriptions: false, + options: options, + }; + const inAppPurchasesResult = await tryAwait(inAppPurchasesShelf.create(objectGraph, data, shelfMetrics, inAppPurchaseShelfContext)); + if (inAppPurchasesResult.success && isSome(inAppPurchasesResult.value)) { + productPage.shelfMapping["inAppPurchases"] = inAppPurchasesResult.value; + } + const productMediaShelves = productMediaShelf.create(objectGraph, data, clientIdentifierOverride, shelfMetrics); + productPage.shelfMapping = { + ...productPage.shelfMapping, + ...productMediaShelves.shelfMapping, + }; + // We need to extract the device family that matches the screenshots we display, for the accesssibility shelves + const firstMediaAppPlatform = (_b = productMediaShelves === null || productMediaShelves === void 0 ? void 0 : productMediaShelves.allProductMedia[0]) === null || _b === void 0 ? void 0 : _b.mediaPlatform.appPlatform; + const deviceFamily = deviceFamilyForAccessibilityLabels(firstMediaAppPlatform); + const actionLinksShelfContext = { + productTitle: productPage.title, + shouldShowRatingsAndReviews: shouldShowRatingsAndReviews, + isFirstPartyHideableApp: isFirstPartyHideableApp, + isBundle: isBundle, + isPreorder: isPreorder, + isArcadeApp: supportsArcade, + isFreeProduct: isFreeProduct, + deviceFamily: deviceFamily, + }; + const actionLinks = actionLinksShelf.create(objectGraph, data, shelfMetrics, actionLinksShelfContext); + if (actionLinks) { + productPage.shelfMapping["actionLinks"] = actionLinks; + } + // Accessibility Support + const isWebViewingWatch = objectGraph.client.isWeb && ((_c = objectGraph.activeIntent) === null || _c === void 0 ? void 0 : _c.platform) === "watch"; + if (!objectGraph.client.isWatch && + !isWebViewingWatch && + isSome(deviceFamily) && + accessibilityShelves.shouldShowAccessibilitySection(objectGraph, data, deviceFamily)) { + const accessibilityHeader = accessibilityShelves.createHeaderShelf(objectGraph, data, deviceFamily, false, shelfMetrics, shelfMetrics.metricsPageInformation, shelfMetrics.locationTracker); + productPage.shelfMapping["accessibilityHeader"] = accessibilityHeader; + const accessibilityFeatures = accessibilityShelves.createFeaturesShelf(objectGraph, data, deviceFamily, false, shelfMetrics, shelfMetrics.metricsPageInformation, shelfMetrics.locationTracker); + if (isSome(accessibilityFeatures)) { + productPage.shelfMapping["accessibilityFeatures"] = accessibilityFeatures; + } + const accessibilityDeveloperLink = accessibilityShelves.createDeveloperLinkShelf(objectGraph, data, false, shelfMetrics.metricsPageInformation, shelfMetrics.locationTracker); + if (isSome(accessibilityDeveloperLink)) { + productPage.shelfMapping["accessibilityDeveloperLink"] = accessibilityDeveloperLink; + } + } + if (objectGraph.client.isiOS) { + const placementId = supportsArcade + ? "arcadeProductPageInlineMessage" + : "productPageInlineMessage"; + const unifiedMessageShelf = shelfForUnifiedMessage(objectGraph, placementId); + productPage.shelfMapping[placementId] = unifiedMessageShelf; + } + applySpotlightOptions(objectGraph, options, productPage); + const badgeActionMetadataByKey = {}; + const contentRatingKey = "contentRating"; + const storefrontContentRatingKey = "storefrontContentRating"; + const languageKey = "languages"; + const sizeKey = "size"; + const categoryKey = "category"; + const friendsPlayingType = "friendsPlaying"; + const highMotionType = "highMotion"; + badgeActionMetadataByKey[contentRatingKey] = ageRatingBadgeActionMetadata; + badgeActionMetadataByKey[storefrontContentRatingKey] = ageRatingBadgeActionMetadata; + badgeActionMetadataByKey[languageKey] = languageBadgeActionMetadata; + badgeActionMetadataByKey[sizeKey] = sizeBadgeActionMetadata; + badgeActionMetadataByKey[categoryKey] = categoryBadgeActionMetadata; + badgeActionMetadataByKey[friendsPlayingType] = friendsPlayingBadgeActionMetadata; + badgeActionMetadataByKey[highMotionType] = highMotionBadgeActionMetadata; + if (productPage.badges) { + productBadgesCommon.applyActionMetadataToBadges(objectGraph, productPage.badges, badgeActionMetadataByKey, shelfMetrics); + } + // If we have an info ribbon, we need to apply actions to badges there too. + if (productPage.shelfMapping["informationRibbon"] != null) { + productBadgesCommon.applyActionMetadataToBadges(objectGraph, productPageUtil.badgesFromInfoRibbonShelf(objectGraph, productPage.shelfMapping["informationRibbon"]), badgeActionMetadataByKey, shelfMetrics); + } + addShelfOrderingToProductPage(objectGraph, productPage, productMediaShelves.sectionMappings, supportsArcade, options); +} +function addShelfOrderingToProductPage(objectGraph, productPage, productMediaSectionMappings, supportsArcade, options) { + const mapShelfOrderings = function (originalShelfOrdering, mediaSection, isMediaExpanded) { + const updatedSectionMappings = originalShelfOrdering + .map((sectionMapping) => { + if (sectionMapping.shelfId === "screenshots" && isMediaExpanded) { + return productMediaSectionMappings; + } + else if (sectionMapping.shelfId === "screenshots") { + return productMediaSectionMappings[0] ? [productMediaSectionMappings[0]] : []; + } + else { + return [sectionMapping]; + } + }) + .reduce((previousValue, currentValue) => { + return previousValue.concat(currentValue); + }); + return productPageShelfIdsFromMappings(updatedSectionMappings); + }; + const purchasedOrdering = shelfBasedProductOrdering.sectionOrdering(objectGraph, true, objectGraph.host.platform, supportsArcade, false, false, options); + const notPurchasedOrdering = shelfBasedProductOrdering.sectionOrdering(objectGraph, false, objectGraph.host.platform, supportsArcade, false, false, options); + const purchasedOrderingCompact = shelfBasedProductOrdering.sectionOrdering(objectGraph, true, objectGraph.host.platform, supportsArcade, true, false, options); + const notPurchasedOrderingCompact = shelfBasedProductOrdering.sectionOrdering(objectGraph, false, objectGraph.host.platform, supportsArcade, true, false, options); + const downloadingOrdering = shelfBasedProductOrdering.sectionOrdering(objectGraph, false, objectGraph.host.platform, supportsArcade, false, true, options); + const downloadingOrderingCompact = shelfBasedProductOrdering.sectionOrdering(objectGraph, false, objectGraph.host.platform, supportsArcade, true, true, options); + productPage.shelfOrderings[models.shelfBasedProductPageOrderingIdPurcahsed] = mapShelfOrderings(purchasedOrdering, productMediaSectionMappings, false); + productPage.shelfOrderings[models.shelfBasedProductPageOrderingIdPurcahsedExpandedMedia] = mapShelfOrderings(purchasedOrdering, productMediaSectionMappings, true); + productPage.shelfOrderings[models.shelfBasedProductPageOrderingIdNotPurcahsed] = mapShelfOrderings(notPurchasedOrdering, productMediaSectionMappings, false); + productPage.shelfOrderings[models.shelfBasedProductPageOrderingIdNotPurcahsedExpandedMedia] = mapShelfOrderings(notPurchasedOrdering, productMediaSectionMappings, true); + productPage.shelfOrderings[models.shelfBasedProductPageOrderingIdPurcahsedCompact] = mapShelfOrderings(purchasedOrderingCompact, productMediaSectionMappings, false); + productPage.shelfOrderings[models.shelfBasedProductPageOrderingIdPurcahsedExpandedMediaCompact] = mapShelfOrderings(purchasedOrderingCompact, productMediaSectionMappings, true); + productPage.shelfOrderings[models.shelfBasedProductPageOrderingIdNotPurcahsedCompact] = mapShelfOrderings(notPurchasedOrderingCompact, productMediaSectionMappings, false); + productPage.shelfOrderings[models.shelfBasedProductPageOrderingIdNotPurcahsedExpandedMediaCompact] = + mapShelfOrderings(notPurchasedOrderingCompact, productMediaSectionMappings, true); + productPage.shelfOrderings[models.shelfBasedProductPageOrderingIdDownloading] = mapShelfOrderings(downloadingOrdering, productMediaSectionMappings, false); + productPage.shelfOrderings[models.shelfBasedProductPageOrderingIdDownloadingExpandedMedia] = mapShelfOrderings(downloadingOrdering, productMediaSectionMappings, true); + productPage.shelfOrderings[models.shelfBasedProductPageOrderingIdDownloadingCompact] = mapShelfOrderings(downloadingOrderingCompact, productMediaSectionMappings, false); + productPage.shelfOrderings[models.shelfBasedProductPageOrderingIdDownloadingExpandedMediaCompact] = + mapShelfOrderings(downloadingOrderingCompact, productMediaSectionMappings, true); + productPage.defaultShelfOrdering = models.shelfBasedProductPageOrderingIdNotPurcahsed; + for (const shelfId of Object.keys(productPage.shelfMapping)) { + if (serverData.isDefinedNonNullNonEmpty(productPage.shelfMapping[shelfId])) { + productPage.shelfMapping[shelfId].id = shelfId; + } + } +} +function productUberVideoPlaybackIdForProduct(data) { + return `ProductPage.${data.id}.productUberVideoPlaybackId`; +} +export function createUber(objectGraph, data, clientIdentifierOverride) { + const attributePlatform = content.iconAttributePlatform(objectGraph, data, clientIdentifierOverride); + if (objectGraph.client.isVision && attributePlatform !== "xros") { + // Ubers are reserved for native apps only + return null; + } + const supportsArcade = content.isArcadeSupported(objectGraph, data); + const uberStyle = shouldUseInlineUberStyle(objectGraph, supportsArcade) ? "inline" : "above"; + const uber = new models.Uber(uberStyle); + uber.artwork = content.productUberFromData(objectGraph, data, { + supportsArcade: supportsArcade, + }); + if (objectGraph.client.isiOS) { + uber.compactArtwork = content.productUberFromData(objectGraph, data, { + supportsArcade: supportsArcade, + prefersCompactVariant: true, + }); + } + uber.video = content.productEditorialVideoFromData(objectGraph, data, 21 /* content.ArtworkUseCase.Uber */); + if (serverData.isDefinedNonNull(uber.video)) { + uber.video.playbackId = productUberVideoPlaybackIdForProduct(data); + } + if (objectGraph.client.isTV || (objectGraph.client.isVision && attributePlatform === "xros")) { + uber.iconArtwork = content.iconFromData(objectGraph, data, { + useCase: 10 /* content.ArtworkUseCase.ProductPageIcon */, + withJoeColorPlaceholder: objectGraph.client.isTV, + allowingTransparency: !objectGraph.client.isTV, + }, clientIdentifierOverride); + if (objectGraph.client.isVision && isNothing(uber.artwork) && isNothing(uber.video)) { + uber.style = "background"; + } + } + // If this is an Arcade app, but we don't have an Arcade Uber to show, use the first video/screenshot instead. + if (supportsArcade && + serverData.isNullOrEmpty(uber.video) && + serverData.isNullOrEmpty(uber.artwork) && + !objectGraph.client.isVision) { + const includedAppPlatformsForProductMedia = includedAppPlatformsForProductMediaOnDevice(objectGraph, objectGraph.client.deviceType); + const productVariantData = productPageVariants.productVariantDataForData(objectGraph, data); + const productMedia = content.productMediaFromData(objectGraph, data, 11 /* content.ArtworkUseCase.ProductPageScreenshots */, includedAppPlatformsForProductMedia, productVariantData, clientIdentifierOverride); + if (productMedia.length > 0 && productMedia[0].items.length > 0) { + const topAsset = productMedia[0].items[0]; + uber.video = topAsset.video; + uber.artwork = topAsset.screenshot; + } + } + // Defer to uberArtwork if no uberArtworkForCompactDisplay exists. + if (serverData.isNullOrEmpty(uber.compactArtwork) && objectGraph.client.isiOS) { + uber.compactArtwork = uber.artwork; + } + return uber.isValidUber(objectGraph.client.isTV, objectGraph.client.isVision) ? uber : null; +} +/** + * Creates the icon artwork to display in the navigation bar of the product page. Currently only used on visionOS. + * @param objectGraph Current object graph. + * @param data The product data. + * @param clientIdentifierOverride Any client identifier override. + * @returns An artwork representing the icon, or null. + */ +function createNavigationBarIconArtwork(objectGraph, data, clientIdentifierOverride) { + if (!objectGraph.client.isVision) { + return null; + } + const iconArtwork = content.iconFromData(objectGraph, data, { + useCase: 10 /* content.ArtworkUseCase.ProductPageIcon */, + }, clientIdentifierOverride); + return iconArtwork; +} +function createProductLockupButtonAction(objectGraph, data, productIcon, shelfMetrics, metricsOptions, options, referrerData) { + var _a; + const offerData = offers.offerDataFromData(objectGraph, data); + const bundleId = contentAttributes.contentAttributeAsString(objectGraph, data, "bundleId"); + const isPreorder = mediaAttributes.attributeAsBoolean(data, "isPreorder"); + const metricsPlatformDisplayStyle = metricsUtil.metricsPlatformDisplayStyleFromData(objectGraph, data, productIcon, options === null || options === void 0 ? void 0 : options.clientIdentifierOverride); + const offerAction = offers.offerActionFromOfferData(objectGraph, offerData, data, isPreorder, false, metricsPlatformDisplayStyle, metricsOptions, "productPage", referrerData, options === null || options === void 0 ? void 0 : options.webBrowser); + if ((_a = options === null || options === void 0 ? void 0 : options.webBrowser) !== null && _a !== void 0 ? _a : false) { + return { + buttonAction: new SelectAppAction(data.id), + offerAction: offerAction, + }; + } + let buttonAction; + if (serverData.isDefinedNonNullNonEmpty(offerAction)) { + if (serverData.isDefinedNonNull(options)) { + offerAction.lineItem = options.iAdLineItem; + } + const clientIdentifierOverride = serverData.isDefinedNonNullNonEmpty(options) + ? options.clientIdentifierOverride + : null; + buttonAction = offers.wrapOfferActionIfNeeded(objectGraph, offerAction, data, isPreorder, metricsOptions, "productPage", clientIdentifierOverride); + if (serverData.isDefinedNonNull(options) && serverData.isDefinedNonNullNonEmpty(buttonAction)) { + const attributePlatform = contentAttributes.defaultAttributePlatform(objectGraph); + const productVariantData = productPageVariants.productVariantDataForData(objectGraph, data); + const cppDeepLinkUrl = contentAttributes.customAttributeAsString(objectGraph, data, productVariantData, "customDeepLink", attributePlatform); + const hasCppDeepLink = serverData.isDefinedNonNullNonEmpty(cppDeepLinkUrl) && + !serverData.asBooleanOrFalse(options.isCppDeepLinkDisabled); + const hasExternalDeepLink = serverData.isDefinedNonNullNonEmpty(options.externalDeepLinkUrl); + let deepLinkUrl; + if (preprocessor.CARRY_BUILD || preprocessor.DEBUG_BUILD) { + const hasAlignedRegionDeepLink = isSome(options.alignedRegionDeepLinkUrl); + if (hasCppDeepLink || hasExternalDeepLink || hasAlignedRegionDeepLink) { + if (hasAlignedRegionDeepLink) { + deepLinkUrl = options.alignedRegionDeepLinkUrl; + } + else if (hasCppDeepLink) { + deepLinkUrl = cppDeepLinkUrl; + } + else { + deepLinkUrl = options.externalDeepLinkUrl; + } + buttonAction = externalDeepLink.deepLinkActionWrappingAction(objectGraph, buttonAction, offerAction.adamId, bundleId, deepLinkUrl, false, metricsOptions); + } + } + else { + if (hasCppDeepLink || hasExternalDeepLink) { + deepLinkUrl = hasCppDeepLink ? cppDeepLinkUrl : options.externalDeepLinkUrl; + buttonAction = externalDeepLink.deepLinkActionWrappingAction(objectGraph, buttonAction, offerAction.adamId, bundleId, deepLinkUrl, false, metricsOptions); + } + } + } + } + return { + buttonAction: buttonAction, + offerAction: offerAction, + }; +} +/** + * Returns the list of included app platforms for the product media shelves. + * This is used to constrain some platforms to media type of just that platform, e.g. tvOS can only show tvOS screenshots and video. + * + * @param deviceType Device type to find the included app platforms for product media for. + * @returns models.AppPlatform[] | null An array of supported app platforms for product media, or `null` if there are no constraints and should fallback to sorting logic in content.ts + */ +function includedAppPlatformsForProductMediaOnDevice(objectGraph, deviceType) { + // We want to return all product media platforms, so that we can generate + // the appropriate platform selector description text. + return null; +} +/** + * Source of truth for the decision on whether or not the product page should be displayed in the inline uber style + * + * @param supportsArcade Does the product say it supports Arcade + */ +function shouldUseInlineUberStyle(objectGraph, supportsArcade) { + if (objectGraph.client.isVision) { + return true; + } + return supportsArcade && (objectGraph.client.isiOS || objectGraph.client.isMac); +} +//# sourceMappingURL=shelf-based-product-page.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/accessibility-shelves.js b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/accessibility-shelves.js new file mode 100644 index 0000000..18e2c25 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/accessibility-shelves.js @@ -0,0 +1,544 @@ +import * as models from "../../../api/models"; +import * as validation from "@jet/environment/json/validation"; +import * as contentArtwork from "../../content/artwork/artwork"; +import { isNothing, isSome } from "@jet/environment"; +import { attributeAsDictionary, attributeAsString, URL } from "@apple-media-services/media-api"; +import { asString } from "../../../foundation/json-parsing/server-data"; +import { isEmpty, isNotEmpty } from "../../../foundation/util/array-util"; +import { Path, Protocol } from "../../../foundation/network/url-constants"; +import { isProductAccessibilityLabelsEnabled, shouldSuppressAccessibilityLabelsForAdamId, shouldSuppressAccessibilityLabelsForBundleId, } from "../../accessibility/accessibility-common"; +import { attributeAsArray } from "../../../foundation/media/attributes"; +import { contentAttributeAsString } from "../../content/attributes"; +import { newLocationTracker, nextPosition } from "../../metrics/helpers/location"; +import { addClickEventToAction } from "../../metrics/helpers/clicks"; +import { addMetricsEventsToPageWithInformation, metricsPageInformationFromMediaApiResponse, } from "../../metrics/helpers/page"; +import { makeRoutableArticlePageIntent } from "../../../api/intents/routable-article-page-intent"; +import { getLocale } from "../../locale"; +import { getPlatform } from "../../preview-platform"; +// region Validation +/** + * Returns whether the accessibility section should be displayed on the product page, based on whether accessibility + * data is available for the device family we want to use, and the feature is enabled. + */ +export function shouldShowAccessibilitySection(objectGraph, data, deviceFamily) { + var _a; + const accessibilityData = attributeAsDictionary(data, `accessibility.${deviceFamily}`); + const bundleId = (_a = contentAttributeAsString(objectGraph, data, "bundleId")) !== null && _a !== void 0 ? _a : ""; + return (isProductAccessibilityLabelsEnabled(objectGraph) && + !shouldSuppressAccessibilityLabelsForAdamId(objectGraph, data.id) && + !shouldSuppressAccessibilityLabelsForBundleId(objectGraph, bundleId) && + isSome(accessibilityData)); +} +// region Shelves +/** + * Builds the header shelf for the accessibility product page section or details page. + */ +export function createHeaderShelf(objectGraph, data, deviceFamily, isSeeAllContext, shelfMetrics, pageInformation, locationTracker) { + return validation.context("createHeaderShelf", () => { + const shelf = new models.Shelf("accessibilityParagraph"); + shelf.presentationHints = { isSeeAllContext: isSeeAllContext }; + // Only applicable for tvOS. Since tvOS doesn't support inline linkable text, we have to show any actions as + // distinct buttons instead. This should remain empty for all other platforms. + const actions = []; + const title = objectGraph.loc.string("ProductPage.Accessibility.Shelf.Title"); + // Add shelf header / see all action on product page of all platforms, except watchOS + if (!isSeeAllContext && !objectGraph.client.isWatch) { + shelf.title = title; + // Only add a details action if there are supported features + const supportedFeatures = supportedFeaturesForDeviceFamily(data, deviceFamily, isSeeAllContext); + const hasSupportedFeatures = isSome(supportedFeatures) && isNotEmpty(supportedFeatures); + if (hasSupportedFeatures) { + const seeDetailsAction = createSeeDetailsAction(objectGraph, data, deviceFamily, "button", pageInformation, locationTracker); + if (objectGraph.client.isTV) { + actions.push(seeDetailsAction); + } + else { + shelf.seeAllAction = seeDetailsAction; + } + } + } + if (objectGraph.client.isTV && !isSeeAllContext) { + // Add learn more link action, if applicable + const learnMoreAction = createLearnMoreAction(objectGraph, pageInformation, locationTracker); + if (isSome(learnMoreAction)) { + actions.push(learnMoreAction); + } + } + // Create item model + const text = headerTextFromData(objectGraph, data, deviceFamily, isSeeAllContext, pageInformation, locationTracker); + const item = new models.AccessibilityParagraph(text, actions); + // Decorate item with impression information + if (isSome(shelfMetrics) && !isSeeAllContext && !objectGraph.client.isWatch) { + shelfMetrics.addImpressionsToShelf(objectGraph, shelf, "accessibilityOverview"); + nextPosition(shelfMetrics.locationTracker); + } + // Add item to shelf + shelf.items = [item]; + return shelf; + }); +} +/** + * Builds a shelf for `accessibilityFeatures`, which will display the supported accessibility features card on the + * product page, or a list on the details page, if there are 1 or more reported features. + */ +export function createFeaturesShelf(objectGraph, data, deviceFamily, isSeeAllContext, shelfMetrics = null, pageInformation, locationTracker) { + return validation.context("createFeaturesShelf", () => { + const features = featuresFromData(objectGraph, data, deviceFamily, isSeeAllContext); + // We only want to show this shelf if there is at least 1 feature + if (isEmpty(features)) { + return null; + } + // Create shelf + const shelf = new models.Shelf("accessibilityFeatures"); + shelf.presentationHints = { isSeeAllContext: isSeeAllContext }; + // Create item + const title = objectGraph.loc.string("ProductPage.Accessibility.Card.SupportedFeaturesTitle"); + const item = new models.AccessibilityFeatures(title, contentArtwork.createArtworkForResource(objectGraph, "systemimage://accessibility"), features); + // Decorate with metrics and add see all action for the product page + if (!isSeeAllContext) { + if (isSome(shelfMetrics)) { + shelfMetrics.addImpressionsToShelf(objectGraph, shelf, "supportedFeaturesCard", undefined, undefined, null, title); + nextPosition(shelfMetrics.locationTracker); + } + // Add the see details click action for the card. + item.clickAction = createSeeDetailsAction(objectGraph, data, deviceFamily, "card", pageInformation, locationTracker); + } + // Add the item to the shelf + shelf.items = [item]; + return shelf; + }); +} +/** + * Builds a shelf with the developer's accessibility site link, if provided. + * + * Not applicable on tvOS; we don't show the developer link at all on tvOS, since opening external urls is not supported. + */ +export function createDeveloperLinkShelf(objectGraph, data, isSeeAllContext, pageInformation, locationTracker) { + return validation.context("createDeveloperLinkShelf", () => { + if (objectGraph.client.isTV) { + // We don't show the developer link at all on tvOS, since opening external urls is not supported. + return null; + } + const text = developerLinkTextFromData(objectGraph, data, isSeeAllContext, pageInformation, locationTracker); + if (isNothing(text)) { + return null; + } + const shelf = new models.Shelf("accessibilityParagraph"); + shelf.items = [new models.AccessibilityParagraph(text, [])]; + shelf.presentationHints = { isSeeAllContext: isSeeAllContext }; + return shelf; + }); +} +/** + * Builds a shelf with the learn more link & developer link actions as buttons, if present, to be displayed on the + * intermediary details page. + * + * Only applicable on watchOS. + */ +function createWatchActionsShelf(objectGraph, data, pageInformation, locationTracker) { + const shelf = new models.Shelf("action"); + const items = []; + const developerLinkAction = createDeveloperLinkAction(objectGraph, data, false, pageInformation, locationTracker); + if (isSome(developerLinkAction)) { + items.push(developerLinkAction); + } + const learnMoreAction = createLearnMoreAction(objectGraph, pageInformation, locationTracker); + if (isSome(learnMoreAction)) { + items.push(learnMoreAction); + } + if (isEmpty(items)) { + return null; + } + else { + shelf.items = items; + return shelf; + } +} +// region Data to Model Mapping +/** + * Returns an array of `AccessibilityFeature` models mapped from the data. + */ +function featuresFromData(objectGraph, data, deviceFamily, isSeeAllContext) { + const supportedFeatures = supportedFeaturesForDeviceFamily(data, deviceFamily, isSeeAllContext); + if (isNothing(supportedFeatures)) { + return []; + } + return supportedFeatures.reduce((features, feature) => { + // Extract properties from data + const identifier = asString(feature, "identifier"); + const title = asString(feature, "title"); + const description = asString(feature, "description"); + // Validate required data + if (isNothing(identifier) || identifier.length === 0 || isNothing(title) || title.length === 0) { + return features; + } + // Map the identifier to a system image + const systemImage = systemImageForFeature(identifier); + if (isNothing(systemImage)) { + return features; + } + // Create the feature model + const artwork = contentArtwork.createArtworkForResource(objectGraph, `systemimage://${systemImage}`); + artwork.imageScale = imageScaleForFeature(identifier); + features.push(new models.AccessibilityFeature(title, description !== null && description !== void 0 ? description : null, artwork)); + return features; + }, []); +} +/** + * Creates the header text based on the data available. + */ +function headerTextFromData(objectGraph, data, deviceFamily, isSeeAllContext, pageInformation, locationTracker) { + // Define input for the header text + const supportedFeatures = supportedFeaturesForDeviceFamily(data, deviceFamily, isSeeAllContext); + const learnMoreLinkAction = createLearnMoreAction(objectGraph, pageInformation, locationTracker); + const developerName = attributeAsString(data, "artistName"); + const hasDeveloperName = isSome(developerName) && isNotEmpty(developerName); + // Localize the template for the text + let templateKey; + if (objectGraph.client.isTV) { + // Since the learn more link is a standalone button on tvOS, we want to ensure that we don't use the inline link version. + templateKey = headerTemplateLocKey(isSeeAllContext, supportedFeatures, null, hasDeveloperName); + } + else if (objectGraph.client.isWatch) { + templateKey = watchHeaderTemplateLocKey(supportedFeatures, hasDeveloperName); + } + else { + templateKey = headerTemplateLocKey(isSeeAllContext, supportedFeatures, learnMoreLinkAction, hasDeveloperName); + } + let text = objectGraph.loc.string(templateKey); + let textType = "text/plain"; + const linkedSubstrings = {}; + // Replace the learn more link with localized text, and add the relevant action + if (isSome(learnMoreLinkAction)) { + const learnMoreLinkText = objectGraph.loc.string("Action.LearnMore"); + text = text.replace("{learnMoreLink}", learnMoreLinkText); + linkedSubstrings[learnMoreLinkText] = learnMoreLinkAction; + } + // Add the developer name + if (hasDeveloperName) { + text = text.replace("{developerName}", `<b>${developerName}</b>`); + textType = "text/x-apple-as3-nqml"; + } + const styledText = new models.StyledText(text, textType); + return new models.LinkableText(styledText, linkedSubstrings); +} +/** + * Creates the text for linking to the developer accessibility site, if provided. + * + * This is not used on tvOS. + */ +function developerLinkTextFromData(objectGraph, data, isSeeAllContext, pageInformation, locationTracker) { + const action = createDeveloperLinkAction(objectGraph, data, isSeeAllContext, pageInformation, locationTracker); + if (isNothing(action)) { + return null; + } + const developerLinkText = objectGraph.loc.string("Accessibility.DeveloperUrlLinkTitle"); + const text = objectGraph.loc + .string("Accessibility.DeveloperUrlText") + .replace("{accessibilitySite}", developerLinkText); + const linkedSubstrings = {}; + linkedSubstrings[developerLinkText] = action; + const styledText = new models.StyledText(text, "text/plain"); + return new models.LinkableText(styledText, linkedSubstrings); +} +// region Data Parsing +/** + * Returns the list of supported features for the device family that we want to display. + */ +function developerLinkUrl(data, isSeeAllContext) { + const accessibilityKey = isSeeAllContext ? "accessibilityDetails" : "accessibility"; + return attributeAsString(data, `${accessibilityKey}.developerUrl`); +} +/** + * Returns the list of supported features for the device family that we want to display. + */ +function supportedFeaturesForDeviceFamily(data, deviceFamily, isSeeAllContext) { + const accessibilityKey = isSeeAllContext ? "accessibilityDetails" : "accessibility"; + return attributeAsArray(data, `${accessibilityKey}.${deviceFamily}.supportedCategories`); +} +// region Actions +/** + * Creates the action for displaying the intermediary details page on watchOS. + * + * This intermediary page is unique to watchOS, and is really a mix between what we show on the product page vs details + * page on all the other platforms. It is a complete page, sidepacked with all the data we have on hand from the product + * page request (which is basically all the accessibility data, except for the feature descriptions). + * + * The "end" details page can be actioned by tapping on the features card platter on this page, and will load the + * feature descriptions. + */ +export function createSeeIntermediaryDetailsAction(objectGraph, data, deviceFamily) { + const action = new models.FlowAction("page"); + action.title = objectGraph.loc.string("ProductPage.Accessibility.Shelf.Title"); + action.pageData = accessibilityIntermediaryDetailsSidePackedPageFromData(objectGraph, data, deviceFamily); + // A click event for this is added in `action-link-shelf.ts`. + return action; +} +/** + * Creates the action for displaying the accessibility details page. + */ +function createSeeDetailsAction(objectGraph, data, deviceFamily, targetType, pageInformation, locationTracker) { + const seeDetailsAction = new models.FlowAction("accessibilityDetails"); + if (objectGraph.client.isWatch) { + // On watchOS, we only display the supported feature descriptions on this "end" details page, which we need to + // load. This initial page is empty. + seeDetailsAction.pageData = watchAccessibilityDetailsPage(objectGraph); + } + else { + seeDetailsAction.title = objectGraph.loc.string("ACTION_SEE_DETAILS"); + seeDetailsAction.pageData = accessibilityDetailsSidePackedPageFromData(objectGraph, data, deviceFamily, pageInformation, locationTracker); + } + const productType = data.type === "app-bundles" ? Path.productBundle : Path.product; + const pageUrl = URL.fromComponents(Protocol.internal, null, `/${Path.accessibilityDetails}/${productType}/${data.id}`, { deviceFamily: deviceFamily }); + seeDetailsAction.pageUrl = pageUrl.build(); + if (objectGraph.props.enabled("supportsClickEventsOnSwiftUIHostingCell")) { + addClickEventToAction(objectGraph, seeDetailsAction, { + id: "AccessibilityDetails", + targetType: targetType, + pageInformation: pageInformation, + locationTracker: locationTracker, + }); + } + return seeDetailsAction; +} +/** + * Creates the action for linking to the learn more URL. + */ +function createLearnMoreAction(objectGraph, pageInformation, locationTracker) { + const editorialItemId = objectGraph.bag.accessibilityLearnMoreEditorialItemId; + if (isNothing(editorialItemId)) { + return null; + } + const learnMoreAction = new models.FlowAction("article"); + learnMoreAction.title = objectGraph.loc.string("Action.LearnMore"); + learnMoreAction.pageUrl = `https://apps.apple.com/story/id${editorialItemId}`; + if (objectGraph.client.isWeb) { + learnMoreAction.destination = makeRoutableArticlePageIntent({ + ...getLocale(objectGraph), + ...getPlatform(objectGraph), + id: editorialItemId, + }); + } + if (objectGraph.client.isVision) { + learnMoreAction.presentation = "sheetPresent"; + } + if (objectGraph.props.enabled("supportsClickEventsOnSwiftUIHostingCell")) { + addClickEventToAction(objectGraph, learnMoreAction, { + id: "LearnMore", + targetType: "button", + pageInformation: pageInformation, + locationTracker: locationTracker, + }); + } + return learnMoreAction; +} +/** + * Creates the action for linking to the developer provided accessibility site. + */ +function createDeveloperLinkAction(objectGraph, data, isSeeAllContext, pageInformation, locationTracker) { + const url = developerLinkUrl(data, isSeeAllContext); + if (isNothing(url) || isEmpty(url)) { + return null; + } + const action = new models.ExternalUrlAction(url, false); + action.title = objectGraph.loc.string("Accessibility.DeveloperUrlLinkActionTitle"); + if (objectGraph.props.enabled("supportsClickEventsOnSwiftUIHostingCell")) { + addClickEventToAction(objectGraph, action, { + id: "AccessibilitySite", + targetType: "link", + pageInformation: pageInformation, + locationTracker: locationTracker, + }); + } + return action; +} +// region Details +/** + * Creates a complete, intermediary accessibility details page, sidepacking all data loaded from the product page. + * + * This intermediary page is unique to watchOS, and is really a mix between what we show on the product page vs details + * page on all the other platforms. The "end" details page can be actioned by tapping on the features card platter on + * this page, and will load the feature descriptions. + * + * NOTE: These shelves are created with `isSeeAllContext = false`, which is important for the `accessibilityFeatures` + * shelf, so that the corresponding view is displayed as a card platter. + */ +function accessibilityIntermediaryDetailsSidePackedPageFromData(objectGraph, data, deviceFamily) { + const shelves = []; + // Create metrics objects + const pageInformation = metricsPageInformationFromMediaApiResponse(objectGraph, "AppAccessibilityDetails", data.id, data); + const locationTracker = newLocationTracker(); + // Add header shelf + const headerShelf = createHeaderShelf(objectGraph, data, deviceFamily, false, null, pageInformation, locationTracker); + shelves.push(headerShelf); + // Add features shelf, if applicable + const featuresShelf = createFeaturesShelf(objectGraph, data, deviceFamily, false, null, pageInformation, locationTracker); + if (isSome(featuresShelf)) { + shelves.push(featuresShelf); + } + // Add actions shelf, if applicable + const actionsShelf = createWatchActionsShelf(objectGraph, data, pageInformation, locationTracker); + if (isSome(actionsShelf)) { + shelves.push(actionsShelf); + } + // Create and return page + const page = new models.GenericPage(shelves); + page.title = objectGraph.loc.string("AccessibilityDetails.Title"); + addMetricsEventsToPageWithInformation(objectGraph, page, pageInformation); + return page; +} +/** + * Creates an incomplete accessibility details page, sidepacking the header shelf & optional developer link shelf. + */ +function accessibilityDetailsSidePackedPageFromData(objectGraph, data, deviceFamily, pageInformation, locationTracker) { + const shelves = []; + // Add header shelf + const headerShelf = createHeaderShelf(objectGraph, data, deviceFamily, true, null, pageInformation, locationTracker); + shelves.push(headerShelf); + // Add developer link shelf, if applicable + const developerLinkShelf = createDeveloperLinkShelf(objectGraph, data, true, pageInformation, locationTracker); + if (isSome(developerLinkShelf)) { + shelves.push(developerLinkShelf); + } + // The web client displays accessibilityDetailPage as a modal, + // so we include accessibility feature data for immediate display. + if (objectGraph.client.isWeb) { + const featuresShelf = createFeaturesShelf(objectGraph, data, deviceFamily, true, null, pageInformation, locationTracker); + if (isSome(featuresShelf)) { + shelves.push(featuresShelf); + } + } + // Create and return page + const page = new models.GenericPage(shelves); + page.title = objectGraph.loc.string("AccessibilityDetails.Title"); + page.isIncomplete = true; + page.presentationOptions = ["prefersLargeTitle", "prefersIndirectTouch", "prefersReadableContentAlignedTitle"]; + return page; +} +/** + * Creates an empty accessibility details page for watchOS. + * + * This page is displayed when the customer taps on the features card platter on the intermediary details page, and + * is intended to display the descriptions of the supported features, which needs to be loaded. + */ +function watchAccessibilityDetailsPage(objectGraph) { + const page = new models.GenericPage([]); + page.title = objectGraph.loc.string("ProductPage.Accessibility.Card.SupportedFeaturesTitle"); + page.isIncomplete = true; + return page; +} +// region Helpers +/** + * Returns a system image name based on the feature identifier. + */ +function systemImageForFeature(identifier) { + switch (identifier) { + case "VOICEOVER": + return "voiceover"; + case "VOICE_CONTROL": + return "voice.control"; + case "LARGER_TEXT": + return "textformat.size"; + case "SUFFICIENT_CONTRAST": + return "circle.lefthalf.filled.inverse"; + case "DARK_INTERFACE": + return "appearance.darkmode"; + case "DIFFERENTIATE_WITHOUT_COLOR_ALONE": + return "xmark.triangle.circle.square.fill"; + case "REDUCED_MOTION": + return "circle.dotted.and.circle"; + case "CAPTIONS": + return "captions.bubble.fill"; + case "AUDIO_DESCRIPTIONS": + return "quote.bubble.fill"; + default: + return null; + } +} +/** + * Returns an image scale we want to override based on the feature identifier. + */ +function imageScaleForFeature(identifier) { + switch (identifier) { + case "VOICE_CONTROL": + return "small"; + default: + return null; + } +} +/** + * Returns the localization key for the header template text, based on the information available. + * + * There are a few different variations of this template. There are variations for the following conditions: + * 1. Whether we are displaying the text on the product page vs details page + * 2. What has been reported by the developer: + * a. Reported with at least 1 supported feature + * b. Reported with NO supported features + * c. Not reported yet + * 3. Whether the learn more link is available + * 4. For the details page, whether the developer name is available (as these strings attempt to use the developer name) + */ +function headerTemplateLocKey(isSeeAllContext, supportedFeatures, learnMoreLinkAction, hasDeveloperName) { + if (isSeeAllContext) { + // We can only get to the details page if there is at least 1 supported feature, so we don't need to worry + // about the reported state (2b) or (2c) from comment above. + if (isSome(learnMoreLinkAction) && hasDeveloperName) { + return "AccessibilityDetails.Header"; + } + else if (hasDeveloperName) { + return "AccessibilityDetails.Header.NoLearnMoreLink"; + } + else if (isSome(learnMoreLinkAction)) { + return "AccessibilityDetails.Header.NoDeveloperName"; + } + else { + return "AccessibilityDetails.Header.NoLearnMoreLink.NoDeveloperName"; + } + } + else { + const isReported = isSome(supportedFeatures); + const hasSupportedFeatures = isReported && isNotEmpty(supportedFeatures); + if (hasSupportedFeatures) { + return isSome(learnMoreLinkAction) + ? "ProductPage.Accessibility.Header.SupportedFeatures" + : "ProductPage.Accessibility.Header.SupportedFeatures.NoLearnMoreLink"; + } + else if (isReported) { + return isSome(learnMoreLinkAction) + ? "ProductPage.Accessibility.Header.NoSupportedFeatures" + : "ProductPage.Accessibility.Header.NoSupportedFeatures.NoLearnMoreLink"; + } + else { + return isSome(learnMoreLinkAction) + ? "ProductPage.Accessibility.Header.NoReportedFeatures" + : "ProductPage.Accessibility.Header.NoReportedFeatures.NoLearnMoreLink"; + } + } +} +/** + * Returns the localization key for the header template text for watchOS, based on the information available. + * + * This is similar to the logic for the other platforms (refer to comment of `headerTemplateLocKey` function above), + * however only returns a subset of those template keys because: + * - Inline linkable text is not supported on watchOS + * - We always display text in the see all context on watchOS + */ +function watchHeaderTemplateLocKey(supportedFeatures, hasDeveloperName) { + const isReported = isSome(supportedFeatures); + const hasSupportedFeatures = isReported && isNotEmpty(supportedFeatures); + if (hasSupportedFeatures) { + if (hasDeveloperName) { + return "AccessibilityDetails.Header.NoLearnMoreLink"; + } + else { + return "AccessibilityDetails.Header.NoLearnMoreLink.NoDeveloperName"; + } + } + else if (isReported) { + return "ProductPage.Accessibility.Header.NoSupportedFeatures.NoLearnMoreLink"; + } + else { + return "ProductPage.Accessibility.Header.NoReportedFeatures.NoLearnMoreLink"; + } +} +//# sourceMappingURL=accessibility-shelves.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/achievements-shelf.js b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/achievements-shelf.js new file mode 100644 index 0000000..3e8488c --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/achievements-shelf.js @@ -0,0 +1,82 @@ +import * as validation from "@jet/environment/json/validation"; +import * as models from "../../../api/models"; +import * as serverData from "../../../foundation/json-parsing/server-data"; +import * as mediaPlatformAttributes from "../../../foundation/media/platform-attributes"; +import { Parameters, Path, Protocol } from "../../../foundation/network/url-constants"; +import * as contentAttributes from "../../content/attributes"; +import * as metricsHelpersClicks from "../../metrics/helpers/clicks"; +import * as metricsHelpersImpressions from "../../metrics/helpers/impressions"; +import * as metricsHelpersLocation from "../../metrics/helpers/location"; +import * as productPageCommon from "../product-page-common"; +import { makeGameCenterHeader, openGamesUIAction } from "../../arcade/arcade-common"; +export function create(objectGraph, data, shelfMetrics, shelfContext) { + return validation.context("achievementsShelf", () => { + if (serverData.isNullOrEmpty(data)) { + return null; + } + if (objectGraph.client.isWeb) { + return null; + } + // rdar://66581575 (D421/18A342b: Achievements shouldn\u2019t be shown for pre-order games) + if (shelfContext.isPreorder) { + return null; + } + const isGameCenterEnabled = contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "isGameCenterEnabled"); + if (!isGameCenterEnabled) { + return null; + } + const attributePlatform = contentAttributes.bestAttributePlatformFromData(objectGraph, data); + const bundleId = mediaPlatformAttributes.platformAttributeAsString(data, attributePlatform, "bundleId"); + if (serverData.isNullOrEmpty(bundleId)) { + return null; + } + const title = objectGraph.loc.string("ProductPage.Section.Achievements.Title", "Achievements"); + const shelf = new models.Shelf("gameCenterPlayerGameAchievementSummary"); + shelf.header = makeGameCenterHeader(objectGraph, title); + shelf.isHorizontal = true; + shelf.items = []; + shelf.mergeWhenFetched = true; + const token = new productPageCommon.ProductPageShelfToken(data.id, [], shelf.title, false, undefined, null, null, null); + token.sourceLocationTracker = shelfMetrics.locationTracker; + token.destinationPageInformation = shelfMetrics.metricsPageInformation; + shelf.url = `${Protocol.internal}:/${Path.product}/${Path.shelf}/?${Parameters.isGameCenterAchievementsShelf}=true&${Parameters.bundleId}=${bundleId}&${Parameters.token}=${productPageCommon.encodedShelfToken(token, shelfMetrics, shelfMetrics.metricsPageInformation)}`; + shelf.batchGroup = "gameCenter"; + metricsHelpersLocation.nextPosition(shelfMetrics.locationTracker); + shelf.footerTitle = objectGraph.loc.string("Lockup.Footer.GamesApp"); + shelf.footerAction = openGamesUIAction(objectGraph, { pageAchievements: {} }); + shelf.footerStyle = { + $kind: "games", + bundleID: "com.apple.games", + width: 16, + height: 16, + }; + return shelf; + }); +} +export function createShelfWithAchievementSummary(objectGraph, achievementSummary, token) { + const shelf = new models.Shelf("gameCenterPlayerGameAchievementSummary"); + shelf.isHorizontal = true; + if (achievementSummary) { + const impressionMetricsOptions = { + pageInformation: token.destinationPageInformation, + locationTracker: token.sourceLocationTracker, + kind: "achievements", + title: "Achievements", + id: "achievements_summary", + idType: "its_id", + softwareType: null, + targetType: "achievements", + badges: { + gameCenter: true, + }, + }; + metricsHelpersImpressions.addImpressionFields(objectGraph, shelf, impressionMetricsOptions); + metricsHelpersClicks.addClickEventToAction(objectGraph, achievementSummary.action, impressionMetricsOptions); + shelf.items = [achievementSummary]; + } + shelf.mergeWhenFetched = true; + shelf.batchGroup = "gameCenter"; + shelf.isHidden = shelf.items.length === 0; + return shelf; +} +//# sourceMappingURL=achievements-shelf.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/action-links-shelf.js b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/action-links-shelf.js new file mode 100644 index 0000000..155f616 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/action-links-shelf.js @@ -0,0 +1,136 @@ +import * as validation from "@jet/environment/json/validation"; +import * as models from "../../../api/models"; +import * as serverData from "../../../foundation/json-parsing/server-data"; +import * as mediaAttributes from "../../../foundation/media/attributes"; +import * as mediaDataStructure from "../../../foundation/media/data-structure"; +import * as mediaRelationship from "../../../foundation/media/relationships"; +import * as contentAttributes from "../../content/attributes"; +import * as content from "../../content/content"; +import * as productCapabilities from "../../content/product-capabilities"; +import * as metricsHelpersClicks from "../../metrics/helpers/clicks"; +import * as metricsHelpersPage from "../../metrics/helpers/page"; +import * as reviews from "../reviews"; +import * as informationShelf from "./annotations/annotations"; +import * as versionHistoryShelves from "./version-history-shelves"; +import * as accessibilityShelves from "../shelves/accessibility-shelves"; +import { isSome } from "@jet/environment"; +/** + * Create a shelf of action links for the product page. + * + * @param data The raw data response for a product page JSON fetch. + * @param shelfMetrics The product page shelf metrics. + * @param shelfContext A collection of any other variables used when creating this shelf. + * @returns A shelf of action links. + */ +export function create(objectGraph, data, shelfMetrics, shelfContext) { + return validation.context("createActionLinks", () => { + if (serverData.isNullOrEmpty(data)) { + return null; + } + if (objectGraph.client.isWatch) { + return standardActionLinks(objectGraph, data, shelfContext.productTitle, shelfMetrics, shelfContext.shouldShowRatingsAndReviews, shelfContext.isFirstPartyHideableApp, shelfContext.isBundle, shelfContext.isPreorder, shelfContext.isArcadeApp, shelfContext.isFreeProduct, shelfContext.deviceFamily); + } + return null; + }); +} +function clickOptionsForActionLinkAction(objectGraph, productId, shelfMetrics) { + return { + targetType: "button", + actionType: "navigate", + id: productId, + pageInformation: shelfMetrics.metricsPageInformation, + locationTracker: shelfMetrics.locationTracker, + }; +} +function standardActionLinks(objectGraph, productData, productTitle, shelfMetrics, shouldShowRatingsAndReviews, isFirstPartyHideableApp, isBundle, isPreorder, isArcadeApp, isFreeProduct, deviceFamily) { + return validation.context("actionLinkShelf", () => { + const shelf = new models.Shelf("action"); + const items = []; + // Ratings & Reviews + if (shouldShowRatingsAndReviews) { + const reviewsData = mediaRelationship.relationship(productData, "reviews"); + const ratingsData = mediaAttributes.attributeAsDictionary(productData, "userRating"); + ratingsData.ratingAverage = serverData.asNumber(ratingsData, "value"); + ratingsData.adamId = productData.id; + ratingsData.isBundle = isBundle; + ratingsData.supportUrl = contentAttributes.contentAttributeAsString(objectGraph, productData, "supportURLForLanguage"); + const reviewItems = mediaDataStructure.dataCollectionFromDataContainer(reviewsData); + const appIcon = content.iconFromData(objectGraph, productData, { + useCase: 2 /* content.ArtworkUseCase.LockupIconMedium */, + }); + const ratingsAndReviewsLink = reviews.reviewsPageActionFromReviewsData(objectGraph, objectGraph.client.guid, ratingsData, reviewItems, reviewsData.next, productTitle, appIcon, false, false); + ratingsAndReviewsLink.title = objectGraph.loc.string("ProductPage.Section.Reviews.Title"); + items.push(ratingsAndReviewsLink); + } + // Version History + const versions = contentAttributes.contentAttributeAsArrayOrEmpty(objectGraph, productData, "versionHistory"); + if (versions.length > 0 && !isFirstPartyHideableApp && !isBundle && !isPreorder) { + const versionHistoryLinkAction = versionHistoryShelves.versionHistoryPageAction(objectGraph, versions, productData.id); + // Metrics (page information configured in version history builder) + metricsHelpersClicks.addClickEventToAction(objectGraph, versionHistoryLinkAction, clickOptionsForActionLinkAction(objectGraph, productData.id, shelfMetrics)); + items.push(versionHistoryLinkAction); + } + // Accessibility + if (isSome(deviceFamily) && + accessibilityShelves.shouldShowAccessibilitySection(objectGraph, productData, deviceFamily)) { + const action = accessibilityShelves.createSeeIntermediaryDetailsAction(objectGraph, productData, deviceFamily); + metricsHelpersClicks.addClickEventToAction(objectGraph, action, clickOptionsForActionLinkAction(objectGraph, productData.id, shelfMetrics)); + items.push(action); + } + // Info List + const infoShelf = informationShelf.create(objectGraph, productData, isFirstPartyHideableApp, shelfMetrics, null, isArcadeApp, isFreeProduct, false); + const infoPage = new models.GenericPage([infoShelf]); + const infoLinkAction = new models.FlowAction("page"); + infoLinkAction.title = objectGraph.loc.string("ProductPage.Section.Information.Title"); + infoPage.title = infoLinkAction.title; + // Metrics + shelfMetrics.addImpressionsToShelf(objectGraph, shelf, "information"); + const infoPageInformation = metricsHelpersPage.pageInformationForActionLinkPage(objectGraph, productData.id, "info"); + metricsHelpersPage.addMetricsEventsToPageWithInformation(objectGraph, infoPage, infoPageInformation); + metricsHelpersClicks.addClickEventToAction(objectGraph, infoLinkAction, clickOptionsForActionLinkAction(objectGraph, productData.id, shelfMetrics)); + infoLinkAction.pageData = infoPage; + items.push(infoLinkAction); + // Capabilities + const capabilities = productCapabilities.productCapabilitiesFromData(objectGraph, productData, isFreeProduct); + if (capabilities.length > 0) { + const capabilitiesShelf = new models.Shelf("productCapability"); + capabilitiesShelf.items = capabilities; + const capabilitiesPage = new models.GenericPage([capabilitiesShelf]); + const capabilitiesLinkAction = new models.FlowAction("page"); + capabilitiesLinkAction.title = objectGraph.loc.string("ProductPage.Section.Supports.Title"); + capabilitiesPage.title = capabilitiesLinkAction.title; + // Metrics + shelfMetrics.addImpressionsToShelf(objectGraph, shelf, "supports"); + const capabilitiesPageInformation = metricsHelpersPage.pageInformationForActionLinkPage(objectGraph, productData.id, "supports"); + metricsHelpersPage.addMetricsEventsToPageWithInformation(objectGraph, capabilitiesPage, capabilitiesPageInformation); + metricsHelpersClicks.addClickEventToAction(objectGraph, capabilitiesLinkAction, clickOptionsForActionLinkAction(objectGraph, productData.id, shelfMetrics)); + capabilitiesLinkAction.pageData = capabilitiesPage; + items.push(capabilitiesLinkAction); + } + // Privacy Link + const privacyPolicyUrl = contentAttributes.contentAttributeAsString(objectGraph, productData, "privacyPolicyUrl"); + if (privacyPolicyUrl) { + const privacyLinkAction = new models.ExternalUrlAction(privacyPolicyUrl, false); + // Metrics + metricsHelpersClicks.addClickEventToAction(objectGraph, privacyLinkAction, clickOptionsForActionLinkAction(objectGraph, productData.id, shelfMetrics)); + privacyLinkAction.title = objectGraph.loc.string("PRIVACY_POLICY"); + items.push(privacyLinkAction); + } + const traderData = contentAttributes.contentAttributeAsDictionary(objectGraph, productData, "sellerInfo"); + const safetyAndComplianceURL = serverData.asString(traderData, "safetyAndComplianceUrl"); + if ((safetyAndComplianceURL === null || safetyAndComplianceURL === void 0 ? void 0 : safetyAndComplianceURL.length) > 0) { + const safetyLinkAction = new models.ExternalUrlAction(safetyAndComplianceURL, false); + safetyLinkAction.title = objectGraph.loc.string("ProductPage.Section.SafetyAndCompliance.Title"); + items.push(safetyLinkAction); + } + shelf.items = items; + shelfMetrics.addImpressionsToShelf(objectGraph, shelf, "actionLinks"); + if (items.length > 0) { + return shelf; + } + else { + return null; + } + }); +} +//# sourceMappingURL=action-links-shelf.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/annotations/annotations.js b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/annotations/annotations.js new file mode 100644 index 0000000..d71ca64 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/annotations/annotations.js @@ -0,0 +1,310 @@ +import * as validation from "@jet/environment/json/validation"; +import * as models from "../../../../api/models/index"; +import * as serverData from "../../../../foundation/json-parsing/server-data"; +import { shallowCopyOf } from "../../../../foundation/util/objects"; +import * as productPageUtil from "../../product-page-util"; +import * as categoryAnnotation from "./category-annotation"; +import * as compatibilityAnnotation from "./compatibility-annotation"; +import * as contentRatingAnnotation from "./content-rating-annotation"; +import * as controllerAnnotation from "./controller-annotation"; +import * as copyrightAnnotation from "./copyright-annotation"; +import * as externalPurchasesAnnotation from "./external-purchases-annotation"; +import * as externalBrowserAnnotation from "./external-browser-annotation"; +import * as gameLicenseAnnotation from "./game-license-annotation"; +import * as highMotionAnnotation from "./high-motion-annotation"; +import * as languagesAnnotation from "./languages-annotation"; +import * as locationAnnotation from "./location-annotation"; +import * as productCapabilitiesAnnotation from "./product-capabilities-annotation"; +import * as sellerAnnotation from "./seller-annotation"; +import * as sizeAnnotation from "./size-annotation"; +import * as spatialControllerAnnotation from "./spatial-controller-annotation"; +import * as storefrontContentRatingAnnotation from "./storefront-content-rating-annotation"; +import * as topInAppPurchasesAnnotation from "./top-in-app-purchases-annotation"; +import * as versionAnnotation from "./version-annotation"; +import * as contentAttributes from "../../../content/attributes"; +import { isSome } from "@jet/environment"; +import { externalPurchasesPlacementIsEnabled } from "../../../offers/external-purchases"; +const standardList = [ + sellerAnnotation.createAnnotation, + sizeAnnotation.createAnnotation, + categoryAnnotation.createAnnotation, + controllerAnnotation.createAnnotation, + spatialControllerAnnotation.createAnnotation, + compatibilityAnnotation.createAnnotation, + externalBrowserAnnotation.createAnnotation, + locationAnnotation.createAnnotation, + languagesAnnotation.createAnnotation, + highMotionAnnotation.createAnnotation, + contentRatingAnnotation.createAnnotation, + topInAppPurchasesAnnotation.createAnnotation, + externalPurchasesAnnotation.createAnnotation, + gameLicenseAnnotation.createAnnotation, + copyrightAnnotation.createAnnotation, +]; +const sellerPositionAdjustedList = [ + sizeAnnotation.createAnnotation, + categoryAnnotation.createAnnotation, + controllerAnnotation.createAnnotation, + spatialControllerAnnotation.createAnnotation, + compatibilityAnnotation.createAnnotation, + externalBrowserAnnotation.createAnnotation, + locationAnnotation.createAnnotation, + languagesAnnotation.createAnnotation, + highMotionAnnotation.createAnnotation, + contentRatingAnnotation.createAnnotation, + topInAppPurchasesAnnotation.createAnnotation, + externalPurchasesAnnotation.createAnnotation, + sellerAnnotation.createAnnotation, + gameLicenseAnnotation.createAnnotation, + copyrightAnnotation.createAnnotation, +]; +const tvInformationList = [ + sellerAnnotation.createAnnotation, + categoryAnnotation.createAnnotation, + versionAnnotation.createAnnotation, + sizeAnnotation.createAnnotation, + gameLicenseAnnotation.createAnnotation, + copyrightAnnotation.createAnnotation, +]; +const tvSupportsList = [ + compatibilityAnnotation.createAnnotation, + languagesAnnotation.createAnnotation, + contentRatingAnnotation.createAnnotation, + storefrontContentRatingAnnotation.createAnnotation, +]; +const gamesList = [ + contentRatingAnnotation.createAnnotation, + categoryAnnotation.createAnnotation, + controllerAnnotation.createAnnotation, + languagesAnnotation.createAnnotation, + sizeAnnotation.createAnnotation, +]; +function createAnnotations(objectGraph, annotationList, data, isFirstPartyHideableApp, isArcadeApp, suppressTitle, shelfMetrics, metadataHandler) { + const annotations = []; + for (const annotationFactory of annotationList) { + const annotation = annotationFactory(objectGraph, data, isFirstPartyHideableApp, isArcadeApp, suppressTitle, shelfMetrics); + if (isSome(annotation)) { + // Don't overwrite existing items + if (annotation.items_V3.length === 0) { + annotation.items_V3 = translateItemsToV3(objectGraph, annotation); + } + annotation.expandAction = createExpandActionForAnnotation(objectGraph, annotation); + annotations.push(annotation); + if (metadataHandler) { + metadataHandler(annotationFactory, annotation, annotations.length - 1); + } + } + } + return annotations; +} +export function translateItemsToV3(objectGraph, annotation) { + const items_V3 = []; + for (const item of annotation.items) { + if (items_V3.length > 0) { + items_V3.push({ + $kind: "spacer", + }); + } + if (isSome(item.headingArtworks)) { + for (const artwork of item.headingArtworks) { + items_V3.push({ + $kind: "artwork", + artwork: artwork, + }); + } + } + // Linkable Text section: covers heading, main text, and lists + let markdownStyledText = ""; + if (isSome(item.heading)) { + markdownStyledText += `**${item.heading}**`; + } + if (isSome(item.text)) { + if (markdownStyledText.length > 0) { + markdownStyledText += "\n"; + } + markdownStyledText += item.text; + } + if (isSome(item.listText)) { + if (markdownStyledText.length > 0) { + markdownStyledText += "\n"; + } + markdownStyledText += item.listText; + } + if (markdownStyledText.length > 0) { + items_V3.push({ + $kind: "linkableText", + linkableText: new models.LinkableText(new models.StyledText(markdownStyledText, "text/markdown")), + }); + } + if (isSome(item.textPairs)) { + for (const textPair of item.textPairs) { + items_V3.push({ + $kind: "textPair", + leadingText: textPair[0], + trailingText: textPair[1], + }); + } + } + } + if (isSome(annotation.linkAction) && !objectGraph.client.isTV) { + items_V3.push({ + $kind: "button", + action: annotation.linkAction, + }); + } + return items_V3; +} +function standardInformationShelf(objectGraph, data, isFirstPartyHideableApp, shelfMetrics, isArcadeApp, metadataHandler) { + return validation.context("standardInformationShelf", () => { + let shelf; + const list = shouldUseSellerPositionAdjustedList(objectGraph, data) ? sellerPositionAdjustedList : standardList; + const informationItems = createAnnotations(objectGraph, list, data, isFirstPartyHideableApp, isArcadeApp, false, shelfMetrics, metadataHandler); + if (informationItems.length > 0) { + shelf = new models.Shelf("annotation"); + shelf.title = objectGraph.loc.string("ProductPage.Section.Information.Title"); + shelf.items = informationItems; + if (!serverData.isNull(shelfMetrics)) { + shelfMetrics.addImpressionsToShelf(objectGraph, shelf, "information"); + } + } + return shelf; + }); +} +function gamesInformationShelf(objectGraph, data, isFirstPartyHideableApp, shelfMetrics, isArcadeApp, metadataHandler) { + return validation.context("standardInformationShelf", () => { + const informationItems = createAnnotations(objectGraph, gamesList, data, isFirstPartyHideableApp, isArcadeApp, true, shelfMetrics, metadataHandler); + if (informationItems.length === 0) { + return undefined; + } + const shelf = new models.Shelf("annotation"); + shelf.items = informationItems; + return shelf; + }); +} +/** Checks if we should use the alternate position arrangement of seller info + * - The Seller ICP Annotation feature is enabled + * - The client platform is iOS + * - The Internet Content Provider info is available + * Or Seller info is available + * @returns true if either of the checks above are true + */ +function shouldUseSellerPositionAdjustedList(objectGraph, data) { + const icpInfo = contentAttributes.contentAttributeAsDictionary(objectGraph, data, "internetContentProviderInfo"); + const hasICPInfo = objectGraph.bag.enableSellerICPAnnotation && objectGraph.client.isiOS && isSome(icpInfo); + const sellerInfo = contentAttributes.contentAttributeAsDictionary(objectGraph, data, "sellerInfo"); + const hasSellerInfo = objectGraph.bag.enableSellerInfo && isSome(sellerInfo); + return hasICPInfo || hasSellerInfo; +} +function watchInformationShelf(objectGraph, data, isFirstPartyHideableApp, shelfMetrics) { + return validation.context("standardInformationShelf", () => { + let shelf; + const list = shouldUseSellerPositionAdjustedList(objectGraph, data) ? sellerPositionAdjustedList : standardList; + const informationItems = createAnnotations(objectGraph, list, data, isFirstPartyHideableApp, false, false, shelfMetrics, undefined); + if (informationItems.length > 0) { + shelf = new models.Shelf("annotation"); + shelf.items = informationItems; + if (!serverData.isNull(shelfMetrics)) { + shelfMetrics.addImpressionsToShelf(objectGraph, shelf, "information"); + } + } + return shelf; + }); +} +function tvInformationShelf(objectGraph, data, isFirstPartyHideableApp, shelfMetrics, forceExpanded, isFreeProduct, metadataHandler) { + return validation.context("tvInformationShelf", () => { + let shelf; + const informationItems = createAnnotations(objectGraph, tvInformationList, data, isFirstPartyHideableApp, false, false, shelfMetrics, metadataHandler); + const supportsItems = productCapabilitiesAnnotation + .createAnnotations(objectGraph, data, isFreeProduct) + .concat(createAnnotations(objectGraph, tvSupportsList, data, isFirstPartyHideableApp, false, false, shelfMetrics, metadataHandler)); + const externalPurchasesEnabled = externalPurchasesPlacementIsEnabled(objectGraph, "product-page-info-section"); + const tvTopInAppPurchasesList = externalPurchasesEnabled + ? [externalPurchasesAnnotation.createAnnotation] + : [topInAppPurchasesAnnotation.createAnnotation]; + const topInAppPurchaseItems = createAnnotations(objectGraph, tvTopInAppPurchasesList, data, isFirstPartyHideableApp, false, true, shelfMetrics, metadataHandler); + const annotationsGroups = []; + if (informationItems.length > 0) { + const annotationGroup = new models.AnnotationGroup(objectGraph.loc.string("ProductPage.Section.Information.Title"), informationItems, forceExpanded); + annotationsGroups.push(annotationGroup); + } + if (supportsItems.length > 0) { + const annotationGroup = new models.AnnotationGroup(objectGraph.loc.string("ProductPage.Section.Supports.Title"), supportsItems, forceExpanded); + annotationsGroups.push(annotationGroup); + } + if (topInAppPurchaseItems.length > 0) { + const inAppPurchasesTitle = externalPurchasesEnabled + ? objectGraph.loc.string("ProductPage.Section.ExternalPurchases.Title") + : objectGraph.loc.string("ProductPage.Section.TopInAppPurchases.Title"); + const annotationGroup = new models.AnnotationGroup(inAppPurchasesTitle, topInAppPurchaseItems, true); + annotationsGroups.push(annotationGroup); + } + if (annotationsGroups.length > 0) { + if (productPageUtil.isShelfBased(objectGraph)) { + shelf = new models.Shelf("annotationGroup"); + shelf.items = annotationsGroups; + shelf.background = { + type: "darkOverlay", + }; + if (!serverData.isNull(shelfMetrics)) { + shelfMetrics.addImpressionsToShelf(objectGraph, shelf, "information"); + } + } + else { + shelf = new models.Shelf("informationContainer"); + shelf.items = [new models.InformationContainer(annotationsGroups)]; + if (!serverData.isNull(shelfMetrics)) { + shelfMetrics.addImpressionsToShelf(objectGraph, shelf, "information"); + } + } + } + return shelf; + }); +} +/** + * Creates the information shelf for the current client's deviceType. + * + * @param productData The product data + * @param isFirstPartyHideableApp Indicates whether this app is first party and/or hideable + * @param shelfMetrics The shelf metrics + * @param metadataHandler The metadata handler used to associate an annotation with another element. + * @param isArcadeApp Whether or not the product is an Arcade app. + * @param isFreeProduct Whether the buy is for a free product. + * @param forceExpanded Whether the items in the shelf should begin expanded + */ +export function create(objectGraph, productData, isFirstPartyHideableApp, shelfMetrics, metadataHandler, isArcadeApp, isFreeProduct, forceExpanded) { + return validation.context("create", () => { + if (preprocessor.GAMES_TARGET) { + return gamesInformationShelf(objectGraph, productData, isFirstPartyHideableApp, shelfMetrics, isArcadeApp, metadataHandler); + } + let shelf; + switch (objectGraph.client.deviceType) { + case "watch": + shelf = watchInformationShelf(objectGraph, productData, isFirstPartyHideableApp, shelfMetrics); + break; + case "tv": + shelf = tvInformationShelf(objectGraph, productData, isFirstPartyHideableApp, shelfMetrics, forceExpanded, isFreeProduct, metadataHandler); + break; + default: + shelf = standardInformationShelf(objectGraph, productData, isFirstPartyHideableApp, shelfMetrics, isArcadeApp, metadataHandler); + } + return shelf; + }); +} +/** + * Creates a flow action for navigating to the expanded view of an annotation. + * + * @param objectGraph The current object graph + * @param annotation The source annotation + * @returns A flow action or null + */ +export function createExpandActionForAnnotation(objectGraph, annotation) { + if (!objectGraph.client.isVision) { + return undefined; + } + const flowAction = new models.FlowAction("annotationDetail"); + const detailAnnotation = shallowCopyOf(annotation); + detailAnnotation.shouldAlwaysPresentExpanded = true; + flowAction.pageData = detailAnnotation; + return flowAction; +} +//# sourceMappingURL=annotations.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/annotations/category-annotation.js b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/annotations/category-annotation.js new file mode 100644 index 0000000..8158739 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/annotations/category-annotation.js @@ -0,0 +1,33 @@ +import { isNothing } from "@jet/environment"; +import * as models from "../../../../api/models/index"; +import * as serverData from "../../../../foundation/json-parsing/server-data"; +import { contentAttributeAsDictionary } from "../../../content/attributes"; +import { artworkFromApiArtwork } from "../../../content/content"; +import * as lockups from "../../../lockups/lockups"; +export function createAnnotation(objectGraph, data, isFirstPartyHideableApp, isArcadeApp, suppressTitle, shelfMetrics) { + var _a; + if (isArcadeApp && !preprocessor.GAMES_TARGET) { + // Design prefers we do not show the category annotation for Arcade apps + return null; + } + const categoryName = lockups.categoryFromData(objectGraph, data); + if (isNothing(categoryName)) { + return null; + } + const categoryAnnotationItem = new models.AnnotationItem(categoryName); + const title = suppressTitle ? "" : objectGraph.loc.string("InfoList.Category.Title"); + const summary = preprocessor.GAMES_TARGET ? categoryName : undefined; + const annotation = new models.Annotation(title, [categoryAnnotationItem], summary); + if (objectGraph.client.isVision || preprocessor.GAMES_TARGET) { + const artworkData = contentAttributeAsDictionary(objectGraph, data, "editorialArtwork.brandLogo"); + if (serverData.isDefinedNonNull(artworkData)) { + annotation.leadingArtwork = + (_a = artworkFromApiArtwork(objectGraph, artworkData, { + useCase: 0 /* ArtworkUseCase.Default */, + allowingTransparency: true, + })) !== null && _a !== void 0 ? _a : undefined; + } + } + return annotation; +} +//# sourceMappingURL=category-annotation.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/annotations/compatibility-annotation.js b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/annotations/compatibility-annotation.js new file mode 100644 index 0000000..e259165 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/annotations/compatibility-annotation.js @@ -0,0 +1,169 @@ +import { isNothing } from "@jet/environment"; +import * as models from "../../../../api/models/index"; +import * as serverData from "../../../../foundation/json-parsing/server-data"; +import * as mediaAttributes from "../../../../foundation/media/attributes"; +import { createArtworkForResource } 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 lockups from "../../../lockups/lockups"; +import { translateItemsToV3 } from "./annotations"; +export function createAnnotation(objectGraph, data, isFirstPartyHideableApp, isArcadeApp, suppressTitle, shelfMetrics) { + const requirementsByDeviceFamily = mediaAttributes.attributeAsDictionary(data, "requirementsByDeviceFamily"); + let annotationItems = []; + let requirementsSummary = defaultRequirementsSummaryFromData(objectGraph, data); + const requiresRosetta = contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "requiresRosetta", contentAttributes.defaultAttributePlatform(objectGraph)); + if (serverData.isDefinedNonNullNonEmpty(requirementsByDeviceFamily)) { + // Create an annotation item for each of the device families in "requirementsByDeviceFamily" + const deviceFamilies = sortedDeviceFamilies(objectGraph); + for (const deviceFamily of deviceFamilies) { + const deviceFamilyRequirements = serverData.asDictionary(requirementsByDeviceFamily, deviceFamily); + if (serverData.isDefinedNonNullNonEmpty(deviceFamilyRequirements)) { + // When Rosetta is not available, use a more specific requirements string explaining it's not available for the region. + const preferRosettaUnavailableRequirementsString = objectGraph.appleSilicon.isSupportEnabled && + deviceFamily === "mac" && + requiresRosetta && + !objectGraph.appleSilicon.isRosettaAvailable; + const annotationItem = compatibilityAnnotationItemFromDeviceFamilyRequirements(objectGraph, deviceFamilyRequirements, preferRosettaUnavailableRequirementsString); + if (serverData.isDefinedNonNull(annotationItem)) { + annotationItems.push(annotationItem); + } + } + } + // If `requirementsSummary` is null, this means we don't meet all the required capabilities + // In this case, use the requirementsString for the first device family as the summary + if (!serverData.isDefinedNonNull(requirementsSummary) && annotationItems.length > 0) { + requirementsSummary = annotationItems[0].text; + } + } + else { + // If "requirementsByDeviceFamily" is empty or otherwise invalid, fall back to the legacy "requirementsString" + const annotationItem = legacyCompatibilityAnnotationItemFromData(objectGraph, data); + if (serverData.isDefinedNonNull(annotationItem)) { + annotationItems = [annotationItem]; + } + } + if (serverData.isNullOrEmpty(annotationItems)) { + return null; + } + const title = suppressTitle ? "" : objectGraph.loc.string("Requirements"); + const annotation = new models.Annotation(title, annotationItems, requirementsSummary); + if (annotation.items_V3.length === 0) { + annotation.items_V3 = translateItemsToV3(objectGraph, annotation); + } + if (preprocessor.GAMES_TARGET) { + const supportedPlatforms = content.supportedAppPlatformsFromData(objectGraph, data); + const isRunnableOnCurrentDevice = content.supportsPlatform(supportedPlatforms, content.currentAppPlatform(objectGraph)); + let systemImageName; + if (!isRunnableOnCurrentDevice && supportedPlatforms.length > 0) { + systemImageName = content.systemImageNameForAppPlatform(supportedPlatforms[0]); + } + else { + systemImageName = content.systemImageNameForAppPlatform(content.currentAppPlatform(objectGraph)); + } + annotation.leadingArtwork = createArtworkForResource(objectGraph, `systemimage://${systemImageName}`); + annotation.prefersSmallLeadingArtwork = true; + } + else if (objectGraph.client.isVision) { + annotation.leadingArtwork = createArtworkForResource(objectGraph, "systemimage://visionpro"); + annotation.prefersSmallLeadingArtwork = true; + } + return annotation; +} +/** + * Returns a `DeviceFamily` list, in the order the device families should be displayed + * This is based on a pre-defined sort order, and the current device type / model + * @returns {contentDeviceFamily.DeviceFamily[]} A list of device families + */ +function sortedDeviceFamilies(objectGraph) { + // Start with the default display order + const deviceFamilies = [ + "iphone", + "ipad", + "ipod", + "mac", + "realityDevice", + "tvos", + "watch", + ]; + // Now move our current device family to the start of the array + const clientDeviceFamily = contentDeviceFamily.deviceFamilyFromDeviceType(objectGraph, objectGraph.client.deviceType, objectGraph.host.deviceModel); + if (serverData.isDefinedNonNull(clientDeviceFamily)) { + const indexOfClientDeviceFamily = deviceFamilies.indexOf(clientDeviceFamily); + if (indexOfClientDeviceFamily > 0) { + deviceFamilies.splice(indexOfClientDeviceFamily, 1); + deviceFamilies.unshift(clientDeviceFamily); + } + } + return deviceFamilies; +} +/** + * Returns the default requirements summary, eg. "Works on this iPhone" + * Currently only returns a value if the device has all required capabilities + * + * @param data MAPI data to build the summary with. + * @returns {string} The requirements summary + */ +function defaultRequirementsSummaryFromData(objectGraph, data) { + let requirementsSummary; + if (lockups.deviceHasCapabilitiesFromData(objectGraph, data)) { + const supportsMacOSCompatibleIOSBinary = content.supportsMacOSCompatibleIOSBinaryFromData(objectGraph, data, objectGraph.appleSilicon.isSupportEnabled); + const supportsVisionOSCompatibleIOSBinary = content.supportsVisionOSCompatibleIOSBinaryFromData(objectGraph, data); + const supportedAppPlatforms = content.supportedAppPlatformsFromData(objectGraph, data); + const isMacOSAppRunnable = content.isMacOSAppBuyableAndRunnableFromData(objectGraph, data, objectGraph.appleSilicon.isSupportEnabled, objectGraph.appleSilicon.isRosettaAvailable); + const isThisDeviceSupported = content.runnableOnDevice(objectGraph, supportedAppPlatforms, objectGraph.client.deviceType, supportsMacOSCompatibleIOSBinary, supportsVisionOSCompatibleIOSBinary, isMacOSAppRunnable); + const deviceName = objectGraph.loc.deviceDisplayName(objectGraph); + if (isThisDeviceSupported && deviceName) { + const requirementsSummaryReplacement = objectGraph.loc.string("InfoList.Requirements.Summary"); + requirementsSummary = requirementsSummaryReplacement.replace("{deviceName}", deviceName); + } + } + return requirementsSummary; +} +/** + * Creates an annotation item from a set of requirements for a specific device family + * + * @param {JSONData} deviceFamilyData data to build the annotation item with + * @param {boolean} preferRosettaUnavailableRequirementsString Use more specific Rosetta unavailable requirements string. + * @returns {models.AnnotationItem} The annotation item + */ +function compatibilityAnnotationItemFromDeviceFamilyRequirements(objectGraph, deviceFamilyData, preferRosettaUnavailableRequirementsString) { + var _a; + const deviceFamilyName = (_a = serverData.asString(deviceFamilyData, "deviceFamily")) !== null && _a !== void 0 ? _a : undefined; + let requirementsString = serverData.asString(deviceFamilyData, "requirementString"); + const devices = serverData.asArrayOrEmpty(deviceFamilyData, "devices"); + // When Rosetta is not available, use a more specific requirements string explaining it's not available for the region. + if (preferRosettaUnavailableRequirementsString) { + const rosettaUnavailableRequirementsString = serverData.asString(deviceFamilyData, "rosettaUnavailableRequirementString"); + if (serverData.isDefinedNonNullNonEmpty(rosettaUnavailableRequirementsString)) { + requirementsString = rosettaUnavailableRequirementsString; + } + } + if (isNothing(requirementsString)) { + return null; + } + // Combine list of devices into a bullet list + const deviceListItems = []; + for (const device of devices) { + // Wrap the device name in a FSI/PDI unicode pair, to ensure it maintains it's correct direction + // in both LTR and RTL environments + deviceListItems.push("\u2022\t\u2068" + device + "\u2069"); + } + const devicesList = deviceListItems.length > 0 ? deviceListItems.join("\n") : undefined; + return new models.AnnotationItem(requirementsString, { heading: deviceFamilyName, listText: devicesList }); +} +/** + * Creates an annotation item from the "requirementsString" text string + * This is only used in fallback scenarios, where "requirementsByDeviceFamily" is empty or invalid + * + * @param data MAPI data to build the annotation item with + * @returns {models.AnnotationItem} The annotation item + */ +function legacyCompatibilityAnnotationItemFromData(objectGraph, data) { + const requirements = contentAttributes.contentAttributeAsString(objectGraph, data, "requirementsString"); + if (serverData.isNull(requirements)) { + return null; + } + return new models.AnnotationItem(requirements); +} +//# sourceMappingURL=compatibility-annotation.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/annotations/content-rating-annotation.js b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/annotations/content-rating-annotation.js new file mode 100644 index 0000000..da7dc7b --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/annotations/content-rating-annotation.js @@ -0,0 +1,368 @@ +import { isNothing, isSome } from "@jet/environment"; +import * as models from "../../../../api/models/index"; +import * as serverData from "../../../../foundation/json-parsing/server-data"; +import * as ageRatings from "../../../content/age-ratings"; +import * as mediaAttributes from "../../../../foundation/media/attributes"; +import * as mediaPlatformAttributes from "../../../../foundation/media/platform-attributes"; +import * as mediaRelationship from "../../../../foundation/media/relationships"; +import * as contentAttributes from "../../../content/attributes"; +import * as storefrontContentRatingAnnotation from "./storefront-content-rating-annotation"; +import { artworkTemplateForBundleImage, createArtworkForResource } from "../../../content/artwork/artwork"; +import { makeRoutableArticlePageIntent } from "../../../../api/intents/routable-article-page-intent"; +import { getLocale } from "../../../../common/locale"; +/** + * Attempts to create an age rating annotation, preferring the new "ageRating" + * MAPI data. Falls back to "contentRatingsBySystem.appsApple" if needed. + */ +export function createAnnotation(objectGraph, data, isFirstPartyHideableApp, isArcadeApp, suppressTitle, shelfMetrics) { + var _a; + return ((_a = createModernAnnotation(objectGraph, data, isFirstPartyHideableApp, isArcadeApp, suppressTitle, shelfMetrics)) !== null && _a !== void 0 ? _a : createLegacyAnnotation(objectGraph, data, isFirstPartyHideableApp, isArcadeApp, suppressTitle, shelfMetrics)); +} +/** + * Returns an `Annotation` model from the new style "ageRating" MAPI age rating + * data. + */ +function createModernAnnotation(objectGraph, data, isFirstPartyHideableApp, isArcadeApp, suppressTitle, shelfMetrics) { + // The textual name of the rating, e.g. "18+" + const name = ageRatings.name(objectGraph, data); + if (isNothing(name)) { + return null; + } + // "Learn More" link + const learnMoreTitle = objectGraph.loc.string("InfoList.AgeRating.LearnMore"); + const learnMoreAction = createLearnMoreAction(objectGraph, learnMoreTitle); + let annotationTitle; + let annotationSummary; + if (preprocessor.GAMES_TARGET) { + if (ageRatings.hasInAppControls(objectGraph, data)) { + annotationTitle = objectGraph.localizer.string("GameDetails.Annotation.AgeRating.Title", { + ageRating: name, + }); + annotationSummary = objectGraph.loc.string("GameDetails.Annotation.AgeRating.InAppControls.Summary"); + } + else { + annotationTitle = ""; + annotationSummary = objectGraph.localizer.string("GameDetails.Annotation.AgeRating.Title", { + ageRating: name, + }); + } + } + else { + annotationTitle = objectGraph.loc.string("InfoList.AgeRating.Title"); + annotationSummary = name; + } + const annotation = new models.Annotation(annotationTitle, [], annotationSummary, learnMoreAction); + // Set text for visionOS collapsed annotation + if (objectGraph.client.isVision || preprocessor.GAMES_TARGET) { + annotation.leadingText = name; + } + // Push either a pictogram representing the rating (currently just Korea + // and Brazil use this), or a text encapsulation of the rating name. + const pictogramResource = ageRatings.pictogramResource(objectGraph, data); + if (isSome(pictogramResource)) { + const pictogramArtwork = createArtworkForResource(objectGraph, artworkTemplateForBundleImage(pictogramResource), 30, 30); + if (preprocessor.GAMES_TARGET) { + annotation.expandedArtwork = pictogramArtwork; + } + else { + annotation.items_V3.push({ + $kind: "artwork", + artwork: pictogramArtwork, + }); + } + } + else { + annotation.items_V3.push({ + $kind: "textEncapsulation", + text: name, + }); + } + // An optional description of the app's rating, potentially with some + // information about the storefront's rating agency. + const description = ageRatings.description(objectGraph, data); + let ageRatingText = ""; + if (isSome(description)) { + ageRatingText += description; + } + const linkedSubstrings = {}; + if ((objectGraph.client.isiOS || objectGraph.client.isMac || objectGraph.client.isVision) && + isSome(learnMoreAction)) { + // iOS, macOS, and visionOS show the "Learn More" link as LinkableText. + linkedSubstrings[learnMoreTitle] = learnMoreAction; + if (ageRatingText.length > 0) { + ageRatingText += "\n"; + } + ageRatingText += learnMoreTitle; + } + const ageRatingLinkableText = new models.LinkableText(new models.StyledText(ageRatingText, "text/markdown"), linkedSubstrings); + annotation.items_V3.push({ + $kind: "linkableText", + linkableText: ageRatingLinkableText, + }); + // Watch and Web use a button for the "Learn More" link. + if ((objectGraph.client.isWatch || objectGraph.client.isWeb) && isSome(learnMoreAction)) { + annotation.items_V3.push({ + $kind: "button", + action: learnMoreAction, + style: "infer", + }); + } + const contentRestrictionsAnnotation = contentRestrictionsAnnotationFromData(objectGraph, data); + if (isSome(contentRestrictionsAnnotation)) { + annotation.items_V3.push({ + $kind: "spacer", + }); + annotation.items_V3.push(contentRestrictionsAnnotation); + } + // Optional external website where developer can provide more context on + // their app's rating and parental controls. This link is omitted for TV + // since there is no web browser. + const developerAgeGuidanceURL = ageRatings.developerAgeGuidanceURL(objectGraph, data); + if (isSome(developerAgeGuidanceURL) && !objectGraph.client.isTV) { + const openDeveloperAgeGuidanceURLAction = new models.ExternalUrlAction(developerAgeGuidanceURL); + openDeveloperAgeGuidanceURLAction.title = objectGraph.loc.string("InfoList.AgeRating.DeveloperInfo"); + annotation.items_V3.push({ + $kind: "spacer", + }); + annotation.items_V3.push({ + $kind: "button", + action: openDeveloperAgeGuidanceURLAction, + }); + } + return annotation; +} +/** + * Returns an `Annotation` model from the old style + * "contentRatingsBySystem.appsApple" MAPI data. + */ +function createLegacyAnnotation(objectGraph, data, isFirstPartyHideableApp, isArcadeApp, suppressTitle, shelfMetrics) { + const contentRating = mediaAttributes.attributeAsDictionary(data, "contentRatingsBySystem.appsApple"); + if (serverData.isNull(contentRating)) { + return null; + } + const name = serverData.asString(contentRating, "name"); + if (serverData.isNull(name)) { + return null; + } + const rank = serverData.asNumber(contentRating, "rank"); + if (rank === 99) { + // This indicates the app is not yet rated, which should only happen in test environments. + return null; + } + const advisories = serverData.asArrayOrEmpty(contentRating, "advisories"); + // Build the summary. + let summary; + const ageBand = mediaPlatformAttributes.platformAttributeAsDictionary(data, contentAttributes.bestAttributePlatformFromData(objectGraph, data), "ageBand"); + const minAge = serverData.asNumber(ageBand, "minAge"); + const maxAge = serverData.asNumber(ageBand, "maxAge"); + if (serverData.isDefinedNonNull(minAge) && serverData.isDefinedNonNull(maxAge)) { + summary = objectGraph.loc + .string("InfoList.AgeRating.Summary") + .replace("{ageRating}", name) + .replace("{minAgeRating}", objectGraph.loc.decimal(minAge)) + .replace("{maxAgeRating}", objectGraph.loc.decimal(maxAge)); + } + else { + summary = name; + } + const ageRatingAnnotationItems = []; + // AnnotationItem will insert vertical space between the summary and each + // advisory if they are separate items, so keep them as a single item. + let appleRatingString = ""; + if (isSome(summary)) { + // Always use the Apple rating summary as our first item. + appleRatingString += summary; + } + for (const advisory of advisories) { + // Tack on the advisories. + appleRatingString += "\n"; + appleRatingString += advisory; + } + ageRatingAnnotationItems.push(new models.AnnotationItem(appleRatingString)); + // Display the storefront-specific content rating if present under the + // App Store content rating (currently this is just for South Korea). + // TV displays the storefront-specific content rating in a separate + // annotation section. + if (!objectGraph.client.isTV) { + const pictogramRow = storefrontContentRatingAnnotation.contentRatingPictogramRowIfNeeded(objectGraph, data); + if (isSome(pictogramRow)) { + ageRatingAnnotationItems.push(pictogramRow); + } + // Check if we are required to display the age verification annotations (e.g. this is true for the South Korean storefront) + const ageVerificationAnnotationItem = ageVerificationAnnotationItemIfNeeded(objectGraph, data); + if (isSome(ageVerificationAnnotationItem)) { + ageRatingAnnotationItems.push(ageVerificationAnnotationItem); + } + } + let finalSummaryToUse; + if (objectGraph.client.isVision) { + finalSummaryToUse = objectGraph.loc.string("InfoList.AgeRating.Years").replace("{age}", name); + } + else if (preprocessor.GAMES_TARGET) { + finalSummaryToUse = objectGraph.loc.string("InfoList.AgeRating.Title"); + } + else if (ageRatingAnnotationItems.length > 1 || ageRatingAnnotationItems[0].text !== summary) { + // Use an explicit summary unless we're just showing a simple rating + // without advisories. E.g. a rating of "4+" does not need a summary. + finalSummaryToUse = summary; + } + const ageRatingTitle = suppressTitle ? "" : objectGraph.loc.string("InfoList.AgeRating.Title"); + // Learn More link + const learnMoreTitle = objectGraph.loc.string("InfoList.AgeRating.LearnMore"); + const learnMoreAction = createLearnMoreAction(objectGraph, learnMoreTitle); + const annotation = new models.Annotation(ageRatingTitle, ageRatingAnnotationItems, finalSummaryToUse, learnMoreAction); + if (objectGraph.client.isVision || preprocessor.GAMES_TARGET) { + annotation.leadingText = name; + } + return annotation; +} +/** + * Creates an `Action` that navigates to the App Store Age Ratings editorial item. + * @param objectGraph The App Store Object Graph. + * @param title The title for the action. + * @returns A platform-appropriate `Action` linking to the Age Ratings editorial page. + */ +function createLearnMoreAction(objectGraph, title) { + let learnMoreAction; + if (isSome(objectGraph.bag.ageRatingLearnMoreEditorialItemId) && + objectGraph.bag.ageRatingLearnMoreEditorialItemId.length > 0) { + const flowAction = new models.FlowAction("article"); + flowAction.title = title; + flowAction.pageUrl = `https://apps.apple.com/story/id${objectGraph.bag.ageRatingLearnMoreEditorialItemId}`; + const routableArticlePageIntent = makeRoutableArticlePageIntent({ + ...getLocale(objectGraph), + id: objectGraph.bag.ageRatingLearnMoreEditorialItemId, + }); + flowAction.destination = routableArticlePageIntent; + if (objectGraph.client.isVision) { + const dismissAction = new models.FlowBackAction("sheetDismiss"); + learnMoreAction = new models.CompoundAction([dismissAction, flowAction]); + learnMoreAction.title = title; + } + else { + learnMoreAction = flowAction; + } + } + return learnMoreAction; +} +// #region Age Verification +/** + * Returns a special annotation item when age verification is necessary to download an app. + * @param objectGraph The App Store object graph. + * @param data Media API data representing an app. + */ +export function ageVerificationAnnotationItemIfNeeded(objectGraph, data) { + if (objectGraph.bag.requireAgeVerification && + checkContentRating(objectGraph, data, "seventeenPlus") && + isProductGenreGame(objectGraph, data)) { + let ageVerificationString; + const isBundle = data.type === "app-bundles"; + if (isBundle) { + // If this bundle contains a GRAC-registered product, show a message for GRAC bundles. + // Search through the bundle's apps and stop if a GRAC-registered app is found. + const childrenRelationship = mediaRelationship.relationship(data, "apps"); + if (serverData.isDefinedNonNull(childrenRelationship)) { + for (const childApp of childrenRelationship.data) { + const gracRegistrationNumber = mediaAttributes.attributeAsString(childApp, "gracRegistrationNumber"); + if (serverData.isDefinedNonNull(gracRegistrationNumber)) { + ageVerificationString = objectGraph.loc.string("AgeVerification.ProductPage.Rating.BundleUnsuitableForJuveniles"); + break; + } + } + } + } + else if (serverData.isDefinedNonNull(mediaAttributes.attributeAsString(data, "gracRegistrationNumber"))) { + // If this product is GRAC-registered, show a message for GRAC products. + ageVerificationString = objectGraph.loc.string("AgeVerification.ProductPage.Rating.UnsuitableForJuveniles"); + } + if (isNothing(ageVerificationString)) { + return null; + } + return new models.AnnotationItem(ageVerificationString); + } + else { + return null; + } +} +/** + * Returns a boolean indicating if a content rating is at least as high as + * the supplied rating rank. + * @param data Media API data representing an app. + * @param isAtLeast A content rating rank name. + */ +function checkContentRating(objectGraph, data, isAtLeast) { + const rating = mediaAttributes.attributeAsNumber(data, "contentRatingsBySystem.appsApple.rank"); + if (isNothing(rating)) { + return false; + } + let contentRatingRankValue; + switch (isAtLeast) { + case "twelvePlus": + contentRatingRankValue = 3; + break; + case "seventeenPlus": + contentRatingRankValue = 4; + break; + case "adultOnly": + contentRatingRankValue = 5; + break; + default: + contentRatingRankValue = 1; + } + return rating >= contentRatingRankValue; +} +/** + * Returns a boolean indicating if the product is classified as game. + * @param data The product data. + */ +function isProductGenreGame(objectGraph, data) { + var _a; + const genreDicts = (_a = mediaRelationship.relationshipCollection(data, "genres")) !== null && _a !== void 0 ? _a : []; + for (const candidateDict of genreDicts) { + if (serverData.asString(candidateDict, "id") === 6014 /* constants.GenreIds.Games */.toString()) { + return true; + } + } + return false; +} +/** + * Returns an `AnnotationItemLinkableText` model representing the content descriptors. + * @param objectGraph The App Store object graph + * @param data The age rating object pulled from the app data. + */ +function contentRestrictionsAnnotationFromData(objectGraph, data) { + const contentLevels = mediaAttributes.attributeAsArrayOrEmpty(data, "ageRating.contentLevels"); + let contentDescriptorsString = ""; + for (const contentLevel of contentLevels) { + const contentLevelHeading = serverData.asString(contentLevel, "level"); + const contentDescriptors = serverData.asArrayOrEmpty(contentLevel, "contentDescriptors"); + // Skip this content level if it somehow doesn't have descriptors. + if (contentDescriptors.length === 0) { + continue; + } + // Add newline between proceeding content level + if (contentDescriptorsString.length > 0) { + contentDescriptorsString += "\n\n"; + } + // Heading + contentDescriptorsString += `**${contentLevelHeading}**`; + // Descriptors + for (const contentDescriptor of contentDescriptors) { + const description = serverData.asString(contentDescriptor, "description"); + if (isSome(description)) { + contentDescriptorsString += "\n"; + contentDescriptorsString += description; + } + } + } + if (contentDescriptorsString.length === 0) { + return undefined; + } + const linkableText = new models.LinkableText(new models.StyledText(contentDescriptorsString, "text/markdown")); + return { + $kind: "linkableText", + linkableText: linkableText, + }; +} +// #endregion +//# sourceMappingURL=content-rating-annotation.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/annotations/controller-annotation.js b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/annotations/controller-annotation.js new file mode 100644 index 0000000..b74159b --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/annotations/controller-annotation.js @@ -0,0 +1,68 @@ +import { isSome } from "@jet/environment"; +import * as models from "../../../../api/models/index"; +import { isFeatureEnabledForCurrentUser } from "../../../../common/util/lottery"; +import * as mediaAttributes from "../../../../foundation/media/attributes"; +import { createArtworkForResource } from "../../../content/artwork/artwork"; +import * as gameController from "./../../../content/game-controller"; +export function createAnnotation(objectGraph, data, isFirstPartyHideableApp, isArcadeApp, suppressTitle, shelfMetrics) { + if (objectGraph.client.isVision && gameController.isSpatialControllerRequired(objectGraph, data)) { + // Controllers and spatial controllers annotations are mutually exclusive + return null; + } + const isFeatureEnabled = isFeatureEnabledForCurrentUser(objectGraph, objectGraph.bag.gameControllerRecommendedRolloutRate); + const isRecommended = gameController.isGameControllerRecommended(objectGraph, data); + const isRequired = objectGraph.client.isVision && gameController.isGameControllerRequired(objectGraph, data); + const isSupported = preprocessor.GAMES_TARGET && gameController.isGameControllerSupported(objectGraph, data); + if (isFeatureEnabled && (isRecommended || isRequired || isSupported)) { + const title = suppressTitle ? "" : objectGraph.loc.string("ProductPage.Info.GameController.Title"); // Game Controller + // Summary + let summaryKey; + if (preprocessor.GAMES_TARGET) { + summaryKey = isRecommended + ? "GameDetails.Annotation.Controller.Recommended.Title" + : isRequired + ? "GameDetails.Annotation.Controller.Required.Title" + : "GameDetails.Annotation.Controller.Supported.Title"; + } + else { + summaryKey = isRecommended + ? "ProductPage.Info.GameController.Recommended" + : "ProductPage.Info.GameController.Required"; + } + const summary = objectGraph.loc.string(summaryKey); + // Items + const artistName = mediaAttributes.attributeAsString(data, "artistName"); + const itemKey = isRecommended + ? "ProductPage.Info.GameController.Recommended.Summary.v2" + : "ProductPage.Info.GameController.Required.Summary.v2"; + const itemText = objectGraph.loc.string(itemKey).replace("{developerName}", artistName); + const items = [new models.AnnotationItem(itemText)]; + // Learn More link + let linkAction; + const storyId = objectGraph.bag.gameControllerLearnMoreEditorialItemId; + if (isSome(storyId) && storyId.length > 0) { + const flowAction = new models.FlowAction("article"); + const titleKey = isRecommended + ? "ProductPage.Info.GameController.Recommended.LearnMore" + : "ProductPage.Info.GameController.Required.LearnMore"; + flowAction.title = objectGraph.loc.string(titleKey); + flowAction.pageUrl = `https://apps.apple.com/story/id${storyId}`; + if (objectGraph.client.isVision) { + const dismissAction = new models.FlowBackAction("sheetDismiss"); + linkAction = new models.CompoundAction([dismissAction, flowAction]); + linkAction.title = flowAction.title; + } + else { + linkAction = flowAction; + } + } + const annotation = new models.Annotation(title, items, summary, linkAction); + if (objectGraph.client.isVision || preprocessor.GAMES_TARGET) { + annotation.leadingArtwork = createArtworkForResource(objectGraph, "systemimage://gamecontroller.fill"); + annotation.prefersSmallLeadingArtwork = true; + } + return annotation; + } + return null; +} +//# sourceMappingURL=controller-annotation.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/annotations/copyright-annotation.js b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/annotations/copyright-annotation.js new file mode 100644 index 0000000..adc274f --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/annotations/copyright-annotation.js @@ -0,0 +1,18 @@ +import { isNothing } from "@jet/environment"; +import * as models from "../../../../api/models/index"; +import { createArtworkForResource } from "../../../content/artwork/artwork"; +import * as contentAttributes from "../../../content/attributes"; +export function createAnnotation(objectGraph, data, isFirstPartyHideableApp, isArcadeApp, suppressTitle, shelfMetrics) { + const copyrightString = contentAttributes.contentAttributeAsString(objectGraph, data, "copyright"); + if (isNothing(copyrightString)) { + return null; + } + const copyrightItem = new models.AnnotationItem(copyrightString); + const title = suppressTitle ? "" : objectGraph.loc.string("InfoList.Copyright"); + const annotation = new models.Annotation(title, [copyrightItem]); + if (objectGraph.client.isVision || preprocessor.GAMES_TARGET) { + annotation.leadingArtwork = createArtworkForResource(objectGraph, "resource://copyright"); + } + return annotation; +} +//# sourceMappingURL=copyright-annotation.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/annotations/external-browser-annotation.js b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/annotations/external-browser-annotation.js new file mode 100644 index 0000000..19cc906 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/annotations/external-browser-annotation.js @@ -0,0 +1,54 @@ +import { isSome } from "@jet/environment/types/optional"; +import * as models from "../../../../api/models/index"; +import { createArtworkForResource } from "../../../content/artwork/artwork"; +import { hasExternalBrowserForData } from "../../../content/content"; +import * as metricsHelpersClicks from "../../../metrics/helpers/clicks"; +import * as serverData from "../../../../foundation/json-parsing/server-data"; +/** + * Creates the external browser annotation, which is only present if an app is using an external browser engine. + */ +export function createAnnotation(objectGraph, data, isFirstPartyHideableApp, isArcadeApp, suppressTitle, shelfMetrics) { + var _a; + if (!hasExternalBrowserForData(objectGraph, data)) { + return null; + } + const learnMoreAction = (_a = createExternalBrowserLearnMoreAction(objectGraph, shelfMetrics.metricsPageInformation, shelfMetrics.locationTracker)) !== null && _a !== void 0 ? _a : undefined; + let linkAction = learnMoreAction; + if (objectGraph.client.isVision && isSome(learnMoreAction)) { + const dismissAction = new models.FlowBackAction("sheetDismiss"); + linkAction = new models.CompoundAction([dismissAction, learnMoreAction]); + linkAction.title = learnMoreAction.title; + } + const title = suppressTitle ? "" : objectGraph.loc.string("Annotations.ExternalBrowser.Title"); + const summary = objectGraph.loc.string("Annotations.ExternalBrowser.Value.Summary"); + const itemText = objectGraph.loc.string("Annotations.ExternalBrowser.Value.Expanded"); + const items = [new models.AnnotationItem(itemText)]; + const annotation = new models.Annotation(title, items, summary, linkAction); + if (objectGraph.client.isVision) { + annotation.leadingArtwork = createArtworkForResource(objectGraph, "systemimage://exclamationmark.triangle"); + } + return annotation; +} +/** + * Creates a flow action for the external browser learn more story. + * @param objectGraph Current object graph + * @returns A flow action for the story, or null + */ +function createExternalBrowserLearnMoreAction(objectGraph, metricsPageInformation, metricsLocationTracker) { + const editorialItemId = objectGraph.bag.externalBrowserLearnMoreEditorialItemId; + if (serverData.isNullOrEmpty(editorialItemId)) { + return null; + } + const flowAction = new models.FlowAction("article"); + flowAction.title = objectGraph.loc.string("Action.LearnMore"); + flowAction.pageUrl = `https://apps.apple.com/story/id${editorialItemId}`; + metricsHelpersClicks.addClickEventToAction(objectGraph, flowAction, { + id: "LearnMore", + targetType: "link", + actionType: "navigate", + pageInformation: metricsPageInformation, + locationTracker: metricsLocationTracker, + }); + return flowAction; +} +//# sourceMappingURL=external-browser-annotation.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/annotations/external-purchases-annotation.js b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/annotations/external-purchases-annotation.js new file mode 100644 index 0000000..a955b00 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/annotations/external-purchases-annotation.js @@ -0,0 +1,39 @@ +import { isSome } from "@jet/environment/types/optional"; +import * as models from "../../../../api/models/index"; +import { externalPurchasesLearnMoreAction, externalPurchasesPlacementIsEnabled, hasExternalPurchasesForData, } from "../../../offers/external-purchases"; +import { createArtworkForResource } from "../../../content/artwork/artwork"; +/** + * Creates the external purchases annotation, which is only present if an app is using external purchases, and this feature is enabled in the bag. + */ +export function createAnnotation(objectGraph, data, isFirstPartyHideableApp, isArcadeApp, suppressTitle, shelfMetrics) { + const hasExternalPurchases = hasExternalPurchasesForData(objectGraph, data); + const externalPurchasesEnabled = externalPurchasesPlacementIsEnabled(objectGraph, "product-page-info-section"); + if (!hasExternalPurchases || !externalPurchasesEnabled) { + return null; + } + const title = suppressTitle ? "" : objectGraph.loc.string("Annotations.ExternalPurchases.Title"); + const summary = objectGraph.loc.string("YES"); + let itemText; + if (isSome(objectGraph.bag.externalPurchasesProductPageAnnotationVariant)) { + const key = `Annotations.ExternalPurchases.Expanded.Variant${objectGraph.bag.externalPurchasesProductPageAnnotationVariant}`; + itemText = objectGraph.loc.string(key); + } + else { + itemText = objectGraph.loc.string("Annotations.ExternalPurchases.Expanded"); + } + const item = new models.AnnotationItem(itemText); + const learnMoreTitle = objectGraph.loc.string("Annotations.ExternalPurchases.Expanded.LearnMore"); + const learnMoreAction = externalPurchasesLearnMoreAction(objectGraph, learnMoreTitle, shelfMetrics.metricsPageInformation, shelfMetrics.locationTracker); + let wrappedLearnMoreAction = learnMoreAction; + if (objectGraph.client.isVision && isSome(learnMoreAction)) { + const dismissAction = new models.FlowBackAction("sheetDismiss"); + wrappedLearnMoreAction = new models.CompoundAction([dismissAction, learnMoreAction]); + wrappedLearnMoreAction.title = learnMoreAction.title; + } + const annotation = new models.Annotation(title, [item], summary, wrappedLearnMoreAction !== null && wrappedLearnMoreAction !== void 0 ? wrappedLearnMoreAction : undefined); + if (objectGraph.client.isVision) { + annotation.leadingArtwork = createArtworkForResource(objectGraph, "systemimage://exclamationmark.triangle"); + } + return annotation; +} +//# sourceMappingURL=external-purchases-annotation.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/annotations/game-license-annotation.js b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/annotations/game-license-annotation.js new file mode 100644 index 0000000..4153f08 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/annotations/game-license-annotation.js @@ -0,0 +1,73 @@ +import * as models from "../../../../api/models/index"; +import { asString, attributeAsDictionary } from "@apple-media-services/media-api"; +import { isSome } from "@jet/environment"; +import { isNotEmpty } from "../../../../foundation/util/array-util"; +import { createArtworkForResource } from "../../../content/artwork/artwork"; +/** + * Create an annotation for a game license, currently supported for the Vietnamese storefront. + */ +export function createAnnotation(objectGraph, data, isFirstPartyHideableApp, isArcadeApp, suppressTitle, shelfMetrics) { + const gameLicense = attributeAsDictionary(data, "licenses.gameLicenseVietnam"); + const items = [plainText(objectGraph.loc.string("InfoList.GameLicense.Disclaimer"))]; + // Add license id + const gameLicenseId = asString(gameLicense, "licenseId"); + if (isSome(gameLicenseId) && isNotEmpty(gameLicenseId)) { + items.push(spacer(), heading(objectGraph.loc.string("InfoList.GameLicense.Id.Title")), plainText(gameLicenseId)); + } + else { + // The license id is required. If this is missing, we should not display this annotation. + return null; + } + // Add license url + const gameLicenseUrl = asString(gameLicense, "licenseUrl"); + if (isSome(gameLicenseUrl) && isNotEmpty(gameLicenseUrl)) { + if (objectGraph.client.isWatch) { + items.push(buttonLink(objectGraph.loc.string("InfoList.GameLicense.Url.ButtonTitle"), gameLicenseUrl)); + } + else { + items.push(spacer(), heading(objectGraph.loc.string("InfoList.GameLicense.Url.Title")), externalLink(gameLicenseUrl)); + } + } + const title = suppressTitle ? "" : objectGraph.loc.string("InfoList.GameLicense.Title"); + const annotation = new models.Annotation(title, [], gameLicenseId); + annotation.items_V3 = items; + if (objectGraph.client.isVision) { + annotation.leadingArtwork = createArtworkForResource(objectGraph, "systemimage://text.page"); + } + return annotation; +} +// region Annotation Helpers +function spacer() { + return { + $kind: "spacer", + }; +} +function heading(text) { + return { + $kind: "linkableText", + linkableText: new models.LinkableText(new models.StyledText(`**${text}**`)), + }; +} +function plainText(text) { + return { + $kind: "linkableText", + linkableText: new models.LinkableText(new models.StyledText(text)), + }; +} +function externalLink(url) { + const linkedSubstrings = {}; + linkedSubstrings[url] = new models.ExternalUrlAction(url, false); + return { + $kind: "linkableText", + linkableText: new models.LinkableText(new models.StyledText(url), linkedSubstrings), + }; +} +function buttonLink(title, url) { + const action = new models.ExternalUrlAction(url, false); + action.title = title; + return { + $kind: "button", + action: action, + }; +} +//# sourceMappingURL=game-license-annotation.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/annotations/high-motion-annotation.js b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/annotations/high-motion-annotation.js new file mode 100644 index 0000000..4c3c5d8 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/annotations/high-motion-annotation.js @@ -0,0 +1,53 @@ +import * as models from "../../../../api/models/index"; +import * as serverData from "../../../../foundation/json-parsing/server-data"; +import { createArtworkForResource } from "../../../content/artwork/artwork"; +import { contentAttributeAsBooleanOrFalse } from "../../../content/attributes"; +export function createAnnotation(objectGraph, data, isFirstPartyHideableApp, isArcadeApp, suppressTitle, shelfMetrics) { + // The "High Motion" flag shouldn't appear in data for other platform apps, but to be sure + // we don't want the annotation appearing anywhere else accidentally. + if (!objectGraph.client.isVision) { + return null; + } + const isHighMotion = contentAttributeAsBooleanOrFalse(objectGraph, data, "isHighMotion"); + if (!isHighMotion) { + return null; + } + const artwork = createArtworkForResource(objectGraph, "resource://motion.circle.fill"); + const highMotionAnnotationItem = new models.AnnotationItem(objectGraph.loc.string("ProductPage.Info.HighMotion.Item.Description"), { heading: objectGraph.loc.string("ProductPage.Info.HighMotion.Item.Heading") }); + const highMotionAnnotationItemHeading = objectGraph.loc.string("ProductPage.Info.HighMotion.Item.Heading"); + const highMotionAnnotationItemDescription = objectGraph.loc.string("ProductPage.Info.HighMotion.Item.Description"); + const highMotionAnnotationItems_V3 = [ + { + $kind: "linkableText", + linkableText: new models.LinkableText(new models.StyledText(`**${highMotionAnnotationItemHeading}**\n\n${highMotionAnnotationItemDescription}`)), + }, + ]; + const title = suppressTitle ? "" : objectGraph.loc.string("Annotations.HighMotion.Title"); + const summary = objectGraph.loc.string("Annotations.HighMotion.Summary"); + // Learn More link + let learnMoreAction; + if (serverData.isDefinedNonNullNonEmpty(objectGraph.bag.highMotionLearnMoreEditorialItemId)) { + const flowAction = new models.FlowAction("article"); + flowAction.title = objectGraph.loc.string("ProductPage.Info.HighMotion.LearnMore"); + flowAction.pageUrl = `https://apps.apple.com/story/id${objectGraph.bag.highMotionLearnMoreEditorialItemId}`; + if (objectGraph.client.isVision) { + const dismissAction = new models.FlowBackAction("sheetDismiss"); + learnMoreAction = new models.CompoundAction([dismissAction, flowAction]); + learnMoreAction.title = flowAction.title; + } + else { + learnMoreAction = flowAction; + } + highMotionAnnotationItems_V3.push({ + $kind: "button", + action: learnMoreAction, + }); + } + const annotation = new models.Annotation(title, [highMotionAnnotationItem], summary, learnMoreAction); + annotation.leadingArtwork = artwork; + annotation.expandedArtwork = artwork; + annotation.prefersSmallLeadingArtwork = true; + annotation.items_V3 = highMotionAnnotationItems_V3; + return annotation; +} +//# sourceMappingURL=high-motion-annotation.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/annotations/languages-annotation.js b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/annotations/languages-annotation.js new file mode 100644 index 0000000..91aecc3 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/annotations/languages-annotation.js @@ -0,0 +1,61 @@ +import * as models from "../../../../api/models/index"; +import * as serverData from "../../../../foundation/json-parsing/server-data"; +import * as stringUtil from "../../../../foundation/util/string-util"; +import * as contentAttributes from "../../../content/attributes"; +import { primaryLanguageLocaleFromLocales } from "../../../content/content"; +export function createAnnotation(objectGraph, data, isFirstPartyHideableApp, isArcadeApp, suppressTitle, shelfMetrics) { + var _a; + const languageList = contentAttributes.contentAttributeAsArrayOrEmpty(objectGraph, data, "languageList"); + if (serverData.isNullOrEmpty(languageList)) { + return null; + } + const languageCount = languageList.length; + if (languageCount < 1) { + return null; + } + let summaryString; + let expandedString; + switch (languageCount) { + case 0: + break; + case 1: + expandedString = languageList[0]; + if (preprocessor.GAMES_TARGET) { + summaryString = expandedString; + } + break; + case 2: + expandedString = objectGraph.loc + .string("InfoList.Two.Languages.Summary") + .replace("{first}", languageList[0]) + .replace("{second}", languageList[1]); + if (preprocessor.GAMES_TARGET) { + summaryString = expandedString; + } + break; + default: + const additionalLanguagesCount = languageCount - 1; + summaryString = objectGraph.loc + .string("InfoList.More.Languages.Summary") + .replace("{language}", languageList[0]) + .replace("{count}", objectGraph.loc.formattedCount(additionalLanguagesCount)); + expandedString = + (_a = stringUtil.join(languageList, objectGraph.loc.string("Conjunction.Separator"))) !== null && _a !== void 0 ? _a : undefined; + break; + } + if (!serverData.isDefinedNonNull(expandedString)) { + return null; + } + const expandedItem = new models.AnnotationItem(expandedString); + const title = suppressTitle ? "" : objectGraph.loc.string("Supported.Languages"); + const annotation = new models.Annotation(title, [expandedItem], summaryString); + if (objectGraph.client.isVision || preprocessor.GAMES_TARGET) { + const supportedLocales = contentAttributes.contentAttributeAsArrayOrEmpty(objectGraph, data, "supportedLocales"); + const primaryLocale = primaryLanguageLocaleFromLocales(objectGraph, supportedLocales); + if (serverData.isDefinedNonNull(primaryLocale)) { + annotation.leadingText = primaryLocale.tag; + } + } + return annotation; +} +//# sourceMappingURL=languages-annotation.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/annotations/location-annotation.js b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/annotations/location-annotation.js new file mode 100644 index 0000000..eb07202 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/annotations/location-annotation.js @@ -0,0 +1,18 @@ +import * as models from "../../../../api/models/index"; +import * as mediaAttributes from "../../../../foundation/media/attributes"; +import { createArtworkForResource } from "../../../content/artwork/artwork"; +export function createAnnotation(objectGraph, data, isFirstPartyHideableApp, isArcadeApp, suppressTitle, shelfMetrics) { + const usesLocationBackgroundMode = mediaAttributes.attributeAsBooleanOrFalse(data, "usesLocationBackgroundMode"); + if (!usesLocationBackgroundMode) { + return null; + } + const locationTitle = suppressTitle ? "" : objectGraph.loc.string("InfoList.Location.Title"); + const locationDescription = objectGraph.loc.string("InfoList.Location.Description"); + const locationItem = new models.AnnotationItem(locationDescription); + const annotation = new models.Annotation(locationTitle, [locationItem]); + if (objectGraph.client.isVision || preprocessor.GAMES_TARGET) { + annotation.leadingArtwork = createArtworkForResource(objectGraph, "systemimage://location.circle"); + } + return annotation; +} +//# sourceMappingURL=location-annotation.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/annotations/product-capabilities-annotation.js b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/annotations/product-capabilities-annotation.js new file mode 100644 index 0000000..08fc843 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/annotations/product-capabilities-annotation.js @@ -0,0 +1,25 @@ +import * as validation from "@jet/environment/json/validation"; +import * as models from "../../../../api/models/index"; +import { productCapabilitiesFromData } from "../../../content/product-capabilities"; +import { translateItemsToV3 } from "./annotations"; +export function createAnnotations(objectGraph, productData, isFreeProduct) { + return validation.context("productCapabilitiesAsAnnotations", () => { + const annotations = []; + const capabilities = productCapabilitiesFromData(objectGraph, productData, isFreeProduct); + for (const capability of capabilities) { + const title = capability.title; + const text = capability.caption.styledText.rawText; + if ((title === null || title === void 0 ? void 0 : title.length) > 0 && (text === null || text === void 0 ? void 0 : text.length) > 0) { + const annotationItem = new models.AnnotationItem(text); + const annotation = new models.Annotation(title, [annotationItem]); + // Don't overwrite existing items + if (annotation.items_V3.length === 0) { + annotation.items_V3 = translateItemsToV3(objectGraph, annotation); + } + annotations.push(annotation); + } + } + return annotations; + }); +} +//# sourceMappingURL=product-capabilities-annotation.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/annotations/seller-annotation.js b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/annotations/seller-annotation.js new file mode 100644 index 0000000..e779e6c --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/annotations/seller-annotation.js @@ -0,0 +1,117 @@ +import { isNothing, isSome } from "@jet/environment"; +import * as models from "../../../../api/models/index"; +import * as serverData from "../../../../foundation/json-parsing/server-data"; +import { createArtworkForResource } from "../../../content/artwork/artwork"; +import * as contentAttributes from "../../../content/attributes"; +export function createAnnotation(objectGraph, data, isFirstPartyHideableApp, isArcadeApp, suppressTitle, shelfMetrics) { + if (isArcadeApp) { + return null; + } + const sellerData = contentAttributes.contentAttributeAsString(objectGraph, data, "seller"); + const sellerInfoData = contentAttributes.contentAttributeAsDictionary(objectGraph, data, "sellerInfo"); + const icpInfo = contentAttributes.contentAttributeAsDictionary(objectGraph, data, "internetContentProviderInfo"); + const enableSellerICP = objectGraph.bag.enableSellerICPAnnotation && objectGraph.client.isiOS; + if (isNothing(sellerData)) { + return null; + } + const sellerLabel = contentAttributes.contentAttributeAsString(objectGraph, data, "sellerLabel") || + objectGraph.loc.string("Seller"); + let annotation; + let annotationItems = []; + const sellerDataName = serverData.asString(sellerInfoData, "name"); + const sellerName = isSome(sellerDataName) && (sellerDataName === null || sellerDataName === void 0 ? void 0 : sellerDataName.length) > 0 ? sellerDataName : sellerData; + const title = suppressTitle ? "" : sellerLabel; + let annotationSummary = null; + if (isSome(sellerInfoData)) { + const [newItems, summary] = createSellerInfoAnnotationItemsAndSummary(objectGraph, sellerInfoData); + annotationSummary = summary; + annotationItems = annotationItems.concat(newItems); + } + if (enableSellerICP && isSome(icpInfo)) { + const annotationItem = setupAnnotationForKey(objectGraph, "filingNumber", objectGraph.loc.string("ProductPage.Info.ICPNumber.Title"), icpInfo); + // If no other seller info, include seller title in ICP annotation + if (annotationItems.length === 0) { + const annotationTitle = new models.AnnotationItem(sellerName); + annotationItems.push(annotationTitle); + } + if (isSome(annotationItem)) { + annotationItems.push(annotationItem); + } + } + if (annotationItems.length > 0) { + if (isNothing(annotationSummary)) { + annotationSummary = annotationItems.length > 1 ? sellerName : undefined; + } + annotation = new models.Annotation(title, annotationItems, annotationSummary, undefined); + } + else { + const sellerItem = new models.AnnotationItem(sellerData); + annotation = new models.Annotation(title, [sellerItem]); + } + if (isSome(annotation) && (objectGraph.client.isVision || preprocessor.GAMES_TARGET)) { + annotation.leadingArtwork = createArtworkForResource(objectGraph, "resource://person.crop.app"); + } + return annotation; +} +/** + * Create the seller info annotation items on the product page. + * + * @param sellerData The sellerInfo map from the product page JSON fetch response. + * @returns A tuple consisting of a list of seller info annotation items and an optional string for the summary, if necessary to provide. + */ +function createSellerInfoAnnotationItemsAndSummary(objectGraph, sellerData) { + const sellerInfoAnnotationItems = []; + const locKeysMap = { + name: "ProductPage.SellerInfo.Name.Title", + dunsNumber: "ProductPage.SellerInfo.DunsNumber.Title", + address: "ProductPage.SellerInfo.Address.Title", + brn: "ProductPage.SellerInfo.BusinessRegistrationNumber.Title", + phoneNumber: "ProductPage.SellerInfo.PhoneNumber.Title", + email: "ProductPage.SellerInfo.Email.Title", + usci: "ProductPage.SellerInfo.UnifiedSocialCreditIdentifier.Title", + }; + const sellerDataName = serverData.asString(sellerData, "name"); + const sellerName = isSome(sellerDataName) && (sellerDataName === null || sellerDataName === void 0 ? void 0 : sellerDataName.length) > 0 ? sellerDataName : objectGraph.loc.string("Seller"); + const traderBooleanProvided = isSome(serverData.asString(sellerData, "isTrader")); + if (traderBooleanProvided) { + const hasTraderInfo = serverData.asBoolean(sellerData, "isTrader"); + // Trader Identified + const traderVerificationString = hasTraderInfo + ? "ProductPage.SellerInfo.VerifiedSeller.Subtitle" + : "ProductPage.SellerInfo.UnverifiedSeller.Subtitle"; + sellerInfoAnnotationItems.push(new models.AnnotationItem(objectGraph.loc.string(traderVerificationString).replace("{seller}", sellerName))); + if (!hasTraderInfo) { + return [sellerInfoAnnotationItems, sellerName]; + } + } + // List remaining annotations from seller info + // https://developer.apple.com/help/app-store-connect/manage-compliance-information/manage-european-union-digital-services-act-trader-requirements/ + let summary = null; + // If there's only one key in seller data and it's not the seller name, put "See Details" summary. + if (Object.keys(sellerData).length === 1 && !Object.keys(sellerData).includes("name")) { + summary = objectGraph.loc.string("ProductPage.SellerInfo.SeeDetails.Title"); + } + for (const [key, val] of Object.entries(locKeysMap)) { + // Don't need a redundant name field if already included in DSA trader verification subtitle. + if (key === "name" && traderBooleanProvided) { + continue; + } + const annotation = setupAnnotationForKey(objectGraph, key, objectGraph.loc.string(val), sellerData); + if (isSome(annotation)) { + sellerInfoAnnotationItems.push(annotation); + } + } + return [sellerInfoAnnotationItems, summary]; +} +function setupAnnotationForKey(objectGraph, key, title, sellerData) { + let itemText = serverData.asString(sellerData, key); + if (isSome(itemText)) { + if (key === "brn") { + itemText = + itemText + "\n" + objectGraph.loc.string("ProductPage.SellerInfo.BusinessRegistrationNumber.Subtitle"); + } + return new models.AnnotationItem(itemText, { heading: title }); + } + return null; +} +//# sourceMappingURL=seller-annotation.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/annotations/size-annotation.js b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/annotations/size-annotation.js new file mode 100644 index 0000000..7dd9905 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/annotations/size-annotation.js @@ -0,0 +1,61 @@ +import { isSome } from "@jet/environment"; +import * as models from "../../../../api/models/index"; +import * as serverData from "../../../../foundation/json-parsing/server-data"; +import * as content from "../../../content/content"; +export function createAnnotation(objectGraph, data, isFirstPartyHideableApp, isArcadeApp, suppressTitle, shelfMetrics) { + const combinedFileSize = content.combinedFileSizeFromData(objectGraph, data); + if (isFirstPartyHideableApp || + serverData.isNull(combinedFileSize) || + serverData.isNull(combinedFileSize.fileSizeByDevice)) { + return null; + } + const sizeString = objectGraph.loc.fileSize(combinedFileSize.fileSizeByDevice); + const title = suppressTitle ? "" : objectGraph.loc.string("File.Size"); + let totalFileSize = combinedFileSize.fileSizeByDevice; + let annotation; + if (serverData.isDefinedNonNull(combinedFileSize.maxEssentialInstallSizeInBytes)) { + const combinedBinaryAndEssentialSize = combinedFileSize.fileSizeByDevice + combinedFileSize.maxEssentialInstallSizeInBytes; + const maxEssentialInstallSizeInBytesString = objectGraph.loc + .string("Install.Size.String") + .replace("{fileSize}", objectGraph.loc.fileSize(combinedBinaryAndEssentialSize)); + const sizeItem = new models.AnnotationItem(maxEssentialInstallSizeInBytesString); + const summary = preprocessor.GAMES_TARGET + ? objectGraph.loc.fileSize(combinedBinaryAndEssentialSize) + : undefined; + annotation = new models.Annotation(title, [sizeItem], summary); + } + else if (serverData.isDefinedNonNull(combinedFileSize.maxInstallSizeInBytes)) { + const sizeListItems = []; + const initialAppSizeItem = new models.AnnotationItem(sizeString, { + heading: objectGraph.loc.string("Initial.App.Size"), + }); + sizeListItems.push(initialAppSizeItem); + const sizeStringInstall = objectGraph.loc + .string("Install.Size.String") + .replace("{fileSize}", objectGraph.loc.fileSize(combinedFileSize.maxInstallSizeInBytes)); + const maxInstallSizeInBytesItem = new models.AnnotationItem(sizeStringInstall, { + heading: objectGraph.loc.string("Additional.Content.After.App.Install"), + }); + sizeListItems.push(maxInstallSizeInBytesItem); + totalFileSize = totalFileSize + combinedFileSize.maxInstallSizeInBytes; + const summarySizeString = preprocessor.GAMES_TARGET + ? objectGraph.loc.fileSize(totalFileSize) + : objectGraph.loc + .string("Install.Size.String") + .replace("{fileSize}", objectGraph.loc.fileSize(totalFileSize)); + annotation = new models.Annotation(title, sizeListItems, summarySizeString); + } + else { + const sizeItem = new models.AnnotationItem(sizeString); + const summary = preprocessor.GAMES_TARGET ? sizeString : undefined; + annotation = new models.Annotation(title, [sizeItem], summary); + } + if (objectGraph.client.isVision || preprocessor.GAMES_TARGET) { + const fileSizeAndUnit = content.fileSizeAndUnitFromCombinedFileSize(objectGraph, combinedFileSize); + if (isSome(fileSizeAndUnit)) { + annotation.leadingText = fileSizeAndUnit.unit; + } + } + return annotation; +} +//# sourceMappingURL=size-annotation.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/annotations/spatial-controller-annotation.js b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/annotations/spatial-controller-annotation.js new file mode 100644 index 0000000..3915c5b --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/annotations/spatial-controller-annotation.js @@ -0,0 +1,60 @@ +import { isNothing, isSome } from "@jet/environment"; +import * as models from "../../../../api/models/index"; +import * as mediaAttributes from "../../../../foundation/media/attributes"; +import { createArtworkForResource } from "../../../content/artwork/artwork"; +import * as gameController from "./../../../content/game-controller"; +export function createAnnotation(objectGraph, data, isFirstPartyHideableApp, isArcadeApp, suppressTitle, shelfMetrics) { + if (!objectGraph.client.isVision) { + return null; + } + const isRequired = gameController.isSpatialControllerRequired(objectGraph, data); + if (!isRequired) { + return null; + } + const title = suppressTitle ? "" : objectGraph.loc.string("ProductPage.Info.SpatialController.Title"); + const summary = objectGraph.loc.string("ProductPage.Info.SpatialController.Required.Summary"); + // Items + const artistName = mediaAttributes.attributeAsString(data, "artistName"); + if (isNothing(artistName) || artistName.length === 0) { + return null; + } + const itemText = objectGraph.loc + .string("ProductPage.Info.SpatialController.Required.Item") + .replace("{developerName}", artistName); + const styledText = new models.StyledText(itemText); + const items = [ + { + $kind: "linkableText", + linkableText: new models.LinkableText(styledText), + }, + ]; + // Learn More link + const storyId = objectGraph.bag.spatialControlsLearnMoreEditorialItemId; + if (isSome(storyId) && storyId.length > 0) { + // The native support for the `small` spacer was introduced in 2025B. Older clients will just ignore this + // and show the standard size space, which is still preferable over no space at all. + items.push({ + $kind: "spacer", + size: "small", + }); + const actionTitle = objectGraph.loc.string("Action.LearnMore"); + const flowAction = new models.FlowAction("article"); + flowAction.title = actionTitle; + flowAction.pageUrl = `https://apps.apple.com/story/id${storyId}`; + const dismissAction = new models.FlowBackAction("sheetDismiss"); + const linkAction = new models.CompoundAction([dismissAction, flowAction]); + linkAction.title = actionTitle; + const linkedSubstrings = {}; + linkedSubstrings[actionTitle] = linkAction; + items.push({ + $kind: "linkableText", + linkableText: new models.LinkableText(new models.StyledText(actionTitle), linkedSubstrings), + }); + } + const annotation = new models.Annotation(title, [], summary); + annotation.items_V3 = items; + annotation.leadingArtwork = createArtworkForResource(objectGraph, "resource://SpatialControllers"); + annotation.prefersSmallLeadingArtwork = true; + return annotation; +} +//# sourceMappingURL=spatial-controller-annotation.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/annotations/storefront-content-rating-annotation.js b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/annotations/storefront-content-rating-annotation.js new file mode 100644 index 0000000..4c8bd56 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/annotations/storefront-content-rating-annotation.js @@ -0,0 +1,53 @@ +import * as models from "../../../../api/models/index"; +import * as serverData from "../../../../foundation/json-parsing/server-data"; +import * as mediaAttributes from "../../../../foundation/media/attributes"; +import * as artwork from "../../../content/artwork/artwork"; +import { isSome, isNothing } from "@jet/environment"; +import { ageVerificationAnnotationItemIfNeeded } from "./content-rating-annotation"; +import { storefrontContentRatingResourceForRank } from "../../../content/content"; +/** + * Only used by TV. Shows the full annotation by default without needing to expand. + */ +export function createAnnotation(objectGraph, data, isFirstPartyHideableApp, isArcadeApp, suppressTitle, shelfMetrics) { + const annotationItems = []; + const storefrontContentRating = contentRatingPictogramRowIfNeeded(objectGraph, data); + if (isSome(storefrontContentRating)) { + annotationItems.push(storefrontContentRating); + } + const ageVerificationAnnotationItem = ageVerificationAnnotationItemIfNeeded(objectGraph, data); + if (isSome(ageVerificationAnnotationItem)) { + annotationItems.push(ageVerificationAnnotationItem); + } + if (annotationItems.length === 0) { + return null; + } + const title = objectGraph.loc.string("InfoList.AgeRating.Title.KR"); + const annotation = new models.Annotation(title, annotationItems); + annotation.shouldAlwaysPresentExpanded = true; + return annotation; +} +export function contentRatingPictogramRowIfNeeded(objectGraph, data) { + const krContentRating = mediaAttributes.attributeAsDictionary(data, "contentRatingsBySystem.appsKorea"); + if (isNothing(krContentRating)) { + return null; + } + const krContentRank = serverData.asNumber(krContentRating, "rank"); + if (isNothing(krContentRank)) { + return null; + } + const krContentAdvisory = serverData.asArrayOrEmpty(krContentRating, "advisories")[0]; + if (isNothing(krContentAdvisory)) { + return null; + } + // Make sure we have a content rank, and the rank is not 19+. 19+ pictogram is not displayed. + const pictogramResource = storefrontContentRatingResourceForRank(objectGraph, krContentRank); + const krRatingPictograms = []; + if (isSome(pictogramResource)) { + krRatingPictograms.push(artwork.createArtworkForResource(objectGraph, `resource://${pictogramResource}`)); + } + const pictogramRow = new models.AnnotationItem(krContentAdvisory, { + headingArtworks: krRatingPictograms, + }); + return pictogramRow; +} +//# sourceMappingURL=storefront-content-rating-annotation.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/annotations/top-in-app-purchases-annotation.js b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/annotations/top-in-app-purchases-annotation.js new file mode 100644 index 0000000..ecfa58c --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/annotations/top-in-app-purchases-annotation.js @@ -0,0 +1,142 @@ +import * as models from "../../../../api/models/index"; +import * as serverData from "../../../../foundation/json-parsing/server-data"; +import { isNothing, isSome } from "@jet/environment"; +import * as mediaAttributes from "../../../../foundation/media/attributes"; +import * as mediaRelationship from "../../../../foundation/media/relationships"; +import { createArtworkForResource } from "../../../content/artwork/artwork"; +import * as offerFormatting from "../../../offers/offer-formatting"; +import * as offers from "../../../offers/offers"; +import { isEmpty } from "../../../../foundation/util/array-util"; +import { makeRoutableArticlePageIntent } from "../../../../api/intents/routable-article-page-intent"; +import { getLocale } from "../../../locale"; +import { makeRoutableArticlePageCanonicalUrl } from "../../../today/routable-article-page-url-utils"; +import { getPlatform } from "../../../preview-platform"; +export function createAnnotation(objectGraph, data, isFirstPartyHideableApp, isArcadeApp, suppressTitle, shelfMetrics) { + const topInApps = mediaRelationship.relationshipCollection(data, "top-in-apps"); + if (isNothing(topInApps) || isEmpty(topInApps)) { + return null; + } + const textPairs = []; + const items_V3 = []; + // Create IAP items + for (const inAppData of topInApps) { + const name = mediaAttributes.attributeAsString(inAppData, "name"); + if (isNothing(name)) { + continue; + } + const offer = offers.offerDataFromData(objectGraph, inAppData); + if (isNothing(offer)) { + continue; + } + const price = serverData.asString(offer, "priceFormatted"); + if (isNothing(price)) { + continue; + } + const duration = serverData.asString(offer, "recurringSubscriptionPeriod"); + const numberOfPeriods = serverData.asNumber(offer, "numOfPeriods"); + let nameAndDuration = objectGraph.loc.string("InfoList.IAP.Duration"); + if (objectGraph.client.isiOS && isSome(duration)) { + nameAndDuration = nameAndDuration.replace("{name}", name); + const subscriptionRecurrence = offerFormatting.subscriptionRecurrenceForServerRecurrence(objectGraph, duration, numberOfPeriods); + let timeUnit; + switch (subscriptionRecurrence === null || subscriptionRecurrence === void 0 ? void 0 : subscriptionRecurrence.type) { + case "D": + timeUnit = "days"; + break; + case "W": + timeUnit = "weeks"; + break; + case "M": + timeUnit = "months"; + break; + case "Y": + timeUnit = "years"; + break; + default: + nameAndDuration = `${name}`; + break; + } + if (isSome(timeUnit) && isSome(subscriptionRecurrence)) { + const formattedDuration = objectGraph.loc.formatDuration(subscriptionRecurrence.periodDuration, timeUnit); + if (isSome(formattedDuration)) { + nameAndDuration = nameAndDuration.replace("{duration}", formattedDuration); + } + } + } + else { + nameAndDuration = `${name}`; + } + textPairs.push([nameAndDuration, price]); + items_V3.push({ + $kind: "textPair", + leadingText: nameAndDuration, + trailingText: price, + }); + } + // Learn More link + const learnMoreTitle = objectGraph.loc.string("ProductPage.Info.InAppPurchases.LearnMore"); + const learnMoreAction = makeLearnMoreAction(objectGraph, learnMoreTitle); + if (isSome(learnMoreAction)) { + if (objectGraph.client.isiOS || objectGraph.client.isMac || objectGraph.client.isVision) { + // The native support for the `small` spacer was introduced in 2025B. Older clients will just ignore this + // and show the standard size space, which is still preferable over no space at all. + items_V3.push({ + $kind: "spacer", + size: "small", + }); + // On iOS, macOS & visionOS, we want this to appear as a link. + const linkedSubstrings = {}; + linkedSubstrings[learnMoreTitle] = learnMoreAction; + items_V3.push({ + $kind: "linkableText", + linkableText: new models.LinkableText(new models.StyledText(learnMoreTitle), linkedSubstrings), + }); + } + else if (objectGraph.client.isWatch || objectGraph.client.isWeb) { + // On watchOS & web, we want this to appear as a button. + items_V3.push({ + $kind: "button", + action: learnMoreAction, + style: "infer", + }); + } + // We intentionally leave out tvOS + } + const title = suppressTitle ? "" : objectGraph.loc.string("InfoList.InAppPurchases"); + const items = [new models.AnnotationItem(undefined, { textPairs: textPairs })]; + const annotation = new models.Annotation(title, items, objectGraph.loc.string("YES")); + annotation.items_V3 = items_V3; + if (objectGraph.client.isVision || preprocessor.GAMES_TARGET) { + annotation.leadingArtwork = createArtworkForResource(objectGraph, "resource://in.app.purchase"); + } + return annotation; +} +function makeLearnMoreAction(objectGraph, title) { + const iapLearnMoreEditorialItemID = objectGraph.bag.inAppPurchasesLearnMoreEditorialItemId; + if (isNothing(iapLearnMoreEditorialItemID) || isEmpty(iapLearnMoreEditorialItemID)) { + return null; + } + const flowAction = new models.FlowAction("article"); + flowAction.title = title; + flowAction.pageUrl = `https://apps.apple.com/story/id${objectGraph.bag.inAppPurchasesLearnMoreEditorialItemId}`; + if (objectGraph.client.isVision) { + const dismissAction = new models.FlowBackAction("sheetDismiss"); + const compoundAction = new models.CompoundAction([dismissAction, flowAction]); + compoundAction.title = title; + return compoundAction; + } + else if (objectGraph.client.isWeb) { + const routableArticlePageIntent = makeRoutableArticlePageIntent({ + ...getLocale(objectGraph), + ...getPlatform(objectGraph), + id: iapLearnMoreEditorialItemID, + }); + flowAction.pageUrl = makeRoutableArticlePageCanonicalUrl(objectGraph, routableArticlePageIntent); + flowAction.destination = routableArticlePageIntent; + return flowAction; + } + else { + return flowAction; + } +} +//# sourceMappingURL=top-in-app-purchases-annotation.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/annotations/version-annotation.js b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/annotations/version-annotation.js new file mode 100644 index 0000000..60341d4 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/annotations/version-annotation.js @@ -0,0 +1,41 @@ +import * as models from "../../../../api/models/index"; +import * as serverData from "../../../../foundation/json-parsing/server-data"; +import * as contentAttributes from "../../../content/attributes"; +export function createAnnotation(objectGraph, data, isFirstPartyHideableApp, isArcadeApp, suppressTitle, shelfMetrics) { + const versions = contentAttributes.contentAttributeAsArrayOrEmpty(objectGraph, data, "versionHistory"); + if (versions.length === 0) { + return null; + } + const latestVersion = versions[0]; + let releaseDate = null; + const releaseDateRaw = serverData.asString(latestVersion, "releaseDate"); + if (releaseDateRaw) { + const releaseDateFormat = objectGraph.loc.string("ProductPage.VersionAnnotation.Value.DateFormat"); + releaseDate = objectGraph.loc.formatDate(releaseDateFormat, new Date(releaseDateRaw)); + } + const versionDisplay = serverData.asString(latestVersion, "versionDisplay"); + if (versions.length === 0) { + return null; + } + let versionItemString; + if (versionDisplay && releaseDate) { + versionItemString = objectGraph.loc + .string("ProductPage.VersionAnnotation.Value") + .replace("@@version@@", versionDisplay) + .replace("@@date@@", releaseDate); + } + else if (versionDisplay) { + versionItemString = versionDisplay; + } + else { + return null; + } + const versionItem = new models.AnnotationItem(versionItemString); + const title = suppressTitle ? "" : objectGraph.loc.string("ProductPage.VersionAnnotation.Title"); + const version = new models.Annotation(title, [versionItem]); + if (version && !isArcadeApp) { + return version; + } + return null; +} +//# sourceMappingURL=version-annotation.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/bundle-children-shelf.js b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/bundle-children-shelf.js new file mode 100644 index 0000000..0b87c87 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/bundle-children-shelf.js @@ -0,0 +1,52 @@ +import * as validation from "@jet/environment/json/validation"; +import * as models from "../../../api/models"; +import * as metricsHelpersLocation from "../../metrics/helpers/location"; +import * as productPageCommon from "./../product-page-common"; +/** + * Create a shelf of bundle children for the product page. + * + * @param productData The raw product data response for a product page JSON fetch. + * @param dataContainer The raw data container response for a product page JSON fetch. + * @param shelfMetrics The product page shelf metrics. + * @returns A shelf of bundle children. + */ +export function create(objectGraph, productData, dataContainer, shelfMetrics) { + if (!dataContainer) { + return null; + } + return validation.context("bundleChildrenShelf", () => { + const shelfContentType = "smallLockup"; + const shelf = new models.Shelf(shelfContentType); + shelf.isHorizontal = true; + const numberOfChildren = dataContainer.data.length; + if (!numberOfChildren) { + return null; + } + shelf.title = objectGraph.loc.stringWithCount("ProductPage.AppsInBundleShelf.Title", numberOfChildren); + const containerItems = productPageCommon.lockupsFromDataContainer(objectGraph, dataContainer, shelfMetrics, 1 /* content.ArtworkUseCase.LockupIconSmall */, shelfContentType, null, 76286 /* filtering.Filter.Bundles */, null, objectGraph.featureFlags.isEnabled("voyager_bundles_2025A")); + if (containerItems.remainingItems.length) { + const shelfToken = new productPageCommon.ProductPageShelfToken(productData.id, containerItems.remainingItems, shelf.title, false, undefined, "smallLockup", "infer", null); + shelfToken.isBundleShelf = true; + shelf.url = productPageCommon.shelfContentUrl(objectGraph, shelfToken, shelfMetrics); + } + metricsHelpersLocation.pushContentLocation(objectGraph, { + pageInformation: shelfMetrics.metricsPageInformation, + locationTracker: shelfMetrics.locationTracker, + targetType: "swoosh", + id: `${shelfMetrics.getSequenceId()}`, + idType: "sequential", + }, shelf.title); + shelf.items = containerItems.items; + metricsHelpersLocation.popLocation(shelfMetrics.locationTracker); + shelfMetrics.addImpressionsToShelf(objectGraph, shelf, "bundleChildren"); + metricsHelpersLocation.nextPosition(shelfMetrics.locationTracker); + if (numberOfChildren < 2 || objectGraph.client.isTV) { + shelf.rowsPerColumn = 1; + } + else { + shelf.rowsPerColumn = 2; + } + return shelf; + }); +} +//# sourceMappingURL=bundle-children-shelf.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/bundle-parents-shelf.js b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/bundle-parents-shelf.js new file mode 100644 index 0000000..5ab9524 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/bundle-parents-shelf.js @@ -0,0 +1,46 @@ +import * as validation from "@jet/environment/json/validation"; +import * as models from "../../../api/models"; +import { isDefinedNonNullNonEmpty } from "../../../foundation/json-parsing/server-data"; +import * as productPageCommon from "../product-page-common"; +/** + * Create a shelf of bundle parents for the product page. + * + * @param productData The product data response for a product page JSON fetch. + * @param dataContainer The raw data response for a product page JSON fetch. + * @param shelfMetrics The product page shelf metrics. + * @returns A shelf of bundle parents. + */ +export function create(objectGraph, productData, dataContainer, shelfMetrics) { + if (!dataContainer) { + return null; + } + return validation.context("bundleParentsShelf", () => { + const numberOfParents = dataContainer.data.length; + if (!numberOfParents) { + return null; + } + const contentType = "smallLockup"; + const shelf = new models.Shelf(contentType); + shelf.isHorizontal = true; + shelf.title = objectGraph.loc.string("ProductPage.Section.IncludedInBundles.Title"); + const containerItems = productPageCommon.lockupsFromDataContainer(objectGraph, dataContainer, shelfMetrics, 1 /* content.ArtworkUseCase.LockupIconSmall */, contentType); + if (containerItems.remainingItems.length === 0 && containerItems.items.length === 0) { + return null; + } + shelf.items = containerItems.items; + if (isDefinedNonNullNonEmpty(containerItems.remainingItems)) { + const shelfToken = new productPageCommon.ProductPageShelfToken(productData.id, containerItems.remainingItems, shelf.title, false, undefined, contentType, "infer", null); + shelfToken.isBundleShelf = true; + shelf.url = productPageCommon.shelfContentUrl(objectGraph, shelfToken, shelfMetrics); + } + if (numberOfParents < 2 || objectGraph.client.isTV) { + shelf.rowsPerColumn = 1; + } + else { + shelf.rowsPerColumn = 2; + } + shelfMetrics.addImpressionsToShelf(objectGraph, shelf, "bundleParent"); + return shelf; + }); +} +//# sourceMappingURL=bundle-parents-shelf.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/capabilities-shelf.js b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/capabilities-shelf.js new file mode 100644 index 0000000..41a2f46 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/capabilities-shelf.js @@ -0,0 +1,41 @@ +import * as validation from "@jet/environment/json/validation"; +import * as models from "../../../api/models"; +import * as serverData from "../../../foundation/json-parsing/server-data"; +import * as productCapabilities from "../../content/product-capabilities"; +import * as contentAttributes from "../../content/attributes"; +import * as mediaPlatformAttributes from "../../../foundation/media/platform-attributes"; +/** + * Create a shelf of capabilities for the product page. + * + * @param data The raw data response for a product page JSON fetch. + * @param isFreeProduct Whether the offer for the given product is free. + * @param shelfMetrics The product page shelf metrics. + * @returns A shelf of capabilities. + */ +export function create(objectGraph, data, isFreeProduct, shelfMetrics) { + return validation.context("capabilitiesShelf", () => { + if (serverData.isNullOrEmpty(data)) { + return null; + } + const attributePlatform = contentAttributes.bestAttributePlatformFromData(objectGraph, data); + const bundleId = mediaPlatformAttributes.platformAttributeAsString(data, attributePlatform, "bundleId"); + if (serverData.isNullOrEmpty(bundleId)) { + return null; + } + const capabilities = productCapabilities.productCapabilitiesFromData(objectGraph, data, isFreeProduct); + if (serverData.isNullOrEmpty(capabilities)) { + return null; + } + const shelf = new models.Shelf("productCapability"); + shelf.title = objectGraph.loc.string("ProductPage.Section.Supports.Title"); + shelf.items = capabilities; + shelf.mergeWhenFetched = true; + shelf.batchGroup = "gameCenter"; + if (objectGraph.client.isVision && capabilities.length < 3) { + shelf.presentationHints = { isLowDensity: true }; + } + shelfMetrics.addImpressionsToShelf(objectGraph, shelf, "supports"); + return shelf; + }); +} +//# sourceMappingURL=capabilities-shelf.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/description-shelf.js b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/description-shelf.js new file mode 100644 index 0000000..90f9daa --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/description-shelf.js @@ -0,0 +1,144 @@ +import * as validation from "@jet/environment/json/validation"; +import * as models from "../../../api/models"; +import * as serverData from "../../../foundation/json-parsing/server-data"; +import { attributeAsString } from "@apple-media-services/media-api"; +import { hrefToRoutableUrl } from "../../builders/url-mapping"; +import { isNothing, isSome } from "@jet/environment"; +import * as mediaRelationship from "../../../foundation/media/relationships"; +import { pageRouter } from "../../builders/routing"; +import * as contentAttributes from "../../content/attributes"; +import * as content from "../../content/content"; +import * as productPageVariants from "../product-page-variants"; +import * as metricsHelpersClicks from "../../metrics/helpers/clicks"; +import * as metricsHelpersImpressions from "../../metrics/helpers/impressions"; +import * as metricsHelpersLocation from "../../metrics/helpers/location"; +/** + * Create a shelf for the product page description. + * + * @param data The raw data response for a product page JSON fetch. + * @param shelfMetrics The product page shelf metrics. + * @param shelfContext A collection of any other variables used when creating this shelf. + * @returns A description shelf. + */ +export function create(objectGraph, data, shelfMetrics, shelfContext) { + return validation.context("descriptionShelf", () => { + if (serverData.isNullOrEmpty(data)) { + return null; + } + const shelf = new models.Shelf("productDescription"); + const links = linksFromData(objectGraph, data); + const tagRibbonItems = createTagsRibbonItems(objectGraph, data, shelfMetrics.metricsPageInformation, shelfMetrics.locationTracker); + const paragraph = paragraphFromData(objectGraph, data); + if (paragraph === null) { + return null; + } + const productDescription = new models.ProductDescription(paragraph, links, tagRibbonItems, shelfContext.developerAction); + shelf.items = [productDescription]; + if (objectGraph.client.isWatch) { + shelf.title = objectGraph.loc.string("PRODUCT_DESCRIPTION"); + } + shelfMetrics.addImpressionsToShelf(objectGraph, shelf, "description"); + return shelf; + }); +} +export function createTagsRibbonItems(objectGraph, data, metricsPageInformation, metricsLocationTracker) { + const tagsData = mediaRelationship.relationshipViewsCollection(data, "categorizations"); + const ribbonItems = []; + for (const tagData of tagsData) { + const name = attributeAsString(tagData, "name"); + if (isSome(name)) { + let pageUrl = null; + switch (tagData.type) { + case "genres": + pageUrl = attributeAsString(tagData, "url"); + break; + case "tags": + const href = serverData.asString(tagData, "href"); + if (href) { + pageUrl = hrefToRoutableUrl(objectGraph, href); + } + break; + default: + break; + } + if (isNothing(pageUrl)) { + continue; + } + const flowPage = objectGraph.required(pageRouter).fetchFlowPage(pageUrl); + const flowAction = new models.FlowAction(flowPage); + flowAction.pageUrl = pageUrl; + flowAction.title = name; + const ribbonItem = new models.RibbonBarItem(name, flowAction); + // Configure impressions + const metricsOptions = { + targetType: "facet", + pageInformation: metricsPageInformation, + locationTracker: metricsLocationTracker, + recoMetricsData: null, + }; + const impressionOptions = metricsHelpersImpressions.impressionOptionsForTagRibbonItem(objectGraph, tagData, name, metricsOptions); + metricsHelpersImpressions.addImpressionFields(objectGraph, ribbonItem, impressionOptions); + const metricsClickOptions = metricsHelpersClicks.clickOptionsForLockup(objectGraph, data, metricsOptions); + metricsClickOptions.targetType = metricsOptions.targetType; + metricsHelpersClicks.addClickEventToAction(objectGraph, flowAction, metricsClickOptions); + ribbonItems.push(ribbonItem); + metricsHelpersLocation.nextPosition(metricsOptions.locationTracker); + } + } + return ribbonItems; +} +/** + * Create a description for the product. + * + * @param data The raw data response for a product page JSON fetch. + * @returns The raw description as a paragraph. + */ +export function paragraphFromData(objectGraph, data, productVariantData = null) { + return validation.context("descriptionFromData", () => { + if (serverData.isNull(productVariantData)) { + productVariantData = productPageVariants.productVariantDataForData(objectGraph, data); // create variant data if not provided by caller. + } + let promotionalText = content.promotionalTextFromData(objectGraph, data, productVariantData); + if (!promotionalText) { + promotionalText = ""; + } + let mainDescription = contentAttributes.contentAttributeAsString(objectGraph, data, "description.standard"); + if (!mainDescription) { + mainDescription = ""; + } + // Join the promotional text and app description, if both are present. + let separator = ""; + if (promotionalText.length > 0 && mainDescription.length > 0) { + separator = "\n\n"; + } + const descriptionString = promotionalText + separator + mainDescription; + if (descriptionString.length > 0) { + const paragraph = new models.Paragraph(descriptionString); + paragraph.isCollapsed = true; + return paragraph; + } + return null; + }); +} +function linksFromData(objectGraph, data) { + if (!data) { + return null; + } + return validation.context("productDescriptionLinksFromData", () => { + const links = []; + const developerWebsiteUrl = contentAttributes.contentAttributeAsString(objectGraph, data, "websiteUrl"); + if (developerWebsiteUrl) { + const action = new models.ExternalUrlAction(developerWebsiteUrl, false); + const text = objectGraph.loc.string("PRODUCT_DEVELOPER_WEBSITE"); + links.push(new models.ProductPageLink(text, action, "safari" /* models.ProductPageLinkImageName.developer */)); + } + const developerSupportUrl = contentAttributes.contentAttributeAsString(objectGraph, data, "supportURLForLanguage"); + if (developerSupportUrl) { + const action = new models.ExternalUrlAction(developerSupportUrl, false); + const text = objectGraph.loc.string("DEVELOPER_SUPPORT"); + links.push(new models.ProductPageLink(text, action, "questionmark.circle" /* models.ProductPageLinkImageName.support */)); + } + return links; + }); +} +//# sourceMappingURL=description-shelf.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/editorial-quote-shelf.js b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/editorial-quote-shelf.js new file mode 100644 index 0000000..0df96ed --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/editorial-quote-shelf.js @@ -0,0 +1,36 @@ +import * as models from "../../../api/models"; +import * as serverData from "../../../foundation/json-parsing/server-data"; +import * as color from "../../../foundation/util/color-util"; +import * as contentAttributes from "../../content/attributes"; +/** + * Create a shelf for the editorial quote on the product page. + * + * @param data The raw data response for a product page JSON fetch. + * @param shelfMetrics The product page shelf metrics. + * @returns An editorial quote shelf. + */ +export function create(objectGraph, data, shelfMetrics) { + if (serverData.isNullOrEmpty(data)) { + return null; + } + const editorialNotes = contentAttributes.contentAttributeAsDictionary(objectGraph, data, "editorialNotes"); + const standardNotes = serverData.asString(editorialNotes, "standard"); + if (standardNotes) { + const attribution = objectGraph.loc.string("APP_STORE_EDITORS_ATTRIBUTION"); + // Create the quote + const editorialQuote = new models.EditorialQuote(standardNotes, attribution); + // Create the shelf + const shelf = new models.Shelf("editorialQuote"); + shelf.items = [editorialQuote]; + shelfMetrics.addImpressionsToShelf(objectGraph, shelf, "editorialQuote"); + if (objectGraph.client.isiOS) { + shelf.background = { + type: "color", + color: color.named("componentBackgroundStandout"), + }; + } + return shelf; + } + return null; +} +//# sourceMappingURL=editorial-quote-shelf.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/editors-choice-shelf.js b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/editors-choice-shelf.js new file mode 100644 index 0000000..5c0e16b --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/editors-choice-shelf.js @@ -0,0 +1,63 @@ +import * as validation from "@jet/environment/json/validation"; +import { isNothing } from "@jet/environment/types/optional"; +import * as models from "../../../api/models"; +import * as serverData from "../../../foundation/json-parsing/server-data"; +import * as contentAttributes from "../../content/attributes"; +import { supportsVisionOSCompatibleIOSBinaryFromData } from "../../content/content"; +/** + * Create an `EditorsChoice` model from the data. + * + * @param objectGraph + * @param data + * @returns A model containing editors choice information. + */ +export function createEditorsChoiceModel(objectGraph, data) { + return validation.context("editorsChoiceItem", () => { + const supportsVisionOSCompatibleIOSBinary = supportsVisionOSCompatibleIOSBinaryFromData(objectGraph, data); + if (supportsVisionOSCompatibleIOSBinary) { + return null; + } + const editorialNotes = contentAttributes.contentAttributeAsDictionary(objectGraph, data, "editorialNotes"); + const editorsNotes = serverData.asString(editorialNotes, "standard"); + if (editorsNotes) { + const editorsChoice = new models.EditorsChoice(editorsNotes); + const editorialBadgeInfo = contentAttributes.contentAttributeAsDictionary(objectGraph, data, "editorialBadgeInfo"); + if (editorialBadgeInfo) { + const badgeType = serverData.asString(editorialBadgeInfo, "editorialBadgeType"); + editorsChoice.showsBadge = badgeType && badgeType === "editorialPriority"; + } + if (!editorsChoice.showsBadge) { + editorsChoice.title = objectGraph.loc.string("APP_STORE_EDITORS_NOTES"); + } + return editorsChoice; + } + return null; + }); +} +/** + * Create an `editorsChoice` shelf using the data provided. + * + * @param objectGraph + * @param data + * @returns A shelf for editors choice. + */ +export function createEditorsChoiceShelf(objectGraph, data) { + const model = createEditorsChoiceModel(objectGraph, data); + if (isNothing(model)) { + return null; + } + const shelf = new models.Shelf("editorsChoice"); + shelf.items = [model]; + if (model.showsBadge) { + // Appears as editors choice, with the "Editors' Choice" badge title and background asset + shelf.background = { + type: "editorsChoice", + }; + } + else { + // Appears as editors notes, which is less prominent than editors choice. + shelf.title = objectGraph.loc.string("ProductPage.Section.EditorsNotes.Title"); + } + return shelf; +} +//# sourceMappingURL=editors-choice-shelf.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/featured-in-shelf.js b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/featured-in-shelf.js new file mode 100644 index 0000000..b13fd67 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/featured-in-shelf.js @@ -0,0 +1,53 @@ +import * as validation from "@jet/environment/json/validation"; +import * as models from "../../../api/models"; +import * as serverData from "../../../foundation/json-parsing/server-data"; +import * as mediaDataStructure from "../../../foundation/media/data-structure"; +import * as metricsHelpersLocation from "../../metrics/helpers/location"; +import * as todayHorizontalCardUtil from "../../today/today-horizontal-card-util"; +import * as productPageCommon from "../product-page-common"; +/** + * Create a shelf for the `featured in` product page shelf. + * + * @param productData The product data response for a product page JSON fetch. + * @param dataContainer The raw data response for a product page JSON fetch. + * @param shelfMetrics The product page shelf metrics. + * @returns A shelf to display the featured in items. + */ +export function create(objectGraph, productData, dataContainer, shelfMetrics) { + function featuredInItemsFromData(innerObjectGraph, container, metricsOptions, contentUnavailable) { + return todayHorizontalCardUtil.featuredInTodayCardsFromData(innerObjectGraph, container.data, metricsOptions.pageInformation, metricsOptions.locationTracker, productPageCommon.includeTodayCardOnProductPage, contentUnavailable); + } + return validation.context("featuredInShelf", () => { + const data = mediaDataStructure.dataFromDataContainer(objectGraph, dataContainer); + if (serverData.isNullOrEmpty(data)) { + return null; + } + const metricsOptions = { + pageInformation: shelfMetrics.metricsPageInformation, + locationTracker: shelfMetrics.locationTracker, + targetType: "swoosh", + id: `${shelfMetrics.getSequenceId()}`, + idType: "sequential", + }; + const shelf = new models.Shelf("todayBrick"); + const title = objectGraph.loc.string("ProductPage.Section.FeaturedIn.Title"); + shelf.title = title; + shelf.isHorizontal = false; + const remainingItems = []; + const cardUnavailable = function (cardData) { + remainingItems.push(cardData); + return false; + }; + shelf.items = [featuredInItemsFromData(objectGraph, dataContainer, metricsOptions, cardUnavailable)]; + if (serverData.isDefinedNonNullNonEmpty(remainingItems)) { + const shelfToken = new productPageCommon.ProductPageShelfToken(productData.id, remainingItems, shelf.title, false, undefined, "todayBrick"); + shelf.url = productPageCommon.shelfContentUrl(objectGraph, shelfToken, shelfMetrics); + } + metricsHelpersLocation.pushContentLocation(objectGraph, metricsOptions, title); + shelfMetrics.addImpressionsToShelf(objectGraph, shelf, "featuredIn"); + metricsHelpersLocation.popLocation(shelfMetrics.locationTracker); + metricsHelpersLocation.nextPosition(shelfMetrics.locationTracker); + return shelf; + }); +} +//# sourceMappingURL=featured-in-shelf.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/friends-playing-shelf.js b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/friends-playing-shelf.js new file mode 100644 index 0000000..61c47a5 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/friends-playing-shelf.js @@ -0,0 +1,177 @@ +import * as validation from "@jet/environment/json/validation"; +import * as models from "../../../api/models"; +import * as serverData from "../../../foundation/json-parsing/server-data"; +import { Parameters, Path, Protocol } from "../../../foundation/network/url-constants"; +import * as contentAttributes from "../../content/attributes"; +import * as metricsHelpersClicks from "../../metrics/helpers/clicks"; +import * as metricsHelpersImpressions from "../../metrics/helpers/impressions"; +import * as metricsHelpersLocation from "../../metrics/helpers/location"; +import { platformPrefersLargeTitles } from "../../room/room-common"; +import * as productPageCommon from "../product-page-common"; +import { makeGameCenterHeader, openGamesUIAction } from "../../arcade/arcade-common"; +/** + * Create a shelf of Game Center friends who play this game. + * + * @param productData The raw data response for a product page JSON fetch. + * @param shelfMetrics The product page shelf metrics. + * @param pageInformation + * @returns A `Friends who played this game` shelf. + */ +export function create(objectGraph, productData, shelfMetrics, shelfContext) { + return validation.context("friendsPlayingShelf", () => { + if (serverData.isNullOrEmpty(productData)) { + return null; + } + const adamId = productData.id; + if (serverData.isNullOrEmpty(adamId)) { + return null; + } + if (objectGraph.client.isWeb) { + return null; + } + const isGameCenterEnabled = contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, productData, "isGameCenterEnabled"); + if (!isGameCenterEnabled) { + return null; + } + // rdar://70124826 (N104/18C23a: friends shown as playing a game when it\u2019s not available yet) + if (shelfContext.isPreorder) { + return null; + } + const title = objectGraph.loc.string("ProductPage.Section.FriendsPlaying.Title", "Playing This Game"); + const metricsOptions = { + pageInformation: shelfMetrics.metricsPageInformation, + locationTracker: shelfMetrics.locationTracker, + targetType: "swoosh", + id: `${shelfMetrics.getSequenceId()}`, + idType: "sequential", + }; + metricsHelpersLocation.pushContentLocation(objectGraph, metricsOptions, title); + const shelf = new models.Shelf("largeGameCenterPlayer"); + shelf.header = makeGameCenterHeader(objectGraph, title); + shelf.isHidden = true; + shelf.isHorizontal = true; + // This shelf is always empty initially; it is populated natively and then updated in JS using the function `createShelfWithFriends`. + shelf.items = []; + shelf.mergeWhenFetched = true; + const token = new productPageCommon.ProductPageShelfToken(productData.id, [], shelf.title, false, undefined, null, null, null); + shelf.url = `${Protocol.internal}:/${Path.product}/${Path.shelf}/?${Parameters.isGameCenterPlayerShelf}=true&${Parameters.id}=${adamId}&${Parameters.token}=${productPageCommon.encodedShelfToken(token, shelfMetrics, shelfMetrics.metricsPageInformation)}`; + shelf.batchGroup = "gameCenter"; + shelfMetrics.addImpressionsToShelf(objectGraph, shelf, "friendsPlaying"); + metricsHelpersLocation.popLocation(shelfMetrics.locationTracker); + metricsHelpersLocation.nextPosition(shelfMetrics.locationTracker); + const seeAllAction = new models.FlowAction("page"); + metricsHelpersClicks.addClickEventToSeeAllAction(objectGraph, seeAllAction, null, { + pageInformation: shelfMetrics.metricsPageInformation, + locationTracker: shelfMetrics.locationTracker, + }); + return shelf; + }); +} +export function createShelfWithFriends(objectGraph, gameCenterPlayers, overflowToSeeAll, token) { + const maxNumberOfPlayersBeforeSeeAll = objectGraph.client.isiOS ? 8 : 20; + const friendPrefix = "FRIEND"; + const shelf = new models.Shelf("largeGameCenterPlayer"); + shelf.isHorizontal = true; + let visiblePlayers = null; + if (overflowToSeeAll) { + visiblePlayers = gameCenterPlayers.slice(0, maxNumberOfPlayersBeforeSeeAll); + } + else { + visiblePlayers = gameCenterPlayers; + } + const shelfItems = []; + for (let index = 0; index < visiblePlayers.length; index++) { + const player = visiblePlayers[index]; + player.action = objectGraph.client.isTV + ? new models.GameCenterPlayerProfileAction(player.playerId) + : new models.OpenGamesUIAction({ player: { playerID: player.playerId } }); + if (serverData.isDefinedNonNullNonEmpty(token)) { + // Content metrics + const contentKind = "friendPlayer"; + const contentMetricsOptions = { + pageInformation: token.destinationPageInformation, + locationTracker: token.sourceLocationTracker, + id: "", + idType: "sequential", + anonymizationOptions: { + anonymizationString: `${friendPrefix}${index + 1}`, + }, + }; + // Impression metrics + const impressionMetricsOptions = { + ...contentMetricsOptions, + kind: contentKind, + title: "", + softwareType: null, + }; + metricsHelpersImpressions.addImpressionFields(objectGraph, player, impressionMetricsOptions); + if (objectGraph.bag.productPageFriendsPlayingClickEventsEnabled) { + // Click metrics + const clickOptions = { + ...contentMetricsOptions, + kind: contentKind, + targetType: "lockup", + actionType: "navigate", + softwareType: null, + }; + metricsHelpersClicks.addClickEventToAction(objectGraph, player.action, clickOptions); + } + } + shelfItems.push(player); + if (serverData.isDefinedNonNullNonEmpty(token)) { + metricsHelpersLocation.nextPosition(token.sourceLocationTracker); + } + } + shelf.items = shelfItems; + shelf.mergeWhenFetched = true; + shelf.isHidden = shelf.items.length === 0; + shelf.batchGroup = "gameCenter"; + if (overflowToSeeAll && gameCenterPlayers.length > maxNumberOfPlayersBeforeSeeAll) { + // The shelf for the see all page + const shelfForAllPlayers = new models.Shelf("smallGameCenterPlayer"); + shelfForAllPlayers.title = objectGraph.loc.stringWithCount("ProductPage.FriendsPlaying.SeeAllTitle", gameCenterPlayers.length); + shelfForAllPlayers.items = gameCenterPlayers.map((player) => { + player.action = new models.GameCenterPlayerProfileAction(player.playerId); + return player; + }); + shelfForAllPlayers.rowsPerColumn = 1; + // See all page + const seeAllPage = new models.GenericPage([shelfForAllPlayers]); + seeAllPage.title = token === null || token === void 0 ? void 0 : token.title; + if (platformPrefersLargeTitles(objectGraph)) { + seeAllPage.presentationOptions = ["prefersLargeTitle"]; + } + // The action that opens the see all page + const seeAllAction = new models.FlowAction("page"); + seeAllAction.title = objectGraph.loc.string("ACTION_SEE_ALL"); + seeAllAction.pageData = seeAllPage; + // Connect the shelf's seeAllAction + shelf.seeAllAction = seeAllAction; + } + if (serverData.isDefinedNonNullNonEmpty(token)) { + const shelfMetricsOptions = { + id: "", + kind: null, + softwareType: null, + targetType: "swoosh", + title: token.title, + pageInformation: token.sourcePageInformation, + locationTracker: token.sourceLocationTracker, + idType: "sequential", + badges: { + gameCenter: true, + }, + }; + metricsHelpersImpressions.addImpressionFields(objectGraph, shelf, shelfMetricsOptions); + } + shelf.footerTitle = objectGraph.loc.string("Lockup.Footer.GamesApp"); + shelf.footerAction = openGamesUIAction(objectGraph, { friends: {} }); + shelf.footerStyle = { + $kind: "games", + bundleID: "com.apple.games", + width: 16, + height: 16, + }; + return shelf; +} +//# sourceMappingURL=friends-playing-shelf.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/in-app-purchases-shelf.js b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/in-app-purchases-shelf.js new file mode 100644 index 0000000..a49c349 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/in-app-purchases-shelf.js @@ -0,0 +1,198 @@ +import * as models from "../../../api/models"; +import * as serverData from "../../../foundation/json-parsing/server-data"; +import { isNothing } from "@jet/environment"; +import * as mediaAttributes from "../../../foundation/media/attributes"; +import * as mediaRelationship from "../../../foundation/media/relationships"; +import { shallowCopyOf } from "../../../foundation/util/objects"; +import { tryAwait } from "../../../foundation/util/promise-util"; +import { withAsyncValidationContext } from "../../../foundation/util/validation-util"; +import * as appPromotionsCommon from "../../app-promotions/app-promotions-common"; +import * as contentAttributes from "../../content/attributes"; +import * as lockups from "../../lockups/lockups"; +import * as metricsHelpersLocation from "../../metrics/helpers/location"; +import * as productPageCommon from "../product-page-common"; +import { ProductPageSectionMapping } from "../shelf-based/product-page-section-mapping"; +/** + * Create a shelf for the in-app purchase items for this product page. + * + * @param data The raw data response for a product page JSON fetch. + * @param shelfMetrics The product page shelf metrics. + * @param shelfContext A collection of any other variables used when creating this shelf. + * @returns An in-app purchase shelf. + */ +export async function create(objectGraph, productData, shelfMetrics, shelfContext) { + return await withAsyncValidationContext("inAppPurchasesShelf", async () => { + var _a; + if (!shouldShowIAPShelf(objectGraph, objectGraph.host.platform)) { + return null; + } + if (serverData.isNullOrEmpty(productData)) { + return null; + } + let inAppsContainer; + if (appPromotionsCommon.appContingentItemsAreEnabled(objectGraph)) { + inAppsContainer = mediaRelationship.relationshipViewsContainer(productData, "standalone-merchandised-in-apps"); + } + else { + inAppsContainer = mediaRelationship.relationship(productData, "merchandised-in-apps"); + } + if (isNothing(inAppsContainer) || inAppsContainer.data.length === 0) { + return null; + } + // Consider only the In-App Purchase lockups that are appropriate for this type (i.e. subscription or not). + const merchandisedInApps = inAppsContainer.data.filter(function (inAppData) { + const isDataForSubscription = mediaAttributes.attributeAsBooleanOrFalse(inAppData, "isSubscription"); + return isDataForSubscription === shelfContext.isForSubscriptions; + }); + if (merchandisedInApps.length === 0) { + return null; + } + const metricsOptions = { + pageInformation: shelfMetrics.metricsPageInformation, + locationTracker: shelfMetrics.locationTracker, + targetType: "swoosh", + id: `${shelfMetrics.getSequenceId()}`, + idType: "sequential", + }; + const contentType = "inAppPurchaseLockup"; + const shelf = new models.Shelf(contentType); + // Disable iAP offer button for preorders + const isPreorder = mediaAttributes.attributeAsBoolean(productData, "isPreorder"); + const offerStyle = isPreorder ? "disabled" : "infer"; + const appBundleId = contentAttributes.contentAttributeAsString(objectGraph, productData, "bundleId"); + const spotlightId = (_a = shelfContext.options) === null || _a === void 0 ? void 0 : _a.spotlightInAppProductIdentifier; + injectParentIntoInAppsContainer(objectGraph, productData, inAppsContainer); + const remainingItems = []; + const contentUnavailable = function (index, dataItem) { + remainingItems.push(dataItem); + return false; + }; + const iapLockups = inAppPurchaseLockupsFromData(objectGraph, inAppsContainer.data, offerStyle, metricsOptions, spotlightId, contentUnavailable).filter(function (item) { + if (item.productIdentifier === spotlightId) { + shelfContext.options.spotlightSection = new ProductPageSectionMapping("shelf", shelfContext.isForSubscriptions ? "subscriptions" : "inAppPurchases"); + } + return item.isSubscription === shelfContext.isForSubscriptions; + }); + if (iapLockups.length === 0 && remainingItems.length === 0) { + return null; + } + shelf.items = await applyClientOrderingToIAPShelfItems(objectGraph, iapLockups, appBundleId, spotlightId); + if (serverData.isDefinedNonNullNonEmpty(remainingItems)) { + const shelfToken = new productPageCommon.ProductPageShelfToken(productData.id, remainingItems, shelf.title, false, undefined, contentType, offerStyle, null); + shelfToken.appBundleId = appBundleId; + shelfToken.iapShelfContext = shelfContext; + if (shelfContext.isForSubscriptions) { + shelfToken.inAppShelfId = "subscriptions"; + } + else { + shelfToken.inAppShelfId = "inAppPurchases"; + } + shelf.url = productPageCommon.shelfContentUrl(objectGraph, shelfToken, shelfMetrics); + } + let targetType; + if (shelfContext.isForSubscriptions) { + shelf.title = objectGraph.loc.string("ProductPage.Section.Subscriptions.Title", "Subscriptions"); + targetType = "subscription"; + } + else { + shelf.title = objectGraph.loc.string("ProductPage.Section.InAppPurchases.Title"); + targetType = "iAP"; + } + metricsHelpersLocation.pushContentLocation(objectGraph, metricsOptions, shelf.title); + metricsHelpersLocation.popLocation(shelfMetrics.locationTracker); + metricsHelpersLocation.nextPosition(shelfMetrics.locationTracker); + shelf.isHorizontal = true; + shelfMetrics.addImpressionsToShelf(objectGraph, shelf, targetType); + return shelf; + }); +} +/** + * In-App Purchases have a relationship back to the parent app. However, this specific relationship is not included + * when the In-App Purchase itself is a relationship on the parent app resource. Therefore, we need to do some work to + * inject the parent app into each IAP on the IAP relationship. This is the path of least resistance, as it will allow + * us to keep our core lockup builder lean and consistent across all call sites (after all, on everywhere else except + * the product page, the parent app will be included in the relationship). + * @param {Data} productData The data for the parent ap. + * @param {DataContainer} inAppsContainer The container for the In-App Purchases relationship. + */ +function injectParentIntoInAppsContainer(objectGraph, productData, inAppsContainer) { + const parentAppRelationshipType = "app"; + for (const data of inAppsContainer.data) { + let existingParentRelationship = mediaRelationship.relationship(data, parentAppRelationshipType); + if (!existingParentRelationship) { + existingParentRelationship = { + data: [productData], + }; + } + if (serverData.isNull(data.relationships)) { + data.relationships = {}; + } + data.relationships[parentAppRelationshipType] = existingParentRelationship; + } +} +function inAppPurchaseLockupsFromData(objectGraph, inAppsData, offerStyle, metricsOptions, spotlightId, contentUnavailable) { + const lockupOptions = { + metricsOptions: metricsOptions, + offerStyle: offerStyle, + skipDefaultClickAction: true, + artworkUseCase: 1 /* content.ArtworkUseCase.LockupIconSmall */, + }; + const options = { + lockupOptions: lockupOptions, + contentUnavailable: contentUnavailable, + }; + const lockupModels = lockups.lockupsFromData(objectGraph, inAppsData, options); + if (spotlightId) { + productPageCommon.moveLockupToFront(objectGraph, spotlightId, lockupModels); + } + return lockupModels; +} +/** + * Whether or not IAP shelf should be shown + */ +function shouldShowIAPShelf(objectGraph, platform) { + if (platform === "macOS" || objectGraph.client.isCompanionVisionApp) { + return false; + } + return true; +} +async function applyClientOrderingToIAPShelfItems(objectGraph, currentItems, appBundleId, spotlightInAppProductIdentifier) { + if (isNothing(appBundleId)) { + return currentItems; + } + const allInAppIds = []; + const defaultVisibleInAppIds = []; + for (const inAppLockup of currentItems) { + const productIdentifier = inAppLockup.productIdentifier; + // We must ensure this item is appropriate for the shelf type. + allInAppIds.push(productIdentifier); + if (inAppLockup.isVisibleByDefault) { + defaultVisibleInAppIds.push(productIdentifier); + } + } + // FIXME: <rdar://99419136> ESLint rule disabled temporarily but code should be fixed. If `orderedVisibleIAPs()` is rejected, nothing will happen. + // eslint-disable-next-line @typescript-eslint/no-floating-promises + const finalOrderingResult = await tryAwait(objectGraph.clientOrdering.orderedVisibleIAPs(appBundleId, allInAppIds, defaultVisibleInAppIds, spotlightInAppProductIdentifier)); + if (!finalOrderingResult.success) { + return currentItems; + } + return filteredInAppLockupsUsingOrdering(currentItems, finalOrderingResult.value); +} +function filteredInAppLockupsUsingOrdering(iAPLockups, iAPOrdering) { + // Use a map to avoid nested iteration. + const identifiersToLockups = {}; + for (const lockup of iAPLockups) { + identifiersToLockups[lockup.productIdentifier] = lockup; + } + // Filter by those present in the ordering. + const filteredOrderedLockups = []; + for (const productIdentifier of iAPOrdering) { + const foundLockup = identifiersToLockups[productIdentifier]; + if (foundLockup) { + const copy = shallowCopyOf(foundLockup); + filteredOrderedLockups.push(copy); + } + } + return filteredOrderedLockups; +} +//# sourceMappingURL=in-app-purchases-shelf.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/links-shelf.js b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/links-shelf.js new file mode 100644 index 0000000..0b2d907 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/links-shelf.js @@ -0,0 +1,269 @@ +import * as validation from "@jet/environment/json/validation"; +import * as models from "../../../api/models"; +import * as serverData from "../../../foundation/json-parsing/server-data"; +import * as mediaAttributes from "../../../foundation/media/attributes"; +import { Path, Protocol } from "../../../foundation/network/url-constants"; +import * as contentAttributes from "../../content/attributes"; +import * as sad from "../../content/sad"; +import * as productPageUtil from "../product-page-util"; +import * as informationShelf from "./annotations/annotations"; +import * as metricsHelpersClicks from "../../metrics/helpers/clicks"; +import { isSome } from "@jet/environment/types/optional"; +import { makeEulaPageIntent } from "../../../api/intents/eula-page-intent"; +/** + * Create a links shelf for the product page. + * + * @param data The raw data response for a product page JSON fetch. + * @param shelfMetrics The product page shelf metrics. + * @param isPurchased Whether the links are for a user who has purchased the product. + * @param isFreeProduct Whether the offer is for a free product. + * @param isArcadeProduct Whether the product is an Arcade product. + * @returns A links shelf. + */ +export function create(objectGraph, data, shelfMetrics, isPurchased, isFreeProduct, isArcadeProduct) { + return validation.context("create", () => { + if (serverData.isNullOrEmpty(data)) { + return null; + } + if (objectGraph.client.isTV) { + return tvLinksShelf(objectGraph, data, isFreeProduct, shelfMetrics); + } + return standardLinksShelf(objectGraph, data, shelfMetrics, isPurchased, isArcadeProduct); + }); +} +/** + * Determines the URL to use for the EULA page. + * @param {Data} data The data for the product. + * @returns {string} The string form of the URL for the EULA page, or `null` if the data is undefined or + * null. + */ +function eulaUrlFromData(objectGraph, data) { + const resourceId = data.id; + const resourceType = data.type; + if (!(serverData.isDefinedNonNull(data) && + serverData.isDefinedNonNull(resourceId) && + serverData.isDefinedNonNull(resourceType))) { + return null; + } + return `${Protocol.internal}:/${Path.eula}?resourceType=${resourceType}&resourceId=${resourceId}`; +} +export function privacyPolicyUrlFromData(objectGraph, data) { + const resourceId = data.id; + if (!(serverData.isDefinedNonNull(data) && serverData.isDefinedNonNull(resourceId))) { + return null; + } + return `${Protocol.internal}:/${Path.privacyPolicy}?resourceId=${resourceId}`; +} +function tvEulaUrlFromData(objectGraph, data) { + const resourceId = data.id; + const resourceType = data.type; + if (!(serverData.isDefinedNonNull(data) && + serverData.isDefinedNonNull(resourceId) && + serverData.isDefinedNonNull(resourceType))) { + return null; + } + return `${Protocol.internal}:/${Path.tvEula}?resourceType=${resourceType}&resourceId=${resourceId}`; +} +function safetyComplianceUrlForContent(objectGraph, pageContent) { + if (serverData.isNullOrEmpty(pageContent)) { + return null; + } + return `${Protocol.internal}:/${Path.safetyCompliance}?pageContent=${encodeURIComponent(pageContent)}`; +} +function reportAProblemUrlFromData(objectGraph, data) { + const resourceId = data.id; + const reportAProblemURL = objectGraph.bag.productPageReportProblemURL; + if (!(serverData.isDefinedNonNull(resourceId) && serverData.isDefinedNonNull(reportAProblemURL))) { + return null; + } + return reportAProblemURL.replace("{productId}", resourceId); +} +/** + * The links shelf for TV deviceTypes. + * @param data The product data to use + * @param isFreeProduct Whether the offer is for a free product. + * @param shelfMetrics The current product page shelf metrics + * @returns {models.Shelf} The shelf of links. + */ +function tvLinksShelf(objectGraph, data, isFreeProduct, shelfMetrics) { + return validation.context("tvLinksShelf", () => { + const shelf = new models.Shelf("titledButtonStack"); + const links = []; + const moreInformationShelf = informationShelf.create(objectGraph, data, false, shelfMetrics, null, false, isFreeProduct, true); + moreInformationShelf.presentationHints = { isSeeAllContext: true }; + const moreInformationPage = new models.GenericPage([moreInformationShelf]); + moreInformationPage.title = mediaAttributes.attributeAsString(data, "name"); + if (objectGraph.client.isTV) { + moreInformationPage.presentationOptions.push("prefersIndirectTouch"); + } + const moreInformationAction = new models.FlowAction("page"); + moreInformationAction.presentationContext = "presentModalBlur"; + moreInformationAction.pageData = moreInformationPage; + const moreInformationButton = new models.TitledButton(objectGraph.loc.string("MORE_INFORMATION"), moreInformationAction); + links.push(moreInformationButton); + const hasPrivacyPolicy = contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "hasPrivacyPolicyText"); + if (hasPrivacyPolicy) { + const url = privacyPolicyUrlFromData(objectGraph, data); + if (url) { + const text = objectGraph.loc.string("PRIVACY_POLICY"); + const action = new models.FlowAction("unknown"); + action.pageUrl = url; + const button = new models.TitledButton(text, action); + links.push(button); + } + } + const hasLicenseAgreement = mediaAttributes.attributeAsBoolean(data, "hasEula"); + if (hasLicenseAgreement) { + const url = tvEulaUrlFromData(objectGraph, data); + if (url) { + const text = objectGraph.loc.string("LICENSE_AGREEMENT"); + const action = new models.FlowAction("unknown"); + action.pageUrl = url; + const button = new models.TitledButton(text, action); + links.push(button); + } + } + const traderData = contentAttributes.contentAttributeAsDictionary(objectGraph, data, "sellerInfo"); + const safetyAndComplianceURL = serverData.asString(traderData, "safetyAndComplianceUrl"); + if ((safetyAndComplianceURL === null || safetyAndComplianceURL === void 0 ? void 0 : safetyAndComplianceURL.length) > 0) { + const url = safetyComplianceUrlForContent(objectGraph, safetyAndComplianceURL); + if (isSome(url)) { + const text = objectGraph.loc.string("ProductPage.Section.SafetyAndCompliance.Title"); + const action = new models.FlowAction("unknown"); + action.pageUrl = url; + const button = new models.TitledButton(text, action); + links.push(button); + } + } + const stack = new models.TitledButtonStack(links); + // On compact width, we want to place a line break after each button. + stack.compactLineBreaks = stack.buttons.map((value, index) => index); + shelf.items = [stack]; + if (productPageUtil.isShelfBased(objectGraph)) { + shelf.background = { + type: "darkOverlay", + }; + } + shelfMetrics.addImpressionsToShelf(objectGraph, shelf, "links"); + return shelf; + }); +} +/** + * The links shelf for all deviceTypes except TV. + * @param data The product data to use + * @param shelfMetrics The current product page shelf metrics + * @param isPurchased Whether the links are to be displayed on a purchased product page + * @param isArcadeProduct Whether the product is an Arcade product + * @returns {models.Shelf} The shelf of links. + */ +function standardLinksShelf(objectGraph, data, shelfMetrics, isPurchased, isArcadeProduct) { + return validation.context("standardLinksShelf", () => { + const shelf = new models.Shelf("productPageLink"); + const { metricsPageInformation, locationTracker } = shelfMetrics; + const links = []; + const isWeb = objectGraph.client.isWeb; + if (objectGraph.client.deviceType !== "mac") { + const developerWebsiteUrl = contentAttributes.contentAttributeAsString(objectGraph, data, "websiteUrl"); + if (developerWebsiteUrl) { + const action = new models.ExternalUrlAction(developerWebsiteUrl, false); + const text = objectGraph.loc.string("DEVELOPER_WEBSITE"); + if (isWeb) { + metricsHelpersClicks.addClickEventToAction(objectGraph, action, { + id: "DeveloperWebsite", + actionDetails: { + type: "developer", + }, + actionType: "navigate", + locationTracker, + pageInformation: metricsPageInformation, + }, false, "link"); + } + links.push(new models.ProductPageLink(text, action, "safari" /* models.ProductPageLinkImageName.developer */)); + } + } + const privacyPolicyUrl = contentAttributes.contentAttributeAsString(objectGraph, data, "privacyPolicyUrl"); + if (privacyPolicyUrl) { + const action = new models.ExternalUrlAction(privacyPolicyUrl, false); + if (isWeb) { + metricsHelpersClicks.addClickEventToAction(objectGraph, action, { + id: "LinkToPrivacyPolicy", + actionType: "navigate", + locationTracker, + pageInformation: metricsPageInformation, + }, false, "link"); + } + const text = objectGraph.loc.string("PRIVACY_POLICY"); + links.push(new models.ProductPageLink(text, action, "hand.raised.fill" /* models.ProductPageLinkImageName.privacy */)); + } + const hasLicenseAgreement = mediaAttributes.attributeAsBoolean(data, "hasEula"); + if (hasLicenseAgreement) { + const url = eulaUrlFromData(objectGraph, data); + if (url) { + const flowPage = isWeb ? "licenseAgreement" : "page"; + const action = new models.FlowAction(flowPage); + action.pageUrl = url; + if (isWeb) { + const resourceId = data.id; + const resourceType = data.type; + // web presents this data in a modal + action.presentationContext = "presentModal"; + action.destination = makeEulaPageIntent({ + resourceId, + resourceType: resourceType, + }); + metricsHelpersClicks.addClickEventToAction(objectGraph, action, { + id: "LicenseAgreement", + actionType: "navigate", + locationTracker, + pageInformation: metricsPageInformation, + }, false, "link"); + } + const text = objectGraph.loc.string("LICENSE_AGREEMENT"); + links.push(new models.ProductPageLink(text, action, "doc.plaintext" /* models.ProductPageLinkImageName.eula */)); + } + } + const traderData = contentAttributes.contentAttributeAsDictionary(objectGraph, data, "sellerInfo"); + const safetyAndComplianceURL = serverData.asString(traderData, "safetyAndComplianceUrl"); + if ((safetyAndComplianceURL === null || safetyAndComplianceURL === void 0 ? void 0 : safetyAndComplianceURL.length) > 0) { + const action = new models.ExternalUrlAction(safetyAndComplianceURL, false); + const text = objectGraph.loc.string("ProductPage.Section.SafetyAndCompliance.Title"); + links.push(new models.ProductPageLink(text, action, "checkmark.seal" /* models.ProductPageLinkImageName.safetyAndCompliance */)); + } + // We want to show the report-a-problem link for apps that are purchased and only SAD apps that are specified by the bag. + // Because isPurchased contains both SAD and non SAD apps, we need a few additional checks. + const clientSupportsReportAProblem = objectGraph.client.isMac || objectGraph.client.isiOS || objectGraph.client.isVision; + const reportProblemSADSubscriptionArray = objectGraph.bag.productPageReportProblemSADSubscriptionArray; + const adamID = data.id; + const isDeletableSystemAppWithSub = reportProblemSADSubscriptionArray.includes(adamID); + const isSystemApp = sad.systemApps(objectGraph).isSystemAppFromData(data); + // Check if the adamID is a bundle id. + const isBundle = data.type === "app-bundles"; + // We only want purchased apps that are not system apps + const isPurchasedAppNotSAD = isPurchased && !isSystemApp; + // We do not support report a problem for second party apps + const isSecondPartyApp = objectGraph.bag.productPageReportProblemSecondPartyAppArray.includes(adamID); + const shouldShowRAPLinkInShelf = !isBundle && + !isSecondPartyApp && + !isArcadeProduct && + clientSupportsReportAProblem && + objectGraph.bag.reportProblemEnabled && + (isDeletableSystemAppWithSub || isPurchasedAppNotSAD); + if (shouldShowRAPLinkInShelf) { + const reportAProblemUrl = reportAProblemUrlFromData(objectGraph, data); + if ((reportAProblemUrl === null || reportAProblemUrl === void 0 ? void 0 : reportAProblemUrl.length) > 0) { + const action = new models.ExternalUrlAction(reportAProblemUrl, false); + const text = objectGraph.loc.string("REPORT_A_PROBLEM"); + // We never want to filter out SAD apps based on purchase history + const adamIdForPurchaseHistoryFilter = isDeletableSystemAppWithSub ? null : adamID; + const link = new models.ProductPageLink(text, action, "exclamationmark.triangle" /* models.ProductPageLinkImageName.reportAProblem */, adamIdForPurchaseHistoryFilter); + links.push(link); + } + } + if (links && links.length > 0) { + shelf.items = links; + } + shelfMetrics.addImpressionsToShelf(objectGraph, shelf, "links"); + return shelf; + }); +} +//# sourceMappingURL=links-shelf.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/more-by-developer-shelf.js b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/more-by-developer-shelf.js new file mode 100644 index 0000000..2788fb6 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/more-by-developer-shelf.js @@ -0,0 +1,192 @@ +import { isSome } from "@jet/environment"; +import * as validation from "@jet/environment/json/validation"; +import * as models from "../../../api/models"; +import * as serverData from "../../../foundation/json-parsing/server-data"; +import * as mediaAttributes from "../../../foundation/media/attributes"; +import * as mediaRelationship from "../../../foundation/media/relationships"; +import * as content from "../../content/content"; +import * as metricsHelpersClicks from "../../metrics/helpers/clicks"; +import * as metricsHelpersLocation from "../../metrics/helpers/location"; +import { insertPlaceholdersIntoProductPageShelfIfRequired } from "../../placeholders/placeholders"; +import * as productPageCommon from "../product-page-common"; +import { ProductPageOnDemandShelfType } from "../product-page-common"; +import { ProductPageShelfMetrics } from "../product-page-shelf-metrics"; +import { getLocale } from "../../locale"; +import { makeDeveloperPageIntent } from "../../../api/intents/developer-page-intent"; +/** + * Create a shelf of developers other apps for the product page. + * + * @param productData The raw data response for a product page JSON fetch. + * @param shelfMetrics The product page shelf metrics. + * @returns A `More by developer` shelf. + */ +export function createInitialShelf(objectGraph, productData, shelfMetrics) { + return validation.context("moreByDeveloperShelf", () => { + if (serverData.isNullOrEmpty(productData)) { + return null; + } + const developer = mediaRelationship.relationshipData(objectGraph, productData, "developer"); + const developerUrl = content.developerUrlFromDeveloperData(objectGraph, developer); + const developerName = mediaAttributes.attributeAsString(productData, "artistName"); + let otherAppsRelationship; + let otherApps; + if (preprocessor.GAMES_TARGET) { + otherApps = mediaRelationship.relationshipViewsCollection(productData, "developer-other-games"); + if (otherApps.length === 0) { + return null; + } + } + else { + otherAppsRelationship = mediaRelationship.relationship(productData, "developer-other-apps"); + if (!otherAppsRelationship || otherAppsRelationship.data.length === 0) { + return null; + } + otherApps = otherAppsRelationship.data; + } + const title = objectGraph.loc + .string("ProductPage.Section.MoreByDeveloper.TitleTemplate") + .replace("{developer}", developerName); + metricsHelpersLocation.pushContentLocation(objectGraph, { + pageInformation: shelfMetrics.metricsPageInformation, + locationTracker: shelfMetrics.locationTracker, + targetType: "swoosh", + id: `${shelfMetrics.getSequenceId()}`, + idType: "sequential", + }, title); + let contentType; + switch (objectGraph.client.deviceType) { + case "mac": + case "tv": + contentType = "mediumLockup"; + break; + default: + contentType = "smallLockup"; + break; + } + const shelf = new models.Shelf(contentType); + shelf.title = title; + const hasShelfBackground = !objectGraph.client.isTV && !objectGraph.client.isiOS; + const offerStyle = hasShelfBackground ? "white" : null; + const filter = objectGraph.client.isCompanionVisionApp ? 32768 /* Filter.VisionOSCompatibility */ : undefined; + let shelfItems; + if (isSome(otherAppsRelationship)) { + shelfItems = productPageCommon.lockupsFromDataContainer(objectGraph, otherAppsRelationship, shelfMetrics, content.artworkUseCaseFromShelfStyle(objectGraph, contentType), contentType, offerStyle, filter); + } + else if (isSome(otherApps)) { + shelfItems = productPageCommon.lockupsFromData(objectGraph, otherApps, shelfMetrics, content.artworkUseCaseFromShelfStyle(objectGraph, contentType), contentType, offerStyle, filter); + } + if (!shelfItems) { + return null; + } + shelf.items = shelfItems.items; + shelf.isHorizontal = true; + if (otherApps.length < 2 || objectGraph.client.isTV) { + shelf.rowsPerColumn = 1; + } + else { + shelf.rowsPerColumn = 2; + } + if (!objectGraph.client.isTV) { + const developerSeeAll = new models.FlowAction("page"); + developerSeeAll.pageUrl = developerUrl; + developerSeeAll.title = objectGraph.loc.string("ACTION_SEE_ALL"); + metricsHelpersClicks.addClickEventToSeeAllAction(objectGraph, developerSeeAll, developerSeeAll.pageUrl, { + pageInformation: shelfMetrics.metricsPageInformation, + locationTracker: shelfMetrics.locationTracker, + }); + if (objectGraph.client.isWeb) { + developerSeeAll.destination = makeDeveloperPageIntent({ + ...getLocale(objectGraph), + id: developer.id, + }); + } + shelf.seeAllAction = developerSeeAll; + } + if (hasShelfBackground) { + shelf.background = { + type: "color", + color: productPageCommon.grayShelfBackgroundColor, + }; + } + if (objectGraph.client.isiOS && objectGraph.bag.isOnDemandShelfFetchingEnabled) { + shelf.fetchStrategy = models.IncompleteShelfFetchStrategy.OnShelfWillAppear; + } + if (serverData.isDefinedNonNullNonEmpty(shelfItems.remainingItems)) { + metricsHelpersLocation.popLocation(shelfMetrics.locationTracker); + shelfMetrics.addImpressionsToShelf(objectGraph, shelf, "developer"); + metricsHelpersLocation.nextPosition(shelfMetrics.locationTracker); + const shelfToken = new productPageCommon.ProductPageShelfToken(productData.id, shelfItems.remainingItems, shelf.title, false, undefined, contentType, offerStyle, null, null, null, null, ProductPageOnDemandShelfType.MoreByDeveloper); + shelfToken.developerUrl = developerUrl; + insertPlaceholdersIntoProductPageShelfIfRequired(objectGraph, shelf, shelfToken); + shelf.url = productPageCommon.shelfContentUrl(objectGraph, shelfToken, shelfMetrics); + } + else { + metricsHelpersLocation.popLocation(shelfMetrics.locationTracker); + shelfMetrics.addImpressionsToShelf(objectGraph, shelf, "developer"); + metricsHelpersLocation.nextPosition(shelfMetrics.locationTracker); + } + return shelf; + }); +} +/** + * Create a shelf of more by developer items for the secondary look up. + * + * @param dataItems The raw data items from the secondary fetch. + * @param shelfToken The shelf token for this shelf. + * @returns A more by developer items shelf. + */ +export function createSecondaryShelf(objectGraph, dataItems, shelfToken) { + return validation.context("moreByDeveloperSecondaryShelf", () => { + if (serverData.isNullOrEmpty(dataItems)) { + return null; + } + const shelfMetrics = new ProductPageShelfMetrics(shelfToken.sourcePageInformation, shelfToken.sourceLocationTracker, shelfToken.sourceSequenceId); + const metricsOptions = { + pageInformation: shelfMetrics.metricsPageInformation, + locationTracker: shelfMetrics.locationTracker, + targetType: "swoosh", + id: `${shelfMetrics.getSequenceId()}`, + idType: "sequential", + excludeAttribution: true, + recoMetricsData: shelfToken.recoMetricsData, + }; + metricsHelpersLocation.pushContentLocation(objectGraph, metricsOptions, shelfToken.title); + const shelf = new models.Shelf(shelfToken.contentType); + shelf.title = shelfToken.title; + const hasShelfBackground = !objectGraph.client.isTV && !objectGraph.client.isiOS; + const filter = objectGraph.client.isCompanionVisionApp ? 32768 /* Filter.VisionOSCompatibility */ : undefined; + const shelfItems = productPageCommon.lockupsFromData(objectGraph, dataItems, shelfMetrics, content.artworkUseCaseFromShelfStyle(objectGraph, shelfToken.contentType), shelfToken.contentType, shelfToken.offerStyle, filter); + if (serverData.isNullOrEmpty(shelfItems)) { + return null; + } + shelf.items = shelfItems.items; + shelf.isHorizontal = true; + if (dataItems.length < 2 || objectGraph.client.isTV) { + shelf.rowsPerColumn = 1; + } + else { + shelf.rowsPerColumn = 2; + } + if (!objectGraph.client.isTV && isSome(shelfToken.developerUrl)) { + const developerSeeAll = new models.FlowAction("page"); + developerSeeAll.pageUrl = shelfToken.developerUrl; + developerSeeAll.title = objectGraph.loc.string("ACTION_SEE_ALL"); + metricsHelpersClicks.addClickEventToSeeAllAction(objectGraph, developerSeeAll, developerSeeAll.pageUrl, { + pageInformation: shelfMetrics.metricsPageInformation, + locationTracker: shelfMetrics.locationTracker, + }); + shelf.seeAllAction = developerSeeAll; + } + if (hasShelfBackground) { + shelf.background = { + type: "color", + color: productPageCommon.grayShelfBackgroundColor, + }; + } + shelfMetrics.addImpressionsToShelf(objectGraph, shelf, "developer"); + metricsHelpersLocation.popLocation(shelfMetrics.locationTracker); + metricsHelpersLocation.nextPosition(shelfMetrics.locationTracker); + return shelf; + }); +} +//# sourceMappingURL=more-by-developer-shelf.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/product-media-shelf.js b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/product-media-shelf.js new file mode 100644 index 0000000..3cc927c --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/product-media-shelf.js @@ -0,0 +1,244 @@ +import * as validation from "@jet/environment/json/validation"; +import * as models from "../../../api/models"; +import * as videoDefaults from "../../../common/constants/video-constants"; +import * as metricsHelpersMedia from "../../../common/metrics/helpers/media"; +import * as serverData from "../../../foundation/json-parsing/server-data"; +import { shallowCopyOf } from "../../../foundation/util/objects"; +import * as content from "../../content/content"; +import { addMetricsEventsToPageWithInformation, fakeMetricsPageInformation } from "../../metrics/helpers/page"; +import * as productPageVariants from "../product-page-variants"; +import { ProductPageSectionMapping } from "../shelf-based/product-page-section-mapping"; +import * as videoShelf from "./video-shelf"; +import { isSome } from "@jet/environment"; +const productMediaShelfPrefix = "product_media"; +export function isProductMediaShelf(shelf) { + var _a; + return isSome(shelf) && ((_a = shelf.id) === null || _a === void 0 ? void 0 : _a.indexOf(productMediaShelfPrefix)) > -1; +} +export function create(objectGraph, data, clientIdentifierOverride, shelfMetrics) { + return validation.context("productMediaShelves", () => { + if (serverData.isNullOrEmpty(data)) { + return null; + } + // <rdar://problem/50189280> REG: tvOS: Prevent tvOS Product Pages from creating media shelves for other platforms. + // For certain platforms, we only show app media for that platform only. + const includedAppPlatformsForProductMedia = includedAppPlatformsForProductMediaOnDevice(objectGraph, objectGraph.client.deviceType); + // rdar://59908940 (CrashTracer: AppStore at AppStore: specialized static AnyComponentView.shelfLayout) + // On TV / Watch, restrict the video previews to the client platform only. + const includedAppPlatformsForPlatformVideoPreviews = includedAppPlatformsForPlatformVideoPreviewsOnDevice(objectGraph, objectGraph.client.deviceType); + // Find all media + const productVariantData = productPageVariants.productVariantDataForData(objectGraph, data); + const allProductMedia = content.productMediaFromData(objectGraph, data, 11 /* content.ArtworkUseCase.ProductPageScreenshots */, includedAppPlatformsForProductMedia, productVariantData, clientIdentifierOverride); + const platformVideoPreviews = content.platformVideoPreviewFromData(objectGraph, data, videoDefaults.defaultVideoConfiguration(objectGraph), includedAppPlatformsForPlatformVideoPreviews); + // Find the first platform that has a video. + let screenshotMediaWithCorrespondingVideo = null; + if (platformVideoPreviews) { + for (const screenshotMedia of allProductMedia) { + if (screenshotMedia.mediaPlatform.isEqualTo(platformVideoPreviews.mediaPlatform)) { + screenshotMediaWithCorrespondingVideo = screenshotMedia; + break; + } + } + } + // For phone screenshots or on visionOS, we show the video(s) inline with the screenshots if-and-only-if the aspect ratios are + // the same. Therefore, we store the prevailing aspect ratio of the screenshots that has a video. + let overridingAspectRatio = null; + if (screenshotMediaWithCorrespondingVideo) { + const targetAppPlatform = screenshotMediaWithCorrespondingVideo.mediaPlatform.appPlatform; + if (objectGraph.client.isVision || targetAppPlatform === "phone" || targetAppPlatform === "messages") { + for (const item of screenshotMediaWithCorrespondingVideo.items) { + const screenshot = item.screenshot; + overridingAspectRatio = screenshot.width / screenshot.height; + if (overridingAspectRatio < 1) { + break; + } + } + } + } + // Determine which videos are eligible to be collapsed with screenshots. + const collapsedVideoItems = []; + const separatedVideos = []; + if (screenshotMediaWithCorrespondingVideo && platformVideoPreviews && platformVideoPreviews.videos) { + for (const video of platformVideoPreviews.videos) { + const videoItem = new models.ProductMediaItem(); + videoItem.video = video; + const videoAspectRatio = video.preview.width / video.preview.height; + const isMatchingAspectRatio = overridingAspectRatio === null || Math.abs(videoAspectRatio - overridingAspectRatio) < 0.01; + const isMatchingAppPlatform = screenshotMediaWithCorrespondingVideo.mediaPlatform.isEqualTo(platformVideoPreviews.mediaPlatform); + if (isMatchingAspectRatio && isMatchingAppPlatform) { + collapsedVideoItems.push(videoItem); + } + else { + separatedVideos.push(video); + } + const metricsOptions = { + pageInformation: shelfMetrics.metricsPageInformation, + locationTracker: shelfMetrics.locationTracker, + id: data.id, + isAdvert: shelfMetrics.metricsPageInformation.iAdInfo + ? shelfMetrics.metricsPageInformation.iAdInfo.iAdIsPresent + : false, + }; + metricsHelpersMedia.addMetricsEventsToVideo(objectGraph, video, metricsOptions); + } + } + // Collapse the eligible videos into the first media row. + if (screenshotMediaWithCorrespondingVideo) { + screenshotMediaWithCorrespondingVideo.items = collapsedVideoItems.concat(screenshotMediaWithCorrespondingVideo.items); + } + let sectionMappings = []; + let shelfMapping = {}; + for (let index = 0; index < allProductMedia.length; index++) { + const productMedia = allProductMedia[index]; + const shelfId = `${productMediaShelfPrefix}_${productMedia.mediaPlatform.appPlatform}_${productMedia.mediaPlatform.supplementaryAppPlatforms.join("_")}`; + sectionMappings.push(new ProductPageSectionMapping("shelf", shelfId)); + const mediaShelf = new models.Shelf("productMediaItem", null, productMedia.items); + mediaShelf.isHorizontal = true; + let hasPortraitMedia = false; + let mediaAspectRatio = aspectRatioForMediaItem(objectGraph, productMedia.items[0]); + for (const mediaItem of productMedia.items) { + if ((serverData.isDefinedNonNullNonEmpty(mediaItem.screenshot) && mediaItem.screenshot.isPortrait()) || + (serverData.isDefinedNonNullNonEmpty(mediaItem.video) && mediaItem.video.preview.isPortrait())) { + mediaAspectRatio = aspectRatioForMediaItem(objectGraph, mediaItem); + hasPortraitMedia = true; + break; + } + } + mediaShelf.contentsMetadata = { + type: "productMedia", + platform: productMedia.mediaPlatform, + allPlatforms: productMedia.allPlatforms, + platformDescription: productMedia.platformDescription, + allPlatformsDescription: productMedia.allPlatformsDescription, + allPlatformsDescriptionPlacement: productMedia.allPlatformsDescriptionPlacement, + hasPortraitMedia: hasPortraitMedia, + expandProductMediaAction: null, + viewProductMediaGalleryAction: createViewProductMediaGalleryAction(objectGraph, productMedia, data.id), + aspectRatio: mediaAspectRatio, + }; + shelfMetrics.addImpressionsToShelf(objectGraph, mediaShelf, "screenshots"); + if (index === 0 && objectGraph.client.deviceType !== "tv" && !objectGraph.client.isWeb) { + mediaShelf.title = objectGraph.loc.string("ProductPage.Section.ScreenshotsPreview.Title"); + } + shelfMapping[shelfId] = mediaShelf; + } + if (objectGraph.client.isVision && sectionMappings.length > 1) { + const firstSectionMapping = sectionMappings[0]; + const mediaShelf = shelfMapping[firstSectionMapping.shelfId]; + if (mediaShelf.contentsMetadata.type === "productMedia") { + mediaShelf.contentsMetadata.expandProductMediaAction = createExpandProductMediaAction(objectGraph, sectionMappings, shelfMapping); + } + // Now that we've created the expand action, we can strip out the other product media shelves, + // as we will only be displaying the first in the non-expanded state. + sectionMappings = [firstSectionMapping]; + const firstShelf = shelfMapping[firstSectionMapping.shelfId]; + shelfMapping = { + [firstSectionMapping.shelfId]: firstShelf, + }; + } + // Videos that stay in the Videos Shelf + if (separatedVideos.length > 0) { + shelfMapping["videos"] = videoShelf.create(objectGraph, separatedVideos, shelfMetrics); + } + return { + sectionMappings: sectionMappings, + shelfMapping: shelfMapping, + allProductMedia: allProductMedia, + }; + }); +} +/** + * Returns the list of included app platforms for the product media shelves. + * This is used to constrain some platforms to media type of just that platform, e.g. tvOS can only show tvOS screenshots and video. + * + * @param deviceType Device type to find the included app platforms for product media for. + * @returns models.AppPlatform[] | null An array of supported app platforms for product media, or `null` if there are no constraints and should fallback to sorting logic in content.ts + */ +export function includedAppPlatformsForProductMediaOnDevice(objectGraph, deviceType) { + // We want to return all product media platforms, so that we can generate + // the appropriate platform selector description text. + return null; +} +/** + * Returns the list of included app platforms for the platform video previews + * This is used to constrain some platforms to video previews of just that platform, e.g. tvOS can only show tvOS video previews. + * + * @param deviceType Device type to determine to app platforms we allow video previews for. + * @returns models.AppPlatform[] | null An array of supported app platforms for video previews, or `null` if there are no constraints + */ +function includedAppPlatformsForPlatformVideoPreviewsOnDevice(objectGraph, deviceType) { + if (objectGraph.client.isTV || objectGraph.client.isWatch) { + return [content.currentAppPlatform(objectGraph)]; + } + return null; +} +/** + * Creates a flow action for navigating to the expanded product media page. + * + * @param objectGraph + * @param sectionMappings The product media section mappings array + * @param shelfMapping The product media shelf mapping + * @returns A flow action + */ +function createExpandProductMediaAction(objectGraph, sectionMappings, shelfMapping) { + if (!objectGraph.client.isVision || sectionMappings.length <= 1) { + return null; + } + const expandedShelves = []; + for (const sectionMapping of sectionMappings) { + const shelf = shelfMapping[sectionMapping.shelfId]; + const expandedShelf = shallowCopyOf(shelf); + expandedShelf.title = null; + expandedShelf.contentsMetadata = shallowCopyOf(shelf.contentsMetadata); + if (expandedShelf.contentsMetadata.type === "productMedia") { + expandedShelf.contentsMetadata.expandProductMediaAction = null; + } + expandedShelves.push(expandedShelf); + } + const page = new models.GenericPage(expandedShelves); + page.title = objectGraph.loc.string("PRODUCT_PREVIEWS_TITLE"); + const flowAction = new models.FlowAction("page"); + flowAction.pageData = page; + flowAction.title = page.title; + return flowAction; +} +/** + * Creates a flow action for navigating to the product media gallery page. + * + * @param objectGraph Current object graph + * @param productMedia The product media object that contains the media items to display + * @param productId The id of the owning product + * @returns A flow action + */ +function createViewProductMediaGalleryAction(objectGraph, productMedia, productId) { + if (!objectGraph.client.isVision && !objectGraph.featureFlags.isEnabled("tv_product_page_2024E")) { + return null; + } + const page = new models.ProductMediaGalleryPage(productMedia); + const pageInformation = fakeMetricsPageInformation(objectGraph, "ProductMediaGallery", productId, ""); + addMetricsEventsToPageWithInformation(objectGraph, page, pageInformation); + const flowAction = new models.FlowAction("productMediaGallery"); + flowAction.pageData = page; + return flowAction; +} +/** + * Determines the aspect ratio for a given ProductMediaItem + * + * @param objectGraph Current object graph + * @param mediaItem The media item + * @returns An aspect ratio determined by width / height + */ +function aspectRatioForMediaItem(objectGraph, mediaItem) { + var _a; + if (serverData.isNull(mediaItem)) { + return 1.0; + } + if (serverData.isDefinedNonNull((_a = mediaItem.video) === null || _a === void 0 ? void 0 : _a.preview) && mediaItem.video.preview.height > 0) { + return mediaItem.video.preview.width / mediaItem.video.preview.height; + } + if (serverData.isDefinedNonNull(mediaItem.screenshot) && mediaItem.screenshot.height > 0) { + return mediaItem.screenshot.width / mediaItem.screenshot.height; + } + return 1.0; +} +//# sourceMappingURL=product-media-shelf.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/ribbon-shelf.js b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/ribbon-shelf.js new file mode 100644 index 0000000..d3064a5 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/ribbon-shelf.js @@ -0,0 +1,40 @@ +import * as models from "../../../api/models"; +import * as productBadges from "../badges/badges"; +import * as productPageUtil from "../product-page-util"; +/** + * Create a shelf for the product page ribbon to display badges. + * + * @param data The raw data response for a product page JSON fetch. + * @param shelfMetrics The product page shelf metrics. + * @returns A ribbon shelf. + */ +export function create(objectGraph, data, shelfMetrics, shelfContext) { + if (productPageUtil.isShelfBased(objectGraph)) { + const badgeShelf = new models.Shelf("productBadge"); + const badges = productBadges.badgesFromResponse(objectGraph, data, true, { + locationTracker: shelfMetrics.locationTracker, + pageInformation: shelfMetrics.metricsPageInformation, + }); + badgeShelf.items = badges; + badgeShelf.isHorizontal = true; + shelfMetrics.addImpressionsToShelf(objectGraph, badgeShelf, "informationRibbon"); + return badgeShelf; + } + else { + const ribbonShelf = new models.Shelf("informationRibbon"); + const badges = productBadges.badgesFromResponse(objectGraph, data, true, { + locationTracker: shelfMetrics.locationTracker, + pageInformation: shelfMetrics.metricsPageInformation, + }); + const hasTopSeparator = !shelfContext.useInlineUberStyle; + const hasBottomSeparator = false; + const separatorsAreFullWidth = shelfContext.useInlineUberStyle; + const alignment = "justified"; + const informationRibbon = new models.InformationRibbon(badges, hasTopSeparator, hasBottomSeparator, separatorsAreFullWidth, alignment); + ribbonShelf.items = [informationRibbon]; + ribbonShelf.isHorizontal = true; + shelfMetrics.addImpressionsToShelf(objectGraph, ribbonShelf, "informationRibbon"); + return ribbonShelf; + } +} +//# sourceMappingURL=ribbon-shelf.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/shelf-based-reviews-shelves.js b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/shelf-based-reviews-shelves.js new file mode 100644 index 0000000..5f66054 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/shelf-based-reviews-shelves.js @@ -0,0 +1,544 @@ +import * as validation from "@jet/environment/json/validation"; +import * as models from "../../../api/models"; +import * as serverData from "../../../foundation/json-parsing/server-data"; +import * as mediaAttributes from "../../../foundation/media/attributes"; +import * as mediaDataStructure from "../../../foundation/media/data-structure"; +import * as mediaRelationship from "../../../foundation/media/relationships"; +import { shallowCopyOf } from "../../../foundation/util/objects"; +import { createArtworkForResource } from "../../content/artwork/artwork"; +import * as contentAttributes from "../../content/attributes"; +import * as content from "../../content/content"; +import * as reviews from "../reviews"; +import { isNothing, isSome } from "@jet/environment"; +/** + * Create a shelf for the product page reviews. + * + * @param objectGraph + * @param productData + * @param shelfMetrics The product page shelf metrics. + * @param shelfContext A collection of any other variables used when creating this shelf. + * @returns A reviews shelf. + */ +export function createReviewsShelves(objectGraph, productData, shelfMetrics, shelfContext, lockupSubtitle) { + return validation.context("createReviewsShelves", () => { + const productReviewsShelves = {}; + if (objectGraph.client.isWatch) { + return productReviewsShelves; + } + if (serverData.isNullOrEmpty(productData)) { + return productReviewsShelves; + } + const isReviewable = reviews.isAppReviewable(objectGraph, productData); + const appPlatforms = content.supportedAppPlatformsFromData(objectGraph, productData); + const reviewsData = mediaRelationship.relationship(productData, "reviews"); + const reviewItems = mediaDataStructure.dataCollectionFromDataContainer(reviewsData); + const ratingsData = mediaAttributes.attributeAsDictionary(productData, "userRating"); + ratingsData.ratingAverage = serverData.asNumber(ratingsData, "value"); + ratingsData.adamId = productData.id; + ratingsData.isBundle = shelfContext.isBundle; + ratingsData.supportUrl = contentAttributes.contentAttributeAsString(objectGraph, productData, "supportURLForLanguage"); + const appIcon = content.iconFromData(objectGraph, productData, { + useCase: 2 /* content.ArtworkUseCase.LockupIconMedium */, + }); + const tvOnlyApp = appPlatforms.length === 1 && appPlatforms[0] === "tv"; + const reviewsTitle = reviews.sectionTitleForPlatform(objectGraph); + const productRatingsWereReset = serverData.asBooleanOrFalse(ratingsData, "wasReset"); + const productRatings = createProductRatings(objectGraph, ratingsData, reviewItems.length, productRatingsWereReset); + const tapToRateProductReviewAction = createTapToRateProductReviewAction(objectGraph, productData, shelfContext.productTitle, ratingsData, shelfContext.isBundle, tvOnlyApp); + const writeAReview = createWriteAReview(objectGraph, productData, ratingsData, tvOnlyApp, appIcon, lockupSubtitle, shelfContext.productTitle); + const editorsChoiceReview = reviews.createEditorsChoiceReview(objectGraph, productData); + const reviewSummary = reviews.createReviewSummaryProductReview(objectGraph, productData, shelfMetrics); + const reviewSummaryForReviewsData = reviews.reviewSummaryFromData(objectGraph, productData, shelfMetrics); + const allProductReviews = reviews.createProductReviewsList(objectGraph, ratingsData, reviewItems, true, editorsChoiceReview, reviewSummary, shelfMetrics); + const userProductReviews = reviews.createProductReviewsList(objectGraph, ratingsData, reviewItems, true, null, null, shelfMetrics); + const productReviewActions = createProductReviewActions(objectGraph, productData, shelfContext.productTitle, ratingsData, shelfContext.isBundle, tvOnlyApp, appIcon, lockupSubtitle); + const includeMoreAction = objectGraph.client.isVision; + const includeFeedbackActions = objectGraph.client.isVision; + const seeAllAction = tvOnlyApp || reviewItems.length === 0 || (objectGraph.client.isVision && reviewsData.next == null) + ? null + : reviews.reviewsPageActionFromReviewsData(objectGraph, objectGraph.client.guid, ratingsData, reviewItems, reviewsData.next, shelfContext.productTitle, appIcon, includeMoreAction, includeFeedbackActions, productData, shelfMetrics); + if (shelfContext.shouldShowRatingsAndReviews) { + const ratingsShelf = new models.Shelf("productRatings"); + ratingsShelf.title = reviewsTitle; + ratingsShelf.items = [productRatings]; + productReviewsShelves.ratingsShelf = ratingsShelf; + shelfMetrics.addImpressionsToShelf(objectGraph, ratingsShelf, "ratingsOverview", null, null, null, objectGraph.loc.string("ProductPage.Section.Reviews.Title")); + const allReviewActionsShelfItems = []; + if (serverData.isDefinedNonNullNonEmpty(tapToRateProductReviewAction) && isReviewable) { + allReviewActionsShelfItems.push(tapToRateProductReviewAction); + } + if (serverData.isDefinedNonNullNonEmpty(writeAReview) && isReviewable) { + allReviewActionsShelfItems.push(writeAReview); + } + if (serverData.isDefinedNonNullNonEmpty(allReviewActionsShelfItems)) { + const allReviewActionsShelf = new models.Shelf("productReviewAction"); + allReviewActionsShelf.items = allReviewActionsShelfItems; + productReviewsShelves.allReviewActionsShelf = allReviewActionsShelf; + } + if (serverData.isDefinedNonNullNonEmpty(reviewSummaryForReviewsData)) { + const reviewSummaryShelf = new models.Shelf("reviewSummary"); + reviewSummaryShelf.items = [reviewSummaryForReviewsData]; + productReviewsShelves.reviewSummaryShelf = reviewSummaryShelf; + } + let tapToRateProductReviewActionsShelf = null; + if (serverData.isDefinedNonNullNonEmpty(tapToRateProductReviewAction) && isReviewable) { + tapToRateProductReviewActionsShelf = new models.Shelf("productReviewAction"); + tapToRateProductReviewActionsShelf.items = [tapToRateProductReviewAction]; + productReviewsShelves.tapToRateActionsShelf = tapToRateProductReviewActionsShelf; + } + let writeAReviewActionsShelf = null; + if (serverData.isDefinedNonNullNonEmpty(writeAReview) && isReviewable) { + writeAReviewActionsShelf = new models.Shelf("productReviewAction"); + writeAReviewActionsShelf.items = [writeAReview]; + productReviewsShelves.writeAReviewActionsShelf = writeAReviewActionsShelf; + } + const userReviewsTitle = objectGraph.loc.string("ProductPage.Section.Reviews.MostHelpfulReviews.Title"); + const reviewSummaryTitle = objectGraph.loc.string("ProductPage.ReviewSummary.Body.Title"); + const hasUserReviews = isSome(userProductReviews) && userProductReviews.length > 0; + let allProductReviewsShelf; + if (isSome(allProductReviews) && allProductReviews.length > 0) { + allProductReviewsShelf = new models.Shelf("productReview"); + allProductReviewsShelf.items = allProductReviews; + allProductReviewsShelf.isHorizontal = true; + if (objectGraph.client.isiOS) { + if (hasUserReviews) { + allProductReviewsShelf.title = userReviewsTitle; + allProductReviewsShelf.contentsMetadata = { + type: "productReviewsSection", + hasReviewSummary: isSome(reviewSummary), + reviewSummaryTitle: reviewSummaryTitle, + }; + shelfMetrics.addImpressionsToShelf(objectGraph, allProductReviewsShelf, "mostHelpfulReviews", null, null, null, objectGraph.loc.string("ProductPage.Section.Reviews.MostHelpfulReviews.Title")); + } + else { + allProductReviewsShelf.title = reviewSummaryTitle; + } + } + productReviewsShelves.allProductReviewsShelf = allProductReviewsShelf; + } + let userProductReviewsShelf; + if (hasUserReviews) { + userProductReviewsShelf = new models.Shelf("productReview"); + userProductReviewsShelf.items = userProductReviews; + userProductReviewsShelf.isHorizontal = true; + if (objectGraph.client.isiOS) { + userProductReviewsShelf.title = userReviewsTitle; + } + shelfMetrics.addImpressionsToShelf(objectGraph, userProductReviewsShelf, "mostHelpfulReviews", null, null, null, objectGraph.loc.string("ProductPage.Section.Reviews.MostHelpfulReviews.Title")); + productReviewsShelves.userProductReviewsShelf = userProductReviewsShelf; + } + if (objectGraph.client.deviceType === "tv") { + const shelf = new models.Shelf("productRatingsAndReviewsComponent"); + shelf.title = reviewsTitle; + const shelfItems = []; + if (serverData.isDefinedNonNullNonEmpty(tapToRateProductReviewAction) && + tapToRateProductReviewAction.action instanceof models.TapToRate && + isReviewable) { + shelfItems.push(tapToRateProductReviewAction.action); + } + if (serverData.isDefinedNonNullNonEmpty(productRatings.status)) { + if (objectGraph.featureFlags.isEnabled("tv_product_page_2024E")) { + shelfItems.push(new models.ProductRatingsAndReviewsMessage(productRatings.status)); + } + else { + shelfItems.push(reviews.noRatingsFromRatings(productRatings)); + } + } + else { + shelfItems.push(reviews.starRatingsFromRatings(productRatings)); + shelfItems.push(reviews.starRatingsHistogramFromRatings(productRatings)); + } + shelf.items = shelfItems; + productReviewsShelves.purchasedRatingsAndReviewsComponentShelf = shelf; + } + if (objectGraph.client.deviceType === "tv") { + const shelf = new models.Shelf("productRatingsAndReviewsComponent"); + shelf.title = reviewsTitle; + const shelfItems = []; + if (serverData.isDefinedNonNullNonEmpty(productRatings.status)) { + if (objectGraph.featureFlags.isEnabled("tv_product_page_2024E")) { + shelfItems.push(new models.ProductRatingsAndReviewsMessage(productRatings.status)); + } + else { + shelfItems.push(reviews.noRatingsFromRatings(productRatings)); + } + } + else { + shelfItems.push(reviews.starRatingsFromRatings(productRatings)); + shelfItems.push(reviews.starRatingsHistogramFromRatings(productRatings)); + } + shelf.items = shelfItems; + productReviewsShelves.notPurchasedRatingsAndReviewsComponentShelf = shelf; + } + if (objectGraph.client.isVision) { + const productReviewsHeader = createProductReviewsLearnMoreShelf(objectGraph); + if (isSome(productReviewsHeader)) { + productReviewsHeader.title = reviewsTitle; + productReviewsHeader.seeAllAction = seeAllAction; + productReviewsShelves.productReviewsHeader = productReviewsHeader; + } + // If there is a learn more shelf, the title and see all action is applied to the learn more shelf, + // not the actual ratings and reviews shelf, due to the learn more text being placed directly + // underneath the shelf title. + const shelfTitle = isSome(productReviewsHeader) ? null : reviewsTitle; + const shelfSeeAllAction = isSome(productReviewsHeader) ? null : seeAllAction; + const purchasedShelf = createVisionRatingsAndReviewsShelf(objectGraph, true, shelfTitle, productRatings, productRatingsWereReset, productReviewActions, allProductReviews, shelfSeeAllAction); + const notPurchasedShelf = createVisionRatingsAndReviewsShelf(objectGraph, false, shelfTitle, productRatings, productRatingsWereReset, productReviewActions, allProductReviews, seeAllAction); + productReviewsShelves.purchasedRatingsAndReviewsComponentShelf = purchasedShelf; + productReviewsShelves.notPurchasedRatingsAndReviewsComponentShelf = notPurchasedShelf; + } + let editorsChoiceProductReviewsShelf = null; + if (serverData.isDefinedNonNullNonEmpty(editorsChoiceReview)) { + editorsChoiceProductReviewsShelf = new models.Shelf("productReview"); + editorsChoiceProductReviewsShelf.items = [editorsChoiceReview]; + if (!shelfContext.shouldShowRatingsAndReviews) { + editorsChoiceProductReviewsShelf.title = reviewsTitle; + } + else if (objectGraph.client.isiOS && + !editorsChoiceReview.review.showsBadge) { + editorsChoiceProductReviewsShelf.title = objectGraph.loc.string("ProductPage.Section.EditorsNotes.Title"); + } + productReviewsShelves.editorsChoiceProductReviewsShelf = editorsChoiceProductReviewsShelf; + } + // Web does not currently support a "See All" page for Bundles + const isBundleOnWeb = shelfContext.isBundle && objectGraph.client.isWeb; + if (!tvOnlyApp && !isBundleOnWeb && reviewItems.length > 0) { + productReviewsShelves.ratingsShelf.seeAllAction = objectGraph.client.isiOS + ? createReviewsPageSeeAllAction(objectGraph, objectGraph.client.guid, productData.id, ratingsData, reviewItems, reviewsData.next, includeMoreAction, includeFeedbackActions, productReviewsShelves) + : reviews.reviewsPageActionFromReviewsData(objectGraph, objectGraph.client.guid, ratingsData, reviewItems, reviewsData.next, shelfContext.productTitle, appIcon, includeMoreAction, includeFeedbackActions, productData, shelfMetrics); + } + } + return productReviewsShelves; + }); +} +function createProductRatings(objectGraph, ratingsData, reviewsCount, wereReset) { + return validation.context("createProductRatings", () => { + const ratings = reviews.ratingsFromApiResponses(objectGraph, objectGraph.client.guid, "productPage", ratingsData); + const hasRatings = ratings.ratingAverage > 0 && ratings.ratingCounts; + if (!hasRatings && !wereReset) { + ratings.status = objectGraph.loc.string("RATINGS_STATUS_NOT_ENOUGH_RATINGS_OR_REVIEWS"); + } + return ratings; + }); +} +function createTapToRateProductReviewAction(objectGraph, productData, productTitle, ratingsData, isBundle, tvOnlyApp) { + return validation.context("createTapToRateProductReviewAction", () => { + const tapToRate = createTapToRate(objectGraph, productData, productTitle, ratingsData, isBundle, tvOnlyApp); + if (tapToRate) { + const productReviewAction = new models.ProductReviewAction(); + productReviewAction.actionType = "tapToRate"; + productReviewAction.action = tapToRate; + productReviewAction.id = "ProductReviewAction.TapToRate"; + return productReviewAction; + } + else { + return null; + } + }); +} +/** + * Creates the TapToRate object, for tapping on a star to rate the app + * @param objectGraph Current object graph + * @param productData The data blob for the product + * @param productTitle The title of the product + * @param ratingsData The data blobs for the ratings + * @param isBundle Whether this product is for a bundle + * @param tvOnlyApp Whether this product is for a TV only app + * @returns The built TapToRate object + */ +function createTapToRate(objectGraph, productData, productTitle, ratingsData, isBundle, tvOnlyApp) { + return validation.context("createTapToRateProductReviewAction", () => { + // Tap to Rate + if (!tvOnlyApp || objectGraph.client.isTV) { + return reviews.tapToRateWithAdamId(objectGraph, productData.id, isBundle, productTitle, null); + } + else { + return null; + } + }); +} +/** + * Creates an action for writing a product review + * @param objectGraph Current object graph + * @param productData The data blob for the product + * @param ratingsData The data blobs for the ratings + * @param tvOnlyApp Whether this product is for a TV only app + * @param appIcon The icon artwork for the product + * @returns A product review action + */ +function createWriteAReview(objectGraph, productData, ratingsData, tvOnlyApp, appIcon, lockupSubtitle, lockupTitle) { + return validation.context("createWriteAReview", () => { + // Tap to Rate + if (!tvOnlyApp) { + const productWriteAReview = new models.ProductWriteAReview(); + productWriteAReview.writeReviewAction = reviews.writeReviewActionFromData(objectGraph, productData, lockupSubtitle, lockupTitle, appIcon); + productWriteAReview.supportAction = createSupportAction(objectGraph, ratingsData); + const productReviewAction = new models.ProductReviewAction(); + productReviewAction.actionType = "writeAReview"; + productReviewAction.action = productWriteAReview; + return productReviewAction; + } + else { + return null; + } + }); +} +/** + * Creates an action for contacting support + * @param objectGraph Current object graph + * @param ratingsData The data blobs for the ratings + * @returns An external URL action + */ +function createSupportAction(objectGraph, ratingsData) { + const supportUrl = serverData.asString(ratingsData, "supportUrl"); + if (supportUrl) { + const supportAction = new models.ExternalUrlAction(supportUrl, false); + supportAction.title = objectGraph.loc.string("APP_SUPPORT"); + supportAction.artwork = createArtworkForResource(objectGraph, "systemimage://questionmark.circle"); + return supportAction; + } + else { + return null; + } +} +/** + * Creates a ProductReviewActions object, which includes tap to rate, write a review, and support + * @param objectGraph Current object graph + * @param productData The data blob for the product + * @param productTitle The title of the product + * @param ratingsData The data blobs for the ratings + * @param isBundle Whether this product is for a bundle + * @param tvOnlyApp Whether this product is for a TV only app + * @param appIcon The icon artwork for the product + * @returns The built ReviewActions object + */ +export function createProductReviewActions(objectGraph, productData, productTitle, ratingsData, isBundle, tvOnlyApp, appIcon, lockupSubtitle) { + return validation.context("createProductReviewActions", () => { + const productReviewActions = new models.ProductReviewActions(); + productReviewActions.tapToRate = createTapToRate(objectGraph, productData, productTitle, ratingsData, isBundle, tvOnlyApp); + productReviewActions.writeReviewAction = reviews.writeReviewActionFromData(objectGraph, productData, lockupSubtitle, productTitle, appIcon); + productReviewActions.supportAction = createSupportAction(objectGraph, ratingsData); + return productReviewActions; + }); +} +/** + * Creates a ratings and reviews shelf for visionOS, which is composed of a + * number of different ProductRatingsAndReviewsComponent items + * @param objectGraph Current object graph + * @param isPurchasedShelf Whether this shelf is for display for a purchased app + * @param shelfTitle The title to use for the shelf + * @param productRatings The ratings for the product + * @param productRatingsWereReset Whether the product ratings were reset by the developer + * @param reviewActions A review actions object + * @param productReviews An array of product reviews + * @param seeAllAction The see all action for the shelf + * @returns The built shelf + */ +function createVisionRatingsAndReviewsShelf(objectGraph, isPurchasedShelf, shelfTitle, productRatings, productRatingsWereReset, productReviewActions, productReviews, seeAllAction) { + const shelf = new models.Shelf("productRatingsAndReviewsComponent"); + shelf.title = shelfTitle; + let shelfItems = []; + const hasRatings = productRatings.ratingAverage > 0 && productRatings.ratingCounts; + const hasReviews = productReviews.length > 0; + if (hasRatings) { + shelfItems.push(reviews.starRatingsFromRatings(productRatings)); + } + else if (productRatingsWereReset) { + const messageText = objectGraph.loc.string("RATINGS_STATUS_DEVELOPER_RESET"); + shelfItems.push(new models.ProductRatingsAndReviewsMessage(messageText)); + } + else if (hasReviews) { + const messageText = objectGraph.loc.string("RATINGS_STATUS_NOT_ENOUGH_RATINGS"); + shelfItems.push(new models.ProductRatingsAndReviewsMessage(messageText)); + } + if (isPurchasedShelf && serverData.isDefinedNonNullNonEmpty(productReviewActions)) { + shelfItems.push(productReviewActions); + } + if (hasReviews) { + shelfItems = shelfItems.concat(productReviews); + } + else if (!hasRatings && !productRatingsWereReset) { + const messageText = objectGraph.loc.string("RATINGS_STATUS_NOT_ENOUGH_RATINGS_OR_REVIEWS"); + shelfItems.push(new models.ProductRatingsAndReviewsMessage(messageText)); + } + else { + const messageText = objectGraph.loc.string("RATINGS_STATUS_NO_REVIEWS"); + shelfItems.push(new models.ProductRatingsAndReviewsMessage(messageText)); + } + shelf.items = shelfItems; + shelf.isHorizontal = true; + shelf.seeAllAction = seeAllAction; + if (shelfItems.filter((shelfItem) => shelfItem instanceof models.ProductRatingsAndReviewsMessage).length >= 1) { + if (shelfItems.length === 1) { + shelf.presentationHints = { isSingleDensity: true }; + } + else if (shelfItems.length === 2) { + shelf.presentationHints = { isLowDensity: true }; + } + } + return shelf; +} +/** + * Creates a flow action to display a reviews page. + * @param deviceId The UUID for the user's device. + * @param adamId The adamid of the app + * @param ratingsData The ratings data response + * @param reviewsData The review row data response. + * @param nextPageHref The Media API href for the next page. + * @param appName The name of the app + * @param appIcon The app's icon. + * @param productData The data for the product + + * @returns A `FlowAction` object pointing to a reviews page. + */ +function createReviewsPageSeeAllAction(objectGraph, deviceId, adamId, ratingsData, reviewItems, nextPageHref = null, includeMoreAction, includeFeedbackActions, reviewsShelves) { + if (!ratingsData) { + return null; + } + return validation.context("reviewsPageActionFromReviewsData", () => { + var _a; + const pageData = new models.ReviewsPage(); + let reviewActionsShelf; + if (isSome(reviewsShelves === null || reviewsShelves === void 0 ? void 0 : reviewsShelves.allReviewActionsShelf)) { + reviewActionsShelf = shallowCopyOf(reviewsShelves.allReviewActionsShelf); + const existingReviewActionsShelfPresentationHints = (_a = reviewActionsShelf.presentationHints) !== null && _a !== void 0 ? _a : {}; + reviewActionsShelf.presentationHints = { + ...existingReviewActionsShelfPresentationHints, + displayIfReviewable: true, + }; + addSeeAllContextPresentationHint(reviewActionsShelf); + } + let ratingsShelf; + if (isSome(reviewsShelves === null || reviewsShelves === void 0 ? void 0 : reviewsShelves.ratingsShelf)) { + ratingsShelf = shallowCopyOf(reviewsShelves.ratingsShelf); + const ratings = ratingsShelf.items[0]; + if (isSome(ratings) && ratingsShelf.items.length === 1) { + const ratingsCopy = shallowCopyOf(ratings); + ratingsCopy.context = "details"; + ratingsShelf.items = [ratingsCopy]; + } + ratingsShelf.seeAllAction = null; + ratingsShelf.title = null; + addSeeAllContextPresentationHint(ratingsShelf); + } + let reviewSummaryShelf; + if (isSome(reviewsShelves === null || reviewsShelves === void 0 ? void 0 : reviewsShelves.reviewSummaryShelf)) { + reviewSummaryShelf = shallowCopyOf(reviewsShelves.reviewSummaryShelf); + addSeeAllContextPresentationHint(reviewSummaryShelf); + } + const pageComponents = reviews.createReviewsPageComponents(objectGraph, deviceId, adamId, ratingsData, reviewItems, nextPageHref, includeMoreAction, includeFeedbackActions, "helpful", { + ratingsShelf: ratingsShelf, + reviewSummaryShelf: reviewSummaryShelf, + reviewActionsShelf: reviewActionsShelf, + }); + pageData.title = reviews.pageTitleForPlatform(objectGraph); + pageData.adamId = serverData.asString(ratingsData, "adamId"); + pageData.nextPage = pageComponents.paginationToken; + pageData.initialSortOptionIdentifier = pageComponents.initialSortId; + pageData.sortActionSheetTitle = pageComponents.sortActionSheetTitle; + pageData.sortOptions = pageComponents.sorts; + if (isSome(ratingsShelf)) { + pageData.shelves.push(ratingsShelf); + } + if (isSome(reviewSummaryShelf)) { + pageData.shelves.push(reviewSummaryShelf); + } + if (isSome(reviewActionsShelf)) { + pageData.shelves.push(reviewActionsShelf); + } + pageData.shelves.push(pageComponents.reviewsShelf); + const reviewSummaryLearnMoreAction = reviews.reviewSummaryLearnMoreAction(objectGraph); + if (isSome(reviewSummaryLearnMoreAction)) { + pageData.trailingNavBarAction = reviewSummaryLearnMoreAction; + } + const action = new models.FlowAction("reviews"); + action.pageData = pageData; + action.title = objectGraph.loc.string("ACTION_SEE_ALL"); + return action; + }); +} +/** + * Sets `isSeeAllContext` to `true` in the presentation hints of the provided shelf. + * @param shelf The shelf to add the presentation hint to + */ +function addSeeAllContextPresentationHint(shelf) { + var _a; + if (isSome(shelf)) { + const existingPresentationHints = (_a = shelf.presentationHints) !== null && _a !== void 0 ? _a : {}; + shelf.presentationHints = { + ...existingPresentationHints, + isSeeAllContext: true, + }; + } +} +/** + * Creates a "Learn More" shelf for product reviews, which includes transparency info about the feature. + * This can be placed in different locations on a page based on the platform. + * @param objectGraph Current object graph + * @returns The built shelf + */ +export function createProductReviewsLearnMoreShelf(objectGraph) { + if (serverData.isNullOrEmpty(objectGraph.bag.ratingsAndReviewsLearnMoreEditorialId)) { + return null; + } + if (!objectGraph.client.isiOS && !objectGraph.client.isVision) { + return null; + } + const shelf = new models.Shelf("linkableText"); + const bodyText = bodyTextForProductReviewsLearnMore(objectGraph); + if (isNothing(bodyText)) { + return null; + } + shelf.items = [bodyText]; + return shelf; +} +/** + * Creates the linkable body text for the product reviews learn more shelf. + * @param objectGraph Current object graph + * @returns The built body text + */ +function bodyTextForProductReviewsLearnMore(objectGraph) { + const learnMoreLinkText = objectGraph.loc.string("ProductPage.RatingsAndReviews.Footer.LearnMore"); + const linkedSubstrings = {}; + const learnMoreAction = createProductReviewsLearnMoreAction(objectGraph); + if (isNothing(learnMoreAction)) { + return null; + } + linkedSubstrings[learnMoreLinkText] = learnMoreAction; + let text = objectGraph.loc.string("ProductPage.RatingsAndReviews.Footer"); + text = text.replace("{learnMoreLink}", learnMoreLinkText); + const textType = "text/plain"; + const styledText = new models.StyledText(text, textType); + return new models.LinkableText(styledText, linkedSubstrings); +} +/** + * Creates the FlowAction to navigate to the article related to the product reviews learn more shelf. + * @param objectGraph Current object graph + * @returns The built FlowAction + */ +function createProductReviewsLearnMoreAction(objectGraph) { + const editorialItemId = objectGraph.bag.ratingsAndReviewsLearnMoreEditorialId; + if (serverData.isNullOrEmpty(editorialItemId)) { + return null; + } + const learnMoreAction = new models.FlowAction("article"); + learnMoreAction.title = objectGraph.loc.string("ProductPage.RatingsAndReviews.Footer.LearnMore"); + learnMoreAction.pageUrl = `https://apps.apple.com/story/id${editorialItemId}`; + return learnMoreAction; +} +/** + * Creates the Review Summary Shelf + * @param objectGraph Current object graph + * @param data The current product page data + * @returns The review summary shelf + */ +export function createReviewSummary(objectGraph, data, shelfMetrics) { + const reviewSummary = reviews.reviewSummaryFromData(objectGraph, data, shelfMetrics); + if (isNothing(reviewSummary)) { + return null; + } + const shelf = new models.Shelf("reviewSummary"); + shelf.items = [reviewSummary]; + return shelf; +} +//# sourceMappingURL=shelf-based-reviews-shelves.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/similar-items-shelf.js b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/similar-items-shelf.js new file mode 100644 index 0000000..70468a0 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/similar-items-shelf.js @@ -0,0 +1,253 @@ +import * as validation from "@jet/environment/json/validation"; +import * as models from "../../../api/models"; +import * as serverData from "../../../foundation/json-parsing/server-data"; +import * as mediaDataStructure from "../../../foundation/media/data-structure"; +import * as mediaRelationship from "../../../foundation/media/relationships"; +import { Parameters, Path, Protocol, ShelfRefreshType } from "../../../foundation/network/url-constants"; +import { URL } from "../../../foundation/network/urls"; +import { applySearchAdMissedOpportunityToShelvesIfNeeded, isAdPlacementEnabled } from "../../ads/ad-common"; +import * as content from "../../content/content"; +import * as metricsHelpersLocation from "../../metrics/helpers/location"; +import * as metricsHelpersPage from "../../metrics/helpers/page"; +import { insertPlaceholdersIntoProductPageShelfIfRequired } from "../../placeholders/placeholders"; +import * as productPageCommon from "../product-page-common"; +import { ProductPageOnDemandShelfType } from "../product-page-common"; +import { ProductPageShelfMetrics } from "../product-page-shelf-metrics"; +import * as adIncidents from "../../../common/ads/ad-incident-recorder"; +import { adStitcherForOnDeviceProductPageYMALAdvertData } from "../../ads/on-device-ad-stitch"; +import * as adStitch from "../../ads/ad-stitcher"; +import { makeSeeAllPageURL } from "../intent-controller-routing"; +import { makeSeeAllPageIntent } from "../../../api/intents/see-all-page-intent"; +import { getPlatform } from "../../preview-platform"; +import { getLocale } from "../../locale"; +/** + * Create a shelf of similar items for the product page. + * + * @param productData The raw data response for a product page JSON fetch. + * @param shelfMetrics The product page shelf metrics. + * @returns A similar items shelf. + */ +export function createInitialShelf(objectGraph, productData, shelfMetrics) { + return validation.context("similarItemsShelf", () => { + var _a; + if (serverData.isNullOrEmpty(productData)) { + return null; + } + if (objectGraph.client.isCompanionVisionApp) { + return null; + } + const relationship = mediaRelationship.relationship(productData, "customers-also-bought-apps"); + if (serverData.isNullOrEmpty(relationship)) { + return null; + } + const supportsArcade = content.isArcadeSupported(objectGraph, productData); + const title = supportsArcade + ? objectGraph.loc.string("Arcade.ProductPage.MoreFromAppleArcade") + : objectGraph.loc.string("ProductPage.Section.SimilarItems.Title"); + const metricsTargetType = "similarItems"; + const metricsIdType = "relationship"; + metricsHelpersLocation.pushContentLocation(objectGraph, { + pageInformation: shelfMetrics.metricsPageInformation, + locationTracker: shelfMetrics.locationTracker, + targetType: metricsTargetType, + id: ProductPageShelfMetrics.similarItemsShelfId, + idType: metricsIdType, + }, title); + let contentType; + let shouldInferSeeAllFromFetchedItems; + switch (objectGraph.client.deviceType) { + case "tv": + contentType = "mediumLockup"; + shouldInferSeeAllFromFetchedItems = false; + break; + case "mac": + contentType = "mediumLockup"; + shouldInferSeeAllFromFetchedItems = true; + break; + default: + contentType = "smallLockup"; + shouldInferSeeAllFromFetchedItems = true; + break; + } + const shelf = new models.Shelf(contentType); + shelf.title = title; + const recoMetricsData = mediaDataStructure.metricsFromMediaApiObject(relationship); + const hasShelfBackground = !objectGraph.client.isTV && !objectGraph.client.isiOS; + const offerStyle = hasShelfBackground ? "white" : null; + const shelfItems = productPageCommon.lockupsFromDataContainer(objectGraph, relationship, shelfMetrics, content.artworkUseCaseFromShelfStyle(objectGraph, contentType), contentType, offerStyle, null, recoMetricsData); + if (!shelfItems) { + return null; + } + shelf.items = shelfItems.items; + const isAdEnabled = isAdPlacementEnabled(objectGraph, "productPageYMAL"); + const destinationPageInformation = metricsHelpersPage.fakeMetricsPageInformation(objectGraph, "SimilarItems", productData.id, ""); + // Add all the ids on the shelf to attach to the refreshUrl as a fallback in case the shelf can't be refreshed with new content. + // Create a `refreshUrl` for this shelf so it can be refreshed by native during downloading. + const refreshShelfToken = new productPageCommon.ProductPageShelfToken(productData.id, relationship.data, shelf.title, shouldInferSeeAllFromFetchedItems, undefined, contentType, null, null, null, null, null, ProductPageOnDemandShelfType.SimilarItems); + if (objectGraph.client.isWeb) { + const action = new models.FlowAction("page"); + action.title = objectGraph.loc.string("ACTION_SEE_ALL"); + const destination = makeSeeAllPageIntent({ + ...getLocale(objectGraph), + ...getPlatform(objectGraph), + "id": productData.id, + "see-all": "customers-also-bought-apps", + }); + action.destination = destination; + action.pageUrl = makeSeeAllPageURL(objectGraph, destination); + shelf.seeAllAction = action; + } + else { + const shelfRefreshUrl = refreshUrl(objectGraph, productData.id, refreshShelfToken, shelfMetrics, destinationPageInformation); + shelf.refreshUrl = shelfRefreshUrl; + } + shelf.isHorizontal = true; + if (relationship.data.length < 2 || objectGraph.client.isTV) { + shelf.rowsPerColumn = 1; + } + else if (supportsArcade || !objectGraph.client.isiOS) { + // Arcade and other platforms (except TV) show 2 apps per column. + shelf.rowsPerColumn = 2; + } + else { + // iOS regular product pages show 3 apps per column. + shelf.rowsPerColumn = 3; + } + if (hasShelfBackground) { + shelf.background = { + type: "color", + color: productPageCommon.grayShelfBackgroundColor, + }; + } + if (objectGraph.client.isiOS && objectGraph.bag.isOnDemandShelfFetchingEnabled) { + shelf.fetchStrategy = models.IncompleteShelfFetchStrategy.OnShelfWillAppear; + } + if (serverData.isDefinedNonNullNonEmpty(shelfItems.remainingItems) || isAdEnabled) { + // Add impressions and pop the location here so we're at the original state for the shelf token / url + metricsHelpersLocation.popLocation(shelfMetrics.locationTracker); + shelfMetrics.addImpressionsFieldsToSimilarItemsShelf(objectGraph, shelf, metricsTargetType, metricsIdType); + metricsHelpersLocation.nextPosition(shelfMetrics.locationTracker); + const shelfToken = new productPageCommon.ProductPageShelfToken(productData.id, (_a = shelfItems.remainingItems) !== null && _a !== void 0 ? _a : [], shelf.title, shouldInferSeeAllFromFetchedItems, undefined, contentType, offerStyle, null, shelf.refreshUrl, recoMetricsData, supportsArcade, ProductPageOnDemandShelfType.SimilarItems); + insertPlaceholdersIntoProductPageShelfIfRequired(objectGraph, shelf, shelfToken); + shelf.url = productPageCommon.shelfContentUrl(objectGraph, shelfToken, shelfMetrics, destinationPageInformation); + if (isAdEnabled) { + const url = new URL(shelf.url); + url.param(Parameters.isShelfWithAd, "true"); + url.param(Parameters.shelfWithAdPlacementType, "productPageYMAL"); + url.param(Parameters.id, productData.id); + shelf.url = url.build(); + } + } + else { + metricsHelpersLocation.popLocation(shelfMetrics.locationTracker); + shelfMetrics.addImpressionsFieldsToSimilarItemsShelf(objectGraph, shelf, metricsTargetType, metricsIdType); + metricsHelpersLocation.nextPosition(shelfMetrics.locationTracker); + } + return shelf; + }); +} +/** + * Create a shelf of similar items for the secondary look up. + * + * @param dataItems The raw data items from the secondary fetch. + * @param shelfToken The shelf token for this shelf. + * @returns A similar items shelf. + */ +export function createSecondaryShelf(objectGraph, dataItems, shelfToken, adResponse) { + return validation.context("similarItemsSecondaryShelf", () => { + var _a, _b; + if (serverData.isNullOrEmpty(dataItems)) { + return null; + } + if (objectGraph.client.isCompanionVisionApp) { + return null; + } + const shelfMetrics = new ProductPageShelfMetrics(shelfToken.sourcePageInformation, shelfToken.sourceLocationTracker, shelfToken.sourceSequenceId); + const metricsTargetType = "similarItems"; + const metricsIdType = "relationship"; + const metricsOptions = { + pageInformation: shelfMetrics.metricsPageInformation, + locationTracker: shelfMetrics.locationTracker, + targetType: metricsTargetType, + id: ProductPageShelfMetrics.similarItemsShelfId, + idType: metricsIdType, + excludeAttribution: true, + recoMetricsData: shelfToken.recoMetricsData, + }; + metricsHelpersLocation.pushContentLocation(objectGraph, metricsOptions, shelfToken.title); + const shelf = new models.Shelf(shelfToken.contentType); + let adIncidentRecorder; + if (isAdPlacementEnabled(objectGraph, "productPageYMAL") || + isAdPlacementEnabled(objectGraph, "productPageYMALDuringDownload")) { + adIncidentRecorder = adIncidents.newRecorder(objectGraph, shelfToken.sourcePageInformation.iAdInfo); + adIncidents.recordAdResponseEventsIfNeeded(objectGraph, adIncidentRecorder, adResponse); + const adStitcher = adStitcherForOnDeviceProductPageYMALAdvertData(objectGraph, adResponse); + // Update metrics data for a new ad request. + (_a = shelfMetrics.metricsPageInformation.iAdInfo) === null || _a === void 0 ? void 0 : _a.updateForAdResponse(objectGraph, adResponse); + /** + * *** Ad Stitching *** + * Get all the tasks for the current shelf, and iterate over the tasks, inserting them in the specified position. + */ + const adTasks = adStitch.consumeTasksForShelfIdentifier(adStitcher, (_b = metricsHelpersLocation.currentLocation(shelfMetrics.locationTracker)) === null || _b === void 0 ? void 0 : _b.id); + adTasks.forEach((task) => { + // Remove any organic dupes of the ad we're about to insert. + dataItems = dataItems.filter((data) => data.id !== task.data.id); + dataItems.splice(task.positionInfo.slot, 0, task.data); + }); + shelf.adIncidents = adIncidents.recordedIncidents(objectGraph, adIncidentRecorder); + } + const shelfItems = productPageCommon.lockupsFromData(objectGraph, dataItems, shelfMetrics, content.artworkUseCaseFromShelfStyle(objectGraph, shelfToken.contentType), shelfToken.contentType, shelfToken.offerStyle, null, shelfToken.recoMetricsData); + if (serverData.isNullOrEmpty(shelfItems)) { + return null; + } + shelf.title = shelfToken.title; + shelf.items = shelfItems.items; + shelf.isHorizontal = true; + const hasShelfBackground = !objectGraph.client.isTV && !objectGraph.client.isiOS; + if (hasShelfBackground) { + shelf.background = { + type: "color", + color: productPageCommon.grayShelfBackgroundColor, + }; + } + // Add all the ids on the shelf to attach to the refreshUrl as a fallback in case the shelf can't be refreshed with new content. + // Create a `refreshUrl` for this shelf so it can be refreshed by native during downloading. + const refreshShelfToken = new productPageCommon.ProductPageShelfToken(shelfToken.productId, dataItems, shelfToken.title, shelfToken.shouldInferSeeAllFromFetchesItems, undefined, shelfToken.contentType, null, null, null, null, null, ProductPageOnDemandShelfType.SimilarItems); + const shelfRefreshUrl = refreshUrl(objectGraph, shelfToken.productId, refreshShelfToken, shelfMetrics, shelfToken.destinationPageInformation); + shelf.refreshUrl = shelfRefreshUrl; + if (dataItems.length < 2 || objectGraph.client.isTV) { + shelf.rowsPerColumn = 1; + } + else if (shelfToken.supportsArcade || !objectGraph.client.isiOS) { + // Arcade and other platforms (except TV) show 2 apps per column. + shelf.rowsPerColumn = 2; + } + else { + // iOS regular product pages show 3 apps per column. + shelf.rowsPerColumn = 3; + } + shelfMetrics.addImpressionsFieldsToSimilarItemsShelf(objectGraph, shelf, metricsTargetType, metricsIdType); + applySearchAdMissedOpportunityToShelvesIfNeeded(objectGraph, [shelf], "productPageYMAL", metricsOptions.id, metricsOptions.pageInformation); + metricsHelpersLocation.popLocation(shelfMetrics.locationTracker); + metricsHelpersLocation.nextPosition(shelfMetrics.locationTracker); + return shelf; + }); +} +/** + * Returns a refresh URL for the similar items shelf. + * @param objectGraph - The object graph. + * @param id - The ID of the product this shelf appears on. + * @param token - The constructed ProductPageShelfToken. + * @param shelfMetrics - Metrics for the shelf. + * @param destinationPageInformation - Information for the destination page. + * @returns A string containing a URL. + */ +function refreshUrl(objectGraph, id, token, shelfMetrics, destinationPageInformation) { + const urlString = `${Protocol.internal}:/${Path.product}/${Path.shelf}/` + + productPageCommon.encodedShelfToken(token, shelfMetrics, destinationPageInformation); + const url = new URL(urlString); + url.param(Parameters.shelfRefreshType, ShelfRefreshType.productPageSimilarItems); + url.param(Parameters.id, id); + return url.build(); +} +//# sourceMappingURL=similar-items-shelf.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/small-story-card-shelf.js b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/small-story-card-shelf.js new file mode 100644 index 0000000..7881975 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/small-story-card-shelf.js @@ -0,0 +1,137 @@ +import * as validation from "@jet/environment/json/validation"; +import * as models from "../../../api/models"; +import { isDefinedNonNullNonEmpty, isNullOrEmpty } from "../../../foundation/json-parsing/server-data"; +import * as metricsHelpersLocation from "../../metrics/helpers/location"; +import { insertPlaceholdersIntoProductPageShelfIfRequired } from "../../placeholders/placeholders"; +import * as todayHorizontalCardUtil from "../../today/today-horizontal-card-util"; +import { TodayParseContext } from "../../today/today-types"; +import * as productPageCommon from "../product-page-common"; +import { ProductPageOnDemandShelfType } from "../product-page-common"; +import { ProductPageShelfMetrics } from "../product-page-shelf-metrics"; +/** + * Create a shelf for the product page's small story cards. + * + * @param productData The raw product data response for a product page JSON fetch. + * @param data The raw data items for a product page JSON fetch. + * @param shelfMetrics The product page shelf metrics. + * @returns A small story card shelf. + */ +export function createInitialShelf(objectGraph, productData, data, shelfMetrics) { + return validation.context("smallStoryShelf", () => { + var _a; + if (isNullOrEmpty(data)) { + return null; + } + const metricsOptions = { + pageInformation: shelfMetrics.metricsPageInformation, + locationTracker: shelfMetrics.locationTracker, + targetType: "swoosh", + id: `${shelfMetrics.getSequenceId()}`, + idType: "sequential", + }; + const title = objectGraph.loc.string("ProductPage.Section.FeaturedIn.Title"); + metricsHelpersLocation.pushContentLocation(objectGraph, metricsOptions, title); + const remainingItems = []; + const cardUnavailable = function (cardData) { + remainingItems.push(cardData); + return false; + }; + let shelf; + if (areMiniTodayCardsEnabled(objectGraph)) { + const todayParseContext = new TodayParseContext(metricsOptions.pageInformation, metricsOptions.locationTracker); + shelf = todayHorizontalCardUtil.shelfForMiniTodayCards(objectGraph, data, title, null, todayParseContext, cardUnavailable); + } + else { + const context = { + metricsLocationTracker: metricsOptions.locationTracker, + metricsPageInformation: metricsOptions.pageInformation, + filterOutMediaCardKinds: new Set(["appIcon"]), // policy decision to filter out these types on the featured in shelf of a product page. + }; + shelf = todayHorizontalCardUtil.shelfForHorizontalCardItems(objectGraph, data, "smallStoryCard", title, null, context, cardUnavailable); + if (Array.isArray(shelf.items)) { + shelf.items = shelf.items.filter((item) => { + if (!(item instanceof models.TodayCard)) { + return true; + } + return todayHorizontalCardUtil.isHorizontalCardSupportedForKind(objectGraph, item.media.kind, "smallStoryCard"); + }); + } + } + shelf.isHorizontal = true; + if (!((_a = shelf.items) === null || _a === void 0 ? void 0 : _a.length) && remainingItems.length === 0) { + return null; + } + if (objectGraph.client.isiOS && objectGraph.bag.isOnDemandShelfFetchingEnabled) { + shelf.fetchStrategy = models.IncompleteShelfFetchStrategy.OnShelfWillAppear; + } + if (isDefinedNonNullNonEmpty(remainingItems)) { + metricsHelpersLocation.popLocation(shelfMetrics.locationTracker); + shelfMetrics.addImpressionsToShelf(objectGraph, shelf, "featuredIn"); + metricsHelpersLocation.nextPosition(shelfMetrics.locationTracker); + const shelfToken = new productPageCommon.ProductPageShelfToken(productData.id, remainingItems, shelf.title, false, undefined, shelf.contentType, null, null, null, null, null, ProductPageOnDemandShelfType.SmallStory); + insertPlaceholdersIntoProductPageShelfIfRequired(objectGraph, shelf, shelfToken); + shelf.url = productPageCommon.shelfContentUrl(objectGraph, shelfToken, shelfMetrics); + } + else { + metricsHelpersLocation.popLocation(shelfMetrics.locationTracker); + shelfMetrics.addImpressionsToShelf(objectGraph, shelf, "featuredIn"); + metricsHelpersLocation.nextPosition(shelfMetrics.locationTracker); + } + return shelf; + }); +} +/** + * Create a shelf of small store card items for the secondary look up. + * + * @param dataItems The raw data items from the secondary fetch. + * @param shelfToken The shelf token for this shelf. + * @returns A small story card items shelf. + */ +export function createSecondaryShelf(objectGraph, dataItems, shelfToken) { + return validation.context("smallStorySecondaryShelf", () => { + if (isNullOrEmpty(dataItems)) { + return null; + } + const shelfMetrics = new ProductPageShelfMetrics(shelfToken.sourcePageInformation, shelfToken.sourceLocationTracker, shelfToken.sourceSequenceId); + const metricsOptions = { + pageInformation: shelfMetrics.metricsPageInformation, + locationTracker: shelfMetrics.locationTracker, + targetType: "swoosh", + id: `${shelfMetrics.getSequenceId()}`, + idType: "sequential", + excludeAttribution: true, + recoMetricsData: shelfToken.recoMetricsData, + }; + metricsHelpersLocation.pushContentLocation(objectGraph, metricsOptions, shelfToken.title); + let shelf; + if (areMiniTodayCardsEnabled(objectGraph)) { + const todayParseContext = new TodayParseContext(metricsOptions.pageInformation, metricsOptions.locationTracker); + shelf = todayHorizontalCardUtil.shelfForMiniTodayCards(objectGraph, dataItems !== null && dataItems !== void 0 ? dataItems : [], shelfToken.title, null, todayParseContext); + } + else { + const context = { + metricsLocationTracker: metricsOptions.locationTracker, + metricsPageInformation: metricsOptions.pageInformation, + filterOutMediaCardKinds: productPageCommon.filteredMediaCardKindsForSmallStoryCardOnPlatform(objectGraph.host.platform), + }; + shelf = todayHorizontalCardUtil.shelfForHorizontalCardItems(objectGraph, dataItems, "smallStoryCard", shelfToken.title, null, context); + if (Array.isArray(shelf === null || shelf === void 0 ? void 0 : shelf.items)) { + shelf.items = shelf.items.filter((item) => { + if (!(item instanceof models.TodayCard)) { + return true; + } + return todayHorizontalCardUtil.isHorizontalCardSupportedForKind(objectGraph, item.media.kind, "smallStoryCard"); + }); + } + } + shelf.isHorizontal = true; + shelfMetrics.addImpressionsToShelf(objectGraph, shelf, "featuredIn"); + metricsHelpersLocation.popLocation(shelfMetrics.locationTracker); + metricsHelpersLocation.nextPosition(shelfMetrics.locationTracker); + return shelf; + }); +} +function areMiniTodayCardsEnabled(objectGraph) { + return objectGraph.client.isiOS && objectGraph.featureFlags.isEnabled("mini_today_cards_product"); +} +//# sourceMappingURL=small-story-card-shelf.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/text-links-shelf.js b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/text-links-shelf.js new file mode 100644 index 0000000..a17dabd --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/text-links-shelf.js @@ -0,0 +1,59 @@ +import * as validation from "@jet/environment/json/validation"; +import * as models from "../../../api/models"; +import * as serverData from "../../../foundation/json-parsing/server-data"; +import * as contentAttributes from "../../content/attributes"; +import * as productPageUtil from "../product-page-util"; +/** + * Creates a shelf of text links + * @param data The raw data response for a product page JSON fetch. + * @param shelfMetrics The current product page shelf metrics + * @returns The shelf of text links. If objectGraph.client.deviceType is not tv, returns null. + */ +export function create(objectGraph, data, shelfMetrics) { + return validation.context("create", () => { + if (objectGraph.client.deviceType !== "tv") { + return null; + } + if (serverData.isNullOrEmpty(data)) { + return null; + } + const shelf = new models.Shelf("paragraph"); + shelf.isHorizontal = false; + const links = externalProductLinksAsParagraphsFromData(objectGraph, data); + if (links && links.length > 0) { + shelf.items = links; + } + if (productPageUtil.isShelfBased(objectGraph)) { + shelf.background = { + type: "darkOverlay", + }; + } + shelfMetrics.addImpressionsToShelf(objectGraph, shelf, "links"); + return shelf; + }); +} +/** + * Product links displayable as text + * @param data The product data to use + * @returns {models.Paragraph[]} Product links displayed as paragraphs + */ +function externalProductLinksAsParagraphsFromData(objectGraph, data) { + if (!data) { + return null; + } + return validation.context("externalProductLinksAsParagraphsFromData", () => { + const links = []; + const developerWebsiteUrl = contentAttributes.contentAttributeAsString(objectGraph, data, "websiteUrl"); + if (developerWebsiteUrl) { + const text = objectGraph.loc.string("DEVELOPER_WEBSITE_WITH_URL").replace("{URL}", developerWebsiteUrl); + const paragraph = new models.Paragraph(text); + if (objectGraph.featureFlags.isEnabled("tv_product_page_2024E")) { + paragraph.style = "unadorned"; + } + paragraph.alignment = "center"; + links.push(paragraph); + } + return links; + }); +} +//# sourceMappingURL=text-links-shelf.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/textcard-shelf.js b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/textcard-shelf.js new file mode 100644 index 0000000..b41eabd --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/textcard-shelf.js @@ -0,0 +1,226 @@ +import * as validation from "@jet/environment/json/validation"; +import * as models from "../../../api/models"; +import * as serverData from "../../../foundation/json-parsing/server-data"; +import * as mediaAttributes from "../../../foundation/media/attributes"; +import * as mediaRelationship from "../../../foundation/media/relationships"; +import { shallowCopyOf } from "../../../foundation/util/objects"; +import * as contentAttributes from "../../content/attributes"; +import { developerUrlFromDeveloperData } from "../../content/content"; +import * as lockups from "../../lockups/lockups"; +import { addClickEventToAction } from "../../metrics/helpers/clicks"; +import { addMetricsEventsToPageWithInformation, fakeMetricsPageInformation } from "../../metrics/helpers/page"; +import * as badgesCommon from "../badges/badges-common"; +import * as descriptionShelf from "./description-shelf"; +import * as versionHistoryShelves from "./version-history-shelves"; +const tvTextCardList = [aboutTextCard, editorsNoteTextCard, whatsNewTextCard]; +const visionTextCardList = [aboutTextCard, whatsNewTextCard]; +/** + * Create a shelf for the text cards displayed on the tv product page. + * + * @param data The raw data response for a product page JSON fetch. + * @param shelfMetrics The product page shelf metrics. + * @returns A textcard shelf. + */ +export function create(objectGraph, data, shelfMetrics) { + return validation.context("textCardShelfFromResponse", () => { + if (serverData.isNullOrEmpty(data)) { + return null; + } + let textCardList; + if (objectGraph.client.isTV) { + textCardList = tvTextCardList; + } + else if (objectGraph.client.isVision) { + textCardList = visionTextCardList; + } + else { + return null; + } + const shelf = new models.Shelf("textCard"); + if (!objectGraph.client.isVision) { + shelf.title = objectGraph.loc.string("PRODUCT_SECTION_TEXT_CARDS"); + shelf.isHorizontal = true; + } + shelf.items = createTextCards(objectGraph, textCardList, data, shelfMetrics); + if (objectGraph.client.isVision && shelf.items.length === 1) { + shelf.presentationHints = { + isSingleDensity: true, + }; + } + shelfMetrics.addImpressionsToShelf(objectGraph, shelf, "links"); + return shelf; + }); +} +/** + * Creates a text card object + * + * @param objectGraph The current object graph + * @param style The style of the text card + * @param title The title text + * @param subtitle The subtitle text + * @param descriptionText The description text + * @param alertTitle The title to show when presenting as an alert + * @param captionTitle The caption title text + * @param captionSubtitle The caption subtitle text + * @param captionArtwork The artwork to display alongside caption text + * @param includeCaptionWhenExpanded Whether to display the caption when in an expanded state + * @param captionAction An action to run when selecting the caption + * @returns A built text card + */ +function createTextCard(objectGraph, style, title, subtitle, descriptionText, alertTitle, captionTitle, captionSubtitle, captionArtwork, includeCaptionWhenExpanded, captionAction) { + const textCard = new models.TextCard(style, title, subtitle); + if (objectGraph.host.isTV || objectGraph.client.isVision) { + const bodyParagraph = new models.Paragraph(descriptionText); + if (objectGraph.featureFlags.isEnabled("tv_product_page_2024E")) { + bodyParagraph.style = "unadorned"; + } + bodyParagraph.isCollapsed = true; + textCard.bodyParagraph = bodyParagraph; + } + const paragraph = new models.Paragraph(descriptionText); + paragraph.wantsCollapsedNewlines = false; + if (objectGraph.client.isVision) { + textCard.captionTitle = captionTitle; + textCard.captionSubtitle = captionSubtitle; + textCard.captionArtwork = captionArtwork; + textCard.includeCaptionWhenExpanded = includeCaptionWhenExpanded; + textCard.captionAction = captionAction; + } + else { + textCard.regularAction = new models.ScrollingAlertAction(alertTitle, paragraph); + } + return textCard; +} +function aboutTextCard(objectGraph, data, metricsContext) { + const paragraph = descriptionShelf.paragraphFromData(objectGraph, data); + if (serverData.isNull(paragraph)) { + return null; + } + const descriptionText = paragraph.text; + if (objectGraph.client.isVision) { + const title = objectGraph.loc.string("PRODUCT_SECTION_DESCRIPTION"); + const captionTitle = objectGraph.loc.string("PRODUCT_DEVELOPER_TITLE"); + const captionSubtitle = mediaAttributes.attributeAsString(data, "artistName"); + const developer = mediaRelationship.relationshipData(objectGraph, data, "developer"); + const developerArtworkData = mediaAttributes.attributeAsDictionary(developer, "editorialArtwork.brandLogo"); + const developerArtwork = badgesCommon.badgeArtwork(objectGraph, developerArtworkData); + // Developer action + let captionAction; + const developerUrl = developerUrlFromDeveloperData(objectGraph, developer); + if (serverData.isDefinedNonNull(developerUrl)) { + const developerAction = new models.FlowAction("page"); + developerAction.title = mediaAttributes.attributeAsString(data, "artistName"); + developerAction.pageUrl = developerUrl; + const flowBackAction = new models.FlowBackAction("sheetDismiss"); + captionAction = new models.CompoundAction([flowBackAction, developerAction]); + addClickEventToAction(objectGraph, captionAction, { + targetType: "button", + id: developer.id, + actionType: "navigate", + pageInformation: metricsContext.pageInformation, + locationTracker: metricsContext.locationTracker, + }); + } + const textCard = createTextCard(objectGraph, "text", title, null, descriptionText, title, captionTitle, captionSubtitle, developerArtwork, true, captionAction); + textCard.regularAction = createTextCardDetailPageFlowAction(objectGraph, textCard, data.id); + return textCard; + } + else { + const title = mediaAttributes.attributeAsString(data, "name"); + const subtitle = lockups.subtitleFromData(objectGraph, data); + return createTextCard(objectGraph, "text", title, subtitle, descriptionText, title); + } +} +function editorsNoteTextCard(objectGraph, data, metricsContext) { + const editorialNotes = contentAttributes.contentAttributeAsDictionary(objectGraph, data, "editorialNotes"); + const editorsNotes = serverData.asString(editorialNotes, "standard"); + if (editorsNotes) { + let title = null; + let alertTitle = null; + let style = "text"; + const editorialBadgeInfo = contentAttributes.contentAttributeAsDictionary(objectGraph, data, "editorialBadgeInfo"); + if (editorialBadgeInfo) { + const badgeType = serverData.asString(editorialBadgeInfo, "editorialBadgeType"); + if (badgeType && badgeType === "editorialPriority") { + style = "editorsChoice"; + } + const alertTitleFromServer = serverData.asString(editorialBadgeInfo, "nameForDisplay"); + if (alertTitleFromServer) { + alertTitle = alertTitleFromServer; + } + else { + alertTitle = objectGraph.loc.string("APP_STORE_EDITORS_NOTES"); + } + } + else { + title = objectGraph.loc.string("APP_STORE_EDITORS_NOTES"); + alertTitle = objectGraph.loc.string("APP_STORE_EDITORS_NOTES"); + } + return createTextCard(objectGraph, style, title, "", editorsNotes, alertTitle); + } + return null; +} +function whatsNewTextCard(objectGraph, data, metricsContext) { + const isBundle = data.type === "app-bundles"; + const isFirstPartyHideableApp = mediaAttributes.attributeAsBooleanOrFalse(data, "isFirstPartyHideableApp"); + const isPreorder = mediaAttributes.attributeAsBoolean(data, "isPreorder"); + const mostRecentVersionNotes = versionHistoryShelves.mostRecentTitledParagraphFromVersionHistory(objectGraph, data, isFirstPartyHideableApp, isBundle, isPreorder); + if (mostRecentVersionNotes) { + const versionNumber = mostRecentVersionNotes.primarySubtitle; + const releaseNotes = mostRecentVersionNotes.text; + const title = objectGraph.loc.string("ProductPage.Section.MostRecentVersion.Title"); + if (objectGraph.client.isVision) { + const releaseDate = mostRecentVersionNotes.secondarySubtitle; + const textCard = createTextCard(objectGraph, "text", title, null, releaseNotes, title, releaseDate, versionNumber); + const versions = contentAttributes.contentAttributeAsArrayOrEmpty(objectGraph, data, "versionHistory"); + if (versions.length > 0 && !isFirstPartyHideableApp && !isBundle && !isPreorder) { + textCard.regularAction = versionHistoryShelves.versionHistoryPageAction(objectGraph, versions, data.id); + } + return textCard; + } + else { + return createTextCard(objectGraph, "text", title, versionNumber, releaseNotes, title); + } + } + return null; +} +function createTextCards(objectGraph, textCardList, data, shelfMetrics) { + const textCards = []; + const metricsContext = { + locationTracker: shelfMetrics.locationTracker, + pageInformation: shelfMetrics.metricsPageInformation, + }; + for (const f of textCardList) { + const textCard = f(objectGraph, data, metricsContext); + if (textCard) { + textCards.push(textCard); + } + } + return textCards; +} +/** + * Creates a flow action for navigating to the detail page for a text card. + * + * @param objectGraph The current object graph + * @param textCard The source text card + * @param productId The id of the owning product + * @returns A flow action + */ +function createTextCardDetailPageFlowAction(objectGraph, textCard, productId) { + const detailTextCard = shallowCopyOf(textCard); + detailTextCard.isExpanded = true; + const shelf = new models.Shelf("textCard"); + shelf.items = [detailTextCard]; + shelf.presentationHints = { + isSingleDensity: true, + }; + const page = new models.GenericPage([shelf]); + page.title = textCard.title; + const pageInformation = fakeMetricsPageInformation(objectGraph, "DescriptionDetails", productId, ""); + addMetricsEventsToPageWithInformation(objectGraph, page, pageInformation); + const flowAction = new models.FlowAction("textCardDetail"); + flowAction.title = textCard.title; + flowAction.pageData = page; + return flowAction; +} +//# sourceMappingURL=textcard-shelf.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/version-history-shelves.js b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/version-history-shelves.js new file mode 100644 index 0000000..41615fb --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/version-history-shelves.js @@ -0,0 +1,149 @@ +import * as validation from "@jet/environment/json/validation"; +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 * as metricsHelpersPage from "../../metrics/helpers/page"; +import { isSome } from "@jet/environment"; +/** + * Create a shelf for the product page's version history. + * + * @param data The raw data response for a product page JSON fetch. + * @param shelfMetrics The product page shelf metrics. + * @param shelfContext A collection of any other variables used when creating this shelf. + * @returns A version history shelf. + */ +export function create(objectGraph, data, shelfMetrics, shelfContext) { + return validation.context("create", () => { + return standardMostRecentVersionShelf(objectGraph, data, shelfContext.isFirstPartyHideableApp, shelfContext.isBundle, shelfContext.isPreorder, shelfMetrics); + }); +} +/** + * Create a shelf for the product page description. + * + * @param data The raw data response for a product page JSON fetch. + * @param isFirstPartyHideableApp Indicates whether this app is first party and/or hideable. + * @param isBundle Whether this is a bundle id + * @param isPreorder Whether the buy is for a preorder. + * @returns A description shelf. + */ +export function mostRecentTitledParagraphFromVersionHistory(objectGraph, data, isFirstPartyHideableApp, isBundle, isPreorder) { + if (!isFirstPartyHideableApp && !isBundle && !isPreorder) { + const versions = contentAttributes.contentAttributeAsArrayOrEmpty(objectGraph, data, "versionHistory"); + if (versions.length > 1) { + const versionHistory = versionHistoryFromApiResponse(objectGraph, versions); + if (versionHistory.length > 0) { + return titledParagraphFromVersionHistoryEntry(objectGraph, versions[0], "overview"); + } + } + } + return null; +} +/** + * A flow action to navigate to the version history. + * + * @param versions The raw data response for the version history of a product. + * @param productId The product ID. + * @returns A flow action for navigating to the version history. + */ +export function versionHistoryPageAction(objectGraph, versions, productId) { + return validation.context("versionHistoryPageAction", () => { + const versionHistory = versionHistoryFromApiResponse(objectGraph, versions); + let action; + if (versionHistory.length > 0) { + // Create version history page + const allVersionHistoryShelf = new models.Shelf("titledParagraph"); + allVersionHistoryShelf.items = versionHistory; + const page = new models.GenericPage([allVersionHistoryShelf]); + page.title = objectGraph.loc.string("VERSION_HISTORY_PAGE_TITLE", "Version History"); + if (objectGraph.client.deviceType !== "watch") { + page.presentationOptions = ["prefersLargeTitle"]; + } + const pageInformation = metricsHelpersPage.fakeMetricsPageInformation(objectGraph, "VersionHistory", productId, ""); + metricsHelpersPage.addMetricsEventsToPageWithInformation(objectGraph, page, pageInformation); + // Create version history action + const versionHistorySeeAll = new models.FlowAction("versionHistory"); + versionHistorySeeAll.title = objectGraph.loc.string("ACTION_VERSION_HISTORY_SEE_ALL"); + versionHistorySeeAll.pageData = page; + action = versionHistorySeeAll; + } + return action; + }); +} +function standardMostRecentVersionShelf(objectGraph, productData, isFirstPartyHideableApp, isBundle, isPreorder, shelfMetrics) { + return validation.context("standardMostRecentVersionShelf", () => { + if (!isFirstPartyHideableApp && !isBundle && !isPreorder) { + const versions = contentAttributes.contentAttributeAsArrayOrEmpty(objectGraph, productData, "versionHistory"); + if (versions.length > 1) { + const versionHistory = versionHistoryFromApiResponse(objectGraph, versions); + if (versionHistory.length > 0) { + const shelf = new models.Shelf("titledParagraph"); + shelf.title = objectGraph.loc.string("ProductPage.Section.MostRecentVersion.Title"); + // Add recent version + const recentVersion = titledParagraphFromVersionHistoryEntry(objectGraph, versions[0], "overview"); + shelf.items = [recentVersion]; + shelfMetrics.addImpressionsToShelf(objectGraph, shelf, "versionHistory"); + if (shelf) { + if (objectGraph.client.deviceType !== "watch") { + shelf.seeAllAction = versionHistorySeeAllAction(objectGraph, versions, productData.id, shelfMetrics); + } + return shelf; + } + } + } + } + return null; + }); +} +function versionHistorySeeAllAction(objectGraph, versions, productId, shelfMetrics) { + return validation.context("versionHistorySeeAllAction", () => { + const action = versionHistoryPageAction(objectGraph, versions, productId); + metricsHelpersClicks.addClickEventToSeeAllAction(objectGraph, action, null, { + pageInformation: shelfMetrics.metricsPageInformation, + locationTracker: shelfMetrics.locationTracker, + }); + return action; + }); +} +/** + * Create an array of product releases from an array of version history data objects. + * @param versions The data objects to convert. + * @return An array of titled paragraph model objects. + */ +function versionHistoryFromApiResponse(objectGraph, versions) { + if (!versions) { + return []; + } + return versions.map((version) => { + return titledParagraphFromVersionHistoryEntry(objectGraph, version, "detail"); + }); +} +function titledParagraphFromVersionHistoryEntry(objectGraph, versionEntry, style) { + return validation.context("titledParagraphFromVersionHistoryEntry", () => { + // Notes + let releaseNotes = serverData.asString(versionEntry, "releaseNotes"); + if (isSome(releaseNotes)) { + releaseNotes = releaseNotes.trim(); + } + const release = new models.TitledParagraph(releaseNotes, style); + // Version + const versionString = serverData.asString(versionEntry, "versionDisplay"); + const versionTemplate = objectGraph.loc.string("VERSION_STRING_TEMPLATE"); + switch (style) { + case "detail": + release.primarySubtitle = versionString; + break; + default: + release.primarySubtitle = versionTemplate.replace("{version}", versionString); + break; + } + // Release date + const releaseDateString = serverData.asString(versionEntry, "releaseTimestamp"); + if (releaseDateString) { + const releaseDate = new Date(releaseDateString); + release.secondarySubtitle = objectGraph.loc.relativeDate(releaseDate); + } + return release; + }); +} +//# sourceMappingURL=version-history-shelves.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/video-shelf.js b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/video-shelf.js new file mode 100644 index 0000000..0d58160 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/product-page/shelves/video-shelf.js @@ -0,0 +1,38 @@ +import * as validation from "@jet/environment/json/validation"; +import * as models from "../../../api/models"; +import * as productPageUtil from "../product-page-util"; +/** + * Create a shelf for the product page's version history. + * + * @param videos An array of videos to present in this shelf. + * @param shelfMetrics The product page shelf metrics. + * @returns A video shelf. + */ +export function create(objectGraph, videos, shelfMetrics) { + return validation.context("videoShelf", () => { + if (!videos || videos.length === 0) { + return null; + } + let shelf; + if (productPageUtil.isShelfBased(objectGraph)) { + shelf = new models.Shelf("productMediaItem"); + shelf.title = objectGraph.loc.string("ProductPage.Section.Videos.Title"); + shelf.items = videos.map((video) => { + const productMediaItem = new models.ProductMediaItem(); + productMediaItem.video = video; + return productMediaItem; + }); + } + else { + shelf = new models.Shelf("framedVideo"); + shelf.title = objectGraph.loc.string("ProductPage.Section.Videos.Title"); + shelf.items = videos.map((video) => { + return new models.FramedVideo(video, true, "text/plain", null, null, true); + }); + } + shelf.isHorizontal = true; + shelfMetrics.addImpressionsToShelf(objectGraph, shelf, "videos"); + return shelf; + }); +} +//# sourceMappingURL=video-shelf.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/refresh/page-refresh-controller.js b/node_modules/@jet-app/app-store/tmp/src/common/refresh/page-refresh-controller.js new file mode 100644 index 0000000..a7c51d7 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/refresh/page-refresh-controller.js @@ -0,0 +1,123 @@ +import * as models from "../../api/models"; +import * as serverData from "../../foundation/json-parsing/server-data"; +import { ResponseMetadata } from "../../foundation/network/network"; +/** + * Creates a new `PageRefreshController` with the provided information. + * @param response the response from the network request. + * @param refreshWhileVisibleForNextPreferredContentRefreshDate indicates that the page supports refreshing while visible, but only where the nextPreferredContentRefreshDate is used as the refresh date. + * @returns a `PageRefreshController`. + */ +export function newPageRefreshControllerFromResponse(response, refreshWhileVisibleForNextPreferredContentRefreshDate = false) { + return { + timeToLive: timeToLiveFromResponse(response), + nextPreferredContentRefreshDate: null, + refreshWhileVisibleForNextPreferredContentRefreshDate: refreshWhileVisibleForNextPreferredContentRefreshDate, + }; +} +/** + * Creates a new `PageRefreshController`. + * This is primarily used for shelf hydration where we don't have a full page response to use for TTL data. + * @returns an empty `PageRefreshController`. + */ +export function newPageRefreshController() { + return { + timeToLive: null, + nextPreferredContentRefreshDate: null, + refreshWhileVisibleForNextPreferredContentRefreshDate: false, + }; +} +/** + * Adds a preferredContentRefreshDate to the refreshController if it's sooner than the currently known nextPreferredContentRefreshDate. + * Provided date must be in the future. + * @param nextPreferredContentRefreshDate the new date to check and add. + * @param refreshController the refreshController to add the new date to + */ +export function addNextPreferredContentRefreshDate(newPreferredContentRefreshDate, refreshController) { + if (serverData.isNull(refreshController) || serverData.isNull(newPreferredContentRefreshDate)) { + return; + } + if ((serverData.isNull(refreshController.nextPreferredContentRefreshDate) || + newPreferredContentRefreshDate.getTime() < refreshController.nextPreferredContentRefreshDate.getTime()) && + newPreferredContentRefreshDate.getTime() > new Date().getTime()) { + refreshController.nextPreferredContentRefreshDate = newPreferredContentRefreshDate; + } +} +/** + * Generates a `PageRefreshPolicy` that can be attached to a page to define the refresh logic of that page. + * @param refreshController the object that has collected data to create a suitable refresh policy + * @returns the refresh policy, if a content TTL or nextPreferredContentRefreshDate has been provided. + */ +export function pageRefreshPolicyForController(objectGraph, refreshController) { + if (!isAutomaticPageRefreshEnabled(objectGraph)) { + return null; + } + if (serverData.isNull(refreshController)) { + return null; + } + let date; + let usingNextPreferredContentRefreshDate = false; + if (serverData.isDefinedNonNull(refreshController.timeToLive) && + serverData.isDefinedNonNull(refreshController.nextPreferredContentRefreshDate)) { + const timeToLiveDate = dateByAddingTimeToNow(refreshController.timeToLive); + if (timeToLiveDate.getTime() < refreshController.nextPreferredContentRefreshDate.getTime() && + timeToLiveDate.getTime() > new Date().getTime()) { + date = timeToLiveDate; + } + else { + date = refreshController.nextPreferredContentRefreshDate; + usingNextPreferredContentRefreshDate = true; + } + } + else if (serverData.isDefinedNonNull(refreshController.timeToLive)) { + date = dateByAddingTimeToNow(refreshController.timeToLive); + } + else if (serverData.isDefinedNonNull(refreshController.nextPreferredContentRefreshDate)) { + date = refreshController.nextPreferredContentRefreshDate; + usingNextPreferredContentRefreshDate = true; + } + else { + return null; + } + if (date.getTime() <= new Date().getTime()) { + return null; + } + const refreshWhileVisible = usingNextPreferredContentRefreshDate && refreshController.refreshWhileVisibleForNextPreferredContentRefreshDate; + return new models.PageRefreshPolicy("timeToLive", 0.0, null, date, refreshWhileVisible); +} +/** + * Returns the nextPreferredContentRefreshDate for the provided refresh controller. + * Primarily used when fetching individual shelves so we can attach a refresh date, rather + * than a `PageRefreshPolicy`, which sits up at a page level. + * @param refreshController the object that has collected refresh data. + * @returns the nextPreferredContentRefreshDate, if it has been provided. + */ +export function nextPreferredContentRefreshDateForController(refreshController) { + return refreshController === null || refreshController === void 0 ? void 0 : refreshController.nextPreferredContentRefreshDate; +} +/** + * Returns the time to live value from the response data. + * + * @param dataContainer The response data container from which to extract the content TTL + * @returns a content TTL, if any, or null. + */ +function timeToLiveFromResponse(response) { + return response[ResponseMetadata.contentMaxAge]; +} +/** + * Returns a new date, adjusted by the number of seconds. + * + * @param seconds The number of seconds to add to now + * @returns The adjusted Date + */ +function dateByAddingTimeToNow(seconds) { + const date = new Date(); + date.setSeconds(date.getSeconds() + seconds); + return date; +} +/** + * Returns whether refreshing pages automatically is enabled in the bag. + */ +function isAutomaticPageRefreshEnabled(objectGraph) { + return objectGraph.bag.enableAutomaticPageRefresh; +} +//# sourceMappingURL=page-refresh-controller.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/room/room-common.js b/node_modules/@jet-app/app-store/tmp/src/common/room/room-common.js new file mode 100644 index 0000000..d1ab4a0 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/room/room-common.js @@ -0,0 +1,139 @@ +import { makeRoomPageIntent } from "../../api/intents/room-page-intent"; +import * as models from "../../api/models"; +import * as mediaDataFetching from "../../foundation/media/data-fetching"; +import { AbstractMediaApiPageBuilder } from "../builders/abstract-media-api-page-builder"; +import * as pagination from "../builders/pagination"; +import * as metricsHelpersLocation from "../metrics/helpers/location"; +import * as metricsHelpersPage from "../metrics/helpers/page"; +import { generateRoutes } from "../util/generate-routes"; +import { defaultAttributesForRoomRequest } from "./room-request"; +import { withAsyncValidationContext } from "../../foundation/util/validation-util"; +import { defaultRoomShelfContentType, roomPageWithContent } from "./room-page"; +export class RoomPageToken { + constructor() { + /// The store platform data profile to use. + this.profile = "lockup"; + /// The maximum number of ids to fetch per load more request. + this.maxPerPage = pagination.suggestedMaxPerPage; + } +} +export class AbstractRoomBuilder extends AbstractMediaApiPageBuilder { + // + // Begin Region: Common + // + defaultAttributes(objectGraph) { + return defaultAttributesForRoomRequest(objectGraph); + } + defaultPlatforms(objectGraph) { + return mediaDataFetching.defaultAdditionalPlatformsForClient(objectGraph); + } + pageType() { + return "page"; + } + // + // Begin Region: Load More + // + generatePaginationRequest(objectGraph, url, parameters, token) { + const pageToken = token; + const mediaApiRequest = new mediaDataFetching.Request(objectGraph, pageToken.remainingContent); + return mediaApiRequest; + } + async renderPaginatedPage(objectGraph, data, token) { + const pageToken = token; + return await this.pageWithContent(objectGraph, data, pageToken); + } + // + // Begin Region: Internal API + // + /** + * Generates a room page given a list of lockups + * + * @param {DataContainer} roomContents + * @param {RoomPageToken} token The token representing the content to load + * @param {boolean} shouldUpdateRemainingContent Whether to update the remaining content in the token, + * false when being handled outside of this method + * @returns {GenericPage} + */ + async pageWithContent(objectGraph, roomContents, token, shouldUpdateRemainingContent = true) { + return await withAsyncValidationContext("pageWithContent", async () => { + return roomPageWithContent(objectGraph, roomContents, token, shouldUpdateRemainingContent); + }); + } +} +/** + * Creates a page that can be used for side-packing see all pages into a room. + * + * @param objectGraph + * @param {string} title The title of the destination page + * @param {ShelfContentType} preferredShelfContentType The content type to use for the page + * @returns {GenericPage} A GenericPage which will use the parentShelfItems from the see all to render the initial room + */ +export function seeAllPage(objectGraph, title, preferredShelfContentType) { + const shelf = new models.Shelf(preferredShelfContentType || defaultRoomShelfContentType); + shelf.isHorizontal = false; + shelf.items = "parentShelfItems"; + const page = new models.GenericPage([shelf]); + page.isIncomplete = true; + page.title = title; + if (platformPrefersLargeTitles(objectGraph)) { + page.presentationOptions = ["prefersLargeTitle"]; + } + return page; +} +export function platformPrefersLargeTitles(objectGraph) { + return objectGraph.client.isWatch || objectGraph.client.isMac; +} +export class AbstractActionRoomBuilder extends AbstractRoomBuilder { + async handlePage(objectGraph, url, parameters, matchedRuleIdentifier, referrerData, isIncomingURL) { + return this.action(); + } + generatePageRequest(objectGraph, url, parameters) { + throw new Error(`generatePageRequest is not supported on: ${this.builderClass}`); + } + async renderPage(objectGraph, data, parameters, additionalPageRequirements) { + throw new Error(`renderPage is not supported on: ${this.builderClass}`); + } +} +export class AbstractFilteredRoom extends AbstractRoomBuilder { + requestWithFilter(objectGraph, type, value) { + const mediaApiRequest = new mediaDataFetching.Request(objectGraph) + .forType("apps") + .includingMacOSCompatibleIOSAppsWhenSupported(true) + .withFilter(type, value); + return mediaApiRequest; + } + async renderPage(objectGraph, data, parameters, additionalPageRequirements) { + return await withAsyncValidationContext("renderPage", async () => { + // Create the token + const token = new RoomPageToken(); + token.url = this.paginationUrl; + token.metricsPageInformation = this.pageInformation(objectGraph, data, parameters); + token.shouldFilter = false; + token.metricsLocationTracker = metricsHelpersLocation.newLocationTracker(); + const page = await this.pageWithContent(objectGraph, data, token); + metricsHelpersPage.addMetricsEventsToPageWithInformation(objectGraph, page, token.metricsPageInformation); + return page; + }); + } +} +/// MARK: `RoomPageIntentController` Routing +const { routes: roomPageRoutesWithoutPlatform, makeCanonicalUrl: makeCanonicalRoomPageUrlWithoutPlatform } = generateRoutes(makeRoomPageIntent, "/room/{id}"); +const { routes: roomPageRoutesWithPlatform, makeCanonicalUrl: makeCanonicalRoomPageUrlWithPlatform } = generateRoutes(makeRoomPageIntent, "/{platform}/room/{id}"); +/** + * Define the `RouteProvider` routes for the `RoomPageIntentController` + */ +export function roomPageRoutes(objectGraph) { + return [...roomPageRoutesWithoutPlatform(objectGraph), ...roomPageRoutesWithPlatform(objectGraph)]; +} +/** + * Generate the URL used by the "web" client to route to a {@linkcode RoomPageIntent} + */ +export function makeCanonicalRoomPageUrl(objectGraph, intent) { + if ("platform" in intent) { + return makeCanonicalRoomPageUrlWithPlatform(objectGraph, intent); + } + else { + return makeCanonicalRoomPageUrlWithoutPlatform(objectGraph, intent); + } +} +//# sourceMappingURL=room-common.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/room/room-page.js b/node_modules/@jet-app/app-store/tmp/src/common/room/room-page.js new file mode 100644 index 0000000..a54ca0e --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/room/room-page.js @@ -0,0 +1,105 @@ +import { unwrapOptional as unwrap } from "@jet/environment/types/optional"; +import { GenericPage, Shelf } from "../../api/models"; +import { attributeAsBooleanOrFalse, attributeAsString } from "../../foundation/media/attributes"; +import { metricsFromMediaApiObject, dataCollectionFromDataContainer, dataFromDataContainer, } from "../../foundation/media/data-structure"; +import { relationship } from "../../foundation/media/relationships"; +import { addMetricsEventsToPageWithInformation, metricsPageInformationFromMediaApiResponse, } from "../metrics/helpers/page"; +import { addImpressionFields } from "../metrics/helpers/impressions"; +import { pushContentLocation, newLocationTracker } from "../metrics/helpers/location"; +import { lockupsFromData } from "../lockups/lockups"; +import { clientIdentifierForEditorialContextInData } from "../lockups/editorial-context"; +import { artworkUseCaseFromShelfStyle } from "../content/content"; +import { nextDataRange } from "../builders/pagination"; +import { platformPrefersLargeTitles, RoomPageToken } from "./room-common"; +export const defaultRoomShelfContentType = "mediumLockup"; +/** + * Provides the shelf content type for featured content kinds that require a particular shelf content type that differs + * from the default. + * + * @param fcKind The featured content ID. + * @returns {ShelfContentType} The content type for the shelf. + */ +export function preferredShelfStyleForFcKind(fcKind) { + if (fcKind === null || fcKind === undefined) { + return null; + } + switch (fcKind) { + case 431 /* FeaturedContentID.AppStore_iAPShelf */: + return "inAppPurchaseTiledLockup"; + default: + return null; + } +} +export function renderRoomPage(objectGraph, data) { + const roomData = unwrap(dataFromDataContainer(objectGraph, data)); + const roomContents = unwrap(relationship(roomData, "contents")); + const pageInformation = metricsPageInformationFromMediaApiResponse(objectGraph, "Room", roomData.id, data); + // Create the token + const token = new RoomPageToken(); + token.remainingContent = roomContents.data; + token.shouldFilter = !attributeAsBooleanOrFalse(roomData, "doNotFilter"); + token.metricsPageInformation = pageInformation; + token.metricsLocationTracker = newLocationTracker(); + const fcKindString = attributeAsString(roomData, "editorialElementKind"); + token.preferredShelfContentType = preferredShelfStyleForFcKind(Number(fcKindString)); + token.clientIdentifierOverride = clientIdentifierForEditorialContextInData(objectGraph, roomData); + // Render the room + token.title = unwrap(attributeAsString(roomData, "title")); + const page = roomPageWithContent(objectGraph, roomContents, token); + page.title = token.title; + if (platformPrefersLargeTitles(objectGraph)) { + page.presentationOptions = ["prefersLargeTitle"]; + } + addMetricsEventsToPageWithInformation(objectGraph, page, pageInformation); + return page; +} +/** + * Shared rendering logic for all kinds of "room" pages + */ +export function roomPageWithContent(objectGraph, roomContents, token, shouldUpdateRemainingContent = true) { + var _a; + const shelfStyle = token.preferredShelfContentType || defaultRoomShelfContentType; + const shelf = new Shelf(shelfStyle); + const shelfMetricsOptions = { + id: null, + kind: null, + softwareType: null, + targetType: "swoosh", + title: token.title, + pageInformation: token.metricsPageInformation, + locationTracker: token.metricsLocationTracker, + idType: "its_contentId", + recoMetricsData: (_a = metricsFromMediaApiObject(roomContents)) !== null && _a !== void 0 ? _a : {}, + }; + /// add impression fields + addImpressionFields(objectGraph, shelf, shelfMetricsOptions); + pushContentLocation(objectGraph, shelfMetricsOptions, token.title); + shelf.isHorizontal = false; + shelf.shouldFilterApps = token.shouldFilter; + const contents = dataCollectionFromDataContainer(roomContents); + if (shouldUpdateRemainingContent) { + token.remainingContent = []; + } + shelf.items = lockupsFromData(objectGraph, contents, { + contentUnavailable: (index, _dataItem) => { + if (shouldUpdateRemainingContent) { + token.remainingContent = nextDataRange(objectGraph, contents, index); + } + return true; + }, + lockupOptions: { + metricsOptions: { + pageInformation: token.metricsPageInformation, + locationTracker: token.metricsLocationTracker, + }, + clientIdentifierOverride: token.clientIdentifierOverride, + artworkUseCase: artworkUseCaseFromShelfStyle(objectGraph, shelfStyle), + }, + }); + const page = new GenericPage([shelf]); + if (token.remainingContent.length) { + page.nextPage = token; + } + return page; +} +//# sourceMappingURL=room-page.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/room/room-request.js b/node_modules/@jet-app/app-store/tmp/src/common/room/room-request.js new file mode 100644 index 0000000..ba0337b --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/room/room-request.js @@ -0,0 +1,48 @@ +import { Request, defaultAdditionalPlatformsForClient } from "../../foundation/media/data-fetching"; +import { shouldFetchCustomAttributes } from "../product-page/product-page-variants"; +import { shouldUsePrerenderedIconArtwork } from "../content/content"; +import { setPreviewPlatform } from "../preview-platform"; +export function createRoomRequest(objectGraph, roomId) { + const mediaApiRequest = new Request(objectGraph) + .withIdOfType(roomId, "rooms") + .includingAgeRestrictions() + .includingMacOSCompatibleIOSAppsWhenSupported(true); + if (objectGraph.client.isWeb) { + mediaApiRequest.includingAdditionalPlatforms(defaultAdditionalPlatformsForClient(objectGraph)); + setPreviewPlatform(objectGraph, mediaApiRequest); + } + return mediaApiRequest; +} +export function prepareRoomRequest(objectGraph, request) { + return request + .includingAdditionalPlatforms(defaultAdditionalPlatformsForClient(objectGraph)) + .includingAttributes(defaultAttributesForRoomRequest(objectGraph)) + .usingCustomAttributes(shouldFetchCustomAttributes(objectGraph)) + .includingAgeRestrictions() + .includingMacOSCompatibleIOSAppsWhenSupported(true); +} +export function defaultAttributesForRoomRequest(objectGraph) { + const attributes = [ + "editorialArtwork", + "editorialVideo", + "isAppleWatchSupported", + "requiredCapabilities", + ]; + if (objectGraph.appleSilicon.isSupportEnabled) { + attributes.push("macRequiredCapabilities"); + } + if (objectGraph.client.isMac) { + attributes.push("hasMacIPAPackage"); + } + if (objectGraph.client.isVision) { + attributes.push("compatibilityControllerRequirement"); + } + if (objectGraph.bag.enableUpdatedAgeRatings) { + attributes.push("ageRating"); + } + if (shouldUsePrerenderedIconArtwork(objectGraph)) { + attributes.push("iconArtwork"); + } + return attributes; +} +//# sourceMappingURL=room-request.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/search/content/search-categories.js b/node_modules/@jet-app/app-store/tmp/src/common/search/content/search-categories.js new file mode 100644 index 0000000..927de7c --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/search/content/search-categories.js @@ -0,0 +1,334 @@ +import { isNothing, isSome } from "@jet/environment"; +import * as models from "../../../api/models"; +import * as serverData from "../../../foundation/json-parsing/server-data"; +import * as mediaAttributes from "../../../foundation/media/attributes"; +import * as mediaRelationships from "../../../foundation/media/relationships"; +import { pageRouter } from "../../builders/routing"; +import { hrefToRoutableUrl } from "../../builders/url-mapping"; +import * as content from "../../content/content"; +import { addClickEventToAction, addClickEventToSeeAllAction } from "../../metrics/helpers/clicks"; +import { addImpressionFields } from "../../metrics/helpers/impressions"; +import * as metricsHelpersLocation from "../../metrics/helpers/location"; +import { addMetricsEventsToPageWithInformation, metricsPageInformationFromMediaApiResponse, } from "../../metrics/helpers/page"; +import { createClickMetricsOptionsForChartOrCategory, createMetricsOptionsForChartOrCategory, } from "../../metrics/helpers/search/search-shelves"; +import { combinedRecoMetricsDataFromMetricsData } from "../../metrics/helpers/util"; +import * as onDevicePersonalization from "../../personalization/on-device-personalization"; +import * as searchShelves from "./search-shelves"; +import * as groupingShelfControllerCommon from "../../grouping/shelf-controllers/grouping-shelf-controller-common"; +/** + * Takes the raw page data and creates a charts and categories page + * @param objectGraph The App Store Object Graph + * @param resultData The data representing the charts and categories page + * @returns A charts and categories page + */ +export function searchChartsAndCategoriesPageFromData(objectGraph, resultData) { + var _a, _b; + /// Get the page's metadata + const pageTitle = mediaAttributes.attributeAsString(resultData, "title"); + const selectedTabId = serverData.asString(resultData, "meta.autoSelectedTabId"); + const sourceShelfCanonicalId = serverData.asString(resultData, "meta.sourceShelfCanonicalId"); + const pageInformation = metricsPageInformationFromMediaApiResponse(objectGraph, "SearchLanding", sourceShelfCanonicalId, resultData); + const onDevicePersonalizationMetricsData = onDevicePersonalization.metricsData(objectGraph); + pageInformation.recoMetricsData = combinedRecoMetricsDataFromMetricsData(pageInformation.recoMetricsData, null, onDevicePersonalizationMetricsData); + /// Generate a new SearchPageContext (mainly for making shelf contexts) + const searchPageContext = { + shelves: [], + metricsLocationTracker: metricsHelpersLocation.newLocationTracker(), + metricsPageInformation: pageInformation, + adStitcher: null, + adIncidentRecorder: null, + pageType: searchShelves.SearchPageType.ChartsAndCategories, + }; + /// Prep the shelves data, mappings, orderings, and page tabs list + const shelvesData = serverData.asArrayOrEmpty(resultData, "data"); + const shelvesMapping = {}; + const shelfOrdering = []; + const pageTabsCollector = []; + const pageTabsLocationTracker = metricsHelpersLocation.newLocationTracker(); + for (const shelfData of shelvesData) { + /// Generate the shelf attributes for the shelf + const shelfAttributes = searchShelves.shelfAttributesFromData(objectGraph, shelfData, models.SearchLandingPageContentKind.CategoriesAndCharts, models.SearchPageKind.CategoriesAndCharts); + /// Since the shelves act as their own page, we need to set a new location tracker for each shelf + const shelfPageContext = { + ...searchPageContext, + metricsLocationTracker: metricsHelpersLocation.newLocationTracker(), + }; + /// Generate the shelf context + const shelfContext = searchShelves.baseShelfContext(objectGraph, shelfData, shelfAttributes, shelfPageContext); + /// Push the shelf content location so each shelf has the correct parent and starting index + metricsHelpersLocation.pushContentLocation(objectGraph, shelfContext.metricsOptions, shelfAttributes.title); + /// Make the shelf + const shelf = createChartsCategoryShelf(objectGraph, shelfData, true, shelfAttributes, shelfPageContext, shelfContext); + metricsHelpersLocation.popLocation(shelfContext.metricsOptions.locationTracker); + if (isNothing(shelf)) { + continue; + } + /// Add the shelf to the shelves mapping for the shelf id + shelvesMapping[shelf.id] = shelf; + /// Add the shelf id to the shelf ordering (to maintain ordered shelves) + shelfOrdering.push(shelf.id); + /// Make a pageTab metadata model from the shelf and action + const pageTab = new models.PageTab(); + const action = new models.PageTabChangeAction(shelfData.id, shelfAttributes.title); + /// Generate the click actions + const pageTabClickActionOptions = { + id: shelfAttributes.title, + canonicalId: (_a = serverData.asString(shelfData.meta, "canonicalId")) !== null && _a !== void 0 ? _a : undefined, + actionType: "navigate", + targetType: "button", + pageInformation: searchPageContext.metricsPageInformation, + locationTracker: pageTabsLocationTracker, + }; + addClickEventToAction(objectGraph, action, pageTabClickActionOptions); + pageTab.action = action; + pageTab.id = shelf.id; + pageTab.title = `${(_b = shelf.title) !== null && _b !== void 0 ? _b : ""}`; /// We need a deep copy of the string as we will remove the reference possibly later. + pageTabsCollector.push(pageTab); + metricsHelpersLocation.nextPosition(pageTabsLocationTracker); + } + /// Return an empty page if there are no real shelves to show + if (!serverData.isDefinedNonNullNonEmpty(shelfOrdering)) { + return new models.SearchChartsAndCategoriesPage(); + } + /// Create the page tabs + const pageTabs = new models.PageTabs(); + /// The id needs to be static but it isn't tied to the payload + pageTabs.id = objectGraph.random.nextUUID(); + /// Add the pageTabs as its own shelf so it can be independent of the chart and category shelves + /// as well as to control the segmented control for swapping between category shelf orderings (e.g. category containers) + const pageTabsShelf = new models.Shelf("pageTabs"); + pageTabsShelf.items = [pageTabs]; + shelvesMapping[pageTabs.id] = pageTabsShelf; + /// Make the page and add the shelf mappings (which includes our pageTabs shelf) + const page = new models.SearchChartsAndCategoriesPage(); + page.shelfMapping = shelvesMapping; + const shelfOrderings = {}; + /** Normally, we would have the shelf orderings be based on some identifier in the payload on a container which contains + an array of shelves. E.g. + { + Container { + containerId: string + shelves: [ + { + shelfId1: string, + ... + }, + ... + ] + } + } + shelf orderings = { containerId: [shelfId1, ...]} + + However, the current protocol has no container, just a loose array of shelves. + { + shelves: [ + { + shelfId1: string, + ... + }, + ... + ] + } + + Therefore, we have to fake our ordering by having the ordering be the shelves' own identifiers. + + shelf orderings = { shelfId1: [shelfId1], shelfId2: [shelfId2], ...} + + We also will append the pageTabs shelf if we have multiple shelves representing multiple tabs on the page. + */ + for (const shelfOrderingElement of shelfOrdering) { + if (pageTabsCollector.length > 1) { + shelfOrderings[shelfOrderingElement] = [pageTabs.id, shelfOrderingElement]; + } + else { + shelfOrderings[shelfOrderingElement] = [shelfOrderingElement]; + } + } + for (const shelf of Object.values(shelvesMapping)) { + shelf.title = undefined; + } + /// Set the data on the page + page.title = pageTitle; + page.pageTabs = pageTabs; + page.columnCount = 2; + page.shelfOrderings = shelfOrderings; + /// If we don't have a reco selected default tab, just default to the first one + page.defaultShelfOrdering = shelfOrdering.includes(selectedTabId) ? selectedTabId : shelfOrdering[0]; + /// Set the data on the page tabs + pageTabs.tabs = pageTabsCollector; + pageTabs.selectedTabId = page.defaultShelfOrdering; + /// Add the page metrics + addMetricsEventsToPageWithInformation(objectGraph, page, searchPageContext.metricsPageInformation); + return page; +} +/** + * Creates a charts and categories shelf from the shelf data + * @param objectGraph The App Store Object Graph + * @param data The shelf data object + * @param isForSeeAllPage Whether or not this shelf will be displayed on the see-all page + * @param shelfAttributes The shelf's attributes + * @param searchPageContext The context for the page this shelf belongs to + * @param searchShelfContext The shelf's context + * @returns A charts and categories shelf + */ +export function createChartsCategoryShelf(objectGraph, data, isForSeeAllPage, shelfAttributes, searchPageContext, searchShelfContext) { + var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l; + const items = []; + const chartsAndCategoriesData = mediaRelationships.relationshipCollection(data, "contents"); + const shelf = new models.Shelf("searchChartsAndCategories"); + shelf.isHorizontal = false; + shelf.id = data.id; + shelf.title = shelfAttributes.title; + shelf.presentationHints = { isWidthConstrained: true }; + if (serverData.isNumber((_a = shelfAttributes.displayStyle) === null || _a === void 0 ? void 0 : _a.layoutSize)) { + shelf.contentsMetadata = { + type: "searchLandingChartsAndCategoriesSection", + numberOfColumns: shelfAttributes.displayStyle.layoutSize, + }; + } + if (shelfAttributes.hasSeeAll) { + const action = new models.FlowAction("searchChartsAndCategories"); + action.pageUrl = shelfAttributes.seeAllLink; + action.title = objectGraph.loc.string("ACTION_SEE_ALL"); + const seeAllMetricsOptions = { + ...searchShelfContext.metricsOptions, + targetType: "button", + }; + addClickEventToSeeAllAction(objectGraph, action, action.pageUrl, seeAllMetricsOptions); + shelf.seeAllAction = action; + } + addImpressionFields(objectGraph, shelf, searchShelfContext.metricsOptions); + const brickUseCase = isForSeeAllPage + ? content.SearchChartOrCategoryBrickUseCase.seeAllPage + : content.SearchChartOrCategoryBrickUseCase.other; + for (const chartOrCategory of chartsAndCategoriesData) { + let name = null; + let badge = null; + if (chartOrCategory.type === "tags") { + const brickName = (_b = mediaAttributes.attributeAsString(chartOrCategory, "name")) !== null && _b !== void 0 ? _b : "tagbrick"; + name = brickName; + } + else { + const editorialNotes = mediaAttributes.attributeAsDictionary(chartOrCategory, "editorialNotes"); + if (serverData.isDefinedNonNullNonEmpty(editorialNotes)) { + name = serverData.asString(editorialNotes, "name"); + badge = serverData.asString(editorialNotes, "badge"); + } + } + const artwork = content.searchChartOrCategoryArtworkFromData(objectGraph, chartOrCategory, brickUseCase, (_c = shelfAttributes === null || shelfAttributes === void 0 ? void 0 : shelfAttributes.displayStyle) === null || _c === void 0 ? void 0 : _c.layoutDensity); + const kind = mediaAttributes.attributeAsString(chartOrCategory, "kind"); + const linkData = mediaAttributes.attributeAsDictionary(chartOrCategory, "link"); + const linkUrl = serverData.asString(linkData, "url"); + const dataCollection = mediaRelationships.relationshipCollection(chartOrCategory, "primary-content"); + let isTitleRequired = true; + let action = null; + if (chartOrCategory.type === "tags") { + const href = serverData.asString(chartOrCategory, "href"); + const url = hrefToRoutableUrl(objectGraph, href); + const flowPage = objectGraph.required(pageRouter).fetchFlowPage(url); + const flowAction = new models.FlowAction(flowPage); + flowAction.pageUrl = url; + flowAction.title = name; + action = flowAction; + } + else if (isSome(linkUrl)) { + switch (kind) { + case "CategoryChart": + const topChartAction = new models.FlowAction("topCharts"); + topChartAction.pageUrl = linkUrl; + topChartAction.title = name; + action = topChartAction; + break; + case "External": + // For external links, we don't require a title we can just show an image + isTitleRequired = false; + const target = serverData.asString(linkData, "target"); + if (target === "external") { + action = new models.ExternalUrlAction(linkUrl); + action.title = name !== null && name !== void 0 ? name : ""; + } + else { + const flowPage = objectGraph.required(pageRouter).fetchFlowPage(linkUrl); + const linkAction = new models.FlowAction(flowPage); + linkAction.pageUrl = linkUrl; + linkAction.title = name !== null && name !== void 0 ? name : ""; + action = linkAction; + } + break; + default: + break; + } + } + else if (serverData.isDefinedNonNullNonEmpty(dataCollection)) { + const chartMetadata = dataCollection[0]; + const chartHref = chartMetadata.href; + const url = hrefToRoutableUrl(objectGraph, chartHref); + if ((url === null || url === void 0 ? void 0 : url.length) > 0) { + const flowAction = new models.FlowAction("page"); + flowAction.pageUrl = url; + flowAction.title = name; + action = flowAction; + } + else { + continue; + } + } + else { + continue; + } + const chartClickOptions = createClickMetricsOptionsForChartOrCategory(objectGraph, (_d = shelfAttributes === null || shelfAttributes === void 0 ? void 0 : shelfAttributes.displayStyle) === null || _d === void 0 ? void 0 : _d.layoutDensity, chartOrCategory, searchShelfContext); + addClickEventToAction(objectGraph, action, chartClickOptions); + if (isTitleRequired && serverData.isNullOrEmpty(name)) { + continue; + } + let chartOrCategoryModel = new models.SearchChartOrCategory(name, artwork, null, null, badge, action, (_e = shelfAttributes === null || shelfAttributes === void 0 ? void 0 : shelfAttributes.displayStyle) === null || _e === void 0 ? void 0 : _e.layoutDensity, artworkSafeArea((_f = shelfAttributes === null || shelfAttributes === void 0 ? void 0 : shelfAttributes.displayStyle) === null || _f === void 0 ? void 0 : _f.layoutDensity), textSafeArea((_g = shelfAttributes === null || shelfAttributes === void 0 ? void 0 : shelfAttributes.displayStyle) === null || _g === void 0 ? void 0 : _g.layoutDensity)); + let chartModelMetricsOptions = createMetricsOptionsForChartOrCategory(objectGraph, chartOrCategoryModel, chartOrCategory, searchShelfContext); + const artworkOptions = { + useCase: 18 /* content.ArtworkUseCase.GroupingBrick */, + }; + const collectionIcons = groupingShelfControllerCommon.artworkForTags(objectGraph, chartOrCategory, 1060, 520, artworkOptions, chartModelMetricsOptions); + if (isSome(collectionIcons) && collectionIcons.length > 0) { + const collectionIconBackgroundColor = collectionIcons[0].backgroundColor; + if (isSome(collectionIconBackgroundColor) && (collectionIconBackgroundColor === null || collectionIconBackgroundColor === void 0 ? void 0 : collectionIconBackgroundColor.type) === "rgb") { + chartOrCategoryModel = new models.SearchChartOrCategory(name, null, collectionIcons, (_h = content.closestTagBackgroundColorForIcon(collectionIconBackgroundColor)) !== null && _h !== void 0 ? _h : undefined, badge, action, (_j = shelfAttributes === null || shelfAttributes === void 0 ? void 0 : shelfAttributes.displayStyle) === null || _j === void 0 ? void 0 : _j.layoutDensity, artworkSafeArea((_k = shelfAttributes === null || shelfAttributes === void 0 ? void 0 : shelfAttributes.displayStyle) === null || _k === void 0 ? void 0 : _k.layoutDensity), textSafeArea((_l = shelfAttributes === null || shelfAttributes === void 0 ? void 0 : shelfAttributes.displayStyle) === null || _l === void 0 ? void 0 : _l.layoutDensity)); + chartModelMetricsOptions = createMetricsOptionsForChartOrCategory(objectGraph, chartOrCategoryModel, chartOrCategory, searchShelfContext); + } + } + addImpressionFields(objectGraph, chartOrCategoryModel, chartModelMetricsOptions); + items.push(chartOrCategoryModel); + metricsHelpersLocation.nextPosition(searchShelfContext.metricsOptions.locationTracker); + } + if (serverData.isNullOrEmpty(items)) { + return null; + } + shelf.items = items; + return shelf; +} +function artworkSafeArea(density) { + switch (density) { + // Tile + case models.GenericSearchPageShelfDisplayStyleDensity.Density1: + return models.ChartOrCategorySafeArea.defaultTileArtworkSafeArea; + // Pill + case models.GenericSearchPageShelfDisplayStyleDensity.Density2: + return models.ChartOrCategorySafeArea.defaultPillArtworkSafeArea; + // Round + case models.GenericSearchPageShelfDisplayStyleDensity.Density3: + return undefined; + default: + return undefined; + } +} +function textSafeArea(density) { + switch (density) { + // Tile + case models.GenericSearchPageShelfDisplayStyleDensity.Density1: + return models.ChartOrCategorySafeArea.defaultTileTextSafeArea; + // Pill + case models.GenericSearchPageShelfDisplayStyleDensity.Density2: + return models.ChartOrCategorySafeArea.defaultPillTextSafeArea; + default: + return undefined; + } +} +//# sourceMappingURL=search-categories.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/search/content/search-content-common.js b/node_modules/@jet-app/app-store/tmp/src/common/search/content/search-content-common.js new file mode 100644 index 0000000..59b2fe5 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/search/content/search-content-common.js @@ -0,0 +1,31 @@ +/** + * Common functions for search results content. + */ +import { isDefinedNonNullNonEmpty } from "../../../foundation/json-parsing/server-data"; +import { attributeAsBooleanOrFalse, attributeAsString } from "../../../foundation/media/attributes"; +// region Editorial Content +/** + * Returns the headline / tagline for Editorial Search Results + * @param resultData Data to determine tagline for. + */ +export function editorialSearchResultTagline(objectGraph, resultData) { + // Flag to disable showing specific headings, e.g. "App of the Day" that may be unnatural in search result + const showLabelInSearch = attributeAsBooleanOrFalse(resultData, "showLabelInSearch"); + if (!showLabelInSearch) { + return null; + } + // <rdar://problem/40468686> LOC: AOTD & GOTD badges in Search result for the Editorial Item Card AOTD & GOTD stories do not show up correctly for JA-JP and TH-TH + // Always use alternative label, if one is provided. + const alternateLabel = attributeAsString(resultData, "alternateLabel"); + if (isDefinedNonNullNonEmpty(alternateLabel)) { + return alternateLabel; // No newline flattening needed for alternativeLabel + } + // Otherwise, fallback to franchise label, if any. + const label = attributeAsString(resultData, "label"); + if (isDefinedNonNullNonEmpty(label)) { + return label.replace(/\n/g, " "); + } + return null; +} +// endregion +//# sourceMappingURL=search-content-common.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/search/content/search-lockup-collection.js b/node_modules/@jet-app/app-store/tmp/src/common/search/content/search-lockup-collection.js new file mode 100644 index 0000000..c5c85e7 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/search/content/search-lockup-collection.js @@ -0,0 +1,171 @@ +import { TodayCardDisplayStyle } from "./../../today/today-types"; +/** + * Builder for SearchLockupCollection. + */ +import { SearchLockupCollection } from "../../../api/models"; +import { isDefinedNonNullNonEmpty, isNullOrEmpty } from "../../../foundation/json-parsing/server-data"; +import { attributeAsArrayOrEmpty, attributeAsString } from "../../../foundation/media/attributes"; +import { relationshipCollection } from "../../../foundation/media/relationships"; +import { createArtworkForResource } from "../../content/artwork/artwork"; +import { notesFromData } from "../../content/content"; +import { currentEditorialCollectionTreatment, } from "../../../foundation/experimentation/search-results-experiments"; +import { actionFromData, lockupsFromData } from "../../lockups/lockups"; +import { addImpressionFields, impressionOptions } from "../../metrics/helpers/impressions"; +import { popLocation, pushContentLocation } from "../../metrics/helpers/location"; +import { cardDisplayStyleFromData } from "../../today/today-card-util"; +import { editorialSearchResultTagline } from "./search-content-common"; +/** + * Whether or not a given result data should be rendered as `SearchLockupCollection` model. + * Semantically, every `collection` should be rendered as a lockup collection, but collections are currently backed by many different types of articles. + * @param resultData Data from results endpoint. + * @param searchResponseMetadata Meta blob from initial search request. + */ +export function resultDataShouldRenderLockupCollection(objectGraph, resultData, searchResponseMetadata) { + // Filter supported platforms + if (!(objectGraph.host.isiOS || objectGraph.host.isVision)) { + return false; + } + if (currentEditorialCollectionTreatment(objectGraph, searchResponseMetadata) !== + 1 /* EditorialCollectionExperimentType.Swoosh */) { + return false; + } + switch (resultData.type) { + case "groupings": + // Groupings can only be rendered if `contentIds` is present + const collectionAdamIds = attributeAsArrayOrEmpty(resultData, "contentIds"); + return isDefinedNonNullNonEmpty(collectionAdamIds); + case "rooms": + case "multirooms": + // Rooms, and multirooms always render as collection. + return true; + case "editorial-items": + // Some subtypes of editorial items render as collection. + const cardDisplayStyle = cardDisplayStyleFromData(resultData); + switch (cardDisplayStyle) { + case TodayCardDisplayStyle.List: + case TodayCardDisplayStyle.NumberedList: + case TodayCardDisplayStyle.Grid: + case TodayCardDisplayStyle.River: + return true; + default: + return false; + } + default: + return false; + } +} +/** + * Create a `SearchLockupCollection` from search results. + * @param resultData Data from results endpoint. + * @param lockupOptions Options for lockups shared throughout search results page. + * @param metricsOptions Metrics context. + */ +export function lockupCollectionFromResultData(objectGraph, resultData, metricsOptions) { + // Text + const heading = lockupCollectionHeadingFromResultData(objectGraph, resultData); + const headingArtwork = lockupCollectionHeadingArtworkFromResultData(objectGraph, resultData); + const title = lockupCollectionTitleFromResultData(objectGraph, resultData); + // Metrics for impression + location + const lockupCollectionMetrics = impressionOptions(objectGraph, resultData, title, { + targetType: "card", + pageInformation: metricsOptions.pageInformation, + locationTracker: metricsOptions.locationTracker, + }); + pushContentLocation(objectGraph, lockupCollectionMetrics, title); + // Click Action ("See All") + const clickMetrics = { + actionType: "navigate", + targetType: "button", + pageInformation: metricsOptions.pageInformation, + locationTracker: metricsOptions.locationTracker, + id: "See All", + idType: "sequential", + }; + const seeAllAction = actionFromData(objectGraph, resultData, clickMetrics, objectGraph.host.clientIdentifier); + seeAllAction.title = objectGraph.loc.string("ACTION_SEE_ALL"); + // Lockups + const listOptions = { + lockupOptions: { + metricsOptions: { + pageInformation: metricsOptions.pageInformation, + locationTracker: metricsOptions.locationTracker, + targetType: "lockup", + }, + skipDefaultClickAction: objectGraph.client.isVision, + artworkUseCase: 8 /* ArtworkUseCase.SearchIcon */, + hideCompatibilityBadge: objectGraph.client.isVision, + }, + filter: 128 /* Filter.UnsupportedPlatform */, + }; + let lockupData = relationshipCollection(resultData, "card-contents"); + if (isNullOrEmpty(lockupData)) { + lockupData = relationshipCollection(resultData, "top-apps"); + } + const items = lockupsFromData(objectGraph, lockupData, listOptions); + popLocation(metricsOptions.locationTracker); + // Lockup Collection + const lockupCollection = new SearchLockupCollection(heading, title, items, seeAllAction, headingArtwork); + addImpressionFields(objectGraph, lockupCollection, lockupCollectionMetrics); + if (isNullOrEmpty(items)) { + return null; + } + return lockupCollection; +} +// endregion +// region Heading +/** + * Returns the title for given editorial results search results data. + * @param resultData The result data to get a title for. + */ +function lockupCollectionTitleFromResultData(objectGraph, resultData) { + const resultType = resultData.type; + switch (resultType) { + case "developers": + return attributeAsString(resultData, "name"); + default: + return notesFromData(objectGraph, resultData, "name"); + } +} +/** + * Returns the heading for given editorial results search results data. + * This builds on `editorialSearchResultTagline` with some additional search-specific logic + * @param resultData The result data to get a heading for. + */ +function lockupCollectionHeadingFromResultData(objectGraph, resultData) { + const resultType = resultData.type; + if (resultType === "developers") { + return objectGraph.loc.string("EDITORIAL_SEARCH_RESULT_TYPE_DEVELOPER_TITLE_CASE"); + } + const editorialTagline = editorialSearchResultTagline(objectGraph, resultData); + if (isDefinedNonNullNonEmpty(editorialTagline)) { + return editorialTagline; + } + if (objectGraph.client.isVision) { + return objectGraph.loc.string("EDITORIAL_SEARCH_RESULT_TYPE_COLLECTION_TITLE_CASE"); + } + else { + return objectGraph.loc.string("Search.EditorialSearchResultType.Heading.Collection"); + } +} +/** + * Returns the heading artwork to use for a given editorial search results data. + * @param resultData The result data to get heading artwork for. + */ +function lockupCollectionHeadingArtworkFromResultData(objectGraph, resultData) { + if (!objectGraph.client.isVision) { + return null; + } + const resultType = resultData.type; + let imageName; + switch (resultType) { + case "developers": + imageName = "person.crop.square"; + break; + default: + imageName = "appstore"; + break; + } + return createArtworkForResource(objectGraph, `systemimage://${imageName}`); +} +// endregion +//# sourceMappingURL=search-lockup-collection.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/search/content/search-results.js b/node_modules/@jet-app/app-store/tmp/src/common/search/content/search-results.js new file mode 100644 index 0000000..5f363b3 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/search/content/search-results.js @@ -0,0 +1,723 @@ +/** + * A builder for all SearchResultsContainers variants except for `SearchLockupCollection`. + * To be cleaned in the future. + */ +import * as validation from "@jet/environment/json/validation"; +import { isSome } from "@jet/environment/types/optional"; +import * as models from "../../../api/models"; +import { EditorialSearchResult } from "../../../api/models"; +import * as serverData from "../../../foundation/json-parsing/server-data"; +import * as mediaAttributes from "../../../foundation/media/attributes"; +import * as mediaRelationship from "../../../foundation/media/relationships"; +import * as errors from "../../../foundation/util/errors"; +import * as client from "../../../foundation/wrappers/client"; +import * as appEvents from "../../app-promotions/app-event"; +import * as appPromotionsCommon from "../../app-promotions/app-promotions-common"; +import { createArtworkForResource } from "../../content/artwork/artwork"; +import * as content from "../../content/content"; +import { editorialDisplayOptionsFromClientParams, extractEditorialClientParams, } from "../../editorial-pages/editorial-data-util"; +import * as filtering from "../../filtering"; +import * as lockups from "../../lockups/lockups"; +import * as metricsHelpersImpressions from "../../metrics/helpers/impressions"; +import * as metricsHelpersUtil from "../../metrics/helpers/util"; +import * as onDevicePersonalization from "../../personalization/on-device-personalization"; +import { defaultTodayCardConfiguration, todayCardFromData } from "../../today/today-card-util"; +import { TodayCardDisplayStyle, TodayParseContext } from "../../today/today-types"; +import * as common from "./search-content-common"; +import * as searchLockupCollection from "./search-lockup-collection"; +export function searchResultFromData(objectGraph, resultData, searchResponseMetadata, personalizationDataContainer, metricsOptions, isNetworkConstrained, searchEntity, searchExperimentsData) { + return validation.context("searchResultFromData", () => { + let searchResult = null; + const resultType = resultData.type; + const standardLockupOptions = { + metricsOptions: { + pageInformation: metricsOptions.pageInformation, + locationTracker: metricsOptions.locationTracker, + targetType: "card", + createUniqueImpressionId: true, + }, + hideZeroRatings: true, + artworkUseCase: 8 /* content.ArtworkUseCase.SearchIcon */, + isNetworkConstrained: isNetworkConstrained, + canDisplayArcadeOfferButton: content.shelfContentTypeCanDisplayArcadeOfferButtons(objectGraph, "mixedMediaLockup"), + clientIdentifierOverride: clientIdentifierOverrideForSearchEntity(objectGraph, searchEntity), + isMultilineTertiaryTitleAllowed: false, + }; + const condensedBehavior = condensedBehaviorForData(objectGraph, resultData, searchExperimentsData); + switch (resultType) { + case "rooms": + case "multirooms": + case "developers": + case "editorial-items": + case "groupings": + if (resultType !== "editorial-items" && resultType !== "developers" && objectGraph.client.isVision) { + return null; + } + const todayCardConfig = defaultTodayCardConfiguration(objectGraph); + todayCardConfig.isSearchContext = true; + const todayCard = todayCardFromData(objectGraph, resultData, todayCardConfig, new TodayParseContext(metricsOptions.pageInformation, metricsOptions.locationTracker)); + if (todayCard && todayCard.media && todayCard.media.kind === "inAppPurchase") { + if (objectGraph.client.isVision) { + return null; + } + const iapMedia = todayCard.media; + const todayCardInAppPurchaseLockup = iapMedia.lockup; + todayCardInAppPurchaseLockup.theme = "dark"; + searchResult = new models.InAppPurchaseSearchResult(todayCardInAppPurchaseLockup); + } + else if (searchLockupCollection.resultDataShouldRenderLockupCollection(objectGraph, resultData, searchResponseMetadata)) { + const lockupCollection = searchLockupCollection.lockupCollectionFromResultData(objectGraph, resultData, metricsOptions); + if (lockupCollection) { + searchResult = lockupCollection; + } + } + else { + const editorialSearchResult = editorialSearchResultFromData(objectGraph, resultData, standardLockupOptions.metricsOptions, condensedBehavior); + if (editorialSearchResult) { + if (editorialSearchResult.title) { + editorialSearchResult.title = editorialSearchResult.title.replace(/\n/g, " "); + } + if (editorialSearchResult instanceof EditorialSearchResult && editorialSearchResult.subtitle) { + editorialSearchResult.subtitle = editorialSearchResult.subtitle.replace(/\n/g, " "); + } + searchResult = editorialSearchResult; + } + } + break; + case "in-apps": + if (objectGraph.client.isVision || objectGraph.client.isWeb) { + return null; + } + // Ensure the parent app data is available before proceeding with building the lockup. + const parentData = lockups.parentDataFromInAppData(objectGraph, resultData); + if (serverData.isNullOrEmpty(parentData)) { + return null; + } + const inAppPurchaseLockup = lockups.inAppPurchaseLockupFromData(objectGraph, resultData, standardLockupOptions); + inAppPurchaseLockup.theme = "dark"; + modifyMetadataBadgeForSearchExperiment(objectGraph, inAppPurchaseLockup, searchExperimentsData); + searchResult = new models.InAppPurchaseSearchResult(inAppPurchaseLockup); + break; + case "apps": + case "app-bundles": + default: + // There should never be an iad in the non-ad search results, so remove it here + // before creating the lockups. + delete resultData.attributes["iad"]; + if (resultType === "app-bundles") { + if (objectGraph.client.isVision) { + return null; + } + standardLockupOptions.shouldIncludeScreenshotsForChildren = + objectGraph.featureFlags.isEnabled("voyager_bundles_2025A"); + const bundleLockup = lockups.lockupFromData(objectGraph, resultData, standardLockupOptions); + bundleLockup.showMetadataInformationInLockup = true; + modifyMetadataBadgeForSearchExperiment(objectGraph, bundleLockup, searchExperimentsData); + searchResult = new models.BundleSearchResult(bundleLockup); + } + else { + const lockup = lockups.mixedMediaLockupFromData(objectGraph, resultData, standardLockupOptions, { + canPlayFullScreen: false, + playbackControls: {}, + }, searchExperimentsData); + modifyMetadataBadgeForSearchExperiment(objectGraph, lockup, searchExperimentsData); + // Extract out any associated app event from meta + const appEventSearchResult = appEventSearchResultFromData(objectGraph, resultData, lockup, standardLockupOptions, personalizationDataContainer, metricsOptions); + if (serverData.isDefinedNonNull(appEventSearchResult)) { + searchResult = appEventSearchResult; + searchResult.condensedBehavior = "never"; + } + else { + // If no app event in meta, fallback to a regular mixed media lockup + searchResult = new models.AppSearchResult(lockup); + } + } + break; + } + if (serverData.isDefinedNonNull(searchResult) && serverData.isNull(searchResult.condensedBehavior)) { + searchResult.condensedBehavior = condensedBehavior; + } + return searchResult; + }); +} +/** + * Indicates whether a search result will display an app event + * @param objectGraph The object graph + * @param searchResultData The data for the search result + * @param installStates A mapping of adamIDs to their respective install states that is used to determine if the app is currently installed by the user + * @param appStates A mapping of adamIDs to their respective app states that is used to determine if the app has been installed by the user in the past + * @param metricsOptions Metrics options for built lockups + * @param personalizationDataContainer Personalization criteria used for sorting and filtering app events + * @returns a boolean result + */ +export function searchResultWillUseAppEventDisplay(objectGraph, searchResultData, installStates, appStates, metricsOptions, personalizationDataContainer) { + const appEventEligibleToDisplay = searchResultIsEligibleToDisplayAppEvent(objectGraph, searchResultData); + if (!appEventEligibleToDisplay) { + return false; + } + const { dataItems } = selectedAppEventDataItems(objectGraph, searchResultData, personalizationDataContainer); + let appEvent; + for (const appEventDataItem of dataItems) { + appEvent = sanitizedAppEvent(objectGraph, appEventDataItem, searchResultData, metricsOptions, undefined, undefined); + if (serverData.isDefinedNonNull(appEvent)) { + break; + } + } + const appIsInstalled = serverData.isDefinedNonNull(installStates) && serverData.isDefinedNonNull(installStates[searchResultData.id]) + ? installStates[searchResultData.id] + : false; + const appHasBeenInstalled = serverData.isDefinedNonNull(appStates) && serverData.isDefinedNonNull(appStates[searchResultData.id]) + ? ["downloadable"].includes(appStates[searchResultData.id]) + : false; + // App Events are only displayed in native when the user has installed the app previously and there is a valid app event + return (appIsInstalled || appHasBeenInstalled) && serverData.isDefinedNonNull(appEvent); +} +/** + * Derive the condensed behavior from a search result + * @param objectGraph The global object graph instance + * @param resultData The data blob for this specific search result + * @param pageSearchExperimentsData The page level search experiments data object + */ +function condensedBehaviorForData(objectGraph, resultData, pageSearchExperimentsData) { + var _a, _b; + /// Guard early against incompatible client devices + if (!canHaveCondensedBehaviorForClient(objectGraph)) { + return "never"; + } + const itemSearchExperimentData = resultData.meta; + const itemCondensedStyle = (_a = itemSearchExperimentData === null || itemSearchExperimentData === void 0 ? void 0 : itemSearchExperimentData.displayStyle) === null || _a === void 0 ? void 0 : _a.condensed; + if (serverData.isDefinedNonNull(itemCondensedStyle)) { + return condensedBehaviorFromStyle(objectGraph, itemCondensedStyle); + } + const pageCondensedStyle = (_b = pageSearchExperimentsData === null || pageSearchExperimentsData === void 0 ? void 0 : pageSearchExperimentsData.displayStyle) === null || _b === void 0 ? void 0 : _b.condensed; + if (serverData.isDefinedNonNull(pageCondensedStyle)) { + return condensedBehaviorFromStyle(objectGraph, pageCondensedStyle); + } + return defaultCondensedBehaviorForClient(objectGraph); +} +/** + * Gets the condensed behavior from a given condensed style, or a default behavior based on the client type + * @param objectGraph The global object graph instance + * @param condensedStyle The condensed style on the data model + * @returns The condensed behavior for the native search result view + */ +function condensedBehaviorFromStyle(objectGraph, condensedStyle) { + switch (condensedStyle) { + case "always": + return "always"; + case "never": + return "never"; + case "when-installed": + return "whenInstalled"; + default: + return defaultCondensedBehaviorForClient(objectGraph); + } +} +/** + * Determines the default condensed behavior based for a given client type + * @param objectGraph The global object graph instance + * @param condensedStyle The condensed style on the data model + * @returns The condensed behavior for the native search result view + */ +function defaultCondensedBehaviorForClient(objectGraph) { + switch (objectGraph.client.deviceType) { + case "phone": + return "whenInstalled"; + default: + return "never"; + } +} +/** + * Determines if the current client type supports condensed behavior + * @param objectGraph The global object graph instance + * @returns Whether condensed behavior is allowed + */ +function canHaveCondensedBehaviorForClient(objectGraph) { + switch (objectGraph.client.deviceType) { + case "phone": + return true; + default: + return false; + } +} +/** + * Modifies the editor's choice badge based on whether the search experiment overrides the metadataPrecendence + * for the badge + * @param objectGraph App Store ObjectGraph + * @param lockup The lockup we need to modify for the experiment + * @param searchExperimentData The experiment data to pull the metadata info from + */ +function modifyMetadataBadgeForSearchExperiment(objectGraph, lockup, searchExperimentData) { + const shouldShowEditorsChoice = metadataPrecedenceTypePreceedsType(objectGraph, searchExperimentData, "editorialBadgeInfo", "userRating"); + if (serverData.isDefinedNonNull(shouldShowEditorsChoice)) { + lockup.isEditorsChoice = lockup.isEditorsChoice && shouldShowEditorsChoice; + } +} +/** + * Determines whether the first metadata type should precede the second type for the given experiment data + * @param objectGraph App Store ObjectGraph + * @param experimentData The experiment data to pull the metadata info from + * @param firstType Does this precede the second type in the experiment order + * @param secondType Does this succeed the second type in the experiment order + * @returns whether the first type precedes the second type + */ +function metadataPrecedenceTypePreceedsType(objectGraph, experimentData, firstType, secondType) { + var _a; + if (serverData.isNull(experimentData) || !objectGraph.client.isPhone) { + return null; + } + const order = (_a = experimentData === null || experimentData === void 0 ? void 0 : experimentData.displayStyle) === null || _a === void 0 ? void 0 : _a.metadataPrecedenceOrder; + if (!serverData.isDefinedNonNullNonEmpty(order)) { + return null; + } + const firstIndex = order.indexOf(firstType); + const secondIndex = order.indexOf(secondType); + if (firstIndex === -1 && secondIndex === -1) { + return null; + } + if (firstIndex === -1) { + return false; + } + if (secondIndex === -1) { + return true; + } + return firstIndex < secondIndex; +} +function editorialSearchResultFromData(objectGraph, resultData, metricsOptions, resultCondensedBehavior) { + return validation.context("editorialSearchResultFromData", () => { + let searchResult; + const title = mediaAttributes.attributeAsString(resultData, "name"); + const resultType = resultData.type; + switch (resultType) { + case "groupings": { + const editorialSearchResult = new models.EditorialSearchResult(title); + const collectionAdamIds = mediaAttributes.attributeAsArrayOrEmpty(resultData, "contentIds"); + if (serverData.isDefinedNonNullNonEmpty(collectionAdamIds)) { + editorialSearchResult.collectionAdamIds = collectionAdamIds; + } + else { + const iconArtwork = content.artworkFromApiArtwork(objectGraph, mediaAttributes.attributeAsDictionary(resultData, "artwork"), { + useCase: 9 /* content.ArtworkUseCase.SearchEditorialResult */, + allowingTransparency: true, + }); + editorialSearchResult.iconArtwork = iconArtwork; + } + editorialSearchResult.type = "category"; + searchResult = editorialSearchResult; + break; + } + case "rooms": + case "multirooms": { + const editorialSearchResult = new models.EditorialSearchResult(title); + editorialSearchResult.artwork = content.artworkFromApiArtwork(objectGraph, mediaAttributes.attributeAsDictionary(resultData, "artwork"), { + useCase: 9 /* content.ArtworkUseCase.SearchEditorialResult */, + cropCode: "sr", + }); + editorialSearchResult.collectionAdamIds = mediaAttributes.attributeAsArrayOrEmpty(resultData, "contentIds"); + editorialSearchResult.type = "collection"; + searchResult = editorialSearchResult; + break; + } + case "editorial-items": { + if (objectGraph.bag.searchFilterEditorialItemIds.has(resultData.id)) { + return null; + } + // Bridge objects to Today builder. + const todayParseContext = new TodayParseContext(metricsOptions.pageInformation, metricsOptions.locationTracker); + const shouldResultBeCondensed = resultCondensedBehavior === "always"; + const editorialSearchResult = editorialSearchResultFromTodayCardData(objectGraph, resultData, todayParseContext, shouldResultBeCondensed); + searchResult = editorialSearchResult; + break; + } + case "developers": { + const editorialSearchResult = new models.EditorialSearchResult(title); + editorialSearchResult.artwork = content.artworkFromApiArtwork(objectGraph, mediaAttributes.attributeAsDictionary(resultData, "editorialArtwork.bannerUber"), { + useCase: 9 /* content.ArtworkUseCase.SearchEditorialResult */, + cropCode: "sr", + }); + editorialSearchResult.type = "developer"; + if (isSome(editorialSearchResult.artwork)) { + searchResult = editorialSearchResult; + } + else { + let topApps = mediaRelationship.relationshipCollection(resultData, "top-apps"); + topApps = topApps.filter((topApp) => { + return !filtering.shouldFilter(objectGraph, topApp, 76532 /* filtering.Filter.DeveloperPage */); + }); + const topAppIds = []; + const topAppArtwork = []; + topApps.forEach((app) => { + topAppIds.push(app.id); + const appIcon = content.iconFromData(objectGraph, app, { + useCase: 9 /* content.ArtworkUseCase.SearchEditorialResult */, + }); + if (serverData.isDefinedNonNull(appIcon)) { + topAppArtwork.push(appIcon); + } + }); + editorialSearchResult.collectionAdamIds = topAppIds; + editorialSearchResult.collectionAppIcons = topAppArtwork; + // On visionOS, the developer result falls back to a lockup collection if no art is available. + if (objectGraph.client.isVision) { + const lockupCollection = searchLockupCollection.lockupCollectionFromResultData(objectGraph, resultData, metricsOptions); + searchResult = lockupCollection; + } + else { + searchResult = editorialSearchResult; + } + } + break; + } + default: + break; + } + if (serverData.isNull(searchResult)) { + return null; + } + if (searchResult instanceof EditorialSearchResult) { + if (searchResult.collectionAdamIds != null && searchResult.collectionAdamIds.length) { + const lockupCount = searchResult.collectionAdamIds.length; + if (lockupCount <= 5) { + searchResult.artworkGridType = "extraLarge"; + } + else if (lockupCount <= 8) { + searchResult.artworkGridType = "large"; + } + else if (lockupCount <= 16) { + searchResult.artworkGridType = "mixed"; + } + else { + searchResult.artworkGridType = "small"; + } + } + if (objectGraph.client.isVision) { + let badgeText; + let badgeArtwork = createArtworkForResource(objectGraph, "systemimage://appstore"); + switch (searchResult.type) { + case "developer": + badgeText = objectGraph.loc.string("EDITORIAL_SEARCH_RESULT_TYPE_DEVELOPER_TITLE_CASE"); + badgeArtwork = createArtworkForResource(objectGraph, "systemimage://person.crop.square"); + break; + case "category": + badgeText = objectGraph.loc.string("EDITORIAL_SEARCH_RESULT_TYPE_CATEGORY_TITLE_CASE"); + break; + case "collection": + badgeText = objectGraph.loc.string("EDITORIAL_SEARCH_RESULT_TYPE_COLLECTION_TITLE_CASE"); + break; + case "story": + badgeText = objectGraph.loc.string("EDITORIAL_SEARCH_RESULT_TYPE_STORY_TITLE_CASE"); + break; + default: + break; + } + searchResult.badgeText = badgeText; + searchResult.badgeArtwork = badgeArtwork; + } + } + const impressionOptions = metricsHelpersImpressions.impressionOptions(objectGraph, resultData, searchResult.title, metricsOptions); + searchResult.clickAction = lockups.actionFromData(objectGraph, resultData, impressionOptions, null); + metricsHelpersImpressions.addImpressionFields(objectGraph, searchResult, impressionOptions); + return searchResult; + }); +} +function editorialSearchResultFromTodayCardData(objectGraph, cardData, todayParseContext, shouldResultBeCondensed) { + // Card configuration to use for building TodayCards that will be converted into editorial search results. + const cardConfig = defaultTodayCardConfiguration(objectGraph); + cardConfig.isSearchContext = true; + if (!objectGraph.client.isVision) { + cardConfig.prevailingCropCodes = + shouldResultBeCondensed && objectGraph.client.isPhone + ? { defaultCrop: "DMGE.AppST01" } + : { defaultCrop: "fo" }; + } + // If we are on visionOS, drop any EIs that have a not-compatible app as the primary content. + if (objectGraph.client.isVision) { + const primaryContent = content.primaryContentForData(objectGraph, cardData); + if (isSome(primaryContent)) { + const runnableOnDevice = content.runnableOnDeviceWithData(objectGraph, primaryContent, objectGraph.client.deviceType, objectGraph.appleSilicon.isSupportEnabled); + if (!runnableOnDevice) { + return null; + } + } + } + const todayCard = todayCardFromData(objectGraph, cardData, cardConfig, todayParseContext); + if (!todayCard) { + return null; + } + const editorialSearchResult = new models.EditorialSearchResult(todayCard.title); + editorialSearchResult.type = "story"; + editorialSearchResult.clickAction = todayCard.clickAction; + let collectionLockups = null; + if (todayCard.media) { + switch (todayCard.media.kind) { + case "brandedSingleApp": + const mediaSingleApp = todayCard.media; + editorialSearchResult.artwork = mediaSingleApp.artworks[0]; + if (serverData.isNull(editorialSearchResult.artwork)) { + editorialSearchResult.iconArtwork = mediaSingleApp.icon; + } + const cardDisplayStyle = mediaAttributes.attributeAsString(cardData, "cardDisplayStyle"); + switch (cardDisplayStyle) { + case TodayCardDisplayStyle.AppOfTheDay: + case TodayCardDisplayStyle.GameOfTheDay: + const relatedContentData = mediaRelationship.relationshipData(objectGraph, cardData, "card-contents"); + if (relatedContentData) { + editorialSearchResult.title = + mediaAttributes.attributeAsString(relatedContentData, "name") || + editorialSearchResult.title; + } + break; + default: + break; + } + break; + case "list": + const mediaList = todayCard.media; + collectionLockups = mediaList.lockups; + break; + case "river": + const mediaRiver = todayCard.media; + collectionLockups = mediaRiver.lockups; + break; + case "artwork": + const mediaArtwork = todayCard.media; + editorialSearchResult.artwork = mediaArtwork.artworks[0]; + break; + case "grid": + const mediaGrid = todayCard.media; + collectionLockups = mediaGrid.lockups; + break; + case "multiApp": + const multiApp = todayCard.media; + collectionLockups = multiApp.lockups; + break; + case "video": + const mediaVideo = todayCard.media; + editorialSearchResult.artwork = mediaVideo.videos[0].preview; + editorialSearchResult.video = mediaVideo.videos[0]; + if (todayCard.overlay instanceof models.TodayCardThreeLineOverlay) { + const overlay = todayCard.overlay; + editorialSearchResult.title = overlay.title; + editorialSearchResult.subtitle = overlay.description; + } + else { + editorialSearchResult.subtitle = mediaVideo.description; + } + break; + case "appEvent": + const media = todayCard.media; + editorialSearchResult.artwork = media.artworks[0]; + editorialSearchResult.appEventFormattedDates = media.formattedDates; + editorialSearchResult.subtitle = todayCard.inlineDescription; + editorialSearchResult.tintColor = media.tintColor; + editorialSearchResult.type = "appEventStory"; + if (serverData.isDefinedNonNull(todayCard.style)) { + switch (todayCard.style) { + case "light": + case "white": + editorialSearchResult.mediaOverlayStyle = "light"; + break; + case "dark": + editorialSearchResult.mediaOverlayStyle = "dark"; + break; + default: + errors.unreachable(todayCard.style); + break; + } + } + break; + default: + break; + } + } + if (todayCard.overlay) { + switch (todayCard.overlay.kind) { + case "lockup": + const cardOverlayLockup = todayCard.overlay; + if (!editorialSearchResult.artwork || objectGraph.client.isVision) { + collectionLockups = [cardOverlayLockup.lockup]; + } + break; + case "lockupList": + const cardOverlayList = todayCard.overlay; + collectionLockups = cardOverlayList.lockups; + break; + case "paragraph": + const cardOverlayParagraph = todayCard.overlay; + editorialSearchResult.subtitle = cardOverlayParagraph.paragraph.text; + break; + default: + break; + } + } + if (serverData.isDefinedNonNull(collectionLockups)) { + editorialSearchResult.collectionAdamIds = []; + editorialSearchResult.collectionAppIcons = []; + for (const lockup of collectionLockups) { + editorialSearchResult.collectionAdamIds.push(lockup.adamId); + editorialSearchResult.collectionAppIcons.push(lockup.icon); + } + if (collectionLockups.length === 1) { + editorialSearchResult.lockup = collectionLockups[0]; + } + } + const editorialClientParams = extractEditorialClientParams(objectGraph, cardData); + editorialSearchResult.editorialDisplayOptions = editorialDisplayOptionsFromClientParams(editorialClientParams); + /** + * Editorial tagline + */ + const storyTagline = common.editorialSearchResultTagline(objectGraph, cardData); + if ((storyTagline === null || storyTagline === void 0 ? void 0 : storyTagline.length) > 0 && storyTagline !== editorialSearchResult.title) { + editorialSearchResult.tagline = storyTagline; + } + const heroMedia = todayCard.heroMedia; + if (serverData.isDefinedNonNullNonEmpty(heroMedia)) { + if (serverData.isDefinedNonNullNonEmpty(heroMedia.artworks[0])) { + editorialSearchResult.artwork = heroMedia.artworks[0]; + editorialSearchResult.artwork.crop = "em"; + } + else if (serverData.isDefinedNonNullNonEmpty(heroMedia.videos[0])) { + editorialSearchResult.video = heroMedia.videos[0]; + } + } + if (editorialSearchResult.video) { + editorialSearchResult.video.canPlayFullScreen = false; + editorialSearchResult.video.playbackControls = {}; + } + if (!editorialSearchResult.collectionAdamIds && + !editorialSearchResult.artwork && + !editorialSearchResult.iconArtwork) { + return null; + } + return editorialSearchResult; +} +/// Gets the client identifier (or null) that should be used when building lockups for a given search entity. +function clientIdentifierOverrideForSearchEntity(objectGraph, searchEntity) { + return searchEntity === "watch" ? client.watchIdentifier : null; +} +/** + * Indicates whether a search result meets basic sanity requirements to display app events. This does not include business rules for massaging a valid app event. For that, see `sanitizedAppEvent` function + * @param objectGraph The object graph + * @param searchResultData The data for the search result + * @returns a boolean result + */ +function searchResultIsEligibleToDisplayAppEvent(objectGraph, searchResultData) { + if (!appPromotionsCommon.appEventsAreEnabled(objectGraph) || serverData.isNull(searchResultData.meta)) { + return false; + } + // The first organic can't use CPP data from the ad if it has an app event that will be displayed. + // In this case, it should continue to use DPP assets + const appEventDataItems = serverData.asArrayOrEmpty(searchResultData.meta, "associations.app-events.data"); + const hasInAppEvents = appEventDataItems.length > 0; + // App events can be displayed on most search result types, except for these ones + const typesIneligibleToDisplayAppEvent = [ + "rooms", + "multirooms", + "developers", + "editorial-items", + "groupings", + "in-apps", + "app-bundles", + ]; + const typeIsEligibleToDisplayAppEvent = !typesIneligibleToDisplayAppEvent.includes(searchResultData.type); + return typeIsEligibleToDisplayAppEvent && hasInAppEvents; +} +/** + * Sorts and filters app event for personalization, if applicable. May filter app events, and may select just the best one + * @param objectGraph The object graph + * @param resultData The data for the search result + * @param personalizationDataContainer Personalization criteria used for sorting and filtering app events + * @returns an object representing the personalized app events for a user, along with the personalization result + */ +function selectedAppEventDataItems(objectGraph, resultData, personalizationDataContainer) { + const alwaysShowAppEvent = serverData.asBooleanOrFalse(resultData.meta, "associations.app-events.attributes.forceAppEvent"); + const appEventDataItems = serverData.asArrayOrEmpty(resultData.meta, "associations.app-events.data"); + if (alwaysShowAppEvent) { + // In this scenario, there should always be only one event to choose from, + // and we don't want to do any personalization. + return { dataItems: [appEventDataItems[0]] }; + } + const personalizedDataResult = onDevicePersonalization.personalizeDataItems(objectGraph, "search", appEventDataItems, false, personalizationDataContainer, false, undefined, resultData.id); + const personalizedDataItems = personalizedDataResult.personalizedData; + if (personalizedDataItems.length <= 0) { + return { dataItems: [] }; + } + return { dataItems: personalizedDataItems, personalizationData: personalizedDataResult }; +} +/** + * Sanitizes a raw app event metadata into an AppEvent model. If the event is not hydratable or has not started, this returns null. + * @param objectGraph The object graph + * @param appEventDataItem The app event that needs to be processed + * @param resultData The data for the search result + * @param baseMetricsOptions Metrics options for built lockups + * @param offerEnvironment Offer environment for the lockup. Typically comes from lockup options + * @param offerStyle Offer style for the lockup. Typically comes from lockup options + * @returns the processed app event with all business rules applied, or null if the app event was disqualified + */ +function sanitizedAppEvent(objectGraph, appEventDataItem, resultData, baseMetricsOptions, offerEnvironment, offerStyle) { + const metricsOptions = { + ...baseMetricsOptions, + targetType: "eventModule", + }; + const appEventOrDate = appEvents.appEventOrPromotionStartDateFromData(objectGraph, appEventDataItem, resultData, false, true, offerEnvironment, offerStyle, false, metricsOptions, false, true, null, false, false); + // Ignore a future AppEvent promotionStartDate here. + if (serverData.isNull(appEventOrDate) || appEventOrDate instanceof Date) { + return null; + } + else { + return appEventOrDate; + } +} +/** + * Creates an app event search result from the data and associated lockup, if an app event exists in the metadata + * @param resultData The data blob + * @param lockup The associated mixed media lockup + * @param standardLockupOptions: The standard lockup options for search results + * @param personalizationDataContainer The data container to use for personalizing the selected app event + * @param baseMetricsOptions Base metrics options for this result + * @returns {AppEventSearchResult} The app event search result, or null + */ +function appEventSearchResultFromData(objectGraph, resultData, lockup, standardLockupOptions, personalizationDataContainer, baseMetricsOptions) { + return validation.context("appEventSearchResultFromData", () => { + const appEventEligibleToDisplay = searchResultIsEligibleToDisplayAppEvent(objectGraph, resultData); + if (!appEventEligibleToDisplay) { + return null; + } + const { dataItems, personalizationData } = selectedAppEventDataItems(objectGraph, resultData, personalizationDataContainer); + let appEvent; + let parentAppData; + for (const appEventDataItem of dataItems) { + const appEventOrNull = sanitizedAppEvent(objectGraph, appEventDataItem, resultData, baseMetricsOptions, standardLockupOptions.offerEnvironment, standardLockupOptions.offerStyle); + if (serverData.isDefinedNonNull(appEventOrNull)) { + appEvent = appEventOrNull; + parentAppData = resultData !== null && resultData !== void 0 ? resultData : mediaRelationship.relationshipData(objectGraph, appEventDataItem, "app"); + break; + } + } + if (serverData.isNull(appEvent)) { + return null; + } + const alwaysShowAppEvent = serverData.asBooleanOrFalse(resultData.meta, "associations.app-events.attributes.forceAppEvent"); + const searchResult = new models.AppEventSearchResult(); + searchResult.lockup = lockup; + searchResult.appEvent = appEvent; + searchResult.alwaysShowAppEvent = alwaysShowAppEvent; + searchResult.clickAction = lockup.clickAction; + const recoMetricsData = metricsHelpersUtil.combinedRecoMetricsDataFromMetricsData(null, personalizationData === null || personalizationData === void 0 ? void 0 : personalizationData.processingType, null); + const impressionOptions = { + ...baseMetricsOptions, + id: appEvent.appEventId, + kind: "inAppEvent", + targetType: "eventModule", + title: appEvent.title, + softwareType: null, + recoMetricsData: recoMetricsData, + }; + if (serverData.isDefinedNonNull(parentAppData)) { + impressionOptions.relatedSubjectIds = [parentAppData.id]; + } + metricsHelpersImpressions.addImpressionFields(objectGraph, searchResult, impressionOptions); + return searchResult; + }); +} +//# sourceMappingURL=search-results.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/search/content/search-shelves.js b/node_modules/@jet-app/app-store/tmp/src/common/search/content/search-shelves.js new file mode 100644 index 0000000..03df66d --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/search/content/search-shelves.js @@ -0,0 +1,53 @@ +import * as models from "../../../api/models"; +import * as mediaAttributes from "../../../foundation/media/attributes"; +import { asInterface } from "../../../foundation/json-parsing/server-data"; +import { createMetricsOptionsForGenericSearchPageShelf } from "../../metrics/helpers/search/search-shelves"; +import { isNothing } from "@jet/environment"; +// The types of search pages shelves can render in +export var SearchPageType; +(function (SearchPageType) { + SearchPageType[SearchPageType["Landing"] = 0] = "Landing"; + SearchPageType[SearchPageType["Results"] = 1] = "Results"; + SearchPageType[SearchPageType["ChartsAndCategories"] = 2] = "ChartsAndCategories"; + SearchPageType[SearchPageType["Focus"] = 3] = "Focus"; +})(SearchPageType || (SearchPageType = {})); +/** + * Collections the shelf's attributes + * @param objectGraph The App Store Object Graph + * @param data The shelf data object + * @returns The attributes for the shelf + */ +export function shelfAttributesFromData(objectGraph, data, shelfKind = undefined, pageKind = models.SearchPageKind.Default) { + var _a, _b, _c; + const title = (_a = mediaAttributes.attributeAsString(data, "title")) !== null && _a !== void 0 ? _a : undefined; + let displayStyle = (_b = mediaAttributes.attributeAsInterface(data, "displayStyle")) !== null && _b !== void 0 ? _b : undefined; + /// If we are on the see-all page, we currently don't get back a displayStyle object, so we need to manually provide one + /// for the tile brick type as see-all is hard-coded for that density everywhere else (natively) + if (isNothing(displayStyle) && pageKind === models.SearchPageKind.CategoriesAndCharts) { + displayStyle = { + layoutDensity: models.GenericSearchPageShelfDisplayStyleDensity.Density1, + layout: undefined, + layoutSize: undefined, + }; + } + const itemDisplayStyleAttributes = mediaAttributes.attributeAsDictionary(data, "itemDisplayStyle"); + const itemDisplayStyle = asInterface(itemDisplayStyleAttributes); + const hasSeeAll = mediaAttributes.attributeAsBooleanOrFalse(data, "hasSeeAll"); + const displayCount = (_c = mediaAttributes.attributeAsNumber(data, "displayCount")) !== null && _c !== void 0 ? _c : undefined; + const seeAllURL = hasSeeAll ? data.href : undefined; + return new models.SearchShelfAttributes(data.id, title, displayStyle, displayCount, hasSeeAll, seeAllURL, itemDisplayStyle, shelfKind); +} +/** + * Creates a base shelf context for the search shelf + * @param objectGraph The App Store Object Graph + * @param data The shelf data object + * @param shelfAttributes The shelf's attributes + * @param searchPageContext The context for the page containing the shelf + * @returns A standard shelf context for the shelf + */ +export function baseShelfContext(objectGraph, data, shelfAttributes, searchPageContext) { + return { + metricsOptions: createMetricsOptionsForGenericSearchPageShelf(objectGraph, data, shelfAttributes, searchPageContext), + }; +} +//# sourceMappingURL=search-shelves.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/search/custom-creative.js b/node_modules/@jet-app/app-store/tmp/src/common/search/custom-creative.js new file mode 100644 index 0000000..9b3aa26 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/search/custom-creative.js @@ -0,0 +1,71 @@ +import * as serverData from "../../foundation/json-parsing/server-data"; +import { isNothing, isSome } from "@jet/environment"; +import { asDictionary } from "@apple-media-services/media-api"; +import { artworkFromApiArtwork } from "../content/content"; +import { asString } from "../../foundation/json-parsing/server-data"; +import { Video } from "../../api/models"; +export function getSelectedCustomCreativeId(data) { + if (preprocessor.CARRY_BUILD || preprocessor.DEBUG_BUILD) { + const customCreativeMeta = asDictionary(data, "meta.creativeAttributes"); + const selectedCustomCreativeId = asString(customCreativeMeta, "creatives.0"); + return selectedCustomCreativeId; + } + return null; +} +/** + * Create a custom creative artwork from the data. + * @param objectGraph Object graph + * @param data that we use to populate custom creative artwork + * @param customCreativeData where we store custom creative data. + * @param cropCode for the artwork + */ +export function customCreativeArtworkFromData(objectGraph, data, customCreativeData, cropCode) { + if (preprocessor.CARRY_BUILD || preprocessor.DEBUG_BUILD) { + const selectedCustomCreativeId = getSelectedCustomCreativeId(data); + if (isNothing(customCreativeData) || isNothing(selectedCustomCreativeId)) { + return null; + } + const creativeAssetArtwork = artworkFromApiArtwork(objectGraph, serverData.asDictionary(customCreativeData, `${selectedCustomCreativeId}.adCreativeArtwork`), { + allowingTransparency: false, + useCase: 4 /* ArtworkUseCase.LockupScreenshots */, + }); + if (isSome(cropCode) && isSome(creativeAssetArtwork)) { + creativeAssetArtwork.crop = cropCode; + } + return creativeAssetArtwork; + } + else { + return null; + } +} +/** + * Create a custom creative video from the data. + * @param objectGraph Object graph + * @param data that we use to populate custom creative video and preview. + * @param videoConfiguration for the custom creative video. + * @param customCreativeData where we store custom creative data. + * @param cropCode for the artwork + */ +export function customCreativeVideoFromData(objectGraph, data, customCreativeData, videoConfiguration, cropCode) { + if (preprocessor.CARRY_BUILD || preprocessor.DEBUG_BUILD) { + const selectedCustomCreativeId = getSelectedCustomCreativeId(data); + if (isNothing(selectedCustomCreativeId)) { + return null; + } + const creativeVideoData = serverData.asDictionary(customCreativeData, `${selectedCustomCreativeId}.adCreativeVideo`); + const videoUrl = serverData.asString(creativeVideoData, "video"); + const preview = artworkFromApiArtwork(objectGraph, serverData.asDictionary(creativeVideoData, "previewFrame"), { + allowingTransparency: false, + useCase: 4 /* ArtworkUseCase.LockupScreenshots */, + }); + if (isNothing(videoUrl) || isNothing(preview)) { + return null; + } + if (isSome(cropCode)) { + preview.crop = cropCode; + } + return new Video(videoUrl, preview, videoConfiguration); + } + return null; +} +//# sourceMappingURL=custom-creative.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/search/guided-search/guided-search-metrics.js b/node_modules/@jet-app/app-store/tmp/src/common/search/guided-search/guided-search-metrics.js new file mode 100644 index 0000000..83cdf90 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/search/guided-search/guided-search-metrics.js @@ -0,0 +1,132 @@ +/** + * Builds metrics entities for Guided Search + */ +import { ImpressionMetrics, } from "../../../api/models"; +import { isDefinedNonNullNonEmpty, isNullOrEmpty } from "../../../foundation/json-parsing/server-data"; +import { createMetricsClickData, createMetricsSearchData } from "../../metrics/builder"; +import { createBasicLocation, currentPosition } from "../../metrics/helpers/location"; +// region Action Metrics +/** + * Add click + search metrics data to guided search token action + * @param objectGraph The dependency graph for the App Store. + * @param action Action to instrument, e.g. toggle or rewrite. + * @param targetToken The token display label, which may be toggleable. + * @param searchTerm The search term for which this token action was returned for. + * @param metricsOptions The metrics option use for adding instrumentation to token toggle. + */ +export function addEventsToGuidedSearchTokenAction(objectGraph, action, targetToken, searchTerm, metricsOptions) { + // Click Fields + const actionType = "guidedSearch"; + const targetType = "guidedLabel"; + const clickFields = { + actionType: actionType, + location: createBasicLocation(objectGraph, { + pageInformation: null, + locationTracker: metricsOptions.locationTracker, + targetType: targetType, + }, targetToken), + searchTerm: searchTerm, + }; + // Search Fields + const searchFields = { + targetId: targetToken, + }; + // SSS: Clicks must be before Search + const clickData = createMetricsClickData(objectGraph, targetToken, targetType, clickFields, ["guidedSearch"]); + action.actionMetrics.addMetricsData(clickData); + const searchData = createMetricsSearchData(objectGraph, searchTerm, targetType, actionType, null, searchFields, [ + "guidedSearch", + ]); + action.actionMetrics.addMetricsData(searchData); +} +export function addEventsToGuidedSearchTokenEntityChangeAction(objectGraph, action, searchTerm, targetEntity, metricsOptions) { + // Click Fields + const actionType = "hint"; + const targetType = "hintsEntity"; + const clickFields = { + actionType: actionType, + location: createBasicLocation(objectGraph, { + pageInformation: null, + locationTracker: metricsOptions.locationTracker, + targetType: targetType, + }, targetEntity), + searchTerm: searchTerm, + }; + // Search Fields + const searchFields = { + targetId: targetEntity, + }; + // SSS: Clicks must be before Search + const clickData = createMetricsClickData(objectGraph, targetEntity, targetType, clickFields); + action.actionMetrics.addMetricsData(clickData); + const searchData = createMetricsSearchData(objectGraph, searchTerm, targetType, actionType, null, searchFields); + action.actionMetrics.addMetricsData(searchData); +} +// endregion +// region Page Metrics +/** + * Build a `MetricsFields` for guided search fields used for page and impression events. + * This is attached onto `MetricsPageInformation` for consumption during page and impression field generation. + */ +export function guidedSearchPageInformationFields(objectGraph, request, guidedSearchData) { + const fields = {}; + // field for what tokens were selected on page. + if (isDefinedNonNullNonEmpty(request.guidedSearchTokens)) { + fields["searchSelectedGuidedFacets"] = request.guidedSearchTokens; + } + // field for what the server computed final query was + if (guidedSearchData && guidedSearchData.finalTerm) { + fields["searchGuidedFinalQuery"] = guidedSearchData.finalTerm; + } + if (isNullOrEmpty(fields)) { + return undefined; + } + return fields; +} +// endregion +// region Impression Metrics +export function addImpressionMetricsToGuidedSearchToken(objectGraph, token, type, metricsOptions) { + const tokenIndex = currentPosition(metricsOptions.locationTracker); + /** + * ID for parent is index. This should be explicit but is not in how we do metrics in JS today (sequential IDs are added later) + */ + const impressionFields = { + impressionIndex: tokenIndex, + id: tokenIndex.toString(), + idType: "sequential", + name: token.displayName, + impressionType: type, + parentId: "search-revisions", + }; + token.impressionMetrics = new ImpressionMetrics(impressionFields); +} +/** + * Add imaginary parent container for search results + * <rdar://problem/70515816> Guided Search: Tech Debt: Trim parent impression tech debt off SearchResults (JS Compatibility Breaking) + */ +export function addSearchResultParentImpressionMetrics(objectGraph, searchResultsPage, metricsOptions) { + const parentIndex = currentPosition(metricsOptions.locationTracker); + searchResultsPage.resultsParentImpressionMetrics = new ImpressionMetrics({ + impressionIndex: parentIndex, + impressionType: "SearchResults", + idType: "relationship", + id: "search-results", + name: "Search Results", + }); +} +/** + * Add an imaginary parent container for guided search tokens + * <rdar://problem/70515816> Guided Search: Tech Debt: Trim parent impression tech debt off SearchResults (JS Compatibility Breaking) + */ +export function addGuidedSearchParentImpressionMetrics(objectGraph, searchResultsPage, metricsOptions) { + const parentIndex = currentPosition(metricsOptions.locationTracker); + searchResultsPage.guidedSearchTokensParentImpressionMetrics = new ImpressionMetrics({ + impressionIndex: parentIndex, + impressionType: "SearchRevisions", + idType: "relationship", + id: "search-revisions", + name: "Search Revisions", + }); +} +// endregion +//# sourceMappingURL=guided-search-metrics.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/search/guided-search/guided-search.js b/node_modules/@jet-app/app-store/tmp/src/common/search/guided-search/guided-search.js new file mode 100644 index 0000000..3be4ae9 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/search/guided-search/guided-search.js @@ -0,0 +1,105 @@ +/** + * Model Builder for Guided Search (STUB) + */ +import { GuidedSearchQuery, GuidedSearchToken, SearchAction, searchEntitySystemImage, } from "../../../api/models"; +import { GuidedSearchTokenToggleAction, SearchEntityChangeAction, } from "../../../api/models/search/guided-search-actions"; +import { isNullOrEmpty } from "../../../foundation/json-parsing/server-data"; +import { unreachable } from "../../../foundation/util/errors"; +import { addEventsToGuidedSearchTokenEntityChangeAction, addEventsToGuidedSearchTokenAction, addImpressionMetricsToGuidedSearchToken, } from "./guided-search-metrics"; +// region exports +/** + * Create a Guided search token from facet data. + * @param objectGraph The App Store object graph. + * @param selectionBehavior The behavior for click action, e.g. query rewrite versus toggling tokens. + * @param requestDescriptor The request descriptor for search request that returned this data. + * @param facetData The facet data to build with. + * @param metricsOptions The metrics options. + */ +export function createGuidedSearchToken(objectGraph, selectionBehavior, requestDescriptor, facetData, metricsOptions) { + if (isNullOrEmpty(facetData)) { + return null; + } + // Create click and search metrics for token action. + const origin = "guidedToken"; + const searchTerm = requestDescriptor.term; + const targetToken = facetData.displayLabel; + const clickAction = selectionBehavior === "rewrite" + ? new SearchAction(targetToken, facetData.finalTerm, null, origin) + : new GuidedSearchTokenToggleAction(targetToken, origin); + addEventsToGuidedSearchTokenAction(objectGraph, clickAction, targetToken, searchTerm, metricsOptions); + // Create the token with associated click action and metrics.. + const token = new GuidedSearchToken(targetToken, facetData.isSelected, undefined, targetToken, clickAction); + addImpressionMetricsToGuidedSearchToken(objectGraph, token, "guidedLabel", metricsOptions); + return token; +} +/** + * Create the guided search queries from facet data. + * These are stored across a guided search session so client can send down precomputed combinations of search term and guided search facets as an optimization. + * @param requestDescriptor The request descriptor for search request that returned this data. + * @param facetData The data to generate `GuidedSearchQuery` for. + */ +export function createGuidedSearchQueries(objectGraph, requestDescriptor, facetData) { + var _a; + if (isNullOrEmpty(facetData)) { + return null; + } + // request parameters that returned this `facetData` + const requestTerm = requestDescriptor.term; + const requestFacets = (_a = requestDescriptor.guidedSearchTokens) !== null && _a !== void 0 ? _a : []; + const queries = []; + for (const data of facetData) { + /** + * For each facet data, project the facet selection if that facet was selected / deselected w.r.t. request's facets + */ + const facetValue = data.displayLabel; + const lookaheadFacets = Array.from(requestFacets); + if (data.isSelected) { + const facetIndex = lookaheadFacets.indexOf(facetValue); + if (facetIndex !== -1) { + lookaheadFacets.splice(facetIndex, 1); + } + } + else { + lookaheadFacets.push(facetValue); + } + const query = new GuidedSearchQuery(requestTerm, lookaheadFacets, data.finalTerm); + queries.push(query); + } + return queries; +} +/** + * Create an action for **reversing** a selected entity hint. This is so user can reverse an selected entity filter for search. + * e.g. If user tapped "Search for Games in Apple Watch Apps", user can deselect the "Apple Watch Apps" entity filter from the Guided Search UI + */ +export function createGuidedSearchTokenClearingEntityFilter(objectGraph, requestDescriptor, metricsOptions) { + var _a; + const selectedEntityFilter = requestDescriptor.searchEntity; + if (!selectedEntityFilter) { + return null; + } + const deselectEntityAction = new SearchEntityChangeAction(null, "guidedToken"); + addEventsToGuidedSearchTokenEntityChangeAction(objectGraph, deselectEntityAction, requestDescriptor.term, selectedEntityFilter, metricsOptions); + let displayText; + switch (selectedEntityFilter) { + case "arcade": + displayText = objectGraph.loc.string("GUIDED_SEARCH_TOKEN_ENTITY_ARCADE"); + break; + case "developer": + displayText = objectGraph.loc.string("GUIDED_SEARCH_TOKEN_ENTITY_DEVELOPERS"); + break; + case "story": + displayText = objectGraph.loc.string("GUIDED_SEARCH_TOKEN_ENTITY_STORIES"); + break; + case "watch": + displayText = objectGraph.loc.string("GUIDED_SEARCH_TOKEN_ENTITY_APPLEWATCH"); + break; + default: + unreachable(selectedEntityFilter); + break; + } + const token = new GuidedSearchToken(displayText, true, (_a = searchEntitySystemImage(selectedEntityFilter)) !== null && _a !== void 0 ? _a : "magnifyingglass", displayText, deselectEntityAction); + addImpressionMetricsToGuidedSearchToken(objectGraph, token, "hintsEntity", metricsOptions); + return token; +} +// endregion +//# sourceMappingURL=guided-search.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/search/landing/search-landing-cohort.js b/node_modules/@jet-app/app-store/tmp/src/common/search/landing/search-landing-cohort.js new file mode 100644 index 0000000..6baf947 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/search/landing/search-landing-cohort.js @@ -0,0 +1,84 @@ +/** + * Manages storing Cohort IDs for Search Landing Page (SLP) + * + * # What is Cohort ID? + * Cohort IDs (a.k.a. cluster IDs) are used to bucket users into different categories, e.g. a gamer. + * This ID can be used to specify a SLP that is suited to that cateogry of users, e.g. a page featuring more games. + * + * # SLP Endpoint Constraints + * Today, SLPs are not personalized, and rely heavily on CDN caching. + * We cannot fire a single request to SLP endpoint to have it adapt to a user's cohort based on cookies, etc. + * + * As a workaround we: + * 1. Store the cohort ID for user if we ever load a personalized endpoint, e.g. Today. + * 2. Send stored cohort ID as a query param on the SLP endpoint, if we have any. + */ +"use strict"; +import * as serverData from "../../../foundation/json-parsing/server-data"; +// region exports +/** + * Should be called whenever we recieve a MAPI response from a personalized endpoint. + * Persists the cohort id that may be present in given MAPI respose for specified user. + * @param dsid The user's dsid. + * @param metaDataProviding MAPI response that may contain cohort id for current user. + */ +export function storeCohortIdForUserFromResponse(objectGraph, dsid, metaDataProviding) { + if (serverData.isNullOrEmpty(dsid)) { + return; + } + const cohortId = cohortIdFromResponse(metaDataProviding); + if (serverData.isNullOrEmpty(cohortId)) { + return; + } + setCohortIdForDSID(objectGraph, dsid, cohortId); +} +/** + * Return the stored cohort id for given dsid. + * @param dsid The DSID of user to fetch cohort id for. + */ +export function cohortIdForUser(objectGraph, dsid) { + if (serverData.isNullOrEmpty(dsid)) { + return null; + } + return getCohortIdForDSID(objectGraph, dsid); +} +/** + * Deletes cohort id for given user. For testing. + */ +export function deleteCohortIdForUser(objectGraph, dsid) { + setCohortIdForDSID(objectGraph, dsid, undefined); +} +// endregion +// region Internals +/** + * Given a top-level MAPI response `metaDataProviding`, returns the cohort ID, if any. + * @param metaDataProviding A MAPI Response that may contain a `meta.metrics` blob with `clusterId` + */ +function cohortIdFromResponse(metaDataProviding) { + return serverData.asString(metaDataProviding, "meta.metrics.clusterId"); +} +/** + * Converts a DSID into a dictionary key for storing cohort ID. + * @param dsid DSID to generate storage key for. + */ +function cohortIDStorageKeyForDSID(dsid) { + return dsid + "-cohort-id"; +} +/** + * Set the stored cohort id for given user (by dsid). + * @param dsid DSID to associate cohortId with + * @param cohortId The cohort id for user. + */ +function setCohortIdForDSID(objectGraph, dsid, cohortId) { + const cohortForDSIDKey = cohortIDStorageKeyForDSID(dsid); + objectGraph.storage.storeString(cohortForDSIDKey, cohortId); +} +/** + * Gets the stored cohort id for given user by dsid. + * @param dsid DSID to get cohort id for. + */ +function getCohortIdForDSID(objectGraph, dsid) { + const cohortForDSIDKey = cohortIDStorageKeyForDSID(dsid); + return objectGraph.storage.retrieveString(cohortForDSIDKey); +} +//# sourceMappingURL=search-landing-cohort.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/search/landing/search-landing-shelf-controller.js b/node_modules/@jet-app/app-store/tmp/src/common/search/landing/search-landing-shelf-controller.js new file mode 100644 index 0000000..808384b --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/search/landing/search-landing-shelf-controller.js @@ -0,0 +1,836 @@ +import { isNothing, isSome, unwrapOptional } from "@jet/environment/types/optional"; +import * as models from "../../../api/models"; +import * as filtering from "../../filtering"; +import * as adLockups from "../../lockups/ad-lockups"; +import * as lockups from "../../lockups/lockups"; +import * as serverData from "../../../foundation/json-parsing/server-data"; +import * as mediaAttributes from "../../../foundation/media/attributes"; +import * as mediaDataStructure from "../../../foundation/media/data-structure"; +import * as mediaRelationships from "../../../foundation/media/relationships"; +import { searchLandingPageAdShelfIdentifier, searchLandingPagePositionInfo } from "../../ads/on-device-ad-stitch"; +import { createArtworkForResource } from "../../content/artwork/artwork"; +import * as content from "../../content/content"; +import * as metricsHelpersPage from "../../metrics/helpers/page"; +import * as metricsHelpersClicks from "../../metrics/helpers/clicks"; +import * as metricsHelpersImpressions from "../../metrics/helpers/impressions"; +import * as metricsHelpersLocation from "../../metrics/helpers/location"; +import { createChartsCategoryShelf } from "../content/search-categories"; +import { MediumAdLockupWithScreenshotsBackground, } from "../../../api/models"; +import * as adStitch from "../../ads/ad-stitcher"; +import * as adIncidents from "../../ads/ad-incident-recorder"; +import * as searchShelves from "../content/search-shelves"; +import { CollectionShelfDisplayStyle } from "../../editorial-pages/editorial-page-types"; +import { buildBrick } from "../../editorial-pages/editorial-page-shelf-builder/editorial-page-collection-shelf-builder/editorial-page-brick-collection-shelf-builder"; +import { iconFromData } from "../../content/content"; +import * as searchHistoryShelf from "../shelves/search-history-shelf"; +import { adLogger } from "../search-ads"; +import { asString } from "../../../foundation/json-parsing/server-data"; +import { addImpressionsFieldsToAd } from "../../metrics/helpers/impressions"; +import { makeSearchResultsPageIntent } from "../../../api/intents/search-results-page-intent"; +import { getLocale } from "../../locale"; +import { actionFor } from "../../../foundation/runtime/action-provider"; +import * as impressionDemotion from "../../personalization/on-device-impression-demotion"; +import { applySearchAdMissedOpportunityToShelvesIfNeeded } from "../../ads/ad-common"; +// #region Shelf Creation +export function firstShelfMarkerMatchingUseCase(dataContainer, searchLandingPageContext, onDevicePersonalizationUseCase) { + const shelfData = dataContainer.data; + if (serverData.isNullOrEmpty(shelfData)) { + return null; + } + for (const dataItem of shelfData) { + if (serverData.isNullOrEmpty(dataItem)) { + continue; + } + /// Skip shelf if not valid for context page type. + const shelfMetadata = serverData.asDictionary(dataItem, "meta"); + const shelfPageType = pageTypeFromShelfMetaCategory(shelfMetadata === null || shelfMetadata === void 0 ? void 0 : shelfMetadata.category); + if (shelfPageType !== searchLandingPageContext.pageType) { + continue; + } + if (onDevicePersonalizationUseCase === + mediaAttributes.attributeAsString(dataItem, "onDevicePersonalizationUseCase")) { + return dataItem; + } + } + return null; +} +/** + * Inserts the shelf made from the data container into the grouping parse context + * @param objectGraph The App Store dependency graph + * @param dataContainer The response data + * @param searchLandingPageContext The context for the search page, e.g. landing or focus + */ +export function insertShelvesIntoSearchPageContext(objectGraph, dataContainer, searchLandingPageContext) { + var _a; + const shelfData = dataContainer.data; + if (serverData.isNullOrEmpty(shelfData)) { + return; + } + // index to compare adPositionInfo + let builtShelves = 0; + const supportsFocus = objectGraph.bag.mediaAPISearchFocusEnabled && isSome(searchLandingPageContext.pageType); + for (const dataItem of shelfData) { + if (serverData.isNullOrEmpty(dataItem)) { + continue; + } + /// Skip shelf if not valid for context page type. + if (supportsFocus) { + const shelfMetadata = serverData.asDictionary(dataItem, "meta"); + const shelfPageType = pageTypeFromShelfMetaCategory(shelfMetadata === null || shelfMetadata === void 0 ? void 0 : shelfMetadata.category); + if (isSome(shelfPageType) && shelfPageType !== searchLandingPageContext.pageType) { + continue; + } + } + const adMeta = dataContainer.meta || null; + const adUnitShelf = shelfFromAdStitcher(objectGraph, searchLandingPageContext, builtShelves, adMeta); + if (isSome(adUnitShelf)) { + searchLandingPageContext.shelves.push(adUnitShelf); + metricsHelpersLocation.nextPosition(searchLandingPageContext.metricsLocationTracker); + } + /// Get the necessary metadata for the shelf + const shelfKind = mediaAttributes.attributeAsString(dataItem, "contentKind"); + const shelfAttributes = searchShelves.shelfAttributesFromData(objectGraph, dataItem, shelfKind); + const shelfContext = searchLandingPageShelfContext(objectGraph, dataItem, shelfAttributes, searchLandingPageContext, shelfKind); + /// Push the shelf content location so each shelf has the correct parent and starting index + metricsHelpersLocation.pushContentLocation(objectGraph, shelfContext.metricsOptions, (_a = shelfAttributes.title) !== null && _a !== void 0 ? _a : ""); + /// Create the shelf + const shelf = createShelf(objectGraph, dataItem, searchLandingPageContext, shelfContext, shelfAttributes, shelfKind); + /// Pop the shelf location + metricsHelpersLocation.popLocation(searchLandingPageContext.metricsLocationTracker); + /// If the shelf is empty, skip it + if (serverData.isNullOrEmpty(shelf)) { + continue; + } + /// Add impressions for the shelf + metricsHelpersImpressions.addImpressionFields(objectGraph, shelf, shelfContext.metricsOptions); + applySearchAdMissedOpportunityToShelvesIfNeeded(objectGraph, [shelf], "searchLanding", shelfContext.metricsOptions.id, searchLandingPageContext.metricsPageInformation); + /// Add the shelf to the page context for processing later + searchLandingPageContext.shelves.push(shelf); + builtShelves += 1; + /// Make sure each shelf is represented by its own impressions index position + metricsHelpersLocation.nextPosition(searchLandingPageContext.metricsLocationTracker); + } +} +function pageTypeFromShelfMetaCategory(category) { + switch (category) { + case "search-landing": + return searchShelves.SearchPageType.Landing; + case "search-focus": + return searchShelves.SearchPageType.Focus; + default: + return undefined; + } +} +/** + * Creates the appropriate shelf for the shelf's type + * @param objectGraph The App Store Object Graph + * @param data The data representing this shelf + * @param pageContext The context for the shelf's page + * @param shelfContext The context for the shelf + * @param shelfAttributes The attributes for the shelf + * @param shelfKind The content kind for the shelf + * @returns A shelf if supported, null otherwise + */ +function createShelf(objectGraph, data, pageContext, shelfContext, shelfAttributes, shelfKind) { + switch (shelfKind) { + case models.SearchLandingPageContentKind.Suggestion: + if (objectGraph.client.isVision || objectGraph.client.isWeb) { + return createSuggestedLinksShelf(objectGraph, data, pageContext, shelfAttributes, shelfContext); + } + else if (pageContext.pageType === searchShelves.SearchPageType.Focus) { + return createSuggestedSearchesShelf(objectGraph, data, pageContext, shelfAttributes, shelfContext); + } + else { + return createSuggestedLinkActionsShelf(objectGraph, data, pageContext, shelfAttributes, shelfContext); + } + case models.SearchLandingPageContentKind.CategoriesAndCharts: + return createChartsCategoryShelf(objectGraph, data, false, shelfAttributes, pageContext, shelfContext); + case models.SearchLandingPageContentKind.Apps: + return createLockupsShelf(objectGraph, data, pageContext, shelfAttributes, shelfContext); + case models.SearchLandingPageContentKind.EditorialCollection: + if (objectGraph.client.isVision || objectGraph.client.isWeb) { + return buildBrickShelf(objectGraph, data, pageContext, shelfAttributes, shelfContext); + } + else { + return null; + } + default: + return createShelfFromMarker(objectGraph, data, pageContext, shelfContext, shelfAttributes); + } +} +/** + * Creates the appropriate shelf for the shelf's type + * @param objectGraph The App Store Object Graph + * @param data The data representing this shelf + * @param pageContext The context for the shelf's page + * @param shelfContext The context for the shelf + * @param shelfAttributes The attributes for the shelf + * @returns A shelf if supported, null otherwise + */ +function createShelfFromMarker(objectGraph, data, pageContext, shelfContext, shelfAttributes) { + if (data.type !== "search-recommendations-marker") { + return null; + } + switch (mediaAttributes.attributeAsString(data, "onDevicePersonalizationUseCase")) { + case "recentSearches": + return searchHistoryShelf.createShelfWithContext(objectGraph, pageContext, shelfAttributes); + default: + return null; + } +} +/** + * Creates the appropriate shelf for the shelf's type + * @param objectGraph The App Store Object Graph + * @param data The data representing this shelf + * @param pageContext The context for the shelf's page + */ +function createMediumAdLockupWithScreenshotsBackgroundShelf(objectGraph, data, pageContext) { + var _a, _b, _c; + const shelf = new models.Shelf("mediumAdLockupWithScreenshotsBackground"); + shelf.isHorizontal = false; + const offerEnvironment = "dark"; + const offerStyle = "white"; + const metricsOptions = metricsHelpersImpressions.impressionOptions(objectGraph, data, asString(data.attributes.name), { + pageInformation: pageContext.metricsPageInformation, + locationTracker: pageContext.metricsLocationTracker, + targetType: "card", + recoMetricsData: mediaDataStructure.metricsFromMediaApiObject(data), + isAdvert: adLockups.isAdvert(objectGraph, data), + }); + metricsOptions.kind = "adItem"; + // Set up iAdInfo + metricsOptions.pageInformation.iAdInfo.apply(objectGraph, data); + const lockupOptions = { + offerEnvironment: offerEnvironment, + offerStyle: offerStyle, + metricsOptions: { + pageInformation: pageContext.metricsPageInformation, + locationTracker: pageContext.metricsLocationTracker, + recoMetricsData: mediaDataStructure.metricsFromMediaApiObject(data), + isAdvert: adLockups.isAdvert(objectGraph, data), + disableFastImpressionsForAds: true, + }, + artworkUseCase: content.artworkUseCaseFromShelfStyle(objectGraph, "mediumAdLockupWithScreenshotsBackground"), + canDisplayArcadeOfferButton: content.shelfContentTypeCanDisplayArcadeOfferButtons(objectGraph, "mediumAdLockupWithScreenshotsBackground"), + }; + const videoConfiguration = { + canPlayFullScreen: false, + playbackControls: {}, + }; + let lockup = lockups.mixedMediaAdLockupFromData(objectGraph, data, lockupOptions, videoConfiguration, null, false); + const platformScreenshots = lockup.screenshots[0]; + const templateString = adLockups.getTemplateTypeForMediumAdFromLockupWithScreenshots(platformScreenshots); + pageContext.metricsPageInformation.iAdInfo.setTemplateType(templateString); + const iconData = iconFromData(objectGraph, data, { + useCase: 0 /* ArtworkUseCase.Default */, + withJoeColorPlaceholder: true, + overrideTextColorKey: "textColor2", + }); + // Update the lockup value after setting the template type so that the value gets added to the lockup. + lockup = lockups.mixedMediaAdLockupFromData(objectGraph, data, lockupOptions, videoConfiguration, null, false); + if (objectGraph.props.enabled("advertSlotReporting")) { + (_a = lockup.searchAdOpportunity) === null || _a === void 0 ? void 0 : _a.setTemplateType(templateString); + } + else { + (_b = lockup.searchAd) === null || _b === void 0 ? void 0 : _b.setTemplateType(templateString); + } + const backgroundColor = iconData.backgroundColor; + const secondaryTextColor = iconData.textColor; + const mediumAd = new MediumAdLockupWithScreenshotsBackground(lockup, [platformScreenshots], true, secondaryTextColor, backgroundColor, (_c = objectGraph.bag.todayAdMediumLockupScreenshotsRiverSpeed) !== null && _c !== void 0 ? _c : 8); + addImpressionsFieldsToAd(objectGraph, mediumAd, metricsOptions, metricsOptions.pageInformation.iAdInfo); + mediumAd.clickAction = lockups.actionFromData(objectGraph, data, metricsOptions, null); + shelf.items = [mediumAd]; + return shelf; +} +/** + * Creates the appropriate shelf for the shelf's type + * @param objectGraph The App Store Object Graph + * @param data The data representing this shelf + * @param pageContext The context for the shelf's page + */ +function createCondensedAdLockupWithIconBackgroundShelf(objectGraph, data, pageContext) { + var _a, _b, _c; + const shelf = new models.Shelf("condensedAdLockupWithIconBackground"); + shelf.isHorizontal = false; + const offerEnvironment = "dark"; + const offerStyle = "white"; + const metricsOptions = metricsHelpersImpressions.impressionOptions(objectGraph, data, asString(data.attributes.name), { + pageInformation: pageContext.metricsPageInformation, + locationTracker: pageContext.metricsLocationTracker, + targetType: "card", + recoMetricsData: mediaDataStructure.metricsFromMediaApiObject(data), + isAdvert: adLockups.isAdvert(objectGraph, data), + }); + metricsOptions.kind = "adItem"; + // Set up iAdInfo + metricsOptions.pageInformation.iAdInfo.apply(objectGraph, data); + const lockupOptions = { + offerEnvironment: offerEnvironment, + offerStyle: offerStyle, + metricsOptions: { + pageInformation: pageContext.metricsPageInformation, + locationTracker: pageContext.metricsLocationTracker, + recoMetricsData: mediaDataStructure.metricsFromMediaApiObject(data), + isAdvert: adLockups.isAdvert(objectGraph, data), + disableFastImpressionsForAds: true, + }, + artworkUseCase: content.artworkUseCaseFromShelfStyle(objectGraph, "condensedAdLockupWithIconBackground"), + canDisplayArcadeOfferButton: content.shelfContentTypeCanDisplayArcadeOfferButtons(objectGraph, "condensedAdLockupWithIconBackground"), + }; + (_a = pageContext.metricsPageInformation.iAdInfo) === null || _a === void 0 ? void 0 : _a.setTemplateType("APPLOCKUP"); + const videoConfiguration = { + canPlayFullScreen: false, + playbackControls: {}, + }; + const lockup = lockups.mixedMediaAdLockupFromData(objectGraph, data, lockupOptions, videoConfiguration, null, false); + if (objectGraph.props.enabled("advertSlotReporting")) { + (_b = lockup.searchAdOpportunity) === null || _b === void 0 ? void 0 : _b.setTemplateType("APPLOCKUP"); + } + else { + (_c = lockup.searchAd) === null || _c === void 0 ? void 0 : _c.setTemplateType("APPLOCKUP"); + } + const condensedAd = new models.CondensedAdLockupWithIconBackground(lockup, lockup.icon); + addImpressionsFieldsToAd(objectGraph, condensedAd, metricsOptions, metricsOptions.pageInformation.iAdInfo); + shelf.items = [condensedAd]; + return shelf; +} +// #endregion +// #region Suggested Links/Actions Shelves +/** + * Creates the suggested links shelf + * @param objectGraph The App Store dependency graph + * @param data The content items for the shelf + * @param pageContext The context for the shelf's page + * @param shelfAttributes The attributes for the shelf and its contents + * @returns A shelf for the suggested links, or null if the shelf would be empty + */ +function createSuggestedLinkActionsShelf(objectGraph, data, pageContext, shelfAttributes, shelfContext) { + var _a; + const linkData = mediaRelationships.relationshipCollection(data, "contents"); + const items = []; + const shelf = new models.Shelf("action"); + shelf.isHorizontal = false; + shelf.title = shelfAttributes.title; + shelf.presentationHints = { isWidthConstrained: true }; + for (const [linkIndex, link] of linkData.entries()) { + const metricsBase = { + targetType: "link", + pageInformation: pageContext.metricsPageInformation, + locationTracker: pageContext.metricsLocationTracker, + }; + const searchAdAction = trendingSearchLinkActionFromData(objectGraph, link, pageContext, shelfAttributes); + if (serverData.isNull(searchAdAction) || serverData.isNull(searchAdAction.action)) { + continue; + } + metricsHelpersImpressions.addImpressionFields(objectGraph, searchAdAction.action, { + ...metricsBase, + kind: "link", + softwareType: null, + title: searchAdAction.action.title, + id: `${linkIndex}`, + idType: "sequential", + }); + items.push(searchAdAction); + metricsHelpersLocation.nextPosition(pageContext.metricsLocationTracker); + } + if (serverData.isNullOrEmpty(items)) { + return null; + } + shelf.items = items; + if (serverData.isNumber((_a = shelfAttributes.displayStyle) === null || _a === void 0 ? void 0 : _a.layoutSize)) { + shelf.contentsMetadata = { + type: "searchLandingTrendingSection", + numberOfColumns: shelfAttributes.displayStyle.layoutSize, + }; + } + else if (objectGraph.client.isPhone || objectGraph.client.isPad) { + shelf.contentsMetadata = { + type: "searchLandingTrendingSection", + numberOfColumns: items.length >= 6 ? 2 : 1, + }; + } + return shelf; +} +/** + * Creates the suggested searches shelf for the focus page. + * @param objectGraph The App Store dependency graph + * @param data The content items for the shelf + * @param pageContext The context for the shelf's page + * @param shelfAttributes The attributes for the shelf and its contents + * @returns A shelf for the suggested links, or null if the shelf would be empty + */ +function createSuggestedSearchesShelf(objectGraph, data, pageContext, shelfAttributes, shelfContext) { + var _a, _b, _c; + const linkData = mediaRelationships.relationshipCollection(data, "contents"); + if (isNothing(linkData)) { + return null; + } + const items = []; + const shelf = new models.Shelf("singleColumnList"); + shelf.isHorizontal = false; + shelf.title = shelfAttributes.title; + shelf.presentationHints = { isWidthConstrained: true }; + for (const [linkIndex, link] of linkData.entries()) { + const searchTerm = mediaAttributes.attributeAsString(link, "searchTerm"); + if (isNothing(searchTerm) || searchTerm.length === 0) { + continue; // search term is required + } + const displayTerm = (_a = mediaAttributes.attributeAsString(link, "displayTerm")) !== null && _a !== void 0 ? _a : searchTerm; + const searchAction = createFocusPageSearchAction(objectGraph, displayTerm !== null && displayTerm !== void 0 ? displayTerm : "", searchTerm !== null && searchTerm !== void 0 ? searchTerm : "", undefined, pageContext.metricsLocationTracker, "suggested", undefined, /// MAINTAINER'S NOTE: In the future, we could use this to attribute the suggestion source to the search result fetch. + pageContext.metricsPageInformation, (_b = shelfAttributes.searchLandingItemDisplayStyle) !== null && _b !== void 0 ? _b : undefined); + if (isNothing(searchAction) || serverData.isNullOrEmpty(searchAction)) { + continue; + } + metricsHelpersImpressions.addImpressionFields(objectGraph, searchAction, { + targetType: "link", + pageInformation: pageContext.metricsPageInformation, + locationTracker: pageContext.metricsLocationTracker, + kind: "link", + softwareType: null, + title: (_c = searchAction.title) !== null && _c !== void 0 ? _c : "", + id: `${linkIndex}`, + idType: "sequential", + }); + items.push(searchAction); + metricsHelpersLocation.nextPosition(pageContext.metricsLocationTracker); + } + if (serverData.isNullOrEmpty(items)) { + return null; + } + shelf.items = items; + return shelf; +} +/** + * Creates the suggested links shelf + * @param objectGraph The App Store dependency graph + * @param data The content items for the shelf + * @param pageContext The context for the shelf's page + * @param shelfAttributes The attributes for the shelf and its contents + * @returns A shelf for the suggested links, or null if the shelf would be empty + */ +function createSuggestedLinksShelf(objectGraph, data, pageContext, shelfAttributes, shelfContext) { + var _a; + const linkData = mediaRelationships.relationshipCollection(data, "contents"); + const links = []; + const shelf = new models.Shelf("searchLink"); + shelf.isHorizontal = false; + shelf.title = shelfAttributes.title; + shelf.presentationHints = { isWidthConstrained: true }; + metricsHelpersImpressions.addImpressionFields(objectGraph, shelf, shelfContext.metricsOptions); + for (const [linkIndex, link] of linkData.entries()) { + const metricsBase = { + targetType: "link", + pageInformation: pageContext.metricsPageInformation, + locationTracker: pageContext.metricsLocationTracker, + }; + const searchLink = trendingSearchLinkFromData(objectGraph, link, pageContext, shelfAttributes); + metricsHelpersImpressions.addImpressionFields(objectGraph, searchLink, { + ...metricsBase, + kind: "link", + softwareType: null, + title: searchLink.clickAction.title, + id: `${linkIndex}`, + idType: "sequential", + }); + if (serverData.isNullOrEmpty(searchLink)) { + continue; + } + links.push(searchLink); + metricsHelpersLocation.nextPosition(pageContext.metricsLocationTracker); + } + if (serverData.isNullOrEmpty(links)) { + return null; + } + shelf.items = links; + if (serverData.isNumber((_a = shelfAttributes.displayStyle) === null || _a === void 0 ? void 0 : _a.layoutSize)) { + shelf.contentsMetadata = { + type: "searchLandingTrendingSection", + numberOfColumns: shelfAttributes.displayStyle.layoutSize, + }; + } + else if (objectGraph.client.isPhone) { + shelf.contentsMetadata = { + type: "searchLandingTrendingSection", + numberOfColumns: links.length >= 6 ? 2 : 1, + }; + } + return shelf; +} +/** + * Creates a suggestion link action for the link data + * NOTE: This is legacy and the newer SLP protocol uses `trendingSearchLinkFromData` + * @param objectGraph The App Store dependency graph + * @param link The data for an individual suggestion link + * @param pageContext The page context for the search link + * @param shelfAttributes The attributes for the shelf and its contents + * @returns An action representing the suggestion link + */ +function trendingSearchLinkActionFromData(objectGraph, link, pageContext, shelfAttributes) { + var _a, _b; + /// We should always have search term, but not necessarily displayTerm. + const searchTerm = mediaAttributes.attributeAsString(link, "searchTerm"); + if (isNothing(searchTerm) || searchTerm.length === 0) { + return null; + } + const displayTerm = (_a = mediaAttributes.attributeAsString(link, "displayTerm")) !== null && _a !== void 0 ? _a : searchTerm; + /// MAINTAINER'S NOTE: In the future, we could use this to attribute the suggestion source to the search result fetch. + const searchAction = new models.SearchAction(displayTerm, searchTerm, null, "suggested", undefined, undefined); + searchAction.artwork = createArtworkForSearchAction((_b = shelfAttributes.searchLandingItemDisplayStyle) !== null && _b !== void 0 ? _b : undefined, objectGraph); + metricsHelpersClicks.addEventsToSearchAction(objectGraph, searchAction, "button", pageContext.metricsLocationTracker); + return isSome(searchAction) ? new models.SearchAdAction(searchAction) : null; +} +/** + * Creates a search action + * @param objectGraph The App Store dependency graph + * @param searchTerm The term to search for + * @param entity The entity to scope the search to, e.g. apps, stories, arcade + * @param metricsLocationTracker The metrics location tracker + * @param origin The source the search was fired from + * @param isFocusPage Whether the origin is in context of the focus page + * @param displayStyle The style to display the action item + * @returns An action representing a search + */ +export function createFocusPageSearchAction(objectGraph, title, searchTerm, entity, metricsLocationTracker, origin, source, metricsPageInformation = undefined, displayStyle) { + if (serverData.isNullOrEmpty(searchTerm)) { + return null; + } + // For Search Focus Page, text uses primary color and icon uses secondary color (instead of tintColor). + const searchAction = new models.SearchAction(title, searchTerm, null, origin, entity !== null && entity !== void 0 ? entity : undefined, source, []); + searchAction.artwork = createArtworkForSearchAction(displayStyle, objectGraph); + metricsHelpersClicks.addEventsToSearchAction(objectGraph, searchAction, "button", metricsLocationTracker, metricsPageInformation); + return searchAction; +} +function createArtworkForSearchAction(displayStyle, objectGraph) { + var _a; + if ((displayStyle === null || displayStyle === void 0 ? void 0 : displayStyle.iconKind) === models.SearchLandingPageShelfItemIconKind.Symbol && ((_a = displayStyle === null || displayStyle === void 0 ? void 0 : displayStyle.iconKind) === null || _a === void 0 ? void 0 : _a.length)) { + return createArtworkForResource(objectGraph, `systemimage://${displayStyle.iconSymbol}`); + } + else if (objectGraph.client.isPhone || objectGraph.client.isVision) { + return createArtworkForResource(objectGraph, "systemimage://magnifyingglass"); + } + return undefined; +} +/** + * Creates a suggestion link action for the link data + * @param objectGraph The App Store dependency graph + * @param link The data for an individual suggestion link + * @param pageContext The page context for the search link + * @param shelfAttributes The attributes for the shelf and its contents + * @returns An action representing the suggestion link + */ +function trendingSearchLinkFromData(objectGraph, link, pageContext, shelfAttributes) { + var _a, _b, _c; + const searchTerm = mediaAttributes.attributeAsString(link, "searchTerm"); + if (isNothing(searchTerm) || searchTerm.length === 0) { + return null; + } + const displayTerm = (_a = mediaAttributes.attributeAsString(link, "displayTerm")) !== null && _a !== void 0 ? _a : searchTerm; /// we should always have search term, but not necessarily displayTerm + let searchAction; + if (objectGraph.client.isWeb) { + const intent = makeSearchResultsPageIntent({ + ...getLocale(objectGraph), + origin: "suggested", + term: displayTerm, + platform: (_b = objectGraph.activeIntent) === null || _b === void 0 ? void 0 : _b.previewPlatform, + }); + searchAction = unwrapOptional(actionFor(intent, objectGraph)); + } + else { + searchAction = new models.SearchAction(displayTerm, displayTerm, null, "suggested"); + metricsHelpersClicks.addEventsToSearchAction(objectGraph, searchAction, "button", pageContext.metricsLocationTracker); + } + const artwork = createArtworkForSearchAction((_c = shelfAttributes.searchLandingItemDisplayStyle) !== null && _c !== void 0 ? _c : undefined, objectGraph); + return new models.SearchLink(displayTerm, searchAction, artwork, null); +} +// #endregion +// #region Discover Lockups Shelves +/** + * Creates a shelf composed of lockups + * @param objectGraph The App Store Object Graph + * @param data The data representing this lockups shelf + * @param pageContext The page context for the shelf + * @param shelfAttributes The attributes for the shelf + * @param shelfContext The context for the shelf + * @returns A lockups shelf if any lockups could be made + */ +function createLockupsShelf(objectGraph, data, pageContext, shelfAttributes, shelfContext) { + var _a, _b, _c, _d, _e; + const filterType = 80894 /* filtering.Filter.All */; + const items = []; + let hasAdLockup = false; + let shelfData = initialShelfContentsFromData(data); + // Set metadata + const shelf = new models.Shelf(shelfContext.shelfStyle); + shelf.isHorizontal = false; + shelf.title = shelfAttributes.title; + if (objectGraph.client.isVision) { + shelf.shouldFilterApps = !mediaAttributes.attributeAsBooleanOrFalse(data, "doNotFilter"); + } + else { + shelf.shouldFilterApps = false; + } + shelf.filteringExcludedItems = shelfContext.filteringExcludedItems; + // Stitch First Position Ad (only if lockup array is nonempty) + if (serverData.isDefinedNonNullNonEmpty(shelfData)) { + const adLockup = lockupFromAdStitcher(objectGraph, pageContext, shelfContext); + if (adLockup && adLockup instanceof models.Lockup) { + hasAdLockup = true; + items.push(adLockup); + metricsHelpersLocation.nextPosition(pageContext.metricsLocationTracker); + shelfData = shelfData.filter((shelfItem) => shelfItem.id !== adLockup.adamId); // Filter dupe + } + } + // Determines whether all apps are displayed on SLP suggested apps shelf. + const hasDisplayCount = isSome(shelfAttributes.displayCount); + // If on device personalization is available, we need to personalize the data items. This reorders the shelf. + if (serverData.isDefinedNonNullNonEmpty(shelfData)) { + shelfData = impressionDemotion.personalizeDataItems(shelfData, (_a = pageContext.recoImpressionData) !== null && _a !== void 0 ? _a : {}, (_c = (_b = shelfContext.metricsOptions) === null || _b === void 0 ? void 0 : _b.recoMetricsData) !== null && _c !== void 0 ? _c : {}); + } + // Build Lockups + for (const lockupData of shelfData) { + // If we encounter a type of app-events, this means they have been incorrectly programmed, + // and we should throw the shelf away. + if (lockupData.type === "app-events") { + return null; + } + if (serverData.isNull(lockupData.attributes)) { + continue; + } + // Filter out unwanted content + if (filtering.shouldFilter(objectGraph, lockupData, filterType)) { + continue; + } + const lockup = lockupFromData(objectGraph, lockupData, pageContext, shelfContext); + if (lockup) { + items.push(lockup); + metricsHelpersLocation.nextPosition(pageContext.metricsLocationTracker); + } + } + if (hasDisplayCount) { + // number of apps displayed on SLP suggested shelf + const displayCount = shelfAttributes.displayCount; + shelf.items = items.slice(0, displayCount); + } + else { + shelf.items = items; + } + if (hasDisplayCount) { + const seeAllShelf = new models.Shelf(shelfContext.shelfStyle); + if (hasAdLockup) { + // Ad is the first item in array so we are dropping it here. + seeAllShelf.items = items.splice(1, items.length - 1); + } + else { + seeAllShelf.items = items; + } + // Setup Page + const seeAllPage = new models.GenericPage([seeAllShelf]); + seeAllPage.title = shelf.title; + // Setup action + const seeAllAction = new models.FlowAction("page"); + seeAllAction.pageUrl = shelfAttributes.seeAllLink; + seeAllAction.title = objectGraph.loc.string("ACTION_SEE_ALL"); + seeAllAction.pageData = seeAllPage; + // Connect action + shelf.seeAllAction = seeAllAction; + // Metrics + metricsHelpersClicks.addClickEventToSeeAllAction(objectGraph, seeAllAction, seeAllAction.pageUrl, { + pageInformation: pageContext.metricsPageInformation, + locationTracker: pageContext.metricsLocationTracker, + }); + const seeAllPageInformation = metricsHelpersPage.pageInformationForRoom(objectGraph, data.id); + metricsHelpersPage.addMetricsEventsToPageWithInformation(objectGraph, seeAllPage, seeAllPageInformation); + } + // Honor A/B treatment for using horizontal shelf for suggested apps, which may have an optional rowsPerColumn defined. + // NOTE: Client will choose an appropriate `rowsPerColumn` at display time, if not provided by server. + if (((_d = shelfAttributes.displayStyle) === null || _d === void 0 ? void 0 : _d.layout) === "horizontal" /* models.GenericSearchPageShelfDisplayStyleLayout.Horizontal */) { + shelf.isHorizontal = true; + shelf.rowsPerColumn = (_e = shelfAttributes.displayStyle) === null || _e === void 0 ? void 0 : _e.layoutSize; + } + return shelf; +} +/** + * Create a lockup for shelfContents to display within a grouping shelf + * @param objectGraph + * @param lockupData shelfContents to create lockup for. + * @param pageContext The page context for the lockup + * @param shelfContext The shelf context for the lockup + */ +function lockupFromData(objectGraph, lockupData, pageContext, shelfContext) { + if (serverData.isNullOrEmpty(lockupData)) { + return null; + } + if (shelfContext.shelfStyle !== "smallLockup") { + return null; + } + let offerStyle = null; + if (serverData.isDefinedNonNull(shelfContext.shelfBackground) && + (shelfContext.shelfBackground.type === "color" || shelfContext.shelfBackground.type === "interactive")) { + offerStyle = "white"; + } + // Create the lockup + const lockupOptions = { + metricsOptions: { + pageInformation: pageContext.metricsPageInformation, + locationTracker: pageContext.metricsLocationTracker, + recoMetricsData: mediaDataStructure.metricsFromMediaApiObject(lockupData), + isAdvert: adLockups.isAdvert(objectGraph, lockupData), + }, + artworkUseCase: content.artworkUseCaseFromShelfStyle(objectGraph, shelfContext.shelfStyle), + offerStyle: offerStyle, + canDisplayArcadeOfferButton: content.shelfContentTypeCanDisplayArcadeOfferButtons(objectGraph, shelfContext.shelfStyle), + isContainedInPreorderExclusiveShelf: false, + shouldHideArcadeHeader: false, + }; + const lockup = lockups.lockupFromData(objectGraph, lockupData, lockupOptions); + if (serverData.isNull(lockup) || !lockup.isValid()) { + return null; + } + return lockup; +} +/** + * Performs `lockupFromData`, but additional with Ad stitch related side-effects. + * @param objectGraph The AppStore dependency graph + * @param pageContext The page context for the ad lockup + * @param shelfContext The shelf context for the ad lockup + */ +function lockupFromAdStitcher(objectGraph, pageContext, shelfContext) { + const task = adStitch.consumeTask(pageContext.adStitcher, shelfContext.adPositionInfo); + if (serverData.isNull(task)) { + return null; // no task for position + } + // Try to create lockup + const lockupData = task.data; + try { + const lockup = lockupFromData(objectGraph, lockupData, pageContext, shelfContext); + if (serverData.isDefinedNonNull(lockup)) { + shelfContext.filteringExcludedItems = [lockupData.id]; + } + else { + adIncidents.recordLockupFromDataFailed(objectGraph, pageContext.adIncidentRecorder, lockupData); + } + return lockup; + } + catch (error) { + adLogger(objectGraph, `Failed to create SLP ad lockup: ${error}`); + adIncidents.recordLockupFromDataFailed(objectGraph, pageContext.adIncidentRecorder, lockupData); + return null; + } +} +// #endregion +// #region Brick Shelves +function buildBrickShelf(objectGraph, data, pageContext, shelfAttributes, shelfContext) { + const items = []; + const shelf = new models.Shelf("brick"); + shelf.isHorizontal = mediaAttributes.attributeAsString(data, "layoutDirection") === "Horizontal"; + const shelfContents = mediaRelationships.relationshipCollection(data, "contents"); + for (const itemData of shelfContents) { + const metricsOptions = { + ...shelfContext.metricsOptions, + targetType: "brickMedium", + recoMetricsData: mediaDataStructure.metricsFromMediaApiObject(itemData), + }; + // Using the location tracker from the context will cause positions to be unintentionally incremented + // by the lockup builder. Since we're only extracting icons we won't use the lockup metrics, so we can + // pass in a new, unused, location tracker instead. + const lockupMetricsOptions = { + pageInformation: metricsOptions.pageInformation, + locationTracker: metricsHelpersLocation.newLocationTracker(), + }; + const brick = buildBrick(objectGraph, itemData, CollectionShelfDisplayStyle.BrickMedium, metricsOptions, lockupMetricsOptions); + brick.clickAction = createPrimaryActionForComponentFromData(objectGraph, itemData, shelfContext); + if (!brick.isValid()) { + continue; + } + items.push(brick); + metricsHelpersLocation.nextPosition(shelfContext.metricsOptions.locationTracker); + } + shelf.title = shelfAttributes.title; + shelf.items = items; + return shelf; +} +export function createPrimaryActionForComponentFromData(objectGraph, data, shelfContext) { + const clickOptions = createBrickClickOptionsFromData(objectGraph, data, shelfContext); + const primaryAction = lockups.actionFromData(objectGraph, data, clickOptions, null); + return primaryAction; +} +function createBrickClickOptionsFromData(objectGraph, data, shelfContext) { + const clickOptions = { + ...shelfContext.metricsOptions, + id: data.id, + targetType: "brickMedium", + }; + return clickOptions; +} +// #endregion +// #region Shelf Data Extraction +/** + * Gets the initial raw shelf contents from the MAPI data object + * @param mediaApiData The raw MAPI data + * @returns A collection of data objects representing a shelf's contents + */ +function initialShelfContentsFromData(mediaApiData) { + const shelfContents = mediaRelationships.relationship(mediaApiData, "contents"); + return shelfContents === null || shelfContents === void 0 ? void 0 : shelfContents.data; +} +// #endregion +// #region Context Generators +/** + * Performs either `createMediumAdLockupWithScreenshotsBackgroundShelf` or `createCondensedAdLockupWithIconBackgroundShelf + * depending on the format defined in adDisplayStyle, but with Ad stitch related side-effects. + * @param objectGraph The AppStore dependency graph + * @param pageContext The page context for the ad lockup + * @param builtShelves The index of the current shelf + * @param adMeta The SearchLandingPageAdMeta which includes adDisplayStyle + */ +function shelfFromAdStitcher(objectGraph, pageContext, builtShelves, adMeta) { + var _a; + const task = adStitch.consumeTask(pageContext.adStitcher, { + shelfIdentifier: searchLandingPageAdShelfIdentifier, + slot: builtShelves, + }); + if (serverData.isNull(task)) { + return null; // no task for position + } + const adData = task.data; + try { + switch ((_a = adMeta === null || adMeta === void 0 ? void 0 : adMeta.adDisplayStyle) === null || _a === void 0 ? void 0 : _a.format) { + case "medium": + return createMediumAdLockupWithScreenshotsBackgroundShelf(objectGraph, adData, pageContext); + case "condensed": + return createCondensedAdLockupWithIconBackgroundShelf(objectGraph, adData, pageContext); + default: + adIncidents.recordLockupFromDataFailed(objectGraph, pageContext.adIncidentRecorder, adData); + return null; + } + } + catch (error) { + adLogger(objectGraph, `Failed to create SLP ad shelf: ${error}`); + adIncidents.recordLockupFromDataFailed(objectGraph, pageContext.adIncidentRecorder, adData); + return null; + } +} +/** + * Generates a shelf context for a search landing page shelf + * @param objectGraph The App Store Object Graph + * @param data The shelf's data object + * @param shelfAttributes The shelf's attribtues + * @param pageContext The context for the page containing the shelf + * @param shelfContentKind The type of content the shelf contains + * @returns A shelf context for a search landing page shelf + */ +function searchLandingPageShelfContext(objectGraph, data, shelfAttributes, pageContext, shelfContentKind = null) { + const baseShelfContext = searchShelves.baseShelfContext(objectGraph, data, shelfAttributes, pageContext); + switch (shelfContentKind) { + case models.SearchLandingPageContentKind.Apps: + return { + ...baseShelfContext, + shelfStyle: "smallLockup", + adPositionInfo: searchLandingPagePositionInfo, + }; + default: + return baseShelfContext; + } +} +// #endregion +//# sourceMappingURL=search-landing-shelf-controller.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/category-metadata-ribbon-item.js b/node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/category-metadata-ribbon-item.js new file mode 100644 index 0000000..70c47c8 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/category-metadata-ribbon-item.js @@ -0,0 +1,38 @@ +import { isSome } from "@jet/environment"; +import { MetadataRibbonItem } from "../../../api/models"; +import { isNullOrEmpty } from "../../../foundation/json-parsing/server-data"; +import { categoryArtworkData } from "../../categories"; +import * as content from "../../content/content"; +import { categoryFromData } from "../../lockups/lockups"; +import * as metricsHelpersImpressions from "../../metrics/helpers/impressions"; +export function createMetadataRibbonItems(objectGraph, data, lockup, dedupeSet, metricsOptions) { + const artworkData = categoryArtworkData(objectGraph, data, true); + const hasArtwork = isSome(artworkData); + const labelText = categoryFromData(objectGraph, data); + if (isNullOrEmpty(labelText)) { + return null; + } + if (labelText != null) { + if (dedupeSet.has(labelText)) { + return null; + } + else { + dedupeSet.add(labelText); + } + } + const viewType = hasArtwork ? "imageWithLabel" : "textLabel"; + const categoryItem = new MetadataRibbonItem(viewType); + categoryItem.moduleType = "genreDisplayName"; + categoryItem.labelText = labelText; + if (hasArtwork) { + const artwork = content.artworkFromApiArtwork(objectGraph, artworkData, { + useCase: 20 /* content.ArtworkUseCase.CategoryIcon */, + }); + artwork.crop = "sr"; + categoryItem.artwork = artwork; + } + const impressionOptions = metricsHelpersImpressions.impressionOptionsForMetadataRibbonItem(metricsOptions, "genreDisplayName", categoryItem.labelText, "static"); + metricsHelpersImpressions.addImpressionFieldsToSearchMetadataRibbonItem(objectGraph, categoryItem, impressionOptions); + return [categoryItem]; +} +//# sourceMappingURL=category-metadata-ribbon-item.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/chart-metadata-ribbon-item.js b/node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/chart-metadata-ribbon-item.js new file mode 100644 index 0000000..26f815c --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/chart-metadata-ribbon-item.js @@ -0,0 +1,75 @@ +import { isSome, isNothing } from "@jet/environment/types/optional"; +import { MetadataRibbonItem } from "../../../api/models"; +import * as serverData from "../../../foundation/json-parsing/server-data"; +import { contentAttributeAsDictionary } from "../../content/attributes"; +import { badgeChartKeyForClientIdentifier } from "../../content/content"; +import * as metricsHelpersImpressions from "../../metrics/helpers/impressions"; +export function createMetadataRibbonItems(objectGraph, data, lockup, dedupeSet, metricsOptions) { + var _a, _b; + const chartData = chartFromData(objectGraph, data); + if (serverData.isNullOrEmpty(chartData)) { + return null; + } + const position = serverData.asNumber(chartData, "position"); + /// Per product, if the app isn't charting in the top 50, we don't want to use the chart module + if (isNothing(position) || position > 50) { + return null; + } + const genreName = (_a = serverData.asString(chartData, "genreShortName")) !== null && _a !== void 0 ? _a : serverData.asString(chartData, "genreName"); + if (genreName != null) { + if (dedupeSet.has(genreName)) { + return null; + } + else { + dedupeSet.add(genreName); + } + } + let chartItem; + if (objectGraph.bag.isLLMSearchTagsEnabled) { + chartItem = new MetadataRibbonItem("highlightedText"); + } + else { + chartItem = new MetadataRibbonItem("borderedTextLabel"); + } + chartItem.moduleType = "chartPositions"; + // Only use an ad override locale if this is an ad. + const adsOverrideLanguage = isSome((_b = lockup.searchAdOpportunity) === null || _b === void 0 ? void 0 : _b.searchAd) || isSome(lockup.searchAd) + ? objectGraph.bag.adsOverrideLanguage + : null; + const useAdsLocale = isSome(adsOverrideLanguage); + const loc = useAdsLocale ? objectGraph.adsLoc : objectGraph.loc; + // MAINTAINER'S NOTE: This was previously guarded by the iOS only `search_tags` feature flag that has been enabled by default on iOS only. + if (objectGraph.client.isiOS) { + const chartPositionText = loc + .string("MetadataRibbon.ChartPosition") + .replace("@@chartPosition@@", objectGraph.loc.formattedCountForPreferredLocale(objectGraph, position, adsOverrideLanguage)); + if (objectGraph.bag.isLLMSearchTagsEnabled) { + chartItem.highlightedText = chartPositionText; + chartItem.labelText = loc + .string("MetadataRibbon.ChartPositionAndCategory.Tags") + .replace("@@chartPosition@@", objectGraph.loc.formattedCountForPreferredLocale(objectGraph, position, adsOverrideLanguage)) + .replace("@@category@@", genreName); + } + else { + chartItem.labelText = genreName; + chartItem.borderedText = chartPositionText; + } + } + chartItem.secondaryViewPlacement = "leading"; + const impressionOptions = metricsHelpersImpressions.impressionOptionsForMetadataRibbonItem(metricsOptions, "chartPosition", chartItem.labelText, "static"); + metricsHelpersImpressions.addImpressionFieldsToSearchMetadataRibbonItem(objectGraph, chartItem, impressionOptions); + return [chartItem]; +} +export function chartFromData(objectGraph, data) { + const chartPositionsByStore = contentAttributeAsDictionary(objectGraph, data, "chartPositions"); + if (serverData.isNullOrEmpty(chartPositionsByStore)) { + return null; + } + const storeChartKey = badgeChartKeyForClientIdentifier(objectGraph, objectGraph.host.clientIdentifier); + if (serverData.isNullOrEmpty(storeChartKey)) { + return null; + } + const chartData = serverData.asDictionary(chartPositionsByStore, storeChartKey); + return chartData; +} +//# sourceMappingURL=chart-metadata-ribbon-item.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/developer-metadata-ribbon-item.js b/node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/developer-metadata-ribbon-item.js new file mode 100644 index 0000000..3a451a2 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/developer-metadata-ribbon-item.js @@ -0,0 +1,37 @@ +import { isNothing, isSome } from "@jet/environment"; +import * as models from "../../../api/models"; +import { attributeAsString } from "../../../foundation/media/attributes"; +import * as contentArtwork from "../../content/artwork/artwork"; +import * as metricsHelpersImpressions from "../../metrics/helpers/impressions"; +export function createMetadataRibbonItems(objectGraph, data, lockup, dedupeSet, metricsOptions) { + var _a; + let developerName = lockup.developerName; + if (isNothing(developerName)) { + developerName = (_a = attributeAsString(data, "artistName")) !== null && _a !== void 0 ? _a : attributeAsString(data, "developerName"); + } + if (developerName != null) { + if (dedupeSet.has(developerName)) { + return null; + } + else { + dedupeSet.add(developerName); + } + } + if (isSome(developerName) && developerName.length > 0) { + const developerItem = new models.MetadataRibbonItem("imageWithLabel"); + developerItem.moduleType = "developerInfo"; + developerItem.labelText = developerName; + developerItem.artwork = contentArtwork.createArtworkForResource(objectGraph, "systemimage://person.crop.square"); + const characterCountThreshold = 6; + developerItem.maxCharacterCount = 16; + developerItem.truncationLegibilityCharacterCountThreshold = Math.min(characterCountThreshold, developerName.length); + developerItem.allowsTruncation = developerName.length >= characterCountThreshold; + const impressionOptions = metricsHelpersImpressions.impressionOptionsForMetadataRibbonItem(metricsOptions, "developerInfo", developerItem.labelText, "static"); + metricsHelpersImpressions.addImpressionFieldsToSearchMetadataRibbonItem(objectGraph, developerItem, impressionOptions); + return [developerItem]; + } + else { + return null; + } +} +//# sourceMappingURL=developer-metadata-ribbon-item.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/divider-metadata-ribbon-item.js b/node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/divider-metadata-ribbon-item.js new file mode 100644 index 0000000..91f5a5f --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/divider-metadata-ribbon-item.js @@ -0,0 +1,8 @@ +import { MetadataRibbonItem } from "../../../api/models"; +export function createMetadataRibbonItems(objectGraph, data, lockup, dedupeSet, metricsOptions) { + const dividerItem = new MetadataRibbonItem("divider"); + dividerItem.moduleType = "divider"; + dividerItem.labelText = "|"; + return [dividerItem]; +} +//# sourceMappingURL=divider-metadata-ribbon-item.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/editors-choice-metadata-ribbon-item.js b/node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/editors-choice-metadata-ribbon-item.js new file mode 100644 index 0000000..764624e --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/editors-choice-metadata-ribbon-item.js @@ -0,0 +1,21 @@ +import { isSome } from "@jet/environment/types/optional"; +import * as models from "../../../api/models"; +import * as metricsHelpersImpressions from "../../metrics/helpers/impressions"; +export function createMetadataRibbonItems(objectGraph, data, lockup, dedupeSet, metricsOptions) { + var _a; + if (lockup.isEditorsChoice) { + const editorsChoiceItem = new models.MetadataRibbonItem("editorsChoice"); + editorsChoiceItem.moduleType = "editorialBadgeInfo"; + // Only use an ad override locale if this is an ad. + editorsChoiceItem.useAdsLocale = + (isSome((_a = lockup.searchAdOpportunity) === null || _a === void 0 ? void 0 : _a.searchAd) || isSome(lockup.searchAd)) && + isSome(objectGraph.bag.adsOverrideLanguage); + const impressionOptions = metricsHelpersImpressions.impressionOptionsForMetadataRibbonItem(metricsOptions, "editorialBadgeInfo", "Editors Choice", "static"); + metricsHelpersImpressions.addImpressionFieldsToSearchMetadataRibbonItem(objectGraph, editorsChoiceItem, impressionOptions); + return [editorsChoiceItem]; + } + else { + return null; + } +} +//# sourceMappingURL=editors-choice-metadata-ribbon-item.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/game-controller-metadata-ribbon-item.js b/node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/game-controller-metadata-ribbon-item.js new file mode 100644 index 0000000..40d18c0 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/game-controller-metadata-ribbon-item.js @@ -0,0 +1,36 @@ +import { isSome } from "@jet/environment/types/optional"; +import * as models from "../../../api/models"; +import * as contentArtwork from "../../content/artwork/artwork"; +import * as contentAttributes from "../../content/attributes"; +import * as metricsHelpersImpressions from "../../metrics/helpers/impressions"; +export function createMetadataRibbonItems(objectGraph, data, lockup, dedupeSet, metricsOptions) { + var _a; + let isGameControllerSupported = false; + switch (contentAttributes.contentAttributeAsString(objectGraph, data, "remoteControllerRequirement")) { + case "CONTROLLER_REQUIRED": + case "CONTROLLER_OPTIONAL": + isGameControllerSupported = true; + break; + default: + break; + } + if (contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "supportsGameController")) { + isGameControllerSupported = true; + } + if (!isGameControllerSupported) { + return null; + } + const gameControllerItem = new models.MetadataRibbonItem("imageWithLabel"); + gameControllerItem.moduleType = "supportsGameController"; + // Only use an ad override locale if this is an ad. + const useAdsLocale = (isSome(lockup.searchAd) || isSome((_a = lockup.searchAdOpportunity) === null || _a === void 0 ? void 0 : _a.searchAd)) && + isSome(objectGraph.bag.adsOverrideLanguage); + gameControllerItem.labelText = useAdsLocale + ? objectGraph.adsLoc.string("BADGE_MFI_SUPPORTED") + : objectGraph.loc.string("BADGE_MFI_SUPPORTED"); + gameControllerItem.artwork = contentArtwork.createArtworkForResource(objectGraph, "systemimage://gamecontroller.fill"); + const impressionOptions = metricsHelpersImpressions.impressionOptionsForMetadataRibbonItem(metricsOptions, "supportsGameController", "Supports Game Controller", "static"); + metricsHelpersImpressions.addImpressionFieldsToSearchMetadataRibbonItem(objectGraph, gameControllerItem, impressionOptions); + return [gameControllerItem]; +} +//# sourceMappingURL=game-controller-metadata-ribbon-item.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/metadata-ribbon-item-factory.js b/node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/metadata-ribbon-item-factory.js new file mode 100644 index 0000000..5c6f3a6 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/metadata-ribbon-item-factory.js @@ -0,0 +1,24 @@ +import * as categoryMetadataRibbonItem from "./category-metadata-ribbon-item"; +import * as chartMetadataRibbonItem from "./chart-metadata-ribbon-item"; +import * as developerMetadataRibbonItem from "./developer-metadata-ribbon-item"; +import * as dividerMetadataRibbonItem from "./divider-metadata-ribbon-item"; +import * as editorsChoiceMetadataRibbonItem from "./editors-choice-metadata-ribbon-item"; +import * as gameControllerMetadataRibbonItem from "./game-controller-metadata-ribbon-item"; +import * as secondaryShortCategoriesMetadataRibbonItem from "./secondary-short-categories-metadata-ribbon-item"; +import * as shortCategoryMetadataRibbonItem from "./short-category-metadata-ribbon-item"; +import * as starRatingMetadataRibbonItem from "./star-rating-metadata-ribbon-item"; +import * as tagMetadataRibbonItem from "./tag-metadata-ribbon-item"; +export const standardList = { + // every key in Key must be present + chartPositions: chartMetadataRibbonItem.createMetadataRibbonItems, + genreDisplayName: categoryMetadataRibbonItem.createMetadataRibbonItems, + genreShortDisplayName: shortCategoryMetadataRibbonItem.createMetadataRibbonItems, + secondaryGenreShortDisplayNames: secondaryShortCategoriesMetadataRibbonItem.createMetadataRibbonItems, + developerInfo: developerMetadataRibbonItem.createMetadataRibbonItems, + editorialBadgeInfo: editorsChoiceMetadataRibbonItem.createMetadataRibbonItems, + userRating: starRatingMetadataRibbonItem.createMetadataRibbonItems, + supportsGameController: gameControllerMetadataRibbonItem.createMetadataRibbonItems, + tag: tagMetadataRibbonItem.createMetadataRibbonItems, + divider: dividerMetadataRibbonItem.createMetadataRibbonItems, +}; +//# sourceMappingURL=metadata-ribbon-item-factory.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/metadata-ribbon.js b/node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/metadata-ribbon.js new file mode 100644 index 0000000..1b487cb --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/metadata-ribbon.js @@ -0,0 +1,27 @@ +import * as serverData from "../../../foundation/json-parsing/server-data"; +import { standardList } from "./metadata-ribbon-item-factory"; +export function createMetadataRibbonItemsForLockup(objectGraph, data, lockup, itemTypes, options, metadataRibbonItemFactory = standardList) { + if (serverData.isNullOrEmpty(itemTypes)) { + return []; + } + const metadataRibbonItems = []; + const dedupeSet = new Set(); + for (const itemSlot of itemTypes) { + if (serverData.isNullOrEmpty(itemSlot)) { + continue; + } + for (const itemType of itemSlot) { + const metadataRibbonFactory = metadataRibbonItemFactory[itemType]; + if (serverData.isNull(metadataRibbonFactory)) { + continue; + } + const results = metadataRibbonFactory(objectGraph, data, lockup, dedupeSet, options.metricsOptions); + if (serverData.isDefinedNonNull(results)) { + metadataRibbonItems.push(...results); + break; + } + } + } + return metadataRibbonItems; +} +//# sourceMappingURL=metadata-ribbon.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/ranked-secondary-category-metadata-ribbon-item.js b/node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/ranked-secondary-category-metadata-ribbon-item.js new file mode 100644 index 0000000..217876d --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/ranked-secondary-category-metadata-ribbon-item.js @@ -0,0 +1,22 @@ +import { MetadataRibbonItem } from "../../../api/models"; +import * as metricsHelpersImpressions from "../../metrics/helpers/impressions"; +import { isNothing, isSome } from "@jet/environment"; +/** + * Creates a metadata ribbon item for ranked secondary category metadata ribbon type. + * This particular function takes in the type string since we grab it from the data in the search-tags-ribbon. + */ +export function createMetadataRibbonItemsForRankedSecondaryCategory(objectGraph, data, lockup, dedupeSet, metricsOptions) { + if (isNothing(data) || data.length === 0 || dedupeSet.has(data)) { + return null; + } + const tagItem = new MetadataRibbonItem("textLabel"); + tagItem.moduleType = "rankedSecondaryGenre"; + if (isSome(data)) { + tagItem.labelText = data; + const impressionOptions = metricsHelpersImpressions.impressionOptionsForMetadataRibbonItem(metricsOptions, "rankedSecondaryGenre", tagItem.labelText, "static"); + metricsHelpersImpressions.addImpressionFieldsToSearchMetadataRibbonItem(objectGraph, tagItem, impressionOptions); + } + dedupeSet.add(data); + return [tagItem]; +} +//# sourceMappingURL=ranked-secondary-category-metadata-ribbon-item.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/search-tags-ribbon.js b/node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/search-tags-ribbon.js new file mode 100644 index 0000000..3901446 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/search-tags-ribbon.js @@ -0,0 +1,72 @@ +import { isSome } from "@jet/environment"; +import * as serverData from "../../../foundation/json-parsing/server-data"; +import * as metricsHelpersLocation from "../../metrics/helpers/location"; +import { standardList } from "./metadata-ribbon-item-factory"; +import * as rankedSecondaryCategoryMetadataRibbonItem from "./ranked-secondary-category-metadata-ribbon-item"; +export function createSearchTagsRibbonItemsForLockup(objectGraph, data, lockup, itemSlots, options, metadataRibbonItemFactory = standardList) { + if (serverData.isNullOrEmpty(itemSlots)) { + return []; + } + const tagData = serverData.asArrayOrEmpty(data.meta, "associations.tags.data"); + const metadataRibbonItems = []; + // We need to keep track of how many tags we have so we can assign the tag properly + let tagIndex = 0; + let rankedSecondaryGenreIndex = 0; + // We are going to pass in a set of strings for items we have already added to the ribbon so we never duplicate items + const dedupeSet = new Set(); + for (const itemSlot of itemSlots) { + const itemTypes = Array.isArray(itemSlot) ? itemSlot : [itemSlot]; + if (serverData.isNullOrEmpty(itemTypes)) { + continue; + } + for (const itemType of itemTypes) { + // If we find a tag, we pass in the tag data specifically + // If we find a rankedSecondaryGenre, we want to call the factory function specifically. + const isTag = itemType === "tag"; + const isRankedSecondaryGenre = itemType === "rankedSecondaryGenre"; + let results; + let metadataItemData = data; + let metadataItemString = ""; + if (isRankedSecondaryGenre) { + const searchExperimentDataForLockup = serverData.asDictionary(data, "meta"); + if (isSome(searchExperimentDataForLockup === null || searchExperimentDataForLockup === void 0 ? void 0 : searchExperimentDataForLockup.rankedSecondaryGenreShortDisplayNames)) { + metadataItemString = + searchExperimentDataForLockup === null || searchExperimentDataForLockup === void 0 ? void 0 : searchExperimentDataForLockup.rankedSecondaryGenreShortDisplayNames[rankedSecondaryGenreIndex]; + } + if (isSome(metadataItemString)) { + results = + rankedSecondaryCategoryMetadataRibbonItem.createMetadataRibbonItemsForRankedSecondaryCategory(objectGraph, metadataItemString, lockup, dedupeSet, options.metricsOptions); + rankedSecondaryGenreIndex = rankedSecondaryGenreIndex + 1; + } + else { + results = []; + } + } + else { + const metadataRibbonFactory = metadataRibbonItemFactory[itemType]; + if (serverData.isNull(metadataRibbonFactory)) { + continue; + } + if (isTag) { + metadataItemData = tagData[tagIndex]; + } + else { + metadataItemData = data; + } + results = metadataRibbonFactory(objectGraph, metadataItemData, lockup, dedupeSet, options.metricsOptions); + tagIndex = isTag ? tagIndex + 1 : tagIndex; + } + if (serverData.isDefinedNonNull(results)) { + metadataRibbonItems.push(...results); + for (const result of results) { + if (isSome(result.impressionMetrics)) { + metricsHelpersLocation.nextPosition(options.metricsOptions.locationTracker); + } + } + break; + } + } + } + return metadataRibbonItems; +} +//# sourceMappingURL=search-tags-ribbon.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/secondary-short-categories-metadata-ribbon-item.js b/node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/secondary-short-categories-metadata-ribbon-item.js new file mode 100644 index 0000000..c5469c7 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/secondary-short-categories-metadata-ribbon-item.js @@ -0,0 +1,27 @@ +import { MetadataRibbonItem } from "../../../api/models"; +import { isNullOrEmpty } from "../../../foundation/json-parsing/server-data"; +import { attributeAsArrayOrEmpty } from "../../../foundation/media/attributes"; +import * as metricsHelpersImpressions from "../../metrics/helpers/impressions"; +import * as metricsHelpersLocation from "../../metrics/helpers/location"; +export function createMetadataRibbonItems(objectGraph, data, lockup, dedupeSet, metricsOptions) { + const secondaryGenres = attributeAsArrayOrEmpty(data, "secondaryGenreShortDisplayNames"); + if (isNullOrEmpty(secondaryGenres)) { + return null; + } + const secondaryCategoryItems = secondaryGenres.map((secondaryGenre) => { + const categoryItem = new MetadataRibbonItem("textLabel"); + // Workaround for changing the moduleType to secondaryGenreShortDisplayNames from secondaryGenreShortDisplayName + // otherwise native doesnt layout the secondary genres correctly + // will be fixed with rdar://127458403 (Allow unknown metadataribbon items) + categoryItem.moduleType = "genreShortDisplayName"; + categoryItem.labelText = secondaryGenre; + const impressionOptions = metricsHelpersImpressions.impressionOptionsForMetadataRibbonItem(metricsOptions, "genreDisplayName", categoryItem.labelText, "static"); + metricsHelpersImpressions.addImpressionFieldsToSearchMetadataRibbonItem(objectGraph, categoryItem, impressionOptions); + metricsHelpersLocation.nextPosition(metricsOptions.locationTracker); + return categoryItem; + }); + return secondaryCategoryItems.filter((category) => { + return category.labelText != null && !dedupeSet.has(category.labelText); + }); +} +//# sourceMappingURL=secondary-short-categories-metadata-ribbon-item.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/short-category-metadata-ribbon-item.js b/node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/short-category-metadata-ribbon-item.js new file mode 100644 index 0000000..16a148a --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/short-category-metadata-ribbon-item.js @@ -0,0 +1,36 @@ +import { isNothing, isSome } from "@jet/environment"; +import { MetadataRibbonItem } from "../../../api/models"; +import { attributeAsString } from "../../../foundation/media/attributes"; +import { categoryArtworkData } from "../../categories"; +import { artworkFromApiArtwork } from "../../content/content"; +import * as metricsHelpersImpressions from "../../metrics/helpers/impressions"; +export function createMetadataRibbonItems(objectGraph, data, lockup, dedupeSet, metricsOptions) { + const artworkData = categoryArtworkData(objectGraph, data, true); + const hasArtwork = isSome(artworkData); + const shortGenre = attributeAsString(data, "genreShortDisplayName"); + if (shortGenre != null) { + if (dedupeSet.has(shortGenre)) { + return null; + } + else { + dedupeSet.add(shortGenre); + } + } + if (isNothing(shortGenre) || shortGenre.length === 0) { + return null; + } + const viewType = hasArtwork ? "imageWithLabel" : "textLabel"; + const shortCategoryItem = new MetadataRibbonItem(viewType); + shortCategoryItem.moduleType = "genreShortDisplayName"; + shortCategoryItem.labelText = shortGenre; + if (hasArtwork) { + shortCategoryItem.artwork = artworkFromApiArtwork(objectGraph, artworkData, { + useCase: 20 /* ArtworkUseCase.CategoryIcon */, + cropCode: "sr", + }); + } + const impressionOptions = metricsHelpersImpressions.impressionOptionsForMetadataRibbonItem(metricsOptions, "genreDisplayName", shortCategoryItem.labelText, "static"); + metricsHelpersImpressions.addImpressionFieldsToSearchMetadataRibbonItem(objectGraph, shortCategoryItem, impressionOptions); + return [shortCategoryItem]; +} +//# sourceMappingURL=short-category-metadata-ribbon-item.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/star-rating-metadata-ribbon-item.js b/node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/star-rating-metadata-ribbon-item.js new file mode 100644 index 0000000..81587f8 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/star-rating-metadata-ribbon-item.js @@ -0,0 +1,20 @@ +import { MetadataRibbonItem } from "../../../api/models"; +import * as serverData from "../../../foundation/json-parsing/server-data"; +import { contentAttributeAsBooleanOrFalse } from "../../content/attributes"; +import * as metricsHelpersImpressions from "../../metrics/helpers/impressions"; +export function createMetadataRibbonItems(objectGraph, data, lockup, dedupeSet, metricsOptions) { + const isPreorder = contentAttributeAsBooleanOrFalse(objectGraph, data, "isPreorder"); + if (serverData.isDefinedNonNull(lockup.ratingCount) && serverData.isDefinedNonNull(lockup.rating) && !isPreorder) { + const starRatingItem = new MetadataRibbonItem("starRating"); + starRatingItem.moduleType = "userRating"; + starRatingItem.starRating = lockup.rating; + starRatingItem.labelText = lockup.ratingCount; + const impressionOptions = metricsHelpersImpressions.impressionOptionsForMetadataRibbonItem(metricsOptions, "userRating", "User Rating", "static"); + metricsHelpersImpressions.addImpressionFieldsToSearchMetadataRibbonItem(objectGraph, starRatingItem, impressionOptions); + return [starRatingItem]; + } + else { + return null; + } +} +//# sourceMappingURL=star-rating-metadata-ribbon-item.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/tag-metadata-ribbon-item.js b/node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/tag-metadata-ribbon-item.js new file mode 100644 index 0000000..37877c3 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/search/metadata-ribbon/tag-metadata-ribbon-item.js @@ -0,0 +1,18 @@ +import { isNothing } from "@jet/environment"; +import { MetadataRibbonItem } from "../../../api/models"; +import * as mediaAttributes from "../../../foundation/media/attributes"; +import * as metricsHelpersImpressions from "../../metrics/helpers/impressions"; +export function createMetadataRibbonItems(objectGraph, data, lockup, dedupeSet, metricsOptions) { + const tagData = data; + const tagItem = new MetadataRibbonItem("textLabel"); + tagItem.moduleType = "tag"; + tagItem.labelText = mediaAttributes.attributeAsString(tagData, "name"); + if (isNothing(tagItem.labelText) || tagItem.labelText.length === 0 || dedupeSet.has(tagItem.labelText)) { + return null; + } + const impressionOptions = metricsHelpersImpressions.impressionOptionsForMetadataRibbonItem(metricsOptions, tagData.id, tagItem.labelText, "tag_id"); + metricsHelpersImpressions.addImpressionFieldsToSearchMetadataRibbonItem(objectGraph, tagItem, impressionOptions); + dedupeSet.add(tagItem.labelText); + return [tagItem]; +} +//# sourceMappingURL=tag-metadata-ribbon-item.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/search/search-ads-odml.js b/node_modules/@jet-app/app-store/tmp/src/common/search/search-ads-odml.js new file mode 100644 index 0000000..aedcef1 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/search/search-ads-odml.js @@ -0,0 +1,87 @@ +/** + * Implements the ODML Treatment (On-device machine learning) for Sponsored search. + */ +"use strict"; +import { adLogger } from "../../common/search/search-ads"; +import { isNull, isNullOrEmpty } from "../../foundation/json-parsing/server-data"; +import { shallowCopyOf } from "../../foundation/util/objects"; +import { decorateAdInstanceIdOnData } from "../ads/ad-common"; +import { productVariantDataForData, productVariantIDForVariantData } from "../product-page/product-page-variants"; +// region exports +/** + * Merge the contents of the raw response with the data in `advertData` from `SearchAds.framework` + * + * @param rawAdverts The adverts returned in the network response. + * @param nativeAdvertData The advert data that was returned from native. + */ +export function applyNativeAdvertData(objectGraph, rawAdverts, nativeAdvertData) { + if (isNull(nativeAdvertData)) { + return rawAdverts; // even if `nativeAdvertData` may contain error, we need to perform apply to merge instance ids. + } + const updated = []; + const rawAdvertsMap = rawAdverts.reduce((acc, data) => ({ ...acc, [data.id]: data }), {}); + for (const nativeAdData of nativeAdvertData.adverts) { + const rawAd = rawAdvertsMap[nativeAdData.adamId]; + if (isNullOrEmpty(rawAd)) { + adLogger(objectGraph, `[${nativeAdData.adamId}] skipped - Data was not part of original response`); + continue; + } + if (isNullOrEmpty(rawAd.attributes)) { + adLogger(objectGraph, `[${rawAd.id}] skipped - Data is missing attributes`); + continue; + } + const newAd = createCopyWithNativeData(objectGraph, rawAd, nativeAdData); + updated.push(newAd); + } + if (!preprocessor.PRODUCTION_BUILD) { + const rawOrder = rawAdverts.map((ad) => ad.id).join(" "); + const updatedOrder = updated.map((ad) => ad.id).join(" "); + adLogger(objectGraph, `applyNativeAdvertData: [${rawOrder}] => [${updatedOrder}]`); + } + return updated; +} +/** + * Returns whether or not ODML treatment was successful + */ +export function wasODMLSuccessful(objectGraph, nativeAdvertData) { + return nativeAdvertData && nativeAdvertData.odmlSuccess; +} +// endregion +// region internals +/** + * Create a copy of `ad` with the `adData` replacing the "iad" attribute. + * @param ad The raw ad to copy + * @param adData The ad blob to overrwite wth. + */ +function createCopyWithNativeData(objectGraph, ad, nativeAdData) { + const copy = shallowCopyOf(ad); + const attributes = shallowCopyOf(ad.attributes); + attributes["iads"] = nativeAdData.adData; + copy.attributes = attributes; + overrideCustomProductPageIdIfRequired(objectGraph, copy, nativeAdData); + decorateAdInstanceIdOnData(objectGraph, copy, nativeAdData.instanceId); + return copy; +} +/** + * Modifies an `ad` by overriding the `ppid` value if native ODML has dictated a new selection. + * Note: This may intentionally replace the `ppid` value with `null` if CPP is disabled for Search Ads. + * If we pass native all `null` values for ODML processing (because the feature is disabled in the bag), + * we expect to receive `null` back and insert `null` into the `ad` here. + * @param ad The ad to modify. + * @param adData The ad blob to overrwite with. + */ +function overrideCustomProductPageIdIfRequired(objectGraph, ad, nativeAdData) { + var _a; + const productVariantData = productVariantDataForData(objectGraph, ad); + const productVariantId = productVariantIDForVariantData(productVariantData); + // If there is a "selected" cppId, and it's different to the `serverCppId`, native has made a new selection. + // Confirm we have a cppData of meta to modify + if (nativeAdData.selectedCppId === productVariantId || isNullOrEmpty((_a = ad === null || ad === void 0 ? void 0 : ad.meta) === null || _a === void 0 ? void 0 : _a.cppData)) { + return; + } + // Modify the meta data with the new selection. + const meta = shallowCopyOf(ad.meta); + meta.cppData["ppid"] = nativeAdData.selectedCppId; + ad.meta = meta; +} +//# sourceMappingURL=search-ads-odml.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/search/search-ads.js b/node_modules/@jet-app/app-store/tmp/src/common/search/search-ads.js new file mode 100644 index 0000000..e8377d4 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/search/search-ads.js @@ -0,0 +1,1047 @@ +// +// search-ads.ts +// AppStoreKit +// +// Created by Joel Parsons on 24/October/2019 +// Copyright (c) 2016 Apple Inc. All rights reserved. +// +import { isSome } from "@jet/environment"; +import * as models from "../../api/models"; +import { ads } from "../../api/typings/constants"; +import * as serverData from "../../foundation/json-parsing/server-data"; +import * as mediaAttributes from "../../foundation/media/attributes"; +import { shallowCopyOf } from "../../foundation/util/objects"; +import { isAdLocalizationValid } from "../ads/ad-common"; +import * as client from "../../foundation/wrappers/client"; +import * as contentAttributes from "../content/attributes"; +import * as content from "../content/content"; +import * as filtering from "../filtering"; +import * as lockups from "../lockups/lockups"; +import * as metricsHelpersLocation from "../metrics/helpers/location"; +import { customCreativeArtworkFromData, customCreativeVideoFromData } from "./custom-creative"; +import { platformAttributeAsDictionary } from "../../foundation/media/platform-attributes"; +import { searchResultWillUseAppEventDisplay } from "./content/search-results"; +/** + * Data passed from native for requesting Sponsored Search + */ +export class SponsoredSearchRequestData { + constructor(data, appStoreClientRequestId) { + if (!data) { + return; + } + this.appStoreClientRequestId = appStoreClientRequestId; + this.iAdId = data["iAdId"]; + this.sponsoredSearchRequestData = data["dataBlob"]; + this.routingInfo = data["iAdRoutingInfo"]; + this.canary = data["canary"]; + } + validAdRequest() { + const hasRequestData = this.sponsoredSearchRequestData && this.sponsoredSearchRequestData.length > 0; + const hasRoutingInfo = this.routingInfo && this.routingInfo.length > 0; + return hasRequestData && hasRoutingInfo; + } +} +const searchVideoConfiguration = { + canPlayFullScreen: false, + playbackControls: {}, +}; +export function adsResultFromSearchResults(objectGraph, advertDatum, resultsDatum, requestMetadata, metricsOptions, installedStates, appStates, searchExperimentsData, personalizationDataContainer) { + var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l; + const advertsSearchResult = new models.AdvertsSearchResult(); + const isNetworkConstrained = (_a = requestMetadata.requestDescriptor.isNetworkConstrained) !== null && _a !== void 0 ? _a : false; + const advertMetricsOptions = { + id: "ad_container", + kind: "iosSoftware", + softwareType: null, + targetType: null, + title: "ad_container", + pageInformation: metricsOptions.pageInformation, + locationTracker: metricsOptions.locationTracker, + idType: "sequential", + }; + /** + * # Why do we push ad_container? + * We push an identifier for element that doesn't exist (`ad_container`) to prevent impression indices of organic search results + * being affected by having multiple ads built for ad-rotation. + * + * It is expected that: + * - `ad_container` will not impresss, since it doesn't exist + * - `parentImpressionId` will not be set expected. + */ + metricsHelpersLocation.pushContentLocation(objectGraph, advertMetricsOptions, "ad_container"); + if (serverData.isNullOrEmpty(advertDatum)) { + return { + result: advertsSearchResult, + }; + } + const firstSearchResult = resultsDatum[0]; + let firstAdComputedStyle; + const adIdsString = advertDatum + .filter(serverData.isDefinedNonNull) + .map((ad) => `[${ad.id}]`) + .join(", "); + const adString = `Adverts received from ad server: ${adIdsString}`; + adLogger(objectGraph, adString); + let isFirstAd = true; + for (const ad of advertDatum) { + if (serverData.isNull(ad)) { + continue; + } + if (filtering.shouldFilter(objectGraph, ad)) { + adLogger(objectGraph, `[${ad.id}] filtered by shouldFilter() - app probably not supported on current os or device`); + continue; + } + const isDupe = adIsDupe(ad.id, firstSearchResult === null || firstSearchResult === void 0 ? void 0 : firstSearchResult.id, installedStates); + // Extract the iAd data dictionary based on the first organic search result. + const adDataType = iAdDataTypeForAdvert(firstSearchResult, isDupe); + ad.attributes["iad"] = iadAttributesForType(ad, adDataType); + if (serverData.isNullOrEmpty(ad.attributes["iad"])) { + adLogger(objectGraph, `[${ad.id}] filtered because no appropriate iAd dictionary was found. (Probably a server issue if hitting this)`); + continue; + } + const adLockupOptions = { + metricsOptions: { + pageInformation: metricsOptions.pageInformation, + locationTracker: metricsOptions.locationTracker, + targetType: "card", + isAdvert: true, + }, + hideZeroRatings: true, + artworkUseCase: 8 /* content.ArtworkUseCase.SearchIcon */, + isNetworkConstrained: isNetworkConstrained, + canDisplayArcadeOfferButton: content.shelfContentTypeCanDisplayArcadeOfferButtons(objectGraph, "mixedMediaLockup"), + }; + const iAdData = contentAttributes.contentAttributeAsDictionary(objectGraph, ad, "iad"); + const iAdAllowsMedia = serverData.asBooleanOrFalse(iAdData, "format.images"); + let creativeHasArtworkToDisplay = false; + if (preprocessor.CARRY_BUILD || preprocessor.DEBUG_BUILD) { + const customCreativeData = platformAttributeAsDictionary(ad, contentAttributes.bestAttributePlatformFromData(objectGraph, ad), "creativeAttributes"); + const customCreativeArtwork = customCreativeArtworkFromData(objectGraph, ad, customCreativeData); + const customCreativeVideo = customCreativeVideoFromData(objectGraph, ad, customCreativeData, searchVideoConfiguration); + creativeHasArtworkToDisplay = isSome(customCreativeArtwork) || isSome(customCreativeVideo); + } + const noPreviousAdStyle = serverData.isNullOrEmpty(firstAdComputedStyle); + const temporaryAdLockup = lockups.mixedMediaAdLockupFromData(objectGraph, ad, adLockupOptions, searchVideoConfiguration, searchExperimentsData); + const platformLockupMedia = platformMediaForLockup(temporaryAdLockup); + // Adverts should only rotate between the same display style. The first advert dictates the display style + // for the rest of the adverts. Once we have an ad display style locked in we try to create the next ads in + // a style compatible with the first. If it can't satisfy any compatible styles, it gets thrown away. + const screenshotsDisplayStyle = creativeHasArtworkToDisplay + ? "four-screenshots" + : (_b = searchExperimentsData === null || searchExperimentsData === void 0 ? void 0 : searchExperimentsData.displayStyle) === null || _b === void 0 ? void 0 : _b.screenshots; + const computedAdDisplayStyle = resolvedAdDisplayStyleForMedia(objectGraph, platformLockupMedia, ad.id, adDataType, iAdAllowsMedia, firstAdComputedStyle, screenshotsDisplayStyle, firstSearchResult, installedStates, appStates, metricsOptions, personalizationDataContainer); + if (serverData.isNull(computedAdDisplayStyle)) { + adLogger(objectGraph, `[${ad.id}] will not be displayed because we could not create an ad style compatible with ${debugDescriptionForStyle(firstAdComputedStyle)}`); + continue; + } + // Check the localization is valid for the ad with the selected display style. + // We do this _before_ storing the style (if this is the first ad) to avoid setting + // a style based on an ad that can't be shown. + if (!isAdLocalizationValid(objectGraph, ad, null, computedAdDisplayStyle.style)) { + adLogger(objectGraph, `[${ad.id}] filtered because localization is not available`); + continue; + } + if (noPreviousAdStyle) { + // For the first advert we calculate a display style based on whether it is a dupe of the first organic result + // and what media is available for display for the app + adLogger(objectGraph, `[${ad.id}] first ad dictates ad display style of: ${debugDescriptionForStyle(computedAdDisplayStyle)}`); + firstAdComputedStyle = computedAdDisplayStyle; + } + else { + adLogger(objectGraph, `[${ad.id}] will be displayed because it is compatible with the display style of: ${debugDescriptionForStyle(computedAdDisplayStyle)}, which is the same height as display style: ${debugDescriptionForStyle(firstAdComputedStyle)}`); + } + metricsOptions.pageInformation.iAdInfo.apply(objectGraph, ad); + // Set the template type before creating the lockup to ensure the click event has the right data. + (_c = metricsOptions.pageInformation.iAdInfo) === null || _c === void 0 ? void 0 : _c.setTemplateType(computedAdDisplayStyle.style); + let adResultLockup = lockups.mixedMediaAdLockupFromData(objectGraph, ad, adLockupOptions, searchVideoConfiguration, searchExperimentsData); + adResultLockup = modifyLockupToMatchAdDisplayStyle(adResultLockup, computedAdDisplayStyle, isDupe, isFirstAd); + if (objectGraph.props.enabled("advertSlotReporting")) { + (_d = adResultLockup.searchAdOpportunity) === null || _d === void 0 ? void 0 : _d.setTemplateType(computedAdDisplayStyle.style); + } + else { + (_e = adResultLockup.searchAd) === null || _e === void 0 ? void 0 : _e.setTemplateType(computedAdDisplayStyle.style); + } + if (computedAdDisplayStyle.style === "TEXT" /* SearchAdDisplayStyle.TEXT */) { + const iAdTextKey = mediaAttributes.attributeAsString(ad, "iad.format.text"); + if (iAdTextKey !== "none") { + let advertisingText; + if (iAdTextKey === "description") { + advertisingText = contentAttributes.contentAttributeAsString(objectGraph, ad, "description.standard"); + } + else { + advertisingText = contentAttributes.contentAttributeAsString(objectGraph, ad, iAdTextKey); + } + const searchAd = (_f = adResultLockup.searchAd) !== null && _f !== void 0 ? _f : (_g = adResultLockup.searchAdOpportunity) === null || _g === void 0 ? void 0 : _g.searchAd; + if (serverData.isDefinedNonNull(searchAd) && serverData.isDefinedNonNull(advertisingText)) { + searchAd.advertisingText = advertisingText; + } + } + advertsSearchResult.displaysScreenshots = false; + } + if (serverData.isDefinedNonNullNonEmpty(adResultLockup)) { + const duplicatePosition = findOrganicLockupPosition(resultsDatum, adResultLockup.adamId); + if (isSome(duplicatePosition) && !creativeHasArtworkToDisplay) { + if (objectGraph.props.enabled("advertSlotReporting")) { + (_h = adResultLockup.searchAdOpportunity) === null || _h === void 0 ? void 0 : _h.setDuplicatePosition(duplicatePosition); + } + else { + (_j = adResultLockup.searchAd) === null || _j === void 0 ? void 0 : _j.setDuplicatePosition(duplicatePosition); + } + } + advertsSearchResult.lockups.push(adResultLockup); + metricsHelpersLocation.nextPosition(metricsOptions.locationTracker); + isFirstAd = false; + } + } + // Always pop the `ad_container` location. + metricsHelpersLocation.popLocation(metricsOptions.locationTracker); + // There are situations where all ads are filtered out - check the results have at least one item prior to modifying metrics data. + if (serverData.isDefinedNonNullNonEmpty(advertsSearchResult.lockups)) { + // Once all the lockups are built re-set the page information to use the iAd Info of the first advert + // This (probably) happens to ensure that the page event accurately reflects what's being presented + // to the user at first view. + const firstAd = advertDatum[0]; + metricsOptions.pageInformation.iAdInfo.apply(objectGraph, firstAd); + metricsHelpersLocation.nextPosition(metricsOptions.locationTracker); + } + if (firstAdComputedStyle) { + (_k = metricsOptions.pageInformation.iAdInfo) === null || _k === void 0 ? void 0 : _k.setTemplateType(firstAdComputedStyle.style); + } + else { + (_l = metricsOptions.pageInformation.iAdInfo) === null || _l === void 0 ? void 0 : _l.setTemplateType(null); + } + advertsSearchResult.condensedBehavior = "never"; + return { + result: advertsSearchResult, + displayStyle: firstAdComputedStyle === null || firstAdComputedStyle === void 0 ? void 0 : firstAdComputedStyle.style, + }; +} +/** + * Determines whether or not the ad is considered a dupe of the first organic result. + * @param adID The adaimID of the ad result + * @param firstResultID The adamID of the first organic result + * @param installedState A mapping of adamIDs to device app install state to determine if the ad/first result is on the user's device + * @returns whether the ad and result are dupes of the same app and that the first result will not condensed as a condensed first result + * will have no bearing on dupe status or display. + */ +function adIsDupe(adID, firstResultID, installedState) { + const isResultInstalled = installedState && installedState[firstResultID]; + const isDupeResult = adID && firstResultID && adID === firstResultID; + return isDupeResult && !isResultInstalled; +} +// endregion +// region Ad Display Styles +/** + * Resolves a SearchAdDisplayStyle for the given ad media and the search results context (the first organic search result). + * If the ad is a dupe of the first organic search result, we attempt to create two lockups with "full creative" to ensure there is enough + * media to show both. + * @param media the media from which to calculate the style. + * @param adId the id of the ad, for logging purposes. + * @param adDataType the ad type, based on the first search result. + * @param firstAdComputedStyle a previously computed `SearchAdDisplayStyleContainer`, if any, to maintain compatibility. + * @param firstSearchResult The first organic search result which we informs how to handle DUP logic + * @param installStates A mapping of adamIDs to their respective install states that is used to determine if the app is currently installed by the user + * @param appStates A mapping of adamIDs to their respective app state to determine if the ad/first result has been installed by the user in the past + * @param metricsOptions Metrics options for built models. + * @param personalizationDataContainer The data container to use for personalizing the data. + * @returns a `SearchAdDisplayStyleContainer` with a style compatible with the first search result, if one can be found. + */ +function resolvedAdDisplayStyleForMedia(objectGraph, media, adId, adDataType, iAdAllowsMedia, firstAdComputedStyle, screenshotsDisplayStyle, firstSearchResult, installStates, appStates, metricsOptions, personalizationDataContainer) { + const isFirstAd = serverData.isNullOrEmpty(firstAdComputedStyle); + const adDisplayStyle = getAdDisplayStyleForMedia(objectGraph, media, adId, iAdAllowsMedia, firstAdComputedStyle, screenshotsDisplayStyle); + if (serverData.isNull(adDisplayStyle)) { + return null; + } + const searchAdDisplayContainer = { + platform: media.mediaPlatformUsedForDisplayStyle, + style: adDisplayStyle, + }; + adLogger(objectGraph, `[${adId}] tentatively resolved to: ${debugDescriptionForStyle(searchAdDisplayContainer)}`); + if (adDataType === "DUP" /* iAdDataType.DUPE_AD */) { + removeUsedMediaForAdDisplayStyle(adDisplayStyle, media); + const organicSearchResultDisplayStyle = getAdDisplayStyleForMedia(objectGraph, media, adId, iAdAllowsMedia, null, screenshotsDisplayStyle); + const organicHasFullCreative = isDisplayStyleFullCreativeForOrganic(objectGraph, organicSearchResultDisplayStyle, screenshotsDisplayStyle); + const organicWillUseAppEvent = searchResultWillUseAppEventDisplay(objectGraph, firstSearchResult, installStates, appStates, metricsOptions, personalizationDataContainer); + if ((organicHasFullCreative || organicWillUseAppEvent) && isFirstAd) { + adLogger(objectGraph, `[${adId}] Organic Dupe would be full creative as ${organicSearchResultDisplayStyle} so choosing tentative style for ad`); + return searchAdDisplayContainer; + } + else if (organicHasFullCreative && + !isFirstAd && + isProposedAdStyleCompatible(adDisplayStyle, firstAdComputedStyle)) { + adLogger(objectGraph, `[${adId}] Organic Dupe would be a full creative, but ad is not the first so returning compatible style with first ${adDisplayStyle}`); + return searchAdDisplayContainer; + } + else if (isProposedAdStyleCompatible("UI1_3x2" /* SearchAdDisplayStyle.UNRESTRICTED_IMAGE_ONE_ASSET */, firstAdComputedStyle)) { + adLogger(objectGraph, `[${adId}] tentative style would not yield full creative for organic result so returning UNRESTRICTED_IMAGE_ONE_ASSET`); + return { + style: "UI1_3x2" /* SearchAdDisplayStyle.UNRESTRICTED_IMAGE_ONE_ASSET */, + }; + } + else if (isProposedAdStyleCompatible("UV1_3x2" /* SearchAdDisplayStyle.UNRESTRICTED_VIDEO_ONE_ASSET */, firstAdComputedStyle)) { + adLogger(objectGraph, `[${adId}] tentative style would not yield full creative for organic result so returning UNRESTRICTED_VIDEO_ONE_ASSET`); + return { + style: "UV1_3x2" /* SearchAdDisplayStyle.UNRESTRICTED_VIDEO_ONE_ASSET */, + }; + } + else if (isProposedAdStyleCompatible("TEXT" /* SearchAdDisplayStyle.TEXT */, firstAdComputedStyle)) { + adLogger(objectGraph, `[${adId}] tentative style would not yield full creative for organic result so returning TEXT`); + return { + style: "TEXT" /* SearchAdDisplayStyle.TEXT */, + }; + } + else { + adLogger(objectGraph, `[${adId}] tentative style would not yield full creative for organic result and first style is not compatible with TEXT so skipping ad`); + return null; + } + } + else if (serverData.isDefinedNonNull(firstAdComputedStyle) && + firstAdComputedStyle.style === "TEXT" /* SearchAdDisplayStyle.TEXT */) { + adLogger(objectGraph, `[${adId}] tentative style would be filtered since the first ad has style: ${debugDescriptionForStyle(firstAdComputedStyle)}, so returning TEXT`); + return { + style: "TEXT" /* SearchAdDisplayStyle.TEXT */, + }; + } + return searchAdDisplayContainer; +} +function allowsFourScreenshots(screenshotsDisplayStyle) { + if (!serverData.isDefinedNonNull(screenshotsDisplayStyle)) { + return false; + } + return screenshotsDisplayStyle === "four-screenshots"; +} +/** + * Finds the position of a specific lockup within the organic search results + * @param searchResults List of search results + * @param adResult The adamId of the app to search for + * @returns The 0-indexed position in the list that contains the lockup. If the organic results doesn't contain the lockup, this instead returns null. + */ +function findOrganicLockupPosition(searchResults, adamId) { + const index = searchResults.findIndex((datum) => datum.id === adamId); + return index === -1 ? null : index; +} +/** + * Returns a suitable style for a given set of lockup media. + * If a firstComputedAdStyle is provided, we check to confirm the proposed style is compatible before selecting that new style. + * @param media a set of media from which to calculate a style. + * @param adId the id of the ad, for logging purposes. + * @param iAdAllowsMedia whether the iAd Data has enabled media for the given ad. + * @param firstComputedAdStyle whether the iAd Data has enabled media for the given ad. + * @returns the preferred display style from the provided set of media. + */ +function getAdDisplayStyleForMedia(objectGraph, media, adId, iAdAllowsMedia, firstComputedAdStyle, screenshotsDisplayStyle) { + if (!iAdAllowsMedia) { + adLogger(objectGraph, `[${adId}] is not allowed to display media because of iAd configuration.`); + return "TEXT" /* SearchAdDisplayStyle.TEXT */; + } + if (media.mediaPlatformUsedForDisplayStyle && + firstComputedAdStyle && + firstComputedAdStyle.mediaPlatform && + !media.mediaPlatformUsedForDisplayStyle.isEqualTo(firstComputedAdStyle.mediaPlatform)) { + adLogger(objectGraph, `[${adId}] filtered because media is derived from: ${media.mediaPlatformUsedForDisplayStyle.mediaType}, but first ad media is derived from: ${firstComputedAdStyle.mediaPlatform.mediaType}`); + return null; + } + let displayStyle; + let firstVideoPreview = null; + if (serverData.isDefinedNonNullNonEmpty(media.videos)) { + const firstVideo = media.videos[0]; + firstVideoPreview = firstVideo.preview; + } + if (isSome(media.alignedRegionArtwork) && + isProposedAdStyleCompatible("UI1_3x2" /* SearchAdDisplayStyle.UNRESTRICTED_IMAGE_ONE_ASSET */, firstComputedAdStyle)) { + displayStyle = "UI1_3x2" /* SearchAdDisplayStyle.UNRESTRICTED_IMAGE_ONE_ASSET */; + } + else if (isSome(media.alignedRegionVideo) && + isProposedAdStyleCompatible("UV1_3x2" /* SearchAdDisplayStyle.UNRESTRICTED_VIDEO_ONE_ASSET */, firstComputedAdStyle)) { + displayStyle = "UV1_3x2" /* SearchAdDisplayStyle.UNRESTRICTED_VIDEO_ONE_ASSET */; + } + else if (serverData.isDefinedNonNullNonEmpty(firstVideoPreview) && + firstVideoPreview.isLandscape() && + isProposedAdStyleCompatible("LV1" /* SearchAdDisplayStyle.LANDSCAPE_VIDEO_ONE_ASSET */, firstComputedAdStyle)) { + // If first trailer's preview is landscape the lockup will render as landscape video + displayStyle = "LV1" /* SearchAdDisplayStyle.LANDSCAPE_VIDEO_ONE_ASSET */; + } + else if (serverData.isDefinedNonNullNonEmpty(firstVideoPreview) && + firstVideoPreview.isPortrait() && + allowsFourScreenshots(screenshotsDisplayStyle) && + isProposedAdStyleCompatible("PV4" /* SearchAdDisplayStyle.PORTRAIT_VIDEO_FOUR_ASSETS */, firstComputedAdStyle)) { + // The assumption here is that all combinations of portrait video are compatible with each other. + // If this changes in future, this logic will need to be revisited. + // If first trailer's preview is portrait the lockup will render as portrait with portrait images filled in + if (serverData.isDefinedNonNullNonEmpty(media.portraitScreenshots) && media.portraitScreenshots.length >= 3) { + displayStyle = "PV4" /* SearchAdDisplayStyle.PORTRAIT_VIDEO_FOUR_ASSETS */; + } + else if (serverData.isDefinedNonNullNonEmpty(media.portraitScreenshots) && + media.portraitScreenshots.length >= 2) { + displayStyle = "PV3" /* SearchAdDisplayStyle.PORTRAIT_VIDEO_THREE_ASSETS */; + } + else if (serverData.isDefinedNonNullNonEmpty(media.portraitScreenshots)) { + displayStyle = "PV2" /* SearchAdDisplayStyle.PORTRAIT_VIDEO_TWO_ASSETS */; + } + else { + displayStyle = "PV1" /* SearchAdDisplayStyle.PORTRAIT_VIDEO_ONE_ASSET */; + } + } + else if (serverData.isDefinedNonNullNonEmpty(firstVideoPreview) && + firstVideoPreview.isPortrait() && + isProposedAdStyleCompatible("PV3" /* SearchAdDisplayStyle.PORTRAIT_VIDEO_THREE_ASSETS */, firstComputedAdStyle)) { + // The assumption here is that all combinations of portrait video are compatible with each other. + // If this changes in future, this logic will need to be revisited. + // If first trailer's preview is portrait the lockup will render as portrait with portrait images filled in + if (serverData.isDefinedNonNullNonEmpty(media.portraitScreenshots) && media.portraitScreenshots.length >= 2) { + displayStyle = "PV3" /* SearchAdDisplayStyle.PORTRAIT_VIDEO_THREE_ASSETS */; + } + else if (serverData.isDefinedNonNullNonEmpty(media.portraitScreenshots)) { + displayStyle = "PV2" /* SearchAdDisplayStyle.PORTRAIT_VIDEO_TWO_ASSETS */; + } + else { + displayStyle = "PV1" /* SearchAdDisplayStyle.PORTRAIT_VIDEO_ONE_ASSET */; + } + } + else if (serverData.isDefinedNonNullNonEmpty(media.landscapeScreenshots) && + isProposedAdStyleCompatible("LI1" /* SearchAdDisplayStyle.LANDSCAPE_IMAGE_ONE_ASSET */, firstComputedAdStyle)) { + displayStyle = "LI1" /* SearchAdDisplayStyle.LANDSCAPE_IMAGE_ONE_ASSET */; + } + else if (serverData.isDefinedNonNullNonEmpty(media.portraitScreenshots) && + allowsFourScreenshots(screenshotsDisplayStyle) && + isProposedAdStyleCompatible("PI4" /* SearchAdDisplayStyle.PORTRAIT_IMAGE_FOUR_ASSETS */, firstComputedAdStyle)) { + // The assumption here is that all combinations of portrait video are compatible with each other. + // If this changes in future, this logic will need to be revisited. + if (media.portraitScreenshots.length >= 4) { + displayStyle = "PI4" /* SearchAdDisplayStyle.PORTRAIT_IMAGE_FOUR_ASSETS */; + } + else if (media.portraitScreenshots.length >= 3) { + displayStyle = "PI3" /* SearchAdDisplayStyle.PORTRAIT_IMAGE_THREE_ASSETS */; + } + else if (media.portraitScreenshots.length >= 2) { + displayStyle = "PI2" /* SearchAdDisplayStyle.PORTRAIT_IMAGE_TWO_ASSETS */; + } + else { + displayStyle = "PI1" /* SearchAdDisplayStyle.PORTRAIT_IMAGE_ONE_ASSET */; + } + } + else if (serverData.isDefinedNonNullNonEmpty(media.portraitScreenshots) && + isProposedAdStyleCompatible("PI3" /* SearchAdDisplayStyle.PORTRAIT_IMAGE_THREE_ASSETS */, firstComputedAdStyle)) { + // The assumption here is that all combinations of portrait video are compatible with each other. + // If this changes in future, this logic will need to be revisited. + if (media.portraitScreenshots.length >= 3) { + displayStyle = "PI3" /* SearchAdDisplayStyle.PORTRAIT_IMAGE_THREE_ASSETS */; + } + else if (media.portraitScreenshots.length >= 2) { + displayStyle = "PI2" /* SearchAdDisplayStyle.PORTRAIT_IMAGE_TWO_ASSETS */; + } + else { + displayStyle = "PI1" /* SearchAdDisplayStyle.PORTRAIT_IMAGE_ONE_ASSET */; + } + } + else if (isProposedAdStyleCompatible("TEXT" /* SearchAdDisplayStyle.TEXT */, firstComputedAdStyle)) { + displayStyle = "TEXT" /* SearchAdDisplayStyle.TEXT */; + } + else { + adLogger(objectGraph, `[${adId}] filtered because we could not create a compatible style for the first style of: ${debugDescriptionForStyle(firstComputedAdStyle)}`); + return null; + } + if (completePortraitMediaCount(objectGraph, screenshotsDisplayStyle) === 2) { + if (displayStyle === "PI3" /* SearchAdDisplayStyle.PORTRAIT_IMAGE_THREE_ASSETS */) { + displayStyle = "PI2" /* SearchAdDisplayStyle.PORTRAIT_IMAGE_TWO_ASSETS */; + } + else if (displayStyle === "PV3" /* SearchAdDisplayStyle.PORTRAIT_VIDEO_THREE_ASSETS */) { + displayStyle = "PV2" /* SearchAdDisplayStyle.PORTRAIT_VIDEO_TWO_ASSETS */; + } + } + return displayStyle; +} +/** + * Removes media used by the nominated displayStyle. + * Used in the case of a dupe where we need to confirm we have "full creative" for a subsequent presentation of a lockup. + * @param lockup the lockup to modify + * @param displayStyle the display style previously calculated as suitable for the lockup + */ +function removeUsedMediaForAdDisplayStyle(displayStyle, media) { + // In the case of any video assets being used, as per the implementation in `getAdDisplayStyleForMedia`, + // we always pick the first video, so whether it's portrait or landscape just remove the first. + switch (displayStyle) { + case "PI4" /* SearchAdDisplayStyle.PORTRAIT_IMAGE_FOUR_ASSETS */: + // We don't want to splice here as we might need these for reuse on dupe ads + if (media.portraitScreenshots.length <= 5) { + media.portraitScreenshots.splice(0, 4); + } + break; + case "PI3" /* SearchAdDisplayStyle.PORTRAIT_IMAGE_THREE_ASSETS */: + media.portraitScreenshots.splice(0, 3); + break; + case "PI2" /* SearchAdDisplayStyle.PORTRAIT_IMAGE_TWO_ASSETS */: + media.portraitScreenshots.splice(0, 2); + break; + case "PI1" /* SearchAdDisplayStyle.PORTRAIT_IMAGE_ONE_ASSET */: + media.portraitScreenshots.splice(0, 1); + break; + case "PV4" /* SearchAdDisplayStyle.PORTRAIT_VIDEO_FOUR_ASSETS */: + // We will keep the video splice as the video is always first, so it will never be reused between the ad and organic + media.videos.splice(0, 1); + // We don't want to splice here as we might need these for reuse on dupe ads + if (media.portraitScreenshots.length <= 4) { + media.portraitScreenshots.splice(0, 3); + } + break; + case "PV3" /* SearchAdDisplayStyle.PORTRAIT_VIDEO_THREE_ASSETS */: + media.videos.splice(0, 1); + media.portraitScreenshots.splice(0, 2); + break; + case "PV2" /* SearchAdDisplayStyle.PORTRAIT_VIDEO_TWO_ASSETS */: + media.videos.splice(0, 1); + media.portraitScreenshots.splice(0, 1); + break; + case "LI1" /* SearchAdDisplayStyle.LANDSCAPE_IMAGE_ONE_ASSET */: + media.landscapeScreenshots.splice(0, 1); + break; + case "PV1" /* SearchAdDisplayStyle.PORTRAIT_VIDEO_ONE_ASSET */: + case "LV1" /* SearchAdDisplayStyle.LANDSCAPE_VIDEO_ONE_ASSET */: + media.videos.splice(0, 1); + break; + default: + break; + } +} +/** + * A function to determine whether a calculated organic search display style is considered "full creative". + * @param displayStyle the display style calculated for an organic dupe result to validate. + * @returns whether the given display style is considered "full creative" for an organic result. + */ +function isDisplayStyleFullCreativeForOrganic(objectGraph, displayStyle, screenshotsDisplayStyle) { + switch (displayStyle) { + case "PV4" /* SearchAdDisplayStyle.PORTRAIT_VIDEO_FOUR_ASSETS */: + case "PI4" /* SearchAdDisplayStyle.PORTRAIT_IMAGE_FOUR_ASSETS */: + return true; + case "PV3" /* SearchAdDisplayStyle.PORTRAIT_VIDEO_THREE_ASSETS */: + case "PI3" /* SearchAdDisplayStyle.PORTRAIT_IMAGE_THREE_ASSETS */: + return completePortraitMediaCount(objectGraph, screenshotsDisplayStyle) === 3; + case "LI1" /* SearchAdDisplayStyle.LANDSCAPE_IMAGE_ONE_ASSET */: + case "LV1" /* SearchAdDisplayStyle.LANDSCAPE_VIDEO_ONE_ASSET */: + return true; + case "PV2" /* SearchAdDisplayStyle.PORTRAIT_VIDEO_TWO_ASSETS */: + case "PI2" /* SearchAdDisplayStyle.PORTRAIT_IMAGE_TWO_ASSETS */: + return completePortraitMediaCount(objectGraph) === 2; + default: + return false; + } +} +/** + * Indicates whether the proposed ad style is compatible with a previously computed ad style. + * If there is no previously computed style, we return true as it's assumed the new style is compatible. + * @param proposedAdStyle + * @param firstAdComputedStyle + * @returns a boolean indicating whether the styles are compatible. + */ +function isProposedAdStyleCompatible(proposedAdStyle, firstComputedAdStyle) { + if (serverData.isNull(firstComputedAdStyle)) { + return true; + } + let areStylesCompatible = true; + switch (proposedAdStyle) { + case "TEXT" /* SearchAdDisplayStyle.TEXT */: + areStylesCompatible = firstComputedAdStyle.style === "TEXT" /* SearchAdDisplayStyle.TEXT */; + break; + case "LV1" /* SearchAdDisplayStyle.LANDSCAPE_VIDEO_ONE_ASSET */: + case "LI1" /* SearchAdDisplayStyle.LANDSCAPE_IMAGE_ONE_ASSET */: + areStylesCompatible = + firstComputedAdStyle.style === "LV1" /* SearchAdDisplayStyle.LANDSCAPE_VIDEO_ONE_ASSET */ || + firstComputedAdStyle.style === "LI1" /* SearchAdDisplayStyle.LANDSCAPE_IMAGE_ONE_ASSET */; + break; + case "PI4" /* SearchAdDisplayStyle.PORTRAIT_IMAGE_FOUR_ASSETS */: + case "PI3" /* SearchAdDisplayStyle.PORTRAIT_IMAGE_THREE_ASSETS */: + case "PI2" /* SearchAdDisplayStyle.PORTRAIT_IMAGE_TWO_ASSETS */: + case "PI1" /* SearchAdDisplayStyle.PORTRAIT_IMAGE_ONE_ASSET */: + case "PV4" /* SearchAdDisplayStyle.PORTRAIT_VIDEO_FOUR_ASSETS */: + case "PV3" /* SearchAdDisplayStyle.PORTRAIT_VIDEO_THREE_ASSETS */: + case "PV2" /* SearchAdDisplayStyle.PORTRAIT_VIDEO_TWO_ASSETS */: + case "PV1" /* SearchAdDisplayStyle.PORTRAIT_VIDEO_ONE_ASSET */: + areStylesCompatible = + firstComputedAdStyle.style === "PI4" /* SearchAdDisplayStyle.PORTRAIT_IMAGE_FOUR_ASSETS */ || + firstComputedAdStyle.style === "PI3" /* SearchAdDisplayStyle.PORTRAIT_IMAGE_THREE_ASSETS */ || + firstComputedAdStyle.style === "PI2" /* SearchAdDisplayStyle.PORTRAIT_IMAGE_TWO_ASSETS */ || + firstComputedAdStyle.style === "PI1" /* SearchAdDisplayStyle.PORTRAIT_IMAGE_ONE_ASSET */ || + firstComputedAdStyle.style === "PV4" /* SearchAdDisplayStyle.PORTRAIT_VIDEO_FOUR_ASSETS */ || + firstComputedAdStyle.style === "PV3" /* SearchAdDisplayStyle.PORTRAIT_VIDEO_THREE_ASSETS */ || + firstComputedAdStyle.style === "PV2" /* SearchAdDisplayStyle.PORTRAIT_VIDEO_TWO_ASSETS */ || + firstComputedAdStyle.style === "PV1" /* SearchAdDisplayStyle.PORTRAIT_VIDEO_ONE_ASSET */; + break; + case "UI1_3x2" /* SearchAdDisplayStyle.UNRESTRICTED_IMAGE_ONE_ASSET */: + areStylesCompatible = firstComputedAdStyle.style === "UI1_3x2" /* SearchAdDisplayStyle.UNRESTRICTED_IMAGE_ONE_ASSET */; + break; + default: + areStylesCompatible = false; + } + return areStylesCompatible; +} +/** + * Extract the best media for the platform from the lockup. + * @param lockup a lockup created from which we should extract the media. + * @returns the best media for the platform. + */ +function platformMediaForLockup(lockup) { + let mediaPlatformUsedForDisplayStyle = null; + // This works on the assumption that when a lockup is created screenshots and trailers are sorted + // and the first in the array of each is what we attempt to display. + const firstTrailer = lockup.trailers[0]; + let videos = null; + if (serverData.isDefinedNonNullNonEmpty(firstTrailer)) { + firstTrailer.videos.sort((a, b) => { + return artworkSortingFunction(a.preview, b.preview); + }); + videos = firstTrailer.videos; + mediaPlatformUsedForDisplayStyle = firstTrailer.mediaPlatform; + } + // Grab the first array of platform screenshots, the one the search ad will display. + // Additional elements in this array might be for other platforms we wouldn't + // display (e.g. if current device is an iPhone then [1] might be iPad screenshots) + const platformScreenshots = lockup.screenshots[0]; + // Split the screenshots into portrait and landscape arrays. + // Landscape is preferred, so we use those first before portrait images, but we also need to know + // of any portrait screenshots so we can fill in any space left after a portrait video, which is + // preferred over a landscape screenshot. + const portraitScreenshots = []; + const landscapeScreenshots = []; + if (serverData.isDefinedNonNullNonEmpty(platformScreenshots)) { + platformScreenshots.artwork.forEach((artwork) => { + if (artwork.isPortrait()) { + portraitScreenshots.push(artwork); + } + else { + landscapeScreenshots.push(artwork); + } + }); + mediaPlatformUsedForDisplayStyle = platformScreenshots.mediaPlatform; + } + return { + portraitScreenshots: portraitScreenshots, + landscapeScreenshots: landscapeScreenshots, + alignedRegionArtwork: lockup.alignedRegionArtwork, + alignedRegionVideo: lockup.alignedRegionVideo, + videos: videos, + mediaPlatformUsedForDisplayStyle: mediaPlatformUsedForDisplayStyle, + }; +} +// endregion +function iadAttributesForType(data, type) { + let iAdDictionaryToUse = null; + const iAdOptions = mediaAttributes.attributeAsDictionary(data, "iads"); + const iAdJSONStringToUse = serverData.asString(iAdOptions, type); + if (iAdJSONStringToUse && iAdJSONStringToUse.length) { + iAdDictionaryToUse = JSON.parse(iAdJSONStringToUse); + } + return iAdDictionaryToUse; +} +function iAdDataTypeForAdvert(firstSearchResult, isDupe) { + if (serverData.isNullOrEmpty(firstSearchResult)) { + return "NOORGANIC" /* iAdDataType.NO_ORGANIC_RESULTS */; + } + if (isDupe) { + return "DUP" /* iAdDataType.DUPE_AD */; + } + return "NORMAL" /* iAdDataType.NORMAL */; +} +/** + * Removes unused media from the provided ad lockup, matching the selected ad display style. + * @param ad the ad lockup from which media should be removed. + * @param searchAdDisplayStyle the selected `SearchAdDisplayStyle` to match. + * @param isDupe if the given ad is a dupe of the first organic search result. + * @param isFirstAd if the given ad is the first search ad. + * @returns + */ +function modifyLockupToMatchAdDisplayStyle(ad, searchAdDisplayStyle, isDupe, isFirstAd) { + var _a, _b; + // Derive whether the ad lockup has a CPP (custom product page/ppid) that's being used for it's assets. + const hasCPP = serverData.isDefinedNonNullNonEmpty((_b = (_a = ad.impressionMetrics) === null || _a === void 0 ? void 0 : _a.fields) === null || _b === void 0 ? void 0 : _b.pageCustomId); + // If this ad lockup is a duplicate of the first organic search result and is not the first received ad, + // the organic search result gets the "first choice" of media. This flag indicates whether we should keep + // the "first set" or "second set" of media for this ad lockup. + // This logic does *not* apply if the ad is using a CPP. This is because the CPP from an ad only gets applied + // to an organic dupe result where the matching ad is in the first position. If not, the ad and organic are + // using a different set of assets, so there's no need to prefer one with the "first" or "second" set of media. + const wantsSecondSetOfMedia = isDupe && !isFirstAd && !hasCPP; + if (serverData.isDefinedNonNullNonEmpty(ad.trailers)) { + const firstTrailers = ad.trailers.shift(); + firstTrailers.videos.sort((a, b) => { + return artworkSortingFunction(a.preview, b.preview); + }); + ad.trailers.unshift(firstTrailers); + } + // Split the screenshots into portrait and landscape arrays. + // When removing screenshots from the array below, we can't just assume the first ones match the orientation + // we're using. For example, we only use portrait images with a portait video and some landscape images could + // be mixed in. + let mediaPlatformUsedForDisplayStyle; + let portraitScreenshots = []; + let landscapeScreenshots = []; + if (serverData.isDefinedNonNullNonEmpty(ad.screenshots)) { + const firstScreenshots = ad.screenshots.shift(); + firstScreenshots.artwork.forEach((artwork) => { + if (artwork.isPortrait()) { + portraitScreenshots.push(artwork); + } + else { + landscapeScreenshots.push(artwork); + } + }); + mediaPlatformUsedForDisplayStyle = firstScreenshots.mediaPlatform; + } + switch (searchAdDisplayStyle.style) { + case "PI4" /* SearchAdDisplayStyle.PORTRAIT_IMAGE_FOUR_ASSETS */: + // We've previously determined advert can support this type. Remove videos and landscape images so + // advert only displays portrait images. + // With 4 portrait assets, we need to make sure that the ads and organic can be full by borrowing some assets from the organic. + // However, per product, if the app has 5 or less portrait assets, then it will go to the text ad as today. + // So we need to check for 5 assets as the lower bound (exclusive) and 8 (exclusive) as the upper for + // the extra work to reuse assets. + ad.trailers = null; + landscapeScreenshots = null; + ad.screenshotsDisplayStyle = "four-screenshots"; + if (wantsSecondSetOfMedia) { + if (portraitScreenshots.length > 5 && portraitScreenshots.length < 8) { + const usedAssets = portraitScreenshots.splice(0, 4); + const reuseCount = 4 - portraitScreenshots.length; + const reuseAssets = usedAssets.splice(usedAssets.length - reuseCount); + portraitScreenshots.unshift(...reuseAssets); + } + else { + portraitScreenshots.splice(0, 4); + } + } + else { + portraitScreenshots.splice(4); + } + break; + case "PI3" /* SearchAdDisplayStyle.PORTRAIT_IMAGE_THREE_ASSETS */: + // We've previously determined advert can support this type. Remove videos and landscape images so + // advert only displays portrait images. + ad.trailers = null; + landscapeScreenshots = null; + if (wantsSecondSetOfMedia) { + portraitScreenshots.splice(0, 3); + } + else { + portraitScreenshots.splice(3); + } + break; + case "PI2" /* SearchAdDisplayStyle.PORTRAIT_IMAGE_TWO_ASSETS */: + // We've previously determined advert can support this type. Remove videos and landscape images so + // advert only displays portrait images. + ad.trailers = null; + landscapeScreenshots = null; + if (wantsSecondSetOfMedia) { + portraitScreenshots.splice(0, 2); + } + else { + portraitScreenshots.splice(2); + } + break; + case "PI1" /* SearchAdDisplayStyle.PORTRAIT_IMAGE_ONE_ASSET */: + // We've previously determined advert can support this type. Remove videos and landscape images so + // advert only displays portrait images. + ad.trailers = null; + landscapeScreenshots = null; + if (wantsSecondSetOfMedia) { + portraitScreenshots.splice(0, 1); + } + else { + portraitScreenshots.splice(1); + } + break; + case "LI1" /* SearchAdDisplayStyle.LANDSCAPE_IMAGE_ONE_ASSET */: + // We've previously determined advert can support this type. Remove videos and portrait images so + // advert only displays landscape images. + ad.trailers = null; + portraitScreenshots = null; + if (wantsSecondSetOfMedia) { + landscapeScreenshots.splice(0, 1); + } + else { + landscapeScreenshots.splice(1); + } + break; + case "PV4" /* SearchAdDisplayStyle.PORTRAIT_VIDEO_FOUR_ASSETS */: + // We've previously determined advert can support this type. Remove landscape images so + // advert only displays portrait assets. + // With 4 portrait assets, we need to make sure that the ads and organic can be full by borrowing some assets from the organic. + // However, per product, if the app has 5 or less portrait assets, then it will go to the text ad as today. + // Since this style includes a portrait video, we only need to check for 4 assets (exclusive) as the lower bound and 7 (exclusive) as the upper for + // the extra work to reuse assets + landscapeScreenshots = null; + ad.screenshotsDisplayStyle = "four-screenshots"; + if (wantsSecondSetOfMedia) { + ad.trailers[0].videos.splice(0, 1); + if (portraitScreenshots.length > 4 && portraitScreenshots.length < 7) { + const usedAssets = portraitScreenshots.splice(0, 3); + const reuseCount = 3 - portraitScreenshots.length; + const reuseAssets = usedAssets.splice(usedAssets.length - reuseCount); + portraitScreenshots.unshift(...reuseAssets); + } + else { + portraitScreenshots.splice(0, 3); + } + } + else { + ad.trailers[0].videos.splice(1); + portraitScreenshots.splice(3); + } + break; + case "PV3" /* SearchAdDisplayStyle.PORTRAIT_VIDEO_THREE_ASSETS */: + // We've previously determined advert can support this type. Remove landscape images so + // advert only displays portrait assets. + landscapeScreenshots = null; + if (wantsSecondSetOfMedia) { + ad.trailers[0].videos.splice(0, 1); + portraitScreenshots.splice(0, 2); + } + else { + ad.trailers[0].videos.splice(1); + portraitScreenshots.splice(2); + } + break; + case "PV2" /* SearchAdDisplayStyle.PORTRAIT_VIDEO_TWO_ASSETS */: + // We've previously determined advert can support this type. Remove landscape images so + // advert only displays portrait assets. + landscapeScreenshots = null; + if (wantsSecondSetOfMedia) { + ad.trailers[0].videos.splice(0, 1); + portraitScreenshots.splice(0, 1); + } + else { + ad.trailers[0].videos.splice(1); + portraitScreenshots.splice(1); + } + break; + case "LV1" /* SearchAdDisplayStyle.LANDSCAPE_VIDEO_ONE_ASSET */: + case "PV1" /* SearchAdDisplayStyle.PORTRAIT_VIDEO_ONE_ASSET */: + // We've determined advert can support this type. Remove images so only the single video displays + if (wantsSecondSetOfMedia) { + ad.trailers[0].videos.splice(0, 1); + } + else { + ad.trailers[0].videos.splice(1); + } + landscapeScreenshots = null; + portraitScreenshots = null; + break; + case "TEXT" /* SearchAdDisplayStyle.TEXT */: + // All adverts can support text only, remove all screenshots and trailers so they display in this style + ad.trailers = null; + landscapeScreenshots = null; + portraitScreenshots = null; + break; + default: + break; + } + if (serverData.isDefinedNonNullNonEmpty(ad.trailers)) { + const firstTrailers = ad.trailers.shift(); + ad.trailers = [firstTrailers]; + } + // Combine the remaining landscape and portrait screenshots, and set them back on the ad as the only values. + const allScreenshots = [].concat(...[landscapeScreenshots, portraitScreenshots].filter(serverData.isDefinedNonNull)); + if (serverData.isDefinedNonNullNonEmpty(allScreenshots)) { + const screenshots = new models.Screenshots(allScreenshots, mediaPlatformUsedForDisplayStyle); + ad.screenshots = [screenshots]; + } + else { + ad.screenshots = null; + } + return ad; +} +function completePortraitMediaCount(objectGraph, searchExperimentsData = null) { + if (objectGraph.client.isPhone) { + return allowsFourScreenshots(searchExperimentsData) ? 4 : 3; + } + else { + return 2; + } +} +function debugDescriptionForStyle(styleContainer) { + if (serverData.isNullOrEmpty(styleContainer)) { + return ""; + } + let mediaTypeString = ""; + if (styleContainer && styleContainer.mediaPlatform) { + mediaTypeString = ` derived from ${styleContainer.mediaPlatform.mediaType} media`; + } + return `${styleContainer.style}${mediaTypeString}`; +} +/** + * Stores a value representing the current setting of the native ad debug logging. + * This starts as `null`, and the first time we log we ask native to provide the current value. + * Requires a re-bootstrap to update, so we avoid sending a message back to native on every call + * if logging is disabled. + */ +let isNativeAdLoggingEnabled = null; +export function adLogger(objectGraph, message) { + objectGraph.console.log(`[Ads] ${message}`); + if (objectGraph.client.buildType === "internal" && + objectGraph.isAvailable(ads) && + serverData.isDefinedNonNull(objectGraph.ads.debugLog)) { + // If we haven't asked native for whether the debug setting for ad logging is enabled, do it now. + if (serverData.isNull(isNativeAdLoggingEnabled) && + serverData.isDefinedNonNull(objectGraph.ads.isNativeAdLoggingEnabled)) { + isNativeAdLoggingEnabled = objectGraph.ads.isNativeAdLoggingEnabled(); + } + if (isNativeAdLoggingEnabled) { + objectGraph.ads.debugLog(message); + } + } +} +const artworkSortingFunction = (a, b) => { + const aLandscape = a.isLandscape(); + const bLandscape = b.isLandscape(); + if (aLandscape === bLandscape) { + return 0; + } + if (aLandscape) { + return -1; + } + return 1; +}; +/** + * A wrapper around the ad search result to include the ad's displayStyle it will present with + */ +export class SearchAdsDisplayStyleResultContainer { +} +/** + * Removes any media used in a provided ad search result from the organic search result. + * @param objectGraph the Object Graph + * @param adResult the ad search result + * @param searchResult the organic search result + * @param searchExperimentsData the metadata for search result experiemnts + * @param searchAdDisplayStyle the display style for the ad that is matching with the organic + */ +export function dedupeAdMediaFromMatchingResult(objectGraph, adResult, searchResult, searchExperimentsData, searchAdDisplayStyle) { + var _a; + // Run de-duping on `AppSearchResult`s and `AppEventSearchResult`s. + // We specifically run this for `AppEventSearchResult`s because they can be presented in the search results + // as either a regular search result (ie. MixedMediaLockup) (if the app isn't installed), or as an IAE search + // result (if it is installed). This means we need to run de-duping in case it appears as a regular result, + // matching behaviour of the `AppSearchResult`. + if (!(searchResult instanceof models.AppSearchResult || searchResult instanceof models.AppEventSearchResult)) { + return; + } + const searchLockup = searchResult.lockup; + const adLockup = adResult.lockups[0]; + if (adLockup.adamId !== searchLockup.adamId) { + return; + } + const usedTemplateUrls = new Set(); + if (serverData.isDefinedNonNullNonEmpty(adLockup.screenshots)) { + for (const screenshot of adLockup.screenshots[0].artwork) { + usedTemplateUrls.add(screenshot.template); + } + } + if (serverData.isDefinedNonNullNonEmpty(adLockup.trailers)) { + for (const video of adLockup.trailers[0].videos) { + usedTemplateUrls.add(video.preview.template); + } + } + if (serverData.isDefinedNonNullNonEmpty(searchLockup.screenshots)) { + const filteredArtwork = searchLockup.screenshots[0].artwork.filter((artwork) => { + return !usedTemplateUrls.has(artwork.template); + }); + searchLockup.screenshots[0] = new models.Screenshots(filteredArtwork, searchLockup.screenshots[0].mediaPlatform); + } + if (serverData.isDefinedNonNullNonEmpty(searchLockup.trailers)) { + const filteredVideos = searchLockup.trailers[0].videos.filter((video) => { + return !usedTemplateUrls.has(video.preview.template); + }); + searchLockup.trailers[0] = new models.Trailers(filteredVideos, searchLockup.trailers[0].mediaPlatform); + } + /// Exit early if we don't need to do anything special for the 4 screenshots case + if (((_a = searchExperimentsData === null || searchExperimentsData === void 0 ? void 0 : searchExperimentsData.displayStyle) === null || _a === void 0 ? void 0 : _a.screenshots) !== "four-screenshots") { + return; + } + // We need to possibly reuse some of the screenshots from the ad to make sure the organic has enough screenshots to show; + // This is only if we will show 4 portrait assets + const padSearchLockupScreenshotsToPreferredSize = (totalRequiredArtwork) => { + const currentScreenshots = searchLockup.screenshots[0].artwork; + if (currentScreenshots.length >= totalRequiredArtwork) { + return; + } + let screenshotsStillNeededCount = totalRequiredArtwork - currentScreenshots.length; + const adArtworksToReuse = adLockup.screenshots[0].artwork.slice().reverse(); + for (const artwork of adArtworksToReuse) { + if (screenshotsStillNeededCount <= 0) { + return; + } + searchLockup.screenshots[0].artwork.unshift(artwork); + screenshotsStillNeededCount -= 1; + } + }; + switch (searchAdDisplayStyle) { + case "PV4" /* SearchAdDisplayStyle.PORTRAIT_VIDEO_FOUR_ASSETS */: + case "PI4" /* SearchAdDisplayStyle.PORTRAIT_IMAGE_FOUR_ASSETS */: + padSearchLockupScreenshotsToPreferredSize(4); + break; + default: + break; + } +} +/** + * As part of the CPP implementation for Search Results ads, if the first ad result has a CPP applied, and there is a dupe scenario + * (ie. the first ad matches the first organic result) the CPP must also be applied to the first organic result. + * Here we update the CPP data on the organic result to match the first ad result, if applicable. + * @param objectGraph The object graph + * @param adData The raw ad data used to build the ad results. + * @param adResult The built ad result. + * @param searchResultData The data for the first organic search result. + * @param installStates A mapping of adamIDs to their respective install states that is used to determine if the app is currently installed by the user + * @param appStates A mapping of adamIDs to their respective app state to determine if the ad/first result has been installed by the user in the past + * @param metricsOptions Metrics options for built models. + * @param personalizationDataContainer The data container to use for personalizing the data. + */ +export function updateDupeOrganicResultCPPData(objectGraph, adData, adResult, searchResultData, installStates, appStates, metricsOptions, personalizationDataContainer) { + var _a, _b; + const adLockup = adResult.lockups[0]; + if (adLockup.adamId !== searchResultData.id) { + return; + } + // Get the data for the first ad lockup. This *should* just be the first, but + // it's possible the first data got dropped when building the ad lockups. + const dataForAdLockup = adData.find((data) => data.id === adLockup.adamId); + const shouldSkipUpdatingOrganicCPP = searchResultWillUseAppEventDisplay(objectGraph, searchResultData, installStates, appStates, metricsOptions, personalizationDataContainer); + if (shouldSkipUpdatingOrganicCPP) { + return; + } + // The first ad lockup matches the first organic result. Apply the Ad cppId to matching organic result. + updatePPIDInData((_b = (_a = dataForAdLockup === null || dataForAdLockup === void 0 ? void 0 : dataForAdLockup.meta) === null || _a === void 0 ? void 0 : _a.cppData) === null || _b === void 0 ? void 0 : _b["ppid"], searchResultData); +} +/** + * Update the ppid for the given data. + * If provided `ppid` is `null`, the field will be deleted. + * If `cppData` does not already exist on `meta`, it will be created. + * @param ppid The ppid to update in the data. Can be null, which will delete the field. + * @param data The data object to update. + */ +function updatePPIDInData(ppid, data) { + var _a; + let meta = shallowCopyOf(data.meta); + if (serverData.isNull(ppid)) { + (_a = meta === null || meta === void 0 ? void 0 : meta.cppData) === null || _a === void 0 ? true : delete _a["ppid"]; + } + else { + if (serverData.isNull(meta)) { + meta = {}; + } + if (serverData.isNull(meta.cppData)) { + meta.cppData = {}; + } + meta.cppData["ppid"] = ppid; + } + data.meta = meta; +} +/** + * Whether or not platform supports adverts. + */ +export function platformSupportsAdverts(objectGraph) { + return ((clientIdentifierSupportsAdverts(objectGraph) && objectGraph.host.isiOS) || + objectGraph.host.platform === "unknown"); +} +function clientIdentifierSupportsAdverts(objectGraph) { + return (objectGraph.host.clientIdentifier === client.appStoreIdentifier || + objectGraph.host.clientIdentifier === client.productPageExtensionIdentifier); +} +//# sourceMappingURL=search-ads.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/search/search-common.js b/node_modules/@jet-app/app-store/tmp/src/common/search/search-common.js new file mode 100644 index 0000000..94e7438 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/search/search-common.js @@ -0,0 +1,59 @@ +/** + * Common operations for builders in search tab. + */ +// region Search Term State +/** + * Build the `SearchTermContext` for a given response triggered by a search for `term`, possibly originating from `originatingTerm` + * @param requestDescriptor The options that describe fetch request. + * @param searchResponse The sequential response that was returned. + */ +export function createTermContextForSpellcheckedSequentialResponse(objectGraph, requestDescriptor, searchResponse) { + var _a, _b, _c, _d, _e, _f, _g; + return { + term: requestDescriptor.term, + suggestedTerm: (_b = (_a = searchResponse.results) === null || _a === void 0 ? void 0 : _a.spellCheck) === null || _b === void 0 ? void 0 : _b.suggestedTerm, + correctedTerm: (_d = (_c = searchResponse.results) === null || _c === void 0 ? void 0 : _c.spellCheck) === null || _d === void 0 ? void 0 : _d.correctedTerm, + resultsTerm: (_g = (_f = (_e = searchResponse.results) === null || _e === void 0 ? void 0 : _e.spellCheck) === null || _f === void 0 ? void 0 : _f.correctedTerm) !== null && _g !== void 0 ? _g : requestDescriptor.term, + originatingTerm: requestDescriptor.originatingTerm, + }; +} +/** + * Create a search term context for the segmented search results completed fetch + * @param objectGraph The app store object graph + * @param requestDescriptor The search request descriptor + * @param searchResponse The response for the segmented search results page + * @returns The search term context for the segmented search results fetch + */ +export function createTermContextForSpellcheckedGroupedResponse(objectGraph, requestDescriptor, searchResponse) { + var _a, _b, _c, _d, _e, _f, _g; + return { + term: requestDescriptor.term, + suggestedTerm: (_b = (_a = searchResponse.results) === null || _a === void 0 ? void 0 : _a.spellCheck) === null || _b === void 0 ? void 0 : _b.suggestedTerm, + correctedTerm: (_d = (_c = searchResponse.results) === null || _c === void 0 ? void 0 : _c.spellCheck) === null || _d === void 0 ? void 0 : _d.correctedTerm, + resultsTerm: (_g = (_f = (_e = searchResponse.results) === null || _e === void 0 ? void 0 : _e.spellCheck) === null || _f === void 0 ? void 0 : _f.correctedTerm) !== null && _g !== void 0 ? _g : requestDescriptor.term, + originatingTerm: requestDescriptor.originatingTerm, + }; +} +/** + * Build the `SearchTermContext` purely from `requestDescriptor` for requests that don't have spellchecking. + * @param requestDescriptor The options that describe fetch request. + */ +export function createTermContextForNonspellcheckRequest(objectGraph, requestDescriptor) { + return { + term: requestDescriptor.term, + resultsTerm: requestDescriptor.term, + originatingTerm: requestDescriptor.originatingTerm, + }; +} +// endregion +// region Constants +/** + * The field name desired within the `meta.metrics` in search response. + */ +export const searchMetricsDataSetID = "data.search.dataSetId"; +/** + * The actual field name within the `meta.metrics` in search response. + */ +export const legacySearchMetricsDataSetID = "dataSetId"; +// endregion +//# sourceMappingURL=search-common.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/search/search-facets.js b/node_modules/@jet-app/app-store/tmp/src/common/search/search-facets.js new file mode 100644 index 0000000..dd0ccf4 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/search/search-facets.js @@ -0,0 +1,146 @@ +/** + * Build methods for Search Facets. + */ +import * as models from "../../api/models"; +import * as serverData from "../../foundation/json-parsing/server-data"; +import { categoryListFromApiResponse } from "../categories"; +import * as metricsClickHelpers from "../metrics/helpers/clicks"; +/** + * Create Search Facets. Theres platform specific variations here. + * @param requestFacets Facets in current request. + * @param categoryFacetsData Additional + */ +export function createSearchFacets(objectGraph, requestFacets, categoryFacetsData) { + const selectedFacets = requestFacets || {}; + const facets = []; + // Device Type + if (objectGraph.client.deviceType !== "mac") { + facets.push(new models.SearchFacetSet("targetPlatform", [ + new models.SearchFacetValue(objectGraph.loc.string("Search.Facets.iPadAndIPhone"), null, selectedFacets["targetPlatform"]), + new models.SearchFacetValue(objectGraph.loc.string("Search.Facets.iPhoneOnly"), "iphone", selectedFacets["targetPlatform"]), + ])); + } + // Price + facets.push(new models.SearchFacetSet("price", [ + new models.SearchFacetValue(objectGraph.loc.string("SEARCH_FACET_ANY_PRICE", "Any"), null, selectedFacets["price"]), + new models.SearchFacetValue(objectGraph.loc.string("SEARCH_FACET_FREE", "Any"), "free", selectedFacets["price"]), + ])); + // Categories + const categoryList = categoryListFromApiResponse(objectGraph, categoryFacetsData, false); + if (categoryList) { + const serverCategories = categoryList.categories; + if (serverCategories.length) { + const genreFacetValues = serverCategories + .filter((category) => { + return serverData.isDefinedNonNull(category.genreId); + }) + .map((category) => { + return new models.SearchFacetValue(category.name, category.genreId, selectedFacets["genre"]); + }); + genreFacetValues.unshift(new models.SearchFacetValue(objectGraph.loc.string("SEARCH_FACET_ANY_CATEGORY", "Any"), null, selectedFacets["genre"])); + facets.push(new models.SearchFacetSet("genre", genreFacetValues)); + } + } + const searchSortOptions = objectGraph.bag.searchSortOptions; + // Sorts + const sortFacetValues = []; + sortFacetValues.push(new models.SearchFacetValue(objectGraph.loc.string("SEARCH_FACET_RELEVANCE"), null, selectedFacets["sort"])); + for (const sortValue of searchSortOptions) { + sortFacetValues.push(new models.SearchFacetValue(objectGraph.loc.string("SEARCH_FACET_" + sortValue), sortValue, selectedFacets["sort"])); + } + if (sortFacetValues.length > 1) { + facets.push(new models.SearchFacetSet("sort", sortFacetValues)); + } + const serverAgeBands = objectGraph.bag.ageBands; + const ageBandFacetValues = serverAgeBands.map((ageBand) => { + return new models.SearchFacetValue(serverData.asString(ageBand, "name"), serverData.asString(ageBand, "ageBandId"), selectedFacets["ages"]); + }); + if (ageBandFacetValues.length > 0 && objectGraph.client.deviceType !== "mac") { + facets.push(new models.SearchFacetSet("ages", ageBandFacetValues)); + } + return facets; +} +/** + * Create Search Facets. Theres platform specific variations here. + * @param categoryFacetsData Additional + */ +export function createSearchPageFacets(objectGraph, categoryFacetsData) { + let categoryFacet = null; + let sortsFacet = null; + let ageBandsFacet = null; + // Platform + const deviceTypeFacet = new models.PageFacetsFacet("targetPlatform", "targetPlatform", objectGraph.loc.string("SEARCH_FACET_TYPE_TITLE_DEVICE_TYPE"), "singleSelection", [ + new models.PageFacetOption(objectGraph.loc.string("Search.Facets.iPadAndIPhone"), null), + new models.PageFacetOption(objectGraph.loc.string("Search.Facets.iPhoneOnly"), "iphone"), + ], null, null, pageFacetChangeAction(objectGraph, "targetPlatform")); + // Price + const priceFacet = new models.PageFacetsFacet("filter[price]", "filter[price]", objectGraph.loc.string("SEARCH_FACET_TYPE_TITLE_PRICE"), "singleSelection", [ + new models.PageFacetOption(objectGraph.loc.string("SEARCH_FACET_ANY_PRICE", "Any"), null), + new models.PageFacetOption(objectGraph.loc.string("SEARCH_FACET_FREE", "Any"), "free"), + ], null, null, pageFacetChangeAction(objectGraph, "price")); + // Categories + const categoryList = categoryListFromApiResponse(objectGraph, categoryFacetsData, false); + if (categoryList) { + const serverCategories = categoryList.categories; + if (serverCategories.length) { + const categories = serverCategories.filter((category) => { + return serverData.isDefinedNonNull(category.genreId); + }); + categoryFacet = new models.PageFacetsFacet("filter[genre]", "filter[genre]", objectGraph.loc.string("SEARCH_FACET_TYPE_TITLE_CATEGORY"), "singleSelection", [new models.PageFacetOption(objectGraph.loc.string("SEARCH_FACET_ANY_CATEGORY", "Any"), null)], null, null, pageFacetChangeAction(objectGraph, "genre")); + for (const category of categories) { + categoryFacet.options.push(new models.PageFacetOption(category.name, category.genreId)); + } + } + } + // Sorts + const searchSortOptions = objectGraph.bag.searchSortOptions; + sortsFacet = new models.PageFacetsFacet("sort", "sort", objectGraph.loc.string("SEARCH_FACET_TYPE_TITLE_SORT"), "singleSelection", [new models.PageFacetOption(objectGraph.loc.string("SEARCH_FACET_RELEVANCE"), null)], null, null, pageFacetChangeAction(objectGraph, "sort")); + for (const sortValue of searchSortOptions) { + sortsFacet.options.push(new models.PageFacetOption(objectGraph.loc.string("SEARCH_FACET_" + sortValue), sortValue)); + } + // Age Bands + const serverAgeBands = objectGraph.bag.ageBands; + const ageBandFacetOptions = serverAgeBands.map((ageBand) => { + return new models.PageFacetOption(serverData.asString(ageBand, "name"), serverData.asString(ageBand, "ageBandId")); + }); + if (ageBandFacetOptions.length > 0 && objectGraph.client.deviceType !== "mac") { + ageBandsFacet = new models.PageFacetsFacet("filter[ages]", "filter[ages]", objectGraph.loc.string("SEARCH_FACET_TYPE_TITLE_AGE_BAND"), "singleSelection", ageBandFacetOptions, null, null, pageFacetChangeAction(objectGraph, "ages")); + } + const pageFacets = new models.PageFacets([], false, null); + if (objectGraph.client.isMac) { + pageFacets.facetGroups.push(new models.PageFacetsGroup([priceFacet])); + if (serverData.isDefinedNonNull(categoryFacet)) { + pageFacets.facetGroups.push(new models.PageFacetsGroup([categoryFacet])); + } + pageFacets.facetGroups.push(new models.PageFacetsGroup([sortsFacet])); + } + else { + const facets = [deviceTypeFacet, priceFacet]; + if (serverData.isDefinedNonNull(categoryFacet)) { + facets.push(categoryFacet); + } + facets.push(sortsFacet); + if (serverData.isDefinedNonNull(ageBandsFacet)) { + facets.push(ageBandsFacet); + } + for (const facet of facets) { + facet.showsSelectedOptions = true; + } + pageFacets.facetGroups.push(new models.PageFacetsGroup(facets)); + } + return pageFacets; +} +export function createDefaultSelectedFacetOptions(objectGraph) { + return { + "targetPlatform": [new models.PageFacetOption(objectGraph.loc.string("SEARCH_FACET_IPAD_ONLY"), null)], + "filter[price]": [new models.PageFacetOption(objectGraph.loc.string("SEARCH_FACET_ANY_PRICE", "Any"), null)], + "sort": [new models.PageFacetOption(objectGraph.loc.string("SEARCH_FACET_RELEVANCE"), null)], + "filter[genre]": [new models.PageFacetOption(objectGraph.loc.string("SEARCH_FACET_ANY_CATEGORY", "Any"), null)], + }; +} +function pageFacetChangeAction(objectGraph, facetParameter) { + const action = new models.BlankAction(); + metricsClickHelpers.addClickEventToPageFacetsChangeAction(objectGraph, action, facetParameter); + return action; +} +//# sourceMappingURL=search-facets.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/search/search-landing-page-utils.js b/node_modules/@jet-app/app-store/tmp/src/common/search/search-landing-page-utils.js new file mode 100644 index 0000000..f26b78b --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/search/search-landing-page-utils.js @@ -0,0 +1,386 @@ +import * as validation from "@jet/environment/json/validation"; +import { FetchTimingMetricsBuilder } from "@jet/environment/metrics/fetch-timing-metrics-builder"; +import { isNothing, isSome } from "@jet/environment/types/optional"; +import { PageRefreshPolicy, SearchAction, SearchFocusPage, SearchLandingPage, Shelf } from "../../api/models"; +import * as mediaRequestUtils from "../../common/builders/url-mapping-utils"; +import * as appStoreExperiments from "../../foundation/experimentation/app-store-experiments"; +import { ExperimentAreaId } from "../../foundation/experimentation/experiment-area-id"; +import * as serverData from "../../foundation/json-parsing/server-data"; +import * as mediaAttributes from "../../foundation/media/attributes"; +import * as mediaDataFetching from "../../foundation/media/data-fetching"; +import * as mediaNetwork from "../../foundation/media/network"; +import * as mediaRelationship from "../../foundation/media/relationships"; +import { Parameters, Path, Protocol } from "../../foundation/network/url-constants"; +import * as impressionDemotion from "../../common/personalization/on-device-impression-demotion"; +import { iadInfoFromOnDeviceAdResponse, isAdPlacementEnabled } from "../ads/ad-common"; +import * as adIncidents from "../ads/ad-incident-recorder"; +import { fetchAds as landingAdFetchFetchAds } from "../ads/on-device-ad-fetch"; +import * as landingAdStitch from "../ads/on-device-ad-stitch"; +import * as contentArtwork from "../content/artwork/artwork"; +import * as groupingCommon from "../grouping/grouping-common"; +import * as metricsHelpersClicks from "../metrics/helpers/clicks"; +import * as metricsHelpersLocation from "../metrics/helpers/location"; +import * as metricsHelpersPage from "../metrics/helpers/page"; +import * as metricsHelpersUtil from "../metrics/helpers/util"; +import * as onDevicePersonalization from "../personalization/on-device-personalization"; +import * as productPageVariants from "../product-page/product-page-variants"; +import { areAppTagsEnabled } from "../util/app-tags-util"; +import { isFeatureEnabledForCurrentUser } from "../util/lottery"; +import { SearchPageType } from "./content/search-shelves"; +import * as searchLandingCohort from "./landing/search-landing-cohort"; +import * as searchLandingShelfController from "./landing/search-landing-shelf-controller"; +/** + * Determines whether or not the user will use the legacy Search Landing Page protocol + * @param objectGraph The App Store Object Graph + * @returns Whether or not the user will use the legacy Search Landing Page protocol + */ +function shouldUseProtocolV1(objectGraph) { + if (objectGraph.client.isVision) { + return false; // visionOS always uses the modern V2 protocol. + } + if (objectGraph.client.isWeb) { + return false; // the "web" expects to use the V2 protocol as well + } + if (!objectGraph.bag.supportsSearchLandingPageV2) { + return true; // Use V1 protocol when V2 is unsupported + } + // Use V1 protocol based on the bags rollout rate for V2 protocol. + return !isFeatureEnabledForCurrentUser(objectGraph, objectGraph.bag.searchLandingPageV2RolloutRate); +} +async function fetchSearchLandingPage(objectGraph, fetchAds) { + if (shouldUseProtocolV1(objectGraph)) { + return await fetchSearchLandingPageV1(objectGraph, fetchAds); + } + if (objectGraph.bag.mediaAPISearchFocusEnabled) { + return await fetchSearchLandingPageV2WithFocusPage(objectGraph, fetchAds); + } + return await fetchSearchLandingPageV2(objectGraph, fetchAds); +} +async function fetchSearchLandingPageV1(objectGraph, fetchAds) { + const searchLandingRequest = new mediaDataFetching.Request(objectGraph) + .forType("landing") + .includingAgeRestrictions() + .includingAdditionalPlatforms(mediaDataFetching.defaultAdditionalPlatformsForClient(objectGraph)) + .usingCustomAttributes(productPageVariants.shouldFetchCustomAttributes(objectGraph)); // for `extend=customArtwork` + searchLandingRequest.targetResourceType = "groupings"; + const cohortIdOrNil = searchLandingCohort.cohortIdForUser(objectGraph, objectGraph.user.dsid); + if ((cohortIdOrNil === null || cohortIdOrNil === void 0 ? void 0 : cohortIdOrNil.length) > 0) { + searchLandingRequest.addingQuery("clusterId", cohortIdOrNil); + } + const fetchTimingMetricsBuilder = new FetchTimingMetricsBuilder(); + const modifiedObjectGraph = objectGraph.addingFetchTimingMetricsBuilder(fetchTimingMetricsBuilder); + const fetchSearchLanding = mediaNetwork.fetchData(modifiedObjectGraph, searchLandingRequest); + return await Promise.all([fetchSearchLanding, fetchAds]).then(([responseData, adResponse]) => { + return fetchTimingMetricsBuilder.measureModelConstruction(() => { + return landingPageFromResponseV1(modifiedObjectGraph, responseData, adResponse); + }); + }); +} +/** + * Creates `SearchLandingPage` model from the V1 Search Landing Page protocol + * @param objectGraph The App Store Object Graph + * @param landingPageResponse The response from the fetch + * @param adResponse The response from the ad fetch + * @returns A `SearchLandingPage` model from the V1 Search Landing Page protocol + */ +function landingPageFromResponseV1(objectGraph, landingPageResponse, adResponse) { + const mediaApiGroupingDataArray = serverData.asArrayOrEmpty(landingPageResponse, "results.contents"); + const mediaApiGroupingData = mediaApiGroupingDataArray[0]; + if (serverData.isNullOrEmpty(mediaApiGroupingData)) { + return null; + } + if (!mediaRelationship.hasRelationship(mediaApiGroupingData, "tabs")) { + return null; + } + const groupingGenreAdamId = mediaAttributes.attributeAsString(mediaApiGroupingData, "id"); + const pageInformation = metricsHelpersPage.metricsPageInformationFromMediaApiResponse(objectGraph, "Genre", mediaApiGroupingData.id, landingPageResponse); + const onDevicePersonalizationMetricsData = onDevicePersonalization.metricsData(objectGraph); + pageInformation.recoMetricsData = metricsHelpersUtil.combinedRecoMetricsDataFromMetricsData(pageInformation.recoMetricsData, null, onDevicePersonalizationMetricsData); + pageInformation.iAdInfo = iadInfoFromOnDeviceAdResponse(objectGraph, "searchLanding", adResponse); + const adIncidentRecorder = adIncidents.newRecorder(objectGraph, pageInformation.iAdInfo); + adIncidents.recordAdResponseEventsIfNeeded(objectGraph, adIncidentRecorder, adResponse); + const groupingParseContext = { + shelves: [], + metricsPageInformation: pageInformation, + metricsLocationTracker: metricsHelpersLocation.newLocationTracker(), + pageGenreAdamId: groupingGenreAdamId, + pageGenreId: mediaAttributes.attributeAsNumber(mediaApiGroupingData, "genre"), + hasAuthenticatedUser: serverData.isDefinedNonNull(objectGraph.user.dsid), + isSearchLandingPage: true, + adStitcher: landingAdStitch.adStitcherForOnDeviceSLPAdvertData(objectGraph, adResponse), + adIncidentRecorder: adIncidentRecorder, + }; + const flattenedGrouping = groupingCommon.flattenMediaApiGroupingData(objectGraph, mediaApiGroupingData); + groupingCommon.insertInitialShelvesIntoGroupingParseContext(objectGraph, flattenedGrouping, groupingParseContext); + const page = new SearchLandingPage(groupingParseContext.shelves); + // Page refresh + const refreshPolicy = new PageRefreshPolicy("timeSinceOnScreen", objectGraph.bag.searchLandingPageRefreshUpdateDelayInterval, objectGraph.bag.searchLandingPageOffscreenRefreshInterval, null); + page.pageRefreshPolicy = refreshPolicy; + // Ad Incidents + page.adIncidents = adIncidents.recordedIncidents(objectGraph, groupingParseContext.adIncidentRecorder); + metricsHelpersPage.addMetricsEventsToPageWithInformation(objectGraph, page, groupingParseContext.metricsPageInformation); + return page; +} +function makeSearchLandingRequestV2(objectGraph, fetchAds) { + const searchLandingRequest = new mediaDataFetching.Request(objectGraph) + .forType("landing:new-protocol") + .includingAgeRestrictions() + .includingAdditionalPlatforms(mediaDataFetching.defaultAdditionalPlatformsForClient(objectGraph)) + .usingCustomAttributes(productPageVariants.shouldFetchCustomAttributes(objectGraph)) // for `extend=customArtwork` + .includingScopedRelationships("search-recommendations", ["contents"]) + .addingQuery("name", "search-landing"); + if (areAppTagsEnabled(objectGraph, "slp")) { + mediaRequestUtils.configureTagsForMediaRequest(searchLandingRequest); + } + if (objectGraph.client.isVision || objectGraph.client.isWeb) { + searchLandingRequest.includingScopedAttributes("editorial-items", ["editorialClientParams"]); + } + const cohortIdOrNil = searchLandingCohort.cohortIdForUser(objectGraph, objectGraph.user.dsid); + if ((cohortIdOrNil === null || cohortIdOrNil === void 0 ? void 0 : cohortIdOrNil.length) > 0) { + searchLandingRequest.addingQuery("clusterId", cohortIdOrNil); + } + if (objectGraph.client.isiOS) { + searchLandingRequest.addingQuery("meta", "adDisplayStyle"); + } + return searchLandingRequest; +} +async function fetchSearchLandingPageV2(objectGraph, fetchAds) { + const searchLandingRequest = makeSearchLandingRequestV2(objectGraph, fetchAds); + const fetchTimingMetricsBuilder = new FetchTimingMetricsBuilder(); + const modifiedObjectGraph = objectGraph.addingFetchTimingMetricsBuilder(fetchTimingMetricsBuilder); + const fetchSearchLanding = mediaNetwork.fetchData(modifiedObjectGraph, searchLandingRequest); + const amsEngagement = objectGraph.amsEngagement; + let amdPromise; + if (amsEngagement && objectGraph.bag.enableRecoOnDeviceReordering) { + const request = { + timeout: 500, + eventType: impressionDemotion.AMSEngagementAppStoreEventKey, + tab: "search", + }; + amdPromise = amsEngagement.performRequest(request); + } + return await Promise.all([fetchSearchLanding, fetchAds, amdPromise]).then(([responseData, adResponse, amdResponse]) => { + return fetchTimingMetricsBuilder.measureModelConstruction(() => { + return landingPageFromResponseV2(modifiedObjectGraph, responseData, adResponse, amdResponse); + }); + }); +} +/** + * Creates `SearchLandingPage` model from the V2 Search Landing Page protocol + * @param objectGraph The App Store Object Graph + * @param landingPageResponse The response from the fetch + * @param adResponse The response from the ad fetch + * @returns A `SearchLandingPage` model from the V2 Search Landing Page protocol + */ +function landingPageFromResponseV2(objectGraph, landingPageResponse, adResponse, impressionData) { + if (serverData.isNullOrEmpty(landingPageResponse.data)) { + return null; + } + // Creates the page info + const pageInformation = metricsHelpersPage.metricsPageInformationFromMediaApiResponse(objectGraph, "SearchLanding", "SearchLanding", landingPageResponse); + // Decorate page info with personalization metrics + const onDevicePersonalizationMetricsData = onDevicePersonalization.metricsData(objectGraph); + pageInformation.recoMetricsData = metricsHelpersUtil.combinedRecoMetricsDataFromMetricsData(pageInformation.recoMetricsData, null, onDevicePersonalizationMetricsData); + pageInformation.iAdInfo = iadInfoFromOnDeviceAdResponse(objectGraph, "searchLanding", adResponse); + const adIncidentRecorder = adIncidents.newRecorder(objectGraph, pageInformation.iAdInfo); + adIncidents.recordAdResponseEventsIfNeeded(objectGraph, adIncidentRecorder, adResponse); + // Creates Search Landing Page Context + const landingPageContext = { + shelves: [], + metricsLocationTracker: metricsHelpersLocation.newLocationTracker(), + metricsPageInformation: pageInformation, + adStitcher: landingAdStitch.adStitcherForOnDeviceSLPAdvertData(objectGraph, adResponse, landingPageResponse), + adIncidentRecorder: adIncidentRecorder, + pageType: SearchPageType.Landing, + recoImpressionData: impressionDemotion.impressionEventsFromData(objectGraph, impressionData), + }; + // Create the shelves for the page + searchLandingShelfController.insertShelvesIntoSearchPageContext(objectGraph, landingPageResponse, landingPageContext); + // Add Unified Messaging placement to top of page for NLS BT. + const bubbleTipShelf = createNaturalLanguageSearchBubbleTipShelf(objectGraph); + if (bubbleTipShelf) { + landingPageContext.shelves.unshift(bubbleTipShelf); + } + const landingPage = new SearchLandingPage(landingPageContext.shelves); + // Page refresh + landingPage.pageRefreshPolicy = new PageRefreshPolicy("timeSinceOnScreen", objectGraph.bag.searchLandingPageRefreshUpdateDelayInterval, objectGraph.bag.searchLandingPageOffscreenRefreshInterval, null); + // Ad Incidents + landingPage.adIncidents = adIncidents.recordedIncidents(objectGraph, landingPageContext.adIncidentRecorder); + metricsHelpersPage.addMetricsEventsToPageWithInformation(objectGraph, landingPage, landingPageContext.metricsPageInformation); + return landingPage; +} +/** + * Creates the NLS BT shelf for SLP if enabled. + * @param objectGraph The app store object graph. + * @returns The shelf for the NLS BT shown on SLP, or undefined if bag has feature disabled. + */ +export function createNaturalLanguageSearchBubbleTipShelf(objectGraph) { + var _a; + if (!objectGraph.bag.isNaturalLanguageSearchEnabled && !objectGraph.bag.isNaturalLanguageSearchResultsEnabled) { + return undefined; // feature not enabled in the bag + } + const context = { + signal: { + lastNLSQueryDate: objectGraph.storage.retrieveString("lastNLSQueryDate"), + treatmentId: (_a = appStoreExperiments.currentTreatmentIdForArea(objectGraph, ExperimentAreaId.SearchLandingPage)) !== null && _a !== void 0 ? _a : null, + }, + }; + const shelf = groupingCommon.shelfForUnifiedMessage(objectGraph, "searchFocusHeader", context, "pullOnly"); + shelf.refreshUrl = `${Protocol.internal}:/${Path.searchLandingPage}/${Path.shelf}/?${Parameters.isSearchFocusHeaderShelf}=true`; + return shelf; +} +async function fetchSearchLandingPageV2WithFocusPage(objectGraph, fetchAds) { + const searchLandingRequest = makeSearchLandingRequestV2(objectGraph, fetchAds).enablingFeature("search-focus-suggestions"); + const fetchTimingMetricsBuilder = new FetchTimingMetricsBuilder(); + const modifiedObjectGraph = objectGraph.addingFetchTimingMetricsBuilder(fetchTimingMetricsBuilder); + const fetchSearchLanding = mediaNetwork.fetchData(modifiedObjectGraph, searchLandingRequest); + const amsEngagement = objectGraph.amsEngagement; + let amdPromise = null; + if (amsEngagement && objectGraph.bag.enableRecoOnDeviceReordering) { + const request = { + timeout: 500, + eventType: impressionDemotion.AMSEngagementAppStoreEventKey, + tab: "search", + }; + amdPromise = amsEngagement.performRequest(request); + } + return await Promise.all([fetchSearchLanding, fetchAds, amdPromise]).then(async ([responseData, adResponse, amdResponse]) => { + return await fetchTimingMetricsBuilder.measureModelConstructionAsync(async () => await landingPageFromResponseV2WithFocusPage(modifiedObjectGraph, responseData, adResponse, amdResponse)); + }); +} +/** + * Creates `SearchLandingPage` model from the V2 Search Landing Page protocol, but with search-focus feature enabled. + * @param objectGraph The App Store Object Graph + * @param landingPageResponse The response from the landing fetch + * @param landingAdResponse The response from the landingAd fetch + * @returns A `SearchLandingPage` model created from server response. + */ +async function landingPageFromResponseV2WithFocusPage(objectGraph, landingPageResponse, landingAdResponse, impressionData) { + // MAINTAINER'S NOTE: V3 protocol does not change any existing SLP v2 fields and is purely additives for focus page support + const landingPage = landingPageFromResponseV2(objectGraph, landingPageResponse, landingAdResponse, impressionData); + // Create Search Focus Page + return await fetchFocusPageUsingLandingPageResponse(objectGraph, landingPageResponse).then((focusPage) => { + landingPage.searchFocusPage = focusPage; + return landingPage; + }); +} +/** + * Creates `SearchFocusPage` model from the V3 Search Landing Page protocol, fetching search history if needed. + * @param objectGraph The App Store Object Graph + * @param focusPageResponse The response from the fetch + * @returns A `SearchFocusPage` model from the V3 Search Landing Page protocol + */ +async function fetchFocusPageUsingLandingPageResponse(objectGraph, landingPageResponse) { + var _a; + if (serverData.isNullOrEmpty(landingPageResponse.data)) { + return null; + } + // Creates the page info + const pageInformation = metricsHelpersPage.metricsPageInformationFromMediaApiResponse(objectGraph, "SearchFocus", "Focus", landingPageResponse, " "); + // Decorate page info with personalization metrics + const onDevicePersonalizationMetricsData = onDevicePersonalization.metricsData(objectGraph); + pageInformation.recoMetricsData = metricsHelpersUtil.combinedRecoMetricsDataFromMetricsData(pageInformation.recoMetricsData, null, onDevicePersonalizationMetricsData); + // Creates Search Focus Page Context + const focusPageContext = { + shelves: [], + metricsLocationTracker: metricsHelpersLocation.newLocationTracker(), + metricsPageInformation: pageInformation, + pageType: SearchPageType.Focus, + }; + const searchHistoryShelfMarker = searchLandingShelfController.firstShelfMarkerMatchingUseCase(landingPageResponse, focusPageContext, "recentSearches"); + // Skip fetching search history if there isn't a marker for it in SLP response. + if (isNothing(searchHistoryShelfMarker)) { + return createFocusPageFromResponse(objectGraph, landingPageResponse, focusPageContext); + } + const searchHistoryDisplayCount = (_a = mediaAttributes.attributeAsNumber(searchHistoryShelfMarker, "displayCount")) !== null && _a !== void 0 ? _a : 0; + const fetchSearchHistory = objectGraph.onDeviceSearchHistoryManager.fetchRecentsWithLimit(searchHistoryDisplayCount); + return await fetchSearchHistory.then((searchHistory) => { + focusPageContext.searchHistory = searchHistory; + return createFocusPageFromResponse(objectGraph, landingPageResponse, focusPageContext); + }); +} +function createFocusPageFromResponse(objectGraph, landingPageResponse, focusPageContext) { + // Create the shelves for the focus page using same logic as landing page, but + // includes search history in page context to support `search-recommendations-marker`. + searchLandingShelfController.insertShelvesIntoSearchPageContext(objectGraph, landingPageResponse, focusPageContext); + const focusPage = new SearchFocusPage(focusPageContext.shelves); + if (serverData.isNullOrEmpty(focusPage.shelves)) { + return null; + } + metricsHelpersPage.addMetricsEventsToPageWithInformation(objectGraph, focusPage, focusPageContext.metricsPageInformation); + return focusPage; +} +async function fetchTrendingSearchesFallbackPage(objectGraph, fetchAds) { + const fetchRequest = { + url: objectGraph.bag.trendingSearchesURL, + }; + const trendingSearchesPromise = objectGraph.network.fetch(fetchRequest).then((response) => { + if (!response.ok) { + throw Error(`Bad Status code ${response.status} for ${fetchRequest.url}`); + } + return JSON.parse(response.body); + }); + return await Promise.all([trendingSearchesPromise, fetchAds]).then(([trendingSearchesData, adResponse]) => { + var _a; + const page = new SearchLandingPage(trendingSearchesShelvesForResponse(objectGraph, trendingSearchesData)); + const pageInformation = metricsHelpersPage.fakeMetricsPageInformation(objectGraph, "SearchLanding", "trending", ""); // old trending endpoint doesn't have metrics meta + pageInformation.iAdInfo = iadInfoFromOnDeviceAdResponse(objectGraph, "searchLanding", adResponse); + (_a = pageInformation.iAdInfo) === null || _a === void 0 ? void 0 : _a.setMissedOpportunity(objectGraph, "SLPLOAD", "searchLanding"); // trending fallback never displays ad, so is always missed opportunity. + metricsHelpersPage.addMetricsEventsToPageWithInformation(objectGraph, page, pageInformation); + return page; + }); +} +/** + * Creates a trending searches shelves from the given JSON response. + * @param objectGraph The App Store Object Graph. + * @param response The API response JSON data. + * @return {Shelf[]} Trending searches shelves created from response. + */ +function trendingSearchesShelvesForResponse(objectGraph, response) { + return validation.context("trendingSearchesShelfForResponse", () => { + const locationTracker = metricsHelpersLocation.newLocationTracker(); + const searches = serverData.asArrayOrEmpty(response, "trendingSearches").map((rawSearch) => { + const term = serverData.asString(rawSearch, "label"); + const searchAction = new SearchAction(term, term, serverData.asString(rawSearch, "url"), "trending"); + if (objectGraph.client.isPhone) { + searchAction.artwork = contentArtwork.createArtworkForResource(objectGraph, "systemimage://magnifyingglass"); + } + metricsHelpersClicks.addEventsToSearchAction(objectGraph, searchAction, "button", locationTracker); + metricsHelpersLocation.nextPosition(locationTracker); + return searchAction; + }); + let maxNumberOfSearches = 0; + switch (objectGraph.client.deviceType) { + case "pad": + maxNumberOfSearches = 10; + break; + case "phone": + maxNumberOfSearches = 7; + break; + default: + break; + } + const shelf = new Shelf("action"); + shelf.title = searches.length > 0 ? serverData.asString(response, "header.label") : null; + shelf.isHorizontal = false; + shelf.items = searches.slice(0, maxNumberOfSearches); + return [shelf]; + }); +} +export async function fetchPage(objectGraph) { + const fetchAds = isAdPlacementEnabled(objectGraph, "searchLanding") + ? landingAdFetchFetchAds(objectGraph, "searchLanding").catch(() => null) + : null; + return await fetchSearchLandingPage(objectGraph, fetchAds).catch(async (e) => { + // If the client has provided a `trendingSearchesURL`, we can fallback to that search + // mechanism if the search landing request fails. If `trendingSearchesURL` is not + // provided, we re-throw the original landing page error for the client to handle. + if (isSome(objectGraph.bag.trendingSearchesURL)) { + return await fetchTrendingSearchesFallbackPage(objectGraph, fetchAds); + } + else { + throw e; + } + }); +} +//# sourceMappingURL=search-landing-page-utils.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/search/search-page-url.js b/node_modules/@jet-app/app-store/tmp/src/common/search/search-page-url.js new file mode 100644 index 0000000..9462f94 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/search/search-page-url.js @@ -0,0 +1,5 @@ +import { generateRoutes } from "../util/generate-routes"; +import { makeSearchResultsPageIntentFromURLParams } from "../../api/intents/search-results-page-intent"; +const { routes: searchResultsPageRoutes, makeCanonicalUrl: makeCanonicalSearchResultsPageUrl } = generateRoutes(makeSearchResultsPageIntentFromURLParams, "/{platform}/search", ["term"]); +export { searchResultsPageRoutes, makeCanonicalSearchResultsPageUrl }; +//# sourceMappingURL=search-page-url.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/search/search-results-fetching.js b/node_modules/@jet-app/app-store/tmp/src/common/search/search-results-fetching.js new file mode 100644 index 0000000..f04a8e1 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/search/search-results-fetching.js @@ -0,0 +1,496 @@ +/** + * Data Fetching for Search Results for: + * - Initial search requests + * - Content pagination requests + */ +import { isNothing } from "@jet/environment/types/optional"; +import * as models from "../../api/models"; +import { asBooleanOrFalse, asString, isDefinedNonNull, isDefinedNonNullNonEmpty, isNullOrEmpty, } from "../../foundation/json-parsing/server-data"; +import { defaultAdditionalPlatformsForClient, Request } from "../../foundation/media/data-fetching"; +import { metricsFromMediaApiObject, } from "../../foundation/media/data-structure"; +import { fetchData } from "../../foundation/media/network"; +import { buildURLFromRequest } from "../../foundation/media/url-builder"; +import * as constants from "../../foundation/util/constants"; +import * as client from "../../foundation/wrappers/client"; +import * as appPromotionsCommon from "../app-promotions/app-promotions-common"; +import * as categories from "../categories"; +import { addVariantParametersToRequestForItems, shouldFetchCustomAttributes, } from "../product-page/product-page-variants"; +import { setPreviewPlatform } from "../preview-platform"; +import { SponsoredSearchRequestData } from "./search-ads"; +import { searchMetricsDataSetID } from "./search-common"; +import { fetchSponsoredSearchNativeAdvertData } from "./sponsored-search-fetching"; +import { dateFlooredToHour } from "../../foundation/util/date-util"; +import { shouldUsePrerenderedIconArtwork } from "../content/content"; +import { AppEventsAttributes } from "../../gameservicesui/src/foundation/media-api/requests/recommendation-request-types"; +export async function fetchSegmentedSearchResults(objectGraph, options) { + var _a; + // Primary search request + const searchRequest = (_a = createSearchRequest(objectGraph, options)) === null || _a === void 0 ? void 0 : _a.addingQuery("groupBy[search]", "platform"); + if (isNothing(searchRequest)) { + return null; + } + const fetchSearchResults = fetchData(objectGraph, searchRequest, undefined); + return await fetchSearchResults.then((catalogResponse) => { + renameDataSetIdKey(catalogResponse); + const data = { + catalogResponse: catalogResponse, + requestMetadata: { + requestDescriptor: options, + searchRequestUrl: buildURLFromRequest(objectGraph, searchRequest).toString(), // Searches are attributed to request url + }, + categoriesFilterData: null, + sponsoredSearchRequestData: null, + sponsoredSearchAdvertData: null, + }; + return data; + }); +} +/** + * Fetch a collection of data for displaying a single, sequential set of search results. + * @param options Parameters for the search being performed. + * @returns `Promise` for aggregate search result data, or `null` if `options` was invalid. + */ +export async function fetchSequentialSearchResultsData(objectGraph, options) { + // Advert targeting data from client + const sponsoredSearchRequestData = new SponsoredSearchRequestData(options.targetingData, objectGraph.random.nextUUID()); + // Primary search request + const searchRequest = createSearchRequest(objectGraph, options); + if (searchRequest === null) { + return null; + } + const fetchSearchResults = fetchData(objectGraph, searchRequest, createSearchFetchOptions(objectGraph, sponsoredSearchRequestData)); + // Search History + if (objectGraph.bag.mediaAPISearchFocusEnabled) { + const historyItem = { + term: options.term.trim(), + entity: options.searchEntity, + }; + // ** MAINTAINER'S NOTE ** + // Fire and forget this request to reduce delay showing search results on persisting recent searches that are not visible at this time. + objectGraph.onDeviceSearchHistoryManager.saveRecentSearchWithLimit(historyItem, 30); + } + // Optional requests + const fetchSponsoredSearchAds = fetchSponsoredSearchNativeAdvertData(objectGraph, sponsoredSearchRequestData, options.term, fetchSearchResults); + const fetchCategoriesOrNull = fetchCategoryFiltersDataIfNeeded(objectGraph); + return await Promise.all([fetchSearchResults, fetchSponsoredSearchAds, fetchCategoriesOrNull]).then(([catalogResponse, sponsoredSearchAdvertData, categoriesFilterData]) => { + var _a, _b, _c, _d; + renameDataSetIdKey(catalogResponse); + // Remember the last time a natural language search was performed. + if ((_c = (_b = (_a = catalogResponse.meta) === null || _a === void 0 ? void 0 : _a.results) === null || _b === void 0 ? void 0 : _b.search) === null || _c === void 0 ? void 0 : _c.naturalLanguage) { + const previousNLSQueryDate = objectGraph.storage.retrieveString("lastNLSQueryDate"); + const lastNLSQueryDate = dateFlooredToHour(new Date()).getTime().toString(); + objectGraph.storage.storeString("lastNLSQueryDate", lastNLSQueryDate); + // Notify ODJ on change using AMSEngagement. + (_d = objectGraph.amsEngagement) === null || _d === void 0 ? void 0 : _d.enqueueData({ + eventType: "lastNLSQueryDateChange", + app: "com.apple.AppStore", + oldState: previousNLSQueryDate, + newState: lastNLSQueryDate, + }); + } + const data = { + catalogResponse: catalogResponse, + categoriesFilterData: categoriesFilterData, + sponsoredSearchRequestData: sponsoredSearchRequestData, + sponsoredSearchAdvertData: sponsoredSearchAdvertData, + requestMetadata: { + requestDescriptor: options, + searchRequestUrl: buildURLFromRequest(objectGraph, searchRequest).toString(), // Searches are attributed to request url + }, + }; + return data; + }); +} +/** + * Fetch a collection of data for displaying several, grouped set of search results. + * @param options Parameters for the search being performed. + * @returns `Promise` for aggregate search result data, or `Promise` of `null` if `options` was invalid. + */ +export async function fetchPlatformGroupedSearchResultsData(objectGraph, options) { + // Primary search request + const searchRequest = createPlatformGroupedSearchRequest(objectGraph, options); + if (isNothing(searchRequest)) { + return null; + } + const fetchSearchResults = fetchData(objectGraph, searchRequest, createSearchFetchOptions(objectGraph)); + // Optional requests + const fetchCategoriesOrNull = fetchCategoryFiltersDataIfNeeded(objectGraph); + return await Promise.all([fetchSearchResults, fetchCategoriesOrNull]).then(([catalogResponse, categoriesFilterData]) => { + renameDataSetIdKey(catalogResponse); + const data = { + catalogResponse: catalogResponse, + categoriesFilterData: categoriesFilterData, + sponsoredSearchRequestData: null, + sponsoredSearchAdvertData: null, + requestMetadata: { + requestDescriptor: options, + searchRequestUrl: buildURLFromRequest(objectGraph, searchRequest).toString(), // Searches are attributed to request url + }, + }; + return data; + }); +} +/** + * Fetch a set of items for appearing in search results + * @param items Items to fetch. + */ +export async function fetchSearchResultItems(objectGraph, items) { + const request = createCatalogRequestForItems(objectGraph, items); + return await fetchData(objectGraph, request); +} +// endregion +// region Request Header +/** + * Create fetch options, annotating with `SponsoredSearchRequestData` header fields. + * @param adData Advert data to include in header. + */ +function createSearchFetchOptions(objectGraph, sponsoredSearchRequestData) { + const searchRequestHeader = {}; + if (sponsoredSearchRequestData && sponsoredSearchRequestData.validAdRequest()) { + searchRequestHeader[constants.appStoreClientRequestIdName] = sponsoredSearchRequestData.appStoreClientRequestId; + searchRequestHeader[constants.iAdRequestDataHeaderName] = sponsoredSearchRequestData.sponsoredSearchRequestData; + searchRequestHeader[constants.iAdRoutingInfoHeaderName] = sponsoredSearchRequestData.routingInfo; + } + return { + headers: searchRequestHeader, + }; +} +// endregion +// region Search Results Request +/** + * Set of relationships fetched as relation for search request / catalog requests + */ +const searchIncludeRelationships = ["apps", "top-apps"]; +/** + * Create the `Request` object to execute a search described by `options` + * @param options Options for search being executed. + * @returns `Request` configured for `options`, or `null` if `options` was invalid. + */ +export function createSearchRequest(objectGraph, options) { + var _a; + const term = (_a = options.term) === null || _a === void 0 ? void 0 : _a.trim(); + if (isNullOrEmpty(term)) { + return null; + } + const origin = options.origin; + const source = options.source; + const searchEntityOrNull = options.searchEntity; + const facetsOrNull = options.facets; + const selectedFacetOptionsOrNull = options.selectedFacetOptions; + const spellCheckEnabled = options.spellCheckEnabled; + const excludedTermsOrNull = options.excludedTerms; + const clientIdentifier = objectGraph.host.clientIdentifier; + const searchRequest = new Request(objectGraph) + .withSearchTerm(term) + .includingAdditionalPlatforms(defaultAdditionalPlatformsForClient(objectGraph)) + .includingAttributes(includeAttributesForSearchResults(objectGraph)) + .includingScopedAttributes("editorial-items", ["showLabelInSearch"]) + .includingRelationshipsForUpsell(true) + .includingMacOSCompatibleIOSAppsWhenSupported() + .usingCustomAttributes(shouldFetchCustomAttributes(objectGraph)); + setPreviewPlatform(objectGraph, searchRequest); + if (!objectGraph.client.isWatch && !objectGraph.client.isVision) { + searchRequest.includingRelationships(searchIncludeRelationships); + } + if (objectGraph.client.isVision) { + // Temporarily increase the sparseLimit on Vision to workaround lack of filtering for bincompat. + searchRequest.addingQuery("sparseLimit[developers:top-apps]", "12"); + } + if (appPromotionsCommon.appEventsAreEnabled(objectGraph)) { + searchRequest.includingAssociateKeys("apps", ["app-events"]); + searchRequest.includingScopedAttributes("app-events", AppEventsAttributes); + searchRequest.includingScopedRelationships("editorial-items", ["primary-content"]); + } + /** + * Tinker Filter + */ + if (asBooleanOrFalse(objectGraph.client.isTinkerWatch)) { + searchRequest.withFilter("contexts", "tinker"); + } + /** + * Guided Search + */ + if (objectGraph.host.isiOS) { + // Feature enablers + searchRequest.enablingFeature("guidedSearch"); + searchRequest.enablingFeature("midScrollGuidedSearch"); + // Selected facets, if any. + if (isDefinedNonNullNonEmpty(options.guidedSearchTokens)) { + searchRequest.addingQuery("selectedFacets", options.guidedSearchTokens.join(",")); + } + // Optimization term, if any were on request. + if (isDefinedNonNullNonEmpty(options.guidedSearchOptimizationTerm)) { + searchRequest.addingQuery("finalTerm", options.guidedSearchOptimizationTerm); + } + if (objectGraph.bag.isLLMSearchTagsEnabled) { + searchRequest.includingAssociateKeys("results:apps", ["tags"]); + } + } + if (objectGraph.featureFlags.isEnabled("voyager_bundles_2025A")) { + searchRequest.includingScopedAttributes("apps", ["screenshotsByType"]); + } + /** + * Entities in results + */ + if (searchEntityOrNull === "story") { + searchRequest.searchingOverTypes(["editorial-items"]); + } + else if (searchEntityOrNull === "developer") { + searchRequest.searchingOverTypes(["developers"]); + } + else if (searchEntityOrNull === "watch" || searchEntityOrNull === "arcade") { + searchRequest.searchingOverTypes(["apps"]).withFilter("contexts", searchEntityOrNull); + } + else if (objectGraph.client.isTV) { + searchRequest.searchingOverTypes(["apps", "developers", "groupings", "editorial-items"]); + } + else if (objectGraph.client.isVision) { + searchRequest.searchingOverTypes([ + "apps", + "developers", + "editorial-items", + "app-bundles", + "in-apps", + "editorial-pages", + ]); + } + else { + searchRequest.searchingOverTypes([ + "apps", + "developers", + "groupings", + "editorial-items", + "app-bundles", + "in-apps", + ]); + } + /** + * Signal Rosetta unavailability. + */ + if (objectGraph.appleSilicon.isSupportEnabled && !objectGraph.appleSilicon.isRosettaAvailable) { + searchRequest.addingQuery("restrict", "!requiresRosetta"); + } + /** + * Facets / filters + */ + if (facetsOrNull) { + for (const key of Object.keys(facetsOrNull)) { + searchRequest.addingQuery(key, facetsOrNull[key]); + } + } + if (selectedFacetOptionsOrNull) { + for (const key of Object.keys(selectedFacetOptionsOrNull)) { + const requestValues = models.PageFacets.requestValuesForSelectedFacetOptions(selectedFacetOptionsOrNull[key]); + if (isDefinedNonNullNonEmpty(requestValues)) { + searchRequest.addingQuery(key, requestValues.value); + for (const additionalKey of Object.keys(requestValues.additionalKeyValuePairs)) { + searchRequest.addingQuery(additionalKey, requestValues.additionalKeyValuePairs[additionalKey]); + } + } + } + } + /** + * natural language search availability + */ + const isNaturalLanguageSearchResultsEnabled = objectGraph.bag.isNaturalLanguageSearchEnabled || objectGraph.bag.isNaturalLanguageSearchResultsEnabled; + /** + * source attribution + */ + const sourceKey = isNaturalLanguageSearchResultsEnabled ? "source" : "src"; + if (origin === "hints") { + const hintSource = isNaturalLanguageSearchResultsEnabled && (source === null || source === void 0 ? void 0 : source.length) ? "hint:".concat(source) : "hint"; + searchRequest.addingQuery(sourceKey, hintSource); + } + else if (origin === "recents") { + searchRequest.addingQuery(sourceKey, "recent"); + } + else if (origin === "trending") { + searchRequest.addingQuery(sourceKey, "trending"); + } + else if (origin === "undoSpellCorrection") { + searchRequest.addingQuery(sourceKey, "searchInstead"); + } + else if (origin === "applySpellCorrection") { + searchRequest.addingQuery(sourceKey, "didYouMean"); + } + else if (origin === "guidedToken") { + searchRequest.addingQuery(sourceKey, "facet"); + } + /** + * contexts + */ + switch (clientIdentifier) { + case client.watchIdentifier: + searchRequest.addingContext("watch"); + break; + case client.messagesIdentifier: + searchRequest.addingContext("messages"); + break; + case client.arcadeIdentifier: + searchRequest.addingContext("arcade"); + break; + default: + break; + } + /** + * Advert limit + */ + searchRequest.addingQuery("limit[ads-result]", objectGraph.bag.mediaAdvertRequestLimit.toString()); + /** + * Ads locale metadata. + */ + if (isDefinedNonNullNonEmpty(objectGraph.bag.adsOverrideLanguage)) { + searchRequest.enablingFeature("adsLocaleMetadata"); + } + /** + * Feature: Spellcheck + */ + if (spellCheckEnabled) { + searchRequest.enablingFeature("spellCheck"); + } + /** + * Feature: Natural Language + */ + if (isNaturalLanguageSearchResultsEnabled) { + searchRequest.enablingFeature("naturalLanguage"); + } + /** + * Feature: Organic CPPs + */ + if (objectGraph.host.isiOS) { + searchRequest.enablingFeature("searchResultCpps"); + } + /** + * Excluded terms + */ + if (excludedTermsOrNull) { + searchRequest.addingQuery("excludeTerms", excludedTermsOrNull.join(",")); + } + return searchRequest; +} +/** + * Create the `Request` object to execute a search described by `options`, with results grouped by platform + * @param options Options for search being executed. + * @returns `Request` configured for `options`, or `null` if `options` was invalid. + */ +export function createPlatformGroupedSearchRequest(objectGraph, options) { + var _a; + return (_a = createSearchRequest(objectGraph, options)) === null || _a === void 0 ? void 0 : _a.addingQuery("groupBy[search]", "platform").includingMacOSCompatibleIOSAppsWhenSupported(); +} +/** + * Create a request for fetching set of `items` for appearing in search tab. Used for pagination. + * @param items Items to fetch. + */ +function createCatalogRequestForItems(objectGraph, items) { + const shouldUseMixedCatalogRequest = objectGraph.bag.isLLMSearchTagsEnabled || + asBooleanOrFalse(objectGraph.bag.supportedMixedMediaRequestUsecases["search"]); + const searchRequest = new Request(objectGraph, items, shouldUseMixedCatalogRequest, ["tags"]) + .includingAdditionalPlatforms(defaultAdditionalPlatformsForClient(objectGraph)) + .includingScopedAttributes("editorial-items", ["showLabelInSearch"]) + .includingAttributes(includeAttributesForSearchResults(objectGraph)) + .includingRelationshipsForUpsell(true) + .includingMacOSCompatibleIOSAppsWhenSupported() + .usingCustomAttributes(shouldFetchCustomAttributes(objectGraph)); + addVariantParametersToRequestForItems(objectGraph, searchRequest, items); + if (!objectGraph.client.isWatch) { + searchRequest.includingRelationships(searchIncludeRelationships); + } + if (appPromotionsCommon.appEventsAreEnabled(objectGraph)) { + searchRequest.includingAssociateKeys("apps", ["app-events"]); + searchRequest.includingScopedAttributes("app-events", AppEventsAttributes); + searchRequest.includingScopedRelationships("editorial-items", ["primary-content"]); + } + return searchRequest; +} +/** + * Returns the include attributes used for: + * 1. Initial search request + * 2. Subsequent catalog request for pagination + */ +function includeAttributesForSearchResults(objectGraph) { + const attributes = [ + "screenshotsByType", + "messagesScreenshots", + "videoPreviewsByType", + "requiredCapabilities", + "editorialBadgeInfo", + "supportsFunCamera", + "minimumOSVersion", + "customScreenshotsByTypeForAd", + "customVideoPreviewsByTypeForAd", + "secondaryGenreShortDisplayNames", + "genreShortDisplayName", + "editorialVideo", + ]; + if (objectGraph.appleSilicon.isSupportEnabled) { + attributes.push("macRequiredCapabilities"); + } + if (objectGraph.client.isMac) { + attributes.push("hasMacIPAPackage"); + } + if (objectGraph.client.isVision) { + attributes.push("compatibilityControllerRequirement"); + } + if (objectGraph.bag.enableUpdatedAgeRatings) { + attributes.push("ageRating"); + } + if (shouldUsePrerenderedIconArtwork(objectGraph)) { + attributes.push("iconArtwork"); + } + /// We don't want to unnecessarily ask for clients that don't have metadata ribbon capability + if (objectGraph.host.isOSAtLeast(15, 5, 0)) { + attributes.push("remoteControllerRequirement"); + } + // Keeping custom creatives out of prod. + if (preprocessor.CARRY_BUILD || preprocessor.DEBUG_BUILD) { + if (objectGraph.featureFlags.isEnabled("aligned_region_artwork_2025A")) { + attributes.push("adCreativeArtwork"); + attributes.push("adCreativeVideo"); + } + } + return attributes; +} +// endregion +// region Categories Filter Request +/** + * Fetches data to for category filters on devices that need them. + */ +async function fetchCategoryFiltersDataIfNeeded(objectGraph) { + const deviceType = objectGraph.client.deviceType; + if (deviceTypeSupportsCategoryFilters(deviceType)) { + const categoriesRequest = categories.createRequest(objectGraph, null, null, defaultAdditionalPlatformsForClient(objectGraph)); + if (categoriesRequest) { + // Failable request. + return await fetchData(objectGraph, categoriesRequest).catch(() => null); + } + } + return null; +} +function deviceTypeSupportsCategoryFilters(deviceType) { + switch (deviceType) { + case "pad": + case "mac": + return true; + default: + return false; + } +} +// endregion +// region Search Result Modification +/** + * The search dataset id is set to the key `dataSetId` on `meta.metrics`, but needs to have `data.search.dataSetId` key. + * <rdar://problem/39920993> Metrics: metrics blob in search response has incorrect field name + */ +function renameDataSetIdKey(searchResponse) { + var _a; + const searchMetrics = metricsFromMediaApiObject(searchResponse); + const oldDataSetId = "dataSetId"; + if (isDefinedNonNull(searchMetrics) && + isDefinedNonNull(searchResponse.meta) && + isDefinedNonNull((_a = searchResponse.meta) === null || _a === void 0 ? void 0 : _a.metrics)) { + searchResponse.meta.metrics[searchMetricsDataSetID] = asString(searchMetrics, oldDataSetId); + delete searchResponse.meta.metrics[oldDataSetId]; + } +} +// endregion +//# sourceMappingURL=search-results-fetching.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/search/search-results-learn-more-notice.js b/node_modules/@jet-app/app-store/tmp/src/common/search/search-results-learn-more-notice.js new file mode 100644 index 0000000..8c2e741 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/search/search-results-learn-more-notice.js @@ -0,0 +1,46 @@ +/** + * Build methods for Search Transparency. + * Search transparency provides additional disclosure message about search ranking required in certain storefronts. + */ +import { isNothing } from "@jet/environment"; +import { FlowAction, LinkableText, StyledText } from "../../api/models"; +import { addClickEventToAction } 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"; +export function createSearchResultsLearnMoreNoticeLinkableText(objectGraph, metricsOptions) { + const editorialItemId = objectGraph.bag.searchResultsLearnMoreEditorialId; + if (isNothing(editorialItemId) || (editorialItemId === null || editorialItemId === void 0 ? void 0 : editorialItemId.length) === 0) { + return undefined; + } + const clickOptions = { + kind: "editorialItem", + softwareType: null, + title: objectGraph.loc.string("SEARCH_TRANSPARENCY_LINK"), + id: editorialItemId, + targetType: "link", + pageInformation: metricsOptions.pageInformation, + locationTracker: metricsOptions.locationTracker, + }; + const text = objectGraph.loc.string("SEARCH_TRANSPARENCY_TEXT"); + const learnMoreAction = new FlowAction("article"); + learnMoreAction.title = text; + learnMoreAction.pageUrl = `https://apps.apple.com/story/id${editorialItemId}`; + if (objectGraph.client.isWeb) { + const destination = makeRoutableArticlePageIntent({ + ...getLocale(objectGraph), + ...getPlatform(objectGraph), + id: editorialItemId, + }); + const pageUrlString = makeRoutableArticlePageCanonicalUrl(objectGraph, destination); + learnMoreAction.pageUrl = pageUrlString; + learnMoreAction.destination = destination; + } + addClickEventToAction(objectGraph, learnMoreAction, clickOptions); + const linkSubstrings = {}; + linkSubstrings[`${objectGraph.loc.string("SEARCH_TRANSPARENCY_LINK")}`] = learnMoreAction; + const styledText = new StyledText(text, "text/plain"); + return new LinkableText(styledText, linkSubstrings); +} +//# sourceMappingURL=search-results-learn-more-notice.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/search/search-results-pipeline.js b/node_modules/@jet-app/app-store/tmp/src/common/search/search-results-pipeline.js new file mode 100644 index 0000000..f6b64c1 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/search/search-results-pipeline.js @@ -0,0 +1,248 @@ +/** + * Builder methods for building a collection of search results models for a series of search result data. + */ +import * as validation from "@jet/environment/json/validation"; +import { isNothing } from "@jet/environment/types/optional"; +import * as models from "../../api/models"; +import * as serverData from "../../foundation/json-parsing/server-data"; +import { isDataHydrated } from "../../foundation/media/data-structure"; +import { shouldFilter } from "../filtering"; +import { nextPosition, pushBasicLocation, popLocation } from "../metrics/helpers/location"; +import * as onDevicePersonalization from "../personalization/on-device-personalization"; +import { searchResultFromData } from "./content/search-results"; +import * as searchAds from "./search-ads"; +import * as guidedSearch from "./guided-search/guided-search"; +import { addImpressionFields } from "../metrics/helpers/impressions"; +import { searchAdMissedOpportunityFromId } from "../lockups/ad-lockups"; +/** + * Determines where to display guided search, e.g. as mid-scroll module or pinned to top. + * @param meta The metadata from the MAPI response for search catalog request. + * @param objectGraph The App Store object graph. + * @returns The position of the mid-scroll guided search module in search results, or `undefined` for the pinned to top experience. + */ +export function guidedSearchPositionFromSearchResponseMeta(meta, objectGraph) { + var _a, _b, _c; + // Client debug override + const guidedSearchPositionOverride = (_a = objectGraph.userDefaults) === null || _a === void 0 ? void 0 : _a.integer("GuidedSearchOverrides.position"); + if (serverData.isNumber(guidedSearchPositionOverride) && guidedSearchPositionOverride > 1) { + return guidedSearchPositionOverride; + } + // Server controlled by default + return (_c = (_b = meta === null || meta === void 0 ? void 0 : meta.displayStyle) === null || _b === void 0 ? void 0 : _b.guidedSearch) === null || _c === void 0 ? void 0 : _c.position; +} +/** + * Create a `SearchResultsBuildResult` containing set of built and deferred results from the top-level search data container. + * This method supports prepending adverts. + * @param objectGraph + * @param requestMetadata Request metadata for search being performed. + * @param searchResponseMetadata Response metadata for search that was performed. + * @param metricsOptions Metrics options for built models. + * @param resultsData Array of search results data. + * @param advertData Data container with advert results. + * @param facetData Array of guided search token response data. + * @param installStates A mapping of adamIDs to their respective install states that is used to determine if the app is currently installed by the user + * @param appStates A mapping of adamIDs to their respective app state to determine if the ad/first result has been installed by the user in the past + */ +export async function createSearchResults(objectGraph, requestMetadata, searchResponseMetadata, metricsOptions, resultsData, advertData = undefined, facetData = undefined, installedStates = undefined, appStates = undefined) { + var _a, _b, _c, _d, _e, _f; + /// Built models + const builtResults = []; + // Unhydrated items that will be fetched in pagination. + const deferredResults = []; + // Search Experiments Data + const searchExperimentsData = searchResponseMetadata || null; + // Generate the personalization data + const appIds = resultsData + .filter((resultData) => { + return resultData.type === "apps"; + }) + .map((resultData) => { + return resultData.id; + }); + const personalizationDataContainer = onDevicePersonalization.personalizationDataContainerForAppIds(objectGraph, new Set(appIds)); + // Build Adverts + let advertsSearchResult; + let advertsDisplayStyle; + if (searchAds.platformSupportsAdverts(objectGraph) && serverData.isDefinedNonNullNonEmpty(advertData)) { + const adsResultAndDisplayStyle = searchAds.adsResultFromSearchResults(objectGraph, advertData, resultsData, requestMetadata, metricsOptions, installedStates !== null && installedStates !== void 0 ? installedStates : null, appStates !== null && appStates !== void 0 ? appStates : null, searchExperimentsData, personalizationDataContainer); + advertsSearchResult = adsResultAndDisplayStyle.result; + advertsDisplayStyle = adsResultAndDisplayStyle.displayStyle; + if (serverData.isDefinedNonNullNonEmpty(advertsSearchResult === null || advertsSearchResult === void 0 ? void 0 : advertsSearchResult.lockups)) { + advertsSearchResult.searchAdOpportunity = advertsSearchResult.lockups[0].searchAdOpportunity; + builtResults.push(advertsSearchResult); + } + } + // Flag for Ad Media Deduping + let isFirstResult = true; + const guidedSearchPosition = guidedSearchPositionFromSearchResponseMeta(searchResponseMetadata, objectGraph); + for (const [index, resultData] of resultsData.entries()) { + // Inject the mid-scroll guided search module if we've reached the desired position. + if (index === guidedSearchPosition) { + const tokens = createGuidedSearchTokens(objectGraph, requestMetadata.requestDescriptor, facetData, metricsOptions); + if (tokens.length > 0) { + const title = (_c = (_b = (_a = searchResponseMetadata === null || searchResponseMetadata === void 0 ? void 0 : searchResponseMetadata.displayStyle) === null || _a === void 0 ? void 0 : _a.guidedSearch) === null || _b === void 0 ? void 0 : _b.title) !== null && _c !== void 0 ? _c : objectGraph.loc.string("Search.Guided.Title.ExploreMore"); // static fallback query context + const guidedSearchResult = new models.GuidedSearchResult(title, tokens); + const impressionOptions = { + ...metricsOptions, + id: "midScrollGuidedSearch", + kind: "grouping", + targetType: "module", + title: title, + softwareType: null, + }; + addImpressionFields(objectGraph, guidedSearchResult, impressionOptions); + builtResults.push(guidedSearchResult); + nextPosition(metricsOptions.locationTracker); + } + } + // Deferred items for subsequent pagination. + if (!isDataHydrated(resultData)) { + // On the first unhydrated item, attach the rest of the queue to `deferredResults` to preserve ordering. + deferredResults.push(...resultsData.slice(index)); + break; + } + // Filter + if (shouldFilter(objectGraph, resultData, 10750 /* Filter.Search */)) { + continue; + } + // Advert: Update CPP data on first organic result. + // We must do this *after* the ad results are built, because we need to ensure we're picking the first lockup that will appear, + // not just the first data (that may be filtered somehow). + if (isFirstResult && serverData.isDefinedNonNullNonEmpty(advertsSearchResult === null || advertsSearchResult === void 0 ? void 0 : advertsSearchResult.lockups)) { + searchAds.updateDupeOrganicResultCPPData(objectGraph, advertData !== null && advertData !== void 0 ? advertData : [], advertsSearchResult, resultData, installedStates !== null && installedStates !== void 0 ? installedStates : null, appStates !== null && appStates !== void 0 ? appStates : null, metricsOptions, personalizationDataContainer); + } + // Build model + const searchResult = searchResultFromData(objectGraph, resultData, searchResponseMetadata, personalizationDataContainer, metricsOptions, requestMetadata.requestDescriptor.isNetworkConstrained, requestMetadata.requestDescriptor.searchEntity, searchExperimentsData); + if (!searchResult || !platformSupportsResultType(objectGraph, searchResult)) { + continue; + } + /** + * Advert: When first advert and result matches, modify media. + */ + if (isFirstResult && + serverData.isDefinedNonNullNonEmpty(advertsSearchResult) && + serverData.isDefinedNonNullNonEmpty(advertsSearchResult.lockups)) { + searchAds.dedupeAdMediaFromMatchingResult(objectGraph, advertsSearchResult, searchResult, searchExperimentsData, advertsDisplayStyle); + } + /** + * Advert: When advert isn't available, mark the organic as a missed opportunity slot + */ + if (isFirstResult && + searchAds.platformSupportsAdverts(objectGraph) && + serverData.isDefinedNonNull((_d = metricsOptions.pageInformation) === null || _d === void 0 ? void 0 : _d.iAdInfo) && + (serverData.isNull(advertsSearchResult) || serverData.isNullOrEmpty(advertsSearchResult === null || advertsSearchResult === void 0 ? void 0 : advertsSearchResult.lockups))) { + searchResult.searchAdOpportunity = searchAdMissedOpportunityFromId(objectGraph, metricsOptions.pageInformation); + (_e = searchResult.searchAdOpportunity) === null || _e === void 0 ? void 0 : _e.setMissedOpportunityReason("NOAD"); + (_f = searchResult.searchAdOpportunity) === null || _f === void 0 ? void 0 : _f.setTemplateType("APPLOCKUP"); + } + builtResults.push(searchResult); + isFirstResult = false; + nextPosition(metricsOptions.locationTracker); + } + return await applyClientFilteringToIAPs(objectGraph, builtResults).then((builtResultsFiltered) => { + return { + builtSearchResults: builtResultsFiltered, + deferredSearchResults: deferredResults, + }; + }); +} +// endregion +// region Internals +/** + * Create guided search tokens for the given search request descriptor and facet MAPI response data. + * @param objectGraph The App Store object graph. + * @param requestDescriptor The search request descriptor. + * @param facetData The media API response for guided search facets. + * @param metricsOptions The metrics options. + * @returns The guided search tokens to display. + */ +function createGuidedSearchTokens(objectGraph, requestDescriptor, facetData, metricsOptions) { + if (!objectGraph.host.isiOS || isNothing(facetData) || facetData.length === 0) { + return []; + } + pushBasicLocation(objectGraph, { + pageInformation: metricsOptions.pageInformation, + locationTracker: metricsOptions.locationTracker, + targetType: "SearchRevisions", + }, ""); + // Tokens from facet data + const tokens = []; + for (const data of facetData) { + const token = guidedSearch.createGuidedSearchToken(objectGraph, "rewrite", requestDescriptor, data, metricsOptions); + if (token) { + tokens.push(token); + nextPosition(metricsOptions.locationTracker); + } + } + popLocation(metricsOptions.locationTracker); + return tokens; +} +/** + * Apply client-side iAP filtering to set of results. + * @param objectGraph + * @param resultsToFilter Results to apply iAP filtering on. + */ +async function applyClientFilteringToIAPs(objectGraph, resultsToFilter) { + return await validation.context("applyClientFilteringToIAPs", async () => { + const iAPProductIDToParentBundleID = {}; + for (const result of resultsToFilter) { + if (result.resultType === "inAppPurchase") { + const inAppPurchaseResult = result; + const inAppPurchaseLockup = inAppPurchaseResult.lockup; + if (inAppPurchaseLockup.parent && + inAppPurchaseLockup.productIdentifier && + inAppPurchaseLockup.parent.bundleId) { + iAPProductIDToParentBundleID[inAppPurchaseLockup.productIdentifier] = + inAppPurchaseLockup.parent.bundleId; + } + else { + validation.unexpectedNull("ignoredValue", "string", `required fields for ${inAppPurchaseLockup.adamId}`); + } + } + } + if (Object.keys(iAPProductIDToParentBundleID).length === 0) { + return await Promise.resolve(resultsToFilter); + } + return await objectGraph.clientOrdering.visibilityForIAPs(iAPProductIDToParentBundleID).then((visibilities) => { + const filteredResults = resultsToFilter.filter((result) => { + if (result.resultType !== "inAppPurchase") { + return true; + } + const inAppPurchaseResult = result; + const inAppPurchaseLockup = inAppPurchaseResult.lockup; + if (inAppPurchaseLockup.productIdentifier && visibilities[inAppPurchaseLockup.productIdentifier]) { + return true; + } + else { + return inAppPurchaseLockup.isVisibleByDefault; + } + }); + return filteredResults; + }); + }); +} +/** + * Whether or not current platform supports displaying given `searchResult`. + */ +function platformSupportsResultType(objectGraph, searchResult) { + if (objectGraph.host.isTV) { + switch (searchResult.resultType) { + case "content": + case "editorial": + return true; + default: + return false; + } + } + if (!objectGraph.host.isiOS && !objectGraph.client.isWeb) { + switch (searchResult.resultType) { + case "appEvent": + return false; + default: + break; + } + } + return true; +} +// endregion +//# sourceMappingURL=search-results-pipeline.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/search/search-spell-correction.js b/node_modules/@jet-app/app-store/tmp/src/common/search/search-spell-correction.js new file mode 100644 index 0000000..60466dd --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/search/search-spell-correction.js @@ -0,0 +1,98 @@ +/** + * Build methods for Search Spell Correction. + * Spell correction allows users to change an previous search term to higher or lower confidence searches. + */ +import * as models from "../../api/models"; +import { isDefinedNonNull } from "../../foundation/json-parsing/server-data"; +import { addEventsToSearchAction } from "../metrics/helpers/clicks"; +import { popLocation, pushBasicLocation } from "../metrics/helpers/location"; +/** + * Build a `SearchResultsMessage` for a search result with given `SearchTermContext` + * @param termContext Context for the state of search terms. + */ +export function spellCorrectionMessageFromTermContext(objectGraph, termContext, metricsOptions) { + const showUndoSpellCorrection = isDefinedNonNull(termContext.correctedTerm); + const showSuggestion = isDefinedNonNull(termContext.suggestedTerm); + pushBasicLocation(objectGraph, { + pageInformation: metricsOptions.pageInformation, + locationTracker: metricsOptions.locationTracker, + targetType: "link", + }, "spellCorrection"); + if (showUndoSpellCorrection) { + return undoSpellCorrectionMessageFromTermContext(objectGraph, termContext, metricsOptions); + } + if (showSuggestion) { + return correctionSuggestionMessageFromTermContext(objectGraph, termContext, metricsOptions); + } + popLocation(metricsOptions.locationTracker); + return null; +} +/** + * Build a message that allows user to undo an auto-applied spell correction on user-initiated term. + * This is the "Showing Results for ABC. Search Instead For XYZ" variation for high-confidence misspellings + * @param termContext Context for the state of search terms. + * @param locationTracker Location tracker for page it is appearing in. + */ +function undoSpellCorrectionMessageFromTermContext(objectGraph, termContext, metricsOptions) { + // SearchAction for `term` again, disabling spell correction to `correctedTerm`. + const uncorrectedTerm = termContext.term; + const searchInsteadAction = searchActionForSpellCorrection(objectGraph, uncorrectedTerm, termContext.resultsTerm, "undoSpellCorrection"); + addEventsToSearchAction(objectGraph, searchInsteadAction, "button", metricsOptions.locationTracker); + // "Showing Results for <i>correctedTerm</i>" with no links. + const correctedToTerm = `<i>${termContext.correctedTerm}</i>`; + const showingResultsForTemplate = objectGraph.loc.string("SEARCH_SHOWING_RESULTS_FOR_TEMPLATE"); + const showingResultsForMessage = showingResultsForTemplate.replace("{searchTerm}", correctedToTerm); + const primaryText = new models.LinkableText(new models.StyledText(showingResultsForMessage, "text/x-apple-as3-nqml"), {}); + // "Search Instead for term" with link. + const searchInsteadForTemplate = objectGraph.loc.string("SEARCH_SEARCH_INSTEAD_FOR_TEMPLATE"); + const searchInsteadForMessage = searchInsteadForTemplate.replace("{searchTerm}", uncorrectedTerm); + const searchInsteadForLinks = {}; + searchInsteadForLinks[`${searchInsteadForMessage}`] = searchInsteadAction; + const secondaryText = new models.LinkableText(new models.StyledText(searchInsteadForMessage), searchInsteadForLinks); + return new models.SearchResultsMessage(primaryText, secondaryText, searchInsteadAction); +} +/** + * Build a message that allows user to accept a suggestion to modify a user-initiated term. + * This is the "Did you mean ABC?" variation for low-confidence misspellings + * @param termContext Context for the state of search terms. + * @param locationTracker Location tracker for page it is appearing in. + */ +function correctionSuggestionMessageFromTermContext(objectGraph, termContext, metricsOptions) { + // Search action for `suggestedTerm` + const suggestedTerm = termContext.suggestedTerm; + const suggestedSearchAction = searchActionForSpellCorrection(objectGraph, suggestedTerm, termContext.resultsTerm, "applySpellCorrection"); + addEventsToSearchAction(objectGraph, suggestedSearchAction, "button", metricsOptions.locationTracker); + // "Did you mean <i>suggestedTerm</i>?" where suggested term is linked. + const styledSuggestedTerm = `<i>${suggestedTerm}</i>`; + const didYouMeanTemplate = objectGraph.loc.string("SEARCH_DID_YOU_MEAN_TEMPLATE"); + const didYouMeanMessage = didYouMeanTemplate.replace("{searchTerm}", styledSuggestedTerm); + // Link both the raw suggested term, and suggested term followed by ? + const didYouMeanLinks = {}; + didYouMeanLinks[`${suggestedTerm}`] = suggestedSearchAction; + didYouMeanLinks[`${suggestedTerm}?`] = suggestedSearchAction; + const primaryText = new models.LinkableText(new models.StyledText(didYouMeanMessage, "text/x-apple-as3-nqml"), didYouMeanLinks); + return new models.SearchResultsMessage(primaryText, null, suggestedSearchAction); +} +// endregion +// region Search Action Builders +/** + * Build a SearchAction that is for: + * - Applying a suggested spell correction. + * - Undoing an automatically applied spell correction. + * + * Exported for testing + * + * @param suggestedOrUncorrectedTerm The suggestion or uncorrected term to search for. + * @param resultTerm The original result term. + * @param spellCorrectionActionType The type of spell correction search. + */ +export function searchActionForSpellCorrection(objectGraph, suggestedOrUncorrectedTerm, resultsTerm, spellCorrectionActionType) { + // SearchAction for `term` again, disabling spell correction to `correctedTerm`. + const suggestedSearchAction = new models.SearchAction(suggestedOrUncorrectedTerm, suggestedOrUncorrectedTerm, null, spellCorrectionActionType); + suggestedSearchAction.spellCheckEnabled = false; // Don't trigger corrections / suggestions again. + suggestedSearchAction.excludedTerms = [resultsTerm]; + suggestedSearchAction.originatingTerm = resultsTerm; + return suggestedSearchAction; +} +// endregion +//# sourceMappingURL=search-spell-correction.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/search/search-token.js b/node_modules/@jet-app/app-store/tmp/src/common/search/search-token.js new file mode 100644 index 0000000..f49b680 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/search/search-token.js @@ -0,0 +1,57 @@ +import { isNullOrEmpty } from "../../foundation/json-parsing/server-data"; +import { currentPosition } from "../metrics/helpers/location"; +/** + * Opaque token to use for paginating a list of search results. + * This is used for both standard and segmented search results. + */ +export class SearchToken { +} +/// The number of results to load per page. +const suggestedMaxResutsPerPage = 30; +/** + * Create a search token for loading more search results. + * @param results Remaining set of data that is yet to be paginated + * @param requestMetadata The nature of request that was fired. + * @param responseMetadata The meta blob returned as part of initial search. This is preserved, as subsequent pagination requests don't hit search endpoints. + * @param metricsOptions Metrics options to preserve during pagination + */ +export function createSearchToken(objectGraph, results, requestMetadata, responseMetadata, metricsOptions) { + if (isNullOrEmpty(results)) { + return null; + } + return { + results: results, + maxPerPage: suggestedMaxResutsPerPage, + requestMetadata: requestMetadata, + metricsOptions: metricsOptions, + responseMetadata: responseMetadata !== null && responseMetadata !== void 0 ? responseMetadata : {}, + contentOffsetWithinResultsShelf: currentPosition(metricsOptions.locationTracker), + }; +} +/** + * Returns the next set of items to load. + * @param searchToken Search token used for paginating. + */ +export function getNextItemsToFetch(objectGraph, searchToken) { + if (!searchToken || !searchToken.results) { + return []; + } + return searchToken.results.slice(0, searchToken.maxPerPage); +} +/** + * Advance the search token by the items that were loaded, consistent with `getNextItemsToFetch` + * @param searchToken Search token to create new token with. + */ +export function advanceSearchTokenResults(objectGraph, searchToken) { + let nextPageResults = []; + if (searchToken && searchToken.results) { + nextPageResults = searchToken.results.slice(searchToken.maxPerPage, searchToken.results.length); + } + if (isNullOrEmpty(nextPageResults)) { + return null; + } + const nextToken = { ...searchToken }; + nextToken.results = nextPageResults; + return nextToken; +} +//# sourceMappingURL=search-token.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/search/search.js b/node_modules/@jet-app/app-store/tmp/src/common/search/search.js new file mode 100644 index 0000000..aec35e0 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/search/search.js @@ -0,0 +1,1024 @@ +// +// search.ts +// AppStoreKit +// +// Created by Kevin MacWhinnie on 8/15/16. +// Copyright (c) 2016 Apple Inc. All rights reserved. +// +import { isNothing, isSome } from "@jet/environment"; +import * as validation from "@jet/environment/json/validation"; +import { FetchTimingMetricsBuilder } from "@jet/environment/metrics/fetch-timing-metrics-builder"; +import { PageInvocationPoint } from "@jet/environment/types/metrics"; +import * as models from "../../api/models"; +import { SearchResultsLearnMoreNotice } from "../../api/models"; +import * as serverData from "../../foundation/json-parsing/server-data"; +import * as mediaDataStructure from "../../foundation/media/data-structure"; +import * as contentArtwork from "../content/artwork/artwork"; +import * as metricsBuilder from "../metrics/builder"; +import * as metricsHelpersClicks from "../metrics/helpers/clicks"; +import * as metricsHelpersImpressions from "../metrics/helpers/impressions"; +import * as metricsHelpersLocation from "../metrics/helpers/location"; +import * as metricsHelpersMisc from "../metrics/helpers/misc"; +import * as metricsHelpersPage from "../metrics/helpers/page"; +import * as guidedSearch from "./guided-search/guided-search"; +import { addGuidedSearchParentImpressionMetrics, addSearchResultParentImpressionMetrics, } from "./guided-search/guided-search-metrics"; +import * as searchAdsODML from "./search-ads-odml"; +import * as searchCommon from "./search-common"; +import { createDefaultSelectedFacetOptions, createSearchFacets, createSearchPageFacets } from "./search-facets"; +import * as searchResultsFetching from "./search-results-fetching"; +import { createSearchResultsLearnMoreNoticeLinkableText } from "./search-results-learn-more-notice"; +import * as searchResultsPipeline from "./search-results-pipeline"; +import * as searchSpellCorrection from "./search-spell-correction"; +import * as searchToken from "./search-token"; +// MARK: - Search Hints +/** + * Convert the response from the search hints endpoint into a model object. + * @param {String} prefixTerm The term the search hints are for (i.e. searchPrefix). + * @param {String} searchUrl The Url if we were to search for this term right away + * @param {*} response The API response. + * @return {SearchHintSet} The search hint set containing the search hint array + */ +export function searchHintsFromApiResponse(objectGraph, prefixTerm, hintsContainer) { + return validation.context("searchHintsFromApiResponse", () => { + var _a, _b, _c, _d; + const metricsOptions = { + targetType: "listItem", + pageInformation: metricsHelpersPage.pageInformationForSearchHintsPage(objectGraph, prefixTerm, hintsContainer.hintsRequestUrl, hintsContainer.dataSetId), + locationTracker: metricsHelpersLocation.newLocationTracker(), + }; + // Build user hint that matches what user typed. Appears in first position. + let userTypedHintAction = null; + if ((_a = hintsContainer.userTypedTerm) === null || _a === void 0 ? void 0 : _a.length) { + userTypedHintAction = new models.SearchAction(hintsContainer.userTypedTerm, hintsContainer.userTypedTerm, null, "userTypedHint"); + userTypedHintAction.spellCheckEnabled = true; + userTypedHintAction.prefixTerm = prefixTerm; + metricsHelpersImpressions.addImpressionMetricsToHintsSearchAction(objectGraph, userTypedHintAction, metricsOptions); + metricsHelpersClicks.addEventsToSearchAction(objectGraph, userTypedHintAction, metricsOptions.targetType, metricsOptions.locationTracker, metricsOptions.pageInformation); + metricsHelpersLocation.nextPosition(metricsOptions.locationTracker); + } + // Build standard hints. + const searchHintActions = (_c = (_b = hintsContainer.rawHints) === null || _b === void 0 ? void 0 : _b.map((rawHint) => { + return searchHintAction(objectGraph, rawHint, prefixTerm, metricsOptions); + })) !== null && _c !== void 0 ? _c : []; + // Prepend user hint if any. + if (userTypedHintAction != null) { + searchHintActions.unshift(userTypedHintAction); + } + const hintSet = new models.SearchHintSet(searchHintActions, (_d = hintsContainer.ghostHintsTerm) !== null && _d !== void 0 ? _d : null); + /** + * Send `input` Search Events when search hints returns. + * For SSS, this is the granularity we agreed on, instead of sending it in native per keystroke. + * Unlike standard page metrics, we only: + * - Fire a 'input' search event + * - Setup pageFields for page fields generator. + */ + const searchEvent = metricsBuilder.createMetricsSearchData(objectGraph, prefixTerm, "key", "input", hintsContainer.hintsRequestUrl, { ...metricsHelpersMisc.fieldsFromPageInformation(metricsOptions.pageInformation) }); + hintSet.pageMetrics.pageFields = metricsHelpersMisc.fieldsFromPageInformation(metricsOptions.pageInformation); + hintSet.pageMetrics.addData(searchEvent, [PageInvocationPoint.pageEnter]); + hintSet.searchClearAction = createSearchCancelledOrClearedAction(objectGraph, "clear", metricsOptions.pageInformation, metricsOptions.locationTracker, prefixTerm); + hintSet.searchCancelAction = createSearchCancelledOrClearedAction(objectGraph, "cancel", metricsOptions.pageInformation, metricsOptions.locationTracker, prefixTerm); + return hintSet; + }); +} +/** + * Creates a search hint action from the search hint data + * @param objectGraph The App Store object graph + * @param hintData The hint object data + * @param prefixTerm The hint prefix term + * @param metricsOptions The metrics options to use for click and impressions metrics for this hint + * @returns A search hint action for the hint data + */ +function searchHintAction(objectGraph, hintData, prefixTerm, metricsOptions) { + var _a, _b, _c, _d, _e; + const searchEntity = (_a = searchEntityFromHintData(hintData)) !== null && _a !== void 0 ? _a : undefined; + const searchAction = new models.SearchAction((_b = hintData.displayTerm) !== null && _b !== void 0 ? _b : "", (_c = hintData.searchTerm) !== null && _c !== void 0 ? _c : "", null, "hints", searchEntity, hintData.source); + searchAction.artwork = contentArtwork.createArtworkForSystemImage(objectGraph, (_d = models.searchEntitySystemImage(searchEntity)) !== null && _d !== void 0 ? _d : "magnifyingglass"); + searchAction.spellCheckEnabled = true; + searchAction.prefixTerm = prefixTerm; + metricsHelpersImpressions.addImpressionMetricsToHintsSearchAction(objectGraph, searchAction, metricsOptions); + metricsHelpersClicks.addEventsToSearchAction(objectGraph, searchAction, metricsOptions.targetType, metricsOptions.locationTracker, (_e = metricsOptions.pageInformation) !== null && _e !== void 0 ? _e : undefined); + metricsHelpersLocation.nextPosition(metricsOptions.locationTracker); + return searchAction; +} +/** + * + * @param objectGraph The App Store object graph + * @param entity The entity value string from the hint object + * @param context The context value string from the hint object + * @returns The correct entity for the hint based on legacy entity types + * + * Entities: + * "arcade" ==> "apps" with context "arcade" + "developer" ==> "developers" + "story" ==> "editorial-items" + "watch" ==> "apps" with context "watch" + */ +function searchEntityFromHintData(rawHint) { + const hintEntity = rawHint.entity; + switch (hintEntity) { + case "apps": + return rawHint.context; + case "developers": + return "developer"; + case "editorial-items": + return "story"; + default: + return null; + } +} +// MARK: - Trending Searches +/** + * Convert the response from the trending searches endpoint into a model object. + * @param {*} response The API response. + * @return {TrendingSearch} A trending searches model object. + */ +export function trendingSearchesFromApiResponse(objectGraph, response) { + return validation.context("trendingSearchesFromApiResponse", () => { + const locationTracker = metricsHelpersLocation.newLocationTracker(); + const searches = serverData.asArrayOrEmpty(response, "trendingSearches").map((rawSearch) => { + const term = serverData.asString(rawSearch, "label"); + const searchAction = new models.SearchAction(term, term, serverData.asString(rawSearch, "url"), "trending"); + metricsHelpersClicks.addEventsToSearchAction(objectGraph, searchAction, "button", locationTracker); + metricsHelpersLocation.nextPosition(locationTracker); + return searchAction; + }); + const title = searches.length > 0 ? serverData.asString(response, "header.label") : null; + const trendingSearches = new models.TrendingSearches(title, searches); + switch (objectGraph.client.deviceType) { + case "pad": + trendingSearches.maxNumberOfSearches = 10; + break; + case "phone": + trendingSearches.maxNumberOfSearches = 7; + break; + default: + break; + } + return trendingSearches; + }); +} +// MARK: - Search Results +/** + * Gets the shelf ID for search results, on a specfic segment if applicable + * @param segmentTitle The title of the segment this shelf is on, if using segmented search + * @returns The id for the search results shelf + * @note This needs to include the segment title for segment search results since the contents on + * each segment would otherwise have the same shelf id and therefore would lead to the content never changing + */ +function getSearchResultsShelfId(segmentTitle) { + if (isSome(segmentTitle) && segmentTitle.length !== 0) { + return `SearchResults.${segmentTitle}.shelfId`; + } + else { + return "SearchResults.shelfId"; + } +} +/** + * Gets metrics pageid to use for the search results page. + * @param segmentType The segment this page is displaying if any + * @returns The id for the search results page + */ +function getSearchResultsPageId(segmentType) { + switch (segmentType) { + case models.SegmentedSearchResultsPageSegmentType.iOS: + return "ios"; + case models.SegmentedSearchResultsPageSegmentType.visionOS: + return "visionos"; + default: + return "SearchTopResults"; + } +} +/** + * Creates a new empty `SearchResults` object. + * @return {SearchResults} An empty search results model object. + */ +export function emptyResults(objectGraph, requestFacets) { + const searchResults = new models.SearchResults(); + if (serverData.isDefinedNonNull(requestFacets)) { + searchResults.facets = createSearchFacets(objectGraph, requestFacets); + searchResults.pageFacets = createSearchPageFacets(objectGraph); + searchResults.selectedFacetOptions = createDefaultSelectedFacetOptions(objectGraph); + } + searchResults.results = []; + return searchResults; +} +/** + * Creates a new empty `SearchResultsPage` object. + * @return {SearchResults} An empty search results model object. + */ +export function emptyResultsPage(objectGraph, requestFacets) { + const searchResultsPage = new models.SearchResultsPage([]); + if (serverData.isDefinedNonNull(requestFacets)) { + searchResultsPage.facets = createSearchFacets(objectGraph, requestFacets); + searchResultsPage.pageFacets = createSearchPageFacets(objectGraph); + searchResultsPage.selectedFacetOptions = createDefaultSelectedFacetOptions(objectGraph); + } + return searchResultsPage; +} +/** + * A container like class to manage a search results page segment and surrounding context + */ +class SegmentedSearchResultsPageSegmentContext { +} +/** + * Builds `BaseSearchPage` from data. + */ +async function baseSearchPageFromResponse(objectGraph, combinedSearchData) { + return await validation.context("searchResultsFromResponse", async () => { + var _a; + const fetchTimingMetricsBuilder = (_a = objectGraph.fetchTimingMetricsBuilder) !== null && _a !== void 0 ? _a : new FetchTimingMetricsBuilder(); + const page = await fetchTimingMetricsBuilder.measureModelConstructionAsync(async () => { + var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v; + const response = combinedSearchData.catalogResponse; + const requestMetadata = combinedSearchData.requestMetadata; + const searchRequestUrl = requestMetadata.searchRequestUrl; + const sponsoredSearchRequestData = combinedSearchData.sponsoredSearchRequestData; + const guidedSearchData = response.results.guidedSearch; + // Term Context + const termContext = searchCommon.createTermContextForSpellcheckedSequentialResponse(objectGraph, requestMetadata.requestDescriptor, response); + // Metrics + const wasOdmlSuccessful = searchAdsODML.wasODMLSuccessful(objectGraph, combinedSearchData.sponsoredSearchAdvertData); + const metricsOptions = { + locationTracker: metricsHelpersLocation.newLocationTracker(), + pageInformation: metricsHelpersPage.pageInformationForSearchPage(objectGraph, requestMetadata.requestDescriptor, response, termContext, searchRequestUrl, getSearchResultsPageId(), sponsoredSearchRequestData, wasOdmlSuccessful, guidedSearchData), + createUniqueImpressionId: true, + }; + // Root container + const isShelfBasedSearch = objectGraph.featureFlags.isEnabled("shelves_2_0_search") || + objectGraph.client.isiOS || + objectGraph.client.isTV || + objectGraph.client.isVision || + objectGraph.client.isWeb; + const baseSearchPage = isShelfBasedSearch + ? new models.SearchResultsPage() + : new models.SearchResults(); + // Guided Search Experimentation (pinned vs mid-scroll) + const guidedSearchPosition = searchResultsPipeline.guidedSearchPositionFromSearchResponseMeta(response.meta, objectGraph); + if (isNothing(guidedSearchPosition)) { + /** + * Guided Search + * - note: This is important to occur before `createSearchResults` since its impression index should be lower than the container for search results. + */ + addModelsForGuidedSearch(objectGraph, baseSearchPage, requestMetadata, guidedSearchData === null || guidedSearchData === void 0 ? void 0 : guidedSearchData.facets, metricsOptions); + } + const shelfMetricsOptions = { + id: "search-results", + kind: null, + softwareType: null, + targetType: "SearchResults", + title: "Search Results", + pageInformation: metricsOptions.pageInformation, + locationTracker: metricsOptions.locationTracker, + idType: "relationship", + }; + let resultsShelf; + if (isShelfBasedSearch) { + resultsShelf = new models.Shelf("searchResult"); + resultsShelf.id = getSearchResultsShelfId(); + resultsShelf.isHorizontal = false; + if (objectGraph.client.isWeb) { + resultsShelf.title = objectGraph.loc + .string("Search.ResultsTitle") + .replace("@@search_term@@", termContext.term); + } + // We need to create and apply impressions fields to the shelf before creating the search results, + // so that the shelf is sitting in the right location relative to its children from an impressions perspective. + metricsHelpersImpressions.addImpressionFields(objectGraph, resultsShelf, shelfMetricsOptions); + metricsHelpersLocation.pushContentLocation(objectGraph, shelfMetricsOptions, "Search Results"); + } + let advertData = dataForAdvertsFromCombinedData(objectGraph, combinedSearchData); + const appliedPolicy = (_a = combinedSearchData.sponsoredSearchAdvertData) === null || _a === void 0 ? void 0 : _a.appliedPolicy; + if (isSome(appliedPolicy)) { + // If there was an applied policy, suppress the ad. + advertData = []; + if (appliedPolicy === "ageRestricted") { + (_b = metricsOptions.pageInformation) === null || _b === void 0 ? void 0 : _b.iAdInfo.setMissedOpportunity(objectGraph, "ODP_NOAD", "searchResults"); + } + } + const unsafeSearch = (_f = (_e = (_d = (_c = response.meta) === null || _c === void 0 ? void 0 : _c.results) === null || _d === void 0 ? void 0 : _d.search) === null || _e === void 0 ? void 0 : _e.searchSafety) !== null && _f !== void 0 ? _f : false; + const missedOpportunityReason = ((_k = (_j = (_h = (_g = response.meta) === null || _g === void 0 ? void 0 : _g.results) === null || _h === void 0 ? void 0 : _h.search) === null || _j === void 0 ? void 0 : _j.reason) === null || _k === void 0 ? void 0 : _k.kind) === "no-results" ? "NLS_NORESULTS" : "NLS_NOAD"; + if (unsafeSearch) { + // If the search was deemed unsafe, suppress Ad and record the missed opportunity. + advertData = []; + (_l = metricsOptions.pageInformation) === null || _l === void 0 ? void 0 : _l.iAdInfo.setMissedOpportunity(objectGraph, missedOpportunityReason, "searchResults"); + } + /** + * Advert + Search Results + */ + const builderResults = await searchResultsPipeline.createSearchResults(objectGraph, requestMetadata, response.meta, metricsOptions, dataForSearchResultsFromCombinedData(objectGraph, combinedSearchData), advertData, guidedSearchData === null || guidedSearchData === void 0 ? void 0 : guidedSearchData.facets, installedStatesForAdvertsData(combinedSearchData), appStatesForAdvertsData(combinedSearchData)); + if (unsafeSearch && builderResults.builtSearchResults.length !== 0) { + const searchAdOpportunity = builderResults.builtSearchResults[0].lockup + .searchAdOpportunity; + searchAdOpportunity === null || searchAdOpportunity === void 0 ? void 0 : searchAdOpportunity.setMissedOpportunityReason(missedOpportunityReason); + searchAdOpportunity === null || searchAdOpportunity === void 0 ? void 0 : searchAdOpportunity.setTemplateType("APPLOCKUP"); + } + // Add result shelves to page. + if (isShelfBasedSearch && resultsShelf) { + const searchResultsPage = baseSearchPage; + resultsShelf.items = builderResults.builtSearchResults; + searchResultsPage.resultsParentImpressionMetrics = resultsShelf.impressionMetrics; + searchResultsPage.shelves.push(resultsShelf); + // Display the context message for the results (or lack thereof). + const contextCard = createSearchResultsContextCard(response.results.queryContext, objectGraph); + const resultsReason = (_p = (_o = (_m = response.meta) === null || _m === void 0 ? void 0 : _m.results) === null || _o === void 0 ? void 0 : _o.search) === null || _p === void 0 ? void 0 : _p.reason; + if ((resultsReason === null || resultsReason === void 0 ? void 0 : resultsReason.kind) === "no-results") { + searchResultsPage.unavailableReason = { + title: objectGraph.loc.stringWithFallback("Search.Results.Empty.Title", "No results"), + message: resultsReason.text, + action: actionFromSearchResultsLinks(resultsReason.links), + contextCard: contextCard, + }; + } + else if (contextCard) { + const paragraphShelf = new models.Shelf("searchResultsContextCard"); + paragraphShelf.items = [contextCard]; + const contextCardPosition = (_t = (_s = (_r = (_q = response.meta) === null || _q === void 0 ? void 0 : _q.displayStyle) === null || _r === void 0 ? void 0 : _r.queryContext) === null || _s === void 0 ? void 0 : _s.position) !== null && _t !== void 0 ? _t : 0; + if (contextCardPosition > 0) { + const postCardShelfContents = resultsShelf.items.splice(contextCardPosition); + const postCardResulsShelf = { + ...resultsShelf, + id: "searchResults2", + items: postCardShelfContents, + isValid: resultsShelf.isValid, + }; + searchResultsPage.shelves.push(paragraphShelf); + searchResultsPage.shelves.push(postCardResulsShelf); + } + else { + searchResultsPage.shelves.unshift(paragraphShelf); + } + } + // Remove unsafe searches from recents if no results were provided. + if (unsafeSearch && builderResults.builtSearchResults.length === 0) { + (_v = (_u = objectGraph.onDeviceSearchHistoryManager).removeRecentSearchTerm) === null || _v === void 0 ? void 0 : _v.call(_u, termContext.term); + } + } + else { + const searchResults = baseSearchPage; + searchResults.results = builderResults.builtSearchResults; + addSearchResultParentImpressionMetrics(objectGraph, searchResults, metricsOptions); + } + /** + * Next Page + */ + if (builderResults.deferredSearchResults.length > 0) { + baseSearchPage.nextPage = searchToken.createSearchToken(objectGraph, builderResults.deferredSearchResults, requestMetadata, response.meta, metricsOptions); + } + if (isShelfBasedSearch) { + metricsHelpersLocation.popLocation(shelfMetricsOptions.locationTracker); + } + /** + * Spell Correction Message + */ + baseSearchPage.message = searchSpellCorrection.spellCorrectionMessageFromTermContext(objectGraph, termContext, metricsOptions); + /** + * Search Entity + */ + const searchEntity = requestMetadata.requestDescriptor.searchEntity; + const searchEntitySpecified = !serverData.isNullOrEmpty(searchEntity); + if (!searchEntitySpecified) { + baseSearchPage.facets = createSearchFacets(objectGraph, requestMetadata.requestDescriptor.facets, combinedSearchData.categoriesFilterData); + baseSearchPage.pageFacets = createSearchPageFacets(objectGraph, combinedSearchData.categoriesFilterData); + baseSearchPage.selectedFacetOptions = serverData.isDefinedNonNullNonEmpty(combinedSearchData.requestMetadata.requestDescriptor.selectedFacetOptions) + ? combinedSearchData.requestMetadata.requestDescriptor.selectedFacetOptions + : createDefaultSelectedFacetOptions(objectGraph); + } + // Attach search term context + baseSearchPage.searchTermContext = termContext; + // Enable autoplay search results on all clients except tv. + baseSearchPage.isAutoPlayEnabled = objectGraph.client.deviceType !== "tv"; + baseSearchPage.isCondensedSearchLockupsEnabled = objectGraph.client.isPhone; + // Search Transparency + baseSearchPage.transparencyLink = createSearchResultsLearnMoreNoticeLinkableText(objectGraph, metricsOptions); + metricsHelpersPage.addMetricsEventsToPageWithInformation(objectGraph, baseSearchPage, metricsOptions.pageInformation); + baseSearchPage.searchClearAction = createSearchCancelledOrClearedAction(objectGraph, "clear", metricsOptions.pageInformation, metricsOptions.locationTracker, termContext.term); + baseSearchPage.searchCancelAction = createSearchCancelledOrClearedAction(objectGraph, "cancel", metricsOptions.pageInformation, metricsOptions.locationTracker, termContext.term); + return baseSearchPage; + }); + return page; + }); +} +// Creates context card model for search results from media API data. +export function createSearchResultsContextCard(queryContext, objectGraph) { + return validation.context("createSearchResultsContextCard", () => { + var _a, _b; + if (isNothing(queryContext) || !["iOS", "macOS"].includes(objectGraph.host.platform)) { + return undefined; // no server data or platform unsupported + } + const linkActions = (_a = queryContext.links) === null || _a === void 0 ? void 0 : _a.map(actionFromSearchResultsLink); + const linkedSubstrings = linkActions === null || linkActions === void 0 ? void 0 : linkActions.reduce((map, linkAction) => { + var _a; + if ((_a = linkAction.title) === null || _a === void 0 ? void 0 : _a.length) { + if (objectGraph.host.isMac) { + linkAction.title += "\u00a0\u2197"; + } + map[linkAction.title] = linkAction; + } + return map; + }, {}); + let rawText = queryContext.text; + if ((linkActions === null || linkActions === void 0 ? void 0 : linkActions.length) === 1 && ((_b = linkActions[0].title) === null || _b === void 0 ? void 0 : _b.length)) { + // Add single link to end of text, separated by a space. + rawText += " " + linkActions[0].title; + } + else if (linkActions && linkActions.length > 1) { + // Add multiple lines below end of text, separated by a new line. + if (rawText.length > 0) { + rawText += "\n"; + } + rawText += linkActions + .map((action) => action.title) + .filter((title) => title === null || title === void 0 ? void 0 : title.length) + .join("\n"); + } + const styledText = new models.StyledText(rawText); + const linkableText = new models.LinkableText(styledText, linkedSubstrings); + const contextCard = new models.SearchResultsContextCard(linkableText); + return contextCard; + }); +} +/// Creates and returns the first url action from the provided link data included in the search results response. +function actionFromSearchResultsLinks(linksData) { + return validation.context("actionFromSearchResultsLinks", () => { + const bestLinkData = linksData === null || linksData === void 0 ? void 0 : linksData.find((linkData) => linkData.url.length > 0); + return bestLinkData ? actionFromSearchResultsLink(bestLinkData) : undefined; + }); +} +/// Creates and returns an action from the provided link data included in the search results response. +function actionFromSearchResultsLink(linkData) { + return validation.context("actionFromSearchResultsLink", () => { + var _a; + const linkAction = new models.ExternalUrlAction(linkData.url, false); + linkAction.title = (_a = linkData.label) === null || _a === void 0 ? void 0 : _a.replace(" ", "\u00a0"); // Use non-breaking spaces for link labels. + linkAction.artwork = new models.Artwork("systemimage://arrow.up.forward", 0, 0, []); + return linkAction; + }); +} +function installedStatesForAdvertsData(combinedSearchData) { + var _a, _b; + return (_b = (_a = combinedSearchData.sponsoredSearchAdvertData) === null || _a === void 0 ? void 0 : _a.installedStates) !== null && _b !== void 0 ? _b : {}; +} +function appStatesForAdvertsData(combinedSearchData) { + var _a, _b; + return (_b = (_a = combinedSearchData.sponsoredSearchAdvertData) === null || _a === void 0 ? void 0 : _a.appStates) !== null && _b !== void 0 ? _b : {}; +} +export async function searchResultsFromResponse(objectGraph, combinedSearchData) { + return await baseSearchPageFromResponse(objectGraph, combinedSearchData); +} +export async function searchResultsPageFromResponse(objectGraph, combinedSearchData) { + return await baseSearchPageFromResponse(objectGraph, combinedSearchData); +} +/** + * Creates a new empty `SegmentedSearchResultsPage` object. + * @return An empty segmented search results model object. + */ +export function emptySegmentedResultsPage(objectGraph) { + return new models.SegmentedSearchResultsPage(); +} +/** + * Creates the segmented search results page form the response + * @param objectGraph The app store object graph + * @param combinedSearchData The combined data for the segmented search results response + * @returns A promise of the segmented search results page + */ +export async function segmentedSearchResultsPageFromResponse(objectGraph, combinedSearchData) { + return await validation.context("segmentedSearchResultsPageFromResponse", async () => { + const response = combinedSearchData.catalogResponse; + const requestMetadata = combinedSearchData.requestMetadata; + if (isSome(response.results.search.groups)) { + const fetchPages = response.results.search.groups.map(async (group) => { + const segmentResponse = await baseSegmentFromGroupResponse(objectGraph, combinedSearchData, response, group, requestMetadata); + return segmentResponse; + }); + return await Promise.all(fetchPages).then((pageContexts) => { + const page = new models.SegmentedSearchResultsPage(); + if (serverData.isNullOrEmpty(pageContexts)) { + return page; + } + const segments = pageContexts.map((context) => { + return context.segment; + }); + page.segments = segments; + const initialSegmentId = response.results.search.autoSelectedGroupId || segments[0].groupId; + page.selectedSegmentId = initialSegmentId; + addSegmentChangeActionsToPages(objectGraph, pageContexts, initialSegmentId); + return page; + }); + } + else { + return new models.SegmentedSearchResultsPage(); + } + }); +} +/** + * Adds all segment change actions to each search result page segment + * @param objectGraph The App Store object graph + * @param contexts The segment contexts for the page + * @param initialSegmentId The initial segment Id. + */ +function addSegmentChangeActionsToPages(objectGraph, contexts, initialSegmentId) { + const firstNonEmptySegmentContext = contexts.find((context) => { + const hasNonEmptyPage = context.segment.page.shelves.some((shelf) => { + const isNotEmpty = shelf.items.length > 0; + return isNotEmpty; + }); + return hasNonEmptyPage; + }); + const firstNonEmptySegmentId = firstNonEmptySegmentContext === null || firstNonEmptySegmentContext === void 0 ? void 0 : firstNonEmptySegmentContext.segment.groupId; + const segments = contexts.map((context) => { + return context.segment; + }); + const nativeSegmentGroupId = segmentGroupIdForPlatform(objectGraph, segments); + // Check if the native segment has content. + const nativeSegmentIsNonEmpty = segments.some((segment) => { + if (segment.groupId !== nativeSegmentGroupId) { + return false; + } + return segment.page.shelves.some((shelf) => { + return shelf.items.length > 0; + }); + }); + contexts.forEach((context) => { + context.segment.emptySegmentChangeAction = segmentChangeAction(objectGraph, SegmentChangeReason.EmptyResults, context.segment.page.id, context.metricsOptions, firstNonEmptySegmentId, context.segment.groupId); + // Only add the non-native segment change action if the native segment has content, and + // the initial segment wasn't the native segment. + if (nativeSegmentIsNonEmpty && initialSegmentId !== nativeSegmentGroupId) { + context.segment.nonNativeSegmentChangeAction = segmentChangeAction(objectGraph, SegmentChangeReason.NonNative, context.segment.page.id, context.metricsOptions, nativeSegmentGroupId, context.segment.groupId); + } + context.segment.segmentPickerSegmentChangeAction = segmentChangeAction(objectGraph, SegmentChangeReason.Picker, context.segment.page.id, context.metricsOptions, context.segment.groupId); + }); +} +/** + * + * @param objectGraph The app store object graph + * @param combinedSearchData The combined search results which contains the response for segmented results + * @param groupResponse The response for the segmented results + * @param searchData The data for a single search results segment + * @param requestMetadata The metadata from the search request + * @returns A promise of a segmented search results page segment + */ +async function baseSegmentFromGroupResponse(objectGraph, combinedSearchData, groupResponse, searchData, requestMetadata) { + return await validation.context("baseSearchPageFromGroupResponse", async () => { + const searchRequestUrl = requestMetadata.searchRequestUrl; + const segmentType = segmentTypeFromGroupId(searchData.groupId); + // Term Context + const termContext = searchCommon.createTermContextForSpellcheckedGroupedResponse(objectGraph, requestMetadata.requestDescriptor, groupResponse); + // Metrics + const metricsOptions = { + locationTracker: metricsHelpersLocation.newLocationTracker(), + pageInformation: metricsHelpersPage.pageInformationForSearchPage(objectGraph, requestMetadata.requestDescriptor, groupResponse, termContext, searchRequestUrl, getSearchResultsPageId(segmentType), null, false, null), + createUniqueImpressionId: true, + }; + const shelfMetricsOptions = { + id: "search-results", + kind: null, + softwareType: null, + targetType: "SearchResults", + title: "Search Results", + pageInformation: metricsOptions.pageInformation, + locationTracker: metricsOptions.locationTracker, + idType: "relationship", + }; + // Root container + const searchResultsPage = new models.SearchResultsPage(); + const searchSegment = new models.SegmentedSearchResultsPageSegment(); + const resultsShelf = new models.Shelf("searchResult"); + resultsShelf.isHorizontal = false; + // We need to create and apply impressions fields to the shelf before creating the search results, + // so that the shelf is sitting in the right location relative to its children from an impressions perspective. + metricsHelpersImpressions.addImpressionFields(objectGraph, resultsShelf, shelfMetricsOptions); + metricsHelpersLocation.pushContentLocation(objectGraph, shelfMetricsOptions, "Search Results"); + const resultsData = mediaDataStructure.dataCollectionFromDataContainer(searchData); + const builderResults = await searchResultsPipeline.createSearchResults(objectGraph, requestMetadata, groupResponse.meta, metricsOptions, resultsData); + const learnMoreNoticeLinkableText = createSearchResultsLearnMoreNoticeLinkableText(objectGraph, metricsOptions); + const shelves = searchResultsShelvesFromBuilderResults(objectGraph, learnMoreNoticeLinkableText, builderResults, resultsShelf); + searchResultsPage.shelves.push(...shelves); + searchResultsPage.resultsParentImpressionMetrics = resultsShelf.impressionMetrics; + if (builderResults.deferredSearchResults.length > 0) { + searchResultsPage.nextPage = searchToken.createSearchToken(objectGraph, builderResults.deferredSearchResults, requestMetadata, groupResponse.meta, metricsOptions); + } + metricsHelpersLocation.popLocation(shelfMetricsOptions.locationTracker); + /** + * Spell Correction Message + */ + searchResultsPage.message = searchSpellCorrection.spellCorrectionMessageFromTermContext(objectGraph, termContext, metricsOptions); + // Enable autoplay search results on all clients except tv. + searchResultsPage.isAutoPlayEnabled = objectGraph.client.deviceType !== "tv"; + searchResultsPage.isCondensedSearchLockupsEnabled = objectGraph.client.isPhone; + // Search Transparency + searchResultsPage.transparencyLink = learnMoreNoticeLinkableText; + metricsHelpersPage.addMetricsEventsToPageWithInformation(objectGraph, searchResultsPage, metricsOptions.pageInformation); + searchSegment.groupId = searchData.groupId; + resultsShelf.id = getSearchResultsShelfId(searchSegment.groupId); + searchSegment.page = searchResultsPage; + searchSegment.title = segmentTitleForSegmentType(objectGraph, false, segmentType); + return { + segment: searchSegment, + metricsOptions: metricsOptions, + }; + }); +} +/** + * Get an array of shelves from the search builder results. + * This is used to decide where to insert the search results learn more notice across + * initial and paginated page builds. + * + * This is currently only used on visionOS search paths, but has the appropriate checks to ensure + * it doesn't run on unsupported platforms, so could be used in all search code paths. + * @param objectGraph The Object Graph. + * @param learnMoreNoticeLinkableText The linkable text to display as part of the learn more notice. + * @param builderResults The results from `createSearchResults`. + * @param resultsShelf The initial shelf created to hold the results. This is passed in + * because in many cases a reference to this is required later when building out the page. + * @param startingContentOffsetWithinResults The starting content offset before the newly built + * content is added. For an initial page build, this will be zero, and for paginated pages this + * is stored on the token. + * @returns An array of shelves for a search results page, with the learn more notice inserted + * if required. + */ +function searchResultsShelvesFromBuilderResults(objectGraph, learnMoreNoticeLinkableText, builderResults, resultsShelf, startingContentOffsetWithinResults = 0) { + let learnMoreNoticeIndex = searchResultsLearnMoreNoticeIndex(objectGraph); + const builtResultsCount = builderResults.builtSearchResults.length; + const hasDeferredResults = builderResults.deferredSearchResults.length > 0; + // The total built results for the page so far. This is the initial page, plus any paginated content. + const totalBuiltResults = startingContentOffsetWithinResults + builtResultsCount; + // There are a number of conditions we don't want to insert the learn more notice: + // 1. No index at which to insert it - this generally means the platform doesn't support this method of insertion. + // 2. No linkable text object - the bag key wasn't present. + // 3. The starting offset is greater than or equal to the insertion index - we can assume we've already shown the + // notice in a previous page build. + // 4. We have deferred results and the total built results count is less than the threshold - we'll insert the + // notice in a subsequent fetch. + // 5. We have no built results and no deferred results, ie. the page will be empty. + // If we fail any of these conditions, we will just attach all the items into the single shelf and return that. + if (learnMoreNoticeIndex === undefined || + learnMoreNoticeLinkableText === undefined || + startingContentOffsetWithinResults >= learnMoreNoticeIndex || + (hasDeferredResults && totalBuiltResults < learnMoreNoticeIndex) || + (totalBuiltResults === 0 && !hasDeferredResults)) { + resultsShelf.items = builderResults.builtSearchResults; + return [resultsShelf]; + } + // Adjust the index by the starting offset, to ensure we take any items built in the initial fetch into account. + learnMoreNoticeIndex = learnMoreNoticeIndex - startingContentOffsetWithinResults; + // We have satisfied the insertion conditions for the learn more notice - we need to split the shelf in two to incorporate + // the learn more notice as its own shelf in between. This is due to layout restrictions on visionOS where we cannot + // accommodate a full width item in a grid of two items per row. + resultsShelf.items = builderResults.builtSearchResults.slice(0, learnMoreNoticeIndex); + const learnMoreNoticeShelf = new models.Shelf("searchResultsLearnMoreNotice"); + learnMoreNoticeShelf.isHorizontal = false; + const searchResult = new SearchResultsLearnMoreNotice(learnMoreNoticeLinkableText); + learnMoreNoticeShelf.items = [searchResult]; + const shelves = [resultsShelf, learnMoreNoticeShelf]; + const secondResultsShelfItems = builderResults.builtSearchResults.slice(learnMoreNoticeIndex); + if (secondResultsShelfItems.length > 0) { + const secondResultsShelf = new models.Shelf("searchResult"); + // Give the second results shelf the same impression metrics. This will result in each shelf contributing to the same impression metrics, + // the only difference is we will see two "viewedInfo" entries for each distinct shelf within the top level impressions item. + secondResultsShelf.impressionMetrics = resultsShelf.impressionMetrics; + secondResultsShelf.isHorizontal = false; + secondResultsShelf.items = secondResultsShelfItems; + shelves.push(secondResultsShelf); + } + return shelves; +} +/** + * Get the index for where the Search Results Learn More Notice should be inserted. + * Returns undefined if either: + * - the editorial ID is not available in the bag, or + * - the platform does not support this style of presentation. + * @param objectGraph The Object Graph + */ +function searchResultsLearnMoreNoticeIndex(objectGraph) { + const editorialItemId = objectGraph.bag.searchResultsLearnMoreEditorialId; + if (isNothing(editorialItemId) || (editorialItemId === null || editorialItemId === void 0 ? void 0 : editorialItemId.length) === 0) { + return undefined; + } + if (objectGraph.client.isVision) { + return 6; + } + return undefined; +} +export async function paginatedSearchResultsWithToken(objectGraph, token) { + return await validation.context("paginatedSearchResultsWithToken", async () => { + const nextItemsToFetch = searchToken.getNextItemsToFetch(objectGraph, token); + const advancedToken = searchToken.advanceSearchTokenResults(objectGraph, token); + if (nextItemsToFetch.length === 0) { + return await Promise.resolve(emptyResults(objectGraph)); + } + return await searchResultsFetching + .fetchSearchResultItems(objectGraph, nextItemsToFetch) + .then(async (dataContainer) => { + const resultsDatum = mediaDataStructure.dataCollectionFromDataContainer(dataContainer); + return await searchResultsPipeline + .createSearchResults(objectGraph, token.requestMetadata, token.responseMetadata, token.metricsOptions, resultsDatum) + .then((builderResults) => { + const searchResults = new models.SearchResults(); + searchResults.results = builderResults.builtSearchResults; + searchResults.nextPage = advancedToken; + return searchResults; + }); + }); + }); +} +export async function paginatedSearchResultsPageWithToken(objectGraph, token) { + return await validation.context("paginatedSearchResultsPageWithToken", async () => { + const nextItemsToFetch = searchToken.getNextItemsToFetch(objectGraph, token); + const advancedToken = searchToken.advanceSearchTokenResults(objectGraph, token); + if (nextItemsToFetch.length === 0) { + return await Promise.resolve(emptyResultsPage(objectGraph)); + } + return await searchResultsFetching + .fetchSearchResultItems(objectGraph, nextItemsToFetch) + .then(async (dataContainer) => { + const resultsDatum = mediaDataStructure.dataCollectionFromDataContainer(dataContainer); + const shelfMetricsOptions = { + id: "search-results", + kind: null, + softwareType: null, + targetType: "SearchResults", + title: "Search Results", + pageInformation: token.metricsOptions.pageInformation, + locationTracker: token.metricsOptions.locationTracker, + idType: "relationship", + }; + // Set up the "new" shelf + const resultsShelf = new models.Shelf("searchResult"); + resultsShelf.id = getSearchResultsShelfId(); + resultsShelf.isHorizontal = false; + // Shelf Impressions: Add impression fields. + // This shelf, and the associated metrics data isn't really used - it's basically just a vehicle for the new items to be + // merged into the old page/shelf. Even so, we create and apply impressions metrics correctly before setting the content + // location and current position so it looks correct. + metricsHelpersImpressions.addImpressionFields(objectGraph, resultsShelf, shelfMetricsOptions); + // Set the content location to the "search-results" shelf. + metricsHelpersLocation.pushContentLocation(objectGraph, shelfMetricsOptions, "Search Results"); + // Update position to content offset within the same search shelf prior to building the new results. + metricsHelpersLocation.setCurrentPosition(shelfMetricsOptions.locationTracker, token.contentOffsetWithinResultsShelf); + return await searchResultsPipeline + .createSearchResults(objectGraph, token.requestMetadata, token.responseMetadata, token.metricsOptions, resultsDatum) + .then((builderResults) => { + const learnMoreNoticeLinkableText = createSearchResultsLearnMoreNoticeLinkableText(objectGraph, shelfMetricsOptions); + const shelves = searchResultsShelvesFromBuilderResults(objectGraph, learnMoreNoticeLinkableText, builderResults, resultsShelf, token.contentOffsetWithinResultsShelf); + const searchResultsPage = new models.SearchResultsPage(shelves); + // Ensure we increment the offset for any future paginated results. + if (serverData.isDefinedNonNull(advancedToken)) { + advancedToken.contentOffsetWithinResultsShelf = metricsHelpersLocation.currentPosition(shelfMetricsOptions.locationTracker); + searchResultsPage.nextPage = advancedToken; + } + searchResultsPage.isCondensedSearchLockupsEnabled = objectGraph.client.isPhone; + searchResultsPage.resultsParentImpressionMetrics = resultsShelf.impressionMetrics; + metricsHelpersLocation.popLocation(token.metricsOptions.locationTracker); + searchResultsPage.searchClearAction = createSearchCancelledOrClearedAction(objectGraph, "clear", token.metricsOptions.pageInformation, token.metricsOptions.locationTracker, token.requestMetadata.requestDescriptor.term); + searchResultsPage.searchCancelAction = createSearchCancelledOrClearedAction(objectGraph, "cancel", token.metricsOptions.pageInformation, token.metricsOptions.locationTracker, token.requestMetadata.requestDescriptor.term); + return searchResultsPage; + }); + }); + }); +} +// region Extracting Data +/** + * Returns the array of data objects to build search results with. + */ +function dataForSearchResultsFromCombinedData(objectGraph, combinedSearchData) { + return mediaDataStructure.dataCollectionFromDataContainer(combinedSearchData.catalogResponse.results.search); +} +/** + * Returns the array of data objects to build search adverts with. + */ +function dataForAdvertsFromCombinedData(objectGraph, combinedSearchData) { + const rawAdvertsData = mediaDataStructure.dataCollectionFromDataContainer(combinedSearchData.catalogResponse.results["ads-result"]); + return searchAdsODML.applyNativeAdvertData(objectGraph, rawAdvertsData, combinedSearchData.sponsoredSearchAdvertData); +} +// endregion +// region Guided Search +/** + * Add models for Guided Search into `SearchResults` model, specifically: + * - `GuidedSearchToken`s + * - `GuidedSearchQuery`s + * - Metrics Container for tokens. + */ +function addModelsForGuidedSearch(objectGraph, searchResultsPage, requestMetadata, facetData, metricsOptions) { + if (!objectGraph.host.isiOS) { + return; + } + const request = requestMetadata.requestDescriptor; + metricsHelpersLocation.pushBasicLocation(objectGraph, { + pageInformation: metricsOptions.pageInformation, + locationTracker: metricsOptions.locationTracker, + targetType: "SearchRevisions", + }, ""); + const tokens = []; + // Tokens from facet data, if any + if (serverData.isDefinedNonNullNonEmpty(facetData)) { + for (const data of facetData) { + const token = guidedSearch.createGuidedSearchToken(objectGraph, "toggle", request, data, metricsOptions); + if (token) { + tokens.push(token); + metricsHelpersLocation.nextPosition(metricsOptions.locationTracker); + } + } + } + // Token from selected entity hint, iff there aren't tokens already + if (serverData.isNullOrEmpty(tokens) && requestMetadata.requestDescriptor.searchEntity) { + const entityClearingToken = guidedSearch.createGuidedSearchTokenClearingEntityFilter(objectGraph, requestMetadata.requestDescriptor, metricsOptions); + tokens.push(entityClearingToken); + metricsHelpersLocation.nextPosition(metricsOptions.locationTracker); + } + const queries = guidedSearch.createGuidedSearchQueries(objectGraph, requestMetadata.requestDescriptor, facetData); + metricsHelpersLocation.popLocation(metricsOptions.locationTracker); + if (serverData.isDefinedNonNullNonEmpty(tokens)) { + searchResultsPage.guidedSearchTokens = tokens; + searchResultsPage.guidedSearchQueries = queries; + addGuidedSearchParentImpressionMetrics(objectGraph, searchResultsPage, metricsOptions); + metricsHelpersLocation.nextPosition(metricsOptions.locationTracker); // increment **after** assigning guided token parent impression. + } +} +/// The contextual reason for the segment change +var SegmentChangeReason; +(function (SegmentChangeReason) { + SegmentChangeReason[SegmentChangeReason["EmptyResults"] = 0] = "EmptyResults"; + SegmentChangeReason[SegmentChangeReason["Picker"] = 1] = "Picker"; + SegmentChangeReason[SegmentChangeReason["NonNative"] = 2] = "NonNative"; +})(SegmentChangeReason || (SegmentChangeReason = {})); +/** + * Creates a segment change action for moving from one segment to another for a specific reason + * @param objectGraph The App Store object graph + * @param reason The reason why the segment is changing + * @param pageId The search results page id + * @param metricsOptions The metrics options for the segment this action will attach to + * @param switchingToGroupId The groupId of the segment we are switching to + * @param switchingFromGroupId The groupId of the segment we are switching from (the current segment) + * @returns A segment change action for the specific context and locations + */ +function segmentChangeAction(objectGraph, reason, pageId, metricsOptions, switchingToGroupId, switchingFromGroupId) { + const includeAppsSuffix = reason !== SegmentChangeReason.Picker; + const switchingToSegmentTitle = segmentTitleForSegmentType(objectGraph, includeAppsSuffix, segmentTypeFromGroupId(switchingToGroupId)); + let action; + switch (reason) { + case SegmentChangeReason.EmptyResults: + if (isNothing(switchingToSegmentTitle) || switchingFromGroupId === switchingToGroupId) { + return undefined; + } + metricsHelpersLocation.pushBasicLocation(objectGraph, { + pageInformation: metricsOptions.pageInformation, + locationTracker: metricsOptions.locationTracker, + targetType: "SearchResults", + }, "emptyResultsSegmentSwitch"); + const emptyResultsTitle = objectGraph.loc + .string("SEARCH_RESULTS_SWITCH_TO_OTHER_RESULTS") + .replace("{platformApps}", switchingToSegmentTitle); + action = new models.SearchPageSegmentChangeAction(switchingToGroupId, switchingToSegmentTitle, new models.StyledText(emptyResultsTitle)); + metricsHelpersClicks.addClickEventToSearchPageSegmentChangeAction(objectGraph, action, "button", metricsOptions.locationTracker); + metricsHelpersLocation.popLocation(metricsOptions.locationTracker); + break; + case SegmentChangeReason.Picker: + if (isNothing(switchingToSegmentTitle)) { + return undefined; + } + metricsHelpersLocation.pushBasicLocation(objectGraph, { + pageInformation: metricsOptions.pageInformation, + locationTracker: metricsOptions.locationTracker, + targetType: "SearchResults", + }, "searchResultsSegmentSwitch"); + action = new models.SearchPageSegmentChangeAction(switchingToGroupId, switchingToSegmentTitle); + metricsHelpersClicks.addClickEventToSearchPageSegmentChangeAction(objectGraph, action, "button", metricsOptions.locationTracker); + metricsHelpersLocation.popLocation(metricsOptions.locationTracker); + break; + case SegmentChangeReason.NonNative: + const currentSegmentType = segmentTypeFromGroupId(switchingFromGroupId); + if (currentSegmentType === segmentTypeForPlatform(objectGraph)) { + return undefined; + } + const switchingFromSegmentTitle = segmentTitleForSegmentType(objectGraph, true, segmentTypeFromGroupId(switchingFromGroupId)); + const nonNativeTitle = objectGraph.loc + .string("Search.Results.ShowingNonNativeResults") + .replace("@@current_platform_apps@@", switchingFromSegmentTitle) + .replace("@@native_platform_apps@@", switchingToSegmentTitle); + metricsHelpersLocation.pushBasicLocation(objectGraph, { + pageInformation: metricsOptions.pageInformation, + locationTracker: metricsOptions.locationTracker, + targetType: "SearchResults", + }, "nonNativeResultsSegmentSwitch"); + action = new models.SearchPageSegmentChangeAction(switchingToGroupId, switchingToSegmentTitle, new models.StyledText(nonNativeTitle, "text/x-apple-as3-nqml")); + metricsHelpersLocation.popLocation(metricsOptions.locationTracker); + break; + default: + break; + } + const clickActionOptions = { + actionType: "navigate", + id: pageId, + targetType: "button", + pageInformation: metricsOptions.pageInformation, + locationTracker: metricsOptions.locationTracker, + }; + metricsHelpersClicks.addClickEventToAction(objectGraph, action, clickActionOptions); + return action; +} +/** + * Gets the segment type for the given group id + * @param groupId The group id of the segment + * @returns The segment type for the group id + */ +function segmentTypeFromGroupId(groupId) { + if (isSome(groupId)) { + switch (groupId) { + case "iOS": + case "ios": + return models.SegmentedSearchResultsPageSegmentType.iOS; + case "xrOS": + case "xros": + return models.SegmentedSearchResultsPageSegmentType.visionOS; + default: + return undefined; + } + } + return undefined; +} +/** + * Gets the segment type for the current platform + * @param objectGraph The App Store object graph + * @returns The segment type for the current platform + */ +function segmentTypeForPlatform(objectGraph) { + switch (objectGraph.client.deviceType) { + case "vision": + return models.SegmentedSearchResultsPageSegmentType.visionOS; + case "pad": + case "phone": + return models.SegmentedSearchResultsPageSegmentType.iOS; + default: + return undefined; + } +} +/** + * Gets the segment title for the given segment type + * @param objectGraph The App Store object graph + * @param includeAppsSuffix Whether to include the word "Apps" at the end of the title + * @param segmentType The segment type we want the title of + * @returns The segment title for the segment type + */ +function segmentTitleForSegmentType(objectGraph, includeAppsSuffix, segmentType) { + switch (segmentType) { + case models.SegmentedSearchResultsPageSegmentType.visionOS: + return includeAppsSuffix + ? objectGraph.loc.string("SEARCH_RESULTS_VISION_APPS_TITLE") + : objectGraph.loc.string("SEARCH_RESULTS_VISION_TITLE"); + case models.SegmentedSearchResultsPageSegmentType.iOS: + return includeAppsSuffix + ? objectGraph.loc.string("SEARCH_RESULTS_IPHONE_IPAD_APPS_TITLE") + : objectGraph.loc.string("SEARCH_RESULTS_IPHONE_IPAD_TITLE"); + default: + return undefined; + } +} +/** + * Gets the group id for the current platform + * @param objectGraph The App Store object graph + * @param segments The segments on the search results page + * @returns The group id for the current platform + */ +function segmentGroupIdForPlatform(objectGraph, segments) { + const nativeSegmentType = segmentTypeForPlatform(objectGraph); + const nativeSegment = segments.find((segment) => { + return segmentTypeFromGroupId(segment.groupId) === nativeSegmentType; + }); + return nativeSegment === null || nativeSegment === void 0 ? void 0 : nativeSegment.groupId; +} +/** + * + * @param objectGraph The App Store Object Graph + * @param searchClearActionType The way the user cleared the search (x in search field or cancel button in toolbar) + * @param pageInformation The metrics page information + * @param locationTracker The metrics location tracker + * @param searchTerm The current search term + * @returns An action to trigger when the search is cancelled or cleared + */ +export function createSearchCancelledOrClearedAction(objectGraph, searchClearActionType, pageInformation, locationTracker, searchTerm) { + const action = new models.BlankAction(); + let type; + let targetId; + switch (searchClearActionType) { + case "cancel": + type = "dismiss"; + targetId = "cancel"; + break; + case "clear": + type = "delete"; + targetId = "clear"; + break; + default: + break; + } + metricsHelpersClicks.addClickEventToSearchCancelOrDismissAction(objectGraph, action, { + targetType: "button", + id: targetId, + idType: undefined, + actionType: type, + pageInformation: pageInformation, + locationTracker: locationTracker, + }, "button", searchTerm); + return action; +} +//# sourceMappingURL=search.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/search/shelves/search-history-shelf.js b/node_modules/@jet-app/app-store/tmp/src/common/search/shelves/search-history-shelf.js new file mode 100644 index 0000000..c24e8c3 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/search/shelves/search-history-shelf.js @@ -0,0 +1,148 @@ +import { isNothing } from "@jet/environment/types/optional"; +import * as models from "../../../api/models"; +import * as serverData from "../../../foundation/json-parsing/server-data"; +import { Parameters, Path, Protocol } from "../../../foundation/network/url-constants"; +import * as metricsHelpersClicks from "../../metrics/helpers/clicks"; +import * as metricsHelpersImpressions from "../../metrics/helpers/impressions"; +import * as metricsHelpersLocation from "../../metrics/helpers/location"; +import * as searchLandingShelfController from "../landing/search-landing-shelf-controller"; +/** + * The `SearchHistoryShelfToken` type is responsible for plumbing through all + * the data needed to render an incomplete shelf (a shelf that requires a + * lookup) on the focus page. + */ +export class SearchHistoryShelfToken { + constructor(title, maxItems, shelfDisplayStyle, itemDisplayStyle) { + this.title = title; + this.maxItems = maxItems; + this.shelfDisplayStyle = shelfDisplayStyle; + this.itemDisplayStyle = itemDisplayStyle; + } +} +function encodedShelfToken(token) { + return encodeURIComponent(JSON.stringify(token)); +} +export function decodeShelfToken(json) { + if (isNothing(json)) { + return undefined; + } + return JSON.parse(decodeURIComponent(json)); +} +export function createShelfWithContext(objectGraph, pageContext, shelfAttributes) { + const token = new SearchHistoryShelfToken(shelfAttributes.title, shelfAttributes.displayCount, shelfAttributes.displayStyle, shelfAttributes.searchLandingItemDisplayStyle); + return createShelfWithToken(objectGraph, token, pageContext.metricsPageInformation, pageContext.metricsLocationTracker, pageContext.searchHistory); +} +function createShelfItems(objectGraph, metricsPageInformation, metricsLocationTracker, itemDisplayStyle, searchHistory) { + if (serverData.isNullOrEmpty(searchHistory)) { + return []; + } + const items = []; + for (const [historyIndex, historyItem] of searchHistory.entries()) { + const item = createShelfItem(objectGraph, historyItem, historyIndex, metricsPageInformation, metricsLocationTracker, itemDisplayStyle); + if (serverData.isNullOrEmpty(item)) { + continue; + } + items.push(item); + metricsHelpersLocation.nextPosition(metricsLocationTracker); + } + return items; +} +function createShelfURL(token) { + return `${Protocol.internal}:/${Path.searchLandingPage}/${Path.shelf}/?${Parameters.isOnDeviceSearchHistoryShelf}=true&${Parameters.token}=${encodedShelfToken(token)}`; +} +export function createShelfWithToken(objectGraph, token, metricsPageInformation, metricsLocationTracker, searchHistory) { + // Create Items + const items = createShelfItems(objectGraph, metricsPageInformation, metricsLocationTracker, token.itemDisplayStyle, searchHistory); + // Clear History Action + const clearHistoryAction = new models.ClearSearchHistoryAction(); + clearHistoryAction.title = objectGraph.loc.string("Action.ClearSearches"); + metricsHelpersClicks.addClickEventToClearSearchHistoryAction(objectGraph, clearHistoryAction); + // Clear History Sheet Action + const clearHistorySheetAction = new models.SheetAction([clearHistoryAction]); + clearHistorySheetAction.title = objectGraph.loc.string("Sheet.ClearSearches.Title"); + clearHistorySheetAction.message = objectGraph.loc.string("Sheet.ClearSearches.Message"); + clearHistorySheetAction.destructiveActionIndex = 0; + clearHistorySheetAction.isCancelable = true; + // Clear History Compound Action + const clearHistoryShelfAction = new models.CompoundAction([clearHistorySheetAction]); + clearHistoryShelfAction.title = objectGraph.loc.string("Action.Clear"); + // Shelf + const contentType = shelfContentTypeForDisplayStyle(token.shelfDisplayStyle); + const shelf = new models.Shelf(contentType); + shelf.id = "onDeviceSearchHistory"; + shelf.presentationHints = { isWidthConstrained: true }; + // Header + shelf.header = { + title: token.title, + accessoryAction: clearHistoryShelfAction, + }; + // Grid + if (shelf.contentType === "scrollablePill") { + shelf.isHorizontal = true; + shelf.rowsPerColumn = token.shelfDisplayStyle.layoutSize; + } + shelf.contentsMetadata = { + type: "searchFocusTwoColumnList", + numberOfColumns: items.length > 1 ? token.shelfDisplayStyle.layoutSize : 1, + }; + // Content + shelf.items = items; + shelf.isHidden = serverData.isNullOrEmpty(items); + shelf.refreshUrl = createShelfURL(token); + return shelf; +} +function shelfContentTypeForDisplayStyle(displayStyle) { + if (displayStyle.layout === "word_cloud" /* models.GenericSearchPageShelfDisplayStyleLayout.WordCloud */) { + return "scrollablePill"; + } + if (displayStyle.layoutSize === 2) { + // MAINTAINER'S NOTE: Automatically renders as single column in AX text sizes. + return "twoColumnList"; + } + return "singleColumnList"; +} +function createItemTitle(objectGraph, searchTerm, searchEntity) { + if (isNothing(searchEntity)) { + return searchTerm; + } + let formatLocKey; + if (searchEntity === "developer") { + formatLocKey = "Search.ResultsTitle.InDevelopers"; + } + else if (searchEntity === "story") { + formatLocKey = "Search.ResultsTitle.InStories"; + } + else if (searchEntity === "watch") { + formatLocKey = "Search.ResultsTitle.InWatch"; + } + else if (searchEntity === "arcade") { + formatLocKey = "Search.ResultsTitle.InArcade"; + } + if (isNothing(formatLocKey)) { + return searchTerm; + } + return objectGraph.loc.string(formatLocKey).replace("@@search_term@@", searchTerm); +} +function createShelfItem(objectGraph, historyItem, itemIndex, metricsPageInformation, metricsLocationTracker, displayStyle) { + const searchTerm = historyItem.term; + const searchEntity = historyItem.entity; + const searchAction = searchLandingShelfController.createFocusPageSearchAction(objectGraph, createItemTitle(objectGraph, searchTerm, searchEntity), searchTerm, searchEntity, metricsLocationTracker, "recents", undefined, /// MAINTAINER'S NOTE: In the future, we could keep track of the original search source and attribute this recent search to that. + metricsPageInformation, displayStyle); + if (isNothing(searchAction)) { + return null; + } + searchAction.id = historyItem.id; + metricsHelpersImpressions.addImpressionFields(objectGraph, searchAction, { + targetType: "link", + pageInformation: metricsPageInformation, + locationTracker: metricsLocationTracker, + kind: "link", + softwareType: null, + title: historyItem.term, + hintsEntity: historyItem.entity, + id: `${itemIndex}`, + idType: "sequential", + }); + return searchAction; +} +//# sourceMappingURL=search-history-shelf.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/search/sponsored-search-fetching.js b/node_modules/@jet-app/app-store/tmp/src/common/search/sponsored-search-fetching.js new file mode 100644 index 0000000..c9ee59f --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/search/sponsored-search-fetching.js @@ -0,0 +1,122 @@ +/** + Data Fetching for Sponsored Search. + */ +import { ads } from "../../api/typings/constants"; +import { asString, isNullOrEmpty } from "../../foundation/json-parsing/server-data"; +import { attributeAsDictionary } from "../../foundation/media/attributes"; +import { dataCollectionFromDataContainer, } from "../../foundation/media/data-structure"; +import { allProductVariantIdsForData, productVariantDataForData, productVariantIDForVariantData, } from "../product-page/product-page-variants"; +import { adLogger } from "./search-ads"; +import * as content from "../content/content"; +// region exports +/** + * Fetch the set of processed search ads with the raw adverts in the sequential response. + * @param sponsoredSearchRequestData The request data to fetch processed ads for. + * @param searchTerm The search term to fetch processed adverts for. + * @param fetchResponse The promise that will resolve to the response. + * @returns A promise that will provide `SponsoredSearchNativeAdvertData`. Note that even error conditions are represented in the result for instrumentation purposes. + */ +export async function fetchSponsoredSearchNativeAdvertData(objectGraph, sponsoredSearchRequestData, searchTerm, fetchResponse) { + var _a; + if (!sponsoredSearchRequestData.validAdRequest()) { + adLogger(objectGraph, `ODML fetch skipped - Malformed request`); + return { + adverts: [], + odmlSuccess: false, + }; + } + /** + * This is a weird path where we: + * 1. Fetch response from search + * 2. Pass parts of the response back to native to be modified by SearchAds. + */ + const response = await fetchResponse; + const adverts = sponsoredSearchAdvertsFromResponse(objectGraph, response); + const organics = sponsoredSearchOrganicsFromResponse(objectGraph, response, 1); // per POR, only 1 organic for now. + try { + if (!objectGraph.isAvailable(ads)) { + adLogger(objectGraph, `ODML fetch skipped - Unsupported client`); + return { + adverts: adverts, + odmlSuccess: false, + }; + } + else { + const processedAdverts = await objectGraph.ads.processAdvertsForSponsoredSearch(adverts, organics, searchTerm, objectGraph.bag.sponsoredSearchODMLTimeout, objectGraph.client.isPhone || objectGraph.client.isPad); + if (!processedAdverts.odmlSuccess) { + adLogger(objectGraph, `ODML processing failed`); + } + else { + adLogger(objectGraph, `ODML processing completed`); + } + return { + adverts: (_a = processedAdverts.adverts) !== null && _a !== void 0 ? _a : adverts, + odmlSuccess: processedAdverts.odmlSuccess, + installedStates: processedAdverts.installedStates, + appliedPolicy: processedAdverts.appliedPolicy, + appStates: processedAdverts.appStates, + }; + } + } + catch (e) { + adLogger(objectGraph, `ODML fetch failed - ${e}`); + return { + adverts: adverts, + odmlSuccess: false, + }; + } +} +// endregion +// region internals +/** + * Build the search advert models from the response. + */ +function sponsoredSearchAdvertsFromResponse(objectGraph, response) { + const adverts = dataCollectionFromDataContainer(response.results["ads-result"]); + const bridgedAdverts = []; + for (const ad of adverts) { + const id = asString(ad, "id"); + const adData = attributeAsDictionary(ad, "iads"); + if (isNullOrEmpty(id) || isNullOrEmpty(adData)) { + continue; + } + let productVariantId = null; + let allProductVariantIds = null; + if (objectGraph.bag.enableCPPInSearchAds) { + const productVariantData = productVariantDataForData(objectGraph, ad); + productVariantId = productVariantIDForVariantData(productVariantData); + allProductVariantIds = allProductVariantIdsForData(objectGraph, ad); + } + bridgedAdverts.push({ + instanceId: objectGraph.random.nextUUID(), + adamId: id, + assetInformation: {}, + adData: adData, + cppIds: allProductVariantIds, + serverCppId: productVariantId, + selectedCppId: productVariantId, + appBinaryTraits: content.appBinaryTraitsFromData(objectGraph, ad), + }); + } + return bridgedAdverts; +} +/** + * Build the search organic models from the response, up to `limit`. + */ +function sponsoredSearchOrganicsFromResponse(objectGraph, response, limit) { + const organics = dataCollectionFromDataContainer(response.results.search); + const bridgedOrganics = []; + for (const result of organics) { + const id = asString(result, "id"); + if (isNullOrEmpty(id)) { + continue; + } + bridgedOrganics.push({ + adamId: id, + assetInformation: {}, + }); + } + return bridgedOrganics; +} +// endregion +//# sourceMappingURL=sponsored-search-fetching.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/search/web-search-action.js b/node_modules/@jet-app/app-store/tmp/src/common/search/web-search-action.js new file mode 100644 index 0000000..28c0491 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/search/web-search-action.js @@ -0,0 +1,23 @@ +import { FlowAction } from "../../api/models"; +import { makeSearchResultsPageIntent, } from "../../api/intents/search-results-page-intent"; +import { getLocale } from "../locale"; +import { makeCanonicalSearchResultsPageUrl } from "./search-page-url"; +/** + * Creates a `FlowAction` destined for the search results page + * + * This will be used by the "web" client to perform search from the search landing page, + * or to change the platform on search results page + */ +export function makeWebSearchAction(objectGraph, platform, term = "") { + const searchAction = new FlowAction("search"); + const destination = makeSearchResultsPageIntent({ + ...getLocale(objectGraph), + platform, + term, + origin: "externalUrl", + }); + searchAction.destination = destination; + searchAction.pageUrl = makeCanonicalSearchResultsPageUrl(objectGraph, destination); + return searchAction; +} +//# sourceMappingURL=web-search-action.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/sharing.js b/node_modules/@jet-app/app-store/tmp/src/common/sharing.js new file mode 100644 index 0000000..65c73b5 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/sharing.js @@ -0,0 +1,428 @@ +// +// sharing.ts +// AppStoreKit +// +// Created by Sam Vafaee on 6/16/17. +// Copyright (c) 2016 Apple Inc. All rights reserved. +// +import { isNothing, isSome } from "@jet/environment"; +import * as validation from "@jet/environment/json/validation"; +import * as models from "../api/models"; +import * as serverData from "../foundation/json-parsing/server-data"; +import * as mediaAttributes from "../foundation/media/attributes"; +import { Parameters, ShareURLParameters } from "../foundation/network/url-constants"; +import * as urls from "../foundation/network/urls"; +import * as dateUtil from "../foundation/util/date-util"; +import * as client from "../foundation/wrappers/client"; +import * as contentArtwork from "./content/artwork/artwork"; +import * as contentAttributes from "./content/attributes"; +import * as content from "./content/content"; +import * as metricsHelpersClicks from "./metrics/helpers/clicks"; +import * as offers from "./offers/offers"; +import { productVariantDataForData, productVariantIDForVariantData } from "./product-page/product-page-variants"; +import { cardDisplayStyleFromData, editorialArtKeyPathForCardDisplayStyle, todayCardArtworkFromArtworkData, todayCardHeroArtForData, } from "./today/today-card-util"; +import { HeroMediaDisplayContext } from "./today/today-types"; +import { dataHasDeviceFamily, dataOnlyHasDeviceFamily } from "./content/device-family"; +// region Data helpers +export function adamIdForShareSheetGiftActivityFromProductData(objectGraph, data) { + if (!objectGraph.bag.isContentGiftingEnabled) { + return null; + } + if (serverData.isNull(data)) { + return null; + } + const offer = offers.offerDataFromData(objectGraph, data); + if (serverData.isNull(offer)) { + return null; + } + // Disable gifting for pre-orders + const isPreorder = mediaAttributes.attributeAsBoolean(data, "isPreorder"); + if (isPreorder) { + return null; + } + const price = serverData.asNumber(offer, "price"); + if (price > 0) { + return data.id; + } + return null; +} +/** + * Return the url for sharing for given product + * @param objectGraph Dependency soup + * @param data Apps Resource data for product + */ +function shareUrlForProductData(objectGraph, data, urlKey) { + const rawUrl = mediaAttributes.attributeAsString(data, urlKey); + if (serverData.isNullOrEmpty(rawUrl)) { + return null; + } + const url = new urls.URL(rawUrl); + // Add client specifier to url so apps are shown in the same client they were shared from + let clientSpecifier = null; + const clientIdentifier = objectGraph.host.clientIdentifier; + switch (clientIdentifier) { + case client.messagesIdentifier: + clientSpecifier = "messages"; + break; + case client.watchIdentifier: + clientSpecifier = "watch"; + break; + default: + break; + } + if (clientSpecifier) { + url.param(ShareURLParameters.clientSpecifier, clientSpecifier); + } + // Custom product page variant id must be added to `url` for MAPI resource caching constraints. + const productVariantData = productVariantDataForData(objectGraph, data); + const productVariantID = productVariantIDForVariantData(productVariantData); + if (serverData.isDefinedNonNull(productVariantID)) { + url.param(Parameters.productVariantID, productVariantID); + } + return url.toString(); +} +function notesMetadataFromProductData(objectGraph, data) { + return validation.context("notesMetadataFromProductData", () => { + var _a; + if (serverData.isNull(data)) { + return null; + } + const itemName = mediaAttributes.attributeAsString(data, "name"); + // Require name + if (isNothing(itemName) || itemName.length === 0) { + return null; + } + const url = shareUrlForProductData(objectGraph, data, "url"); + const developer = mediaAttributes.attributeAsString(data, "artistName"); + const category = mediaAttributes.attributeAsString(data, "genreNames.0"); + const fileSize = (_a = content.combinedFileSizeFromData(objectGraph, data)) === null || _a === void 0 ? void 0 : _a.fileSizeByDevice; + let mediaType; + switch (data.type) { + case "apps": { + mediaType = "app"; + break; + } + case "app-bundles": { + mediaType = "bundle"; + break; + } + case "in-apps": { + mediaType = "iap"; + break; + } + default: { + mediaType = null; + } + } + return new models.ShareSheetNotesMetadata(itemName, url, developer, category, fileSize, mediaType); + }); +} +function copyLinkShareSheetActivityForURL(objectGraph, url, title) { + if (serverData.isNullOrEmpty(url)) { + return null; + } + const copyTextAction = new models.CopyTextAction(url); + copyTextAction.title = title !== null && title !== void 0 ? title : objectGraph.loc.string("ShareSheet.CopyLink.Title"); + copyTextAction.artwork = contentArtwork.createArtworkForResource(objectGraph, "systemimage://link"); + const copyLinkActionActivity = new models.ShareSheetActivity("com.apple.AppStore.copyLinkActivity", copyTextAction); + return copyLinkActionActivity; +} +function openInGameCenterShareSheetActivityForURL(url, title) { + if (serverData.isNullOrEmpty(url)) { + return undefined; + } + const openURLAction = new models.ExternalUrlAction(url); + openURLAction.title = title; + const activity = new models.ShareSheetActivity("com.apple.AppStore.openInGameCenterActivity", openURLAction); + return activity; +} +// endregion +// region Article +export function shareSheetDataForArticle(objectGraph, text, url, shortUrl, articleArtwork, data) { + // Sharing is currently not supported on watchOS. + if (serverData.isNull(data) || objectGraph.client.isWatch) { + return null; + } + return validation.context("shareSheetDataForArticle", () => { + let artwork = articleArtwork; + if (isNothing(artwork) && isSome(data)) { + artwork = shareSheetArtForData(objectGraph, data); + } + const subtitle = objectGraph.loc.string("ShareSheet.Story.Subtitle"); + const articleMetadata = new models.ShareSheetArticleMetadata(data.id, text, subtitle, artwork); + return new models.ShareSheetData(articleMetadata, url, shortUrl); + }); +} +/** + * Determines the artwork to use in share sheets. + * @param {Data} data The data from which to retrieve the artwork. + * @returns {Artwork} The artwork suitable for display in the share sheet. + */ +export function shareSheetArtForData(objectGraph, data) { + const cardDisplayStyle = cardDisplayStyleFromData(data); + if (!objectGraph.client.isVision && !preprocessor.GAMES_TARGET) { + const heroArt = todayCardHeroArtForData(objectGraph, data, HeroMediaDisplayContext.Article, cardDisplayStyle); + if (serverData.isDefinedNonNull(heroArt)) { + return heroArt; + } + } + let cropCode; + if (objectGraph.client.isVision) { + cropCode = "SCS.ApDPCS01"; + } + const artKeyPath = editorialArtKeyPathForCardDisplayStyle(objectGraph, cardDisplayStyle); + return todayCardArtworkFromArtworkData(objectGraph, mediaAttributes.attributeAsDictionary(data, artKeyPath), cropCode); +} +export function shareSheetActivitiesForArticle(objectGraph, shareUrl, todayCardPreviewUrl, storyID) { + const shareSheetActivities = []; + if ((shareUrl === null || shareUrl === void 0 ? void 0 : shareUrl.length) > 0) { + const copyLinkActivity = copyLinkShareSheetActivityForURL(objectGraph, shareUrl); + if (serverData.isDefinedNonNull(copyLinkActivity)) { + shareSheetActivities.push(copyLinkActivity); + } + } + if ((todayCardPreviewUrl === null || todayCardPreviewUrl === void 0 ? void 0 : todayCardPreviewUrl.length) > 0) { + const copyCardPreviewLinkActivity = copyLinkShareSheetActivityForURL(objectGraph, todayCardPreviewUrl, "Copy Card Preview Link"); + if (serverData.isDefinedNonNull(copyCardPreviewLinkActivity)) { + shareSheetActivities.push(copyCardPreviewLinkActivity); + } + } + if (isSome(storyID)) { + let activity; + if (objectGraph.featureFlags.isGSEUIEnabled("de7bbd8e") && + objectGraph.featureFlags.isEnabled("open_in_story_share_sheet")) { + activity = openInGameCenterShareSheetActivityForURL(`games:///story/id${storyID}`, objectGraph.loc.string("ShareSheet.OpenInGameCenter.Title")); + } + if (serverData.isDefinedNonNull(activity)) { + shareSheetActivities.push(activity); + } + } + return shareSheetActivities; +} +// endregion +// region App Events +export function shareSheetDataForAppEvent(objectGraph, text, subtitle, url, shortUrl, appEventArtwork) { + return validation.context("shareSheetDataForAppEvent", () => { + const artwork = appEventArtwork; + const appEventMetadata = new models.ShareSheetAppEventMetadata(text, subtitle, artwork); + return new models.ShareSheetData(appEventMetadata, url, shortUrl); + }); +} +export function shareSheetActivitiesForAppEvent(objectGraph, appEvent, shareUrl) { + var _a; + const shareSheetActivities = []; + // Copy link action + if ((shareUrl === null || shareUrl === void 0 ? void 0 : shareUrl.length) > 0) { + const copyLinkActivity = copyLinkShareSheetActivityForURL(objectGraph, shareUrl); + if (serverData.isDefinedNonNull) { + shareSheetActivities.push(copyLinkActivity); + } + } + // If the event has already started, remove the option to create a calendar event + if (appEvent.startDate.getTime() <= Date.now()) { + return shareSheetActivities; + } + // Creating events is not supported in the product page extension + if (objectGraph.host.clientIdentifier === client.productPageExtensionIdentifier) { + return shareSheetActivities; + } + // Not authorized action + const notAuthorizedAction = new models.AlertAction("default"); + notAuthorizedAction.title = objectGraph.loc.string("APP_EVENTS_CALENDAR_NOT_AUTHORIZED_TITLE"); + notAuthorizedAction.message = objectGraph.loc.string("APP_EVENTS_CALENDAR_NOT_AUTHORIZED_DETAIL"); + notAuthorizedAction.isCancelable = true; + notAuthorizedAction.buttonTitles = [objectGraph.loc.string("ACTION_SETTINGS")]; + // NOTE: This URL only works on iOS. If this feature is expanded beyond iOS, this code will need to be split per-platform. + notAuthorizedAction.buttonActions = [new models.ExternalUrlAction("prefs:root=Privacy&path=CALENDARS", true)]; + let isAllDay = false; + if (serverData.isDefinedNonNull(appEvent.endDate)) { + // If the start and end date are > 6 hours apart, and spans over multiple days, then mark as all-day + const startMidnight = dateUtil.convertLocalDateToLocalMidnight(appEvent.startDate); + const endMidnight = dateUtil.convertLocalDateToLocalMidnight(appEvent.endDate); + const difference = appEvent.endDate.getTime() - appEvent.startDate.getTime(); + const sixHoursDifference = 1000 * 60 * 60 * 6; + if (endMidnight.getTime() > startMidnight.getTime() && difference > sixHoursDifference) { + isAllDay = true; + } + // If the start and end date are on the same day, and the event runs for 23 hours & 59 mins + // then mark as all-day. This effectively ignores the seconds portion as calendar ignores + // this anyway. + const fullDayDifference = 1000 * 60 * 60 * 23 + 1000 * 60 * 59; + if (startMidnight.getTime() === endMidnight.getTime() && difference >= fullDayDifference) { + isAllDay = true; + } + } + // Create calendar event activity + const createCalendarEventAction = new models.CreateCalendarEventAction(appEvent.startDate, appEvent.endDate, isAllDay, appEvent.title, (_a = appEvent.lockup) === null || _a === void 0 ? void 0 : _a.title, appEvent.detail, shareUrl, notAuthorizedAction, "free"); + createCalendarEventAction.title = objectGraph.loc.string("SHARE_SHEET_ADD_TO_CALENDAR"); + createCalendarEventAction.artwork = contentArtwork.createArtworkForResource(objectGraph, "systemimage://calendar.circle"); + const createCalendarEventActivity = new models.ShareSheetActivity("com.apple.AppStore.createCalendarEventActivity", createCalendarEventAction); + // Create Calendar Event should be at the beginning. + shareSheetActivities.unshift(createCalendarEventActivity); + return shareSheetActivities; +} +// endregion +// region Products +export function shareSheetDataForProductFromProductData(objectGraph, data, clientIdentifierOverride) { + return validation.context("shareSheetDataForProductFromProductData", () => { + // Sharing is currently not supported on watchOS or the web client. + if (serverData.isNull(data) || objectGraph.client.isWatch || objectGraph.client.isWeb) { + return null; + } + // required attributes + const url = shareUrlForProductData(objectGraph, data, "url"); + const title = mediaAttributes.attributeAsString(data, "name"); + const developerName = mediaAttributes.attributeAsString(data, "artistName"); + const adamId = data.id; + const storeFrontIdentifier = objectGraph.client.storefrontIdentifier; + // Sanity check + if (!url || !title || !developerName || !adamId) { + return null; + } + // optional attributes + const shortUrl = shareUrlForProductData(objectGraph, data, "shortUrl"); + let artwork = null; + let notesMetadata = null; + const screenshots = content.screenshotsFromData(objectGraph, data, 4 /* content.ArtworkUseCase.LockupScreenshots */); + const videos = content.videoPreviewsFromData(objectGraph, data); + const subtitle = contentAttributes.contentAttributeAsString(objectGraph, data, "subtitle") || developerName; + const genreName = null; + const isMessagesOnlyApp = false; + const messagesAppIcon = null; + // Platform + let platform; + const isMacOnlyApp = dataOnlyHasDeviceFamily(objectGraph, data, "mac"); + const isMacApp = dataHasDeviceFamily(objectGraph, data, "mac"); + if (isMacOnlyApp || (objectGraph.client.isMac && isMacApp)) { + platform = "Mac"; + } + else { + platform = "iOS"; + } + // Add product info + if (serverData.isDefinedNonNull(data) && mediaAttributes.attributeAsString(data, "url")) { + artwork = content.iconFromData(objectGraph, data, { + useCase: 1 /* content.ArtworkUseCase.LockupIconSmall */, + }, clientIdentifierOverride); + notesMetadata = notesMetadataFromProductData(objectGraph, data); + } + const productMetadata = new models.ShareSheetProductMetadata(adamId, storeFrontIdentifier, title, platform, artwork, screenshots, videos, isMessagesOnlyApp, subtitle, genreName, messagesAppIcon, notesMetadata); + return new models.ShareSheetData(productMetadata, url, shortUrl); + }); +} +export function shareProductActionFromData(objectGraph, data, metricsPageInformation, metricsLocationTracker, clientIdentifierOverride) { + return validation.context(`shareActionFromData: ${data.type}`, () => { + var _a; + const id = data.id; + switch (objectGraph.client.deviceType) { + case "mac": { + const shareSheetData = shareSheetDataForProductFromProductData(objectGraph, data); + // Share action + if (shareSheetData) { + const shareAction = new models.ShareSheetAction(shareSheetData, []); + metricsHelpersClicks.addClickEventToAction(objectGraph, shareAction, { + targetType: "button", + id: id, + actionType: "share", + pageInformation: metricsPageInformation, + locationTracker: metricsLocationTracker, + }); + return shareAction; + } + break; + } + case "phone": + case "pad": + case "vision": { + const shareSheetData = shareSheetDataForProductFromProductData(objectGraph, data, clientIdentifierOverride); + const shareSheetActivities = []; + // Copy link action + if (((_a = shareSheetData === null || shareSheetData === void 0 ? void 0 : shareSheetData.url) === null || _a === void 0 ? void 0 : _a.length) > 0) { + const copyLinkActivity = copyLinkShareSheetActivityForURL(objectGraph, shareSheetData.url); + if (serverData.isDefinedNonNull) { + shareSheetActivities.push(copyLinkActivity); + } + } + // Gift action + const giftAdamId = adamIdForShareSheetGiftActivityFromProductData(objectGraph, data); + if (giftAdamId) { + const giftAction = new models.FlowAction("finance"); + giftAction.presentationContext = "presentModal"; + giftAction.title = objectGraph.loc.string("SHARE_GIFT_APP"); + giftAction.artwork = contentArtwork.createArtworkForResource(objectGraph, "systemimage://app.gift"); + giftAction.pageUrl = `gift/${giftAdamId}`; + metricsHelpersClicks.addClickEventToAction(objectGraph, giftAction, { + targetType: "button", + id: id, + actionType: "gift", + actionContext: "shareSheet", + pageInformation: metricsPageInformation, + locationTracker: metricsLocationTracker, + }); + const giftActionActivity = new models.ShareSheetActivity("com.apple.AppStore.giftActivity", giftAction); + shareSheetActivities.push(giftActionActivity); + } + if (shareSheetData) { + // Share action + const shareSheetStyle = "expanded"; + const shareAction = new models.ShareSheetAction(shareSheetData, shareSheetActivities, shareSheetStyle); + shareAction.title = objectGraph.loc.string("SHARE_APP"); + shareAction.artwork = contentArtwork.createArtworkForResource(objectGraph, "systemimage://square.and.arrow.up"); + metricsHelpersClicks.addClickEventToAction(objectGraph, shareAction, { + targetType: "button", + id: id, + actionType: "share", + actionContext: "shareSheet", + pageInformation: metricsPageInformation, + locationTracker: metricsLocationTracker, + }); + return shareAction; + } + else if (shareSheetActivities.length > 0) { + // Map ActionActivity[] to Action[] as we're not dealing with a share sheet + const sheetActions = shareSheetActivities.map((activity) => activity.action); + // Create action sheet instead of share sheet + const sheetAction = new models.SheetAction(sheetActions); + sheetAction.isCancelable = true; + sheetAction.isCustom = true; + metricsHelpersClicks.addClickEventToAction(objectGraph, sheetAction, { + targetType: "button", + id: id, + actionType: "actionSheet", + pageInformation: metricsPageInformation, + locationTracker: metricsLocationTracker, + }); + return sheetAction; + } + break; + } + default: { + // These device types do not support sharing, so we don't create any share actions. + break; + } + } + return null; + }); +} +// region Generic Pages +export function shareSheetDataForGenericPage(objectGraph, text, url, subtitle, shortUrl, artwork) { + return validation.context("shareSheetDataForGenericPage", () => { + if (serverData.isNullOrEmpty(url)) { + return null; + } + const metadata = new models.ShareSheetGenericMetadata(text, subtitle, artwork !== null && artwork !== void 0 ? artwork : undefined); + return new models.ShareSheetData(metadata, url, shortUrl); + }); +} +export function shareSheetActivitiesForGenericPage(objectGraph, shareUrl) { + const shareSheetActivities = []; + if ((shareUrl === null || shareUrl === void 0 ? void 0 : shareUrl.length) > 0) { + const copyLinkActivity = copyLinkShareSheetActivityForURL(objectGraph, shareUrl); + if (serverData.isDefinedNonNull) { + shareSheetActivities.push(copyLinkActivity); + } + } + return shareSheetActivities; +} +// endregion +//# sourceMappingURL=sharing.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/today/article-request.js b/node_modules/@jet-app/app-store/tmp/src/common/today/article-request.js new file mode 100644 index 0000000..4371605 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/today/article-request.js @@ -0,0 +1,86 @@ +import { defaultAdditionalPlatformsForClient, Request } from "../../foundation/media/data-fetching"; +import { shouldFetchCustomAttributes } from "../product-page/product-page-variants"; +import { appEventsAreEnabled, appOfferItemsAreEnabled } from "../app-promotions/app-promotions-common"; +import { shouldUsePrerenderedIconArtwork } from "../content/content"; +import { AppEventsAttributes } from "../../gameservicesui/src/foundation/media-api/requests/recommendation-request-types"; +function buildAttributesForArticlePageRequest(objectGraph) { + const attributes = [ + "screenshotsByType", + "videoPreviewsByType", + "requiredCapabilities", + "minimumOSVersion", + "editorialArtwork", + "editorialVideo", + "editorialClientParams", + "shortEditorialNotes", + "enrichedEditorialNotes", + ]; + if (objectGraph.bag.enableUpdatedAgeRatings) { + attributes.push("ageRating"); + } + if (objectGraph.appleSilicon.isSupportEnabled) { + attributes.push("macRequiredCapabilities"); + } + if (objectGraph.client.isMac) { + attributes.push("hasMacIPAPackage"); + } + if (objectGraph.client.isVision) { + attributes.push("compatibilityControllerRequirement"); + } + if (shouldUsePrerenderedIconArtwork(objectGraph)) { + attributes.push("iconArtwork"); + } + return attributes; +} +/** + * Create a Media API request for an `editorial-item` + * + * This corresponsd to an "Article" or "Story" page + */ +export function buildArticlePageRequest(objectGraph, intent, isIncomingURL) { + const request = new Request(objectGraph) + .withIdOfType(intent.id, "editorial-items") + .includingAdditionalPlatforms(defaultAdditionalPlatformsForClient(objectGraph)) + .includingAttributes(buildAttributesForArticlePageRequest(objectGraph)) + .includingRelationships(["canvas"]) + .includingRelationshipsForUpsell(true) + .includingMacOSCompatibleIOSAppsWhenSupported(true) + .usingCustomAttributes(shouldFetchCustomAttributes(objectGraph)); + if (!isIncomingURL) { + request.includingAgeRestrictions(); + } + if (appEventsAreEnabled(objectGraph)) { + request.enablingFeature("appEvents"); + request.includingScopedAttributes("app-events", AppEventsAttributes); + request.includingScopedRelationships("app-events", ["app"]); + request.includingScopedRelationships("editorial-item-shelves", ["app-events"]); + request.includingScopedAvailableIn("app-events", ["past"]); + } + if (appOfferItemsAreEnabled(objectGraph)) { + request.enablingFeature("offerItems"); + request.includingScopedRelationships("offer-items", ["salables"]); + request.includingAssociateKeys("editorial-items", ["editorial-cards"]); + request.includingMetaKeys("offer-items:salables", ["discountOffer"]); + request.includingScopedAttributes("offer-items", [ + "title", + "subtitle", + "additionalTerms", + "redemptionExpirationDate", + ]); + } + if (objectGraph.client.isVision) { + request.enablingFeature("supportsCustomTextColor"); + request.includingScopedAttributes("editorial-items", ["enrichedEditorialNotes"]); + } + if (objectGraph.client.isWeb) { + request.includingAttributes([ + // Publication date is used as part of SEO meta-data + "lastPublishedDate", + ]); + } + if (preprocessor.GAMES_TARGET) { + request.includingScopedAttributes("apps", ["isEligibleForGamesApp"]); + } + return request; +} +//# sourceMappingURL=article-request.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/today/article.js b/node_modules/@jet-app/app-store/tmp/src/common/today/article.js new file mode 100644 index 0000000..a67fde4 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/today/article.js @@ -0,0 +1,1572 @@ +/** + * Created by keithpk on 3/21/17. + */ +import { isNothing, isSome } from "@jet/environment"; +import * as validation from "@jet/environment/json/validation"; +import * as models from "../../api/models"; +import * as serverData from "../../foundation/json-parsing/server-data"; +import * as mediaAttributes from "../../foundation/media/attributes"; +import * as mediaAugment from "../../foundation/media/augment"; +import * as mediaDataStructure from "../../foundation/media/data-structure"; +import * as mediaNetwork from "../../foundation/media/network"; +import * as mediaRelationships from "../../foundation/media/relationships"; +import * as urls from "../../foundation/network/urls"; +import * as color from "../../foundation/util/color-util"; +import { PageID } from "../../gameservicesui/src/common/id-builder"; +import * as gamesComponentBuilder from "../../gameservicesui/src/editorial-page/editorial-component-builder"; +import * as appPromotionsShelf from "../app-promotions/app-promotions-shelf"; +import * as arcadeCommon from "../arcade/arcade-common"; +import * as arcadeUpsell from "../arcade/arcade-upsell"; +import * as breakoutsCommon from "../arcade/breakouts-common"; +import * as videoDefaults from "../constants/video-constants"; +import * as artworkBuilder from "../content/artwork/artwork"; +import * as content from "../content/content"; +import { EditorialMediaPlacement } from "../editorial-pages/editorial-media-util"; +import { buildSmallStoryCardShelf } from "../editorial-pages/editorial-page-shelf-builder/editorial-page-collection-shelf-builder/editorial-page-story-card-collection-shelf-builder"; +import { buildStoryCard } from "../editorial-pages/editorial-page-shelf-builder/editorial-page-collection-shelf-builder/editorial-page-story-card-utils"; +import { createBaseShelfToken } from "../editorial-pages/editorial-page-shelf-token"; +import { CollectionShelfDisplayStyle } from "../editorial-pages/editorial-page-types"; +import * as externalDeepLink from "../linking/external-deep-link"; +import * as links from "../linking/os-update-links"; +import * as lockups from "../lockups/lockups"; +import * as metricsHelpersClicks from "../metrics/helpers/clicks"; +import * as metricsHelpersImpressions from "../metrics/helpers/impressions"; +import * as metricsHelpersLocation from "../metrics/helpers/location"; +import * as metricsHelpersMedia from "../metrics/helpers/media"; +import * as metricsHelpersPage from "../metrics/helpers/page"; +import * as metricsHelpersUtil from "../metrics/helpers/util"; +import * as sharing from "../sharing"; +import { crossLinkSubtitleFromData, defaultTodayCardConfiguration, fallbackWatchTodayCardFromData, todayCardFromData, } from "./today-card-util"; +import * as todayHorizontalCardUtil from "./today-horizontal-card-util"; +import { todayCardPreviewUrlForTodayCard } from "./today-parse-util"; +import { HeroMediaDisplayContext, TodayCardDisplayStyle, TodayParseContext, } from "./today-types"; +export const iAPBackgroundColor = color.named("componentBackgroundStandout"); +const appShowcaseBackgroundColor = color.named("componentBackgroundStandout"); +const arcadeShowcaseShelfBackgroundColor = color.named("componentBackgroundStandout"); +/** + * Resolves the article module's app media platform to an `AppPlatform` to use for screenshots. + * @param {AppMediaPlatform} appMediaPlatform The server-dictated media platform to use for the module. + * @returns {AppPlatform} The app platform that is appropriate for this media platform, taking into account our device. + */ +function appPlatformFromAppMediaPlatform(objectGraph, appMediaPlatform) { + switch (appMediaPlatform) { + case "Watch": + return "watch"; + case "iOS": + if (objectGraph.client.isPad) { + return "pad"; + } + else { + return "phone"; + } + case "tvOS": + return "tv"; + case "Messages": + return "messages"; + case "visionOS": + return "vision"; + default: + return null; + } +} +export class ArticleParseContext { + constructor() { + // The index of the current module + this.index = 0; + // The reco metrics from the shelf on the today page + this.todayShelfRecoMetricsData = {}; + /// Whether there are any focusable elements (for touch mode) + this.hasFocusableElements = false; + /// Whether there are any non-focusable elements (for touch mode) + this.hasNonFocusableElements = false; + /// Whether there is a resilient deep link. + this.isResilientDeepLink = false; + /// Whether or not to allow app event previews, used by editorial to preview app event stories before they are published + this.allowUnpublishedAppEventPreviews = false; + } +} +function todayCardConfigFromArticleContext(objectGraph, articleContext) { + if (!serverData.isDefinedNonNull(articleContext)) { + return null; + } + if (isSome(articleContext.todayCardConfig)) { + return articleContext.todayCardConfig; + } + const config = defaultTodayCardConfiguration(objectGraph); + config.enableListCardToMultiAppFallback = false; + config.clientIdentifierOverride = articleContext.clientIdentifierOverride; + config.useOTDTextStyle = false; + config.allowUnpublishedAppEventPreviews = articleContext.allowUnpublishedAppEventPreviews; + config.currentRowIndex = undefined; + switch (objectGraph.client.deviceType) { + case "mac": + config.prevailingCropCodes = { defaultCrop: "en" }; + config.coercedCollectionTodayCardDisplayStyle = TodayCardDisplayStyle.Grid; + config.heroDisplayContext = HeroMediaDisplayContext.Article; + break; + case "tv": + config.prevailingCropCodes = { + "defaultCrop": "ek", + "editorialArtwork.storyCenteredStatic16x9": "SCS.ApDHXL01", + }; + config.coercedCollectionTodayCardDisplayStyle = TodayCardDisplayStyle.Grid; + config.heroDisplayContext = HeroMediaDisplayContext.Article; + break; + case "web": + config.coercedCollectionTodayCardDisplayStyle = TodayCardDisplayStyle.River; + config.prevailingCropCodes = { + "defaultCrop": "sr", + "editorialArtwork.dayCard": "grav.west", + }; + break; + default: + break; + } + return config; +} +export function articlePageFromResponse(objectGraph, articleResponse, context) { + return validation.context("articlePageWithResponse", () => { + var _a; + const articleData = mediaDataStructure.dataFromDataContainer(objectGraph, articleResponse); + context.metricsPageInformation = metricsHelpersPage.metricsPageInformationFromMediaApiResponse(objectGraph, "editorialItem", articleData.id, articleResponse); + context.metricsLocationTracker = metricsHelpersLocation.newLocationTracker(); + context.pageId = articleData.id; + // Bridge over article contexts to today's metrics context and card config + const todayParseContext = new TodayParseContext(context.metricsPageInformation, context.metricsLocationTracker, context.refreshController); + const todayCardConfig = todayCardConfigFromArticleContext(objectGraph, context); + // Render the top card + let todayCard = todayCardFromData(objectGraph, articleData, todayCardConfig, todayParseContext); + let editorialStoryCard = null; + const todayCardMedia = todayCard === null || todayCard === void 0 ? void 0 : todayCard.media; + if (objectGraph.client.isVision || preprocessor.GAMES_TARGET) { + editorialStoryCard = buildStoryCard(objectGraph, articleData, EditorialMediaPlacement.StoryDetail, { + pageInformation: context.metricsPageInformation, + locationTracker: context.metricsLocationTracker, + }, CollectionShelfDisplayStyle.StoryMedium, false); + todayCard = null; + } + if (isNothing(todayCard)) { + todayCard = fallbackWatchTodayCardFromData(objectGraph, articleData, todayCardConfig, todayParseContext); + } + // Get the title for metrics purposes. + const title = (_a = todayCard === null || todayCard === void 0 ? void 0 : todayCard.title) !== null && _a !== void 0 ? _a : editorialStoryCard === null || editorialStoryCard === void 0 ? void 0 : editorialStoryCard.title; + const editorialItemKind = mediaAttributes.attributeAsString(articleData, "kind"); + // Configure subtitle for cross link + context.crossLinkSubtitle = crossLinkSubtitleFromData(objectGraph, articleData); + // Bridge today config back into articles, now that cards are created. + // Now we've created the card, reference the clientIdentifierOverride it used for the rest of the article. + context.clientIdentifierOverride = todayCardConfig.clientIdentifierOverride; + // Start a metrics location + metricsHelpersLocation.pushContentLocation(objectGraph, { + pageInformation: context.metricsPageInformation, + locationTracker: context.metricsLocationTracker, + targetType: "article", + id: context.pageId, + idType: "its_id", + }, title); + // Render the article itself + const shelves = renderArticle(objectGraph, articleData, todayCardMedia, context); + const lastShelf = shelves[shelves.length - 1]; + // Sharing + const shareAction = objectGraph.client.isTV || + objectGraph.client.isWeb || + context.isResilientDeepLink || + preprocessor.GAMES_TARGET || + editorialItemKind === "OfferItem" + ? null + : shareSheetActionFromData(objectGraph, articleData, todayCardConfig); + if (serverData.isDefinedNonNull(shareAction)) { + // Add click event + metricsHelpersClicks.addClickEventToAction(objectGraph, shareAction, { + targetType: "button", + id: context.pageId, + actionType: "share", + pageInformation: context.metricsPageInformation, + locationTracker: context.metricsLocationTracker, + }); + const isLastModuleFullWidth = isArticleShelfFullWidth(objectGraph, lastShelf, context.module); + const shareButtonShelf = createShareShelf(objectGraph, shareAction, context, isLastModuleFullWidth); + if (shareButtonShelf) { + shelves.push(shareButtonShelf); + } + } + const page = new models.ArticlePage(todayCard, shelves, shareAction); + page.editorialStoryCard = editorialStoryCard; + page.title = todayCard === null || todayCard === void 0 ? void 0 : todayCard.title; + page.subtitle = todayCard === null || todayCard === void 0 ? void 0 : todayCard.inlineDescription; + addFooterLockupForPageIfNeeded(objectGraph, page, articleData, context); + if (objectGraph.client.isTV) { + if (context.hasFocusableElements && !context.hasNonFocusableElements) { + page.touchMode = "focus"; + } + else if (!context.hasFocusableElements && context.hasNonFocusableElements) { + page.touchMode = "pan"; + } + else { + page.touchMode = "auto"; + } + } + // Map whether the article should terminate on close. + page.shouldTerminateOnClose = context.isResilientDeepLink; + metricsHelpersPage.addMetricsEventsToPageWithInformation(objectGraph, page, context.metricsPageInformation, (fields) => { + let additionalValue = title; + if ((todayCard === null || todayCard === void 0 ? void 0 : todayCard.media) instanceof models.TodayCardMediaBrandedSingleApp && + (todayCard === null || todayCard === void 0 ? void 0 : todayCard.overlay) instanceof models.TodayCardLockupOverlay) { + const lockupOverlay = todayCard === null || todayCard === void 0 ? void 0 : todayCard.overlay; + additionalValue = lockupOverlay.lockup.title; + } + if (!additionalValue) { + return; + } + let pageDetails = serverData.asString(serverData.asJSONValue(fields["pageDetails"]), "coercible"); + pageDetails = pageDetails || serverData.asString(serverData.asJSONValue(fields["pageId"])); + if (pageDetails) { + fields["pageDetails"] = `${pageDetails}_${additionalValue}`; + } + else { + fields["pageDetails"] = `unknown_${additionalValue}`; + } + }); + page.canonicalURL = mediaAttributes.attributeAsString(articleData, "url"); + if (isSome(articleData)) { + const articleUrl = mediaAttributes.attributeAsString(articleData, "url"); + if (isSome(articleUrl)) { + page.viewArticleAction = new models.ExternalUrlAction(articleUrl, true); + } + } + return page; + }); +} +function renderArticle(objectGraph, articleData, cardMedia, context) { + return validation.context("renderArticle", () => { + var _a; + const shelves = []; + const canvas = (_a = mediaRelationships.relationshipCollection(articleData, "canvas")) !== null && _a !== void 0 ? _a : []; + for (const storyModule of canvas) { + context.module = mediaAttributes.attributeAsString(storyModule, "displayType"); + context.subStyle = null; + const shelfIndex = shelves.length; + const shelvesToRender = renderModule(objectGraph, storyModule, articleData, context, shelfIndex); + if (shelvesToRender.length > 0) { + for (const shelf of shelvesToRender) { + shelf.title = context.titleForNextShelf; + if (objectGraph.client.isTV) { + // Skip unsupported tvOS shelves + if (shelf.contentType === "editorialLink") { + continue; + } + } + else if (objectGraph.client.isWatch) { + // Skip unsupported watchOS shelves + if (shelf.contentType === "editorialLink") { + continue; + } + } + shelves.push(shelf); + context.titleForNextShelf = null; + } + } + context.index++; + metricsHelpersLocation.nextPosition(context.metricsLocationTracker); + } + // If we we're showing the fallback list card type on the today page, we're going to show + // the lockups as a list shelf underneath so we can still display the list contents. + // If we're on watchOS, we also want to hit this codepath so that lockup lists do not + // show as empty pages. + if ((context.showingFallbackMediaInline || + objectGraph.client.isWatch || + objectGraph.client.isVision || + objectGraph.client.isWeb || + preprocessor.GAMES_TARGET) && + shelves.length === 0) { + const fallbackShelf = createFallbackListShelf(objectGraph, cardMedia); + if (serverData.isDefinedNonNull(fallbackShelf)) { + shelves.push(fallbackShelf); + } + } + return shelves; + }); +} +// region Data Augmenting +/** + * Article specific entrypoint for page response augmenting. See `augment.ts`. + * @param response Response to augment. + */ +export async function fetchAdditionalDataForInitialResponse(objectGraph, response) { + return await mediaAugment.fetchAugmentedData(objectGraph, response, findAdditionalDataKeysForArticleResponse, fetchDataForArticleDataKey); +} +/** + * Determine the set of data, expressed as an set of `ArticleAdditionalDataKey`s, that need to be fetched for given article response to be displayed. + * This is equivalent to `AbstractMediaApiPageBuilder.additionalDataKeysNeededForData`, but for article builder which doesn't adopt the `builder` API. + * + * @param articleResponse Initial response to determine additional data requirements for. + * @returns {Set<ArticleAdditionalDataKey>} Additional data needed expressed as set of `ArticleAdditionalDataKey` + */ +function findAdditionalDataKeysForArticleResponse(objectGraph, articleResponse) { + /** + * Keys for requested requirements determined by: + * - Modules in canvas only now :) + */ + const allAdditionalDataKeySet = new Set(); + // Requirements based on canvas items: + const articleData = mediaDataStructure.dataFromDataContainer(objectGraph, articleResponse); + const canvasModules = mediaRelationships.relationshipCollection(articleData, "canvas"); + for (const storyModule of canvasModules) { + // Determine additional requests and add to `allRequirementsSet` + const dataKeysForModule = additionalDataKeysForArticleModule(objectGraph, storyModule, articleData); + if (serverData.isDefinedNonNullNonEmpty(dataKeysForModule)) { + for (const requirement of dataKeysForModule) { + allAdditionalDataKeySet.add(requirement); + } + } + } + return allAdditionalDataKeySet; +} +/** + * Builds a promise that will fetch data fulfilling given requirement. Note that these promises will return `null` when they fail, + * and their failure should not cause the entire page to fail. + * This is equivalent to `AbstractMediaApiPageBuilder.fetchAdditionalDataForKey`, but for article builder which doesn't adopt the `builder` API. + * + * @param dataKey Corresponding data key to fetch data for. + */ +// eslint-disable-next-line @typescript-eslint/promise-function-async +function fetchDataForArticleDataKey(objectGraph, dataKey) { + let request; + if (dataKey === "upsellForNonacquisitionCanvas") { + // Use `editorialItem` matching context of that would've otherwise been joined if this story was an acquisition story. + request = arcadeCommon.arcadeUpsellRequest(objectGraph, models.marketingItemContextFromString("editorialItemCanvas")); + } + if (dataKey === "arcadeIcons") { + // Require 10 for now. + request = arcadeCommon.arcadeAppsRequestForIcons(objectGraph, 10); + } + if (serverData.isNull(request)) { + return null; + } + // Failable data fetch, either resolving to valid response or `null`. + return mediaNetwork.fetchData(objectGraph, request).catch(() => null); +} +/** + * Determine the requirements for single article module as determined by it's type. + * @param storyModule The module to fetch additional requirements for. + * @param articleData The article that contains `storyModule` in its canvas. + * @returns {ArticleAdditionalDataKey[] | undefined} Set of data keys if any are needed for rendering given module. + */ +export function additionalDataKeysForArticleModule(objectGraph, storyModule, articleData) { + // Only `AppMarker` has additional requirements. + const moduleType = mediaAttributes.attributeAsString(storyModule, "displayType"); + if (moduleType !== "AppMarker") { + return null; + } + const markerType = mediaAttributes.attributeAsString(storyModule, "appMarkerType"); + // <rdar://problem/55919205> In story Arcade acquisition module dropping from stories + // Editorial wants to use the acquisition module in non-acquisition stories, but the `upsell` relationship is only joined for EIs marked with the acquisition flag. + // When an article is missing the upsell relationship, we'll fetch it separately if we have modules that need it... + const articleDataIsMissingUpsell = serverData.isNull(arcadeCommon.upsellFromRelationshipOf(objectGraph, articleData)); + /** + * Acquisition AppMarker, i.e. `ArcadeShowcase` needs: + * 1. Upsell data for text data, e.g. editorial notes and breakoutCallToAction label, provided this data isn't already provided as part of original page. + * 2. Assortment of Arcade App Icons (iOS Only). + */ + const additionalDataKeysForModule = []; + if (markerType === "Acquisition") { + // iOS needs icon dependency + if (objectGraph.host.isiOS || objectGraph.client.isVision) { + additionalDataKeysForModule.push("arcadeIcons"); + } + // All platform needs upsell to render acquisition modules, add it if missing. + if (articleDataIsMissingUpsell) { + additionalDataKeysForModule.push("upsellForNonacquisitionCanvas"); + } + } + return additionalDataKeysForModule; +} +// endregion +/** + * Create a shelf model representing a single module within article pages. + * @param storyModule Module server data to build shelf and contents from. + * @param articleData The data for article that contains `storyModule` above. + * @param context Global parse context updated while entire sets of modules are being parsed. + * @returns an array of `Shelf` or `null` if building fails for given module. + */ +function renderModule(objectGraph, storyModule, articleData, context, shelfIndex) { + return validation.catchingContext(`module: ${context.module}`, () => { + var _a; + const shelves = []; + switch (context.module) { + case "Header": { + context.titleForNextShelf = mediaAttributes.attributeAsString(storyModule, "editorialCopy"); + break; + } + case "TextBlock": { + const textBlockShelf = createParagraph(objectGraph, storyModule, context); + if (isSome(textBlockShelf)) { + shelves.push(textBlockShelf); + context.hasNonFocusableElements = true; + } + break; + } + case "CollectionLockup": { + const appListShelf = createAppList(objectGraph, storyModule, context); + if (isSome(appListShelf)) { + shelves.push(appListShelf); + context.hasFocusableElements = true; + } + break; + } + case "InlineImage": { + const inlineImageShelf = createImage(objectGraph, storyModule, context); + if (isSome(inlineImageShelf)) { + shelves.push(inlineImageShelf); + context.hasNonFocusableElements = true; + } + break; + } + case "AppLockup": { + const appLockupShelf = createAppLockup(objectGraph, storyModule, context); + if (isSome(appLockupShelf)) { + shelves.push(appLockupShelf); + context.hasFocusableElements = true; + } + break; + } + case "TipBlock": { + const tipShelf = createTip(objectGraph, storyModule, context); + if (isSome(tipShelf)) { + shelves.push(tipShelf); + context.hasNonFocusableElements = true; + } + break; + } + case "PullQuote": { + const pullQuoteShelf = createPullQuote(objectGraph, storyModule, context); + if (isSome(pullQuoteShelf)) { + shelves.push(pullQuoteShelf); + context.hasNonFocusableElements = true; + } + break; + } + case "HorizontalRule": { + const horizontalRuleShelf = createHorizontalRule(objectGraph, storyModule, context); + if (isSome(horizontalRuleShelf)) { + shelves.push(horizontalRuleShelf); + context.hasNonFocusableElements = true; + } + break; + } + case "InlineVideo": { + const inlineVideoShelf = createVideo(objectGraph, storyModule, context); + if (isSome(inlineVideoShelf)) { + shelves.push(inlineVideoShelf); + context.hasFocusableElements = true; + } + break; + } + case "AppMedia": { + const appMediaShelf = createAppMedia(objectGraph, storyModule, context); + if (isSome(appMediaShelf)) { + shelves.push(appMediaShelf); + context.hasFocusableElements = true; + } + break; + } + case "LinkBlock": { + const linkBlockShelf = createLink(objectGraph, storyModule, context); + if (isSome(linkBlockShelf)) { + shelves.push(linkBlockShelf); + context.hasFocusableElements = true; + } + break; + } + case "TextList": { + const textListShelf = createTextList(objectGraph, storyModule, context); + if (isSome(textListShelf)) { + shelves.push(textListShelf); + context.hasNonFocusableElements = true; + } + break; + } + case "IAPLockup": { + const iapLockupShelf = createIAPLockup(objectGraph, storyModule, context); + if (isSome(iapLockupShelf)) { + shelves.push(iapLockupShelf); + context.hasFocusableElements = true; + } + break; + } + case "AppMarker": { + const appMarkerShelf = createAppMarker(objectGraph, storyModule, articleData, context); + if (isSome(appMarkerShelf)) { + shelves.push(appMarkerShelf); + context.hasFocusableElements = true; + } + break; + } + case "StoryList": { + const storyListShelf = createStoryCards(objectGraph, storyModule, context, shelfIndex); + if (isSome(storyListShelf)) { + shelves.push(storyListShelf); + context.hasFocusableElements = true; + } + break; + } + case "AppEventLockup": { + const appEventShelf = createAppEventLockup(objectGraph, storyModule, context); + if (isSome(appEventShelf)) { + shelves.push(appEventShelf); + context.hasFocusableElements = true; + } + break; + } + case "OfferItemLockup": { + const offerItemShelves = createOfferItemLockup(objectGraph, storyModule, context); + if (isSome(offerItemShelves)) { + shelves.push(...offerItemShelves); + context.hasFocusableElements = true; + } + break; + } + default: { + objectGraph.console.log(`Unknown module: ${context.module}`); + } + } + for (const shelf of shelves) { + const existingShelfPresentationHints = (_a = shelf.presentationHints) !== null && _a !== void 0 ? _a : {}; + shelf.presentationHints = { + ...existingShelfPresentationHints, + isArticleContext: true, + }; + } + return shelves; + }); +} +const FULL_WIDTH_MODULES = ["AppLockup", "InlineImage", "InlineVideo", "AppMarker"]; +/** + * Determines whether the provided parameters signifies a full-width article + * module. + * @param shelf The shelf in question. + * @param type The type of article module. + * @returns Whether or not the given shelf for the article type is full width. + */ +function isArticleShelfFullWidth(objectGraph, shelf, type) { + if (shelf && type) { + const itemCount = shelf.items.length; + if (itemCount > 0 && FULL_WIDTH_MODULES.indexOf(type) !== -1) { + const lastItem = shelf.items[itemCount - 1]; + switch (shelf.contentType) { + case "framedArtwork": { + const framedArt = lastItem; + return framedArt && framedArt.isFullWidth; + } + case "framedVideo": { + const framedVideo = lastItem; + return framedVideo && framedVideo.isFullWidth; + } + default: { + return true; + } + } + } + } + return false; +} +// region Footer Lockup +/** + * Adds either a `footerLockup` or `arcadeFooterLockup` property on `ArticlePage` model, based on type of article. + * @param page Page to add footer to if needed. + * @param articleData Original data of article being rendered. + * @param context Parse context for article builder. + */ +function addFooterLockupForPageIfNeeded(objectGraph, page, articleData, context) { + // App Lockup for Articles about single specific app. + const footerProductData = productDataFromArticle(objectGraph, articleData); + if (footerProductData) { + const externalDeepLinkUrl = externalDeepLink.deepLinkUrlFromData(objectGraph, articleData); + page.footerLockup = productFooterLockupFromData(objectGraph, footerProductData, context, externalDeepLinkUrl); + return; + } + // Arcade Lockup for Acquisition Story for supported platforms + const isArcadeAcquisitionEI = mediaAttributes.attributeAsBooleanOrFalse(articleData, "isAcquisition"); + const platformSupportsArcadeFooterLockup = objectGraph.host.isiOS || objectGraph.host.isMac; + const additionalDataIsAvailable = serverData.isDefinedNonNull(context.additionalData); + if (additionalDataIsAvailable && isArcadeAcquisitionEI && platformSupportsArcadeFooterLockup) { + const upsellData = arcadeCommon.upsellFromRelationshipOf(objectGraph, articleData); + page.arcadeFooterLockup = arcadeFooterLockupFromData(objectGraph, upsellData, context); + } +} +/** + * Find platform data from editorial item to enhance sharing and display in footer lockup + * At the moment, only single app editorials get footer lockups and have enhanced sharing. + * + * @param editorialItem Item to find footer content for + * @returns content to display in footer lockup, or null if no content should be displayed + */ +export function productDataFromArticle(objectGraph, editorialItem) { + const relatedContent = mediaRelationships.relationshipCollection(editorialItem, "card-contents"); + if (relatedContent.length !== 1) { + return null; + } + const contentData = relatedContent[0]; + if (!contentData) { + return null; + } + switch (contentData.type) { + case "apps": + case "app-bundles": + return contentData; + default: + return null; + } +} +/** + * Creates a footer lockup with a data for a specific app. + * Cover method over `lockupFromData` to override `offerStyle`. + * + * @param data MAPI data to build footer with. + * @param context Parse context + * @param externalDeepLinkUrl promotional deep link url to use on the lockup's offer. + * @returns A new `Lockup` object for footer lockups. + */ +function productFooterLockupFromData(objectGraph, data, context, externalDeepLinkUrl) { + const lockupOptions = { + offerStyle: footerLockupOfferStyle(objectGraph), + metricsOptions: { + pageInformation: context.metricsPageInformation, + locationTracker: context.metricsLocationTracker, + }, + clientIdentifierOverride: context.clientIdentifierOverride, + externalDeepLinkUrl: externalDeepLinkUrl, + crossLinkSubtitle: context.crossLinkSubtitle, + artworkUseCase: 0 /* content.ArtworkUseCase.Default */, + canDisplayArcadeOfferButton: content.shelfContentTypeCanDisplayArcadeOfferButtons(objectGraph, "smallLockup"), + }; + return lockups.lockupFromData(objectGraph, data, lockupOptions); +} +/** + * Creates a footer lockup representing the Arcade subscription service. + * @param upsellData Contains both editorial and iAP data for Arcade + * @param context Parse context. + */ +function arcadeFooterLockupFromData(objectGraph, upsellData, context) { + const metricsOptions = { + pageInformation: context.metricsPageInformation, + locationTracker: context.metricsLocationTracker, + }; + return lockups.arcadeLockupFromData(objectGraph, upsellData, metricsOptions, models.marketingItemContextFromString("editorialItem"), "infer", null); +} +/** + * Determines the offer style to use for the footer lockup. + */ +function footerLockupOfferStyle(objectGraph) { + switch (objectGraph.client.deviceType) { + case "mac": + return "white"; + default: + return "infer"; + } +} +// endregion +function createFallbackListShelf(objectGraph, cardMedia) { + if (cardMedia instanceof models.TodayCardMediaList || cardMedia instanceof models.TodayCardMediaRiver) { + const fallbackShelf = new models.Shelf("smallLockup"); + fallbackShelf.items = cardMedia.lockups; + if (objectGraph.client.isWeb) { + fallbackShelf.presentationHints = { + ...fallbackShelf.presentationHints, + isArticleContext: true, + }; + } + return fallbackShelf; + } + return null; +} +function shareSheetActionFromData(objectGraph, editorialItem, todayCardConfig) { + const productData = productDataFromArticle(objectGraph, editorialItem); + /* + * Determine title + */ + let title = null; + const name = content.notesFromData(objectGraph, editorialItem, "name"); + const short = content.notesFromData(objectGraph, editorialItem, "short"); + // Prefer "name: short" + if (name && short) { + title = objectGraph.loc + .string("ShareSheet.TitleSubtitle.Format", "{title}: {subtitle}") + .replace("{title}", name) + .replace("{subtitle}", short); + } + // Followed by name + if (!title && name) { + title = name; + } + // Followed by short + if (!title && short) { + title = short; + } + // Followed by product name + if (!title && productData) { + const productTitle = mediaAttributes.attributeAsString(productData, "name"); + const cardDisplayStyle = mediaAttributes.attributeAsString(editorialItem, "cardDisplayStyle"); + switch (cardDisplayStyle) { + case TodayCardDisplayStyle.GameOfTheDay: { + title = objectGraph.loc.string("SHARE_SHEET_GAME_OF_DAY_TITLE_FORMAT").replace("{title}", productTitle); + break; + } + case TodayCardDisplayStyle.AppOfTheDay: { + title = objectGraph.loc.string("SHARE_SHEET_APP_OF_DAY_TITLE_FORMAT").replace("{title}", productTitle); + break; + } + default: { + objectGraph.console.log(`No title for article with unknown style: ${cardDisplayStyle}`); + break; + } + } + } + const url = mediaAttributes.attributeAsString(editorialItem, "url"); + let articleArtwork; + const cardDisplayStyle = mediaAttributes.attributeAsString(editorialItem, "cardDisplayStyle"); + switch (cardDisplayStyle) { + case TodayCardDisplayStyle.Grid: + case TodayCardDisplayStyle.List: + case TodayCardDisplayStyle.River: + articleArtwork = artworkBuilder.createArtworkForResource(objectGraph, "resource://ShareCollectionThumbnail", 40, 40); + break; + default: + articleArtwork = null; + break; + } + // Create share sheet model (bail out if unable to do so) + const shareData = sharing.shareSheetDataForArticle(objectGraph, title, url, null, articleArtwork, editorialItem); + if (!serverData.isDefinedNonNull(shareData)) { + return null; + } + const activities = sharing.shareSheetActivitiesForArticle(objectGraph, url, todayCardPreviewUrlForTodayCard(objectGraph, editorialItem.id, todayCardConfig), editorialItem.id); + return new models.ShareSheetAction(shareData, activities); +} +function createShareShelf(objectGraph, shareAction, context, isLastModuleFullWidth) { + if (!serverData.isDefinedNonNull(shareAction) || + objectGraph.client.isVision || + preprocessor.GAMES_TARGET || + objectGraph.client.isCompanionVisionApp) { + return null; + } + // Create share button + const shareButton = new models.RoundedButton("share", objectGraph.loc.string("SHARE_STORY"), !isLastModuleFullWidth, shareAction); + // Add share shelf + const shareButtonShelf = new models.Shelf("roundedButton"); + shareButtonShelf.items = [shareButton]; + return shareButtonShelf; +} +function createParagraph(objectGraph, module, context) { + const text = mediaAttributes.attributeAsString(module, "editorialCopy"); + if (!text) { + return null; + } + const paragraph = new models.Paragraph(text, "text/x-apple-as3-nqml", "article"); + // Setup impressions + addImpressionsFieldsToModel(objectGraph, paragraph, context); + const shelf = new models.Shelf("paragraph"); + shelf.items = [paragraph]; + return shelf; +} +function createImage(objectGraph, module, context) { + const displayStyle = mediaAttributes.attributeAsString(module, "inlineImageDisplayType"); + const artworkData = mediaAttributes.attributeAsDictionary(module, "artwork"); + // If the displayStyle is FullWidth want to 'allowTransparency' so that images blend into the page in both + // light and dark mode. Previously editorial would bake white backgrounds into images they wanted to 'blend' + // with the page + const artwork = content.artworkFromApiArtwork(objectGraph, artworkData, { + useCase: 13 /* content.ArtworkUseCase.ArticleImage */, + allowingTransparency: displayStyle === "FullWidth" && !objectGraph.client.isVision, + withJoeColorPlaceholder: objectGraph.client.isVision, + }); + if (!artwork) { + return null; + } + const frame = new models.FramedArtwork(artwork, false, "text/x-apple-as3-nqml"); + // Get the optional caption + frame.caption = mediaAttributes.attributeAsString(module, "editorialCopy"); + context.subStyle = displayStyle; + if (displayStyle) { + switch (displayStyle) { + case "BoundingBox": { + frame.isFullWidth = false; + frame.hasRoundedCorners = true; + break; + } + case "FullWidth": + default: { + frame.isFullWidth = true; + frame.hasRoundedCorners = false; + break; + } + } + } + // Setup impressions + addImpressionsFieldsToModel(objectGraph, frame, context); + const shelf = new models.Shelf("framedArtwork"); + shelf.items = [frame]; + return shelf; +} +function createTip(objectGraph, module, context) { + const artworkData = mediaAttributes.attributeAsDictionary(module, "artwork"); + const artwork = content.artworkFromApiArtwork(objectGraph, artworkData, { + useCase: 13 /* content.ArtworkUseCase.ArticleImage */, + }); + if (!artwork) { + return null; + } + const caption = mediaAttributes.attributeAsString(module, "editorialCopy"); + const ordinal = mediaAttributes.attributeAsString(module, "tipNumber"); + // Create the tip image + const frame = new models.FramedArtwork(artwork, false, "text/x-apple-as3-nqml"); + frame.isFullWidth = false; + frame.hasRoundedCorners = true; + frame.caption = caption; + frame.ordinal = ordinal; + // Setup impressions + addImpressionsFieldsToModel(objectGraph, frame, context); + // Create the shelf + const shelf = new models.Shelf("framedArtwork"); + shelf.items = [frame]; + return shelf; +} +function createPullQuote(objectGraph, module, context) { + const text = mediaAttributes.attributeAsString(module, "quote"); + const attribution = mediaAttributes.attributeAsString(module, "quoteAttribution"); + // Get the optional artwork + const artworkData = mediaAttributes.attributeAsDictionary(module, "artwork"); + const artwork = content.artworkFromApiArtwork(objectGraph, artworkData, { + useCase: 13 /* content.ArtworkUseCase.ArticleImage */, + }); + const fullWidth = mediaAttributes.attributeAsString(module, "pullQuoteDisplayType") === "FullWidth"; + // Create the quote + const quote = new models.Quote(text, attribution, artwork, fullWidth); + // Setup impressions + addImpressionsFieldsToModel(objectGraph, quote, context); + // Create the shelf + const shelf = new models.Shelf("quote"); + shelf.items = [quote]; + return shelf; +} +function createHorizontalRule(objectGraph, module, context) { + const lineStyle = mediaAttributes.attributeAsString(module, "lineStyle"); + const fullWidth = mediaAttributes.attributeAsString(module, "displayStyle") === "FullWidth"; + let ruleColor = color.named("defaultLine"); + if (objectGraph.client.isVision && (lineStyle === "Dotted" || lineStyle === "Dashed")) { + ruleColor = color.white; + } + // Parse the customColor from Media API. This can only be a dynamic color. + const apiColor = mediaAttributes.attributeAsDictionary(module, "customColor"); + const lightColor = color.fromHex(serverData.asString(apiColor, "lightMode")); + const darkColor = color.fromHex(serverData.asString(apiColor, "darkMode")); + if (!serverData.isNullOrEmpty(lightColor) && !serverData.isNullOrEmpty(darkColor)) { + ruleColor = color.dynamicWith(lightColor, darkColor); + } + const horizontalRule = new models.HorizontalRule(lineStyle, ruleColor, fullWidth); + // Create the Shelf + const shelf = new models.Shelf("horizontalRule"); + shelf.items = [horizontalRule]; + return shelf; +} +function createVideo(objectGraph, module, context) { + // Get the preview artwork + const artworkData = mediaAttributes.attributeAsDictionary(module, "video.previewFrame"); + const artwork = content.artworkFromApiArtwork(objectGraph, artworkData, { + useCase: 13 /* content.ArtworkUseCase.ArticleImage */, + }); + if (!artwork) { + return null; + } + // Get the video URL + const videoUrl = mediaAttributes.attributeAsString(module, "video.video"); + if (!videoUrl) { + return null; + } + const videoDisplayType = mediaAttributes.attributeAsString(module, "inlineVideoDisplayType"); + const isFullWidth = videoDisplayType === "FullWidth"; + // Create the video + const video = new models.Video(videoUrl, artwork, videoDefaults.defaultVideoConfiguration(objectGraph)); + metricsHelpersMedia.addMetricsEventsToVideo(objectGraph, video, { + pageInformation: context.metricsPageInformation, + locationTracker: context.metricsLocationTracker, + id: context.pageId, + }); + const videoModule = new models.FramedVideo(video, isFullWidth, "text/x-apple-as3-nqml"); + // Get the optional caption + videoModule.caption = mediaAttributes.attributeAsString(module, "editorialCopy"); + // Setup impressions + addImpressionsFieldsToModel(objectGraph, videoModule, context); + // Create the shelf + const shelf = new models.Shelf("framedVideo"); + shelf.items = [videoModule]; + return shelf; +} +function createAppLockup(objectGraph, module, context) { + const contentData = contentFromModule(objectGraph, module, context); + if (!contentData) { + return null; + } + // Shelf to generate. Either lockup, app showcase, or app event shelf + let shelf = null; + // If we have an app-events relationship, we want to use this as the priority. This sometimes exists on the + // AppLockup type, rather than as the AppEventLockup type, so that older clients can still render this + // item by falling back to the AppLockup type. + const appEventsDataItems = mediaRelationships.relationshipCollection(module, "app-events"); + if (serverData.isDefinedNonNullNonEmpty(appEventsDataItems)) { + shelf = appPromotionsShelf.appEventsShelfForArticle(objectGraph, appEventsDataItems, context.metricsPageInformation, context.metricsLocationTracker, context); + if (serverData.isDefinedNonNull(shelf)) { + return shelf; + } + } + // Set the display style + const displayStyle = mediaAttributes.attributeAsString(module, "appLockupSize"); + context.subStyle = displayStyle; + let shelfStyle; + let isLockup = false; + if (displayStyle) { + switch (displayStyle) { + case "Small": { + shelfStyle = "smallLockup"; + isLockup = true; + break; + } + case "Medium": { + shelfStyle = "mediumLockup"; + isLockup = true; + break; + } + case "Large": + default: { + if (objectGraph.client.isWatch || + objectGraph.client.isTV || + objectGraph.client.isVision || + preprocessor.GAMES_TARGET) { + // Per design, on watchOS we always show a lockup for app showcases. + // Watch App Store treats all lockup sizes the same -- let's pick small. + shelfStyle = "smallLockup"; + isLockup = true; + } + else { + shelfStyle = "appShowcase"; + } + break; + } + } + } + // Determine the deep link URL, if there is one. + const externalDeepLinkUrl = externalDeepLink.deepLinkUrlFromData(objectGraph, module); + // Create the appropriate shelf item + if (isLockup) { + const lockupShelf = new models.Shelf(shelfStyle); + const metricsOptions = { + metricsOptions: { + pageInformation: context.metricsPageInformation, + locationTracker: context.metricsLocationTracker, + }, + clientIdentifierOverride: context.clientIdentifierOverride, + externalDeepLinkUrl: externalDeepLinkUrl, + artworkUseCase: content.artworkUseCaseFromShelfStyle(objectGraph, shelfStyle), + }; + let lockup; + if (preprocessor.GAMES_TARGET) { + const shelfID = new PageID(context.pageId).shelfID(module.id); + lockup = gamesComponentBuilder.makeArticleGameLockup(objectGraph, contentData, shelfID); + } + else { + lockup = lockups.lockupFromData(objectGraph, contentData, metricsOptions); + } + if (isNothing(lockup)) { + return null; + } + lockupShelf.items = [lockup]; + shelf = lockupShelf; + } + else { + // On all platforms, the AppLockup platform generates a AppShowcase when display style is large. + shelf = createAppShowcase(objectGraph, module, context); + } + return shelf; +} +function createAppShowcase(objectGraph, module, context) { + // Create the shelf + const shelf = new models.Shelf("appShowcase"); + // Parameterize by platform: + // tvOS populates the `screenshots` field to display alongside video. + const showcaseHasScreenshots = objectGraph.client.isTV; + // Only non-tvOS has shelf background color + const shelfHasBackgroundColor = objectGraph.client.deviceType !== "tv"; + const contentData = contentFromModule(objectGraph, module, context); + const externalDeepLinkUrl = externalDeepLink.deepLinkUrlFromData(objectGraph, module); + const lockup = lockups.lockupFromData(objectGraph, contentData, { + offerStyle: "colored", + metricsOptions: { + pageInformation: context.metricsPageInformation, + locationTracker: context.metricsLocationTracker, + }, + clientIdentifierOverride: context.clientIdentifierOverride, + externalDeepLinkUrl: externalDeepLinkUrl, + crossLinkSubtitle: context.crossLinkSubtitle, + artworkUseCase: 1 /* content.ArtworkUseCase.LockupIconSmall */, + }); + const showcase = new models.AppShowcase("large", lockup); + showcase.description = lockups.subtitleFromData(objectGraph, contentData); + // Add Video + // Configure the video for the showcase, if the module demands it. + let showcaseVideo = null; + const videoType = mediaAttributes.attributeAsString(module, "appLockupVideo"); + switch (videoType) { + case "AppTrailer": { + const allAppVideos = content.videoPreviewsFromData(objectGraph, contentData); + if (allAppVideos && allAppVideos.length > 0) { + showcaseVideo = allAppVideos[0]; + } + break; + } + default: + break; + } + if (showcaseVideo) { + metricsHelpersMedia.addMetricsEventsToVideo(objectGraph, showcaseVideo, { + pageInformation: context.metricsPageInformation, + locationTracker: context.metricsLocationTracker, + id: context.pageId, + }); + showcase.video = showcaseVideo; + } + // Add Screenshots for AppShowcase if necessary + if (showcaseHasScreenshots) { + showcase.screenshots = content.screenshotsFromData(objectGraph, contentData, 14 /* content.ArtworkUseCase.ArticleScreenshots */, [content.currentAppPlatform(objectGraph)]); + } + // Configure background if necessary. + if (shelfHasBackgroundColor) { + shelf.background = { + type: "color", + color: appShowcaseBackgroundColor, + }; + } + shelf.items = [showcase]; + return shelf; +} +function createIAPLockup(objectGraph, module, context) { + const contentData = contentFromModule(objectGraph, module, context); + if (!contentData) { + return null; + } + // Create the lockup + const lockup = lockups.inAppPurchaseLockupFromData(objectGraph, contentData, { + metricsOptions: { + pageInformation: context.metricsPageInformation, + locationTracker: context.metricsLocationTracker, + }, + clientIdentifierOverride: context.clientIdentifierOverride, + artworkUseCase: 1 /* content.ArtworkUseCase.LockupIconSmall */, + }); + if (!lockup) { + return null; + } + const showcase = new models.InAppPurchaseShowcase(lockup); + // Create the shelf + const shelf = new models.Shelf("inAppPurchaseShowcase"); + shelf.background = { + type: "color", + color: iAPBackgroundColor, + }; + shelf.items = [showcase]; + return shelf; +} +function createAppList(objectGraph, module, context) { + const showOrdinals = mediaAttributes.attributeAsBooleanOrFalse(module, "showOrdinals"); + const ordinalDirection = mediaAttributes.attributeAsString(module, "collectionLockupDisplayType") === "OrdinalDesc" + ? "descending" + : "ascending"; + // Set the display style + const displayStyle = mediaAttributes.attributeAsString(module, "collectionLockupSize"); + context.subStyle = displayStyle; + let style; + if (displayStyle) { + switch (displayStyle) { + case "Large": { + style = "largeLockup"; + break; + } + case "Medium": { + style = "mediumLockup"; + break; + } + case "Small": + default: { + style = "smallLockup"; + break; + } + } + } + // Construct the lockup options + const lockupOptions = { + metricsOptions: { + pageInformation: context.metricsPageInformation, + locationTracker: context.metricsLocationTracker, + }, + clientIdentifierOverride: context.clientIdentifierOverride, + artworkUseCase: content.artworkUseCaseFromShelfStyle(objectGraph, style), + canDisplayArcadeOfferButton: content.shelfContentTypeCanDisplayArcadeOfferButtons(objectGraph, style), + }; + // Check if we have content + const contents = mediaRelationships.relationshipCollection(module, "contents"); + if (isNothing(contents)) { + return null; + } + let childLockups = []; + if (preprocessor.GAMES_TARGET) { + const shelfID = new PageID(context.pageId).shelfID(module.id); + childLockups = gamesComponentBuilder.makeArticleGameLockups(objectGraph, contents, shelfID); + } + else { + childLockups = lockups.lockupsFromData(objectGraph, contents, { + includeOrdinals: showOrdinals, + ordinalDirection: ordinalDirection, + lockupOptions: lockupOptions, + }); + } + if (!childLockups || childLockups.length === 0) { + return null; + } + // Create the shelf + const shelf = new models.Shelf(style); + shelf.items = childLockups; + return shelf; +} +function createAppMedia(objectGraph, module, context) { + const contentData = contentFromModule(objectGraph, module, context); + if (!contentData) { + return null; + } + // Set the display style + const mediaOption = mediaAttributes.attributeAsString(module, "appMediaOption"); + const appMediaPlatform = mediaAttributes.attributeAsString(module, "appMediaPlatform"); + context.subStyle = mediaOption; + switch (mediaOption) { + case "Screenshots": { + let shelf = null; + // I'm so sorry, but making this split makes the macOS client code infinitely better because we are able + // to reuse the same product media view and component contract that is used on product page screenshots/trailers. + // Really, iOS should be reworked such that its module & product page implementation has a single source, + // but this has serious design obstacles that need to be worked through. + if (objectGraph.client.isMac) { + shelf = new models.Shelf("productMedia"); + const productMedia = content.productMediaFromData(objectGraph, contentData, 14 /* content.ArtworkUseCase.ArticleScreenshots */); + if (serverData.isDefinedNonNull(productMedia) && productMedia.length) { + shelf.items = productMedia; + } + } + else { + shelf = new models.Shelf("screenshots"); + if (serverData.isNull(appMediaPlatform)) { + /** + * The server did not tell us which app platform to use, so we need to infer based on various keys in + * the response. These parameters are only fully baked into product-dv responses, so we we need to do + * the more expensive product-dv lookup in order to correctly infer the default screenshots to use for + * the shelf. + */ + const screenshots = content.screenshotsFromData(objectGraph, contentData, 14 /* content.ArtworkUseCase.ArticleScreenshots */); + if (screenshots && screenshots.length > 0) { + shelf.items = [screenshots[0]]; + } + } + else { + /** + * Server tells us which platform to use -- dictated by `appMediaPlatform`. Selectively do a lookup for + * just those screenshots. + */ + const desiredAppPlatform = appPlatformFromAppMediaPlatform(objectGraph, appMediaPlatform); + if (desiredAppPlatform) { + const screenshots = content.screenshotsFromData(objectGraph, contentData, 14 /* content.ArtworkUseCase.ArticleScreenshots */, [desiredAppPlatform]); + if (screenshots && screenshots.length) { + shelf.items = [screenshots[0]]; + } + } + } + } + if (serverData.isDefinedNonNull(shelf) && shelf.items.length === 0) { + return null; + } + return shelf; + } + case "AppTrailers": + const trailersShelf = new models.Shelf("framedVideo"); + const videoPreviews = content.videoPreviewsFromData(objectGraph, contentData); + if (videoPreviews && videoPreviews.length > 0) { + const video = videoPreviews[0]; + metricsHelpersMedia.addMetricsEventsToVideo(objectGraph, video, { + pageInformation: context.metricsPageInformation, + locationTracker: context.metricsLocationTracker, + id: context.pageId, + }); + const firstTrailer = new models.FramedVideo(video, false, "text/plain", null, null, true); + trailersShelf.items = [firstTrailer]; + return trailersShelf; + } + else { + return null; + } + default: { + return null; + } + } +} +function createLink(objectGraph, module, context) { + if (objectGraph.client.isTV || objectGraph.client.isWatch) { + return null; + } + const urlString = mediaAttributes.attributeAsString(module, "url"); + if (!urlString) { + return null; + } + const url = new urls.URL(urlString); + const linkTitle = mediaAttributes.attributeAsString(module, "urlTitle"); + let text = mediaAttributes.attributeAsString(module, "editorialCopy"); + if (!text) { + text = url.host; + } + const mediaHosts = [ + "itunes.apple.com", + "apps.apple.com", + "music.apple.com", + "books.apple.com", + "podcasts.apple.com", + "watch-app.cdn-apple.com", + "tv.apple.com", + ]; + let linkPresentationEnabled = false; + for (const mediaHost of mediaHosts) { + if (url.host.endsWith(mediaHost)) { + linkPresentationEnabled = true; + } + } + const action = new models.ExternalUrlAction(urlString); + metricsHelpersClicks.addClickEventToAction(objectGraph, action, { + targetType: "link", + pageInformation: context.metricsPageInformation, + id: `${context.index}`, + locationTracker: context.metricsLocationTracker, + }); + const link = new models.EditorialLink(linkTitle, text, action, linkPresentationEnabled); + // Setup impressions + addImpressionsFieldsToModel(objectGraph, link, context); + const shelf = new models.Shelf("editorialLink"); + shelf.items = [link]; + return shelf; +} +function createTextList(objectGraph, module, context) { + const listEntries = mediaAttributes.attributeAsArrayOrEmpty(module, "editorialCopy"); + if (!listEntries.length) { + return null; + } + const type = mediaAttributes.attributeAsString(module, "textListDisplayType"); + context.subStyle = type; + let isBulleted = false; + switch (type) { + case "Bulleted": { + isBulleted = true; + break; + } + default: { + isBulleted = false; + break; + } + } + let text; + if (isBulleted) { + text = "<ul>"; + } + else { + text = "<ol>"; + } + for (const textEntry of listEntries) { + const listItemJSONString = JSON.stringify(textEntry); + // rdar://104446319 - We must use `parse` on our JSON string to convert back to + // a raw string object as this ensures leading/trailing quotation marks are *not* escaped + const listItem = JSON.parse(listItemJSONString); + text = `${text}<li>${listItem}</li>`; + } + if (isBulleted) { + text = `${text}</ul>`; + } + else { + text = `${text}</ol>`; + } + const paragraph = new models.Paragraph(text, "text/x-apple-as3-nqml", "article"); + // Setup impressions + addImpressionsFieldsToModel(objectGraph, paragraph, context); + const shelf = new models.Shelf("paragraph"); + shelf.items = [paragraph]; + return shelf; +} +function createStoryCards(objectGraph, module, context, shelfIndex) { + if (objectGraph.client.isVision) { + const shelfToken = createBaseShelfToken(objectGraph, undefined, module, false, shelfIndex, context.metricsPageInformation, context.metricsLocationTracker); + const shelf = buildSmallStoryCardShelf(objectGraph, shelfToken); + shelf.isHorizontal = true; + return shelf; + } + const cards = mediaRelationships.relationshipCollection(module, "contents"); + if (!cards) { + return null; + } + const title = mediaAttributes.attributeAsString(module, "name"); + const subtitle = mediaAttributes.attributeAsString(module, "tagline"); + let shelf = null; + if (objectGraph.client.isiOS && objectGraph.featureFlags.isEnabled("mini_today_cards_article")) { + const todayParseContext = new TodayParseContext(context.metricsPageInformation, context.metricsLocationTracker); + shelf = todayHorizontalCardUtil.shelfForMiniTodayCards(objectGraph, cards, title, subtitle, todayParseContext); + } + else { + const isSmallStoryCardsSupported = objectGraph.host.isiOS || objectGraph.host.isMac || objectGraph.host.isWeb; + const resolvedContentType = isSmallStoryCardsSupported ? "smallStoryCard" : "todayBrick"; + shelf = todayHorizontalCardUtil.shelfForHorizontalCardItems(objectGraph, cards, resolvedContentType, title, subtitle, context, null); + if (isSmallStoryCardsSupported) { + // Only specific small story cards are supported and will crash otherwise, filter those here preemptively. + // rdar://91965501 (MAS Crashing - Earth Day Landing Page - 4/19) + if (Array.isArray(shelf.items)) { + shelf.items = shelf.items.filter((item) => { + if (!(item instanceof models.TodayCard)) { + return true; + } + return todayHorizontalCardUtil.isHorizontalCardSupportedForKind(objectGraph, item.media.kind, resolvedContentType); + }); + } + } + } + return shelf; +} +function createAppEventLockup(objectGraph, module, context) { + const contentData = contentFromModule(objectGraph, module, context); + if (!contentData) { + return null; + } + return appPromotionsShelf.appEventsShelfForArticle(objectGraph, [contentData], context.metricsPageInformation, context.metricsLocationTracker, context); +} +function createOfferItemLockup(objectGraph, module, context) { + if (!objectGraph.client.isiOS) { + return []; + } + const offerItem = mediaRelationships.relationshipData(objectGraph, module, "contents"); + if (serverData.isNullOrEmpty(offerItem)) { + return null; + } + // Offer detail Paragraph + const offerParagraph = mediaAttributes.attributeAsString(module, "editorialCopy"); + const paragraph = new models.Paragraph(offerParagraph, "text/x-apple-as3-nqml", "article"); + const paragraphShelf = new models.Shelf("paragraph"); + paragraphShelf.items = [paragraph]; + // Winback Offer Card + const offerItemShelf = appPromotionsShelf.appEventsShelfForArticle(objectGraph, [offerItem], context.metricsPageInformation, context.metricsLocationTracker, context); + return [paragraphShelf, offerItemShelf]; +} +/** + * Ingests EI canvas modules of form: + * { + * id: <editorial-id>, + * type: "editorial-item-shelves", + * attributes: { + * displayType: "AppMarker", + * appMarkerType: <AppMarkerType> + * } + * } + * to generate an shelf for AppMarker model. + */ +function createAppMarker(objectGraph, appMarkerModule, articleData, context) { + const markerType = mediaAttributes.attributeAsString(appMarkerModule, "appMarkerType"); + context.subStyle = markerType; + let shelf = null; + switch (markerType) { + case "OSUpgrade": + shelf = createOSUpgradeClientControlButton(objectGraph, appMarkerModule, context); + break; + case "Acquisition": + shelf = createArcadeShowcase(objectGraph, appMarkerModule, articleData, context); + break; + default: + break; + } + return shelf; +} +/** + * Ingests EI canvas modules of form: + * { + * id: <editorial-id>, + * type: "editorial-item-shelves", + * attributes: { + * displayType: "AppMarker", + * appMarkerType: "OSUpgrade" + * } + * } + * to generate an shelf with an button that links to preferences updates. + */ +function createOSUpgradeClientControlButton(objectGraph, osUpgradeModule, context) { + const deviceType = objectGraph.client.deviceType; + if (deviceType !== "mac") { + return null; // Early exit - Only MAS utilizes OS Upgrade Client Control Button currently. + } + const installUpdateUrl = links.osUpdateUrl(deviceType); + if (installUpdateUrl === null) { + return null; + } + // Action to Preferences + const openUpdatesAction = new models.ExternalUrlAction(installUpdateUrl); + // Action to open preferences is configured as `link` + metricsHelpersClicks.addClickEventToAction(objectGraph, openUpdatesAction, { + targetType: "link", + pageInformation: context.metricsPageInformation, + id: `${context.index}`, + locationTracker: context.metricsLocationTracker, + }); + // Shelf model + const upgradeControlText = objectGraph.loc.string("CLIENT_CONTROL_OS_UPGRADE_TITLE", "CHECK FOR UPDATE"); + const upgradeControl = new models.ClientControlButton(upgradeControlText, openUpdatesAction); + // Add impressions + addImpressionsFieldsToModel(objectGraph, upgradeControl, context); + const shelf = new models.Shelf("clientControlButton"); + shelf.items = [upgradeControl]; + return shelf; +} +/** + * Ingests EI canvas modules of form: + * { + * id: <editorial-id>, + * type: "editorial-item-shelves", + * } + * with additional data: + * - Upsell data on `context.additionalData` + * - Icon Artwork data (iOS only) on `context.additionalData` + * + * to generate an shelf that promotes Arcade service. + * + * @param arcadeShowcaseModule Arcade showcase module + * @param articleData The data backing the article containing the module. Used for top-level relationship. + * @param context Parse context for this page parsing. This context contains the additional requirements data. + */ +function createArcadeShowcase(objectGraph, arcadeShowcaseModule, articleData, context) { + const supportedOnPlatform = objectGraph.host.isiOS || objectGraph.host.isMac || objectGraph.client.isVision; + if (!supportedOnPlatform) { + return null; + } + // Default to upsell on relation, falling back to upsell that may have been fetched separately for orphaned acquisition modules. + let upsellData = arcadeCommon.upsellFromRelationshipOf(objectGraph, articleData); + if (!upsellData && context.additionalData) { + const upsellResponse = context.additionalData.get("upsellForNonacquisitionCanvas"); + upsellData = arcadeCommon.upsellFromContentsOfUpsellResponse(objectGraph, upsellResponse); + } + if (!serverData.isDefinedNonNull(upsellData)) { + return null; + } + const baseMetricsOptions = { + pageInformation: context.metricsPageInformation, + locationTracker: context.metricsLocationTracker, + }; + // Flow to See All games if subscribed + const subscribedAction = arcadeCommon.openArcadeMainAction(objectGraph, context.metricsPageInformation, context.metricsLocationTracker, objectGraph.client.isVision); + if (preprocessor.GAMES_TARGET) { + subscribedAction.title = objectGraph.loc.string("OfferButton.Arcade.Title.Explore"); + } + else { + subscribedAction.title = objectGraph.loc.string("ARCADE_ACTION_TITLE_EXPLORE", "EXPLORE"); + } + // Flow to Arcade Subscribe page if unsubscribed. + let unsubscribedAction; + const unsubscribedActionTitle = breakoutsCommon.callToActionLabelFromData(objectGraph, upsellData.marketingItemData); + if (serverData.isDefinedNonNullNonEmpty(unsubscribedActionTitle)) { + // We support an inline offer here instead, when the pricing token is there. + unsubscribedAction = arcadeUpsell.arcadeOfferButtonActionFromData(objectGraph, upsellData.marketingItemData, models.marketingItemContextFromString("editorialItemCanvas"), baseMetricsOptions); + if (serverData.isDefinedNonNull(unsubscribedAction)) { + unsubscribedAction.title = unsubscribedActionTitle; + } + } + else { + // If Upsell EI is misconfigured and missing `breakoutCallToActionLabel`, default to opening Arcade app for unsubscribed state. + unsubscribedAction = arcadeCommon.openArcadeMainAction(objectGraph, context.metricsPageInformation, context.metricsLocationTracker, objectGraph.client.isVision); + if (preprocessor.GAMES_TARGET) { + unsubscribedAction.title = objectGraph.loc.string("OfferButton.Arcade.Title.Explore"); + } + else { + unsubscribedAction.title = objectGraph.loc.string("ARCADE_ACTION_TITLE_EXPLORE", "EXPLORE"); + } + } + const arcadeShowcase = new models.ArcadeShowcase(unsubscribedAction, subscribedAction); + const unsubscribedDescription = arcadeUpsell.descriptionFromData(objectGraph, upsellData.marketingItemData); + arcadeShowcase.unsubscribedDescription = unsubscribedDescription; + const offerDisplayProperties = new models.OfferDisplayProperties("arcade", objectGraph.bag.arcadeAppAdamId, null, "colored", null, "dark", null, null, null, null, null, null, null, null, null, null, null, null, objectGraph.bag.arcadeProductFamilyId); + if (preprocessor.GAMES_TARGET) { + offerDisplayProperties.titles["subscribed"] = objectGraph.loc.string("OfferButton.Arcade.Title.Explore"); + } + else { + offerDisplayProperties.titles["subscribed"] = objectGraph.loc.string("ARCADE_ACTION_TITLE_EXPLORE", "EXPLORE"); + } + arcadeShowcase.offerDisplayProperties = offerDisplayProperties; + const showcaseMetricsOptions = { + ...baseMetricsOptions, + targetType: "arcadeShowcase", + title: unsubscribedActionTitle, + id: arcadeShowcaseModule.id, + kind: "arcadeShowcase", + softwareType: null, + displaysArcadeUpsell: true, + }; + metricsHelpersImpressions.addImpressionFields(objectGraph, arcadeShowcase, showcaseMetricsOptions); + // Build Artwork for iOS only + if (objectGraph.host.isiOS || objectGraph.client.isVision) { + // Context should have additional data to source icons. + if (serverData.isNull(context.additionalData)) { + return null; + } + const iconResponse = context.additionalData.get("arcadeIcons"); + if (serverData.isDefinedNonNullNonEmpty(iconResponse)) { + const iconMetricsOptions = { + pageInformation: context.metricsPageInformation, + locationTracker: context.metricsLocationTracker, + }; + const iconsDataCollection = mediaDataStructure.dataCollectionFromResultsListContainer(iconResponse); + arcadeShowcase.iconArtworks = content.impressionableAppIconsFromDataCollection(objectGraph, iconsDataCollection, iconMetricsOptions, { + useCase: 2 /* content.ArtworkUseCase.LockupIconMedium */, + }); + } + } + const shelf = new models.Shelf("arcadeShowcase"); + shelf.items = [arcadeShowcase]; + const shelfHasBackgroundColor = objectGraph.host.isiOS || objectGraph.client.isVision; + if (shelfHasBackgroundColor) { + shelf.background = { + type: "color", + color: arcadeShowcaseShelfBackgroundColor, + }; + } + return shelf; +} +// endregion +function contentFromModule(objectGraph, module, context) { + const contents = mediaRelationships.relationshipData(objectGraph, module, "contents"); + if (!contents) { + return null; + } + return contents; +} +function addImpressionsFieldsToModel(objectGraph, model, context, impressionData) { + if (!model) { + return; + } + let impressionType = context.module; + if (context.subStyle) { + impressionType = impressionType + "_" + context.subStyle; + } + if (serverData.isNull(impressionData)) { + impressionData = { + id: `${context.index}`, + impressionIndex: context.index, + idType: "sequential", + impressionType: impressionType, + kind: "iosModule", + }; + } + model.impressionMetrics = new models.ImpressionMetrics(metricsHelpersUtil.sanitizedMetricsDictionary(impressionData)); +} +//# sourceMappingURL=article.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/today/artwork/media-configurations/category-detail-motion-16x9.js b/node_modules/@jet-app/app-store/tmp/src/common/today/artwork/media-configurations/category-detail-motion-16x9.js new file mode 100644 index 0000000..b209cba --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/today/artwork/media-configurations/category-detail-motion-16x9.js @@ -0,0 +1,34 @@ +import { ArtworkContentMode, EdgeInsetsZero, TodayCardArtworkSizedLayoutMetricsPriority, } from "../../../../api/models"; +const categoryDetailMotion16x9Layout_mini = { + collapsedContentMode: ArtworkContentMode.bottom, + expandedContentMode: ArtworkContentMode.bottom, + collapsedLayoutInsets: EdgeInsetsZero, + expandedLayoutInsets: EdgeInsetsZero, +}; +const categoryDetailMotion16x9Layout_base = { + collapsedContentMode: ArtworkContentMode.top, + expandedContentMode: ArtworkContentMode.top, + collapsedLayoutInsets: EdgeInsetsZero, + expandedLayoutInsets: EdgeInsetsZero, +}; +const categoryDetailMotion16x9Metrics_mini = { + maxWidth: 250.0, + ltr: categoryDetailMotion16x9Layout_mini, + rtl: categoryDetailMotion16x9Layout_mini, + collapsedSize: { type: "absolute", width: 455, height: 256 }, + priority: TodayCardArtworkSizedLayoutMetricsPriority.Mini, +}; +const categoryDetailMotion16x9Metrics_base = { + ltr: categoryDetailMotion16x9Layout_base, + rtl: categoryDetailMotion16x9Layout_base, + collapsedSize: { type: "fractionalHeight", height: 1.0 }, +}; +export const categoryDetailMotion16x9 = { + objectPath: "editorialVideo.categoryDetailMotion16x9", + cardArtLayoutMetrics: [categoryDetailMotion16x9Metrics_mini, categoryDetailMotion16x9Metrics_base], + sourceWidth: 800, + sourceHeight: 450, + type: "video", + crops: ["sr"], +}; +//# sourceMappingURL=category-detail-motion-16x9.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/today/artwork/media-configurations/category-detail-static-16x9.js b/node_modules/@jet-app/app-store/tmp/src/common/today/artwork/media-configurations/category-detail-static-16x9.js new file mode 100644 index 0000000..ab3c57f --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/today/artwork/media-configurations/category-detail-static-16x9.js @@ -0,0 +1,49 @@ +import { ArtworkContentMode, EdgeInsetsZero, TodayCardArtworkSizedLayoutMetricsPriority, } from "../../../../api/models"; +const categoryDetailStatic16x9Layout_mini = { + collapsedContentMode: ArtworkContentMode.bottom, + expandedContentMode: ArtworkContentMode.bottom, + collapsedLayoutInsets: EdgeInsetsZero, + expandedLayoutInsets: EdgeInsetsZero, +}; +const categoryDetailStatic16x9Layout_base = { + collapsedContentMode: ArtworkContentMode.bottom, + expandedContentMode: ArtworkContentMode.bottom, + collapsedLayoutInsets: EdgeInsetsZero, + expandedLayoutInsets: EdgeInsetsZero, +}; +const categoryDetailStatic16x9Layout_700w = { + collapsedContentMode: ArtworkContentMode.scaleAspectFill, + expandedContentMode: ArtworkContentMode.bottom, + collapsedLayoutInsets: EdgeInsetsZero, + expandedLayoutInsets: EdgeInsetsZero, +}; +const categoryDetailStatic16x9Metrics_mini = { + maxWidth: 250.0, + ltr: categoryDetailStatic16x9Layout_mini, + rtl: categoryDetailStatic16x9Layout_mini, + collapsedSize: { type: "absolute", width: 455, height: 256 }, + priority: TodayCardArtworkSizedLayoutMetricsPriority.Mini, +}; +const categoryDetailStatic16x9Metrics_base = { + maxWidth: 704, + ltr: categoryDetailStatic16x9Layout_base, + rtl: categoryDetailStatic16x9Layout_base, + collapsedSize: { type: "fractionalHeight", height: 1.08 }, +}; +const categoryDetailStatic16x9Metrics_700w = { + ltr: categoryDetailStatic16x9Layout_700w, + rtl: categoryDetailStatic16x9Layout_700w, +}; +export const categoryDetailStatic16x9 = { + objectPath: "editorialArtwork.categoryDetailStatic16x9", + cardArtLayoutMetrics: [ + categoryDetailStatic16x9Metrics_mini, + categoryDetailStatic16x9Metrics_base, + categoryDetailStatic16x9Metrics_700w, + ], + crops: ["sr"], + sourceWidth: 800, + sourceHeight: 450, + type: "image", +}; +//# sourceMappingURL=category-detail-static-16x9.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/today/artwork/media-configurations/day-card.js b/node_modules/@jet-app/app-store/tmp/src/common/today/artwork/media-configurations/day-card.js new file mode 100644 index 0000000..4688784 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/today/artwork/media-configurations/day-card.js @@ -0,0 +1,51 @@ +import { ArtworkContentMode, EdgeInsetsZero, TodayCardArtworkSizedLayoutMetricsPriority, } from "../../../../api/models"; +const dayCardLayout_mini = { + collapsedContentMode: ArtworkContentMode.bottomLeft, + expandedContentMode: ArtworkContentMode.bottomLeft, + collapsedLayoutInsets: { + top: 0, + left: -8, + bottom: 0, + right: 0, + }, + expandedLayoutInsets: EdgeInsetsZero, +}; +const dayCardLayout_base = { + collapsedContentMode: ArtworkContentMode.bottomLeft, + expandedContentMode: ArtworkContentMode.bottomLeft, + collapsedLayoutInsets: EdgeInsetsZero, + expandedLayoutInsets: EdgeInsetsZero, +}; +const dayCardLayout_700w = { + collapsedContentMode: ArtworkContentMode.topLeft, + expandedContentMode: ArtworkContentMode.bottomLeft, + collapsedLayoutInsets: EdgeInsetsZero, + expandedLayoutInsets: EdgeInsetsZero, +}; +const dayCardMetrics_mini = { + maxWidth: 250, + ltr: dayCardLayout_mini, + rtl: dayCardLayout_mini, + collapsedSize: { type: "fractionalHeight", height: 1.0 }, + priority: TodayCardArtworkSizedLayoutMetricsPriority.Mini, +}; +const dayCardMetrics_base = { + maxWidth: 704, + ltr: dayCardLayout_base, + rtl: dayCardLayout_base, + collapsedSize: { type: "fractionalHeight", height: 1.062 }, +}; +const dayCardMetrics_700w = { + ltr: dayCardLayout_700w, + rtl: dayCardLayout_700w, + collapsedSize: { type: "fractionalWidth", width: 1.0 }, +}; +export const dayCard = { + objectPath: "editorialArtwork.dayCard", + cardArtLayoutMetrics: [dayCardMetrics_mini, dayCardMetrics_base, dayCardMetrics_700w], + crops: ["sr"], + sourceWidth: 800, + sourceHeight: 490, + type: "image", +}; +//# sourceMappingURL=day-card.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/today/artwork/media-configurations/event-card.js b/node_modules/@jet-app/app-store/tmp/src/common/today/artwork/media-configurations/event-card.js new file mode 100644 index 0000000..3317d1b --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/today/artwork/media-configurations/event-card.js @@ -0,0 +1,51 @@ +import { ArtworkContentMode, EdgeInsetsZero, TodayCardArtworkSizedLayoutMetricsPriority, } from "../../../../api/models"; +const eventCardLayout_mini = { + collapsedContentMode: ArtworkContentMode.bottomLeft, + collapsedLayoutInsets: { + top: 0, + left: -8, + bottom: 0, + right: 0, + }, + expandedContentMode: ArtworkContentMode.bottomLeft, + expandedLayoutInsets: EdgeInsetsZero, +}; +const eventCardLayout_base = { + collapsedContentMode: ArtworkContentMode.bottomLeft, + collapsedLayoutInsets: EdgeInsetsZero, + expandedContentMode: ArtworkContentMode.bottomLeft, + expandedLayoutInsets: EdgeInsetsZero, +}; +const eventCardLayout_700w = { + collapsedContentMode: ArtworkContentMode.scaleAspectFill, + expandedContentMode: ArtworkContentMode.bottomLeft, + collapsedLayoutInsets: EdgeInsetsZero, + expandedLayoutInsets: EdgeInsetsZero, +}; +const eventCardMetrics_mini = { + maxWidth: 250, + ltr: eventCardLayout_mini, + rtl: eventCardLayout_mini, + collapsedSize: { type: "fractionalHeight", height: 1.0 }, + priority: TodayCardArtworkSizedLayoutMetricsPriority.Mini, +}; +const eventCardMetrics_base = { + maxWidth: 704, + ltr: eventCardLayout_base, + rtl: eventCardLayout_base, + collapsedSize: { type: "fractionalHeight", height: 1.062 }, +}; +const eventCardMetrics_700w = { + ltr: eventCardLayout_700w, + rtl: eventCardLayout_700w, + collapsedSize: { type: "fractionalWidth", width: 1.0 }, +}; +export const eventCard = { + objectPath: "editorialArtwork.eventCard", + cardArtLayoutMetrics: [eventCardMetrics_mini, eventCardMetrics_base, eventCardMetrics_700w], + crops: ["sr"], + sourceWidth: 800, + sourceHeight: 490, + type: "image", +}; +//# sourceMappingURL=event-card.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/today/artwork/media-configurations/general-card.js b/node_modules/@jet-app/app-store/tmp/src/common/today/artwork/media-configurations/general-card.js new file mode 100644 index 0000000..f50840f --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/today/artwork/media-configurations/general-card.js @@ -0,0 +1,51 @@ +import { ArtworkContentMode, EdgeInsetsZero, TodayCardArtworkSizedLayoutMetricsPriority, } from "../../../../api/models"; +const generalCardLayout_mini = { + collapsedContentMode: ArtworkContentMode.bottomLeft, + expandedContentMode: ArtworkContentMode.bottomLeft, + collapsedLayoutInsets: { + top: 0, + left: -8, + bottom: 0, + right: 0, + }, + expandedLayoutInsets: EdgeInsetsZero, +}; +const generalCardLayout_base = { + collapsedContentMode: ArtworkContentMode.bottomLeft, + expandedContentMode: ArtworkContentMode.bottomLeft, + collapsedLayoutInsets: EdgeInsetsZero, + expandedLayoutInsets: EdgeInsetsZero, +}; +const generalCardLayout_700w = { + collapsedContentMode: ArtworkContentMode.scaleAspectFill, + expandedContentMode: ArtworkContentMode.bottomLeft, + collapsedLayoutInsets: EdgeInsetsZero, + expandedLayoutInsets: EdgeInsetsZero, +}; +const generalCardMetrics_mini = { + maxWidth: 250, + ltr: generalCardLayout_mini, + rtl: generalCardLayout_mini, + collapsedSize: { type: "fractionalHeight", height: 1.0 }, + priority: TodayCardArtworkSizedLayoutMetricsPriority.Mini, +}; +const generalCardMetrics_base = { + maxWidth: 704, + ltr: generalCardLayout_base, + rtl: generalCardLayout_base, + collapsedSize: { type: "fractionalHeight", height: 1.062 }, +}; +const generalCardMetrics_700w = { + ltr: generalCardLayout_700w, + rtl: generalCardLayout_700w, + collapsedSize: { type: "fractionalWidth", width: 1.0 }, +}; +export const generalCard = { + objectPath: "editorialArtwork.generalCard", + cardArtLayoutMetrics: [generalCardMetrics_mini, generalCardMetrics_base, generalCardMetrics_700w], + crops: ["MC.ApSCFB01"], + sourceWidth: 800, + sourceHeight: 490, + type: "image", +}; +//# sourceMappingURL=general-card.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/today/artwork/media-configurations/index.js b/node_modules/@jet-app/app-store/tmp/src/common/today/artwork/media-configurations/index.js new file mode 100644 index 0000000..5efa2cb --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/today/artwork/media-configurations/index.js @@ -0,0 +1,85 @@ +import { isSome } from "@jet/environment"; +import { dayCard } from "./day-card"; +import { eventCard } from "./event-card"; +import { generalCard } from "./general-card"; +import { listCardMotion21x9 } from "./list-card-motion-21x9"; +import { listCardStatic21x9 } from "./list-card-static-21x9"; +import { mediaCard } from "./media-card"; +import { storeFrontVideo } from "./store-front-video"; +import { storeFrontVideo4x3 } from "./store-front-video-4x3"; +import { storyCardMotion16x9 } from "./story-card-motion-16x9"; +import { storyCardStatic16x9 } from "./story-card-static-16x9"; +import { storyCenteredMotion16x9 } from "./story-centered-motion-16x9"; +import { storyCenteredStatic16x9 } from "./story-centered-static-16x9"; +import { storySearchStatic16x9 } from "./story-search-static-16x9"; +import { universalAMotion16x9 } from "./universal-a-motion-16x9"; +import { universalAStatic16x9 } from "./universal-a-static-16x9"; +import { categoryDetailMotion16x9 } from "./category-detail-motion-16x9"; +import { categoryDetailStatic16x9 } from "./category-detail-static-16x9"; +/** + * Hero position on iPad requires the new artwork flavor to be considered valid + */ +export function universalAConfigurations(objectGraph, isHeroPosition) { + return isHeroPosition + ? [universalAMotion16x9Configuration(objectGraph), universalAStatic16x9Configuration(objectGraph)] + : []; +} +export function dayCardConfiguration(objectGraph) { + return dayCard; +} +export function eventCardConfiguration(objectGraph) { + return eventCard; +} +export function generalCardConfiguration(objectGraph, cropCodeOverrides) { + return applyOverrides(generalCard, cropCodeOverrides); +} +export function listCardMotion21x9Configuration(objectGraph) { + return listCardMotion21x9; +} +export function listCardStatic21x9Configuration(objectGraph) { + return listCardStatic21x9; +} +export function mediaCardConfiguration(objectGraph, cropCodeOverrides) { + return applyOverrides(mediaCard, cropCodeOverrides); +} +export function storeFrontVideoConfiguration(objectGraph) { + return storeFrontVideo; +} +export function storeFrontVideo4x3Configuration(objectGraph) { + return storeFrontVideo4x3; +} +export function storyCardMotion16x9Configuration(objectGraph) { + return storyCardMotion16x9; +} +export function storyCardStatic16x9Configuration(objectGraph) { + return storyCardStatic16x9; +} +export function storyCenteredMotion16x9Configuration(objectGraph) { + return storyCenteredMotion16x9; +} +export function storyCenteredStatic16x9Configuration(objectGraph, cropCodeOverrides) { + return applyOverrides(storyCenteredStatic16x9, cropCodeOverrides); +} +export function categoryDetailMotion16x9Configuration(objectGraph) { + return categoryDetailMotion16x9; +} +export function categoryDetailStatic16x9Configuration(objectGraph, cropCodeOverrides) { + return applyOverrides(categoryDetailStatic16x9, cropCodeOverrides); +} +export function storySearchStatic16x9Configuration(objectGraph) { + return storySearchStatic16x9; +} +function universalAMotion16x9Configuration(objectGraph) { + return universalAMotion16x9; +} +function universalAStatic16x9Configuration(objectGraph) { + return universalAStatic16x9; +} +function applyOverrides(mediaConfiguration, cropCodeOverrides) { + const updatedMediaConfiguration = { ...mediaConfiguration }; + if (isSome(cropCodeOverrides)) { + updatedMediaConfiguration.crops = cropCodeOverrides; + } + return updatedMediaConfiguration; +} +//# sourceMappingURL=index.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/today/artwork/media-configurations/list-card-motion-21x9.js b/node_modules/@jet-app/app-store/tmp/src/common/today/artwork/media-configurations/list-card-motion-21x9.js new file mode 100644 index 0000000..8570df2 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/today/artwork/media-configurations/list-card-motion-21x9.js @@ -0,0 +1,116 @@ +import { ArtworkContentMode, TodayCardArtworkSizedLayoutMetricsPriority, } from "../../../../api/models"; +const expandedContentMode = ArtworkContentMode.topRight; +const expandedLayoutInsets = { + top: -108, + left: 495, + bottom: 0, + right: 0, +}; +const expandedContentMode_rtl = ArtworkContentMode.topLeft; +const expandedLayoutInsets_rtl = { + top: -108, + left: -495, + bottom: 0, + right: 0, +}; +const expandedSize = { type: "absolute", width: 1124, height: 482 }; +const listCardMotion16x9Layout_mini = { + collapsedContentMode: ArtworkContentMode.top, + collapsedLayoutInsets: { + top: -117, + left: 0, + bottom: 0, + right: 0, + }, + expandedContentMode, + expandedLayoutInsets, +}; +const listCardMotion16x9Layout_mini_rtl = { + collapsedContentMode: ArtworkContentMode.top, + collapsedLayoutInsets: { + top: -117, + left: 0, + bottom: 0, + right: 0, + }, + expandedContentMode: expandedContentMode_rtl, + expandedLayoutInsets: expandedLayoutInsets_rtl, +}; +const listCardMotion16x9Layout_base = { + collapsedContentMode: ArtworkContentMode.topRight, + collapsedLayoutInsets: { + top: -165, + left: 524, + bottom: 0, + right: 0, + }, + expandedContentMode, + expandedLayoutInsets, +}; +const listCardMotion16x9Layout_base_rtl = { + collapsedContentMode: ArtworkContentMode.topLeft, + collapsedLayoutInsets: { + top: -165, + left: -524, + bottom: 0, + right: 0, + }, + expandedContentMode: expandedContentMode_rtl, + expandedLayoutInsets: expandedLayoutInsets_rtl, +}; +const listCardMotion16x9Layout_extraWide = { + collapsedContentMode: ArtworkContentMode.top, + collapsedLayoutInsets: { + top: -55, + left: 0, + bottom: 0, + right: 0, + }, + expandedContentMode, + expandedLayoutInsets, +}; +const listCardMotion16x9Layout_extraWide_rtl = { + collapsedContentMode: ArtworkContentMode.top, + collapsedLayoutInsets: { + top: -55, + left: 0, + bottom: 0, + right: 0, + }, + expandedContentMode: expandedContentMode_rtl, + expandedLayoutInsets: expandedLayoutInsets_rtl, +}; +const listCardMotion16x9Metrics_mini = { + maxWidth: 250.0, + ltr: listCardMotion16x9Layout_mini, + rtl: listCardMotion16x9Layout_mini_rtl, + collapsedSize: { type: "absolute", width: 914, height: 392 }, + expandedSize, + priority: TodayCardArtworkSizedLayoutMetricsPriority.Mini, +}; +const listCardMotion16x9Metrics_base = { + ltr: listCardMotion16x9Layout_base, + rtl: listCardMotion16x9Layout_base_rtl, + collapsedSize: { type: "absolute", width: 1188, height: 509 }, + expandedSize, +}; +const listCardMotion16x9Metrics_extraWide = { + ltr: listCardMotion16x9Layout_extraWide, + rtl: listCardMotion16x9Layout_extraWide_rtl, + collapsedSize: { type: "absolute", width: 1456, height: 624 }, + expandedSize, + priority: TodayCardArtworkSizedLayoutMetricsPriority.ExtraWide, +}; +export const listCardMotion21x9 = { + objectPath: "editorialVideo.listCardMotion21x9", + cardArtLayoutMetrics: [ + listCardMotion16x9Metrics_mini, + listCardMotion16x9Metrics_base, + listCardMotion16x9Metrics_extraWide, + ], + crops: [], + sourceWidth: 1208, + sourceHeight: 518, + type: "video", +}; +//# sourceMappingURL=list-card-motion-21x9.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/today/artwork/media-configurations/list-card-static-21x9.js b/node_modules/@jet-app/app-store/tmp/src/common/today/artwork/media-configurations/list-card-static-21x9.js new file mode 100644 index 0000000..8adecbe --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/today/artwork/media-configurations/list-card-static-21x9.js @@ -0,0 +1,92 @@ +import { ArtworkContentMode, EdgeInsetsZero, Size, TodayCardArtworkSizedLayoutMetricsPriority, } from "../../../../api/models"; +const listCardStatic21x9Layout_mini = { + collapsedContentMode: ArtworkContentMode.bottomRight, + collapsedLayoutInsets: EdgeInsetsZero, + expandedContentMode: ArtworkContentMode.topRight, + expandedLayoutInsets: EdgeInsetsZero, +}; +const listCardStatic21x9Layout_mini_rtl = { + collapsedContentMode: ArtworkContentMode.bottomLeft, + collapsedLayoutInsets: EdgeInsetsZero, + expandedContentMode: ArtworkContentMode.topLeft, + expandedLayoutInsets: EdgeInsetsZero, +}; +const listCardStatic21x9Layout_base = { + collapsedContentMode: ArtworkContentMode.topRight, + collapsedLayoutInsets: { + top: -49, + left: 0, + bottom: 0, + right: 0, + }, + expandedContentMode: ArtworkContentMode.right, + expandedLayoutInsets: EdgeInsetsZero, +}; +const listCardStatic21x9Layout_base_rtl = { + collapsedContentMode: ArtworkContentMode.topLeft, + collapsedLayoutInsets: { + top: -49, + left: 0, + bottom: 0, + right: 0, + }, + expandedContentMode: ArtworkContentMode.left, + expandedLayoutInsets: EdgeInsetsZero, +}; +const listCardStatic16x9Layout_extraWide = { + collapsedContentMode: ArtworkContentMode.scaleAspectFill, + collapsedLayoutInsets: EdgeInsetsZero, + expandedContentMode: ArtworkContentMode.bottomRight, + expandedLayoutInsets: { + top: 50, + left: 495, + bottom: 0, + right: 0, + }, +}; +const listCardStatic16x9Layout_extraWide_rtl = { + collapsedContentMode: ArtworkContentMode.scaleAspectFill, + collapsedLayoutInsets: EdgeInsetsZero, + expandedContentMode: ArtworkContentMode.bottomRight, + expandedLayoutInsets: { + top: 50, + left: -495, + bottom: 0, + right: 0, + }, +}; +const listCardStatic21x9Metrics_mini = { + maxWidth: 250.0, + ltr: listCardStatic21x9Layout_mini, + rtl: listCardStatic21x9Layout_mini_rtl, + sourceCropOverrideLTR: "LCS.ApLCS01", + sourceCropOverrideRTL: "LCS.ApLCS02", + sourceSizeOverride: new Size(550, 264), + priority: TodayCardArtworkSizedLayoutMetricsPriority.Mini, +}; +const listCardStatic21x9Metrics_base = { + ltr: listCardStatic21x9Layout_base, + rtl: listCardStatic21x9Layout_base_rtl, + collapsedSize: { type: "absolute", width: 672, height: 279 }, +}; +const listCardMotion16x9Metrics_extraWide = { + ltr: listCardStatic16x9Layout_extraWide, + rtl: listCardStatic16x9Layout_extraWide_rtl, + expandedSize: { type: "absolute", width: 1124, height: 482 }, + sourceCropOverrideLTR: "LCS.ApLCXW01", + sourceCropOverrideRTL: "LCS.ApLCXW01", + priority: TodayCardArtworkSizedLayoutMetricsPriority.ExtraWide, +}; +export const listCardStatic21x9 = { + objectPath: "editorialArtwork.listCardStatic21x9", + cardArtLayoutMetrics: [ + listCardStatic21x9Metrics_mini, + listCardStatic21x9Metrics_base, + listCardMotion16x9Metrics_extraWide, + ], + crops: ["LCS.ApLCL01", "LCS.ApLCL02"], + sourceWidth: 688, + sourceHeight: 286, + type: "image", +}; +//# sourceMappingURL=list-card-static-21x9.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/today/artwork/media-configurations/media-card.js b/node_modules/@jet-app/app-store/tmp/src/common/today/artwork/media-configurations/media-card.js new file mode 100644 index 0000000..3b8ff21 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/today/artwork/media-configurations/media-card.js @@ -0,0 +1,51 @@ +import { ArtworkContentMode, EdgeInsetsZero, TodayCardArtworkSizedLayoutMetricsPriority, } from "../../../../api/models"; +const mediaCardLayout_mini = { + collapsedContentMode: ArtworkContentMode.bottomLeft, + collapsedLayoutInsets: { + top: 0, + left: -8, + bottom: 0, + right: 0, + }, + expandedContentMode: ArtworkContentMode.bottomLeft, + expandedLayoutInsets: EdgeInsetsZero, +}; +const mediaCardLayout_base = { + collapsedContentMode: ArtworkContentMode.bottomLeft, + collapsedLayoutInsets: EdgeInsetsZero, + expandedContentMode: ArtworkContentMode.bottomLeft, + expandedLayoutInsets: EdgeInsetsZero, +}; +const mediaCardLayout_700w = { + collapsedContentMode: ArtworkContentMode.scaleAspectFill, + expandedContentMode: ArtworkContentMode.bottomLeft, + collapsedLayoutInsets: EdgeInsetsZero, + expandedLayoutInsets: EdgeInsetsZero, +}; +const mediaCardMetrics_mini = { + maxWidth: 250, + ltr: mediaCardLayout_mini, + rtl: mediaCardLayout_mini, + collapsedSize: { type: "fractionalHeight", height: 1.0 }, + priority: TodayCardArtworkSizedLayoutMetricsPriority.Mini, +}; +const mediaCardMetrics_base = { + maxWidth: 704, + ltr: mediaCardLayout_base, + rtl: mediaCardLayout_base, + collapsedSize: { type: "fractionalHeight", height: 1.062 }, +}; +const mediaCardMetrics_700w = { + ltr: mediaCardLayout_700w, + rtl: mediaCardLayout_700w, + collapsedSize: { type: "fractionalWidth", width: 1.0 }, +}; +export const mediaCard = { + objectPath: "editorialArtwork.mediaCard", + cardArtLayoutMetrics: [mediaCardMetrics_mini, mediaCardMetrics_base, mediaCardMetrics_700w], + crops: ["MC.ApSCFB01"], + sourceWidth: 800, + sourceHeight: 490, + type: "image", +}; +//# sourceMappingURL=media-card.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/today/artwork/media-configurations/store-front-video-4x3.js b/node_modules/@jet-app/app-store/tmp/src/common/today/artwork/media-configurations/store-front-video-4x3.js new file mode 100644 index 0000000..653df0e --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/today/artwork/media-configurations/store-front-video-4x3.js @@ -0,0 +1,22 @@ +import { ArtworkContentMode, EdgeInsetsZero } from "../../../../api/models"; +const storeFrontVideo4x3Layout = { + collapsedContentMode: ArtworkContentMode.center, + collapsedLayoutInsets: EdgeInsetsZero, + expandedContentMode: ArtworkContentMode.center, + expandedLayoutInsets: EdgeInsetsZero, +}; +const storeFrontVideo4x3Metrics = { + ltr: storeFrontVideo4x3Layout, + rtl: storeFrontVideo4x3Layout, + collapsedSize: { type: "fractionalHeight", height: 1.0 }, + expandedSize: { type: "fractionalHeight", height: 1.0 }, +}; +export const storeFrontVideo4x3 = { + objectPath: "editorialVideo.storeFrontVideo4x3", + cardArtLayoutMetrics: [storeFrontVideo4x3Metrics], + crops: [], + sourceWidth: 656, + sourceHeight: 492, + type: "video", +}; +//# sourceMappingURL=store-front-video-4x3.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/today/artwork/media-configurations/store-front-video.js b/node_modules/@jet-app/app-store/tmp/src/common/today/artwork/media-configurations/store-front-video.js new file mode 100644 index 0000000..472e52c --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/today/artwork/media-configurations/store-front-video.js @@ -0,0 +1,25 @@ +import { ArtworkContentMode, EdgeInsetsZero, } from "../../../../api/models"; +const storeFrontVideoLayout = { + collapsedContentMode: ArtworkContentMode.center, + collapsedLayoutInsets: EdgeInsetsZero, + expandedContentMode: ArtworkContentMode.center, + expandedLayoutInsets: EdgeInsetsZero, +}; +const storeFrontVideoMetrics = { + ltr: storeFrontVideoLayout, + rtl: storeFrontVideoLayout, + collapsedSize: { type: "fractionalHeight", height: 1.0 }, + expandedSize: { type: "fractionalHeight", height: 1.0 }, +}; +/** + * This configuration is in 16x9 aspect ratio. + */ +export const storeFrontVideo = { + objectPath: "editorialVideo.storeFrontVideo", + cardArtLayoutMetrics: [storeFrontVideoMetrics], + crops: [], + sourceWidth: 875, + sourceHeight: 492, + type: "video", +}; +//# sourceMappingURL=store-front-video.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/today/artwork/media-configurations/story-card-motion-16x9.js b/node_modules/@jet-app/app-store/tmp/src/common/today/artwork/media-configurations/story-card-motion-16x9.js new file mode 100644 index 0000000..b0e4530 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/today/artwork/media-configurations/story-card-motion-16x9.js @@ -0,0 +1,34 @@ +import { ArtworkContentMode, EdgeInsetsZero, TodayCardArtworkSizedLayoutMetricsPriority, } from "../../../../api/models"; +const storyCardMotion16x9Layout_mini = { + collapsedContentMode: ArtworkContentMode.bottom, + expandedContentMode: ArtworkContentMode.bottom, + collapsedLayoutInsets: EdgeInsetsZero, + expandedLayoutInsets: EdgeInsetsZero, +}; +const storyCardMotion16x9Layout_base = { + collapsedContentMode: ArtworkContentMode.top, + expandedContentMode: ArtworkContentMode.top, + collapsedLayoutInsets: EdgeInsetsZero, + expandedLayoutInsets: EdgeInsetsZero, +}; +const storyCardMotion16x9Metrics_mini = { + maxWidth: 250.0, + ltr: storyCardMotion16x9Layout_mini, + rtl: storyCardMotion16x9Layout_mini, + collapsedSize: { type: "absolute", width: 455, height: 256 }, + priority: TodayCardArtworkSizedLayoutMetricsPriority.Mini, +}; +const storyCardMotion16x9Metrics_base = { + ltr: storyCardMotion16x9Layout_base, + rtl: storyCardMotion16x9Layout_base, + collapsedSize: { type: "fractionalHeight", height: 1.0 }, +}; +export const storyCardMotion16x9 = { + objectPath: "editorialVideo.storyCardMotion16x9", + cardArtLayoutMetrics: [storyCardMotion16x9Metrics_mini, storyCardMotion16x9Metrics_base], + sourceWidth: 800, + sourceHeight: 450, + type: "video", + crops: ["sr"], +}; +//# sourceMappingURL=story-card-motion-16x9.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/today/artwork/media-configurations/story-card-static-16x9.js b/node_modules/@jet-app/app-store/tmp/src/common/today/artwork/media-configurations/story-card-static-16x9.js new file mode 100644 index 0000000..c4e7220 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/today/artwork/media-configurations/story-card-static-16x9.js @@ -0,0 +1,49 @@ +import { ArtworkContentMode, EdgeInsetsZero, TodayCardArtworkSizedLayoutMetricsPriority, } from "../../../../api/models"; +const storyCardStatic16x9Layout_mini = { + collapsedContentMode: ArtworkContentMode.bottom, + expandedContentMode: ArtworkContentMode.bottom, + collapsedLayoutInsets: EdgeInsetsZero, + expandedLayoutInsets: EdgeInsetsZero, +}; +const storyCardStatic16x9Layout_base = { + collapsedContentMode: ArtworkContentMode.bottom, + expandedContentMode: ArtworkContentMode.bottom, + collapsedLayoutInsets: EdgeInsetsZero, + expandedLayoutInsets: EdgeInsetsZero, +}; +const storyCardStatic16x9Layout_700w = { + collapsedContentMode: ArtworkContentMode.scaleAspectFill, + expandedContentMode: ArtworkContentMode.bottom, + collapsedLayoutInsets: EdgeInsetsZero, + expandedLayoutInsets: EdgeInsetsZero, +}; +const storyCardStatic16x9Metrics_mini = { + maxWidth: 250.0, + ltr: storyCardStatic16x9Layout_mini, + rtl: storyCardStatic16x9Layout_mini, + collapsedSize: { type: "absolute", width: 455, height: 256 }, + priority: TodayCardArtworkSizedLayoutMetricsPriority.Mini, +}; +const storyCardStatic16x9Metrics_base = { + maxWidth: 704, + ltr: storyCardStatic16x9Layout_base, + rtl: storyCardStatic16x9Layout_base, + collapsedSize: { type: "fractionalHeight", height: 1.08 }, +}; +const storyCardStatic16x9Metrics_700w = { + ltr: storyCardStatic16x9Layout_700w, + rtl: storyCardStatic16x9Layout_700w, +}; +export const storyCardStatic16x9 = { + objectPath: "editorialArtwork.storyCardStatic16x9", + cardArtLayoutMetrics: [ + storyCardStatic16x9Metrics_mini, + storyCardStatic16x9Metrics_base, + storyCardStatic16x9Metrics_700w, + ], + crops: ["sr"], + sourceWidth: 800, + sourceHeight: 450, + type: "image", +}; +//# sourceMappingURL=story-card-static-16x9.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/today/artwork/media-configurations/story-centered-motion-16x9.js b/node_modules/@jet-app/app-store/tmp/src/common/today/artwork/media-configurations/story-centered-motion-16x9.js new file mode 100644 index 0000000..273503b --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/today/artwork/media-configurations/story-centered-motion-16x9.js @@ -0,0 +1,34 @@ +import { ArtworkContentMode, EdgeInsetsZero, TodayCardArtworkSizedLayoutMetricsPriority, } from "../../../../api/models"; +const storyCenteredMotion16x9Layout_mini = { + collapsedContentMode: ArtworkContentMode.bottom, + expandedContentMode: ArtworkContentMode.bottom, + collapsedLayoutInsets: EdgeInsetsZero, + expandedLayoutInsets: EdgeInsetsZero, +}; +const storyCenteredMotion16x9Layout_base = { + collapsedContentMode: ArtworkContentMode.top, + expandedContentMode: ArtworkContentMode.top, + collapsedLayoutInsets: EdgeInsetsZero, + expandedLayoutInsets: EdgeInsetsZero, +}; +const storyCenteredMotion16x9Metrics_mini = { + maxWidth: 250.0, + ltr: storyCenteredMotion16x9Layout_mini, + rtl: storyCenteredMotion16x9Layout_mini, + collapsedSize: { type: "absolute", width: 455, height: 256 }, + priority: TodayCardArtworkSizedLayoutMetricsPriority.Mini, +}; +const storyCenteredMotion16x9Metrics_base = { + ltr: storyCenteredMotion16x9Layout_base, + rtl: storyCenteredMotion16x9Layout_base, + collapsedSize: { type: "fractionalHeight", height: 1.0 }, +}; +export const storyCenteredMotion16x9 = { + objectPath: "editorialVideo.storyCenteredMotion16x9", + cardArtLayoutMetrics: [storyCenteredMotion16x9Metrics_mini, storyCenteredMotion16x9Metrics_base], + sourceWidth: 800, + sourceHeight: 450, + type: "video", + crops: ["sr"], +}; +//# sourceMappingURL=story-centered-motion-16x9.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/today/artwork/media-configurations/story-centered-static-16x9.js b/node_modules/@jet-app/app-store/tmp/src/common/today/artwork/media-configurations/story-centered-static-16x9.js new file mode 100644 index 0000000..404d58c --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/today/artwork/media-configurations/story-centered-static-16x9.js @@ -0,0 +1,49 @@ +import { ArtworkContentMode, EdgeInsetsZero, TodayCardArtworkSizedLayoutMetricsPriority, } from "../../../../api/models"; +const storyCenteredStatic16x9Layout_mini = { + collapsedContentMode: ArtworkContentMode.bottom, + expandedContentMode: ArtworkContentMode.bottom, + collapsedLayoutInsets: EdgeInsetsZero, + expandedLayoutInsets: EdgeInsetsZero, +}; +const storyCenteredStatic16x9Layout_base = { + collapsedContentMode: ArtworkContentMode.bottom, + expandedContentMode: ArtworkContentMode.bottom, + collapsedLayoutInsets: EdgeInsetsZero, + expandedLayoutInsets: EdgeInsetsZero, +}; +const storyCenteredStatic16x9Layout_700w = { + collapsedContentMode: ArtworkContentMode.scaleAspectFill, + expandedContentMode: ArtworkContentMode.bottom, + collapsedLayoutInsets: EdgeInsetsZero, + expandedLayoutInsets: EdgeInsetsZero, +}; +const storyCenteredStatic16x9Metrics_mini = { + maxWidth: 250.0, + ltr: storyCenteredStatic16x9Layout_mini, + rtl: storyCenteredStatic16x9Layout_mini, + collapsedSize: { type: "absolute", width: 455, height: 256 }, + priority: TodayCardArtworkSizedLayoutMetricsPriority.Mini, +}; +const storyCenteredStatic16x9Metrics_base = { + maxWidth: 704, + ltr: storyCenteredStatic16x9Layout_base, + rtl: storyCenteredStatic16x9Layout_base, + collapsedSize: { type: "fractionalHeight", height: 1.08 }, +}; +const storyCenteredStatic16x9Metrics_700w = { + ltr: storyCenteredStatic16x9Layout_700w, + rtl: storyCenteredStatic16x9Layout_700w, +}; +export const storyCenteredStatic16x9 = { + objectPath: "editorialArtwork.storyCenteredStatic16x9", + cardArtLayoutMetrics: [ + storyCenteredStatic16x9Metrics_mini, + storyCenteredStatic16x9Metrics_base, + storyCenteredStatic16x9Metrics_700w, + ], + crops: ["sr"], + sourceWidth: 800, + sourceHeight: 450, + type: "image", +}; +//# sourceMappingURL=story-centered-static-16x9.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/today/artwork/media-configurations/universal-a-motion-16x9.js b/node_modules/@jet-app/app-store/tmp/src/common/today/artwork/media-configurations/universal-a-motion-16x9.js new file mode 100644 index 0000000..f16d51f --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/today/artwork/media-configurations/universal-a-motion-16x9.js @@ -0,0 +1,146 @@ +import { ArtworkContentMode, TodayCardArtworkSizedLayoutMetricsPriority, } from "../../../../api/models"; +const expandedSize = { type: "absolute", width: 1200, height: 675 }; +const expandedLayoutInsets = { + top: -130, + left: 0, + bottom: 0, + right: 0, +}; +const universalAMotion16x9Layout_mini = { + collapsedContentMode: ArtworkContentMode.top, + collapsedLayoutInsets: { + top: -16, + left: 0, + bottom: 0, + right: 0, + }, + expandedContentMode: ArtworkContentMode.top, + expandedLayoutInsets, +}; +const universalAMotion16x9Layout_base = { + collapsedContentMode: ArtworkContentMode.top, + collapsedLayoutInsets: { + top: -116, + left: 0, + bottom: 0, + right: 0, + }, + expandedContentMode: ArtworkContentMode.top, + expandedLayoutInsets, +}; +const universalAMotion16x9Layout_700w = { + collapsedContentMode: ArtworkContentMode.topLeft, + collapsedLayoutInsets: { + top: -116, + left: -70, + bottom: 0, + right: 0, + }, + expandedContentMode: ArtworkContentMode.top, + expandedLayoutInsets, +}; +const universalAMotion16x9Layout_700w_rtl = { + collapsedContentMode: ArtworkContentMode.topRight, + collapsedLayoutInsets: { + top: -116, + left: 70, + bottom: 0, + right: 0, + }, + expandedContentMode: ArtworkContentMode.top, + expandedLayoutInsets, +}; +const universalAMotion16x9Layout_920w = { + collapsedContentMode: ArtworkContentMode.topLeft, + collapsedLayoutInsets: { + top: -148, + left: 0, + bottom: 0, + right: 0, + }, + expandedContentMode: ArtworkContentMode.top, + expandedLayoutInsets, +}; +const universalAMotion16x9Layout_920w_rtl = { + collapsedContentMode: ArtworkContentMode.topRight, + collapsedLayoutInsets: { + top: -148, + left: 0, + bottom: 0, + right: 0, + }, + expandedContentMode: ArtworkContentMode.top, + expandedLayoutInsets, +}; +const universalAMotion16x9Layout_max = { + collapsedContentMode: ArtworkContentMode.topLeft, + collapsedLayoutInsets: { + top: -170, + left: 0, + bottom: 0, + right: 0, + }, + expandedContentMode: ArtworkContentMode.top, + expandedLayoutInsets, +}; +const universalAMotion16x9Layout_max_rtl = { + collapsedContentMode: ArtworkContentMode.topRight, + collapsedLayoutInsets: { + top: -170, + left: 0, + bottom: 0, + right: 0, + }, + expandedContentMode: ArtworkContentMode.top, + expandedLayoutInsets, +}; +const universalAMotion16x9Metrics_mini = { + maxWidth: 250.0, + ltr: universalAMotion16x9Layout_mini, + rtl: universalAMotion16x9Layout_mini, + collapsedSize: { type: "absolute", width: 455, height: 256 }, + expandedSize, + priority: TodayCardArtworkSizedLayoutMetricsPriority.Mini, +}; +const universalAMotion16x9Metrics_base = { + maxWidth: 699.0, + ltr: universalAMotion16x9Layout_base, + rtl: universalAMotion16x9Layout_base, + collapsedSize: { type: "absolute", width: 1094, height: 614 }, + expandedSize, +}; +const universalAMotion16x9Metrics_700w = { + maxWidth: 899.0, + ltr: universalAMotion16x9Layout_700w, + rtl: universalAMotion16x9Layout_700w_rtl, + collapsedSize: { type: "absolute", width: 1092, height: 614 }, + expandedSize, +}; +const universalAMotion16x9Metrics_920w = { + maxWidth: 1090.0, + ltr: universalAMotion16x9Layout_920w, + rtl: universalAMotion16x9Layout_920w_rtl, + collapsedSize: { type: "absolute", width: 1392, height: 783 }, + expandedSize, +}; +const universalAMotion16x9Metrics_max = { + ltr: universalAMotion16x9Layout_max, + rtl: universalAMotion16x9Layout_max_rtl, + collapsedSize: { type: "absolute", width: 1600, height: 900 }, + expandedSize, +}; +export const universalAMotion16x9 = { + objectPath: "editorialVideo.universalAMotion16x9", + cardArtLayoutMetrics: [ + universalAMotion16x9Metrics_mini, + universalAMotion16x9Metrics_base, + universalAMotion16x9Metrics_700w, + universalAMotion16x9Metrics_920w, + universalAMotion16x9Metrics_max, + ], + crops: ["UAS.ApXWC01"], + sourceWidth: 1600, + sourceHeight: 900, + type: "video", +}; +//# sourceMappingURL=universal-a-motion-16x9.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/today/artwork/media-configurations/universal-a-static-16x9.js b/node_modules/@jet-app/app-store/tmp/src/common/today/artwork/media-configurations/universal-a-static-16x9.js new file mode 100644 index 0000000..f0d34ce --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/today/artwork/media-configurations/universal-a-static-16x9.js @@ -0,0 +1,107 @@ +import { ArtworkContentMode, EdgeInsetsZero, TodayCardArtworkSizedLayoutMetricsPriority, } from "../../../../api/models"; +const universalAStatic16x9Layout_mini = { + collapsedContentMode: ArtworkContentMode.top, + collapsedLayoutInsets: { + top: 16, + left: 0, + bottom: 0, + right: 0, + }, + expandedContentMode: ArtworkContentMode.top, + expandedLayoutInsets: EdgeInsetsZero, +}; +const universalAStatic16x9Layout_base = { + collapsedContentMode: ArtworkContentMode.scaleAspectFill, + collapsedLayoutInsets: EdgeInsetsZero, + expandedContentMode: ArtworkContentMode.scaleAspectFill, + expandedLayoutInsets: EdgeInsetsZero, +}; +const universalAStatic16x9Layout_700w = { + collapsedContentMode: ArtworkContentMode.topLeft, + collapsedLayoutInsets: { + top: 0, + left: -70, + bottom: 0, + right: 0, + }, + expandedContentMode: ArtworkContentMode.center, + expandedLayoutInsets: EdgeInsetsZero, +}; +const universalAStatic16x9Layout_700w_rtl = { + collapsedContentMode: ArtworkContentMode.topRight, + collapsedLayoutInsets: { + top: 0, + left: 70, + bottom: 0, + right: 0, + }, + expandedContentMode: ArtworkContentMode.center, + expandedLayoutInsets: EdgeInsetsZero, +}; +const universalAStatic16x9Layout_920w = { + collapsedContentMode: ArtworkContentMode.topLeft, + collapsedLayoutInsets: EdgeInsetsZero, + expandedContentMode: ArtworkContentMode.center, + expandedLayoutInsets: EdgeInsetsZero, +}; +const universalAStatic16x9Layout_920w_rtl = { + collapsedContentMode: ArtworkContentMode.topRight, + collapsedLayoutInsets: EdgeInsetsZero, + expandedContentMode: ArtworkContentMode.center, + expandedLayoutInsets: EdgeInsetsZero, +}; +const universalAStatic16x9Layout_max = { + collapsedContentMode: ArtworkContentMode.topLeft, + collapsedLayoutInsets: EdgeInsetsZero, + expandedContentMode: ArtworkContentMode.center, + expandedLayoutInsets: EdgeInsetsZero, +}; +const universalAStatic16x9Layout_max_rtl = { + collapsedContentMode: ArtworkContentMode.topRight, + collapsedLayoutInsets: EdgeInsetsZero, + expandedContentMode: ArtworkContentMode.center, + expandedLayoutInsets: EdgeInsetsZero, +}; +const universalAStatic16x9Metrics_mini = { + maxWidth: 250.0, + ltr: universalAStatic16x9Layout_mini, + rtl: universalAStatic16x9Layout_mini, + collapsedSize: { type: "absolute", width: 455, height: 256 }, + priority: TodayCardArtworkSizedLayoutMetricsPriority.Mini, +}; +const universalAStatic16x9Metrics_base = { + maxWidth: 699.0, + ltr: universalAStatic16x9Layout_base, + rtl: universalAStatic16x9Layout_base, +}; +const universalAStatic16x9Metrics_700w = { + maxWidth: 899.0, + ltr: universalAStatic16x9Layout_700w, + rtl: universalAStatic16x9Layout_700w_rtl, +}; +const universalAStatic16x9Metrics_920w = { + maxWidth: 1090.0, + ltr: universalAStatic16x9Layout_920w, + rtl: universalAStatic16x9Layout_920w_rtl, + collapsedSize: { type: "absolute", width: 1400, height: 527 }, +}; +const universalAStatic16x9Metrics_max = { + ltr: universalAStatic16x9Layout_max, + rtl: universalAStatic16x9Layout_max_rtl, + collapsedSize: { type: "absolute", width: 1600, height: 604 }, +}; +export const universalAStatic16x9 = { + objectPath: "editorialArtwork.universalAStatic16x9", + cardArtLayoutMetrics: [ + universalAStatic16x9Metrics_mini, + universalAStatic16x9Metrics_base, + universalAStatic16x9Metrics_700w, + universalAStatic16x9Metrics_920w, + universalAStatic16x9Metrics_max, + ], + crops: ["UAS.ApXWC01"], + sourceWidth: 1600, + sourceHeight: 604, + type: "image", +}; +//# sourceMappingURL=universal-a-static-16x9.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/today/artwork/today-artwork-util.js b/node_modules/@jet-app/app-store/tmp/src/common/today/artwork/today-artwork-util.js new file mode 100644 index 0000000..4705bef --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/today/artwork/today-artwork-util.js @@ -0,0 +1,267 @@ +import { isNothing, isSome } from "@jet/environment"; +import { Video } from "../../../api/models"; +import { asDictionary, asNumber, asString } from "../../../foundation/json-parsing/server-data"; +import { editorialCardFromData } from "../../../foundation/media/associations"; +import * as mediaAttributes from "../../../foundation/media/attributes"; +import { autoPlayControls, standardControls } from "../../constants/video-constants"; +import { artworkFromApiArtwork, joeColorSetFromData } from "../../content/content"; +import { cardDisplayStyleFromData } from "../today-card-util"; +import { TodayCardDisplayStyle } from "../today-types"; +import { categoryDetailMotion16x9Configuration, categoryDetailStatic16x9Configuration, dayCardConfiguration, eventCardConfiguration, generalCardConfiguration, listCardMotion21x9Configuration, listCardStatic21x9Configuration, mediaCardConfiguration, storeFrontVideo4x3Configuration, storeFrontVideoConfiguration, storyCardMotion16x9Configuration, storyCardStatic16x9Configuration, storyCenteredMotion16x9Configuration, storyCenteredStatic16x9Configuration, storySearchStatic16x9Configuration, universalAConfigurations, } from "./media-configurations"; +/** + * Retrieves the artwork details for a today card. This function will determine the + * appropriate media configuration to use based on the card display style and the + * media data available in the card response. + * + * @param objectGraph - The object graph of the app store. + * @param data - The media data. + * @param cardConfig - The configuration for the today card. + * @returns The artwork details for the today card, or undefined if no artwork is available. + */ +export function todayCardArtworkDetails(objectGraph, data, cardConfig) { + var _a; + // Watch uses different artwork lookup logic + const isWebViewingWatch = objectGraph.client.isWeb && ((_a = objectGraph.activeIntent) === null || _a === void 0 ? void 0 : _a.previewPlatform) === "watch"; + if (objectGraph.client.isWatch || isWebViewingWatch) { + return todayCardArtworkDetailsForWatch(objectGraph, data, cardConfig); + } + const cardDisplayStyle = cardDisplayStyleFromData(data, cardConfig.coercedCollectionTodayCardDisplayStyle); + // Determine our preferred ranking of media flavors for the display style, + // then get the highest ranked media available on the card response. + const mediaSearchOrder = cardConfig.isHorizontalShelfContext + ? horizontalShelfMediaSearchOrderForCardDisplayStyle(objectGraph, cardConfig) + : mediaSearchOrderForCardDisplayStyle(objectGraph, cardDisplayStyle, cardConfig); + let mediaConfigurationForCard; + let isUsingEditorialCardOverride = false; + const editorialCardData = editorialCardFromData(data); + if (mediaAttributes.hasAttributes(editorialCardData)) { + mediaConfigurationForCard = mediaSearchOrder.find((conf) => { + return isSome(mediaAttributes.attributeAsDictionary(editorialCardData, conf.objectPath)); + }); + isUsingEditorialCardOverride = isSome(mediaConfigurationForCard); + } + if (isNothing(mediaConfigurationForCard)) { + mediaConfigurationForCard = mediaSearchOrder.find((conf) => { + return isSome(mediaAttributes.attributeAsDictionary(data, conf.objectPath)); + }); + } + if (!mediaConfigurationForCard) { + return undefined; + } + const mediaData = isUsingEditorialCardOverride + ? mediaAttributes.attributeAsDictionary(editorialCardData, mediaConfigurationForCard.objectPath) + : mediaAttributes.attributeAsDictionary(data, mediaConfigurationForCard.objectPath); + if (mediaConfigurationForCard.type === "image") { + // Return an Artwork and cardArtLayout + const artworks = mediaConfigurationForCard.crops.map((crop) => { + let cropCodeToUse = crop; + if (cardConfig.isSearchContext && isSome(cardConfig.prevailingCropCodes)) { + cropCodeToUse = cardConfig.prevailingCropCodes.defaultCrop; + } + else if (isSome(cardConfig.prevailingCropCodes) && + isSome(cardConfig.prevailingCropCodes[mediaConfigurationForCard.objectPath])) { + cropCodeToUse = cardConfig.prevailingCropCodes[mediaConfigurationForCard.objectPath]; + } + const artwork = artworkFromApiArtwork(objectGraph, mediaData, { + withJoeColorPlaceholder: true, + cropCode: cropCodeToUse, + useCase: 15 /* ArtworkUseCase.TodayCardMedia */, + overrideHeight: mediaConfigurationForCard.sourceHeight, + overrideWidth: mediaConfigurationForCard.sourceWidth, + }); + return artwork; + }); + const joeColors = joeColorSetFromData(mediaData); + return { + artworks: artworks, + videos: [], + artworkLayoutsWithMetrics: mediaConfigurationForCard.cardArtLayoutMetrics, + joeColors: joeColors, + }; + } + else { + // Return a Video and cardArtLayout + const previewArtworkData = asDictionary(mediaData, "previewFrame"); + // All TodayCardArtworkLayouts are specified in landscape aspect ratio, + // but videos can come back in either landscape or portrait. If we + // receive a portrait video, we need to flip the TodayCardArtworkLayout + const previewArtworkWidth = asNumber(previewArtworkData, "width"); + const previewArtworkHeight = asNumber(previewArtworkData, "height"); + const isPortrait = previewArtworkHeight >= previewArtworkWidth; + let overrideHeight = mediaConfigurationForCard.sourceHeight; + let overrideWidth = mediaConfigurationForCard.sourceWidth; + if (isPortrait) { + [overrideHeight, overrideWidth] = [overrideWidth, overrideHeight]; + } + const previewArtwork = artworkFromApiArtwork(objectGraph, previewArtworkData, { + withJoeColorPlaceholder: true, + cropCode: mediaConfigurationForCard.crops[0], + useCase: 15 /* ArtworkUseCase.TodayCardMedia */, + overrideHeight: overrideHeight, + overrideWidth: overrideWidth, + }); + if (isNothing(previewArtwork)) { + return undefined; + } + let playbackControls = standardControls(objectGraph); + if (isSome(cardConfig.videoPlaybackControls)) { + playbackControls = cardConfig.videoPlaybackControls; + } + if (objectGraph.client.isMac) { + playbackControls.scrubber = true; + } + let autoplayPlaybackControls = autoPlayControls(objectGraph); + if (isSome(cardConfig.videoPlaybackControls)) { + autoplayPlaybackControls = cardConfig.videoAutoplayPlaybackControls; + } + const videoUrl = asString(mediaData, "video"); + if (isNothing(videoUrl)) { + return undefined; + } + const video = new Video(videoUrl, previewArtwork, { + playbackControls: playbackControls, + autoPlayPlaybackControls: autoplayPlaybackControls, + canPlayFullScreen: cardConfig.enableFullScreenVideo, + }); + const joeColors = joeColorSetFromData(previewArtworkData); + return { + artworks: [], + videos: [video], + artworkLayoutsWithMetrics: mediaConfigurationForCard.cardArtLayoutMetrics, + joeColors: joeColors, + }; + } +} +function mediaSearchOrderForCardDisplayStyle(objectGraph, cardDisplayStyle, cardConfig) { + // Hero position, non-list card display styles, on iPad requires the UniversalA flavor artwork to be considered valid, + // so we'll only check those 2 configurations in this scenario. + const listCardDisplayStyles = new Set([TodayCardDisplayStyle.List, TodayCardDisplayStyle.NumberedList]); + if (objectGraph.client.isPad && cardConfig.isHeroCard && !listCardDisplayStyles.has(cardDisplayStyle)) { + return universalAConfigurations(objectGraph, cardConfig.isHeroCard); + } + switch (cardDisplayStyle) { + case TodayCardDisplayStyle.AppEventCard: + return [ + storyCardMotion16x9Configuration(objectGraph), + storyCardStatic16x9Configuration(objectGraph), + storyCenteredMotion16x9Configuration(objectGraph), + storyCenteredStatic16x9Configuration(objectGraph), + ...universalAConfigurations(objectGraph, cardConfig.isHeroCard), + eventCardConfiguration(objectGraph), + ]; + case TodayCardDisplayStyle.AppOfTheDay: + case TodayCardDisplayStyle.GameOfTheDay: + return [ + storyCardMotion16x9Configuration(objectGraph), + storyCardStatic16x9Configuration(objectGraph), + storyCenteredMotion16x9Configuration(objectGraph), + storyCenteredStatic16x9Configuration(objectGraph), + ...universalAConfigurations(objectGraph, cardConfig.isHeroCard), + dayCardConfiguration(objectGraph), + ]; + case TodayCardDisplayStyle.List: + case TodayCardDisplayStyle.NumberedList: + return [listCardMotion21x9Configuration(objectGraph), listCardStatic21x9Configuration(objectGraph)]; + case TodayCardDisplayStyle.ShortImage: + if (cardConfig.isSearchContext) { + return [ + generalCardConfiguration(objectGraph), + storyCardMotion16x9Configuration(objectGraph), + storyCardStatic16x9Configuration(objectGraph), + storyCenteredMotion16x9Configuration(objectGraph), + storyCenteredStatic16x9Configuration(objectGraph), + ...universalAConfigurations(objectGraph, cardConfig.isHeroCard), + storeFrontVideoConfiguration(objectGraph), + storeFrontVideo4x3Configuration(objectGraph), + mediaCardConfiguration(objectGraph), + ]; + } + else { + return [ + storyCardMotion16x9Configuration(objectGraph), + storyCardStatic16x9Configuration(objectGraph), + storyCenteredMotion16x9Configuration(objectGraph), + storyCenteredStatic16x9Configuration(objectGraph), + ...universalAConfigurations(objectGraph, cardConfig.isHeroCard), + storeFrontVideoConfiguration(objectGraph), + storeFrontVideo4x3Configuration(objectGraph), + generalCardConfiguration(objectGraph), + mediaCardConfiguration(objectGraph), + ]; + } + case TodayCardDisplayStyle.FullBleedImage: + const fullBleedConfigurations = [ + storyCardMotion16x9Configuration(objectGraph), + storyCardStatic16x9Configuration(objectGraph), + categoryDetailMotion16x9Configuration(objectGraph), + categoryDetailStatic16x9Configuration(objectGraph), + storyCenteredMotion16x9Configuration(objectGraph), + storyCenteredStatic16x9Configuration(objectGraph), + ...universalAConfigurations(objectGraph, cardConfig.isHeroCard), + storeFrontVideoConfiguration(objectGraph), + storeFrontVideo4x3Configuration(objectGraph), + mediaCardConfiguration(objectGraph), + generalCardConfiguration(objectGraph), + ]; + if (objectGraph.client.isVision && isSome(cardConfig.isSearchContext) && cardConfig.isSearchContext) { + fullBleedConfigurations.unshift(storySearchStatic16x9Configuration(objectGraph)); + } + return fullBleedConfigurations; + case TodayCardDisplayStyle.SingleApp: + case TodayCardDisplayStyle.Video: + default: + const configurations = [ + storyCardMotion16x9Configuration(objectGraph), + storyCardStatic16x9Configuration(objectGraph), + storyCenteredMotion16x9Configuration(objectGraph), + storyCenteredStatic16x9Configuration(objectGraph), + ...universalAConfigurations(objectGraph, cardConfig.isHeroCard), + storeFrontVideoConfiguration(objectGraph), + storeFrontVideo4x3Configuration(objectGraph), + mediaCardConfiguration(objectGraph), + generalCardConfiguration(objectGraph), + ]; + if (objectGraph.client.isVision && isSome(cardConfig.isSearchContext) && cardConfig.isSearchContext) { + configurations.unshift(storySearchStatic16x9Configuration(objectGraph)); + } + return configurations; + } +} +function horizontalShelfMediaSearchOrderForCardDisplayStyle(objectGraph, cardConfig) { + const searchOrder = []; + if (objectGraph.client.isiOS || objectGraph.client.isWeb) { + searchOrder.push(storyCenteredStatic16x9Configuration(objectGraph, ["SCS.ApDPCS01"])); + } + if (cardConfig.horizontalCardContentType === "largeStoryCard") { + searchOrder.push(storeFrontVideoConfiguration(objectGraph)); + } + const shouldUseHorizontalCardCrop = objectGraph.client.isiOS || objectGraph.client.isMac; + searchOrder.push(mediaCardConfiguration(objectGraph, shouldUseHorizontalCardCrop ? ["sr"] : undefined), generalCardConfiguration(objectGraph, shouldUseHorizontalCardCrop ? ["sr"] : undefined)); + return searchOrder; +} +/** + * Watch uses a more straightforward artwork lookup than other platforms. + * @param objectGraph The AppStoreObjectGraph + * @param data MAPI data for a Today Card + * @param cardConfig The TodayCardConfiguration + * @returns TodayCardArtworkDetails appropriate for display on Apple Watch + */ +function todayCardArtworkDetailsForWatch(objectGraph, data, cardConfig) { + var _a, _b; + const mediaData = mediaAttributes.attributeAsDictionary(data, "editorialArtwork.subscriptionHero"); + const artwork = artworkFromApiArtwork(objectGraph, mediaData, { + withJoeColorPlaceholder: true, + cropCode: (_b = (_a = cardConfig.prevailingCropCodes) === null || _a === void 0 ? void 0 : _a.defaultCrop) !== null && _b !== void 0 ? _b : "SH.ApHXS01", + useCase: 15 /* ArtworkUseCase.TodayCardMedia */, + }); + const joeColors = joeColorSetFromData(mediaData); + // Don't return an array containing `undefined` + const artworks = isSome(artwork) ? [artwork] : []; + return { + artworks: artworks, + videos: [], + artworkLayoutsWithMetrics: [], + joeColors: joeColors, + }; +} +//# sourceMappingURL=today-artwork-util.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/today/cards/today-ad-card-builder.js b/node_modules/@jet-app/app-store/tmp/src/common/today/cards/today-ad-card-builder.js new file mode 100644 index 0000000..dfe9836 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/today/cards/today-ad-card-builder.js @@ -0,0 +1,232 @@ +import { isSome } from "@jet/environment"; +import * as validation from "@jet/environment/json/validation"; +import { MediumAdLockupWithScreenshotsBackground, CondensedAdLockupWithIconBackground, TodayCardMediaSingleLockup, TodayCardMediaMediumLockupWithScreenshots, TodayCardMediaMediumLockupWithAlignedRegion, MediumAdLockupWithAlignedRegionBackground, } from "../../../api/models"; +import { asString, isDefinedNonNull, isNull } from "../../../foundation/json-parsing/server-data"; +import { isColorEqualToColor } from "../../../foundation/util/color-util"; +import { todayAdStyle } from "../../ads/ad-common"; +import { recordLockupFromDataFailed } from "../../ads/ad-incident-recorder"; +import { iconFromData } from "../../content/content"; +import { getTemplateTypeForMediumAdFromLockupWithCustomCreative, getTemplateTypeForMediumAdFromLockupWithScreenshots, isCppDeeplinkEnabledForAdvert, performAdOverridesForCard, } from "../../lockups/ad-lockups"; +import * as lockups from "../../lockups/lockups"; +import * as metricsHelpersImpressions from "../../metrics/helpers/impressions"; +import { productVariantDataForData } from "../../product-page/product-page-variants"; +import { popTodayCardLocation, pushTodayCardLocation } from "../today-card-util"; +import { createTodayBaseCard } from "./today-base-card-builder"; +import { getSelectedCustomCreativeId } from "../../search/custom-creative"; +/** + * Create TodayCard displaying an ad fetched from Ad platforms + * + * @param objectGraph The dependency graph for the App Store + * @param adData The media api data to build the ad card from + * @param adIncidentRecorder The incident recorder for the ad, for when an issues arises + * @param cardConfig The configuration for the card + * @param context The parse context for the over all today page + * @returns The newly created TodayCard, using the full bleed image media. + */ +export function createTodayAdCard(objectGraph, adData, adIncidentRecorder, cardConfig, context) { + return validation.context("createTodayAdCard", () => { + var _a, _b, _c, _d; + if (isNull(adData)) { + return null; // No task for the position + } + const adCard = createTodayBaseCard(objectGraph, adData, cardConfig, context); + pushTodayCardLocation(objectGraph, adData, cardConfig, context, asString(adData.attributes.name)); + const metricsOptions = metricsHelpersImpressions.impressionOptions(objectGraph, adData, asString(adData.attributes.name), { + targetType: "todayCard", + pageInformation: context.pageInformation, + locationTracker: context.locationTracker, + isAdvert: true, + rowIndex: cardConfig.currentRowIndex, + displayStyle: cardConfig.metricsDisplayStyle, + }); + const productVariantData = productVariantDataForData(objectGraph, adData); + metricsOptions.productVariantData = productVariantData; + metricsOptions.adSlotOverride = context.parsedCardCount; + metricsOptions.kind = "adItem"; + const clickOptions = metricsOptions; + // Set up iAdInfo + metricsOptions.pageInformation.iAdInfo.apply(objectGraph, adData); + // Configure the action + let clientIdentifierOverride; + if (isDefinedNonNull(cardConfig)) { + clientIdentifierOverride = cardConfig.clientIdentifierOverride; + } + const customCreativeId = getSelectedCustomCreativeId(adData); + const isCustomCreative = isSome(customCreativeId); + adCard.style = "dark"; + switch (todayAdStyle(objectGraph)) { + case "singleLockup": + // Hardcode the condensed ad template type. This is not dependent on media count. + (_a = metricsOptions.pageInformation.iAdInfo) === null || _a === void 0 ? void 0 : _a.setTemplateType("APPLOCKUP"); + break; + case "mediumLockup": + if (isCustomCreative) { + (_b = metricsOptions.pageInformation.iAdInfo) === null || _b === void 0 ? void 0 : _b.setTemplateType(getTemplateTypeForMediumAdFromLockupWithCustomCreative()); + } + break; + default: + break; + } + let lockup = createAdCardLockup(objectGraph, adData, cardConfig, context); + if (isNull(lockup)) { + recordLockupFromDataFailed(objectGraph, adIncidentRecorder, adData); + popTodayCardLocation(context); + // Configure impressions + metricsHelpersImpressions.addImpressionsFieldsToTodayCard(objectGraph, adCard, metricsOptions, null, null, false, false); + return null; + } + if (objectGraph.props.enabled("advertSlotReporting")) { + (_c = lockup.searchAdOpportunity) === null || _c === void 0 ? void 0 : _c.setTemplateType("APPLOCKUP"); + } + else { + (_d = lockup.searchAd) === null || _d === void 0 ? void 0 : _d.setTemplateType("APPLOCKUP"); + } + switch (todayAdStyle(objectGraph)) { + case "singleLockup": + const condensedAdLockupWithIconBackground = new CondensedAdLockupWithIconBackground(lockup, lockup.icon); + adCard.media = new TodayCardMediaSingleLockup(condensedAdLockupWithIconBackground); + adCard.media.impressionMetrics = lockup.impressionMetrics; + break; + case "mediumLockup": + if (preprocessor.CARRY_BUILD || preprocessor.DEBUG_BUILD) { + if (isCustomCreative && objectGraph.featureFlags.isEnabled("aligned_region_artwork_2025A")) { + adCard.media = createMediumLockupWithAlignedRegion(objectGraph, lockup, adData, metricsOptions.pageInformation); + // After the media is created, we need to recreate the lockup to ensure all instrumentation + // has the updated template type, which is derived from the final set of media in + // `createMediumLockupWithAlignedRegion`. + lockup = createAdCardLockup(objectGraph, adData, cardConfig, context); + // Preserve SearchAd metadata + lockup.searchAdOpportunity = adCard.media.mediumAdLockupWithAlignedRegionBackground.lockup.searchAdOpportunity; + lockup.searchAd = adCard.media.mediumAdLockupWithAlignedRegionBackground.lockup.searchAd; + // The new lockup with the correct metrics should be set on the media. + adCard.media.mediumAdLockupWithAlignedRegionBackground.lockup = lockup; + adCard.media.impressionMetrics = lockup.impressionMetrics; + break; + } + else { + adCard.media = createMediumScreenshotsCardMedia(adCard, objectGraph, lockup, adData, cardConfig, context, metricsOptions); + } + } + else { + adCard.media = createMediumScreenshotsCardMedia(adCard, objectGraph, lockup, adData, cardConfig, context, metricsOptions); + } + break; + default: + return null; + } + popTodayCardLocation(context); + // CPP deeplinks used for ads have a requirement to not be used when targeting criteria is too narrow. + const isCppDeeplinkingEnabled = isCppDeeplinkEnabledForAdvert(adData); + // It's important the click action is created after the template type is set, to ensure it's in the metrics. + adCard.clickAction = lockups.actionFromData(objectGraph, adData, clickOptions, clientIdentifierOverride, undefined, isCppDeeplinkingEnabled); + performAdOverridesForCard(objectGraph, adData, adCard, metricsOptions); + // Configure impressions + metricsHelpersImpressions.addImpressionsFieldsToTodayCard(objectGraph, adCard, metricsOptions, null, null, false, false); + return adCard; + }); +} +function createMediumScreenshotsCardMedia(adCard, objectGraph, lockup, adData, cardConfig, context, metricsOptions) { + adCard.media = createMediumLockupWithScreenshots(objectGraph, lockup, objectGraph.bag.todayAdMediumLockupScreenshotAnimationEnabled, metricsOptions.pageInformation, adData); + // After the media is created, we need to recreate the lockup to ensure all instrumentation + // has the updated template type, which is derived from the final set of media in + // `createMediumLockupWithScreenshots`. + lockup = createAdCardLockup(objectGraph, adData, cardConfig, context); + if (isNull(lockup)) { + return null; + } + // Preserve SearchAd metadata + lockup.searchAdOpportunity = adCard.media.mediumAdLockupWithScreenshotsBackground.lockup.searchAdOpportunity; + lockup.searchAd = adCard.media.mediumAdLockupWithScreenshotsBackground.lockup.searchAd; + // The new lockup with the correct metrics should be set on the media. + adCard.media.mediumAdLockupWithScreenshotsBackground.lockup = lockup; + adCard.media.impressionMetrics = lockup.impressionMetrics; + return adCard.media; +} +function createAdCardLockup(objectGraph, data, cardConfig, context) { + const offerEnvironment = "ad"; + const offerStyle = "transparent"; + return lockups.mixedMediaAdLockupFromData(objectGraph, data, { + offerEnvironment: offerEnvironment, + offerStyle: offerStyle, + metricsOptions: { + pageInformation: context.pageInformation, + locationTracker: context.locationTracker, + isAdvert: true, + adSlotOverride: context.parsedCardCount, + disableFastImpressionsForAds: true, + }, + clientIdentifierOverride: cardConfig.clientIdentifierOverride, + crossLinkSubtitle: cardConfig.crossLinkSubtitle, + artworkUseCase: 1 /* ArtworkUseCase.LockupIconSmall */, + canDisplayArcadeOfferButton: cardConfig.canDisplayArcadeOfferButton, + }, { + allowsAutoPlay: true, + looping: true, + canPlayFullScreen: false, + playbackControls: {}, + }, null, false); +} +function createMediumLockupWithAlignedRegion(objectGraph, lockup, adData, pageInformation) { + var _a, _b, _c; + const alignedRegionArtwork = lockup.alignedRegionArtwork; + const templateString = getTemplateTypeForMediumAdFromLockupWithCustomCreative(); + (_a = pageInformation === null || pageInformation === void 0 ? void 0 : pageInformation.iAdInfo) === null || _a === void 0 ? void 0 : _a.setTemplateType(templateString); + if (objectGraph.props.enabled("advertSlotReporting")) { + (_b = lockup.searchAdOpportunity) === null || _b === void 0 ? void 0 : _b.setTemplateType(templateString); + } + else { + (_c = lockup.searchAd) === null || _c === void 0 ? void 0 : _c.setTemplateType(templateString); + } + const mediumContainer = new MediumAdLockupWithAlignedRegionBackground(lockup, alignedRegionArtwork); + return new TodayCardMediaMediumLockupWithAlignedRegion(mediumContainer); +} +/** + * Creates the media for a medium Today ad, with the appropriate media. + * Also applies information about the created media template to a provided MetricsPageInformation. + * @param objectGraph The object graph. + * @param lockup The lockup for the app. + * @param isAnimated Whether the card background should be animated. + * @param pageInformation A MetricsPageInformation to apply the created template metrics information to. + * @param adData The data for the ad. + * @returns A `TodayCardMediumLockupWithScreenshots` with the correct media to be displayed. + */ +function createMediumLockupWithScreenshots(objectGraph, lockup, isAnimated, pageInformation, adData) { + var _a, _b, _c; + // Grab the first of the platform screenshots and trailers - the ones the ad will display. + const platformScreenshots = lockup.screenshots[0]; + const templateString = getTemplateTypeForMediumAdFromLockupWithScreenshots(platformScreenshots); + (_a = pageInformation.iAdInfo) === null || _a === void 0 ? void 0 : _a.setTemplateType(templateString); + if (objectGraph.props.enabled("advertSlotReporting")) { + (_b = lockup.searchAdOpportunity) === null || _b === void 0 ? void 0 : _b.setTemplateType(templateString); + } + else { + (_c = lockup.searchAd) === null || _c === void 0 ? void 0 : _c.setTemplateType(templateString); + } + const iconData = iconFromData(objectGraph, adData, { + useCase: 0 /* ArtworkUseCase.Default */, + withJoeColorPlaceholder: true, + overrideTextColorKey: "textColor2", + }); + let backgroundColor = iconData.backgroundColor; + let secondaryTextColor = iconData.textColor; + const rgbWhite = { + type: "rgb", + red: 1, + green: 1, + blue: 1, + }; + // If `backgroundColor` is white, use `secondaryTextColor` as the "primary" color. + // Otherwise, if `secondaryTextColor` is white, remove it and use only `backgroundColor`. + // Native code has a fallback state where only `backgroundColor` is provided, so if + // either color is white, we prefer the single non-white color. + if (isColorEqualToColor(rgbWhite, backgroundColor)) { + backgroundColor = secondaryTextColor; + secondaryTextColor = undefined; + } + else if (isColorEqualToColor(rgbWhite, secondaryTextColor)) { + secondaryTextColor = undefined; + } + const mediumContainer = new MediumAdLockupWithScreenshotsBackground(lockup, [platformScreenshots], isAnimated, secondaryTextColor, backgroundColor, 8); + return new TodayCardMediaMediumLockupWithScreenshots(mediumContainer); +} +//# sourceMappingURL=today-ad-card-builder.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/today/cards/today-app-event-card-builder.js b/node_modules/@jet-app/app-store/tmp/src/common/today/cards/today-app-event-card-builder.js new file mode 100644 index 0000000..c030a5a --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/today/cards/today-app-event-card-builder.js @@ -0,0 +1,112 @@ +import { isSome } from "@jet/environment"; +import * as validation from "@jet/environment/json/validation"; +import { isNothing } from "@jet/environment/types/optional"; +import { TodayCardLockupOverlay, TodayCardMediaAppEvent, } from "../../../api/models"; +import { asBooleanOrFalse } from "../../../foundation/json-parsing/server-data"; +import * as mediaAttributes from "../../../foundation/media/attributes"; +import * as mediaRelationship from "../../../foundation/media/relationships"; +import * as color from "../../../foundation/util/color-util"; +import { appEventOrPromotionStartDateFromData } from "../../app-promotions/app-event"; +import { appEventsAreEnabled } from "../../app-promotions/app-promotions-common"; +import { addNextPreferredContentRefreshDate } from "../../refresh/page-refresh-controller"; +import { todayCardArtworkDetails } from "../artwork/today-artwork-util"; +import { applyTodayCardOverridesForAcquisitionStoryIfNecessary } from "../today-card-overlay-util"; +import { cardDisplayStyleFromData, cardStyleFromJoeColorsWithoutFallback, offerEnvironmentForTodayCard, offerStyleForTodayCard, popTodayCardLocation, pushTodayCardLocation, } from "../today-card-util"; +import { createTodayBaseCard } from "./today-base-card-builder"; +/** + * Create TodayCard displaying an app event + * + * @param objectGraph The dependency graph for the App Store + * @param data The media api data to build the card from + * @param cardConfig The configuration for the card + * @param context The parse context for the over all today page + * @param augmentingData that stores some additional responses that may be used to enhance the contents of `data` + * @returns The newly created TodayCard, using a app event media. + */ +export function createTodayAppEventCard(objectGraph, data, cardConfig, context, augmentingData) { + return validation.context("createTodayAppEventCard", () => { + var _a, _b, _c, _d, _e; + if (!appEventsAreEnabled(objectGraph)) { + return null; + } + const appEventData = appEventDataFromData(objectGraph, data); + if (isNothing(appEventData)) { + return null; + } + const appEventCard = createTodayBaseCard(objectGraph, data, cardConfig, context, (clickOptions) => { + // Add app event ID to click options + clickOptions.inAppEventId = appEventData.id; + const parentAppData = mediaRelationship.relationshipData(objectGraph, appEventData, "app"); + if (isSome(parentAppData)) { + clickOptions.relatedSubjectIds = [parentAppData.id]; + } + }); + if (isSome(appEventCard.editorialDisplayOptions)) { + appEventCard.editorialDisplayOptions.useMaterialBlur = true; + } + pushTodayCardLocation(objectGraph, data, cardConfig, context); + // Artwork + const mediaDetails = todayCardArtworkDetails(objectGraph, data, cardConfig); + if (isNothing(mediaDetails)) { + popTodayCardLocation(context); + return null; + } + // Tint and styling + const tintColor = (_b = (_a = mediaDetails.joeColors) === null || _a === void 0 ? void 0 : _a.textColor4) !== null && _b !== void 0 ? _b : color.black; + const artworkBackgroundColor = (_d = (_c = mediaDetails.joeColors) === null || _c === void 0 ? void 0 : _c.bgColor) !== null && _d !== void 0 ? _d : color.black; + const blurStyle = color.isDarkColor(artworkBackgroundColor) ? "dark" : "light"; + // App event + const metricsOptions = { + pageInformation: context.pageInformation, + locationTracker: context.locationTracker, + targetType: "eventModule", + }; + // Card style + appEventCard.style = + (_e = cardStyleFromJoeColorsWithoutFallback(mediaDetails.joeColors, "textColor4")) !== null && _e !== void 0 ? _e : (color.isDarkColor(tintColor) ? "dark" : "light"); + // Offer style + const offerStyle = offerStyleForTodayCard(objectGraph, appEventCard.style); + const editorialKind = mediaAttributes.attributeAsString(data, "label"); + const appEventOrDate = appEventOrPromotionStartDateFromData(objectGraph, appEventData, null, false, false, offerEnvironmentForTodayCard(appEventCard.style), offerStyle, true, metricsOptions, true, true, editorialKind, false, asBooleanOrFalse(cardConfig.allowUnpublishedAppEventPreviews)); + // Return early if we received a Date, as this means the App Event shouldn't be shown yet. + if (isNothing(appEventOrDate) || appEventOrDate instanceof Date) { + addNextPreferredContentRefreshDate(appEventOrDate, context.refreshController); + popTodayCardLocation(context); + return null; + } + const appEvent = appEventOrDate; + // Card + appEventCard.media = new TodayCardMediaAppEvent(appEvent.formattedDates, appEvent.startDate, tintColor, mediaDetails.artworks, mediaDetails.videos, mediaDetails.artworkLayoutsWithMetrics, blurStyle); + if (isSome(appEvent.lockup)) { + appEventCard.overlay = new TodayCardLockupOverlay(appEvent.lockup); + } + // Special post-processing step for Acquisition story cards. + // This is needed to splice in data not included in initial response. + applyTodayCardOverridesForAcquisitionStoryIfNecessary(objectGraph, appEventCard, cardConfig, cardDisplayStyleFromData(data, cardConfig.coercedCollectionTodayCardDisplayStyle), data, augmentingData, context); + popTodayCardLocation(context); + return appEventCard; + }); +} +/** + * @param objectGraph The dependency graph for the App Store + * @param data The media api data to build the card from + * @returns The app event relationship data for this card + */ +function appEventDataFromData(objectGraph, data) { + let appEventData; + // Primary content + const primaryContent = mediaRelationship.relationshipCollection(data, "primary-content"); + if (primaryContent.length > 0) { + appEventData = primaryContent[0]; + } + else { + // Card contents + const cardContents = mediaRelationship.relationshipCollection(data, "card-contents"); + if (cardContents.length === 0) { + return null; + } + appEventData = cardContents[0]; + } + return appEventData; +} +//# sourceMappingURL=today-app-event-card-builder.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/today/cards/today-base-card-builder.js b/node_modules/@jet-app/app-store/tmp/src/common/today/cards/today-base-card-builder.js new file mode 100644 index 0000000..77bd0de --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/today/cards/today-base-card-builder.js @@ -0,0 +1,244 @@ +import * as validation from "@jet/environment/json/validation"; +import { isNothing, isSome } from "@jet/environment/types/optional"; +import { ExternalUrlAction, TodayCard, TodayCardActionOverlay, TodayCardMediaArtwork, } from "../../../api/models"; +import { isNull } from "../../../foundation/json-parsing/server-data"; +import * as mediaAttributes from "../../../foundation/media/attributes"; +import { relationshipCollection } from "../../../foundation/media/relationships"; +import * as contentAttributes from "../../content/attributes"; +import { editorialNotesFromData, notesFromData } from "../../content/content"; +import { extractEditorialClientParams } from "../../editorial-pages/editorial-data-util"; +import { editorialItemActionFromData, subtitleFromData } from "../../lockups/lockups"; +import * as metricsHelpersImpressions from "../../metrics/helpers/impressions"; +import { addMetricsEventsToVideo } from "../../metrics/helpers/media"; +import { todayCardArtworkDetails } from "../artwork/today-artwork-util"; +import { isCardDataOnboardingCard } from "../onboarding-cards"; +import { brandedTitleArtworkForCard, cardDisplayStyleFromData, cardStyleFromJoeColors, isCardOTDIntention, relatedCardContentsContentsFromData, todayCardArtworkTitleBackingGradientForKey, todayCardEditorialDisplayOptionsFromData, todayCardMetricsOptions, } from "../today-card-util"; +import { recoMetricsFromTodayItem } from "../today-parse-util"; +import { TodayCardDisplayStyle } from "../today-types"; +/** + * If the title is not supplied for app of the day we use this + */ +const appOfTheDayFallbackTitle = "FEATURED APP"; +/** + * If the title is not supplied for game of the day we use this + */ +const gameOfTheDayFallbackTitle = "FEATURED GAME"; +/** + * Creates a TodayCard used as the base for all other TodayCards + * + * @param objectGraph The dependency graph for the App Store + * @param data The media api data to build the card from + * @param cardConfig The configuration for the card + * @param context The parse context for the over all today page + * @param clickOptionsModifier A function that can modify the click options for the card + * @returns The newly created TodayCard, which can then be modified by other TodayCard builders + */ +export function createTodayBaseCard(objectGraph, data, cardConfig, context, clickOptionsModifier) { + return validation.context("createTodayBaseCard", () => { + const baseCard = new TodayCard(); + const cardDisplayStyle = cardDisplayStyleFromData(data, cardConfig === null || cardConfig === void 0 ? void 0 : cardConfig.coercedCollectionTodayCardDisplayStyle); + // Heading + const heading = cardHeadingFromData(objectGraph, data, cardDisplayStyle, cardConfig); + baseCard.heading = heading; + // Title, title artwork + const title = cardTitleFromData(objectGraph, data, cardConfig); + baseCard.title = title; + const shortTitle = contentAttributes.contentAttributeAsString(objectGraph, data, [ + "shortEditorialNotes", + "name", + ]); + baseCard.shortTitle = shortTitle; + const cardTitleArtwork = brandedTitleArtworkForCard(objectGraph, data); + baseCard.titleArtwork = cardTitleArtwork; + // Description + const inlineDescription = cardDescriptionFromData(objectGraph, data); + baseCard.inlineDescription = inlineDescription; + const metricsOptions = todayCardMetricsOptions(objectGraph, data, cardConfig, context, title); + metricsOptions.adSlotOverride = context.parsedCardCount; + baseCard.clickAction = cardClickAction(objectGraph, data, cardDisplayStyle, cardConfig, context, metricsOptions, clickOptionsModifier); + // Configure impressions + metricsHelpersImpressions.addImpressionsFieldsToTodayCard(objectGraph, baseCard, metricsOptions, heading, cardDisplayStyle, isCardDataOnboardingCard(objectGraph, data)); + baseCard.editorialDisplayOptions = todayCardEditorialDisplayOptionsFromData(objectGraph, data, cardConfig); + return baseCard; + }); +} +/** + * @param objectGraph The dependency graph for the App Store + * @param cardDisplayStyle The display style of the card + * @param data The data to get the branded single app overlay heading from + * @param cardConfig The configuration for the card + * @returns The heading to use on the branded single app overlay + */ +export function cardHeadingFromData(objectGraph, data, cardDisplayStyle, cardConfig) { + var _a, _b; + let heading = null; + if (cardConfig === null || cardConfig === void 0 ? void 0 : cardConfig.useOTDTextStyle) { + if (cardConfig.isHorizontalShelfContext) { + // We don't support branded images or titles for a cards in a horizontal shelf + // so want to show the "APP OF THE DAY" / "GAME OF THE DAY" text as the heading instead. + heading = mediaAttributes.attributeAsString(data, "label"); + } + else { + heading = null; + } + } + else if (isCardOTDIntention(data, cardConfig)) { + heading = + (_a = mediaAttributes.attributeAsString(data, "alternateLabel")) !== null && _a !== void 0 ? _a : mediaAttributes.attributeAsString(data, "label"); + if (isNull(heading) && cardDisplayStyle === TodayCardDisplayStyle.AppOfTheDay) { + heading = appOfTheDayFallbackTitle; + } + else if (isNull(heading) && cardDisplayStyle === TodayCardDisplayStyle.GameOfTheDay) { + heading = gameOfTheDayFallbackTitle; + } + } + else { + switch (cardDisplayStyle) { + case TodayCardDisplayStyle.AppEventCard: + const fallbackCardContents = fallbackCardContentFromData(objectGraph, data); + if (isSome(fallbackCardContents)) { + heading = + (_b = editorialNotesFromData(objectGraph, data, "badge", true)) !== null && _b !== void 0 ? _b : mediaAttributes.attributeAsString(fallbackCardContents, "kind"); + } + break; + default: + heading = mediaAttributes.attributeAsString(data, "label"); + break; + } + } + return heading; +} +/** + * @param objectGraph The dependency graph for the App Store + * @param data The media api data used to determine the title of this card + * @param cardConfig The configuration for the card + * @returns The title to use for this card + */ +function cardTitleFromData(objectGraph, data, cardConfig) { + var _a; + let title = null; + if (cardConfig === null || cardConfig === void 0 ? void 0 : cardConfig.useOTDTextStyle) { + title = + (_a = mediaAttributes.attributeAsString(data, "ofTheDayLabel")) !== null && _a !== void 0 ? _a : mediaAttributes.attributeAsString(data, "label"); + // We need to replace \n characters with <br> tags to ensure the + // newlines in APP OF THE DAY titles are maintained. The pattern must be a RegExp since string patterns + // only replace the first occurrence. + if (isSome(title)) { + title = title.replace(/\n/g, "<br>"); + } + } + if (isNothing(title)) { + title = notesFromData(objectGraph, data, "name", true); + } + if (isNothing(title)) { + // Lastly fallback to the first related content title + const fallbackCardContents = fallbackCardContentFromData(objectGraph, data); + title = isSome(fallbackCardContents) ? mediaAttributes.attributeAsString(fallbackCardContents, "name") : null; + } + return title; +} +/** + * @param objectGraph The dependency graph for the App Store + * @param data The media api data used to determine the description of this card + * @returns The title to use for this card + */ +function cardDescriptionFromData(objectGraph, data) { + var _a; + const editorialClientParams = extractEditorialClientParams(objectGraph, data); + const ignoreShortNotes = mediaAttributes.attributeAsBooleanOrFalse(data, "ignoreITunesShortNotes"); + if (ignoreShortNotes || editorialClientParams.suppressNoteShort) { + return null; + } + let description = notesFromData(objectGraph, data, "short", true); + if (isNothing(description) && !editorialClientParams.suppressNoteTagline) { + // Lastly fallback to the first related content title + const fallbackCardContents = fallbackCardContentFromData(objectGraph, data); + if (isSome(fallbackCardContents)) { + description = + (_a = notesFromData(objectGraph, fallbackCardContents, "tagline")) !== null && _a !== void 0 ? _a : subtitleFromData(objectGraph, fallbackCardContents); + } + } + return description; +} +/** + * + * @param objectGraph The dependency graph for the App Store + * @param data The media api data used to build the card + * @returns + */ +function fallbackCardContentFromData(objectGraph, data) { + const primaryContent = relationshipCollection(data, "primary-content"); + if ((primaryContent === null || primaryContent === void 0 ? void 0 : primaryContent.length) === 1) { + return primaryContent[0]; + } + // Card contents + const cardContents = relatedCardContentsContentsFromData(objectGraph, data); + if (cardContents.length === 1) { + return cardContents[0]; + } + return null; +} +/** + * @param objectGraph The dependency graph for the App Store + * @param data The media api data used to build the card this action is for + * @param cardDisplayStyle The display style of the card + * @param cardConfig The configuration for the card + * @param context The parse context for the over all today page + * @param baseMetricsOptions The base metrics options for the card + * @returns The click action used for tapping on the card + */ +function cardClickAction(objectGraph, data, cardDisplayStyle, cardConfig, context, baseMetricsOptions, clickOptionsModifier) { + var _a; + const clickOptions = baseMetricsOptions; + const franchise = mediaAttributes.attributeAsString(data, "label"); + // <rdar://problem/33677354> Metrics: JS: Add franchise/label to todayCard click events + const actionDetails = { + cardType: cardDisplayStyle, + franchise: franchise, + }; + const isOnboardingCard = isCardDataOnboardingCard(objectGraph, data); + if (isOnboardingCard) { + actionDetails["isOnboardingCard"] = isOnboardingCard; + } + clickOptions["actionDetails"] = actionDetails; + if (isSome(clickOptionsModifier)) { + clickOptionsModifier(clickOptions); + } + return editorialItemActionFromData(objectGraph, data, clickOptions, (_a = cardConfig === null || cardConfig === void 0 ? void 0 : cardConfig.clientIdentifierOverride) !== null && _a !== void 0 ? _a : objectGraph.host.clientIdentifier, recoMetricsFromTodayItem(context.currentTodayItem), cardConfig); +} +/** + * @param objectGraph The dependency graph for the App Store + * @param data The media api data used to build the base card + * @param baseCard The base card to add the artwork media to + * @param cropCode The crop code to use for the artwork + * @param context The context for the page were creating the card for + * @returns Whether or not we successfully added artwork media to the base card + */ +export function addArtworkMediaToBaseCard(objectGraph, data, baseCard, cardConfig, context) { + return validation.context("addArtworkMediaToBaseCard", () => { + const ignoreEditorialArt = mediaAttributes.attributeAsBooleanOrFalse(data, "ignoreEditorialArt"); + if (ignoreEditorialArt) { + return false; + } + const mediaDetails = todayCardArtworkDetails(objectGraph, data, cardConfig); + if (isNothing(mediaDetails)) { + return false; + } + if (isSome(context)) { + addMetricsEventsToVideo(objectGraph, mediaDetails === null || mediaDetails === void 0 ? void 0 : mediaDetails.videos[0], { + pageInformation: context.pageInformation, + locationTracker: context.locationTracker, + id: data.id, + }); + } + baseCard.media = new TodayCardMediaArtwork(mediaDetails.artworks, mediaDetails.videos, mediaDetails.artworkLayoutsWithMetrics, todayCardArtworkTitleBackingGradientForKey(objectGraph, data, cardConfig)); + baseCard.style = cardStyleFromJoeColors(mediaDetails.joeColors, "bgColor"); + // External URL + if (baseCard.clickAction instanceof ExternalUrlAction && objectGraph.client.isiOS) { + baseCard.overlay = new TodayCardActionOverlay(baseCard.clickAction); + baseCard.style = "white"; + } + return true; + }); +} +//# sourceMappingURL=today-base-card-builder.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/today/cards/today-branded-card-builder.js b/node_modules/@jet-app/app-store/tmp/src/common/today/cards/today-branded-card-builder.js new file mode 100644 index 0000000..9088bfa --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/today/cards/today-branded-card-builder.js @@ -0,0 +1,58 @@ +import * as validation from "@jet/environment/json/validation"; +import { isSome } from "@jet/environment/types/optional"; +import { TodayCardMediaBrandedSingleApp } from "../../../api/models"; +import { isNullOrEmpty } from "../../../foundation/json-parsing/server-data"; +import { deepLinkUrlFromData } from "../../linking/external-deep-link"; +import { todayCardArtworkDetails } from "../artwork/today-artwork-util"; +import { applyTodayCardOverridesForAcquisitionStoryIfNecessary, relatedContentOverlayFromData, } from "../today-card-overlay-util"; +import { cardDisplayStyleFromData, cardStyleFromJoeColors, lockupsForRelatedContent, offerEnvironmentForTodayCard, offerStyleForTodayCard, popTodayCardLocation, pushTodayCardLocation, relatedCardContentsContentsFromData, todayCardArtworkTitleBackingGradientForKey, } from "../today-card-util"; +import { createTodayBaseCard } from "./today-base-card-builder"; +import { createTodaySingleAppCard } from "./today-single-app-card-builder"; +/** + * Create TodayCard displaying the App of the Day and Game of the Day editorial items + * + * @param objectGraph The dependency graph for the App Store + * @param data The media api data to build the card from + * @param cardConfig The configuration for the card + * @param context The parse context for the over all today page + * @param augmentingData that stores some additional responses that may be used to enhance the contents of `data` + * @returns The newly created TodayCard, using an artwork media to display the App of the Day and Game of the Day + */ +export function createTodayBrandedCard(objectGraph, data, cardConfig, context, augmentingData) { + return validation.context("createTodayBrandedCard", () => { + const brandedCard = createTodayBaseCard(objectGraph, data, cardConfig, context); + pushTodayCardLocation(objectGraph, data, cardConfig, context); + // Configure the card style + const mediaDetails = todayCardArtworkDetails(objectGraph, data, cardConfig); + if (isSome(mediaDetails) && + isSome(mediaDetails.joeColors.bgColor) && + (mediaDetails.artworks.length > 0 || mediaDetails.videos.length > 0)) { + brandedCard.style = cardStyleFromJoeColors(mediaDetails === null || mediaDetails === void 0 ? void 0 : mediaDetails.joeColors, "bgColor"); + } + else { + // If we don't have artwork, try building a single app card instead. + popTodayCardLocation(context); + return createTodaySingleAppCard(objectGraph, data, cardConfig, context); + } + const offerStyle = offerStyleForTodayCard(objectGraph, brandedCard.style); + const relatedContent = relatedCardContentsContentsFromData(objectGraph, data); + const relatedContentLockups = lockupsForRelatedContent(objectGraph, relatedContent, cardConfig, context.pageInformation, context.locationTracker, offerEnvironmentForTodayCard(brandedCard.style), offerStyle, deepLinkUrlFromData(objectGraph, data), true); + if (isNullOrEmpty(relatedContentLockups) || relatedContentLockups.length !== 1) { + popTodayCardLocation(context); + return null; + } + if (!cardConfig.isHorizontalShelfContext) { + brandedCard.overlay = relatedContentOverlayFromData(objectGraph, brandedCard, cardConfig, data, relatedContentLockups); + } + // Configure the card media + const brandedAppLockup = relatedContentLockups[0]; + brandedCard.media = new TodayCardMediaBrandedSingleApp(brandedAppLockup.icon, mediaDetails.artworks, mediaDetails.videos, mediaDetails.artworkLayoutsWithMetrics, todayCardArtworkTitleBackingGradientForKey(objectGraph, data, cardConfig)); + brandedCard.media.impressionMetrics = brandedAppLockup.impressionMetrics; + // Special post-processing step for Acquisition story cards. + // This is needed to splice in data not included in initial response. + applyTodayCardOverridesForAcquisitionStoryIfNecessary(objectGraph, brandedCard, cardConfig, cardDisplayStyleFromData(data, cardConfig.coercedCollectionTodayCardDisplayStyle), data, augmentingData, context); + popTodayCardLocation(context); + return brandedCard; + }); +} +//# sourceMappingURL=today-branded-card-builder.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/today/cards/today-full-bleed-image-card-builder.js b/node_modules/@jet-app/app-store/tmp/src/common/today/cards/today-full-bleed-image-card-builder.js new file mode 100644 index 0000000..86cacec --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/today/cards/today-full-bleed-image-card-builder.js @@ -0,0 +1,37 @@ +import * as validation from "@jet/environment/json/validation"; +import { deepLinkUrlFromData } from "../../linking/external-deep-link"; +import { applyTodayCardOverridesForAcquisitionStoryIfNecessary, relatedContentOverlayFromData, } from "../today-card-overlay-util"; +import { cardDisplayStyleFromData, lockupsForRelatedContent, offerEnvironmentForTodayCard, offerStyleForTodayCard, popTodayCardLocation, pushTodayCardLocation, relatedCardContentsContentsFromData, } from "../today-card-util"; +import { addArtworkMediaToBaseCard, createTodayBaseCard } from "./today-base-card-builder"; +/** + * Create TodayCard displaying the full bleed image + * + * @param objectGraph The dependency graph for the App Store + * @param data The media api data to build the card from + * @param cardConfig The configuration for the card + * @param context The parse context for the over all today page + * @param augmentingData that stores some additional responses that may be used to enhance the contents of `data` + * @returns The newly created TodayCard, using the full bleed image media. + */ +export function createTodayFullBleedImageCard(objectGraph, data, cardConfig, context, augmentingData) { + return validation.context("createTodayFullBleedImageCard", () => { + const fullBleedImageCard = createTodayBaseCard(objectGraph, data, cardConfig, context); + pushTodayCardLocation(objectGraph, data, cardConfig, context); + const didAddArtworkToCard = addArtworkMediaToBaseCard(objectGraph, data, fullBleedImageCard, cardConfig, context); + if (!didAddArtworkToCard) { + popTodayCardLocation(context); + return null; + } + const relatedContent = relatedCardContentsContentsFromData(objectGraph, data); + const offerStyle = offerStyleForTodayCard(objectGraph, fullBleedImageCard.style); + const relatedContentLockups = lockupsForRelatedContent(objectGraph, relatedContent, cardConfig, context.pageInformation, context.locationTracker, offerEnvironmentForTodayCard(fullBleedImageCard.style), offerStyle, deepLinkUrlFromData(objectGraph, data)); + const overlay = relatedContentOverlayFromData(objectGraph, fullBleedImageCard, cardConfig, data, relatedContentLockups); + fullBleedImageCard.overlay = overlay; + // Special post-processing step for Acquisition story cards. + // This is needed to splice in data not included in initial response. + applyTodayCardOverridesForAcquisitionStoryIfNecessary(objectGraph, fullBleedImageCard, cardConfig, cardDisplayStyleFromData(data, cardConfig.coercedCollectionTodayCardDisplayStyle), data, augmentingData, context); + popTodayCardLocation(context); + return fullBleedImageCard; + }); +} +//# sourceMappingURL=today-full-bleed-image-card-builder.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/today/cards/today-in-app-purchase-card-builder.js b/node_modules/@jet-app/app-store/tmp/src/common/today/cards/today-in-app-purchase-card-builder.js new file mode 100644 index 0000000..d326c5c --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/today/cards/today-in-app-purchase-card-builder.js @@ -0,0 +1,62 @@ +import * as validation from "@jet/environment/json/validation"; +import { TodayCardInAppPurchase } from "../../../api/models"; +import { isNullOrEmpty } from "../../../foundation/json-parsing/server-data"; +import { inAppPurchaseLockupFromData } from "../../lockups/lockups"; +import { applyTodayCardOverridesForAcquisitionStoryIfNecessary } from "../today-card-overlay-util"; +import { cardDisplayStyleFromData, popTodayCardLocation, pushTodayCardLocation, relatedCardContentsContentsFromData, } from "../today-card-util"; +import { createTodayBaseCard } from "./today-base-card-builder"; +/** + * Create TodayCard displaying an in app purchase + * + * @param objectGraph The dependency graph for the App Store + * @param data The media api data to build the card from + * @param cardConfig The configuration for the card + * @param context The parse context for the over all today page + * @param augmentingData that stores some additional responses that may be used to enhance the contents of `data` + * @returns The newly created TodayCard, using the in app purchase media. + */ +export function createTodayInAppPurchaseCard(objectGraph, data, cardConfig, context, augmentingData) { + return validation.context("createTodayInAppPurchaseCard", () => { + const inAppPurchaseCard = createTodayBaseCard(objectGraph, data, cardConfig, context); + pushTodayCardLocation(objectGraph, data, cardConfig, context); + const iAPData = inAppPurchaseDataFromRelatedContent(objectGraph, relatedCardContentsContentsFromData(objectGraph, data)); + if (isNullOrEmpty(iAPData)) { + popTodayCardLocation(context); + return null; + } + const lockup = inAppPurchaseLockupFromData(objectGraph, iAPData, { + offerStyle: "colored", + metricsOptions: { + pageInformation: context.pageInformation, + locationTracker: context.locationTracker, + }, + artworkUseCase: 1 /* ArtworkUseCase.LockupIconSmall */, + }); + if (isNullOrEmpty(lockup)) { + popTodayCardLocation(context); + return null; + } + lockup.theme = "infer"; + inAppPurchaseCard.media = new TodayCardInAppPurchase(lockup); + // we need to add impressions to the media here because + // there is no overlay on an IAP inAppPurchaseCard for some reason + inAppPurchaseCard.media.impressionMetrics = lockup.impressionMetrics; + inAppPurchaseCard.media.impressionMetrics.fields["parentId"] = inAppPurchaseCard.impressionMetrics.fields["id"]; + inAppPurchaseCard.style = "white"; + // Special post-processing step for Acquisition story cards. + // This is needed to splice in data not included in initial response. + applyTodayCardOverridesForAcquisitionStoryIfNecessary(objectGraph, inAppPurchaseCard, cardConfig, cardDisplayStyleFromData(data, cardConfig.coercedCollectionTodayCardDisplayStyle), data, augmentingData, context); + popTodayCardLocation(context); + return inAppPurchaseCard; + }); +} +function inAppPurchaseDataFromRelatedContent(objectGraph, relatedContent) { + if (relatedContent.length === 1) { + const contentData = relatedContent[0]; + if (contentData.type === "in-apps") { + return contentData; + } + } + return null; +} +//# sourceMappingURL=today-in-app-purchase-card-builder.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/today/cards/today-list-card-builder.js b/node_modules/@jet-app/app-store/tmp/src/common/today/cards/today-list-card-builder.js new file mode 100644 index 0000000..b803c9f --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/today/cards/today-list-card-builder.js @@ -0,0 +1,71 @@ +import * as validation from "@jet/environment/json/validation"; +import { TodayCardMediaList } from "../../../api/models"; +import * as videoDefaults from "../../constants/video-constants"; +import { todayCardArtworkDetails } from "../artwork/today-artwork-util"; +import { applyTodayCardOverridesForAcquisitionStoryIfNecessary } from "../today-card-overlay-util"; +import { applyMultiAppFallbackToCollectionCard, cardDisplayStyleFromData, cardStyleFromJoeColorsWithoutFallback, listFallbackLimit, lockupsForRelatedContent, popTodayCardLocation, pushTodayCardLocation, relatedCardContentsContentsFromData, } from "../today-card-util"; +import { TodayCardDisplayStyle, } from "../today-types"; +import { createTodayBaseCard } from "./today-base-card-builder"; +import { isEligibleForGamesApp } from "../../content/content"; +/** + * Create TodayCard displaying the list of lockups + * + * @param objectGraph The dependency graph for the App Store + * @param data The media api data to build the card from + * @param cardConfig The configuration for the card + * @param context The parse context for the over all today page + * @param augmentingData that stores some additional responses that may be used to enhance the contents of `data` + * @returns The newly created TodayCard, using a list or numbered list media. + */ +export function createTodayListCard(objectGraph, data, cardConfig, context, augmentingData) { + return validation.context("createTodayListCard", () => { + var _a, _b, _c; + const listCard = createTodayBaseCard(objectGraph, data, cardConfig, context); + pushTodayCardLocation(objectGraph, data, cardConfig, context); + cardConfig.useJoeColorIconPlaceholder = true; + let relatedContents = relatedCardContentsContentsFromData(objectGraph, data); + if (preprocessor.GAMES_TARGET) { + // Filtering out non-eligible games as they can be used as content for stories collection. + // see `createFallbackListShelf` in article.ts. + relatedContents = relatedContents.filter((item) => isEligibleForGamesApp(item)); + } + const listLockups = lockupsForRelatedContent(objectGraph, relatedContents, cardConfig, context.pageInformation, context.locationTracker, undefined, undefined, undefined); + if (listLockups.length === 0) { + popTodayCardLocation(context); + return null; + } + cardConfig.canDisplayArcadeOfferButton = false; + // We require at least `listFallbackLimit` lockups for the list card + // If this is for use on top of an article its ok to have less than `listFallbackLimit` + if (listLockups.length < listFallbackLimit(objectGraph) && cardConfig.enableListCardToMultiAppFallback) { + applyMultiAppFallbackToCollectionCard(objectGraph, data, listLockups, listCard); + } + else { + const cardDisplayStyle = cardDisplayStyleFromData(data, cardConfig.coercedCollectionTodayCardDisplayStyle); + if (cardDisplayStyle === TodayCardDisplayStyle.NumberedList) { + let index = 1; + for (const lockup of listLockups) { + lockup.ordinal = `${index}`; + index++; + } + } + const mediaDetails = todayCardArtworkDetails(objectGraph, data, cardConfig); + const cardStyle = cardStyleFromJoeColorsWithoutFallback(mediaDetails === null || mediaDetails === void 0 ? void 0 : mediaDetails.joeColors, "bgColor"); + const isModernListCard = objectGraph.host.isiOS; + // Since list videos are very wide (21:9) and ~50% masked, we prevent them from going fullscreen + mediaDetails === null || mediaDetails === void 0 ? void 0 : mediaDetails.videos.forEach((video) => { + video.canPlayFullScreen = false; + video.playbackControls = videoDefaults.noControls(objectGraph); + video.autoPlayPlaybackControls = videoDefaults.noControls(objectGraph); + }); + listCard.style = isModernListCard ? undefined : "white"; + listCard.media = new TodayCardMediaList(listLockups, (_a = mediaDetails === null || mediaDetails === void 0 ? void 0 : mediaDetails.artworks) !== null && _a !== void 0 ? _a : [], (_b = mediaDetails === null || mediaDetails === void 0 ? void 0 : mediaDetails.videos) !== null && _b !== void 0 ? _b : [], (_c = mediaDetails === null || mediaDetails === void 0 ? void 0 : mediaDetails.artworkLayoutsWithMetrics) !== null && _c !== void 0 ? _c : [], undefined, isModernListCard ? cardStyle !== "white" : cardStyle === "dark"); + } + // Special post-processing step for Acquisition story cards. + // This is needed to splice in data not included in initial response. + applyTodayCardOverridesForAcquisitionStoryIfNecessary(objectGraph, listCard, cardConfig, cardDisplayStyleFromData(data, cardConfig.coercedCollectionTodayCardDisplayStyle), data, augmentingData, context); + popTodayCardLocation(context); + return listCard; + }); +} +//# sourceMappingURL=today-list-card-builder.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/today/cards/today-river-card-builder.js b/node_modules/@jet-app/app-store/tmp/src/common/today/cards/today-river-card-builder.js new file mode 100644 index 0000000..d93fa7e --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/today/cards/today-river-card-builder.js @@ -0,0 +1,62 @@ +import * as validation from "@jet/environment/json/validation"; +import { TodayCardMediaRiver } from "../../../api/models"; +import { isDarkColor, rgbWith } from "../../../foundation/util/color-util"; +import { applyTodayCardOverridesForAcquisitionStoryIfNecessary, relatedContentOverlayFromData, } from "../today-card-overlay-util"; +import { applyMultiAppFallbackToCollectionCard, cardDisplayStyleFromData, gridFallbackLimit, lockupsForCollectionCardFromData, popTodayCardLocation, pushTodayCardLocation, } from "../today-card-util"; +import { createTodayBaseCard } from "./today-base-card-builder"; +/** + * Create TodayCard displaying the river of lockups + * + * @param objectGraph The dependency graph for the App Store + * @param data The media api data to build the card from + * @param cardConfig The configuration for the card + * @param context The parse context for the over all today page + * @param augmentingData that stores some additional responses that may be used to enhance the contents of `data` + * @returns The newly created TodayCard, using a river media. + */ +export function createTodayRiverCard(objectGraph, data, cardConfig, context, augmentingData) { + return validation.context("createTodayRiverCard", () => { + const riverCard = createTodayBaseCard(objectGraph, data, cardConfig, context); + cardConfig.useJoeColorIconPlaceholder = true; + pushTodayCardLocation(objectGraph, data, cardConfig, context); + const shouldIncludeLockupClickActions = objectGraph.client.isWeb; + const riverLockups = lockupsForCollectionCardFromData(objectGraph, data, cardConfig, context, shouldIncludeLockupClickActions); + if (riverLockups.length === 0) { + popTodayCardLocation(context); + return null; + } + cardConfig.canDisplayArcadeOfferButton = false; + if (riverLockups.length < gridFallbackLimit(objectGraph)) { + // <rdar://problem/37261537> P2: Some Today cards not showing on Cinar + // less that `gridFallbackLimit` lockups gets a fallback card on Emet only + applyMultiAppFallbackToCollectionCard(objectGraph, data, riverLockups, riverCard); + } + else { + riverCard.style = "dark"; + replaceHighLuminanceIconColorsInLockups(riverLockups); + riverCard.media = new TodayCardMediaRiver(riverLockups); + } + if (objectGraph.client.isWatch) { + riverCard.overlay = relatedContentOverlayFromData(objectGraph, riverCard, cardConfig, data, riverLockups); + } + // Special post-processing step for Acquisition story cards. + // This is needed to splice in data not included in initial response. + applyTodayCardOverridesForAcquisitionStoryIfNecessary(objectGraph, riverCard, cardConfig, cardDisplayStyleFromData(data, cardConfig.coercedCollectionTodayCardDisplayStyle), data, augmentingData, context); + popTodayCardLocation(context); + return riverCard; + }); +} +/** + * Replace any icon background colors with luminance > 0.9 with grey color. + * This ensure that river background colors don't compete with the overlayed white text. + * + * @param riverLockups The river lockups. + */ +function replaceHighLuminanceIconColorsInLockups(riverLockups) { + for (const lockup of riverLockups) { + if (!isDarkColor(lockup.icon.backgroundColor, 90)) { + lockup.icon.backgroundColor = rgbWith(0.8, 0.8, 0.8); + } + } +} +//# sourceMappingURL=today-river-card-builder.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/today/cards/today-short-image-card-builder.js b/node_modules/@jet-app/app-store/tmp/src/common/today/cards/today-short-image-card-builder.js new file mode 100644 index 0000000..c50836a --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/today/cards/today-short-image-card-builder.js @@ -0,0 +1,42 @@ +import * as validation from "@jet/environment/json/validation"; +import { deepLinkUrlFromData } from "../../linking/external-deep-link"; +import { applyTodayCardOverridesForAcquisitionStoryIfNecessary, relatedContentOverlayFromData, } from "../today-card-overlay-util"; +import { cardDisplayStyleFromData, lockupsForRelatedContent, offerEnvironmentForTodayCard, offerStyleForTodayCard, popTodayCardLocation, pushTodayCardLocation, relatedCardContentsContentsFromData, } from "../today-card-util"; +import { addArtworkMediaToBaseCard, createTodayBaseCard } from "./today-base-card-builder"; +import { addSingleAppFallbackToCard } from "./today-single-app-card-builder"; +/** + * Create TodayCard displaying the full bleed image + * + * @param objectGraph The dependency graph for the App Store + * @param data The media api data to build the card from + * @param cardConfig The configuration for the card + * @param context The parse context for the over all today page + * @param augmentingData that stores some additional responses that may be used to enhance the contents of `data` + * @returns The newly created TodayCard, using the full bleed image media. + */ +export function createTodayShortImageCard(objectGraph, data, cardConfig, context, augmentingData) { + return validation.context("createTodayShortImageCard", () => { + const shortImageCard = createTodayBaseCard(objectGraph, data, cardConfig, context); + pushTodayCardLocation(objectGraph, data, cardConfig, context); + const didAddArtworkToCard = addArtworkMediaToBaseCard(objectGraph, data, shortImageCard, cardConfig, context); + let didAddSingleAppFallback = false; + const relatedContent = relatedCardContentsContentsFromData(objectGraph, data); + const offerStyle = offerStyleForTodayCard(objectGraph, shortImageCard.style); + const relatedContentLockups = lockupsForRelatedContent(objectGraph, relatedContent, cardConfig, context.pageInformation, context.locationTracker, offerEnvironmentForTodayCard(shortImageCard.style), offerStyle, deepLinkUrlFromData(objectGraph, data)); + const overlay = relatedContentOverlayFromData(objectGraph, shortImageCard, cardConfig, data, relatedContentLockups); + shortImageCard.overlay = overlay; + if (!didAddArtworkToCard && relatedContent.length === 1) { + didAddSingleAppFallback = addSingleAppFallbackToCard(objectGraph, data, shortImageCard, relatedContent, cardConfig, context); + } + if (!didAddArtworkToCard && !didAddSingleAppFallback) { + popTodayCardLocation(context); + return null; + } + // Special post-processing step for Acquisition story cards. + // This is needed to splice in data not included in initial response. + applyTodayCardOverridesForAcquisitionStoryIfNecessary(objectGraph, shortImageCard, cardConfig, cardDisplayStyleFromData(data, cardConfig.coercedCollectionTodayCardDisplayStyle), data, augmentingData, context); + popTodayCardLocation(context); + return shortImageCard; + }); +} +//# sourceMappingURL=today-short-image-card-builder.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/today/cards/today-single-app-card-builder.js b/node_modules/@jet-app/app-store/tmp/src/common/today/cards/today-single-app-card-builder.js new file mode 100644 index 0000000..18828c8 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/today/cards/today-single-app-card-builder.js @@ -0,0 +1,81 @@ +import * as validation from "@jet/environment/json/validation"; +import { TodayCardMediaAppIcon, TodayCardMediaList, TodayCardParagraphOverlay } from "../../../api/models"; +import { isDefinedNonNull } from "../../../foundation/json-parsing/server-data"; +import { deepLinkUrlFromData } from "../../linking/external-deep-link"; +import { applyTodayCardOverridesForAcquisitionStoryIfNecessary, relatedContentOverlayFromData, } from "../today-card-overlay-util"; +import { cardDisplayStyleFromData, lockupsForRelatedContent, offerEnvironmentForTodayCard, offerStyleForTodayCard, popTodayCardLocation, pushTodayCardLocation, relatedCardContentsContentsFromData, } from "../today-card-util"; +import { addArtworkMediaToBaseCard, createTodayBaseCard } from "./today-base-card-builder"; +/** + * Create TodayCard displaying the SingleApp card display style + * + * @param objectGraph The dependency graph for the App Store + * @param data The media api data to build the card from + * @param cardConfig The configuration for the card + * @param context The parse context for the over all today page + * @param augmentingData that stores some additional responses that may be used to enhance the contents of `data` + * @returns The newly created TodayCard, either artwork or the single app icon. + */ +export function createTodaySingleAppCard(objectGraph, data, cardConfig, context, augmentingData) { + return validation.context("createTodaySingleAppCard", () => { + const singleAppCard = createTodayBaseCard(objectGraph, data, cardConfig, context); + pushTodayCardLocation(objectGraph, data, cardConfig, context); + // Next prefer artwork card, and then fallback to substyles + const didAddArtworkToCard = addArtworkMediaToBaseCard(objectGraph, data, singleAppCard, cardConfig, context); + const relatedContent = relatedCardContentsContentsFromData(objectGraph, data); + const relatedContentLockups = lockupsForRelatedContent(objectGraph, relatedContent, cardConfig, context.pageInformation, context.locationTracker, "todayCard", null, deepLinkUrlFromData(objectGraph, data)); + const overlay = relatedContentOverlayFromData(objectGraph, singleAppCard, cardConfig, data, relatedContentLockups); + if (isDefinedNonNull(overlay) && overlay instanceof TodayCardParagraphOverlay) { + overlay.style = "white"; + } + singleAppCard.overlay = overlay; + if (!didAddArtworkToCard && relatedContent.length === 1) { + const didAddSingleAppFallback = addSingleAppFallbackToCard(objectGraph, data, singleAppCard, relatedContent, cardConfig, context); + if (!didAddSingleAppFallback) { + popTodayCardLocation(context); + return null; + } + } + // Special post-processing step for Acquisition story cards. + // This is needed to splice in data not included in initial response. + applyTodayCardOverridesForAcquisitionStoryIfNecessary(objectGraph, singleAppCard, cardConfig, cardDisplayStyleFromData(data, cardConfig.coercedCollectionTodayCardDisplayStyle), data, augmentingData, context); + popTodayCardLocation(context); + return singleAppCard; + }); +} +/** + * Adds a single app fallback to the card. + * + * @param objectGraph - The object graph of the app store. + * @param data - The media data. + * @param card - The today card. + * @param relatedContent - The related content data. + * @param cardConfig - The configuration for the today card. + * @param context - The parse context for today card. + * @returns Returns true or false based on whether we could create a single app fallback. + */ +export function addSingleAppFallbackToCard(objectGraph, data, card, relatedContent, cardConfig, context) { + if (relatedContent.length !== 1) { + return false; + } + cardConfig.useJoeColorIconPlaceholder = true; + card.style = "dark"; + const offerStyle = offerStyleForTodayCard(objectGraph, card.style); + const relatedContentLockups = lockupsForRelatedContent(objectGraph, relatedContent, cardConfig, context.pageInformation, context.locationTracker, offerEnvironmentForTodayCard(card.style), offerStyle, deepLinkUrlFromData(objectGraph, data)); + if (relatedContentLockups.length !== 1) { + return false; + } + const lockup = relatedContentLockups[0]; + // tvOS doesn't have layout to properly render appIcon media. + // However, `media` can't be simply removed from TodayCard, as it is a required field in native code. + // This `TodayCardMediaList` works as an empty media placeholder, + // that allows to render text from TodayCard but not the image. + // From StarlightB AppIcon is disabled in native code and this check can be removed in the next major release. + if (objectGraph.client.isTV && !objectGraph.props.enabled("disableAppIconMediaTodayHeader")) { + card.media = new TodayCardMediaList([], [], [], [], undefined, undefined); + } + else { + card.media = new TodayCardMediaAppIcon(lockup.icon); + } + return true; +} +//# sourceMappingURL=today-single-app-card-builder.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/today/cards/today-video-card-builder.js b/node_modules/@jet-app/app-store/tmp/src/common/today/cards/today-video-card-builder.js new file mode 100644 index 0000000..77516f6 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/today/cards/today-video-card-builder.js @@ -0,0 +1,33 @@ +import * as validation from "@jet/environment/json/validation"; +import { deepLinkUrlFromData } from "../../linking/external-deep-link"; +import { applyTodayCardOverridesForAcquisitionStoryIfNecessary, relatedContentOverlayFromData, } from "../today-card-overlay-util"; +import { cardDisplayStyleFromData, lockupsForRelatedContent, offerStyleForTodayCard, popTodayCardLocation, pushTodayCardLocation, relatedCardContentsContentsFromData, } from "../today-card-util"; +import { addArtworkMediaToBaseCard, createTodayBaseCard } from "./today-base-card-builder"; +/** + * Create TodayCard displaying a video + * + * @param objectGraph The dependency graph for the App Store + * @param data The media api data to build the card from + * @param cardConfig The configuration for the card + * @param context The parse context for the over all today page + * @param augmentingData that stores some additional responses that may be used to enhance the contents of `data` + * @returns The newly created TodayCard, using the video media. + */ +export function createTodayVideoCard(objectGraph, data, cardConfig, context, augmentingData) { + return validation.context("createTodayVideoCard", () => { + const videoCard = createTodayBaseCard(objectGraph, data, cardConfig, context); + addArtworkMediaToBaseCard(objectGraph, data, videoCard, cardConfig, context); + pushTodayCardLocation(objectGraph, data, cardConfig, context); + const offerStyle = offerStyleForTodayCard(objectGraph, videoCard.style); + const relatedContent = relatedCardContentsContentsFromData(objectGraph, data); + const relatedContentLockups = lockupsForRelatedContent(objectGraph, relatedContent, cardConfig, context.pageInformation, context.locationTracker, "todayCard", offerStyle, deepLinkUrlFromData(objectGraph, data)); + const overlay = relatedContentOverlayFromData(objectGraph, videoCard, cardConfig, data, relatedContentLockups); + videoCard.overlay = overlay; + // Special post-processing step for Acquisition story cards. + // This is needed to splice in data not included in initial response. + applyTodayCardOverridesForAcquisitionStoryIfNecessary(objectGraph, videoCard, cardConfig, cardDisplayStyleFromData(data, cardConfig.coercedCollectionTodayCardDisplayStyle), data, augmentingData, context); + popTodayCardLocation(context); + return videoCard; + }); +} +//# sourceMappingURL=today-video-card-builder.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/today/onboarding-cards.js b/node_modules/@jet-app/app-store/tmp/src/common/today/onboarding-cards.js new file mode 100644 index 0000000..86af694 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/today/onboarding-cards.js @@ -0,0 +1,23 @@ +/** + * Created by ls on 6/23/17. + */ +import * as serverData from "../../foundation/json-parsing/server-data"; +const isOnboardingCardKey = "isOnBoardingCard"; +/** + * Tag a data collection as onboarding cards for metrics. + * @param cardsData + */ +export function markDataCollectionAsOnboardingCards(objectGraph, cardsData) { + for (const card of cardsData) { + card[isOnboardingCardKey] = true; + } +} +/** + * Check whether given data was injected as onboarding card. + * @param cardData Media API data for single card. + * @returns A boolean returning `true` if `cardData` was from `makeOnBoardingCardToken`, `false` otherwise. + */ +export function isCardDataOnboardingCard(objectGraph, cardData) { + return serverData.asBooleanOrFalse(cardData, isOnboardingCardKey); +} +//# sourceMappingURL=onboarding-cards.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/today/routable-article-page-url-utils.js b/node_modules/@jet-app/app-store/tmp/src/common/today/routable-article-page-url-utils.js new file mode 100644 index 0000000..5603a86 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/today/routable-article-page-url-utils.js @@ -0,0 +1,32 @@ +import { makeRoutableArticlePageIntent, } from "../../api/intents/routable-article-page-intent"; +import { generateRoutes } from "../util/generate-routes"; +const { routes: routableArticlePageWithPlatformRoutes, makeCanonicalUrl: makeRoutableArticlePageWithPlatformUrl } = generateRoutes(makeRoutableArticlePageIntent, "/{platform}/story/{id}"); +const { routes: routableArticlePageWithoutPlatformRoutes, makeCanonicalUrl: makeRoutableArticlePageWithoutPlatformUrl, } = generateRoutes(makeRoutableArticlePageIntent, "/story/{id}"); +export { routableArticlePageWithPlatformRoutes, routableArticlePageWithoutPlatformRoutes }; +/** + * Generate the URL for an "Article Page" based on the {@linkcode intent} + * + * If the {@linkcode intent} has an explicitly-configured `platform` property, + * then the resulting URL will include the explicitly as the `{platform}` prefix + * segment. If the `platform` property is not provided, the resulting URL will + * not contain an explicit `{platform}` segment at all. + * + * @param objectGraph + * @param intent + * @returns the URL that, when parsed, produces the given `intent` + */ +export function makeRoutableArticlePageCanonicalUrl(objectGraph, intent) { + const intentWithExpectedID = { + ...intent, + // The `{id}` segment of the URL is expected to include the `id` prefix, which is removed + // when parsing the incoming URL into the `Intent` + id: `id${intent.id}`, + }; + if (intent.platform) { + return makeRoutableArticlePageWithPlatformUrl(objectGraph, intentWithExpectedID); + } + else { + return makeRoutableArticlePageWithoutPlatformUrl(objectGraph, intentWithExpectedID); + } +} +//# sourceMappingURL=routable-article-page-url-utils.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/today/today-card-overlay-util.js b/node_modules/@jet-app/app-store/tmp/src/common/today/today-card-overlay-util.js new file mode 100644 index 0000000..ab0829f --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/today/today-card-overlay-util.js @@ -0,0 +1,111 @@ +import { isSome } from "@jet/environment"; +import { isNothing } from "@jet/environment/types/optional"; +import { marketingItemContextFromString, TodayCardArcadeLockupOverlay, TodayCardLockupListOverlay, TodayCardLockupOverlay, TodayCardThreeLineOverlay, } from "../../api/models"; +import * as mediaAttributes from "../../foundation/media/attributes"; +import { upsellFromContentsOfUpsellResponse, upsellFromRelationshipOf, } from "../arcade/arcade-common"; +import { extractEditorialClientParams } from "../editorial-pages/editorial-data-util"; +import { arcadeLockupFromData } from "../lockups/lockups"; +import { todayCardArtworkDetails } from "./artwork/today-artwork-util"; +import { cardStyleFromJoeColors, offerStyleForTodayCard } from "./today-card-util"; +// MARK: - Paragraph / Related Content Overlays +/** + * @param objectGraph The dependency graph for the App Store + * @param card The card being built that we'd like to add the overlay to + * @param cardConfig The configuration used to create the card + * @param data The media api data to build the card from + * @param relatedContentLockups The lockups created from the related content + * @returns A TodayCardLockupOverlay, TodayCardLockupListOverlay, TodayCardThreeLineOverlay, depending on whether + * the card should ignore the short notes, has a deep link, or has related content lockups. + */ +export function relatedContentOverlayFromData(objectGraph, card, cardConfig, data, relatedContentLockups) { + const editorialClientParams = extractEditorialClientParams(objectGraph, data); + if (isSome(editorialClientParams.suppressLockup) && editorialClientParams.suppressLockup) { + return null; + } + let overlay = null; + const hasSingleLockup = isSome(relatedContentLockups) && relatedContentLockups.length === 1; + const hasMultipleLockups = isSome(relatedContentLockups) && relatedContentLockups.length > 1; + const hasThreeLineCards = objectGraph.client.isMac || objectGraph.client.isWatch; + if (hasThreeLineCards) { + overlay = new TodayCardThreeLineOverlay(card.heading, card.title, card.inlineDescription); + } + else if (hasSingleLockup) { + overlay = new TodayCardLockupOverlay(relatedContentLockups[0]); + } + else if (hasMultipleLockups) { + overlay = new TodayCardLockupListOverlay(relatedContentLockups); + } + return overlay; +} +// MARK: - Arcade Acquisitions +/** + * Applies a set of overrides to an existing `card` built by the standard pipelie using the contents of augmenting data fetched externally. + * This function provides support for parsing editorial-items fetched for: + * - Today + * - Articles (with data augmentation) + * - Groupings (with data augmentation) + * + * @param card Card to override behavior of. + * @param data Original data `card` was created with + * @param augmentingData Data used to augment `card`. + * @param metricsContext Metrics context to use for overrides. + * @returns Overridden Card, or `null` if overrides failed. Note that `card` is modified *IN PLACE*. + */ +export function applyTodayCardOverridesForAcquisitionStoryIfNecessary(objectGraph, card, cardConfig, cardDisplayStyle, data, augmentingData, context) { + const isAcquisitionStory = mediaAttributes.attributeAsBooleanOrFalse(data, "isAcquisition"); + if (!isAcquisitionStory) { + return; + } + // Try to build upsell data from upsell relationship, if that exists. + let upsellData = upsellFromRelationshipOf(objectGraph, data); + if (isNothing(upsellData) && isSome(augmentingData)) { + // Fallback: Try to build off upsell data off `augmentingData`. This relies on the caller being proactive about fetching upsell separately for endpoints that don't return upsell relationship. + upsellData = upsellFromContentsOfUpsellResponse(objectGraph, augmentingData.arcadeUpsellEditorialResponse); + } + // Override: Arcade Overlay + const arcadeLockupOverlay = arcadeOverlayFromData(objectGraph, data, card, cardConfig, cardDisplayStyle, upsellData, context); + if (arcadeLockupOverlay) { + card.overlay = arcadeLockupOverlay; + card.impressionMetrics.fields["displaysArcadeUpsell"] = true; + // If we're overriding the card style used with the overlay we should also update the style for the card itself. + const style = cardStyleForArcadeOverlay(objectGraph, data, card, cardConfig); + if (card.style !== style) { + card.style = style; + } + } +} +/** + * Whether or not current platform supports arcade overlay. + */ +function currentPlatformSupportsArcadeOverlay(objectGraph) { + // Check if on platform that will actually display data. + const platform = objectGraph.host.platform; + const platformSupportsArcadeLockupOverlay = platform === "iOS" || platform === "macOS"; + return platformSupportsArcadeLockupOverlay; +} +/** + * Creates an instance of `TodayCardArcadeLockupOverlay` to use for Acquisition Editorial Items. + * @param upsellData Upsell data containing editorial and iAP data. + * @param metricsContext Metrics context to use for generated lockup. + */ +function arcadeOverlayFromData(objectGraph, data, card, cardConfig, cardDisplayStyle, upsellData, context) { + if (!currentPlatformSupportsArcadeOverlay(objectGraph)) { + return null; + } + const style = cardStyleForArcadeOverlay(objectGraph, data, card, cardConfig); + const offerStyle = offerStyleForTodayCard(objectGraph, style); + const lockup = arcadeLockupFromData(objectGraph, upsellData, context, marketingItemContextFromString("editorialItem"), offerStyle, "todayCard"); + return new TodayCardArcadeLockupOverlay(lockup); +} +/** + * Returns the `TodayCardStyle` that should be used for the arcade overlay. + * Some cards use different styles when fetched as part of an article vs. today feed. Using the media artwork to determine the + * cardStyle in these cases avoids issues with today cards changing styles when transitionin from to today feed to articles. + */ +function cardStyleForArcadeOverlay(objectGraph, data, card, cardConfig) { + const useCardStyle = !cardConfig.enableListCardToMultiAppFallback; + const mediaDetails = todayCardArtworkDetails(objectGraph, data, cardConfig); + const styleFromMedia = cardStyleFromJoeColors(mediaDetails === null || mediaDetails === void 0 ? void 0 : mediaDetails.joeColors); + return useCardStyle ? card.style : styleFromMedia; +} +//# sourceMappingURL=today-card-overlay-util.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/today/today-card-util.js b/node_modules/@jet-app/app-store/tmp/src/common/today/today-card-util.js new file mode 100644 index 0000000..688f0fb --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/today/today-card-util.js @@ -0,0 +1,785 @@ +import { isNothing, isSome } from "@jet/environment"; +import * as validation from "@jet/environment/json/validation"; +import { ArtworkContentMode, ExternalUrlAction, FlowAction, TodayCardActionOverlay, TodayCardMediaArtwork, TodayCardMediaHero, TodayCardMediaMultiApp, TodayCardMediaVideo, TodayCardMediaWithArtwork, } from "../../api/models"; +import { asBoolean, asDictionary, asString, isDefinedNonNull, isDefinedNonNullNonEmpty, isNull, isNullOrEmpty, objectPathToString, } from "../../foundation/json-parsing/server-data"; +import * as mediaAttributes from "../../foundation/media/attributes"; +import { relationshipCollection } from "../../foundation/media/relationships"; +import { Parameters } from "../../foundation/network/url-constants"; +import { URL } from "../../foundation/network/urls"; +import * as color from "../../foundation/util/color-util"; +import { artworkFromApiArtwork, editorialNotesFromData, hasMessagesExtensionFromData, isHiddenFromSpringboardFromData, joeColorSetFromData, notesFromData, screenSizeIPhone134, } from "../content/content"; +import { extractEditorialClientParams } from "../editorial-pages/editorial-data-util"; +import * as lockupsEditorialContext from "../lockups/editorial-context"; +import { lockupsFromData } from "../lockups/lockups"; +import * as metricsHelpersLocation from "../metrics/helpers/location"; +import * as metricsHelpersImpressions from "./../metrics/helpers/impressions"; +import { createTodayAppEventCard } from "./cards/today-app-event-card-builder"; +import { createTodayBrandedCard } from "./cards/today-branded-card-builder"; +import { createTodayFullBleedImageCard } from "./cards/today-full-bleed-image-card-builder"; +import { createTodayGridCard } from "./cards/today-grid-card-builder"; +import { createTodayInAppPurchaseCard } from "./cards/today-in-app-purchase-card-builder"; +import { createTodayListCard } from "./cards/today-list-card-builder"; +import { createTodayRiverCard } from "./cards/today-river-card-builder"; +import { createTodayShortImageCard } from "./cards/today-short-image-card-builder"; +import { createTodaySingleAppCard } from "./cards/today-single-app-card-builder"; +import { createTodayVideoCard } from "./cards/today-video-card-builder"; +import { HeroMediaDisplayContext, OfTheDayIntention, TodayCardDisplayStyle, } from "./today-types"; +import { createTodayBaseCard } from "./cards/today-base-card-builder"; +// MARK: - Today Card Creation +/** + * Create a card with some appearance configuration within today page. + * @param {AppStoreObjectGraph} objectGraph The object graph for the app store. + * @param {Data} data to build card of. + * @param {TodayCardConfiguration} cardConfig flags on how to configure a given card. + * @param {TodayParseContext} context to store state updated throughout parsing. + * @param {TodayCardAugmentingData} augmentingData that stores some additional responses that may be used to enhance the contents of `data` + */ +export function todayCardFromData(objectGraph, data, cardConfig, context, augmentingData) { + return validation.catchingContext("todayCardFromData", () => { + const cardDisplayStyle = cardDisplayStyleFromData(data, cardConfig.coercedCollectionTodayCardDisplayStyle); + const clientIdentifier = lockupsEditorialContext.clientIdentifierForEditorialContextInData(objectGraph, data); + // If clientIdentifier is empty, don't override the cardConfig as this may have been set + // at a higher level. + if (isSome(clientIdentifier)) { + if (shouldRespectClientIdentifierOverride(objectGraph, data, clientIdentifier, cardConfig)) { + cardConfig.clientIdentifierOverride = clientIdentifier; + } + else { + cardConfig.clientIdentifierOverride = null; + } + } + // Configure subtitle for cross link. + cardConfig.crossLinkSubtitle = crossLinkSubtitleFromData(objectGraph, data); + let card = null; + switch (cardDisplayStyle) { + case TodayCardDisplayStyle.AppOfTheDay: + case TodayCardDisplayStyle.GameOfTheDay: + card = createTodayBrandedCard(objectGraph, data, cardConfig, context, augmentingData); + break; + case TodayCardDisplayStyle.Video: + card = createTodayVideoCard(objectGraph, data, cardConfig, context, augmentingData); + break; + case TodayCardDisplayStyle.FullBleedImage: + card = createTodayFullBleedImageCard(objectGraph, data, cardConfig, context, augmentingData); + break; + case TodayCardDisplayStyle.InAppPurchase: + card = createTodayInAppPurchaseCard(objectGraph, data, cardConfig, context, augmentingData); + break; + case TodayCardDisplayStyle.AppEventCard: + card = createTodayAppEventCard(objectGraph, data, cardConfig, context, augmentingData); + break; + case TodayCardDisplayStyle.List: + case TodayCardDisplayStyle.NumberedList: + card = createTodayListCard(objectGraph, data, cardConfig, context, augmentingData); + break; + case TodayCardDisplayStyle.River: + case TodayCardDisplayStyle.Grid: + if (objectGraph.client.isMac) { + card = createTodayGridCard(objectGraph, data, cardConfig, context, augmentingData); + } + else { + card = createTodayRiverCard(objectGraph, data, cardConfig, context, augmentingData); + } + break; + case TodayCardDisplayStyle.SingleApp: + card = createTodaySingleAppCard(objectGraph, data, cardConfig, context, augmentingData); + break; + case TodayCardDisplayStyle.ShortImage: + card = createTodayShortImageCard(objectGraph, data, cardConfig, context, augmentingData); + break; + default: + card = null; + break; + } + if (isNull(card)) { + objectGraph.console.log(`Unknown style: ${cardDisplayStyle}`); + return card; + } + // For certain platforms we add hero media to the card. + addHeroMediaToTodayCardIfNecessary(objectGraph, card, cardConfig, data); + addExternalLinkOverlayToTodayCardIfNecessary(card); + addOTDStyleToCardIfNecessary(card, cardConfig); + enableFlipAndBlurIfNecessary(objectGraph, card); + if (isNothing(card.media)) { + objectGraph.console.log(`Missing required media: ${cardDisplayStyle}`); + card = null; + } + return card; + }, (error) => { + objectGraph.console.log(error); + return null; + }); +} +/** + * On the watch we need the todayCard on the article to get the title and subtitle, + * so even if we dont successfully make a card for the article we create a basic one with an empty media so the title / subtitle can be used + * @param {AppStoreObjectGraph} objectGraph The object graph for the app store. + * @param {Data} data to build card of. + * @param {TodayCardConfiguration} cardConfig flags on how to configure a given card. + * @param {TodayParseContext} context to store state updated throughout parsing. + */ +export function fallbackWatchTodayCardFromData(objectGraph, data, cardConfig, context) { + if (!objectGraph.client.isWatch) { + return null; + } + const fallbackCard = createTodayBaseCard(objectGraph, data, cardConfig, context); + fallbackCard.media = new TodayCardMediaArtwork([], [], []); + return fallbackCard; +} +/** + * Create the {EditorialDisplayOptions for a card given the MAPI data + * @param objectGraph The dependency graph for the App Store + * @param data The MAPI data for this today card + * @param cardConfig The configuration for the card + * @returns The editorialDisplayOptions for this card + */ +export function todayCardEditorialDisplayOptionsFromData(objectGraph, data, cardConfig) { + var _a; + // here is where we make the display options + const editorialClientParams = extractEditorialClientParams(objectGraph, data); + const isExtraWideCard = objectGraph.client.isPad && (cardConfig === null || cardConfig === void 0 ? void 0 : cardConfig.isHeroCard); + const editorialDisplayOptions = { + suppressTagline: mediaAttributes.attributeAsBoolean(data, "ignoreITunesShortNotes"), + suppressLockup: asBoolean(editorialClientParams["suppressLockup"]), + showBadgeInSmallCards: (_a = cardConfig.alwaysShowBadgeInSmallCards) !== null && _a !== void 0 ? _a : asBoolean(editorialClientParams["showBadgeInSmallCards"]), + useMaterialBlur: cardConfig.alwaysUseMaterialBlur || isExtraWideCard || asBoolean(editorialClientParams["useMaterialBlur"]), + }; + return editorialDisplayOptions; +} +// MARK: - Today Configuration +/** + * Create the "default" card configuration that callers can generally base their configuration from. + */ +export function defaultTodayCardConfiguration(objectGraph) { + return { + useOTDTextStyle: false, + enableFullScreenVideo: true, + enableListCardToMultiAppFallback: true, + canDisplayArcadeOfferButton: true, + isHeroCard: false, + }; +} +// MARK: - Metrics / Location Tracking +/** + * Allows for specialized card builders to share the same location tracking push / pop code. + * + * @param objectGraph The dependency graph for the App Store + * @param data The media api data to get location information from + * @param cardConfig The configuration for the card + * @param context The parse context for the over all today page + */ +export function pushTodayCardLocation(objectGraph, data, cardConfig, context, titleOverride) { + const title = isDefinedNonNullNonEmpty(titleOverride) + ? titleOverride + : todayCardTitleFromData(objectGraph, data, cardDisplayStyleFromData(data, cardConfig.coercedCollectionTodayCardDisplayStyle)); + metricsHelpersLocation.pushContentLocation(objectGraph, todayCardMetricsOptions(objectGraph, data, cardConfig, context, title), title !== null && title !== void 0 ? title : ""); +} +export function todayCardMetricsOptions(objectGraph, data, cardConfig, context, titleOverride) { + var _a; + return metricsHelpersImpressions.impressionOptions(objectGraph, data, titleOverride !== null && titleOverride !== void 0 ? titleOverride : "", { + ...cardConfig === null || cardConfig === void 0 ? void 0 : cardConfig.baseMetricsOptions, + targetType: "todayCard", + pageInformation: context.pageInformation, + locationTracker: context.locationTracker, + isAdEligible: (_a = cardConfig === null || cardConfig === void 0 ? void 0 : cardConfig.isAdEligible) !== null && _a !== void 0 ? _a : false, + optimizationId: asString(data, "meta.personalizationData.optimizationId"), + optimizationEntityId: asString(data, "meta.personalizationData.optimizationEntityId"), + rowIndex: cardConfig === null || cardConfig === void 0 ? void 0 : cardConfig.currentRowIndex, + displayStyle: cardConfig === null || cardConfig === void 0 ? void 0 : cardConfig.metricsDisplayStyle, + }); +} +/** + * Pop the today card location that was pushed by the pushTodayCardLocation function. + * + * @param context The parse context for the over all today page + */ +export function popTodayCardLocation(context) { + metricsHelpersLocation.popLocation(context.locationTracker); +} +// MARK: - Card Title +/** + * @param objectGraph The dependency graph for the App Store + * @param data The media api data used to determine the title of the card + * @param cardDisplayStyle The display style of the card + * @returns The title of the card + */ +export function todayCardTitleFromData(objectGraph, data, cardDisplayStyle) { + let title = notesFromData(objectGraph, data, "name"); + if (isNullOrEmpty(title)) { + switch (cardDisplayStyle) { + case TodayCardDisplayStyle.AppOfTheDay: + case TodayCardDisplayStyle.GameOfTheDay: + title = mediaAttributes.attributeAsString(data, "label"); + break; + default: + break; + } + } + return title; +} +// MARK: - Card Display Styles +/** + * These are today card types that can be coerced into an override type, there are times where we want to + * have all List cards for instance, render as grids, this set denotes the types that can be coerced. + */ +const coercibleTodayCardStyles = new Set([ + TodayCardDisplayStyle.Grid, + TodayCardDisplayStyle.List, + TodayCardDisplayStyle.NumberedList, + TodayCardDisplayStyle.River, +]); +/** + * Find the today card style for the given data, allowing coercion if necessary. + * @param data The media api data for an editorial item + * @param styleOverride The override style to use if the data has a style that can be coerced + * @returns The resolved todayCardDisplayStyle + */ +export function cardDisplayStyleFromData(data, styleOverride) { + const cardDisplayStyle = mediaAttributes.attributeAsString(data, "cardDisplayStyle"); + if (coercibleTodayCardStyles.has(cardDisplayStyle) && isSome(styleOverride)) { + return styleOverride; + } + return cardDisplayStyle; +} +// MARK: - Collection Cards +/** + * @param objectGraph Dependency graph for the App Store + * @param data MAPI data for the card these lockups are included in + * @param cardConfig The configuration for the card these lockups are included in + * @param context The parse context for the over all today page + * @param includeLockupClickActions Whether we should include the click actions for the lockups, this is only needed + * when a card allows clicks on a lockup + * @returns The list of lockups for the collection displayed on a TodayCard + */ +export function lockupsForCollectionCardFromData(objectGraph, data, cardConfig, context, includeLockupClickActions) { + const relatedContent = relatedCardContentsContentsFromData(objectGraph, data); + const filteredRelatedContent = relatedContent.filter((cardData) => { + const isHiddenFromSpringboard = isHiddenFromSpringboardFromData(objectGraph, cardData); + const hasMessagesExtension = hasMessagesExtensionFromData(objectGraph, cardData); + return !hasMessagesExtension || !isHiddenFromSpringboard; + }); + return lockupsForRelatedContent(objectGraph, filteredRelatedContent, cardConfig, context.pageInformation, context.locationTracker, undefined, undefined, undefined, includeLockupClickActions); +} +/** + * @param objectGraph Dependency graph for the App Store + * @param relatedContent The list of MAPI data objects to generate lockups for + * @param cardConfig The configuration for the card these lockups are included in + * @param pageInformation The pageInformation for the page these lockups are included in + * @param locationTracker The locationTracker used for impressions on these lockups + * @param offerEnvironment The offer environment to use for the lockups + * @param offerStyle The offerStyle to use for the lockups + * @param externalDeepLinkUrl The promotional deep link url to use on the lockup's offer. + * @param includeLockupClickActions Whether we should include the click actions for the lockups, this is only needed + * when a card allows clicks on a lockup + * @returns The list of lockups for the collection displayed on a TodayCard + */ +export function lockupsForRelatedContent(objectGraph, relatedContent, cardConfig, pageInformation, locationTracker, offerEnvironment, offerStyle, externalDeepLinkUrl, includeLockupClickActions = true) { + if (isNullOrEmpty(relatedContent)) { + return []; + } + // IAPs are only suitable for the TodayCardInAppPurchase card. That card follows a separate path, so we should + // filter them out here. + const filteredRelatedContent = relatedContent.filter((cardData) => { + if (isDefinedNonNullNonEmpty(cardData.attributes)) { + return cardData.type !== "in-apps"; + } + return true; + }); + const options = { + lockupOptions: { + metricsOptions: { + pageInformation: pageInformation, + locationTracker: locationTracker, + }, + offerEnvironment: offerEnvironment, + offerStyle: offerStyle, + clientIdentifierOverride: cardConfig.clientIdentifierOverride, + externalDeepLinkUrl: externalDeepLinkUrl, + crossLinkSubtitle: cardConfig.crossLinkSubtitle, + artworkUseCase: 1 /* ArtworkUseCase.LockupIconSmall */, + canDisplayArcadeOfferButton: cardConfig.canDisplayArcadeOfferButton, + useJoeColorIconPlaceholder: cardConfig.useJoeColorIconPlaceholder, + skipDefaultClickAction: !includeLockupClickActions, + }, + filter: 76670 /* Filter.TodayCard */, + }; + return lockupsFromData(objectGraph, filteredRelatedContent, options); +} +// MARK: - Related Content +/** + * @param objectGraph Dependency graph for the App Store + * @param data MAPI data to get the relationship from + * @returns The card-contents relationship from the MAPI data + */ +export function relatedCardContentsContentsFromData(objectGraph, data) { + return relationshipCollection(data, "card-contents"); +} +// MARK: - Grid / List Collection Card Fallback +/** + * @param objectGraph The object graph for the app store + * @returns The min icon count that a Grid card should use before falling back to fallback media + */ +export function gridFallbackLimit(objectGraph) { + switch (objectGraph.client.deviceType) { + case "tv": + return 2; + default: + return 4; + } +} +/** + * @param objectGraph The object graph for the app store + * @returns The min icon count that a List card should use before falling back to fallback media + */ +export function listFallbackLimit(objectGraph) { + // Limits before using fallback media card for list-types and grid-types (including river). + switch (objectGraph.client.deviceType) { + case "tv": + return 3; + default: + return 4; + } +} +/** + * When we do not have enough content to render a list or grid card, we will use a multi + * app fallback media in its place. This also updates the click action to include a param + * indicating we're using fallback media. + * + * @param objectGraph The object graph for the app store + * @param data The MAPI data for the card + * @param fallbackItems The list of lockups to use as fallback media + * @param card The base card to apply the fallback media to + */ +export function applyMultiAppFallbackToCollectionCard(objectGraph, data, fallbackItems, card) { + const extraText = notesFromData(objectGraph, data, "short"); + card.media = new TodayCardMediaMultiApp(fallbackItems, extraText); + card.style = "dark"; + if (card.clickAction instanceof FlowAction) { + const updatedPageUrl = URL.from(card.clickAction.pageUrl); + updatedPageUrl.param(Parameters.showingFallbackMedia, "true"); + card.clickAction.pageUrl = updatedPageUrl.build(); + } +} +// MARK: - Editorial Artwork +/** + * @param objectGraph The object graph for the app store + * @param cardDisplayStyle The display style for the card we're getting the key path for + * @returns The keypath used to find the editorial artwork for the card + */ +export function editorialArtKeyPathForCardDisplayStyle(objectGraph, cardDisplayStyle) { + if (objectGraph.client.isWatch) { + return "editorialArtwork.subscriptionHero"; + } + else if (objectGraph.client.isVision) { + return "editorialArtwork.storyCenteredStatic16x9"; + } + else { + switch (cardDisplayStyle) { + case TodayCardDisplayStyle.AppOfTheDay: + case TodayCardDisplayStyle.GameOfTheDay: + return "editorialArtwork.dayCard"; + case TodayCardDisplayStyle.AppEventCard: + return "editorialArtwork.eventCard"; + case TodayCardDisplayStyle.Video: + case TodayCardDisplayStyle.FullBleedImage: + return "editorialArtwork.mediaCard"; + default: + return "editorialArtwork.generalCard"; + } + } +} +/** + * @param objectGraph The object graph for the app store + * @param artworkData The artwork data to create the artwork from, from the cards media api data + * @param cropCode The crop code to use for the artwork, otherwise we use the correct crop for the platform + * @returns The artwork model for the card + */ +export function todayCardArtworkFromArtworkData(objectGraph, artworkData, cropCode) { + if (isNullOrEmpty(artworkData)) { + return null; + } + const artwork = artworkFromApiArtwork(objectGraph, artworkData, { + withJoeColorPlaceholder: true, + useCase: 15 /* ArtworkUseCase.TodayCardMedia */, + }); + if (cropCode) { + artwork.crop = cropCode; + } + else if (objectGraph.client.isMac || objectGraph.client.isTV) { + // <rdar://problem/39155084> Articles: Use new crossover crop code for macOS story card art + artwork.crop = "fn"; + } + else { + artwork.crop = "sr"; + } + return artwork; +} +/** + * Returns a branded title `Artwork` model object from the API `editorialArtwork.contentLogoTrimmed` response. + * This artwork can be used in place of a text title for a Today Card. + * @param objectGraph The dependency graph for the App Store + * @param data The media API data to fetch the Artwork from + * @returns The branded title `Artwork` object, or `undefined` if a branded title could not be found + */ +export function brandedTitleArtworkForCard(objectGraph, data) { + const brandedTitleData = mediaAttributes.attributeAsDictionary(data, "editorialArtwork.contentLogoTrimmed"); + return artworkFromApiArtwork(objectGraph, brandedTitleData, { + contentMode: ArtworkContentMode.scaleAspectFit, + allowingTransparency: true, + useCase: 17 /* ArtworkUseCase.TodayCardBrandedTitle */, + }); +} +/** + * @param objectGraph The object graph for the app store + * @param data The media api data for the card + * @param cardConfig The configuration for the card being built + * @returns The `bgGradientKind` field from the correct artwork dictionary + */ +export function todayCardArtworkTitleBackingGradientForKey(objectGraph, data, cardConfig) { + const cardDisplayStyle = cardDisplayStyleFromData(data, cardConfig.coercedCollectionTodayCardDisplayStyle); + const artworkKey = editorialArtKeyPathForCardDisplayStyle(objectGraph, cardDisplayStyle); + const artworkData = mediaAttributes.attributeAsDictionary(data, artworkKey); + if (!isDefinedNonNull(artworkData)) { + return null; + } + return asString(artworkData, "bgGradientKind"); +} +/** + * @param objectGraph The object graph for the app store + * @param artworkData The artwork data from the media api data for a card, that allows us to determine the style + * @returns The light or dark style for this card, if we can determine it. + */ +export function todayCardStyleFromArtwork(objectGraph, artworkData) { + var _a; + if (isNullOrEmpty(artworkData)) { + return undefined; + } + const joeColors = joeColorSetFromData(artworkData); + const backgroundColor = joeColors.bgColor; + const hasGradient = ((_a = joeColors.textGradient) === null || _a === void 0 ? void 0 : _a.length) === 2; + if (!backgroundColor && !hasGradient) { + return undefined; + } + if (objectGraph.client.isiOS || objectGraph.client.isWeb) { + return cardStyleFromJoeColors(joeColors, "bgColor"); + } + else if (hasGradient) { + // Gradient colors are the text color, so if the gradient is a dark color, then + // the environment is a light environment + return color.isDarkColor(joeColors.textGradient[0]) ? "light" : "dark"; + } + else { + return color.isDarkColor(backgroundColor) ? "dark" : "light"; + } +} +export function cardStyleFromJoeColorsWithoutFallback(joeColors, preferredColorKeypath = "bgColor") { + if (isNothing(joeColors)) { + return undefined; + } + if (isSome(joeColors === null || joeColors === void 0 ? void 0 : joeColors.textGradient) && joeColors.textGradient.length === 2) { + // Rare, but if gradient colors are present, defer to them + return color.isDarkColor(joeColors.textGradient[0]) ? "white" : "dark"; + } + const preferredColor = joeColors[preferredColorKeypath]; + if (isNothing(preferredColor)) { + return undefined; + } + const luminance = color.luminanceFrom(preferredColor); + if (luminance <= 0.1) { + return "dark"; + } + else if (luminance >= 0.84) { + return "white"; + } + else { + return "light"; + } +} +export function cardStyleFromJoeColors(joeColors, preferredColorKeypath = "bgColor") { + var _a; + return (_a = cardStyleFromJoeColorsWithoutFallback(joeColors, preferredColorKeypath)) !== null && _a !== void 0 ? _a : "light"; +} +// MARK: - Editorial Text +/** + * Determines the heading for the card, were it to appear inline. + * If the source card heading is null, we default to the existing + * inline heading, if any. + * @param objectGraph The object graph for the app store + * @param card The today card we're collapsing the heading for + * @returns string The title to use for the card. + */ +export function collapsedHeadingForTodayCard(objectGraph, card) { + if (isDefinedNonNull(card.heading)) { + return card.heading.replace(/\n/g, " "); + } + return card.collapsedHeading; +} +// MARK: - Offers +/** + * Determines an appropriate OfferStyle for a TodayCard to ensure correct + * contrast with the card's contents. + * + * @param cardStyle The today style for the card + * @returns an appropriate offer style to use for the button + */ +export function offerStyleForTodayCard(objectGraph, cardStyle) { + return "transparent"; +} +/** + * Determines an appropriate OfferEnvironment for a TodayCard to ensure correct + * contrast with the card's contents. + * + * @param cardStyle The today style for the card + * @returns an appropriate offer environment to use for the button + */ +export function offerEnvironmentForTodayCard(cardStyle) { + if (cardStyle === "white") { + return "light"; + } + else { + return "todayCard"; + } +} +// MARK: - Hero Media +/** + * For mac and tv platforms, we add hero media to the today cards. + * + * @param objectGraph The dependency graph for the app store + * @param card The today card we're adding hero media to + * @param cardConfig The config used to create this card + * @param data The media api data used to create this card + */ +export function addHeroMediaToTodayCardIfNecessary(objectGraph, card, cardConfig, data) { + const cardDisplayStyle = cardDisplayStyleFromData(data, cardConfig.coercedCollectionTodayCardDisplayStyle); + // MacOS and tvOS: Hero + const heroArt = todayCardHeroArtForData(objectGraph, data, cardConfig.heroDisplayContext, cardDisplayStyle, cardConfig.prevailingCropCodes); + let artworks = []; + let videos = []; + if (isDefinedNonNull(heroArt)) { + artworks = [heroArt]; + } + if (isDefinedNonNull(card.media) && + (card.media instanceof TodayCardMediaVideo || card.media instanceof TodayCardMediaArtwork)) { + videos = card.media.videos; + } + const hero = new TodayCardMediaHero(artworks, videos); + const supportsHeroMedia = objectGraph.client.isMac || + objectGraph.client.isTV || + objectGraph.client.isWeb || + (objectGraph.client.isVision && !cardConfig.isSearchContext) || + preprocessor.GAMES_TARGET; + if (supportsHeroMedia && hero.isValid()) { + card.heroMedia = hero; + // Ensure we update the card styling. We prefer hero art, and fallback to hero video. + const heroArtworkData = mediaAttributes.attributeAsDictionary(data, heroMediaArtworkKeyForContext(objectGraph, cardConfig.heroDisplayContext, cardDisplayStyle, Object.keys(mediaAttributes.attributeAsDictionary(data, "editorialArtwork")))); + let style = todayCardStyleFromArtwork(objectGraph, heroArtworkData); + if (!isDefinedNonNull(style)) { + const heroVideoPreviewData = asDictionary(todayCardHeroVideoFromData(objectGraph, data), "previewFrame"); + style = todayCardStyleFromArtwork(objectGraph, heroVideoPreviewData); + } + card.style = style; + } +} +/** + * For external links we will display the standard link overlay in place of whatever overlay we had previously + * + * @param card The today card we're adding hero media to + */ +export function addExternalLinkOverlayToTodayCardIfNecessary(card) { + if (card.clickAction instanceof ExternalUrlAction) { + card.overlay = new TodayCardActionOverlay(card.clickAction); + card.style = "white"; + } +} +/** + * @param objectGraph The dependency graph for the app store + * @param data The media api data to search for hero art in + * @param context The context in which this hero media will be displayed + * @param displayStyle The display style of the card + * @param prevailingCropCodes The prevailing crop code to use for inline hero media + * @returns The artwork to be used in the hero position for a today card + */ +export function todayCardHeroArtForData(objectGraph, data, context, displayStyle, prevailingCropCodes) { + const heroArtworkKey = heroMediaArtworkKeyForContext(objectGraph, context, displayStyle, Object.keys(mediaAttributes.attributeAsDictionary(data, "editorialArtwork"))); + const heroArtworkData = mediaAttributes.attributeAsDictionary(data, heroArtworkKey); + return todayCardArtworkFromArtworkData(objectGraph, heroArtworkData, heroMediaArtworkCropForContext(objectGraph, context, objectPathToString(heroArtworkKey), prevailingCropCodes)); +} +/** + * @param objectGraph The dependency graph for the app store + * @param context The context in which this hero media will be displayed + * @param displayStyle The display style of the card + * @returns The key to use to find the hero media artwork + */ +function heroMediaArtworkKeyForContext(objectGraph, context, displayStyle, availableArtworkKeys) { + if (context === "article" && (objectGraph.client.isMac || objectGraph.client.isiOS)) { + // Use "iOS" art + return editorialArtKeyPathForCardDisplayStyle(objectGraph, displayStyle); + } + else if (objectGraph.client.isVision) { + return "editorialArtwork.heroStatic16x9"; + } + else { + if (objectGraph.client.isTV && availableArtworkKeys.includes("categoryDetailStatic16x9")) { + return "editorialArtwork.categoryDetailStatic16x9"; + } + return "editorialArtwork.crossoverCard"; + } +} +/** + * @param objectGraph The dependency graph for the app store + * @param context The context in which this hero media will be displayed + * @param prevailingCropCodes The crop code to use if we're not overriding it + * @returns The crop code to use for hero artwork in a given context + */ +function heroMediaArtworkCropForContext(objectGraph, context, heroArtworkKey, prevailingCropCodes) { + if (context === HeroMediaDisplayContext.Article && objectGraph.client.isMac) { + // Use "iOS" art crop code + return "fn"; + } + else if (context === HeroMediaDisplayContext.Article && + objectGraph.client.isTV && + heroArtworkKey === "editorialArtwork.categoryDetailStatic16x9") { + return "sr"; + } + else { + return prevailingCropCodes === null || prevailingCropCodes === void 0 ? void 0 : prevailingCropCodes.defaultCrop; + } +} +/** + * @param objectGraph The dependency graph for the app store + * @param data The media api data to search for hero video in + * @returns The video to be used in the hero position for a today card + */ +function todayCardHeroVideoFromData(objectGraph, data) { + let videoData; + const videoData4x3 = mediaAttributes.attributeAsDictionary(data, "editorialVideo.storeFrontVideo4x3"); + const videoData16x9 = mediaAttributes.attributeAsDictionary(data, "editorialVideo.storeFrontVideo"); + // <rdar://problem/34339194> Today: Video Card: Incorrect key for 4x3 videos + // On pad prefer 4x3 fallback to 16x9 and on phone vice versa + if (objectGraph.client.isPad || objectGraph.client.screenSize.isEqualTo(screenSizeIPhone134)) { + videoData = videoData4x3 || videoData16x9; + } + else { + videoData = videoData16x9 || videoData4x3; + } + return videoData; +} +// MARK: - Crosslinks +/** + * Determines the cross link subtitle from card data. + */ +export function crossLinkSubtitleFromData(objectGraph, data) { + // Start with short notes + let subtitle = editorialNotesFromData(objectGraph, data, "short"); + // Fallback to name + if (!isDefinedNonNullNonEmpty(subtitle)) { + subtitle = notesFromData(objectGraph, data, "name"); + } + // Fallback to label + if (!isDefinedNonNullNonEmpty(subtitle)) { + const cardDisplayStyle = mediaAttributes.attributeAsString(data, "displayStyle"); + if (cardDisplayStyle === TodayCardDisplayStyle.AppOfTheDay || + cardDisplayStyle === TodayCardDisplayStyle.GameOfTheDay) { + subtitle = mediaAttributes.attributeAsString(data, "label"); + } + } + return subtitle; +} +// MARK: - Client Identifier Overrids +function shouldRespectClientIdentifierOverride(objectGraph, data, clientIdentifier, cardConfig) { + var _a; + if (clientIdentifier === "com.apple.visionproapp" /* ClientIdentifier.VisionCompanion */ || clientIdentifier === "VisionAppStore" /* ClientIdentifier.VisionAppStore */) { + // If using the companion app, it's expected that we always respect the given identifier. + return true; + } + const relatedContent = relatedCardContentsContentsFromData(objectGraph, data); + const cardDisplayStyle = cardDisplayStyleFromData(data, cardConfig.coercedCollectionTodayCardDisplayStyle); + // Do not apply the override if this is not a Content type card. + const cardTypesRequiringClientIdentifierOverride = new Set([ + TodayCardDisplayStyle.AppEventCard, + TodayCardDisplayStyle.Grid, + TodayCardDisplayStyle.InAppPurchase, + TodayCardDisplayStyle.List, + TodayCardDisplayStyle.NumberedList, + TodayCardDisplayStyle.River, + TodayCardDisplayStyle.ShortImage, + TodayCardDisplayStyle.SingleApp, + ]); + if (!cardTypesRequiringClientIdentifierOverride.has(cardDisplayStyle)) { + return false; + } + // Grids with >= 6 items are unsupported + if (relatedContent.length >= 6 && cardDisplayStyle === TodayCardDisplayStyle.Grid) { + return false; + } + // Content cards with Artwork are unsupported + const artworkKey = editorialArtKeyPathForCardDisplayStyle(objectGraph, cardDisplayStyle); + if (!mediaAttributes.attributeAsBooleanOrFalse(data, "ignoreEditorialArt") && + todayCardArtworkFromArtworkData(objectGraph, mediaAttributes.attributeAsDictionary(data, artworkKey), (_a = cardConfig.prevailingCropCodes) === null || _a === void 0 ? void 0 : _a.defaultCrop)) { + return false; + } + // Content cards with iAP are unsupported + if (inAppPurchaseDataFromRelatedContent(objectGraph, relatedContent)) { + return false; + } + return true; +} +function inAppPurchaseDataFromRelatedContent(objectGraph, relatedContent) { + if (relatedContent.length === 1) { + const contentData = relatedContent[0]; + if (contentData.type === "in-apps") { + return contentData; + } + } + return null; +} +/** + * If a card should be using the OTD style we need to make sure we set that on the card + * + * @param card The today card we're adding the style to + * @param cardConfig The config used to create this card + */ +function addOTDStyleToCardIfNecessary(card, cardConfig) { + if (isNothing(card.media)) { + return; + } + card.media.otdTextStyle = cardConfig.useOTDTextStyle; +} +/** + * Check if this card should have the flip and blur enabled, this is the case when a card has videos. + * @param objectGraph The dependency graph for the app store + * @param card The today card we're modifying + */ +function enableFlipAndBlurIfNecessary(objectGraph, card) { + const cardMedia = card.media; + const cardMediaHasArtwork = cardMedia instanceof TodayCardMediaWithArtwork; + if (!cardMediaHasArtwork) { + return; + } + const cardMdiaWithArtwork = cardMedia; + const hasVideos = isDefinedNonNullNonEmpty(cardMdiaWithArtwork.videos); + card.supportsMediaMirroring = hasVideos; +} +/** + * @param data The media API data used to determine the intention of the card + * @param cardConfig The configuration for the card + * @returns Whether this app is an App of the Day or Game of the Day + */ +export function isCardOTDIntention(data, cardConfig) { + let otdIntention = mediaAttributes.attributeAsString(data, "ofTheDayIntent"); + if (isNothing(otdIntention)) { + const cardDisplayStyle = cardDisplayStyleFromData(data, cardConfig === null || cardConfig === void 0 ? void 0 : cardConfig.coercedCollectionTodayCardDisplayStyle); + switch (cardDisplayStyle) { + case TodayCardDisplayStyle.AppOfTheDay: + otdIntention = OfTheDayIntention.AppOfTheDay; + break; + case TodayCardDisplayStyle.GameOfTheDay: + otdIntention = OfTheDayIntention.GameOfTheDay; + break; + default: + break; + } + } + return otdIntention === OfTheDayIntention.AppOfTheDay || otdIntention === OfTheDayIntention.GameOfTheDay; +} +//# sourceMappingURL=today-card-util.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/today/today-controller-util.js b/node_modules/@jet-app/app-store/tmp/src/common/today/today-controller-util.js new file mode 100644 index 0000000..98f9fbc --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/today/today-controller-util.js @@ -0,0 +1,534 @@ +import { isNothing, isSome } from "@jet/environment"; +import * as validation from "@jet/environment/json/validation"; +import { FetchTimingMetricsBuilder } from "@jet/environment/metrics"; +import * as models from "../../api/models"; +import * as impressionDemotion from "../../common/personalization/on-device-impression-demotion"; +import { asArrayOrEmpty, asBoolean, asString, isDefinedNonNull, isDefinedNonNullNonEmpty, isNullOrEmpty, } from "../../foundation/json-parsing/server-data"; +import { hasAttributes } from "../../foundation/media/attributes"; +import * as mediaDataFetching from "../../foundation/media/data-fetching"; +import * as mediaDataStructure from "../../foundation/media/data-structure"; +import { fetchData } from "../../foundation/media/network"; +import { Parameters, Path } from "../../foundation/network/url-constants"; +import { URL } from "../../foundation/network/urls"; +import { optional, required } from "../../foundation/util/promise-util"; +import { AppEventsAttributes } from "../../gameservicesui/src/foundation/media-api/requests/recommendation-request-types"; +import { applySearchAdMissedOpportunityToShelvesIfNeeded, eligibleSlotPositionsForAdPlacement, iadInfoFromOnDeviceAdResponse, isAdPlacementEnabled, } from "../ads/ad-common"; +import * as adIncidents from "../ads/ad-incident-recorder"; +import { fetchAds, parallelOrganicRequestDidFinish } from "../ads/on-device-ad-fetch"; +import * as appPromotionsCommon from "../app-promotions/app-promotions-common"; +import { fetchPageWithAdditionalPageRequirements } from "../builders/additional-page-requirement-util"; +import { pageRouter } from "../builders/routing"; +import { shouldUsePrerenderedIconArtwork } from "../content/content"; +import { shelfForFooterButtons, shelfForTermsAndConditions, shelfForUnifiedMessage } from "../grouping/grouping-common"; +import { newLocationTracker } from "../metrics/helpers/location"; +import { iAdInformationFromMediaApiResponse } from "../metrics/helpers/models"; +import { addMetricsEventsToPageWithInformation, metricsPageInformationFromMediaApiResponse, } from "../metrics/helpers/page"; +import { combinedRecoMetricsDataFromMetricsData } from "../metrics/helpers/util"; +import * as onDevicePersonalization from "../personalization/on-device-personalization"; +import { fetchTodayRecommendationsWithTimeout, isTodayTabArcadePersonalizationAvailable, } from "../personalization/on-device-recommendations-today"; +import { setPreviewPlatform } from "../preview-platform"; +import * as productPageVariants from "../product-page/product-page-variants"; +import { newPageRefreshControllerFromResponse } from "../refresh/page-refresh-controller"; +import { storeCohortIdForUserFromResponse } from "../search/landing/search-landing-cohort"; +import { FlattenedTodayItemType, createAdShelfForTodayPageContextIfNecessary, feedPreviewUrlFromFlattenedTodayItems, flattenTodayModules, nextFlattenedItemIsHydrated, todayShelfForEditorialItem, todayShelfForEditorialItemGroup, } from "./today-parse-util"; +import { TodayPageContext } from "./today-types"; +/** + * Keys used to manage additional data needed to render the today page. + * + * - onboardingCards: key used to fetch the onboarding cards for the feed. + * + * - ads: Key used for fetching the today placement ads + */ +export var TodayControllerAdditionalDataKey; +(function (TodayControllerAdditionalDataKey) { + TodayControllerAdditionalDataKey["OnboardingCards"] = "onboardingCards"; + TodayControllerAdditionalDataKey["Ads"] = "ads"; + TodayControllerAdditionalDataKey["ODP"] = "ODP"; + TodayControllerAdditionalDataKey["AMDData"] = "amdData"; +})(TodayControllerAdditionalDataKey || (TodayControllerAdditionalDataKey = {})); +export function defaultPlatforms(objectGraph) { + return mediaDataFetching.defaultAdditionalPlatformsForClient(objectGraph); +} +/// Resource attributes to include for `editorial-items` fetched for displaying in today page. +export function defaultAttributes(objectGraph) { + const attributes = [ + "editorialArtwork", + "editorialVideo", + "enrichedEditorialNotes", + "minimumOSVersion", + "headerName", + "headerBadge", + "headerTagline", + "editorialClientParams", + "requiredCapabilities", + "shortEditorialNotes", + ]; + if (objectGraph.bag.enableUpdatedAgeRatings) { + attributes.push("ageRating"); + } + if (shouldUsePrerenderedIconArtwork(objectGraph)) { + attributes.push("iconArtwork"); + } + return attributes; +} +export function prepareRequest(objectGraph, request, isPageRequest) { + var _a, _b; + request + .includingAgeRestrictions() + .includingAdditionalPlatforms(defaultPlatforms(objectGraph)) + .includingAttributes(defaultAttributes(objectGraph)) + .includingScopedAttributes("editorial-item-groups", ["editorialClientParams"]) + .includingScopedRelationships("editorial-items", ["primary-content", "header-contents"]) + .includingScopedRelationships("editorial-item-groups", ["header-contents"]) + .enablingFeature("editorialItemGroups") + .usingCustomAttributes(productPageVariants.shouldFetchCustomAttributes(objectGraph)) + .includingRelationshipsForUpsell(true); + if (isPageRequest) { + request.includingAssociateKeys("editorial-item-groups", ["recommendations", "editorial-cards"]); + request.includingAssociateKeys("editorial-items", ["editorial-cards"]); + } + if (appPromotionsCommon.appEventsAreEnabled(objectGraph)) { + request.enablingFeature("appEvents"); + request.addingQuery("meta", "personalizationData"); + request.includingScopedRelationships("app-events", ["app"]); + request.includingScopedAttributes("app-events", AppEventsAttributes); + } + if (appPromotionsCommon.appOfferItemsAreEnabled(objectGraph)) { + request.enablingFeature("offerItems"); + if (isPageRequest) { + request.includingKindsKeys("offer-items", ["winback"]); + } + } + if (impressionDemotion.isImpressionDemotionAvailable(objectGraph)) { + request.enablingFeature("eiGroupEISelectionOnDevice"); + } + if (isAdPlacementEnabled(objectGraph, "today")) { + request.enablingFeature("adSupport"); + } + request.enablingFeature("heroStyles"); + const displayDeviceDrivenContent = (_b = (_a = objectGraph.userDefaults) === null || _a === void 0 ? void 0 : _a.bool("displayDeviceDrivenContent")) !== null && _b !== void 0 ? _b : false; + if (objectGraph.bag.enableDeviceDrivenDiscoveryContent && + displayDeviceDrivenContent && + request.resourceType === "today") { + request.addingQuery("pairedDevices", "visionPro"); + } +} +// MARK: - Page Fetching +export function generateTodayPageRequest(objectGraph) { + // Note: The sparse limit is now 2 because story groups will include all of their content which means + // if we continue to use the original 6 count we'll end up with way too many EI's + // rdar://108635958 ([Network Launch | 283ms | 19%] App Store - Dawn - D33: 21A236 vs 20E245 network-warm-launch: + // extendedLaunchTime is 1731.0ms vs 1448.0ms) + const sparseCount = objectGraph.client.isWeb ? 40 : 1; + const sparseLimit = objectGraph.client.isWeb ? 40 : 2; + const mediaApiRequest = new mediaDataFetching.Request(objectGraph) + .forType("today") + .withSparseCount(sparseCount) + .withSparseLimit(sparseLimit); + return setPreviewPlatform(objectGraph, mediaApiRequest); +} +/** + * @param objectGraph The object graph for the AppStore. + * @param onboardingCardIds The onboardingCard ids to fetch. + * @returns The map of additional page requirements. + */ +function generateAdditionalPageRequirements(objectGraph, onboardingCardIds) { + const additionalRequirements = {}; + /** + * Currently, to avoid double filtering, the fix for: + * <rdar://problem/45031644> If an onboarding card is empty don't add it to the request + * depends on the `Request` object filtering IDs from under caller. The caller is expected to perform some validation to + * guarantee that this `Request` object is not malformed after it was processed here, and it can still build a valid URL. + * + * This mechanic is undesired, and once this filtering is removed from `Request`, the caller should filter empty onboarding + * IDs via an explicit method instead before instantiating an malformed `Request`. + */ + if (isDefinedNonNullNonEmpty(onboardingCardIds)) { + const onboardingCardsRequest = new mediaDataFetching.Request(objectGraph) + .withIdsOfType(onboardingCardIds, "editorial-items") + .includingAdditionalPlatforms(defaultPlatforms(objectGraph)) + .includingAttributes(defaultAttributes(objectGraph)); + additionalRequirements[TodayControllerAdditionalDataKey.OnboardingCards] = optional(fetchData(objectGraph, onboardingCardsRequest, {})); + } + if (isAdPlacementEnabled(objectGraph, "today")) { + additionalRequirements[TodayControllerAdditionalDataKey.Ads] = optional(fetchAds(objectGraph, "today")); + } + if (isTodayTabArcadePersonalizationAvailable(objectGraph)) { + additionalRequirements[TodayControllerAdditionalDataKey.ODP] = optional(fetchTodayRecommendationsWithTimeout(objectGraph, undefined)); + } + const amsEngagement = objectGraph.amsEngagement; + if (amsEngagement && impressionDemotion.isImpressionDemotionAvailable(objectGraph)) { + const request = { + timeout: 1000, + eventType: impressionDemotion.AMSEngagementAppStoreEventKey, + tab: "today", + }; + additionalRequirements[TodayControllerAdditionalDataKey.AMDData] = optional(amsEngagement.performRequest(request)); + } + return additionalRequirements; +} +export async function fetchTodayPage(objectGraph, options) { + const router = objectGraph.required(pageRouter); + const isTodayTestPage = options.url === "x-as3-internal:/today/test"; + const isTodayCardPreviewPage = options.url.indexOf(Path.todayCardPreview) !== -1; + if (isTodayTestPage) { + return await router.fetchPage(objectGraph, options.url, models.TodayPage); + } + else if (isTodayCardPreviewPage) { + const dataFetchUrl = URL.from(options.url); + dataFetchUrl.param(Parameters.fetchData, "true"); + return await router.fetchPage(objectGraph, dataFetchUrl.build(), models.TodayPage); + } + else { + const request = generateTodayPageRequest(objectGraph); + prepareRequest(objectGraph, request, true); + const requestHeaders = isSome(options.experimentIdHeader) + ? { "X-Apple-User-Experiment-Ids": options.experimentIdHeader } + : undefined; + return await buildTodayPageFromRequest(objectGraph, request, requestHeaders, options); + } +} +export async function buildTodayPageFromRequest(objectGraph, request, requestHeaders, options) { + var _a; + const fetchTimingMetricsBuilder = new FetchTimingMetricsBuilder(); + const modifiedObjectGraph = objectGraph.addingFetchTimingMetricsBuilder(fetchTimingMetricsBuilder); + const primaryDataFetchPromise = fetchData(modifiedObjectGraph, request, { + headers: requestHeaders !== null && requestHeaders !== void 0 ? requestHeaders : undefined, + }); + // Should this be needed on web, this should have a .catch() added to avoid + // an unhandled rejection. + if (!objectGraph.client.isWeb) { + void primaryDataFetchPromise.then((response) => { + if (isAdPlacementEnabled(objectGraph, "today")) { + parallelOrganicRequestDidFinish(objectGraph, "today"); + } + storeCohortIdForUserFromResponse(objectGraph, objectGraph.user.dsid, response); + }); + } + const primaryDataFetchInput = required(primaryDataFetchPromise); + const additionalRequirements = generateAdditionalPageRequirements(objectGraph, (_a = options === null || options === void 0 ? void 0 : options.onboardingCardIds) !== null && _a !== void 0 ? _a : []); + const pageDataRequirements = await fetchPageWithAdditionalPageRequirements(objectGraph, primaryDataFetchInput, additionalRequirements); + return fetchTimingMetricsBuilder.measureModelConstruction(() => { + var _a, _b; + const onboardingCardsResponse = pageDataRequirements.additionalData[TodayControllerAdditionalDataKey.OnboardingCards]; + const onboardingCards = mediaDataStructure.dataCollectionFromDataContainer(onboardingCardsResponse); + const adsResponse = pageDataRequirements.additionalData[TodayControllerAdditionalDataKey.Ads]; + const todayRecommendations = pageDataRequirements.additionalData[TodayControllerAdditionalDataKey.ODP]; + const amdData = pageDataRequirements.additionalData[TodayControllerAdditionalDataKey.AMDData]; + const flattenedTodayItems = flattenTodayModules(objectGraph, (_b = (_a = pageDataRequirements.primaryPageData.results) === null || _a === void 0 ? void 0 : _a.data) !== null && _b !== void 0 ? _b : [], todayRecommendations, onboardingCards, adsResponse); + return buildTodayPage(modifiedObjectGraph, flattenedTodayItems, pageDataRequirements.primaryPageData, adsResponse, amdData); + }); +} +// MARK: - Today Page Creation +/** + * @param objectGraph The dependency graph for the App Store + * @param flattenedTodayItems The flattened Today items used to render the page + * @param primaryPageData The primary page data for the Today page + * @param adsResponse The response from ad platforms + * @returns A TodayPage model + */ +export function buildTodayPage(objectGraph, flattenedTodayItems, primaryPageData, adsResponse, amdResponse) { + return validation.context("renderPage", () => { + if (isNullOrEmpty(flattenedTodayItems)) { + return null; + } + const pageContext = createTodayPageContext(objectGraph, flattenedTodayItems, primaryPageData, adsResponse, amdResponse); + const todayPage = todayPageFromPageContext(objectGraph, pageContext); + todayPage.shelves.splice(0, 0, shelfForUnifiedMessage(objectGraph, "todayPageHeader")); + return todayPage; + }); +} +/** + * From the provided today page context, create a new today page with the hydrated content + * @param objectGraph The dependency graph for the App Store + * @param pageContext The page context for the Today page + */ +export function todayPageFromPageContext(objectGraph, pageContext) { + var _a; + const shelves = []; + const feedPreviewUrl = feedPreviewUrlFromFlattenedTodayItems(objectGraph, pageContext.remainingContent); + let hasMoreHydratedContent = nextFlattenedItemIsHydrated(pageContext.remainingContent); + const isPageLoadNotPaginated = (_a = pageContext.remainingContent[0]) === null || _a === void 0 ? void 0 : _a.isFirstItemInModule; + while (hasMoreHydratedContent) { + const todayItem = pageContext.remainingContent.shift(); + if (isNothing(todayItem)) { + hasMoreHydratedContent = false; + break; + } + const preItemAdShelf = createAdShelfForTodayPageContextIfNecessary(objectGraph, pageContext); + if (isSome(preItemAdShelf)) { + shelves.push(preItemAdShelf); + } + pageContext.currentTodayItem = todayItem; + let todayItemShelf = null; + switch (todayItem.type) { + case FlattenedTodayItemType.EditorialItem: + todayItemShelf = todayShelfForEditorialItem(objectGraph, todayItem, pageContext); + break; + case FlattenedTodayItemType.EditorialItemGroup: + todayItemShelf = todayShelfForEditorialItemGroup(objectGraph, todayItem, pageContext); + break; + default: + break; + } + pageContext.currentTodayItem = undefined; + if (isSome(todayItemShelf)) { + shelves.push(todayItemShelf); + } + const postItemAdShelf = createAdShelfForTodayPageContextIfNecessary(objectGraph, pageContext); + if (isSome(postItemAdShelf)) { + shelves.push(postItemAdShelf); + } + hasMoreHydratedContent = nextFlattenedItemIsHydrated(pageContext.remainingContent); + } + if (isPageLoadNotPaginated) { + applySearchAdMissedOpportunityToShelvesIfNeeded(objectGraph, shelves, "today", "today", pageContext.pageInformation); + } + const todayPage = new models.TodayPage(shelves); + const todayTab = objectGraph.bag.tabsStandard.find((tab) => tab.id === "today"); + const todayTabTitle = asString(todayTab, "title"); + const tabImageIdentifier = asString(todayTab, "image-identifier"); + const includeTitleDetail = isNothing(tabImageIdentifier) || tabImageIdentifier === "doc.text.image"; + const todayTitle = todayTabTitle !== null && todayTabTitle !== void 0 ? todayTabTitle : objectGraph.loc.string("PAGE_TITLE_TODAY"); + todayPage.title = todayTitle; + todayPage.tabTitle = todayTitle; + todayPage.titleDetail = includeTitleDetail ? todayPageTitleDetail(objectGraph) : undefined; + todayPage.shortTitleDetail = includeTitleDetail ? todayPageShortTitleDetail(objectGraph) : undefined; + todayPage.longTitle = includeTitleDetail ? todayPageLongTitle(objectGraph) : undefined; + todayPage.feedPreviewUrl = feedPreviewUrl; + if (isDefinedNonNullNonEmpty(pageContext.remainingContent)) { + pageContext.pageHasDisplayedContent = + pageContext.pageHasDisplayedContent || + shelves.some((shelf) => { + isDefinedNonNullNonEmpty(shelf.items); + }); + todayPage.nextPage = pageContext; + } + else if (!objectGraph.client.isWeb) { + footerShelvesForTodayPage(objectGraph, pageContext.pageInformation, pageContext.locationTracker).forEach((shelf) => { + todayPage.shelves.push(shelf); + }); + } + if (isAdPlacementEnabled(objectGraph, "today")) { + // Ad Incidents + todayPage.adIncidents = adIncidents.recordedIncidents(objectGraph, pageContext.adIncidentRecorder); + } + addMetricsEventsToPageWithInformation(objectGraph, todayPage, pageContext.pageInformation); + return todayPage; +} +/** + * Fetch the next batch of content for the Today page, and hydrate the page context + * @param objectGraph The dependency graph for the App Store + * @param pageContext The page context for the Today page + * @returns + */ +export async function hydrateNextBatchOfContent(objectGraph, pageContext) { + const batchLoadSize = pageContext.pageHasDisplayedContent ? 6 : 12; + let continueAddingContent = true; + let unhydratedItemIndex = 0; + let unhydratedContents = []; + while (continueAddingContent) { + const nextContent = pageContext.remainingContent[unhydratedItemIndex]; + switch (nextContent.type) { + case FlattenedTodayItemType.EditorialItem: + unhydratedContents.push(nextContent.data); + break; + case FlattenedTodayItemType.EditorialItemGroup: + unhydratedContents.push(nextContent.data); + const groupItems = asArrayOrEmpty(nextContent.data, "meta.associations.recommendations.data"); + unhydratedContents = [...unhydratedContents, ...groupItems]; + break; + default: + break; + } + unhydratedItemIndex++; + continueAddingContent = + unhydratedContents.length < batchLoadSize && unhydratedItemIndex < pageContext.remainingContent.length; + } + // MAPI request for module slice's contents + const requestedContentIds = new Set(unhydratedContents.map((content) => content.id)); + const contentsFetchRequest = new mediaDataFetching.Request(objectGraph, unhydratedContents, true, [ + "recommendations", + "editorial-cards", + ]); + prepareRequest(objectGraph, contentsFetchRequest, false); + const fetchedData = await fetchData(objectGraph, contentsFetchRequest, { allowEmptyDataResponse: true }); + const fetchedDataMap = {}; + for (const data of fetchedData.data) { + fetchedDataMap[data.id] = data; + } + // Hydrate the page context with the fetched data + for (const content of pageContext.remainingContent) { + hydrateFlattenedItemWithFetchedDataMap(content, fetchedDataMap); + } + /// Now take a pass and remove any items that are still unhydrated after performing the fetch + pageContext.remainingContent = pageContext.remainingContent.filter((content) => { + return content.isDataHydrated || !requestedContentIds.has(content.data.id); + }); + return pageContext; +} +/** + * For a given today item, take the fetched MAPI data and apply that to the item. + * + * @param flattenedItem The today item we're going to be hydrating + * @param fetchedDataMap The mapping from id to fetched media api data. + */ +function hydrateFlattenedItemWithFetchedDataMap(flattenedItem, fetchedDataMap) { + var _a, _b, _c; + switch (flattenedItem.type) { + case FlattenedTodayItemType.EditorialItem: + if (isDefinedNonNullNonEmpty(fetchedDataMap[flattenedItem.data.id])) { + flattenedItem.data = { + ...flattenedItem.data, + ...fetchedDataMap[flattenedItem.data.id], + }; + } + flattenedItem.isDataHydrated = hasAttributes(flattenedItem.data); + break; + case FlattenedTodayItemType.EditorialItemGroup: + if (isDefinedNonNullNonEmpty(fetchedDataMap[flattenedItem.data.id])) { + flattenedItem.data = { + ...flattenedItem.data, + attributes: (_a = fetchedDataMap[flattenedItem.data.id]) === null || _a === void 0 ? void 0 : _a.attributes, + relationships: (_b = fetchedDataMap[flattenedItem.data.id]) === null || _b === void 0 ? void 0 : _b.relationships, + meta: { + ...flattenedItem.data.meta, + ...(_c = fetchedDataMap[flattenedItem.data.id]) === null || _c === void 0 ? void 0 : _c.meta, + }, + }; + } + flattenedItem.isDataHydrated = hasAttributes(flattenedItem.data); + break; + default: + break; + } +} +/** + * @param objectGraph The dependency graph for the App Store + * @param todayItems The list of all the items displayed on the today page. + * @param todayResponse The response from the today endpoint + * @param adResponse The response from the ads service + * @returns The context to use when parsing today cards + */ +function createTodayPageContext(objectGraph, todayItems, todayResponse, adsResponse, amdResponse) { + var _a, _b, _c; + const pageInformation = metricsPageInformationFromMediaApiResponse(objectGraph, "Today", "today", todayResponse, null, (_a = iadInfoFromOnDeviceAdResponse(objectGraph, "today", adsResponse, todayItems)) !== null && _a !== void 0 ? _a : iAdInformationFromMediaApiResponse(objectGraph, "today", todayResponse, (_b = adsResponse === null || adsResponse === void 0 ? void 0 : adsResponse.onDeviceAd) === null || _b === void 0 ? void 0 : _b.positionInfo, todayItems)); + const onDevicePersonalizationMetricsData = onDevicePersonalization.metricsData(objectGraph); + pageInformation.recoMetricsData = combinedRecoMetricsDataFromMetricsData(pageInformation.recoMetricsData, null, onDevicePersonalizationMetricsData); + const context = new TodayPageContext(todayItems, pageInformation, newLocationTracker(), newPageRefreshControllerFromResponse(todayResponse), impressionDemotion.impressionEventsFromData(objectGraph, amdResponse)); + if (isAdPlacementEnabled(objectGraph, "today")) { + const eligibleAdPositions = eligibleSlotPositionsForAdPlacement(objectGraph, "today"); + if (isSome(eligibleAdPositions)) { + // The slot as provided by ad platforms is one-based - adjust it so we're working with zero-based numbers. + context.eligibleAdLocations = eligibleAdPositions.map((position) => position.slot); + } + const adIncidentRecorder = adIncidents.newRecorder(objectGraph, pageInformation.iAdInfo); + adIncidents.recordAdResponseEventsIfNeeded(objectGraph, adIncidentRecorder, adsResponse); + context.adIncidentRecorder = adIncidentRecorder; + context.adPlacementBehavior = getAdPlacementBehaviorForPage(todayItems); + if (isNothing(adsResponse === null || adsResponse === void 0 ? void 0 : adsResponse.failureReason)) { + context.adData = mediaDataStructure.dataFromDataContainer(objectGraph, adsResponse === null || adsResponse === void 0 ? void 0 : adsResponse.mediaResponse); + const rawPositionInfo = (_c = adsResponse === null || adsResponse === void 0 ? void 0 : adsResponse.onDeviceAd) === null || _c === void 0 ? void 0 : _c.positionInfo; + if (isDefinedNonNullNonEmpty(rawPositionInfo)) { + // The slot as provided by ad platforms is one-based - adjust it so we're working with zero-based numbers. + const adjustedAdLocation = rawPositionInfo.slot - 1; + context.adLocation = adjustedAdLocation; + } + } + } + return context; +} +function getAdPlacementBehaviorForPage(todayItems) { + let adPlacementBehavior = models.AdPlacementBehavior.insertIntoShelf; + for (const todayItem of todayItems) { + let todayItemAdPlacementBehavior = adPlacementBehavior; + switch (todayItem.type) { + case FlattenedTodayItemType.EditorialItem: + todayItemAdPlacementBehavior = getAdPlacementBehaviorForData(todayItem.data); + break; + case FlattenedTodayItemType.EditorialItemGroup: + const groupItems = asArrayOrEmpty(todayItem.data, "meta.associations.recommendations.data"); + if (isDefinedNonNullNonEmpty(groupItems)) { + for (const groupItem of groupItems) { + todayItemAdPlacementBehavior = getAdPlacementBehaviorForData(groupItem); + if (todayItemAdPlacementBehavior !== models.AdPlacementBehavior.insertIntoShelf) { + break; + } + } + } + break; + default: + break; + } + if (todayItemAdPlacementBehavior !== models.AdPlacementBehavior.insertIntoShelf) { + adPlacementBehavior = todayItemAdPlacementBehavior; + break; + } + } + return adPlacementBehavior; +} +function getAdPlacementBehaviorForData(data) { + const replaceIfAdPresent = asBoolean(data, "meta.personalizationData.replaceIfAdPresent"); + if (isDefinedNonNull(replaceIfAdPresent) && replaceIfAdPresent) { + return models.AdPlacementBehavior.replaceOrganic; + } + else if (isDefinedNonNull(replaceIfAdPresent) && !replaceIfAdPresent) { + return models.AdPlacementBehavior.dropAd; + } + else { + return models.AdPlacementBehavior.insertIntoShelf; + } +} +/** + * Return the first shelf title for the today page. This is used to generate the tab title. + * @param objectGraph The dependency graph of the app store + */ +function todayPageTitleDetail(objectGraph) { + const now = new Date(); + const dateFormat = objectGraph.loc.string("Today.ShelfTitle.DateFormat"); + return objectGraph.loc.formatDate(dateFormat, now); +} +/** + * Return the first shelf title for the today page on iPad. This is used to generate the tab title. + * @param objectGraph The dependency graph of the app store + */ +function todayPageLongTitle(objectGraph) { + if (!objectGraph.client.isPad) { + return undefined; + } + const now = new Date(); + const dateFormat = objectGraph.loc.string("Today.ShelfTitle.DateFormat.IPad"); + return objectGraph.loc.formatDate(dateFormat, now); +} +/** + * Return a short version of the first shelf title for the today page. This is used to generate the tab title, + * and is used where less horizontal space is available. + * @param objectGraph The dependency graph of the app store + */ +export function todayPageShortTitleDetail(objectGraph) { + const now = new Date(); + return objectGraph.loc.formatDate("MMM d", now); +} +/** + * Generate the last shelves of the today page + * @param objectGraph The dependency graph for the app + * @param pageInformation The page information for the today page + * @param locationTracker The Location tracker for the current today page + * @returns The shelves that appear tha the bottom of the today page + */ +function footerShelvesForTodayPage(objectGraph, pageInformation, locationTracker) { + const shelves = []; + // Add Footer Buttons + const footerButtonShelf = shelfForFooterButtons(objectGraph, pageInformation, locationTracker); + if (isDefinedNonNull(footerButtonShelf)) { + shelves.push(footerButtonShelf); + } + // Add T&C + const url = objectGraph.bag.termsAndConditionsURL; + if (isDefinedNonNull(url)) { + const termsAndConditionsShelf = shelfForTermsAndConditions(objectGraph, url); + shelves.push(termsAndConditionsShelf); + } + return shelves; +} +//# sourceMappingURL=today-controller-util.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/today/today-horizontal-card-util.js b/node_modules/@jet-app/app-store/tmp/src/common/today/today-horizontal-card-util.js new file mode 100644 index 0000000..0a2c0cb --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/today/today-horizontal-card-util.js @@ -0,0 +1,352 @@ +import * as validation from "@jet/environment/json/validation"; +import * as models from "../../api/models"; +import * as serverData from "../../foundation/json-parsing/server-data"; +import { attributeAsBooleanOrFalse } from "../../foundation/media/attributes"; +import { openTVArcadeAppAction } from "../arcade/arcade-common"; +import { editorialNotesFromData } from "../content/content"; +import * as metricsHelpersLocation from "../metrics/helpers/location"; +import { cardDisplayStyleFromData, collapsedHeadingForTodayCard, defaultTodayCardConfiguration, todayCardFromData, } from "./today-card-util"; +import { TodayCardDisplayStyle, TodayCardMetricsDisplayStyle, TodayParseContext, } from "./today-types"; +const supportedSmallHorizontalCardKinds = new Set([ + "artwork", + "appIcon", + "grid", + "multiApp", + "video", +]); +const supportedMediumHorizontalCardKinds = new Set([ + "brandedSingleApp", + "artwork", + "appIcon", + "grid", + "multiApp", + "video", +]); +const supportedLargeHorizontalCardKinds = new Set([ + "brandedSingleApp", + "artwork", + "appIcon", + "grid", + "multiApp", + "video", +]); +const iOSSupportedSmallHorizontalCardKinds = new Set([ + "brandedSingleApp", + "artwork", + "grid", + "video", +]); +const webSupportedSmallHorizontalCardKinds = new Set([ + "brandedSingleApp", + "artwork", + "river", + "appIcon", + "video", +]); +// MARK: - Horizontal Cards +/** + * The crop code to use for a card using the provided horizontal card shelf content type. + * @param {ShelfContentType} contentType The content type of the shelf's items. + * @returns {CropCode} The crop code to use for the artwork. + */ +function horizontalCardCropCodeForContentType(objectGraph, contentType, cardDisplayStyle) { + switch (contentType) { + case "smallStoryCard": + if (objectGraph.host.isTV) { + return cardDisplayStyle === TodayCardDisplayStyle.Video ? null : { defaultCrop: "fo" }; + } + else if (objectGraph.host.isiOS) { + return null; + } + else if (objectGraph.client.isWeb) { + return { defaultCrop: "sr" }; + } + else { + return { defaultCrop: "em" }; + } + case "mediumStoryCard": + return { defaultCrop: "el" }; + case "largeStoryCard": + return cardDisplayStyle === TodayCardDisplayStyle.Video ? { defaultCrop: "sr" } : { defaultCrop: "ek" }; + default: + return null; + } +} +export function isHorizontalCardSupportedForKind(objectGraph, kind, contentType) { + switch (contentType) { + case "smallStoryCard": + if (objectGraph.host.isiOS) { + return iOSSupportedSmallHorizontalCardKinds.has(kind); + } + else if (objectGraph.client.isWeb) { + return webSupportedSmallHorizontalCardKinds.has(kind); + } + else { + return supportedSmallHorizontalCardKinds.has(kind); + } + case "mediumStoryCard": + return supportedMediumHorizontalCardKinds.has(kind); + case "largeStoryCard": + return supportedLargeHorizontalCardKinds.has(kind); + case "todayCard": + return objectGraph.client.isWatch; + default: + return false; + } +} +export function horizontalCardItemsFromCards(objectGraph, cards, contentType, context, contentUnavailable, horizontalShelfCardConfig) { + const items = []; + for (const cardData of cards) { + const todayCard = horizontalCardItemFromCard(objectGraph, cardData, contentType, context, contentUnavailable, horizontalShelfCardConfig); + if (serverData.isNullOrEmpty(todayCard)) { + continue; + } + items.push(todayCard); + metricsHelpersLocation.nextPosition(context.metricsLocationTracker); + } + return items; +} +export function horizontalCardItemFromCard(objectGraph, cardData, contentType, context, contentUnavailable, horizontalShelfCardConfig) { + const parseContext = new TodayParseContext(context.metricsPageInformation, context.metricsLocationTracker); + if (horizontalShelfCardConfig === null || horizontalShelfCardConfig === undefined) { + horizontalShelfCardConfig = defaultTodayCardConfiguration(objectGraph); + horizontalShelfCardConfig.isHorizontalShelfContext = true; + } + if (objectGraph.client.deviceType !== "watch") { + // All today cards are deserialized through this code path on watchOS. + // We support all substyles, so we need to not specify this override. + horizontalShelfCardConfig.coercedCollectionTodayCardDisplayStyle = TodayCardDisplayStyle.Grid; + } + const cardDisplayStyle = cardDisplayStyleFromData(cardData, horizontalShelfCardConfig.coercedCollectionTodayCardDisplayStyle); + horizontalShelfCardConfig.prevailingCropCodes = horizontalCardCropCodeForContentType(objectGraph, contentType, cardDisplayStyle); + horizontalShelfCardConfig.horizontalCardContentType = contentType; + if (!serverData.isDefinedNonNull(cardData.attributes)) { + if (contentUnavailable) { + contentUnavailable(cardData); + } + return null; + } + const todayCard = todayCardFromData(objectGraph, cardData, horizontalShelfCardConfig, parseContext, context.augmentingData); + // Ignore the Date here, as we don't care about using it as a refresh signal when embedded in a page other than Today for now. + if (serverData.isNull(todayCard)) { + return null; + } + if (objectGraph.client.isiOS) { + todayCard.collapsedHeading = collapsedHeadingForTodayCard(objectGraph, todayCard); + todayCard.inlineDescription = featuredInDescriptionForCard(objectGraph, todayCard, cardData); + // In certain fallback cases, todayCardFromData() will make title and heading the same. + // If our generated heading and description are the same, we just show the heading. + if (todayCard.inlineDescription === todayCard.collapsedHeading) { + todayCard.inlineDescription = null; + } + // We want to avoid displaying the branded title over AotD/GotD cards, but only when they are not using + // the fallback art. + const brandedMedia = todayCard.media; + if (brandedMedia && brandedMedia.kind === "brandedSingleApp") { + // <rdar://problem/37249620> LOC: GLOBAL: App Store on iOS: Word wrapping in App/Game of the... + // Branded text on inline today cards is kind of a mess visually, so rely on the inline + // description. + todayCard.title = null; + } + } + if (contentType === "largeStoryCard") { + const heroMedia = todayCard.heroMedia; + if (!serverData.isDefinedNonNull(heroMedia) || + (heroMedia.artworks.length === 0 && heroMedia.videos.length === 0)) { + return null; + } + } + if (isHorizontalCardSupportedForKind(objectGraph, todayCard.media.kind, contentType)) { + if (serverData.isDefinedNonNull(context.filterOutMediaCardKinds) && + context.filterOutMediaCardKinds.has(todayCard.media.kind)) { + return null; + } + } + // Override `todayCard.clickAction` for some special cards. + overrideClickActionForTVAcquisitionCardIfNeeded(objectGraph, todayCard, cardData); + return todayCard; +} +export function shelfForHorizontalCardItems(objectGraph, cards, contentType, title, subtitle, context, contentUnavailable) { + if (!serverData.isDefinedNonNull(contentType)) { + return null; + } + const shelf = new models.Shelf(contentType); + if (title) { + shelf.title = title; + } + shelf.subtitle = subtitle; + shelf.isHorizontal = true; + switch (contentType) { + case "todayBrick": // As iOS doesn't support horizontalCardItemsFromCards, we map the small/medium/large story cards to todayBricks instead. + shelf.items = [ + featuredInTodayCardsFromData(objectGraph, cards, context.metricsPageInformation, context.metricsLocationTracker, () => true, contentUnavailable), + ]; + break; + default: + shelf.items = horizontalCardItemsFromCards(objectGraph, cards, contentType, context, contentUnavailable); + break; + } + return shelf; +} +/** + * Creates a shelf for mini today cards. + * + * @param objectGraph - The application store object graph. + * @param cards - An array of media data structures. + * @param contentType - The type of content for the shelf. + * @param title - The title of the shelf. + * @param subtitle - The subtitle of the shelf. + * @param context - The context for parsing today data. + * @param contentUnavailable - Optional handler for unavailable content. + * @returns A shelf containing today cards. + */ +export function shelfForMiniTodayCards(objectGraph, cards, title, subtitle, context, contentUnavailable) { + const shelf = new models.Shelf("miniTodayCard"); + if (title) { + shelf.title = title; + } + shelf.subtitle = subtitle; + shelf.isHorizontal = true; + const items = []; + const cardConfig = defaultTodayCardConfiguration(objectGraph); + cardConfig.metricsDisplayStyle = TodayCardMetricsDisplayStyle.SmallCard; + cardConfig.alwaysShowBadgeInSmallCards = true; + cardConfig.alwaysUseMaterialBlur = true; + for (const cardData of cards) { + if (!serverData.isDefinedNonNull(cardData.attributes)) { + if (contentUnavailable) { + contentUnavailable(cardData); + } + continue; + } + const todayCard = todayCardFromData(objectGraph, cardData, cardConfig, context); + if (serverData.isNullOrEmpty(todayCard)) { + continue; + } + items.push(todayCard); + metricsHelpersLocation.nextPosition(context.locationTracker); + } + shelf.items = items; + return shelf; +} +// MARK: - Featured In +/** + * Determines the description for the card, were it to appear inline. + * @param {TodayCard} card The card in question (for legacy support) + * @param {Data} data the card data to get the description from + * @returns {string} The description text to use for the card. + */ +function featuredInDescriptionForCard(objectGraph, card, data) { + const isSmallStoryCardsSupported = objectGraph.host.isiOS || objectGraph.host.isMac || objectGraph.host.isWeb; + if (isSmallStoryCardsSupported) { + let editorialName = editorialNotesFromData(objectGraph, data, "name"); + if (serverData.isDefinedNonNull(editorialName)) { + editorialName = editorialName.replace(/\n/g, " "); + } + switch (card.media.kind) { + case "brandedSingleApp": + if (card.overlay instanceof models.TodayCardMarketingLockupOverlay && + serverData.isDefinedNonNull(card.overlay.lockup)) { + return card.overlay.lockup.title; + } + else { + return editorialName; + } + default: + return editorialName; + } + } + switch (card.media.kind) { + case "brandedSingleApp": + if (card.overlay instanceof models.TodayCardThreeLineOverlay) { + return card.overlay.heading; + } + else if (serverData.isDefinedNonNull(card.title)) { + return card.title.replace(/\n/g, " "); + } + return null; + default: + const unformattedDescription = objectGraph.loc.string("TODAY_BRICK_STANDARD_DESCRIPTION"); + let heading; + let title; + if (card.overlay instanceof models.TodayCardThreeLineOverlay) { + heading = card.overlay.heading; + title = card.overlay.title; + } + else { + heading = card.heading; + title = card.title; + } + if (!unformattedDescription || !heading || !title) { + return null; + } + return unformattedDescription.replace("{heading}", heading).replace("{title}", title); + } +} +export function featuredInTodayCardsFromData(objectGraph, dataArray, metricsPageInformation, metricsLocationTracker, isIncluded, contentUnavailable) { + return validation.context("todayCardsFromPlatformData", () => { + if (!dataArray) { + return null; + } + const featuredInCardConfig = defaultTodayCardConfiguration(objectGraph); + featuredInCardConfig.enableFullScreenVideo = false; + featuredInCardConfig.coercedCollectionTodayCardDisplayStyle = TodayCardDisplayStyle.Grid; + featuredInCardConfig.prevailingCropCodes = { defaultCrop: "fo" }; + featuredInCardConfig.isHorizontalShelfContext = true; + const context = new TodayParseContext(metricsPageInformation, metricsLocationTracker); + const todayCards = []; + for (const data of dataArray) { + if (!serverData.isDefinedNonNull(data.attributes)) { + if (contentUnavailable) { + contentUnavailable(data); + } + continue; + } + const todayCard = todayCardFromData(objectGraph, data, featuredInCardConfig, context); + // Ignore the Date here, as we don't care about using it as a refresh signal when embedded in a page other than Today for now. + if (serverData.isNull(todayCard)) { + continue; + } + if (isIncluded(todayCard)) { + todayCard.collapsedHeading = collapsedHeadingForTodayCard(objectGraph, todayCard); + todayCard.inlineDescription = featuredInDescriptionForCard(objectGraph, todayCard, data); + // In certain fallback cases, todayCardFromData() will make title and heading the same. + // If our generated heading and description are the same, we just show the heading. + const isSmallStoryCardsSupported = objectGraph.host.isiOS || objectGraph.host.isMac || objectGraph.host.isWeb; + if (isSmallStoryCardsSupported && todayCard.inlineDescription === todayCard.collapsedHeading) { + todayCard.inlineDescription = null; + } + // We want to avoid displaying the branded title over AotD/GotD cards, but only when they are not using + // the fallback art. + const brandedMedia = todayCard.media; + if (brandedMedia && brandedMedia.kind === "brandedSingleApp") { + // <rdar://problem/37249620> LOC: GLOBAL: App Store on iOS: Word wrapping in App/Game of the... + // Branded text on inline today cards is kind of a mess visually, so rely on the inline + // description. + todayCard.title = null; + } + todayCards.push(todayCard); + } + } + if (!todayCards.length) { + return null; + } + return new models.InlineTodayCards(todayCards); + }); +} +// MARK: - tvOS Acquisition EI Card +/** + * On tvOS, some special editorial items are supposed to link directly into the Apple Arcade app. + * This is to override the click action if needed. + * @param card Today card to potentially modify. + * @param data Data that card was built from. + */ +function overrideClickActionForTVAcquisitionCardIfNeeded(objectGraph, card, data) { + if (objectGraph.client.deviceType !== "tv" || !attributeAsBooleanOrFalse(data, "isAcquisition")) { + return; // Only override on acquisition stories on tvOS. + } + const openArcadeAppAction = openTVArcadeAppAction(objectGraph); + card.clickAction = openArcadeAppAction; +} +// endregion +//# sourceMappingURL=today-horizontal-card-util.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/today/today-parse-util.js b/node_modules/@jet-app/app-store/tmp/src/common/today/today-parse-util.js new file mode 100644 index 0000000..a1e27b0 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/today/today-parse-util.js @@ -0,0 +1,1047 @@ +import { isNothing, isSome } from "@jet/environment"; +import * as validation from "@jet/environment/json/validation"; +import * as models from "../../api/models"; +import { asArrayOrEmpty, asBoolean, asDictionary, asInterface, asString, isDefinedNonNull, isDefinedNonNullNonEmpty, isNull, isNullOrEmpty, } from "../../foundation/json-parsing/server-data"; +import { editorialCardFromData } from "../../foundation/media/associations"; +import * as mediaAttributes from "../../foundation/media/attributes"; +import { attributeAsString, hasAttributes } from "../../foundation/media/attributes"; +import * as mediaDataStructure from "../../foundation/media/data-structure"; +import { relationshipData } from "../../foundation/media/relationships"; +import { Parameters, Path, Protocol } from "../../foundation/network/url-constants"; +import { URL } from "../../foundation/network/urls"; +import { isAdPlacementEnabled } from "../ads/ad-common"; +import { categoryArtworkData } from "../categories"; +import { artworkFromApiArtwork, iconFromData } from "../content/content"; +import { extractEditorialClientParams } from "../editorial-pages/editorial-data-util"; +import { addImpressionFields } from "../metrics/helpers/impressions"; +import { nextPosition, popLocation, pushContentLocation } from "../metrics/helpers/location"; +import { combinedRecoMetricsDataFromMetricsData } from "../metrics/helpers/util"; +import * as onDevicePersonalization from "../personalization/on-device-personalization"; +import * as color from "./../../foundation/util/color-util"; +import { asNumber } from "@apple-media-services/media-api"; +import { createTodayAdCard } from "./cards/today-ad-card-builder"; +import { defaultTodayCardConfiguration, todayCardFromData } from "./today-card-util"; +import * as impressionDemotion from "../../common/personalization/on-device-impression-demotion"; +import { EditorialBackgroundType, TodayCardMetricsDisplayStyle, TodayHeaderArtworkBehavior, TodaySectionHeaderArtworkPlacement, } from "./today-types"; +/** + * We try to flatten the today response so we can treat it as a single list + * of items. These are the different item types in the flattened list. + */ +export var FlattenedTodayItemType; +(function (FlattenedTodayItemType) { + FlattenedTodayItemType["EditorialItem"] = "editorialItem"; + FlattenedTodayItemType["EditorialItemGroup"] = "editorialItemGroup"; +})(FlattenedTodayItemType || (FlattenedTodayItemType = {})); +/** + * Flatten the entire modules array from the TodayPage response into a single list of items. + * @param objectGraph The dependency graph for the App Store + * @param todayModules The today modules to flatten from the page response + * @param onboardingCardsData The media api data for the onboarding cards, inserted into the flattened list + * @param todayRecommendations The today recommendations result, inserted into the flattened list + * @param adsResponse The response from ad platforms + * @returns The flattened list of today items + */ +export function flattenTodayModules(objectGraph, todayModules, todayRecommendations, onboardingCardsData, adsResponse) { + const flattenedItems = []; + let insertedOnboardingCards = isNullOrEmpty(onboardingCardsData); + // Retrieve our personalization data + const personalizationDataContainer = personalizationDataContainerForTodayModules(objectGraph, todayModules); + let absoluteFeedIndex = 0; + for (const todayModule of todayModules) { + if (isNullOrEmpty(todayModule.contents)) { + continue; + } + const personalizedModuleDataResult = onDevicePersonalization.personalizeDataItems(objectGraph, "today", todayModule.contents, true, personalizationDataContainer); + todayModule.contents = personalizedModuleDataResult.personalizedData; + todayModule.onDevicePersonalizationProcessingType = personalizedModuleDataResult.processingType; + let isFirstItemInModule = true; + const moduleMetadata = { + label: todayModule.label, + title: todayModule.title, + meta: todayModule.meta, + date: todayModule.date, + onDevicePersonalizationProcessingType: todayModule.onDevicePersonalizationProcessingType, + isTodayForAppsModule: mediaDataStructure.isModuleTodayForApps(todayModule), + }; + if (!insertedOnboardingCards) { + for (const onboardingCardData of onboardingCardsData) { + flattenedItems.push({ + type: FlattenedTodayItemType.EditorialItem, + data: onboardingCardData, + isDataHydrated: hasAttributes(onboardingCardData), + isFirstItemInModule, + moduleMetadata: { ...moduleMetadata }, + containedAdSlots: [absoluteFeedIndex], + }); + } + isFirstItemInModule = false; + insertedOnboardingCards = true; + absoluteFeedIndex += 1; + } + for (let moduleItem of todayModule.contents) { + const onDeviceUseCase = asString(moduleItem, "meta.personalizationData.onDeviceUseCase"); + switch (moduleItem.type) { + case "editorial-items": + if (isSome(onDeviceUseCase)) { + // ODP personalization for Today Arcade stories. + const storyData = todayRecommendations === null || todayRecommendations === void 0 ? void 0 : todayRecommendations.storyData(onDeviceUseCase); + if (isSome(storyData)) { + moduleItem = storyData; + } + } + flattenedItems.push({ + type: FlattenedTodayItemType.EditorialItem, + data: moduleItem, + isDataHydrated: hasAttributes(moduleItem), + isFirstItemInModule, + moduleMetadata: { ...moduleMetadata }, + containedAdSlots: [absoluteFeedIndex], + }); + isFirstItemInModule = false; + absoluteFeedIndex += 1; + break; + case "editorial-item-groups": + const groupContents = asArrayOrEmpty(moduleItem, "meta.associations.recommendations.data"); + if (isNullOrEmpty(groupContents)) { + continue; + } + let storyGroupData; + if (isSome(onDeviceUseCase)) { + // ODP personalization for Today Arcade story groups. + storyGroupData = todayRecommendations === null || todayRecommendations === void 0 ? void 0 : todayRecommendations.storyGroupData(onDeviceUseCase); + } + if (isSome(storyGroupData)) { + moduleItem = storyGroupData; + } + else { + // ODP personalization for in-app event story groups. + const personalizedStoryGroupDataResult = onDevicePersonalization.personalizeDataItems(objectGraph, "today", groupContents, true, personalizationDataContainer); + moduleItem["meta"]["associations"]["recommendations"]["data"] = + personalizedStoryGroupDataResult.personalizedData; + todayModule.onDevicePersonalizationProcessingType = + personalizedStoryGroupDataResult.processingType; + } + flattenedItems.push({ + type: FlattenedTodayItemType.EditorialItemGroup, + data: moduleItem, + isDataHydrated: hasAttributes(moduleItem), + isFirstItemInModule, + moduleMetadata: { ...moduleMetadata }, + containedAdSlots: Array.from({ length: groupContents.length }, (key, value) => value + absoluteFeedIndex), + }); + isFirstItemInModule = false; + absoluteFeedIndex += groupContents.length; + break; + default: + break; + } + } + } + return flattenedItems; +} +/** + * Whether the next content item in the flattened list is hydrated. + * @param flattenedItems The flattened list of today items + * @returns True if the next content item is hydrated, false otherwise + */ +export function nextFlattenedItemIsHydrated(flattenedItems) { + for (const flattenedItem of flattenedItems) { + switch (flattenedItem.type) { + case FlattenedTodayItemType.EditorialItem: + case FlattenedTodayItemType.EditorialItemGroup: + return hasAttributes(flattenedItem.data); + default: + break; + } + } + return false; +} +/** + * Iterates through all the today modules, and creates a set of personalization + * data that is targetted only to the contents of these modules. + * + * @param dataArray The input array of today modules. + * @returns Any relevant OnDevicePersonalizaionData + */ +export function personalizationDataContainerForTodayModules(objectGraph, todayModules) { + if (!onDevicePersonalization.isPersonalizationAvailable(objectGraph)) { + return null; + } + // Here we iterate through all the modules, and look inside each content items meta for personalizationData. + const appIds = new Set(); + for (const todayModule of todayModules) { + if (isNull(todayModule.contents)) { + continue; + } + const appIdFromContent = (contentData) => { + return asString(contentData, "meta.personalizationData.appId"); + }; + for (const contentData of todayModule.contents) { + switch (contentData.type) { + case "editorial-item-groups": + const groupItems = asArrayOrEmpty(contentData.meta, "associations.recommendations.data"); + for (const groupItem of groupItems) { + const appId = appIdFromContent(groupItem); + if (isDefinedNonNullNonEmpty(appId)) { + appIds.add(appId.toString()); + } + } + break; + default: + const appId = appIdFromContent(contentData); + if (isDefinedNonNullNonEmpty(appId)) { + appIds.add(appId.toString()); + } + break; + } + } + } + return onDevicePersonalization.personalizationDataContainerForAppIds(objectGraph, appIds); +} +// MARK: - Shelf Creation +/** + * @param objectGraph The dependency graph for the App Store + * @param todayItem The today item that this card is contained in, this could be a single item or a story group item + * @param todayCardData The MAPI data that will be used to create this today card + * @param isAdEligible Whether the current card is being placed in a slot that was eligible for an ad + * @param currentRowIndex The current row index this card is going to be in + * @param metricsDisplayStyle: The display style for impressions and location + * @param isHeroCard: Whether this is the first card in a hero story group + * @returns The configuration to use when parsing a today card + */ +function createTodayCardConfiguration(objectGraph, todayCardData, isAdEligible, currentRowIndex, metricsDisplayStyle, isHeroCard) { + var _a; + const cardConfig = defaultTodayCardConfiguration(objectGraph); + cardConfig.useOTDTextStyle = (_a = asBoolean(todayCardData, "meta.personalizationData.isOfTheDay")) !== null && _a !== void 0 ? _a : false; + cardConfig.replaceIfAdPresent = asBoolean(todayCardData, "meta.personalizationData.replaceIfAdPresent"); + cardConfig.isAdEligible = isAdEligible; + cardConfig.currentRowIndex = currentRowIndex; + cardConfig.metricsDisplayStyle = metricsDisplayStyle; + cardConfig.isHeroCard = isHeroCard; + if (objectGraph.client.isWeb) { + cardConfig.prevailingCropCodes = { + "defaultCrop": "sr", + "editorialArtwork.dayCard": "grav.west", + }; + } + return cardConfig; +} +/** + * @param objectGraph The dependency graph for the App Store + * @param pageContext The page context for the today page + * @returns The currentRowIndex from the pageContext, this is only used on iPhone, otherwise return + * null since we cant reliably know the row index + */ +function rowCountFromContext(objectGraph, pageContext) { + if (!objectGraph.client.isPhone) { + return undefined; + } + return pageContext.currentRowIndex; +} +/** + * Update the page context with the current row count, this uses the groupDisplayStyle to determine + * the display style for the cards in the current row + * @param pageContext The page context for the today page we're parsing + * @param currentGroupDisplayStyle The current groupDisplayItem from the TodayItem we're parsing + * @param currentItemIndex The index of the current item in the shelf, that we just created a card + * for, this method is called after creating a card + */ +function incrementRowIndexIfNecessary(pageContext, currentGroupDisplayStyle, currentItemIndex) { + switch (currentGroupDisplayStyle) { + case models.GroupDisplayStyle.Grid: + if (currentItemIndex % 2 === 1) { + pageContext.currentRowIndex++; + } + break; + case models.GroupDisplayStyle.Hero: + if (currentItemIndex === 0 || (currentItemIndex - 1) % 2 === 1) { + pageContext.currentRowIndex++; + } + break; + case models.GroupDisplayStyle.Standard: + pageContext.currentRowIndex++; + break; + default: + break; + } +} +/** + * Update the page context with the current metrics display style, this is based off the groupDisplayStyle + * @param objectGraph The dependency graph for the App Store + * @param pageContext The page context for the today page we're parsing + * @param currentGroupDisplayStyle The current groupDisplayItem from the TodayItem we're parsing + * @param currentItemIndex The index of the current item in the shelf, that we just created a card + * for, this method is called after creating a card + */ +function updateCurrentRowMetricsDisplayStyle(objectGraph, pageContext, currentGroupDisplayStyle, currentItemIndex) { + if (objectGraph.client.isPad) { + pageContext.currentRowMetricsDisplayStyle = TodayCardMetricsDisplayStyle.MediumCard; + return; + } + switch (currentGroupDisplayStyle) { + case models.GroupDisplayStyle.Grid: + pageContext.currentRowMetricsDisplayStyle = TodayCardMetricsDisplayStyle.SmallCard; + break; + case models.GroupDisplayStyle.Hero: + if (currentItemIndex === 0) { + pageContext.currentRowMetricsDisplayStyle = TodayCardMetricsDisplayStyle.MediumCard; + } + else { + pageContext.currentRowMetricsDisplayStyle = TodayCardMetricsDisplayStyle.SmallCard; + } + break; + case models.GroupDisplayStyle.Standard: + pageContext.currentRowMetricsDisplayStyle = TodayCardMetricsDisplayStyle.MediumCard; + break; + default: + break; + } +} +/** + * @param objectGraph The app store object graph + * @param data The data from MAPI for a single item in the today feed, group or single EI + * @returns The group display style for the given data + */ +function groupDisplayStyleFromData(objectGraph, data) { + var _a; + if (data.type === "editorial-items") { + return models.GroupDisplayStyle.Standard; + } + let groupDisplayStyle; + const editorialCard = editorialCardFromData(data); + if (hasAttributes(editorialCard)) { + groupDisplayStyle = mediaAttributes.attributeAsString(editorialCard, "editorialItemGroupDisplayStyle"); + } + if (isNothing(groupDisplayStyle)) { + groupDisplayStyle = + (_a = mediaAttributes.attributeAsString(data, "displayStyle")) !== null && _a !== void 0 ? _a : models.GroupDisplayStyle.Standard; + } + return isGroupDisplayStyleSupported(objectGraph, groupDisplayStyle) + ? groupDisplayStyle + : models.GroupDisplayStyle.Standard; +} +/** + * @param objectGraph The app dependency graph + * @param displayStyle The display style to check support for + * @returns Whether group display style is supported for this platform + */ +function isGroupDisplayStyleSupported(objectGraph, displayStyle) { + if (isNothing(displayStyle)) { + return false; + } + switch (displayStyle) { + case models.GroupDisplayStyle.Grid: + return objectGraph.client.isPhone; + default: + return true; + } +} +/** + * Determine if the current card is eligible for an ad, based on the parsed card count, and the + * bag defined values for slots that can have ads + * @param pageContext The page context for the today page + * @returns Whether the current card is eligible for an ad + */ +function isTodayCardConfigurationAdEligible(pageContext) { + if (pageContext.adLocation === pageContext.parsedCardCount) { + return true; + } + if (isNothing(pageContext.eligibleAdLocations)) { + return false; + } + return pageContext.eligibleAdLocations.includes(pageContext.parsedCardCount); +} +/** + * @param objectGraph The dependency graph for the App Store + * @param todayItem The flattened today item to create a shelf for + * @param pageContext The page context for the today page + * @returns The shelf for the given today item, or null if we cant create one + */ +export function todayShelfForEditorialItem(objectGraph, todayItem, pageContext) { + var _a; + const shelf = createTodayShelfWithItems(objectGraph, todayItem, pageContext, () => { + var _a, _b, _c; + const shelfItems = []; + (_a = pageContext.pageInformation.iAdInfo) === null || _a === void 0 ? void 0 : _a.updateContainerId((_b = pageContext.pageInformation.iAdInfo) === null || _b === void 0 ? void 0 : _b.containerIdForSlotIndex((_c = pageContext.parsedCardCount) !== null && _c !== void 0 ? _c : 0)); + const groupDisplayStyle = groupDisplayStyleFromData(objectGraph, todayItem.data); + updateCurrentRowMetricsDisplayStyle(objectGraph, pageContext, groupDisplayStyle, 0); + const cardConfig = createTodayCardConfiguration(objectGraph, todayItem.data, isTodayCardConfigurationAdEligible(pageContext), rowCountFromContext(objectGraph, pageContext), pageContext.currentRowMetricsDisplayStyle, false); + cardConfig.baseMetricsOptions = { + recoMetricsData: recoMetricsFromTodayItem(todayItem), + }; + const editorialItemTodayCard = createTodayCardForPageContext(objectGraph, pageContext, cardConfig, todayItem.data); + if (isNothing(editorialItemTodayCard)) { + return shelfItems; + } + shelfItems.push(editorialItemTodayCard); + nextPosition(pageContext.locationTracker); + pageContext.parsedCardCount++; + incrementRowIndexIfNecessary(pageContext, groupDisplayStyle, 0); + return shelfItems; + }); + shelf.contentsMetadata = { + type: "todaySection", + debugSectionTypeIndicatorColor: todayItem.type === FlattenedTodayItemType.EditorialItemGroup + ? color.named("systemGreen") + : color.named("systemBlue"), + groupDisplayStyle: models.GroupDisplayStyle.Standard, + }; + // If this is not the first item in the module, attempt to set the background + if (!todayItem.isFirstItemInModule) { + const editorialShelfBackgroundInfo = todaySectionEditorialBackground(objectGraph, todayItem); + if (isSome(editorialShelfBackgroundInfo)) { + shelf.background = editorialShelfBackgroundInfo.shelfBackground; + if (isSome((_a = shelf.header) === null || _a === void 0 ? void 0 : _a.configuration)) { + shelf.header.configuration.eyebrowColor = editorialShelfBackgroundInfo.eyebrowColor; + shelf.header.configuration.titleColor = editorialShelfBackgroundInfo.titleColor; + shelf.header.configuration.subtitleColor = editorialShelfBackgroundInfo.subtitleColor; + } + } + } + return shelf; +} +/** + + * @param objectGraph The dependency graph for the App Store + * @param todayItem The flattened today item, of type EditorialItemGroup to create a shelf for + * @param pageContext The page context for the today page + * @returns The shelf for the given today item, or null if we cant create one + */ +export function todayShelfForEditorialItemGroup(objectGraph, todayItem, pageContext) { + var _a; + let isValidHeroStoryGroup = true; + const shelf = createTodayShelfWithItems(objectGraph, todayItem, pageContext, () => { + var _a, _b, _c, _d, _e, _f, _g; + const shelfItems = []; + const displayLimit = impressionDemotion.isImpressionDemotionAvailable(objectGraph) + ? (_a = asNumber(todayItem.data, "meta.personalizationData.displayEICount")) !== null && _a !== void 0 ? _a : 100 + : 100; + let groupItems = asArrayOrEmpty(todayItem.data.meta, "associations.recommendations.data"); + if (isSome(pageContext.recoImpressionData)) { + groupItems = impressionDemotion.personalizeDataItems(groupItems, pageContext.recoImpressionData, (_b = pageContext.pageInformation.recoMetricsData) !== null && _b !== void 0 ? _b : {}); + } + const groupDisplayStyle = groupDisplayStyleFromData(objectGraph, todayItem.data); + let parsedGroupItemCount = 0; + for (const [index, groupItem] of groupItems.entries()) { + (_c = pageContext.pageInformation.iAdInfo) === null || _c === void 0 ? void 0 : _c.updateContainerId((_d = pageContext.pageInformation.iAdInfo) === null || _d === void 0 ? void 0 : _d.containerIdForSlotIndex((_e = pageContext.parsedCardCount) !== null && _e !== void 0 ? _e : 0)); + updateCurrentRowMetricsDisplayStyle(objectGraph, pageContext, groupDisplayStyle, parsedGroupItemCount); + const cardConfig = createTodayCardConfiguration(objectGraph, groupItem, isTodayCardConfigurationAdEligible(pageContext), rowCountFromContext(objectGraph, pageContext), pageContext.currentRowMetricsDisplayStyle, groupDisplayStyle === models.GroupDisplayStyle.Hero && index === 0); + const shelfItem = createTodayCardForPageContext(objectGraph, pageContext, cardConfig, groupItem); + if (isSome(shelfItem)) { + shelfItems.push(shelfItem); + nextPosition(pageContext.locationTracker); + pageContext.parsedCardCount++; + incrementRowIndexIfNecessary(pageContext, groupDisplayStyle, parsedGroupItemCount); + parsedGroupItemCount++; + } + if (cardConfig.isHeroCard && isNothing(shelfItem)) { + if (["debug", "internal"].includes(objectGraph.client.buildType)) { + validation.unexpectedType("defaultValue", `Hero story group ${(_f = todayItem.data) === null || _f === void 0 ? void 0 : _f.id} must contain a valid hero card at index ${index}. Unable to parse card ${groupItem.id}.`, null); + } + isValidHeroStoryGroup = false; + } + if (index < groupItems.length - 1 && + pageContext.adPlacementBehavior === models.AdPlacementBehavior.insertIntoShelf) { + // Attempt to insert the ad card after creating the next item in the group, but only + // if we're still **within** the story group, ads that fall at the beginning or end of + // the group are handled at the top level of the page parsing. + const adCard = createAdCardForTodayPageContextIfNecessary(objectGraph, pageContext, createTodayCardConfiguration(objectGraph, pageContext.adData, isTodayCardConfigurationAdEligible(pageContext), rowCountFromContext(objectGraph, pageContext), undefined, false)); + if (isSome(adCard)) { + pageContext.parsedCardCount++; + incrementRowIndexIfNecessary(pageContext, models.GroupDisplayStyle.Standard, 0); + nextPosition(pageContext.locationTracker); + shelfItems.push(adCard); + } + } + // if the count is equal to the story limit. stop here and break. + if (shelfItems.length === displayLimit) { + break; + } + } + if (isValidHeroStoryGroup) { + // The number of items required to be a valid hero story group, this includes the hero + // card, and two additional cards to form a row + const heroStoryGroupRequiredItemCount = 3; + if (shelfItems.length !== heroStoryGroupRequiredItemCount) { + if (["debug", "internal"].includes(objectGraph.client.buildType)) { + validation.unexpectedType("defaultValue", `Hero story group ${(_g = todayItem.data) === null || _g === void 0 ? void 0 : _g.id} must contain exactly ${heroStoryGroupRequiredItemCount} items but only found ${shelfItems.length} items.`, null); + } + isValidHeroStoryGroup = false; + } + } + return shelfItems; + }); + // Fallback to standard group display style if we had an issue creating a valid hero group + let groupDisplayStyle = groupDisplayStyleFromData(objectGraph, todayItem.data); + if (groupDisplayStyle === models.GroupDisplayStyle.Hero && !isValidHeroStoryGroup) { + groupDisplayStyle = models.GroupDisplayStyle.Standard; + } + shelf.contentsMetadata = { + type: "todaySection", + debugSectionTypeIndicatorColor: color.named("systemGreen"), + groupDisplayStyle: groupDisplayStyle, + }; + // If this is not the first item in the module, attempt to set the background + if (!todayItem.isFirstItemInModule) { + const editorialShelfBackgroundInfo = todaySectionEditorialBackground(objectGraph, todayItem); + if (isSome(editorialShelfBackgroundInfo)) { + shelf.background = editorialShelfBackgroundInfo.shelfBackground; + if (isSome((_a = shelf.header) === null || _a === void 0 ? void 0 : _a.configuration)) { + shelf.header.configuration.eyebrowColor = editorialShelfBackgroundInfo.eyebrowColor; + shelf.header.configuration.titleColor = editorialShelfBackgroundInfo.titleColor; + shelf.header.configuration.subtitleColor = editorialShelfBackgroundInfo.subtitleColor; + } + } + else if (groupDisplayStyle === models.GroupDisplayStyle.Hero && Array.isArray(shelf.items)) { + shelf.background = todaySectionBackgroundForHeroDisplayStyle(shelf.items); + } + } + return shelf; +} +/** + * @param objectGraph The dependency graph for the App Store + * @param todayItem The flattened today item, used to create the items in this shelf + * @param pageContext The page context for the today page + * @param itemProvider The function that will provide the items for this shelf + * @returns The shelf for the given today item + */ +function createTodayShelfWithItems(objectGraph, todayItem, pageContext, itemProvider) { + const shouldRecordShelfMetrics = todayItem.type === FlattenedTodayItemType.EditorialItemGroup; + const shelf = new models.Shelf("todayCard"); + shelf.id = todayItem.data.id; + shelf.isHorizontal = false; + shelf.header = createTodayShelfHeaderForTodayItem(objectGraph, todayItem, pageContext); + if (shouldRecordShelfMetrics) { + const shelfMetricsOptions = { + id: shelf.id, + kind: "editorialItemGroup", + softwareType: null, + targetType: "swoosh", + title: sectionTitleFromTodayItem(objectGraph, todayItem, true), + pageInformation: pageContext.pageInformation, + locationTracker: pageContext.locationTracker, + idType: "its_id", + recoMetricsData: recoMetricsFromTodayItem(todayItem), + }; + if (todayItem.type === FlattenedTodayItemType.EditorialItemGroup) { + shelfMetricsOptions["optimizationId"] = asString(todayItem.data, "meta.personalizationData.optimizationId"); + shelfMetricsOptions["optimizationEntityId"] = asString(todayItem.data, "meta.personalizationData.optimizationEntityId"); + } + addImpressionFields(objectGraph, shelf, shelfMetricsOptions); + pushContentLocation(objectGraph, shelfMetricsOptions, shelfMetricsOptions.title); + } + shelf.items = itemProvider(); + shelf.isHidden = isNullOrEmpty(shelf.items); + if (shouldRecordShelfMetrics) { + popLocation(pageContext.locationTracker); + nextPosition(pageContext.locationTracker); + } + return shelf; +} +function createTodayShelfHeaderForTodayItem(objectGraph, todayItem, pageContext) { + var _a; + const shouldSuppressHeader = (_a = asBoolean(todayItem.data, "meta.personalizationData.suppressHeader")) !== null && _a !== void 0 ? _a : false; + if (shouldSuppressHeader) { + return null; + } + const shelfHeaderConfiguration = { + eyebrowImageColor: null, + titleImageColor: null, + includeSeparator: false, + }; + const shelfHeader = { + eyebrow: sectionEyebrowFromTodayItem(objectGraph, todayItem), + eyebrowArtwork: sectionHeaderArtworkFromTodayItemForPlacement(objectGraph, todayItem, TodaySectionHeaderArtworkPlacement.Eyebrow), + eyebrowArtworkType: sectionHeaderArtworkTypeFromTodayItemForPlacement(objectGraph, todayItem, TodaySectionHeaderArtworkPlacement.Eyebrow), + title: sectionTitleFromTodayItem(objectGraph, todayItem), + titleArtwork: sectionHeaderArtworkFromTodayItemForPlacement(objectGraph, todayItem, TodaySectionHeaderArtworkPlacement.Title), + titleArtworkType: sectionHeaderArtworkTypeFromTodayItemForPlacement(objectGraph, todayItem, TodaySectionHeaderArtworkPlacement.Title), + subtitle: sectionSubtitleFromTodayItem(objectGraph, todayItem), + configuration: shelfHeaderConfiguration, + }; + if (isSome(shelfHeader.eyebrow) || isSome(shelfHeader.title) || isSome(shelfHeader.subtitle)) { + return shelfHeader; + } + else { + return null; + } +} +/** + * Create a today card for a given today item in a page context. This handles requirements for ad replacement if needed + * @param objectGraph The dependency graph of the app store + * @param pageContext The context of the page so we can tell whether its time to insert an ad + * @param cardConfig The configuration for the today item + * @param todayItem The flattened today item that should be placed in the shelf or replaced with an ad card + * @returns The today card to insert in a shelf + */ +function createTodayCardForPageContext(objectGraph, pageContext, cardConfig, todayItem) { + var _a, _b; + let editorialItemTodayCard; + if (pageContext.adPlacementBehavior === models.AdPlacementBehavior.replaceOrganic && + isDefinedNonNull(cardConfig.replaceIfAdPresent) && + asBoolean(cardConfig.replaceIfAdPresent)) { + // the ad card should replace the organic + const adCard = createAdCardForTodayPageContextIfNecessary(objectGraph, pageContext, cardConfig); + if (isDefinedNonNullNonEmpty(adCard)) { + editorialItemTodayCard = adCard; + } + else { + editorialItemTodayCard = todayCardFromData(objectGraph, todayItem, cardConfig, pageContext); + } + } + else if (pageContext.adPlacementBehavior === models.AdPlacementBehavior.dropAd && + isDefinedNonNull(cardConfig.replaceIfAdPresent) && + !asBoolean(cardConfig.replaceIfAdPresent)) { + // the organic is not replaceable + editorialItemTodayCard = todayCardFromData(objectGraph, todayItem, cardConfig, pageContext); + if (isDefinedNonNullNonEmpty(pageContext.adData)) { + const recorder = pageContext.adIncidentRecorder; + (_a = recorder === null || recorder === void 0 ? void 0 : recorder.iAdInfo) === null || _a === void 0 ? void 0 : _a.setMissedOpportunity(objectGraph, "EDITORIALTAKEOVER", (_b = recorder === null || recorder === void 0 ? void 0 : recorder.iAdInfo) === null || _b === void 0 ? void 0 : _b.placementType); + } + } + else { + // we're using existing insertion logic that will insert an ad elsewhere. create a regular card for the flattened item + editorialItemTodayCard = todayCardFromData(objectGraph, todayItem, cardConfig, pageContext); + } + return editorialItemTodayCard; +} +// MARK: - Ads +/** + * Try create a shelf to display an ad if we're currently at the correct spot in the feed. This is only used + * for single editorialItem shelves, and the end of an editorial item group, since these locations it does not + * make sense to include the ad within the shelf for that TodayItem. + * @param objectGraph The dependency graph of the app store + * @param pageContext The context of the page so we can tell whether its time to insert an ad + * @returns The card to insert at the ad slot if necessary + */ +export function createAdShelfForTodayPageContextIfNecessary(objectGraph, pageContext) { + let adShelf = null; + if (!isAdPlacementEnabled(objectGraph, "today") || + isNothing(pageContext.adData) || + pageContext.adPlacementBehavior !== models.AdPlacementBehavior.insertIntoShelf) { + return adShelf; + } + if (pageContext.adLocation !== pageContext.parsedCardCount) { + return adShelf; + } + adShelf = new models.Shelf("todayCard"); + adShelf.id = pageContext.adData.id; + adShelf.isHorizontal = false; + adShelf.contentsMetadata = { + type: "todaySection", + debugSectionTypeIndicatorColor: color.named("systemBlue"), + groupDisplayStyle: models.GroupDisplayStyle.Standard, + }; + const shelfItems = []; + /// Attempt to create an ad card before creating the next item in the group + const adCard = createAdCardForTodayPageContextIfNecessary(objectGraph, pageContext, createTodayCardConfiguration(objectGraph, pageContext.adData, isTodayCardConfigurationAdEligible(pageContext), rowCountFromContext(objectGraph, pageContext), undefined, false)); + if (isSome(adCard)) { + pageContext.parsedCardCount++; + incrementRowIndexIfNecessary(pageContext, models.GroupDisplayStyle.Standard, 0); + nextPosition(pageContext.locationTracker); + shelfItems.push(adCard); + } + adShelf.items = shelfItems; + return isDefinedNonNullNonEmpty(adShelf.items) ? adShelf : null; +} +/** + * Try to create an ad card for the page context if the requirements are met + * @param objectGraph The dependency graph of the app store + * @param pageContext The context of the page so we can tell whether its time to insert an ad + * @param cardConfig The configuration of the card + * @returns The card to insert at the ad slot if necessary + */ +export function createAdCardForTodayPageContextIfNecessary(objectGraph, pageContext, cardConfig) { + var _a, _b, _c; + if (!isAdPlacementEnabled(objectGraph, "today")) { + return null; + } + // The ad should respect its slot, unless we're using reco logic for organic replacement. If we need to replace + // the organic, we will arbitrarily respect the reco flag to avoid edge cases with mismatching data + if (pageContext.adLocation !== pageContext.parsedCardCount && + pageContext.adPlacementBehavior !== models.AdPlacementBehavior.replaceOrganic) { + return null; + } + (_a = pageContext.pageInformation.iAdInfo) === null || _a === void 0 ? void 0 : _a.updateContainerId((_b = pageContext.pageInformation.iAdInfo) === null || _b === void 0 ? void 0 : _b.containerIdForSlotIndex((_c = pageContext.parsedCardCount) !== null && _c !== void 0 ? _c : 0)); + const adCard = createTodayAdCard(objectGraph, pageContext.adData, pageContext.adIncidentRecorder, cardConfig, pageContext); + if (isSome(adCard)) { + return adCard; + } + else { + return null; + } +} +// MARK: - Shelf Header Content +/** + * @param objectGraph The dependency graph of the app store + * @param item The today item to look for the eyebrow in + * @returns The text displayed above the title on a section header + */ +export function sectionEyebrowFromTodayItem(objectGraph, item) { + const editorialClientParams = extractEditorialClientParams(objectGraph, item.data); + if (item.isFirstItemInModule || editorialClientParams.suppressHeaderBadge) { + return null; + } + let sectionBadge; + const editorialCard = editorialCardFromData(item.data); + if (hasAttributes(editorialCard)) { + sectionBadge = mediaAttributes.attributeAsString(editorialCard, "headerBadge"); + } + if (isSome(sectionBadge)) { + return sectionBadge; + } + switch (item.data.type) { + case "editorial-items": + sectionBadge = attributeAsString(item.data, "headerBadge"); + break; + case "editorial-item-groups": + sectionBadge = attributeAsString(item.data, ["editorialNotes", "badge"]); + break; + default: + break; + } + return sectionBadge; +} +/** + * @param objectGraph The dependency graph of the app store + * @param item The today item to look for the title in + * @param alwaysReturnTitle If true, the title will be returned even if it is the first item in a module + * @returns The title displayed above a section + */ +export function sectionTitleFromTodayItem(objectGraph, item, alwaysReturnTitle = false) { + const editorialClientParams = extractEditorialClientParams(objectGraph, item.data); + if ((item.isFirstItemInModule || editorialClientParams.suppressHeaderName) && !alwaysReturnTitle) { + return null; + } + let sectionTitle; + const editorialCard = editorialCardFromData(item.data); + if (hasAttributes(editorialCard)) { + sectionTitle = mediaAttributes.attributeAsString(editorialCard, "headerName"); + } + if (isSome(sectionTitle)) { + return sectionTitle; + } + switch (item.data.type) { + case "editorial-items": + sectionTitle = attributeAsString(item.data, "headerName"); + break; + case "editorial-item-groups": + sectionTitle = attributeAsString(item.data, ["editorialNotes", "name"]); + break; + default: + break; + } + return sectionTitle; +} +/** + * @param objectGraph The dependency graph of the app store + * @param item The today item to look for the subtitle in + * @returns The subtitle displayed below the title on a section header + */ +function sectionSubtitleFromTodayItem(objectGraph, item) { + const editorialClientParams = extractEditorialClientParams(objectGraph, item.data); + if (item.isFirstItemInModule || editorialClientParams.suppressHeaderTagline) { + return null; + } + let sectionSubtitle; + const editorialCard = editorialCardFromData(item.data); + if (hasAttributes(editorialCard)) { + sectionSubtitle = mediaAttributes.attributeAsString(editorialCard, "headerTagline"); + } + if (isSome(sectionSubtitle)) { + return sectionSubtitle; + } + switch (item.data.type) { + case "editorial-items": + sectionSubtitle = attributeAsString(item.data, "headerTagline"); + break; + case "editorial-item-groups": + sectionSubtitle = attributeAsString(item.data, ["editorialNotes", "tagline"]); + break; + default: + break; + } + return sectionSubtitle; +} +/** + * @param objectGraph The dependency graph of the app store + * @param item The today item to look for the subtitle in + * @returns The artwork displayed in the eyebrow of a today section. + */ +function sectionHeaderArtworkFromTodayItemForPlacement(objectGraph, item, placement) { + var _a; + const editorialClientParams = extractEditorialClientParams(objectGraph, item.data); + const headerContents = relationshipData(objectGraph, item.data, "header-contents"); + const artworkBehavior = (_a = editorialClientParams.headerArtworkBehavior) !== null && _a !== void 0 ? _a : TodayHeaderArtworkBehavior.NoArtwork; + switch (placement) { + case TodaySectionHeaderArtworkPlacement.Eyebrow: + switch (artworkBehavior) { + case TodayHeaderArtworkBehavior.CategoryArtworkWithBadge: + return categoryArtworkFromData(objectGraph, headerContents); + default: + return null; + } + case TodaySectionHeaderArtworkPlacement.Title: + switch (artworkBehavior) { + case TodayHeaderArtworkBehavior.CategoryArtworkWithTitle: + return categoryArtworkFromData(objectGraph, headerContents); + case TodayHeaderArtworkBehavior.ContentArtworkWithTitle: + return iconFromData(objectGraph, headerContents, { + useCase: 1 /* ArtworkUseCase.LockupIconSmall */, + }); + default: + return null; + } + default: + return null; + } +} +/** + * @param objectGraph The dependency graph of the app store + * @param item The today item to look for the subtitle in + * @returns The artwork type displayed in the eyebrow of a today section. + */ +function sectionHeaderArtworkTypeFromTodayItemForPlacement(objectGraph, item, placement) { + const headerContents = relationshipData(objectGraph, item.data, "header-contents"); + const artworkBehavior = attributeAsString(item.data, [ + "editorialClientParams", + "headerArtworkBehavior", + ]); + switch (placement) { + case TodaySectionHeaderArtworkPlacement.Eyebrow: + switch (artworkBehavior) { + case TodayHeaderArtworkBehavior.CategoryArtworkWithBadge: + const hasCategoryArtwork = isSome(categoryArtworkFromData(objectGraph, headerContents)); + return hasCategoryArtwork ? models.ShelfHeaderArtworkType.Category : null; + default: + return null; + } + case TodaySectionHeaderArtworkPlacement.Title: + switch (artworkBehavior) { + case TodayHeaderArtworkBehavior.CategoryArtworkWithTitle: + const hasCategoryArtwork = isSome(categoryArtworkFromData(objectGraph, headerContents)); + return hasCategoryArtwork ? models.ShelfHeaderArtworkType.Category : null; + case TodayHeaderArtworkBehavior.ContentArtworkWithTitle: + const hasIconArtwork = isSome(categoryArtworkFromData(objectGraph, headerContents)); + return hasIconArtwork ? models.ShelfHeaderArtworkType.Icon : null; + default: + return null; + } + default: + return null; + } +} +/** + * + * @param objectGraph The dependency graph of the app store + * @param data The MAPI data for the related content for a today item + * @returns The artwork to use for the related content + */ +function categoryArtworkFromData(objectGraph, data) { + const artworkData = categoryArtworkData(objectGraph, data, false, false, false); + if (isNothing(artworkData)) { + return null; + } + const artwork = artworkFromApiArtwork(objectGraph, artworkData, { + useCase: 20 /* ArtworkUseCase.CategoryIcon */, + allowingTransparency: true, + cropCode: "sr", + }); + return artwork; +} +/** + * @param objectGraph The dependency graph for the App Store + * @param todayItem The today item that contains the data for a editorial-item or editorial-item-group, this is used to look for + * editorialBackground, which can then be used to generate the section gradient background + * @returns The background to use for the section + */ +function todaySectionEditorialBackground(objectGraph, todayItem) { + const editorialBackground = mediaAttributes.attributeAsDictionary(todayItem.data, "editorialBackground", null); + const editorialBackgroundType = editorialBackground === null || editorialBackground === void 0 ? void 0 : editorialBackground["type"]; + if (isNothing(editorialBackgroundType)) { + return null; + } + let backgroundInfo = null; + switch (editorialBackgroundType) { + case EditorialBackgroundType.LinearGradient: + const linearGradientData = asInterface(editorialBackground); + const colors = linearGradientData.stops.map((stop) => color.fromHex(stop.color)); + const shelfBackground = { + type: "gradient", + colors: colors, + start: models.ShelfBackgroundGradientLocation.Top, + end: models.ShelfBackgroundGradientLocation.Bottom, + }; + const isDark = color.isDarkColor(colors[0]); + const secondaryLabelLightColor = { + type: "rgb", + red: 60 / 255, + green: 60 / 255, + blue: 67 / 255, + alpha: 0.6, + }; + const secondaryLabelDarkColor = { + type: "rgb", + red: 235.0 / 255, + green: 235.0 / 255, + blue: 245.0 / 255, + alpha: 0.6, + }; + backgroundInfo = { + shelfBackground: shelfBackground, + eyebrowColor: isDark ? secondaryLabelDarkColor : secondaryLabelLightColor, + titleColor: isDark ? color.named("white") : color.named("black"), + subtitleColor: isDark ? secondaryLabelDarkColor : secondaryLabelLightColor, + }; + break; + default: + backgroundInfo = null; + break; + } + return backgroundInfo; +} +/** + * @param todayCards The today cards that are contained in the section + * @returns The background to use for the section + */ +function todaySectionBackgroundForHeroDisplayStyle(todayCards) { + const backgroundColors = todayCards + .map((todayCard) => { + return todayCard.media.bestBackgroundColor(); + }) + .filter((backgroundColor) => isSome(backgroundColor)); + let shelfBackground = null; + if (backgroundColors.length > 0 && backgroundColors.length <= 4 && backgroundColors.length === todayCards.length) { + switch (backgroundColors.length) { + case 1: + shelfBackground = { + type: "materialGradient", + colors: { + colorCount: "oneColor", + color: backgroundColors[0], + }, + }; + break; + case 2: + shelfBackground = { + type: "materialGradient", + colors: { + colorCount: "twoColor", + top: backgroundColors[0], + bottom: backgroundColors[1], + }, + }; + break; + case 3: + shelfBackground = { + type: "materialGradient", + colors: { + colorCount: "threeColor", + top: backgroundColors[0], + bottomLeading: backgroundColors[1], + bottomTrailing: backgroundColors[2], + }, + }; + break; + case 4: + shelfBackground = { + type: "materialGradient", + colors: { + colorCount: "fourColor", + topLeading: backgroundColors[0], + topTrailing: backgroundColors[1], + bottomLeading: backgroundColors[2], + bottomTrailing: backgroundColors[3], + }, + }; + break; + default: + break; + } + } + else { + shelfBackground = { + type: "color", + color: color.named("secondarySystemBackground"), + }; + } + return shelfBackground; +} +// MARK: - Metrics +/** + * Retrieves the recommendation metrics data from a FlattenedTodayItem object. + * @param todayItem - The FlattenedTodayItem object containing the module metadata. + * @returns The recommendation metrics data as a JSONData object. + */ +export function recoMetricsFromTodayItem(todayItem) { + var _a, _b; + if (isNothing(todayItem)) { + return {}; + } + const recoMetricsData = (_a = asDictionary(todayItem.moduleMetadata, "meta.metrics")) !== null && _a !== void 0 ? _a : {}; + const combinedRecoMetricsData = (_b = combinedRecoMetricsDataFromMetricsData(recoMetricsData, todayItem.moduleMetadata.onDevicePersonalizationProcessingType, null)) !== null && _b !== void 0 ? _b : {}; + return combinedRecoMetricsData; +} +// MARK: - Debug Util +/** + * Generate a todayCardPreview url that contains the entire original today feed. + * @param objectGraph The dependency graph of the app store + * @param todayItems The flattened today feed items + * @returns The url to add to the today page for debug purposes + */ +export function feedPreviewUrlFromFlattenedTodayItems(objectGraph, todayItems) { + switch (objectGraph.client.buildType) { + case "debug": + case "internal": + const feedPreviewUrl = new URL(); + feedPreviewUrl.protocol = Protocol.https; + feedPreviewUrl.host = "apps.apple.com"; + feedPreviewUrl.pathname = `/${Path.todayCardPreview}`; + const idsParamValues = []; + for (const item of todayItems) { + switch (item.type) { + case FlattenedTodayItemType.EditorialItem: + idsParamValues.push(item.data.id); + break; + case FlattenedTodayItemType.EditorialItemGroup: + const groupItems = asArrayOrEmpty(item.data.meta, "associations.recommendations.data"); + idsParamValues.push(`${item.data.id}:[${groupItems.map((groupItem) => groupItem.id).join(",")}]`); + break; + default: + break; + } + } + feedPreviewUrl.param(Parameters.ids, idsParamValues.join(",")); + feedPreviewUrl.param(Parameters.isTodayFeedPreview, "true"); + return decodeURIComponent(feedPreviewUrl.build()); + default: + return null; + } +} +/** + * Generate a todayCardPreview url that can display a single today card + * @param objectGraph The dependency graph of the app store + * @param todayCardId The id of the created today card + * @param cardConfig The card config for the created today card + * @returns The url to add to the today page for debug purposes + */ +export function todayCardPreviewUrlForTodayCard(objectGraph, todayCardId, cardConfig) { + if (isNothing(todayCardId) || !objectGraph.client.isiOS) { + return null; + } + switch (objectGraph.client.buildType) { + case "debug": + case "internal": + const feedPreviewUrl = new URL(); + feedPreviewUrl.protocol = Protocol.https; + feedPreviewUrl.host = "apps.apple.com"; + feedPreviewUrl.pathname = `/${Path.todayCardPreview}`; + feedPreviewUrl.param(Parameters.ids, `${todayCardId}`); + feedPreviewUrl.param(Parameters.isTodayFeedPreview, "true"); + feedPreviewUrl.param(Parameters.isTodaySection, cardConfig.useOTDTextStyle ? "true" : "false"); + return decodeURIComponent(feedPreviewUrl.build()); + default: + return null; + } +} +//# sourceMappingURL=today-parse-util.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/today/today-types.js b/node_modules/@jet-app/app-store/tmp/src/common/today/today-types.js new file mode 100644 index 0000000..3c02e97 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/today/today-types.js @@ -0,0 +1,118 @@ +import * as models from "../../api/models"; +import * as metricsHelpersLocation from "../metrics/helpers/location"; +/** + * Stateful container passed along pipeline from initial page building, mutating while components are being built. + * Properties on this *can* be snapshotted to re-initialize a context for shelf-reloading and pagination. + */ +export class TodayParseContext { + constructor(pageInformation, locationTracker, refreshController) { + // The location tracker for building metrics location information + this.locationTracker = metricsHelpersLocation.newLocationTracker(); + /// The number of cards that we've successfully parsed, This excludes failed cards + this.parsedCardCount = 0; + /// The number of rows of cards that we've successfully added to the page. This is only applicable on + /// iPhone where we know there is only one layout per page. This differs from parsedCardCount in that + /// rows can contain multiple cards, such is the case with hero / grid shelves. + this.currentRowIndex = 0; + this.pageInformation = pageInformation; + this.locationTracker = locationTracker !== null && locationTracker !== void 0 ? locationTracker : metricsHelpersLocation.newLocationTracker(); + this.refreshController = refreshController; + } +} +/** + * The `TodayPageContext` is used for building the initial page. It is used to maintain a record + * of the remaining items left in the today page, so it is also used for pagination + */ +export class TodayPageContext extends TodayParseContext { + constructor(remainingContent, pageInformation, locationTracker, refreshController, recoImpressionData) { + super(pageInformation, locationTracker, refreshController); + // Flag indicating whether we've ever successfully displayed any content on the page, if we're + // doing a secondary fetch and this is false, we try to fetch a bit more than normal pagination + // to have a higher chance of getting **some** content back. + this.pageHasDisplayedContent = false; + this.remainingContent = remainingContent; + this.adPlacementBehavior = models.AdPlacementBehavior.insertIntoShelf; + this.recoImpressionData = recoImpressionData; + } +} +/** + * A `TodayShelfToken` is used to maintain a record of each shelf's contents when subsequent content fetch is needed. It is encoded in the `url` property of `Shelf`. + */ +export class TodayShelfToken { + // endregion + constructor(todayModule, contentOffsetWithinModule, metricsPageInformation, metricsLocationTracker) { + this.todayModule = todayModule; + this.metricsPageInformation = metricsPageInformation; + this.contentOffsetWithinModule = contentOffsetWithinModule; + this.metricsLocationTracker = metricsLocationTracker; + } +} +/** + * These are the different ways we can display a today card + */ +export var TodayCardDisplayStyle; +(function (TodayCardDisplayStyle) { + TodayCardDisplayStyle["AppEventCard"] = "AppEventCard"; + TodayCardDisplayStyle["AppOfTheDay"] = "AppOfTheDay"; + TodayCardDisplayStyle["FullBleedImage"] = "FullBleedImage"; + TodayCardDisplayStyle["GameOfTheDay"] = "GameOfTheDay"; + TodayCardDisplayStyle["Grid"] = "Grid"; + TodayCardDisplayStyle["InAppPurchase"] = "InAppPurchase"; + TodayCardDisplayStyle["List"] = "List"; + TodayCardDisplayStyle["NumberedList"] = "NumberedList"; + TodayCardDisplayStyle["River"] = "River"; + TodayCardDisplayStyle["ShortImage"] = "ShortImage"; + TodayCardDisplayStyle["SingleApp"] = "SingleApp"; + TodayCardDisplayStyle["Video"] = "Video"; +})(TodayCardDisplayStyle || (TodayCardDisplayStyle = {})); +/** + * These are the different sizes of today card we can have, + * the reason this uses the term `DisplayStyle` is because the the metrics spec + * already uses the term `DisplayStyle` to describe the size of different components + * so we're reusing that term here. This is not to be confused with `TodayCardDisplayStyle` + */ +export var TodayCardMetricsDisplayStyle; +(function (TodayCardMetricsDisplayStyle) { + TodayCardMetricsDisplayStyle["SmallCard"] = "smallCard"; + TodayCardMetricsDisplayStyle["MediumCard"] = "mediumCard"; +})(TodayCardMetricsDisplayStyle || (TodayCardMetricsDisplayStyle = {})); +/** + * These are the different ways we can display artwork within a today section header + */ +export var TodayHeaderArtworkBehavior; +(function (TodayHeaderArtworkBehavior) { + TodayHeaderArtworkBehavior["NoArtwork"] = "no-artwork"; + TodayHeaderArtworkBehavior["ContentArtworkWithTitle"] = "content-artwork-with-title"; + TodayHeaderArtworkBehavior["CategoryArtworkWithTitle"] = "category-artwork-with-title"; + TodayHeaderArtworkBehavior["CategoryArtworkWithBadge"] = "category-artwork-with-badge"; +})(TodayHeaderArtworkBehavior || (TodayHeaderArtworkBehavior = {})); +/** + * The different types of today card OTD intentions + */ +export var OfTheDayIntention; +(function (OfTheDayIntention) { + OfTheDayIntention["AppOfTheDay"] = "app-of-the-day"; + OfTheDayIntention["GameOfTheDay"] = "game-of-the-day"; +})(OfTheDayIntention || (OfTheDayIntention = {})); +/** + * Indicates the different places we can have artwork in the section header + */ +export var TodaySectionHeaderArtworkPlacement; +(function (TodaySectionHeaderArtworkPlacement) { + TodaySectionHeaderArtworkPlacement["Eyebrow"] = "eyebrow"; + TodaySectionHeaderArtworkPlacement["Title"] = "title"; +})(TodaySectionHeaderArtworkPlacement || (TodaySectionHeaderArtworkPlacement = {})); +export var EditorialBackgroundType; +(function (EditorialBackgroundType) { + EditorialBackgroundType["LinearGradient"] = "linear-gradient"; +})(EditorialBackgroundType || (EditorialBackgroundType = {})); +/** + * Describes the context in which hero media appears. Used to + * configure cards differently on macOS depending on where it appears. + */ +export var HeroMediaDisplayContext; +(function (HeroMediaDisplayContext) { + HeroMediaDisplayContext["Inline"] = "inline"; + HeroMediaDisplayContext["Article"] = "article"; +})(HeroMediaDisplayContext || (HeroMediaDisplayContext = {})); +//# sourceMappingURL=today-types.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/top-charts-common.js b/node_modules/@jet-app/app-store/tmp/src/common/top-charts-common.js new file mode 100644 index 0000000..c93843b --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/top-charts-common.js @@ -0,0 +1,212 @@ +/** + * Common for top-charts. + * Incrementally migrate `top-charts-controller` builder logic here. + */ +import { isSome, isNothing } from "@jet/environment/types/optional"; +import * as categories from "../common/categories"; +import { isDefinedNonNullNonEmpty } from "../foundation/json-parsing/server-data"; +import { attributeAsString } from "../foundation/media/attributes"; +import { fetchData } from "../foundation/media/network"; +import { Parameters } from "../foundation/network/url-constants"; +import { URL } from "../foundation/network/urls"; +import * as client from "../foundation/wrappers/client"; +import { mediaApiChartRequestForGenre } from "./builders/url-mapping"; +// region API - URLs +/** + * Given a chart type, lookup the most appropriate chart to show next to it using + * fallback mapping logic, e.g. `top-paid` for `top-free` and visa versa. + * + * MAINTAINERS NOTE: This is typically driven by Editorial's programming but this can be used as a fallback. + * + * @param objectGraph The App Store object graph. + * @param chartType The type of chart to lookup a sibling for, e.g. `top-free`. + * @returns The sibling of the provided chart type, e.g. `top-paid`. + */ +export function lookupFallbackSiblingChart(objectGraph, chartType) { + // Vision currently doesn't support sibling charts - don't attempt to find one. + if (isNothing(chartType) || objectGraph.client.isVision) { + return undefined; + } + switch (chartType) { + case "top-free" /* TopChartType.TopFree */: + return "top-paid" /* TopChartType.TopPaid */; + case "top-paid" /* TopChartType.TopPaid */: + return "top-free" /* TopChartType.TopFree */; + /* + ** MAINTAINERS NOTE ** + We don't want to show "Top Apps" / "Paid Apps" for extensions, trending, or emerging, + so don't map these together until we can confirm the desired UX we want for this. + + case TopChartType.TopFreeSafariExtensions: + return TopChartType.TopPaidSafariExtensions; + case TopChartType.TopPaidSafariExtensions: + return TopChartType.TopFreeSafariExtensions; + case TopChartType.TopFreeTrending: + return TopChartType.TopPaidTrending; + case TopChartType.TopPaidTrending: + return TopChartType.TopFreeTrending; + case TopChartType.TopFreeEmerging: + return TopChartType.TopPaidEmerging; + case TopChartType.TopPaidEmerging: + return TopChartType.TopFreeEmerging; + */ + default: + return undefined; + } +} +/** + * Creates a charts parameter value for an invidual chart shelf's see all flow action URL, which may show multiple charts. + * + * ** MAINTAINERS NOTE ** + * This function guarantees the selected chart is always included in charts. + * This helps make the user experience more resilient to either unexpected editorial programming or client JS programmer error. + * For example, if provided chart set is missing the selected chart or is missing any siblings, we lookup hardcoded fallback + * mappings, e.g. `top-paid` for `top-free`. + * + * @param objectGraph The App Store object graph. + * @param selectedChartType The selected chart, e.g. `top-free`. + * @param chartSet The collection fo charts to be shown as sibligs, e.g. `top-free` and `top-paid` in a segmented control + * @returns Value for charts URL query paramater. + */ +function makeChartsParameterValue(objectGraph, selectedChartType, chartSet) { + var _a; + const hasMultipleCharts = (_a = (chartSet === null || chartSet === void 0 ? void 0 : chartSet.editorialCharts.length) > 1) !== null && _a !== void 0 ? _a : false; + const charts = chartSet === null || chartSet === void 0 ? void 0 : chartSet.editorialCharts.map((chart) => chart.type.toString()).join(","); + // use editorially programmed chart set if defined with multiple charts, including the selection. + if (isSome(charts) && charts.includes(selectedChartType) && hasMultipleCharts) { + return charts; + } + // otherwise, lookup hardcoded fallback sibling if available, e.g. `top-paid` for `top-free`. + const siblingChartType = lookupFallbackSiblingChart(objectGraph, selectedChartType); + if (siblingChartType === undefined) { + return selectedChartType; // no sibling found, show just the single selected chart + } + return [selectedChartType, siblingChartType].join(","); // show harcoded pair of charts +} +/** + * Construct the URL for a single chart shelf "See All", that might be part of an editorial chart set. + * @param objectGraph The App Store object graph. + * @param chartData The media API data for the chart shelf, e.g. `top-paid`. + * @param chartSet The set of charts shown when tapping "See All", e.g. `top-free` and `top-paid`. + */ +export function makeSeeAllUrlFromMediaApiData(objectGraph, chartData, chartSet) { + const baseUrl = attributeAsString(chartData, "chartHref"); + const chartSeeAllUrl = new URL(baseUrl); + /** + * The selected chart to show, e.g. `top-free`. + */ + const selectedChart = attributeAsString(chartData, "chart"); + if (isDefinedNonNullNonEmpty(selectedChart)) { + chartSeeAllUrl.param(Parameters.chart, selectedChart); + } + /** + * The collection of charts to show, e.g. `top-free` and `top-paid`. + * This is needed since a chart sets are vended down as a single fcKind item with children that is divided into multiple shelves. + * Tapping "See All" on a chart shelf, should show a top charts page with all `charts` provided and `chart` selected. + */ + const charts = makeChartsParameterValue(objectGraph, selectedChart, chartSet); + if (isDefinedNonNullNonEmpty(charts)) { + chartSeeAllUrl.param(Parameters.charts, charts); + } + return chartSeeAllUrl.toString(); +} +/** + * Returns whether or not given chart page URL charts parameter is for safari extensions. + * Charts for safari extensions must be handled as a special case since they don't have an associated specific genre. + * + * @param url The URL to check if it is Safari Extension charts. + */ +function isSafariExtensionCharts(charts) { + return (isDefinedNonNullNonEmpty(charts) && + (charts.indexOf("top-free-safari-extensions" /* TopChartType.TopFreeSafariExtensions */) !== -1 || + charts.indexOf("top-paid-safari-extensions" /* TopChartType.TopPaidSafariExtensions */) !== -1)); +} +// endregion API +// region MAPI Requests +/** + * Fetch the top-level structural data for top charts. + * + * @param objectGraph The App Store object graph. + * @param genreId The genre to fetch charts for, e.g. Apps or Games. + * @param charts The specific charts in this genre. + */ +export async function fetchTopChartsData(objectGraph, genreId, charts, ages = null) { + // Request + const topChartsRequest = mediaApiChartRequestForGenre(objectGraph, genreId, charts, ages); + // Pagination + Sparse limits + topChartsRequest.withLimit(200); + if (objectGraph.client.isMac || objectGraph.client.isWeb) { + topChartsRequest.withSparseLimit(25); + } + topChartsRequest.enablingFeature("newChartsElements"); + return await fetchData(objectGraph, topChartsRequest); +} +/** + * Fetch the categories data promise accompanying given `chartUrl`. + * + * @param objectGraph Object graph + * @param charts The charts to fetch categories data for, e.g. `top-free,top-paid` + * @param genreId The genre for chart to fetch categories data for, e.g. Apps or Games + */ +export async function fetchCategoriesDataForPlatform(objectGraph, charts, genreId) { + let categoriesRequestPromise; + /** + * No categories filter on: + * - All tvOS charts + * - All Messages AppStore charts + * - Safari Extension Charts (Genre-less Genre) + */ + if (objectGraph.client.isTV || + objectGraph.host.clientIdentifier === client.messagesIdentifier || + isSafariExtensionCharts(charts)) { + categoriesRequestPromise = Promise.resolve({}); + } + else { + const categoriesRequest = categories.createRequest(objectGraph, genreId); + categoriesRequestPromise = fetchData(objectGraph, categoriesRequest).catch(() => undefined); + } + return await categoriesRequestPromise; +} +// endregion +// region Page Title +/** + * Return the top charts page title to use for a collection of top chart segments. + * + * @param objectGraph Object graph + * @param segments The segments to derive page title for + */ +export function topChartsPageTitleForSegments(objectGraph, segments) { + const everySegmentIsTrending = segments.every((seg) => isTrendingChartType(seg.chart)); + if (everySegmentIsTrending) { + return objectGraph.loc.string("PAGE_TITLE_TRENDING_CHARTS"); + } + const everySegmentIsEmerging = segments.every((seg) => isEmergingChartType(seg.chart)); + if (everySegmentIsEmerging) { + return objectGraph.loc.string("PAGE_TITLE_EMERGING_CHARTS"); + } + return objectGraph.loc.string("PAGE_TITLE_TOP_CHARTS"); +} +// endregion +// region Top Charts Type +/// Whether or not given chart type is trending chart type +function isTrendingChartType(chart) { + switch (chart) { + case "top-trending-free" /* TopChartType.TopFreeTrending */: + case "top-trending-paid" /* TopChartType.TopPaidTrending */: + return true; + default: + return false; + } +} +/// Whether or not given chart type is emerging chart type +function isEmergingChartType(chart) { + switch (chart) { + case "top-emerging-free" /* TopChartType.TopFreeEmerging */: + case "top-emerging-paid" /* TopChartType.TopPaidEmerging */: + return true; + default: + return false; + } +} +// endregion +//# sourceMappingURL=top-charts-common.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/top-charts/top-charts-page.js b/node_modules/@jet-app/app-store/tmp/src/common/top-charts/top-charts-page.js new file mode 100644 index 0000000..0f24279 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/top-charts/top-charts-page.js @@ -0,0 +1,161 @@ +import { FetchTimingMetricsBuilder } from "@jet/environment/metrics/fetch-timing-metrics-builder"; +import * as validation from "@jet/environment/json/validation"; +import * as models from "../../api/models"; +import * as mediaUrlMapping from "../../common/builders/url-mapping"; +import * as categories from "../../common/categories"; +import * as topChartsCommon from "../../common/top-charts-common"; +import * as serverData from "../../foundation/json-parsing/server-data"; +import { isSome } from "@jet/environment/types/optional"; +import { makeChartsPageIntent } from "../../api/intents/charts-page-intent"; +import { segmentFromData } from "../../common/charts/charts-page-model"; +import { makeChartsPageURL } from "../../common/charts/charts-page-url"; +import { getLocale } from "../../common/locale"; +import { getPlatform } from "../../common/preview-platform"; +import * as mediaDataStructure from "../../foundation/media/data-structure"; +import * as mediaUrlBuilder from "../../foundation/media/url-builder"; +import * as urls from "../../foundation/network/urls"; +export async function fetchAndRenderTopChartPage(objectGraph, genreId, charts, selectedChart, ages) { + if (serverData.isNullOrEmpty(charts)) { + const siblingChart = topChartsCommon.lookupFallbackSiblingChart(objectGraph, selectedChart); + if (siblingChart !== undefined) { + charts = `${selectedChart},${siblingChart}`; // charts not provided, use hardcoded sibling along with selected chart + } + else { + charts = selectedChart; // charts not provided, use selected chart + } + } + else if (!charts.includes(selectedChart)) { + charts += `,${selectedChart}`; // charts missing selected chart, so inject it. + } + // Request + // Note: `FetchTimingMetricsBuilder` API currently only supports instrumenting a single primary network call. + // We will choose to use the network metrics data from the top charts call. + const fetchTimingMetricsBuilder = new FetchTimingMetricsBuilder(); + const modifiedObjectGraph = objectGraph.addingFetchTimingMetricsBuilder(fetchTimingMetricsBuilder); + const topChartsPromise = topChartsCommon.fetchTopChartsData(modifiedObjectGraph, genreId, charts, ages); + let categoriesRequestPromise; + // Vision currently doesn't support displaying categories on the Charts page. + if (!objectGraph.client.isVision) { + categoriesRequestPromise = topChartsCommon.fetchCategoriesDataForPlatform(objectGraph, charts, genreId); + } + // Response + return await Promise.all([topChartsPromise, categoriesRequestPromise]).then(([topChartsResponse, categoriesResponse]) => { + return fetchTimingMetricsBuilder.measureModelConstruction(() => { + const categoriesList = categories.categoryListFromApiResponse(objectGraph, categoriesResponse); + const context = serverData.isNull(categoriesList) + ? categories.createContextFromGenre(genreId) + : categories.createContextFromList(categoriesList); + return topChartsPageFromApiResponse(modifiedObjectGraph, topChartsResponse, genreId, categoriesList, selectedChart, context); + }); + }); +} +/** + * Creates a Top Charts Page model object from a server data structure. + * + * @param objectGraph The object graph for the App Store. + * @param response The server response to parse. + * @param genreId The genre identifier of the genre to which this page belongs. + * @param categoriesList The list of categories for top charts picker. + * @param selectedChart The selected chart, typically shown in a segemented control. + * @param context The context of the charts, .e.g. All, Apps, or Games. + * @returns A top charts page model object. + */ +export function topChartsPageFromApiResponse(objectGraph, response, genreId, categoriesList, selectedChart, context) { + return validation.context("topChartsPageFromApiResponse", () => { + var _a; + // Categories + const topChartsCategories = []; + if (isSome(categoriesList)) { + for (const categoryListItem of categoriesList.categories) { + if (isSome(categoryListItem.genreId)) { + topChartsCategories.push(convertCategoryToTopChartsCategory(objectGraph, categoryListItem)); + } + } + } + // Construct the segments and sort by chart name to prevent the order from changing + const rawSegments = mediaDataStructure.chartResultsFromServerResponse(response); + const segments = rawSegments + .map((segment) => { + return segmentFromData(objectGraph, segment, response, genreId, context); + }) + .sort((a, b) => { + return a.chart.localeCompare(b.chart); + }); + const segmentHref = serverData.asString(rawSegments, "0.href"); + const segmentURL = new urls.URL(segmentHref); + const segmentGenreId = (_a = segmentURL.query["genre"]) !== null && _a !== void 0 ? _a : genreId; + const ages = segmentURL.query["ages"]; + const selectedCategory = findCategoryInTree(segmentGenreId, ages, topChartsCategories); + const title = topChartsCommon.topChartsPageTitleForSegments(objectGraph, segments); + // Build Page + const topChartsPage = new models.TopChartsPage(segmentGenreId, selectedCategory === null || selectedCategory === void 0 ? void 0 : selectedCategory.ageBandId, title, segments, selectedCategory === null || selectedCategory === void 0 ? void 0 : selectedCategory.name, topChartsCategories); + // The "web" client needs a `FlowAction` defined on each category and segment to perform navigation + if (objectGraph.client.isWeb) { + for (const category of topChartsCategories) { + const destination = makeChartsPageIntent({ + ...getLocale(objectGraph), + ...getPlatform(objectGraph), + genreId: category.genreId, + chart: selectedChart, + ageBandId: category.ageBandId, + }); + const pageUrl = makeChartsPageURL(objectGraph, destination); + const action = new models.FlowAction("topCharts"); + action.destination = destination; + action.pageUrl = pageUrl; + category.chartSelectAction = action; + } + for (const segment of segments) { + const destination = makeChartsPageIntent({ + ...getLocale(objectGraph), + ...getPlatform(objectGraph), + genreId, + chart: segment.chart, + ageBandId: selectedCategory === null || selectedCategory === void 0 ? void 0 : selectedCategory.ageBandId, + }); + const pageUrl = makeChartsPageURL(objectGraph, destination); + const action = new models.FlowAction("topCharts"); + action.destination = destination; + action.pageUrl = pageUrl; + segment.segmentSelectAction = action; + } + } + // Find selected chart segment + const selectedIndex = segments.findIndex((segment) => segment.chart === selectedChart); + if (selectedIndex >= 0) { + topChartsPage.initialSegmentIndex = selectedIndex; + } + // Decorate the final page model with network performance metrics. + const fetchTimingMetricsBuilder = objectGraph.fetchTimingMetricsBuilder; + if (isSome(fetchTimingMetricsBuilder)) { + fetchTimingMetricsBuilder.decorate(topChartsPage); + } + return topChartsPage; + }); +} +export function convertCategoryToTopChartsCategory(objectGraph, category) { + const topChartsPageRequest = mediaUrlMapping.mediaApiChartsRequest(objectGraph, category.genreId, category.ageBandId); + const children = category.children.map((child) => convertCategoryToTopChartsCategory(objectGraph, child)); + return new models.TopChartCategory(category, mediaUrlBuilder.buildURLFromRequest(objectGraph, topChartsPageRequest).toString(), children); +} +/** + * Recursively search through a category list to find a matching genre/age + * @param genreId The genreId to search for + * @param ageBandId The age band to include when searching for selected genre + * @param categoryList The array of categories to search recursively through + * @returns A found category, or null if not found. + */ +function findCategoryInTree(genreId, ageBandId, categoryList) { + for (const category of categoryList) { + if ((ageBandId && category.ageBandId === ageBandId) || + (!ageBandId && !category.ageBandId && category.genreId === genreId)) { + return category; + } + const result = findCategoryInTree(genreId, ageBandId, category.children); + if (result) { + return result; + } + } + return null; +} +//# sourceMappingURL=top-charts-page.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/util/app-tags-util.js b/node_modules/@jet-app/app-store/tmp/src/common/util/app-tags-util.js new file mode 100644 index 0000000..0a7808a --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/util/app-tags-util.js @@ -0,0 +1,25 @@ +import { isNothing } from "@jet/environment"; +/** + * @param objectGraph The object graph for our dependencies + * @returns Whether we should fetch app tags + */ +export function areAppTagsEnabled(objectGraph, featureSurface) { + const isClientFeatureFlagEnabled = objectGraph.client.isPhone && objectGraph.featureFlags.isEnabled("app_genome_2025A"); + if (!isClientFeatureFlagEnabled) { + return false; + } + if (isNothing(featureSurface)) { + return true; + } + switch (featureSurface) { + case "grouping": + return objectGraph.bag.isAppsGroupingTagsEnabled; + case "product": + return objectGraph.bag.isAppsProductPageTagsEnabled; + case "slp": + return objectGraph.bag.isAppsSlpTagsEnabled; + default: + return false; + } +} +//# sourceMappingURL=app-tags-util.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/util/generate-routes.js b/node_modules/@jet-app/app-store/tmp/src/common/util/generate-routes.js new file mode 100644 index 0000000..f370dbe --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/util/generate-routes.js @@ -0,0 +1,183 @@ +import { isSome } from "@jet/environment"; +import { URL } from "@jet/environment/util/urls"; +import { Path, Parameters } from "../../foundation/network/url-constants"; +import { normalizeLocale, deriveLocaleForUrl } from "../locale"; +/** + * Generate the routes property for a `RouteProvider` and a canonical URL factory + * function for a {@link RoutableIntent} (non-detail page). + * + * `makeIntent` should be a function that accepts a single parameter, + * `options: Omit<YourIntent, '$kind'>`, and returns the intent. + * + * `path` is the static part of the path. For example, in `'/us/browse?l=es'`, + * `'/browse'` is the static part of the path. Path can contain multiple + * segments like `'/foo/bar'`, but it does not support parameters like + * `'/foo/{id}'`. + * + * `expectedQueryParameters` are the names of the query parameters for the routes + * that also double as properties on the intent. This instructs the intent + * builder on which properties to provide to `makeIntent`. For example, + * `SearchResultsPageIntent`'s `RouteProvider` recognizes a `'term'` query param, which + * is then stored in the intent as `{ term: 'valueFromParam' }`. For that case, + * this array should be `['term']`. Put another way, this should be a list of the + * non-optional (non-nullable, etc.) string properties on the intent: + * + * ```typescript + * interface SearchResultsPageIntent extends RouteableIntent { + * $kind: 'SearchResultsPageIntent'; // not this one + * term: string; // this one should be included! + * facet: Opt<String>; // but it doesn't include this; it's optional + * } + * ``` + * + * NOTE: If you receive a type error like `Type 'string' is not assignable + * to type 'never'` for this argument, it means that your array of parameters + * doesn't match the intent definition. In the above example, if we added a + * `originalTerm: string` to the intent, then passing ['term'] would fail, + * because 'originalTerm' is missing. Adding it does mean, however, that you + * expect users to be able to pass it as a query parameter + * (ex. /search?term=foo&originalTerm=bar). If that's unintended, you may want + * to consider making this property Opt<> (and also maybe ?). + * + * Usage Example: For a contrived intent `FooIntent`, + * + * ```typescript + * interface FooIntent extends RouteableIntent { + * $kind: 'FooIntent'; + * bar: string; + * } + * ``` + * + * If we expect users to be able to route to this intent via `/us/foo?bar=baz` + * or `/foo?bar=baz&l=es`, then in the IntentController's file: + * + * ```typescript + * const { + * routes, + * makeCanonicalUrl, + * } = generateRoutes(makeFooIntent, '/foo', ['bar']); + * ``` + * + * And then, `routes` can be used in the `RouteProvider`: + * + * ```typescript + * export const FooIntentController: RouteProvider<FooIntent> & ... = { + * routes, + * + * // ... + * }; + * ``` + * + * And then in perform, when returning a `Page`, append the `canonicalURL`: + * + * ```typescript + * async perform(intent: FooIntent, objectGraph: ObjectGraph): Promise<Page> { + * // ... + * return { + * ...page, + * canonicalURL: makeCanonicalUrl(intent), + * }; + * }, + * ``` + */ +export function generateRoutes(makeIntent, path, requiredQueryParameters = [], extras) { + const ruleExtras = { exclusions: extras === null || extras === void 0 ? void 0 : extras.exclusions }; + const queryParameters = requiredQueryParameters.slice(); + if (extras === null || extras === void 0 ? void 0 : extras.optionalQuery) { + queryParameters.push(...extras.optionalQuery.map((p) => `${p}?`)); + } + return { + routes(objectGraph) { + var _a; + return [ + { + rules: [ + { + ...ruleExtras, + path, + query: [`${Parameters.language}?`, ...queryParameters], + }, + { + ...ruleExtras, + path: `/{${Path.storeFront}}${path}`, + query: [`${Parameters.language}?`, ...queryParameters], + }, + ...((_a = extras === null || extras === void 0 ? void 0 : extras.extraRules) !== null && _a !== void 0 ? _a : []), + ], + handler(_, parameters) { + const { [Path.storeFront]: storefront, [Parameters.language]: language, ...rest } = parameters; + const typedRestParams = rest; + return makeIntent({ + ...typedRestParams, + ...normalizeLocale(objectGraph, { + storefront, + language, + }), + }); + }, + }, + ]; + }, + makeCanonicalUrl(objectGraph, intent) { + const url = buildBaseUrl(objectGraph, path, intent); + // Replace dynamic segments with `Intent` properties + const pathComponents = url.pathComponents(); + for (const [i, pathComponent] of pathComponents.entries()) { + if (pathComponent.startsWith("{") && pathComponent.endsWith("}")) { + const variableInPathComponent = pathComponent.substring(1, pathComponent.length - 1); + pathComponents[i] = intent[variableInPathComponent]; + } + } + url.set("pathname", "/" + pathComponents.join("/")); + // Set query params + const sortedQueryParameters = requiredQueryParameters.slice(); + // Sorted for consistent order + sortedQueryParameters.sort((a, b) => a.localeCompare(b)); + for (const queryParameter of sortedQueryParameters) { + if (!(queryParameter in intent)) { + throw new Error(`expected queryParmeters to contain: ${queryParameter}`); + } + const value = intent[queryParameter]; + // NOTE: TypeScript is unfortunately not able to see that + // parameters[queryParameter] will always be string. We prove + // this in the type. queryParameters is required to contain the + // string names of all keys on I with a string value (see + // RequiredIntentOptions<I>). This escape hatch is preferable + // within this function as it means that the function is + // externally typesafe. If a user forgets to pass in params, + // there will be a type error. + url.param(queryParameter, value); + } + if (extras === null || extras === void 0 ? void 0 : extras.optionalQuery) { + const optionalParams = extras.optionalQuery.slice(); + optionalParams.sort((a, b) => a.localeCompare(b)); + optionalParams.forEach((query) => { + const value = intent[query]; + if (value) { + url.param(query, value); + } + }); + } + return url.toString(); + }, + }; +} +/** + * Builds the basis for a "canonical URL" given a {@linkcode path} and {@linkcode locale} + * + * The locale data will be encoded into the resulting {@linkcode URL}; other "dynamic segments" + * within {@linkcode path} are expected to be replaced separately + */ +function buildBaseUrl(objectGraph, path, locale) { + const url = new URL("https://apps.apple.com"); + const { storefront, language } = deriveLocaleForUrl(objectGraph, locale); + url.path(storefront); + // Actual path (ex. browse in https://apps.apple.com/us/browse) + // substr(1), because URL.path doesn't expect a leading slash + url.path(path.substr(1)); + if (isSome(language)) { + url.param("l", language); + } + return url; +} +//# sourceMappingURL=generate-routes.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/util/lottery.js b/node_modules/@jet-app/app-store/tmp/src/common/util/lottery.js new file mode 100644 index 0000000..5916128 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/util/lottery.js @@ -0,0 +1,62 @@ +import { isNothing } from "@jet/environment"; +import { MetricsIdentifierType } from "../../foundation/metrics/metrics-identifiers-cache"; +export function isFeatureEnabledForCurrentUser(objectGraph, rolloutRate) { + var _a; + if (rolloutRate <= 0.0) { + return false; + } + if (rolloutRate >= 1.0) { + // It is important that we not require a metrics ID when our rollout rate is 100% + return true; + } + const metricsIdString = (_a = objectGraph.metricsIdentifiersCache) === null || _a === void 0 ? void 0 : _a.getMetricsIdForType(MetricsIdentifierType.user); + if (isNothing(metricsIdString) || metricsIdString.length === 0) { + // if the user does not have a DSID, err on the side of caution. + return false; + } + if (metricsIdString.length < 2) { + // If we don't have enough characters to normalize, which should never happen, safely return false. + return false; + } + return normalizedMetricsIdLotteryValue(metricsIdString) < rolloutRate; +} +/** + * Gets a normalized value based on the metrics ID to match against the rollout rate to bucket a user. + * @param metricsId The user's metrics ID string + * @returns A value from 0.0 to 1.0 that we use to bucket the user + */ +function normalizedMetricsIdLotteryValue(metricsId) { + // Character mapping function that converts a character to its corresponding number. + // This value is guaranteed to be between 0 and 61, inclusive. + function charToNumber(char) { + const charCode = char.charCodeAt(0); + if (charCode >= 48 && charCode <= 57) { + // '0'-'9' + return 52 + charCode - 48; + } + else if (charCode >= 65 && charCode <= 90) { + // 'A'-'Z' + return 26 + charCode - 65; + } + else if (charCode >= 97 && charCode <= 122) { + // 'a'-'z' + return charCode - 97; + } + // This should never happen, if it does, the metrics ID is corrupt, but we rely on that not happening. + // This would indeed break the value guarantee. + return charCode; + } + // Extract the last two characters from the string. + const lastTwoChars = metricsId.slice(-2); + // Convert each of the last two characters to their respective number representation. + const num1 = charToNumber(lastTwoChars[0]); + const num2 = charToNumber(lastTwoChars[1]); + // Combine the two numbers into a single base-62 number. + const combinedValue = num1 * 62 + num2; + // Calculate the maximum possible value for two characters (each char has a value of 0 to 61, inclusive). + const maxValue = 61 * 62 + 61; + // Normalize the combined value to be between 0.0 and 1.0. + const normalizedValue = combinedValue / maxValue; + return normalizedValue; +} +//# sourceMappingURL=lottery.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/util/page-common.js b/node_modules/@jet-app/app-store/tmp/src/common/util/page-common.js new file mode 100644 index 0000000..706fa9e --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/util/page-common.js @@ -0,0 +1,25 @@ +/** + * Created by ls on 6/19/2019. + */ +/** + * On different platforms, different shelves are expected to be in certain multiples to not appear like there is a "gap" + * This function is used to truncate an array of models to fit given expected multiple. + * Note that ideally we want to request the exact number of items needed, but this accounts for cases where some data fails to build into full UI elements. + * @param items Array of items to truncate + * @param mod Modulo to apply. + */ +export function truncateItems(items, mod) { + if (!items) { + return null; + } + const length = items.length; + const remainder = length % mod; + // If we have less than mod items, we should return them all instead of 0 (because i - (i % mod)=0 when i < mod) + if (length >= mod) { + return items.slice(0, length - remainder); + } + else { + return items; + } +} +//# sourceMappingURL=page-common.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/web-navigation/flow-action-presentation.js b/node_modules/@jet-app/app-store/tmp/src/common/web-navigation/flow-action-presentation.js new file mode 100644 index 0000000..41b5bb9 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/web-navigation/flow-action-presentation.js @@ -0,0 +1,79 @@ +import { isSystemImage } from "../../api/models"; +import { asString } from "../../foundation/json-parsing/server-data"; +import { createArtworkForSystemImage } from "../content/artwork/artwork"; +import * as metricsHelpersClicks from "../metrics/helpers/clicks"; +import * as metricsHelpersLocation from "../metrics/helpers/location"; +/** + * Creates a {@linkcode PrepareAction} that sets the link's text and icon + */ +function createWebNavigationFlowActionConfiguration(id) { + return (objectGraph, action) => { + const tab = objectGraph.bag.tabsStandard.find((t) => t.id === id); + const imageIdentifier = asString(tab, "image-identifier"); + const artwork = isSystemImage(imageIdentifier) + ? createArtworkForSystemImage(objectGraph, imageIdentifier) + : null; + action.title = asString(tab, "title"); + action.artwork = artwork; + if (objectGraph.client.isWeb) { + metricsHelpersClicks.addClickEventToAction(objectGraph, action, { + targetType: "link", + id: id, + pageInformation: undefined, + locationTracker: metricsHelpersLocation.newLocationTracker(), + }); + } + }; +} +/** + * {@linkcode PrepareAction} that presents a link to the "Apps" landing page + */ +export const appLandingPageFlowActionPresentation = createWebNavigationFlowActionConfiguration("apps"); +/** + * {@linkcode PrepareAction} that presents a link to the "Apps & Games" landing page + */ +export const appAndGamesLandingPageFlowActionPresentation = createWebNavigationFlowActionConfiguration("apps-and-games"); +/** + * {@linkcode PrepareAction} that presents a link to the "Arcade" landing page + */ +export const arcadeLandingPageFlowActionPresentation = createWebNavigationFlowActionConfiguration("arcade"); +/** + * {@linkcode PrepareAction} that presents a link to the "Discover" landing page + */ +export const discoverLandingPageFlowActionPresentation = createWebNavigationFlowActionConfiguration("discover"); +/** + * {@linkcode PrepareAction} that presents a link to the "Games" landing page + */ +export const gamesLandingPageFlowActionPresentation = createWebNavigationFlowActionConfiguration("games"); +/** + * {@linkcode PrepareAction} that presents a link to the "Create" landing page + * + * Note: `create` refers to the specific "Create" landing page, rather than this being + * a function that creates landing page presentations + */ +export const createLandingPageFlowActionPresentation = createWebNavigationFlowActionConfiguration("create"); +/** + * {@linkcode PrepareAction} that presents a link to the "Work" landing page + */ +export const workLandingPageFlowActionPresentation = createWebNavigationFlowActionConfiguration("work"); +/** + * {@linkcode PrepareAction} that presents a link to the "Play" landing page + */ +export const playLandingPageFlowActionPresentation = createWebNavigationFlowActionConfiguration("play"); +/** + * {@linkcode PrepareAction} that presents a link to the "Develop" landing page + */ +export const developLandingPageFlowActionPresentation = createWebNavigationFlowActionConfiguration("develop"); +/** + * {@linkcode PrepareAction} that presents a link to the "Categories" landing page + */ +export const categoriesLandingPageFlowActionPresentation = createWebNavigationFlowActionConfiguration("categories"); +/** + * {@linkcode PrepareAction} that presents a link to the "Today" landing page + */ +export const todayLandingPageFlowActionPresentation = createWebNavigationFlowActionConfiguration("today"); +/** + * {@linkcode PrepareAction} that presents a link to the "Apps & Games" landing page + */ +export const searchLandingPageFlowActionPresentation = createWebNavigationFlowActionConfiguration("search"); +//# sourceMappingURL=flow-action-presentation.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/web-navigation/inject-web-navigation.js b/node_modules/@jet-app/app-store/tmp/src/common/web-navigation/inject-web-navigation.js new file mode 100644 index 0000000..b882c17 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/web-navigation/inject-web-navigation.js @@ -0,0 +1,32 @@ +import { createWebNavigation, setActivePlatform } from "./web-navigation"; +/** + * The "default" {@linkcode PreviewPlatform} that should be used for the navigation state in cases + * where an explicit value is not provided + */ +const FALLBACK_PREVIEW_PLATFORM = "iphone"; +/** + * Inject the `WebNavigation` into a page model + * + * @param objectGraph + * @param page the page to inject the navigation into + * @param platform the `PreviewPlatform` to render navigation links for + * @returns the web navigation shelf, in case further mutation is required + */ +export function injectWebNavigation(objectGraph, page, platform) { + const webNavigation = createWebNavigation(objectGraph, platform !== null && platform !== void 0 ? platform : FALLBACK_PREVIEW_PLATFORM); + if (page.title) { + webNavigation.title = page.title; + } + page.webNavigation = webNavigation; + return webNavigation; +} +/** + * Sets the `WebNavigation` contained by {@linkcode page} to reflect {@linkcode platform} + * as the active platform. Landing pages links will also be made active based on {@linkcode intent} + */ +export function updateWebNavigation(objectGraph, page, platform, intent) { + if (page.webNavigation) { + setActivePlatform(objectGraph, page.webNavigation, platform, intent); + } +} +//# sourceMappingURL=inject-web-navigation.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/web-navigation/landing-page-links-by-platform.js b/node_modules/@jet-app/app-store/tmp/src/common/web-navigation/landing-page-links-by-platform.js new file mode 100644 index 0000000..02aad6c --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/web-navigation/landing-page-links-by-platform.js @@ -0,0 +1,62 @@ +import { isPreviewPlatform } from "../../api/models/preview-platform"; +import { getLocale } from "../locale"; +import { landingPageIntentsAreEquivalent, } from "./platform-landing-page-utils"; +import { iPadTodayPageController, iPadAppsController, iPadArcadeController, iPadGamesController, iPadSearchLandingController, iPhoneTodayPageController, iPhoneAppsController, iPhoneGamesController, iPhoneArcadeController, iPhoneSearchLandingController, MacDiscoverController, MacArcadeController, MacCreateController, MacWorkController, MacPlayController, MacDevelopController, MacCategoriesController, MacSearchLandingController, TVAppsController, TVArcadeController, TVDiscoverController, TVGamesController, TVSearchLandingController, VisionAppsAndGamesController, VisionArcadeController, VisionSearchLandingController, WatchDiscoverController, WatchSearchLandingController, } from "./platform-landing-page-intent-controllers"; +const ARCADE_CONTROLLERS = new Set([ + iPadArcadeController, + iPhoneArcadeController, + MacArcadeController, + TVArcadeController, + VisionArcadeController, +]); +export function createLandingPageLinks(objectGraph, platform, intent) { + if (!platform || !isPreviewPlatform(platform)) { + return []; + } + // NOTE: this map cannot be defined at the module level, for risk of creating + // a circular dependency between module-level declarations that will break the build + // tools used by the "web" client + const landingPageControllers = { + ipad: [ + iPadTodayPageController, + iPadGamesController, + iPadAppsController, + iPadArcadeController, + iPadSearchLandingController, + ], + iphone: [ + iPhoneTodayPageController, + iPhoneGamesController, + iPhoneAppsController, + iPhoneArcadeController, + iPhoneSearchLandingController, + ], + mac: [ + MacDiscoverController, + MacArcadeController, + MacCreateController, + MacWorkController, + MacPlayController, + MacDevelopController, + MacCategoriesController, + MacSearchLandingController, + ], + tv: [TVDiscoverController, TVGamesController, TVAppsController, TVArcadeController, TVSearchLandingController], + vision: [VisionAppsAndGamesController, VisionArcadeController, VisionSearchLandingController], + watch: [WatchDiscoverController, WatchSearchLandingController], + }; + const locale = getLocale(objectGraph); + const isArcadeEnabled = objectGraph.bag.isArcadeEnabled; + return landingPageControllers[platform] + .filter((landingPageController) => { + // Rejects Arcade controllers if not supported and let's all other controllers pass + const isArcadeController = ARCADE_CONTROLLERS.has(landingPageController); + return isArcadeEnabled || !isArcadeController; + }) + .map((controller) => controller.buildAction(locale, objectGraph)) + .map((action) => ({ + action, + isActive: landingPageIntentsAreEquivalent(intent, action.destination), + })); +} +//# sourceMappingURL=landing-page-links-by-platform.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/web-navigation/platform-landing-page-intent-controllers.js b/node_modules/@jet-app/app-store/tmp/src/common/web-navigation/platform-landing-page-intent-controllers.js new file mode 100644 index 0000000..d19d46b --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/web-navigation/platform-landing-page-intent-controllers.js @@ -0,0 +1,155 @@ +// This module, despite exporting `IntentController` definitions, lives inside of +// `src/common` because these controllers need to be import-able by other "common" +// code in order to directly call their `buildIntent` method +// +// The "web" client expects all exports from this module to be `IntentController` instances +import { defineLandingPageController, definePlatformRootRedirectController, defineRootRedirectController, } from "./platform-landing-page-utils"; +import { makeEditorialPageIntentByName } from "../../api/intents/editorial/editorial-page-intent"; +import { makeGroupingPageIntentByName } from "../../api/intents/grouping-page-intent"; +import { makeRoutableTodayPageIntent } from "../../api/intents/routable-today-page-intent"; +import { makeArcadeGroupingPageIntent } from "../../api/intents/arcade-grouping-page-intent"; +import { appLandingPageFlowActionPresentation, appAndGamesLandingPageFlowActionPresentation, arcadeLandingPageFlowActionPresentation, discoverLandingPageFlowActionPresentation, gamesLandingPageFlowActionPresentation, createLandingPageFlowActionPresentation, workLandingPageFlowActionPresentation, playLandingPageFlowActionPresentation, todayLandingPageFlowActionPresentation, developLandingPageFlowActionPresentation, categoriesLandingPageFlowActionPresentation, searchLandingPageFlowActionPresentation, } from "./flow-action-presentation"; +import { makeSearchLandingPageIntent } from "../../api/intents/search/search-landing-page-intent"; +// Ensures that the root URL (e.g. apps.apple.com) redirects to the iPhone Today page +export const RootRedirectController = defineRootRedirectController((locale) => iPhoneTodayPageController.buildIntent(locale)); +/// MARK: Vision Pro Landing Pages +export const VisionRootRedirectController = definePlatformRootRedirectController("/vision", (locale) => VisionAppsAndGamesController.buildIntent(locale)); +export const VisionAppsAndGamesController = defineLandingPageController("/vision/apps-and-games", (locale) => makeEditorialPageIntentByName({ + ...locale, + platform: "vision", + name: "apps-and-games", +}), appAndGamesLandingPageFlowActionPresentation); +export const VisionArcadeController = defineLandingPageController("/vision/arcade", (locale) => makeEditorialPageIntentByName({ + ...locale, + platform: "vision", + name: "arcade-subscriber", +}), arcadeLandingPageFlowActionPresentation); +export const VisionSearchLandingController = defineLandingPageController("/vision/search", (locale) => makeSearchLandingPageIntent({ + ...locale, + platform: "vision", +}), searchLandingPageFlowActionPresentation); +/// MARK: Mac Landing Pages +export const MacRootRedirectController = definePlatformRootRedirectController("/mac", (locale) => MacDiscoverController.buildIntent(locale)); +export const MacDiscoverController = defineLandingPageController("/mac/discover", (locale) => makeGroupingPageIntentByName({ + ...locale, + platform: "mac", + name: "apps", +}), discoverLandingPageFlowActionPresentation); +export const MacArcadeController = defineLandingPageController("/mac/arcade", (locale) => makeArcadeGroupingPageIntent({ + ...locale, + platform: "mac", +}), arcadeLandingPageFlowActionPresentation); +export const MacCreateController = defineLandingPageController("/mac/create", (locale) => makeGroupingPageIntentByName({ + ...locale, + platform: "mac", + name: "create", +}), createLandingPageFlowActionPresentation); +export const MacWorkController = defineLandingPageController("/mac/work", (locale) => makeGroupingPageIntentByName({ + ...locale, + platform: "mac", + name: "work", +}), workLandingPageFlowActionPresentation); +export const MacPlayController = defineLandingPageController("/mac/play", (locale) => makeGroupingPageIntentByName({ + ...locale, + platform: "mac", + name: "play", +}), playLandingPageFlowActionPresentation); +export const MacDevelopController = defineLandingPageController("/mac/develop", (locale) => makeGroupingPageIntentByName({ + ...locale, + platform: "mac", + name: "develop", +}), developLandingPageFlowActionPresentation); +export const MacCategoriesController = defineLandingPageController("/mac/categories", (locale) => makeGroupingPageIntentByName({ + ...locale, + platform: "mac", + name: "categories", +}), categoriesLandingPageFlowActionPresentation); +export const MacSearchLandingController = defineLandingPageController("/mac/search", (locale) => makeSearchLandingPageIntent({ + ...locale, + platform: "mac", +}), searchLandingPageFlowActionPresentation); +/// MARK: iPhone Landing Pages +export const iPhoneRootRedirectController = definePlatformRootRedirectController("/iphone", (locale) => iPhoneTodayPageController.buildIntent(locale)); +export const iPhoneTodayPageController = defineLandingPageController("/iphone/today", (locale) => makeRoutableTodayPageIntent({ + ...locale, + platform: "iphone", +}), todayLandingPageFlowActionPresentation); +export const iPhoneAppsController = defineLandingPageController("/iphone/apps", (locale) => makeGroupingPageIntentByName({ + ...locale, + platform: "iphone", + name: "apps", +}), appLandingPageFlowActionPresentation); +export const iPhoneGamesController = defineLandingPageController("/iphone/games", (locale) => makeGroupingPageIntentByName({ + ...locale, + platform: "iphone", + name: "games", +}), gamesLandingPageFlowActionPresentation); +export const iPhoneArcadeController = defineLandingPageController("/iphone/arcade", (locale) => makeArcadeGroupingPageIntent({ + ...locale, + platform: "iphone", +}), arcadeLandingPageFlowActionPresentation); +export const iPhoneSearchLandingController = defineLandingPageController("/iphone/search", (locale) => makeSearchLandingPageIntent({ + ...locale, + platform: "iphone", +}), searchLandingPageFlowActionPresentation); +/// MARK: iPad Landing Pages +export const iPadRootRedirectController = definePlatformRootRedirectController("/ipad", (locale) => iPadTodayPageController.buildIntent(locale)); +export const iPadTodayPageController = defineLandingPageController("/ipad/today", (locale) => makeRoutableTodayPageIntent({ + ...locale, + platform: "ipad", +}), todayLandingPageFlowActionPresentation); +export const iPadGamesController = defineLandingPageController("/ipad/games", (locale) => makeGroupingPageIntentByName({ + ...locale, + platform: "ipad", + name: "games", +}), gamesLandingPageFlowActionPresentation); +export const iPadAppsController = defineLandingPageController("/ipad/apps", (locale) => makeGroupingPageIntentByName({ + ...locale, + platform: "ipad", + name: "apps", +}), appLandingPageFlowActionPresentation); +export const iPadArcadeController = defineLandingPageController("/ipad/arcade", (locale) => makeArcadeGroupingPageIntent({ + ...locale, + platform: "ipad", +}), arcadeLandingPageFlowActionPresentation); +export const iPadSearchLandingController = defineLandingPageController("/ipad/search", (locale) => makeSearchLandingPageIntent({ + ...locale, + platform: "ipad", +}), searchLandingPageFlowActionPresentation); +/// MARK: Watch Landing Pages +export const WatchRootRedirectController = definePlatformRootRedirectController("/watch", (locale) => WatchDiscoverController.buildIntent(locale)); +export const WatchDiscoverController = defineLandingPageController("/watch/apps-and-games", (locale) => makeGroupingPageIntentByName({ + ...locale, + platform: "watch", + name: "apps", +}), appAndGamesLandingPageFlowActionPresentation); +export const WatchSearchLandingController = defineLandingPageController("/watch/search", (locale) => makeSearchLandingPageIntent({ + ...locale, + platform: "watch", +}), searchLandingPageFlowActionPresentation); +/// MARK: TV Landing Pages +export const TVRootRedirectController = definePlatformRootRedirectController("/tv", (locale) => TVDiscoverController.buildIntent(locale)); +export const TVDiscoverController = defineLandingPageController("/tv/discover", (locale) => makeGroupingPageIntentByName({ + ...locale, + platform: "tv", + name: "discover", +}), discoverLandingPageFlowActionPresentation); +export const TVGamesController = defineLandingPageController("/tv/games", (locale) => makeGroupingPageIntentByName({ + ...locale, + platform: "tv", + name: "games", +}), gamesLandingPageFlowActionPresentation); +export const TVAppsController = defineLandingPageController("/tv/apps", (locale) => makeGroupingPageIntentByName({ + ...locale, + platform: "tv", + name: "apps", +}), appLandingPageFlowActionPresentation); +export const TVArcadeController = defineLandingPageController("/tv/arcade", (locale) => makeArcadeGroupingPageIntent({ + ...locale, + platform: "tv", +}), arcadeLandingPageFlowActionPresentation); +export const TVSearchLandingController = defineLandingPageController("/tv/search", (locale) => makeSearchLandingPageIntent({ + ...locale, + platform: "tv", +}), searchLandingPageFlowActionPresentation); +//# sourceMappingURL=platform-landing-page-intent-controllers.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/web-navigation/platform-landing-page-utils.js b/node_modules/@jet-app/app-store/tmp/src/common/web-navigation/platform-landing-page-utils.js new file mode 100644 index 0000000..c408425 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/web-navigation/platform-landing-page-utils.js @@ -0,0 +1,139 @@ +import { isNothing, unwrapOptional as unwrap } from "@jet/environment/types/optional"; +import { FlowAction } from "../../api/models"; +import { generateRoutes } from "../util/generate-routes"; +import { updateWebNavigation } from "./inject-web-navigation"; +/** + * Determine if the two {@linkcode LandingPageIntent} arguments are equivalent + */ +export function landingPageIntentsAreEquivalent(activeIntent, navLinkIntent) { + if (isNothing(activeIntent) || isNothing(navLinkIntent)) { + return false; + } + for (const key of ["$kind", "storefront", "language"]) { + if (activeIntent[key] !== navLinkIntent[key]) { + return false; + } + } + return true; +} +/** + * Dynamically create an {@linkcode IntentController} for a platform "landing page" + * + * A "landing page" is a vanity URL within a specific platform that loads some other + * page under-the-hood; the "canonical URL" for the "landing page" should reflect this + * vanity URL, rather than the real canonical URL for the underlying Media API entity. + * + * @param url the vanity URL for the landing page + * @param intentFactory a function that produces the underlying `Intent` for the data backing the landing page + * @param prepareAction a function that can be used to provide additional configuration of the `FlowAction` for a landing page + */ +export function defineLandingPageController(url, intentFactory, prepareAction) { + const intentKind = `LandingPage_${url.substring(1)}_Intent`; + const { routes, makeCanonicalUrl } = generateRoutes((locale) => ({ + ...locale, + $kind: intentKind, + }), url, [], { + exclusions: [ + { + query: ["term"], + }, + ], + }); + return { + $intentKind: intentKind, + routes, + async perform(landingPageIntent, objectGraph) { + var _a; + const pageIntent = intentFactory({ + storefront: landingPageIntent.storefront, + language: landingPageIntent.language, + }); + const viewModel = await objectGraph.dispatcher.dispatch(pageIntent, objectGraph); + if (viewModel) { + // Override the default canonical URL to use the vanity URL instead; this ensures + // that the navigation mechanics of the "web" client respect the vanity URL, which the + // Media API would not know about when providing it's own value for this property + // @ts-expect-error TypeScript can't know that `landingPageIntent` satisfies the requirements of + // `makeCanonicalUrl`, but this will be safe at runtime because all we actually require + // from the intent is the locale information + viewModel.canonicalURL = makeCanonicalUrl(objectGraph, landingPageIntent); + (_a = objectGraph.seo) === null || _a === void 0 ? void 0 : _a.updateCanonicalURL(viewModel, viewModel.canonicalURL); + // If the web navigation has already been injected, the active state of the "tabs" will be incorrect + // since non-"landing pages" never have display active landing page links. We need to update the + // navigation shelf, if it exists, based on the `landingPageIntent` + updateWebNavigation(objectGraph, viewModel, unwrap(pageIntent.platform), landingPageIntent); + } + return viewModel; + }, + buildIntent(locale) { + return { + ...locale, + $kind: intentKind, + }; + }, + buildAction(locale, objectGraph) { + const intent = this.buildIntent(locale); + return this.actionFor(intent, objectGraph, {}); + }, + actionFor(landingPageIntent, objectGraph) { + const action = new FlowAction("page"); + action.destination = landingPageIntent; + // @ts-expect-error TypeScript can't know that `landingPageIntent` satisfies the requirements of + // `makeCanonicalUrl`, but this will be safe at runtime because all we actually require + // from the intent is the locale information + action.pageUrl = makeCanonicalUrl(objectGraph, landingPageIntent); + prepareAction === null || prepareAction === void 0 ? void 0 : prepareAction(objectGraph, action); + return action; + }, + }; +} +/** + * Dynamically create an {@linkcode IntentController} that redirects from the platform root + * to the default landing page + * + * @param url the URL for the platform to redirect for + * @param intentFactory a function that produces the `Intent` for the `LandingPageController` that should be delegated to + */ +export function definePlatformRootRedirectController(url, intentFactory) { + const intentKind = `PlatformRootRedirect_${url.substring(1)}_Intent`; + const { routes } = generateRoutes((locale) => ({ + ...locale, + $kind: intentKind, + }), url); + return { + $intentKind: intentKind, + routes, + async perform(landingPageIntent, objectGraph) { + const pageIntent = intentFactory({ + storefront: landingPageIntent.storefront, + language: landingPageIntent.language, + }); + return await objectGraph.dispatcher.dispatch(pageIntent, objectGraph); + }, + }; +} +/** + * Dynamically create an {@linkcode IntentController} that redirects from the root + * to the default landing page + * + * @param intentFactory a function that produces the `Intent` for the `LandingPageController` that should be delegated to + */ +export function defineRootRedirectController(intentFactory) { + const intentKind = `RootRedirect_Intent`; + const { routes } = generateRoutes((locale) => ({ + ...locale, + $kind: intentKind, + }), "/"); + return { + $intentKind: intentKind, + routes, + async perform(landingPageIntent, objectGraph) { + const pageIntent = intentFactory({ + storefront: landingPageIntent.storefront, + language: landingPageIntent.language, + }); + return await objectGraph.dispatcher.dispatch(pageIntent, objectGraph); + }, + }; +} +//# sourceMappingURL=platform-landing-page-utils.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/web-navigation/platform-selection.js b/node_modules/@jet-app/app-store/tmp/src/common/web-navigation/platform-selection.js new file mode 100644 index 0000000..b583859 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/web-navigation/platform-selection.js @@ -0,0 +1,115 @@ +import { allPreviewPlatforms } from "../../api/models/preview-platform"; +import { iPadTodayPageController, WatchDiscoverController, VisionAppsAndGamesController, MacDiscoverController, iPhoneTodayPageController, TVDiscoverController, } from "./platform-landing-page-intent-controllers"; +import { unreachable } from "../../foundation/util/errors"; +import { getLocale } from "../locale"; +import { createArtworkForSystemImage } from "../content/artwork/artwork"; +import * as metricsHelpersClicks from "../metrics/helpers/clicks"; +import { newLocationTracker } from "../metrics/helpers/location"; +function makeiPhonePlatformSelector(objectGraph, isActive) { + const action = iPhoneTodayPageController.buildAction(getLocale(objectGraph), objectGraph); + action.title = objectGraph.loc.string("Web.Navigation.Platform.Phone"); + action.artwork = createArtworkForSystemImage(objectGraph, "iphone.gen2"); + return { + action, + isActive, + }; +} +function makeiPadPlatformSelector(objectGraph, isActive) { + const action = iPadTodayPageController.buildAction(getLocale(objectGraph), objectGraph); + action.title = objectGraph.loc.string("Web.Navigation.Platform.Pad"); + action.artwork = createArtworkForSystemImage(objectGraph, "ipad.gen2.landscape"); + return { + action, + isActive, + }; +} +function makeMacPlatformSelector(objectGraph, isActive) { + const action = MacDiscoverController.buildAction(getLocale(objectGraph), objectGraph); + action.title = objectGraph.loc.string("Web.Navigation.Platform.Mac"); + action.artwork = createArtworkForSystemImage(objectGraph, "macbook.gen2"); + return { + action, + isActive, + }; +} +function makeVisionPlatformSelector(objectGraph, isActive) { + const action = VisionAppsAndGamesController.buildAction(getLocale(objectGraph), objectGraph); + action.title = objectGraph.loc.string("Web.Navigation.Platform.Vision"); + action.artwork = createArtworkForSystemImage(objectGraph, "visionpro"); + return { + action, + isActive, + }; +} +function makeWatchPlatformSelector(objectGraph, isActive) { + const action = WatchDiscoverController.buildAction(getLocale(objectGraph), objectGraph); + action.title = objectGraph.loc.string("Web.Navigation.Platform.Watch"); + action.artwork = createArtworkForSystemImage(objectGraph, "applewatch"); + return { + action, + isActive, + }; +} +function makeTVPlatformSelector(objectGraph, isActive) { + const action = TVDiscoverController.buildAction(getLocale(objectGraph), objectGraph); + action.title = objectGraph.loc.string("Web.Navigation.Platform.TV"); + action.artwork = createArtworkForSystemImage(objectGraph, "tv"); + return { + action, + isActive, + }; +} +function makePlatformSelector(objectGraph, platform, activePlatform) { + let selector; + switch (platform) { + case "iphone": { + selector = makeiPhonePlatformSelector(objectGraph, activePlatform === "iphone"); + break; + } + case "ipad": { + selector = makeiPadPlatformSelector(objectGraph, activePlatform === "ipad"); + break; + } + case "mac": { + selector = makeMacPlatformSelector(objectGraph, activePlatform === "mac"); + break; + } + case "vision": { + selector = makeVisionPlatformSelector(objectGraph, activePlatform === "vision"); + break; + } + case "watch": { + selector = makeWatchPlatformSelector(objectGraph, activePlatform === "watch"); + break; + } + case "tv": { + selector = makeTVPlatformSelector(objectGraph, activePlatform === "tv"); + break; + } + default: + unreachable(platform); + } + if (objectGraph.client.isWeb) { + metricsHelpersClicks.addClickEventToAction(objectGraph, selector.action, { + id: platform, + actionType: "navigate", + locationTracker: newLocationTracker(), + pageInformation: undefined, + }, false, "link"); + } + return selector; +} +/** + * + * @param objectGraph + * @param activePlatform the active platform; navigation links to it will be presented with an "active" style + * @returns + */ +export function createPlatformSelectors(objectGraph, activePlatform) { + const isVisionSupported = objectGraph.bag.enableVisionPlatform; + return (allPreviewPlatforms + // Rejects vision if its not supported and let's all other platforms pass + .filter((platform) => isVisionSupported || platform !== "vision") + .map((platform) => makePlatformSelector(objectGraph, platform, activePlatform))); +} +//# sourceMappingURL=platform-selection.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/web-navigation/search-results-platform-selection.js b/node_modules/@jet-app/app-store/tmp/src/common/web-navigation/search-results-platform-selection.js new file mode 100644 index 0000000..00993f8 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/web-navigation/search-results-platform-selection.js @@ -0,0 +1,28 @@ +import { allPreviewPlatforms } from "../../api/models/preview-platform"; +import { makeCanonicalSearchResultsPageUrl } from "../search/search-page-url"; +function replaceDestinationIntent(objectGraph, action, intent, platform) { + const updatedIntent = { + ...intent, + platform, + }; + action.destination = updatedIntent; + action.pageUrl = makeCanonicalSearchResultsPageUrl(objectGraph, updatedIntent); +} +/** + * Replaces each of the "platform selector" actions with one that navigates to the + * {@linkcode searchResultsPageIntent} in each platform + * + * This is necessary because the existing platform selector, which normally takes + * the user to a platform landing page, instead takes the user to the search results + * page within the selected platform + */ +export function replacePlatformSelectorDestinationWithSearchResults(objectGraph, webNavigation, searchResultsPageIntent) { + const isVisionSupported = objectGraph.bag.enableVisionPlatform; + allPreviewPlatforms + // Rejects vision if its not supported and let's all other platforms pass + .filter((platform) => isVisionSupported || platform !== "vision") + .forEach((platform, index) => { + replaceDestinationIntent(objectGraph, webNavigation.platforms[index].action, searchResultsPageIntent, platform); + }); +} +//# sourceMappingURL=search-results-platform-selection.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/common/web-navigation/web-navigation.js b/node_modules/@jet-app/app-store/tmp/src/common/web-navigation/web-navigation.js new file mode 100644 index 0000000..13b7f6d --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/web-navigation/web-navigation.js @@ -0,0 +1,28 @@ +import { createPlatformSelectors } from "./platform-selection"; +import { createLandingPageLinks } from "./landing-page-links-by-platform"; +import { makeWebSearchAction } from "../search/web-search-action"; +/** + * Create a {@linkcode WebNavigation} based on the active {@linkcode Intent} + */ +export function createWebNavigation(objectGraph, platform) { + const webNavigation = { + platforms: [], + tabs: [], + searchAction: makeWebSearchAction(objectGraph, platform), + }; + setActivePlatform(objectGraph, webNavigation, platform); + return webNavigation; +} +/** + * Updates a {@linkcode WebNavigation} to reflect: + * + * 1. the platform specified by {@linkcode platform} + * 2. the "active landing page" specified by {@linkcode intent} + */ +export function setActivePlatform(objectGraph, shelf, platform, intent) { + shelf.platforms = createPlatformSelectors(objectGraph, platform); + const searchActionDestination = shelf.searchAction.destination; + shelf.tabs = createLandingPageLinks(objectGraph, platform, intent); + searchActionDestination.platform = platform; +} +//# sourceMappingURL=web-navigation.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/controllers/app-events/app-event-page-intent-controller.js b/node_modules/@jet-app/app-store/tmp/src/controllers/app-events/app-event-page-intent-controller.js new file mode 100644 index 0000000..1cda6ab --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/controllers/app-events/app-event-page-intent-controller.js @@ -0,0 +1,42 @@ +import { isNothing } from "@jet/environment/types/optional"; +import { appEventPageRoutes } from "../../common/product-page/intent-controller-routing"; +import { injectSEOData } from "../../api/models/web-renderable-page"; +import { makeAppEventPageRequest, makeShelfBasedAppEventDetailPage } from "../../common/app-events/app-events-common"; +import { withActiveIntent } from "../../foundation/dependencies/active-intent"; +import { fetchData, NetworkError } from "../../foundation/media/network"; +import { URL } from "../../foundation/network/urls"; +import { injectWebNavigation } from "../../common/web-navigation/inject-web-navigation"; +import { validateAdamId } from "../../foundation/media/util"; +export const AppEventPageIntentController = { + $intentKind: "AppEventPageIntent", + routes: appEventPageRoutes, + async perform(intent, objectGraphWithoutActiveIntent) { + return await withActiveIntent(objectGraphWithoutActiveIntent, intent, async (objectGraph) => { + var _a; + // See: https://github.pie.apple.com/its/Jingle/blob/ce14e21b6ac3dd4be884aa12b26d4e79c6d8aa7a/MZStorePlatform/src/main/java/com/apple/jingle/store/mediaapi/resource/SFMediaAPICommonResourceType.java#L50 + validateAdamId(objectGraph, intent.id); + const mediaApiRequest = makeAppEventPageRequest(objectGraph, intent); + const response = await fetchData(objectGraph, mediaApiRequest); + const page = makeShelfBasedAppEventDetailPage(objectGraph, response); + if (isNothing(page)) { + const notFoundError = new NetworkError("Media API response lacked required data"); + notFoundError.statusCode = 404; + throw notFoundError; + } + // The page URL (`.canonicalURL` property on the view-model) from Media API does not account for the `platform` + // query param; we need to add that in ourselves. We can't rely on `makeCanonicalUrl` as that does not handle the + // `appName` segment; we need to take the Media API URL and append the `platform` ourselves + if (intent.platform && page.canonicalURL) { + const pageURL = new URL(page.canonicalURL); + pageURL.param("platform", intent.platform); + page.canonicalURL = pageURL.toString(); + } + if (objectGraph.client.isWeb) { + injectWebNavigation(objectGraph, page, intent.platform); + injectSEOData(page, (_a = objectGraph.seo) === null || _a === void 0 ? void 0 : _a.getSEODataForAppEventPage(objectGraph, page, response)); + } + return page; + }); + }, +}; +//# sourceMappingURL=app-event-page-intent-controller.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/controllers/arcade/arcade-grouping-page-intent-controller.js b/node_modules/@jet-app/app-store/tmp/src/controllers/arcade/arcade-grouping-page-intent-controller.js new file mode 100644 index 0000000..ba1e42a --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/controllers/arcade/arcade-grouping-page-intent-controller.js @@ -0,0 +1,71 @@ +import { isNothing } from "@jet/environment/types/optional"; +import { fetchData } from "../../foundation/media/network"; +import { makeBaseGroupingPageRequest, prepareGroupingPageRequest } from "../../common/grouping/grouping-request"; +import { flattenedGroupingFromDataContainer, groupingPageFromFlattenedGrouping, groupingParseContextFromDataContainer, } from "../../common/grouping/render-grouping-page"; +import { arcadeAppsRequestForIcons } from "../../common/arcade/arcade-common"; +import { GroupingArcadeFooterShelfController } from "../../common/grouping/shelf-controllers/grouping-arcade-footer-shelf-controller"; +import { injectWebNavigation } from "../../common/web-navigation/inject-web-navigation"; +import { injectSEOData } from "../../api/models/web-renderable-page"; +import { withActiveIntent } from "../../foundation/dependencies/active-intent"; +import { setPreviewPlatform } from "../../common/preview-platform"; +function makeMediaApiArcadeGroupingPageRequest(objectGraph, intent) { + const mediaApiRequest = makeBaseGroupingPageRequest(objectGraph) + .addingQuery("name", "arcade") + .addingQuery("tabs", "subscriber"); + prepareGroupingPageRequest(objectGraph, mediaApiRequest); + setPreviewPlatform(objectGraph, mediaApiRequest); + return mediaApiRequest; +} +function makeArcadeSeeAllShelf(objectGraph, groupingParseContext, response) { + const token = { + id: "arcade-see-all-games-footer", + presentationHints: {}, + featuredContentId: -1 /* FeaturedContentID.Native_GroupingShelf */, + featuredContentData: null, + nativeGroupingShelfId: 1 /* NativeGroupingShelfID.Arcade_SeeAllGamesFooter */, + metricsPageInformation: groupingParseContext.metricsPageInformation, + metricsLocationTracker: groupingParseContext.metricsLocationTracker, + pageGenreId: groupingParseContext.pageGenreId, + title: null, + shouldFilter: false, + remainingItems: [], + isFirstRender: true, + isDeferring: false, + showOrdinals: false, + hasExistingContent: false, + showingPlaceholders: false, + ordinalIndex: 1, + isSearchLandingPage: groupingParseContext.isSearchLandingPage, + isArcadePage: groupingParseContext === null || groupingParseContext === void 0 ? void 0 : groupingParseContext.isArcadePage, + }; + const groupingArcadeFooterShelfController = new GroupingArcadeFooterShelfController(); + const arcadeFooterShelf = groupingArcadeFooterShelfController.createShelf(objectGraph, response, groupingParseContext, token, null); + return arcadeFooterShelf; +} +export const ArcadeGroupingPageIntentController = { + $intentKind: "ArcadeGroupingPageIntent", + async perform(intent, objectGraphWithoutActiveIntent) { + return await withActiveIntent(objectGraphWithoutActiveIntent, intent, async (objectGraph) => { + var _a; + const groupingPageRequest = makeMediaApiArcadeGroupingPageRequest(objectGraph, intent); + const arcadeFooterRequest = arcadeAppsRequestForIcons(objectGraph, 20); + const [groupingPageResponse, arcadeFooterResponse] = await Promise.all([ + fetchData(objectGraph, groupingPageRequest), + fetchData(objectGraph, arcadeFooterRequest), + ]); + const flattenedGrouping = flattenedGroupingFromDataContainer(objectGraph, groupingPageResponse); + const groupingParseContext = groupingParseContextFromDataContainer(objectGraph, groupingPageResponse); + if (isNothing(flattenedGrouping) || isNothing(groupingParseContext)) { + return null; + } + const page = groupingPageFromFlattenedGrouping(objectGraph, flattenedGrouping, groupingParseContext); + page.shelves.push(makeArcadeSeeAllShelf(objectGraph, groupingParseContext, arcadeFooterResponse)); + if (objectGraph.client.isWeb) { + injectWebNavigation(objectGraph, page, intent.platform); + injectSEOData(page, (_a = objectGraph.seo) === null || _a === void 0 ? void 0 : _a.getSEODataForGroupingPage(objectGraph, page, groupingPageResponse)); + } + return page; + }); + }, +}; +//# sourceMappingURL=arcade-grouping-page-intent-controller.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/controllers/arcade/routable-arcade-see-all-page-controller.js b/node_modules/@jet-app/app-store/tmp/src/controllers/arcade/routable-arcade-see-all-page-controller.js new file mode 100644 index 0000000..9751c7b --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/controllers/arcade/routable-arcade-see-all-page-controller.js @@ -0,0 +1,42 @@ +import { ArcadeSeeAllGamesPage } from "../../api/models"; +import { defaultAdditionalPlatformsForClient } from "../../foundation/media/data-fetching"; +import { fetchData } from "../../foundation/media/network"; +import { withActiveIntent } from "../../foundation/dependencies/active-intent"; +import { arcadeAppsRequest } from "../../common/arcade/arcade-common"; +import { defaultRequestAttributes, prepareRequestWithSelectedFacets } from "../../common/arcade/arcade-see-all-request"; +import { createArcadeSeeAllGamesPaginationToken, createShelves, } from "../../common/arcade/render-arcade-see-all-games-page"; +import { shouldFetchCustomAttributes } from "../../common/product-page/product-page-variants"; +import { metricsPageInformationFromMediaApiResponse } from "../../common/metrics/helpers/page"; +import { asArrayOrEmpty } from "../../foundation/json-parsing/server-data"; +import { injectWebNavigation } from "../../common/web-navigation/inject-web-navigation"; +import { createDefaultSelectedFacetOptions } from "../../common/arcade/arcade-see-all-games-facets"; +import { arcadeSeeAllRoutes as routes, makeArcadeSeeAllCanonicalUrl } from "../../common/arcade/arcade-see-all-routing"; +import { injectSEOData } from "../../api/models/web-renderable-page"; +export const RoutableArcadeSeeAllPageController = { + $intentKind: "RoutableArcadeSeeAllPageIntent", + routes, + async perform(intent, objectGraphWithoutActiveIntent) { + return await withActiveIntent(objectGraphWithoutActiveIntent, intent, async (objectGraph) => { + var _a; + const mediaApiRequest = arcadeAppsRequest(objectGraph) + .includingAdditionalPlatforms(defaultAdditionalPlatformsForClient(objectGraph)) + .includingAttributes(defaultRequestAttributes(objectGraph)) + .usingCustomAttributes(shouldFetchCustomAttributes(objectGraph)); + if (objectGraph.client.isWeb) { + mediaApiRequest.withSparseLimit(20); + } + prepareRequestWithSelectedFacets(mediaApiRequest, createDefaultSelectedFacetOptions(objectGraph)); + const mediaApiResponse = await fetchData(objectGraph, mediaApiRequest); + const pageInformation = metricsPageInformationFromMediaApiResponse(objectGraph, "Room", "arcadeSeeAllGames", mediaApiResponse); + const paginationToken = createArcadeSeeAllGamesPaginationToken(objectGraph, { isCompactMode: false }, pageInformation); + const page = new ArcadeSeeAllGamesPage(createShelves(objectGraph, asArrayOrEmpty(mediaApiResponse, "results.groups"), paginationToken)); + if (objectGraph.client.isWeb) { + page.canonicalURL = makeArcadeSeeAllCanonicalUrl(objectGraph, intent); + injectWebNavigation(objectGraph, page, intent.platform); + injectSEOData(page, (_a = objectGraph.seo) === null || _a === void 0 ? void 0 : _a.getSEODataForArcadeSeeAllPage(objectGraph, page, mediaApiResponse)); + } + return page; + }); + }, +}; +//# sourceMappingURL=routable-arcade-see-all-page-controller.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/controllers/developer/developer-page-intent-controller.js b/node_modules/@jet-app/app-store/tmp/src/controllers/developer/developer-page-intent-controller.js new file mode 100644 index 0000000..458db1d --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/controllers/developer/developer-page-intent-controller.js @@ -0,0 +1,28 @@ +import { Path } from "../../foundation/network/url-constants"; +import { makeDeveloperPageIntent } from "../../api/intents/developer-page-intent"; +import { generateRoutes } from "../../common/util/generate-routes"; +import { makeDeveloperRequest } from "../../common/developer/developer-request"; +import { fetchData } from "../../foundation/media/network"; +import { developerPageFromResponse } from "../../common/developer/developer-common"; +import { injectWebNavigation } from "../../common/web-navigation/inject-web-navigation"; +import { injectSEOData } from "../../api/models/web-renderable-page"; +import { validateAdamId } from "../../foundation/media/util"; +const { routes } = generateRoutes(makeDeveloperPageIntent, `/${Path.developer}/{name}/{id}`); +export const DeveloperPageIntentController = { + $intentKind: "DeveloperPageIntent", + routes, + async perform(intent, objectGraph) { + var _a; + // See: https://github.pie.apple.com/its/Jingle/blob/ce14e21b6ac3dd4be884aa12b26d4e79c6d8aa7a/MZStorePlatform/src/main/java/com/apple/jingle/store/mediaapi/resource/SFMediaAPICommonResourceType.java#L52 + validateAdamId(objectGraph, intent.id); + const mediaApiRequest = makeDeveloperRequest(objectGraph, intent.id); + const response = await fetchData(objectGraph, mediaApiRequest); + const page = developerPageFromResponse(objectGraph, response); + if (objectGraph.client.isWeb) { + injectWebNavigation(objectGraph, page); + injectSEOData(page, (_a = objectGraph.seo) === null || _a === void 0 ? void 0 : _a.getSEODataForDeveloperPage(objectGraph, page, response)); + } + return page; + }, +}; +//# sourceMappingURL=developer-page-intent-controller.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/controllers/editorial-pages/editorial-page-intent-controller.js b/node_modules/@jet-app/app-store/tmp/src/controllers/editorial-pages/editorial-page-intent-controller.js new file mode 100644 index 0000000..8c19eb7 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/controllers/editorial-pages/editorial-page-intent-controller.js @@ -0,0 +1,55 @@ +import { isNothing } from "@jet/environment"; +import { renderPage as renderEditorialPage } from "../../common/editorial-pages/editorial-page-controller-util"; +import { makeEditorialPageRequest } from "../../common/editorial-pages/editorial-page-media-api-utils"; +import * as mediaNetwork from "../../foundation/media/network"; +import { FlowAction } from "../../api/models"; +import { withActiveIntent } from "../../foundation/dependencies/active-intent"; +import { isEditorialPageIntentByID } from "../../api/intents/editorial/editorial-page-intent"; +import { editorialPageRoutes, makeEditorialPageURL, } from "../../common/editorial-pages/editorial-page-intent-controller-utils"; +import { injectWebNavigation } from "../../common/web-navigation/inject-web-navigation"; +import { injectSEOData } from "../../api/models/web-renderable-page"; +import { validateAdamId, validateNeedsVisionRestriction } from "../../foundation/media/util"; +export const EditorialPageIntentController = { + $intentKind: "EditorialPageIntent", + routes(objectGraph) { + return editorialPageRoutes(objectGraph); + }, + async perform(intent, objectGraphWithoutActiveIntent) { + return await withActiveIntent(objectGraphWithoutActiveIntent, intent, async (objectGraph) => { + var _a; + if ("id" in intent) { + // See: https://github.pie.apple.com/its/Jingle/blob/ce14e21b6ac3dd4be884aa12b26d4e79c6d8aa7a/MZStorePlatform/src/main/java/com/apple/jingle/store/mediaapi/resource/SFMediaAPICommonResourceType.java#L91 + validateAdamId(objectGraph, intent.id); + } + validateNeedsVisionRestriction(objectGraph); + const mediaApiRequest = makeEditorialPageRequest(objectGraph, intent); + const response = await mediaNetwork.fetchData(objectGraph, mediaApiRequest); + const editorialPage = await renderEditorialPage(objectGraph, response, { + isArcadePage: false, + }); + if (isNothing(editorialPage)) { + const notFoundError = new mediaNetwork.NetworkError("Media API response lacked required data"); + notFoundError.statusCode = 404; + throw notFoundError; + } + // The Media API often does not provide a URL; if that is the case, synthesize one + if (isNothing(editorialPage.canonicalURL) && isEditorialPageIntentByID(intent)) { + editorialPage.canonicalURL = makeEditorialPageURL(objectGraph, intent); + } + if (objectGraph.client.isWeb) { + injectWebNavigation(objectGraph, editorialPage, intent.platform); + injectSEOData(editorialPage, (_a = objectGraph.seo) === null || _a === void 0 ? void 0 : _a.getSEODataForEditorialPage(objectGraph, editorialPage, response)); + } + return editorialPage; + }); + }, + actionFor(intent, objectGraph) { + const action = new FlowAction("page"); + action.destination = intent; + if (isEditorialPageIntentByID(intent)) { + action.pageUrl = makeEditorialPageURL(objectGraph, intent); + } + return action; + }, +}; +//# sourceMappingURL=editorial-page-intent-controller.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/controllers/editorial-pages/editorial-shelf-collection-page-intent-controller.js b/node_modules/@jet-app/app-store/tmp/src/controllers/editorial-pages/editorial-shelf-collection-page-intent-controller.js new file mode 100644 index 0000000..a8fdf9c --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/controllers/editorial-pages/editorial-shelf-collection-page-intent-controller.js @@ -0,0 +1,35 @@ +import { isNothing } from "@jet/environment/types/optional"; +import * as mediaNetwork from "../../foundation/media/network"; +import { withActiveIntent } from "../../foundation/dependencies/active-intent"; +import { editorialShelfCollectionPageRoutes, makeEditorialShelfCollectionPageURL, makeEditorialShelfCollectionPageRequest, renderEditorialShelfCollectionPage, } from "../../common/editorial-pages/editorial-shelf-collection-page-utils"; +import { injectWebNavigation } from "../../common/web-navigation/inject-web-navigation"; +import { injectSEOData } from "../../api/models/web-renderable-page"; +import { validateEditorialShelfCollectionId } from "../../foundation/media/util"; +export const EditorialShelfCollectionPageIntentController = { + $intentKind: "EditorialShelfCollectionPageIntent", + routes: editorialShelfCollectionPageRoutes, + async perform(intent, objectGraphWithoutActiveIntent) { + return await withActiveIntent(objectGraphWithoutActiveIntent, intent, async (objectGraph) => { + var _a; + // See: https://github.pie.apple.com/its/amp-enums/blob/f75500b44f871f35ba3ce459a5ff4c9f225e71b0/src/main/java/com/apple/jingle/store/IdSpace.java#L43 + validateEditorialShelfCollectionId(objectGraph, intent.id); + const mediaApiReqest = makeEditorialShelfCollectionPageRequest(objectGraph, intent); + const response = await mediaNetwork.fetchData(objectGraph, mediaApiReqest); + const editorialShelfCollectionPage = renderEditorialShelfCollectionPage(objectGraph, response); + if (isNothing(editorialShelfCollectionPage)) { + const notFoundError = new mediaNetwork.NetworkError("Media API response lacked required data"); + notFoundError.statusCode = 404; + throw notFoundError; + } + if (isNothing(editorialShelfCollectionPage.canonicalURL)) { + editorialShelfCollectionPage.canonicalURL = makeEditorialShelfCollectionPageURL(objectGraph, intent); + } + if (objectGraph.client.isWeb) { + injectWebNavigation(objectGraph, editorialShelfCollectionPage, intent.platform); + injectSEOData(editorialShelfCollectionPage, (_a = objectGraph.seo) === null || _a === void 0 ? void 0 : _a.getSEODataForEditorialShelfCollectionPage(objectGraph, editorialShelfCollectionPage, response)); + } + return editorialShelfCollectionPage; + }); + }, +}; +//# sourceMappingURL=editorial-shelf-collection-page-intent-controller.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/controllers/grouping/grouping-page-intent-controller.js b/node_modules/@jet-app/app-store/tmp/src/controllers/grouping/grouping-page-intent-controller.js new file mode 100644 index 0000000..a12827a --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/controllers/grouping/grouping-page-intent-controller.js @@ -0,0 +1,58 @@ +import { isNothing } from "@jet/environment/types/optional"; +import { fetchData } from "../../foundation/media/network"; +import { withActiveIntent } from "../../foundation/dependencies/active-intent"; +import { makeBaseGroupingPageRequest, prepareGroupingPageRequest } from "../../common/grouping/grouping-request"; +import { flattenedGroupingFromDataContainer, groupingPageFromFlattenedGrouping, groupingParseContextFromDataContainer, } from "../../common/grouping/render-grouping-page"; +import { injectWebNavigation } from "../../common/web-navigation/inject-web-navigation"; +import { setPreviewPlatform } from "../../common/preview-platform"; +import { groupingPageRoutes as routes, makeGroupingPageCanonicalURL } from "../../common/grouping/grouping-page-url"; +import { injectSEOData } from "../../api/models/web-renderable-page"; +import { validateGroupingId } from "../../foundation/media/util"; +function makeMediaApiGroupingPageRequest(objectGraph, intent) { + const mediaApiRequest = makeBaseGroupingPageRequest(objectGraph); + if ("name" in intent) { + mediaApiRequest.addingQuery("name", intent.name); + } + else { + mediaApiRequest.withIdOfType(intent.id, "groupings"); + } + if ("tabs" in intent) { + mediaApiRequest.addingQuery("tabs", intent.tabs); + } + prepareGroupingPageRequest(objectGraph, mediaApiRequest); + setPreviewPlatform(objectGraph, mediaApiRequest); + return mediaApiRequest; +} +export const GroupingPageIntentController = { + $intentKind: "GroupingPageIntent", + routes, + async perform(intent, objectGraphWithoutActiveIntent) { + return await withActiveIntent(objectGraphWithoutActiveIntent, intent, async (objectGraph) => { + var _a; + if ("id" in intent) { + // See: https://github.pie.apple.com/its/Jingle/blob/aaccec936f1feed227fd171ae66bb160cf38e497/MZStorePlatform/src/main/clojure/jingle/store/platform/api/realm/apps/apps_endpoints.clj#L1554 + // See: https://github.pie.apple.com/its/Jingle/blob/aaccec936f1feed227fd171ae66bb160cf38e497/MZStorePlatform/src/main/clojure/jingle/store/platform/api/controller/editorial_resource.clj#L9 + // See: https://github.pie.apple.com/its/Jingle/blob/aaccec936f1feed227fd171ae66bb160cf38e497/MZStorePlatform/src/main/java/com/apple/jingle/store/mediaapi/util/SFMediaAPIEditorialUtil.java#L585 + // See: https://github.pie.apple.com/its/Jingle/blob/aaccec936f1feed227fd171ae66bb160cf38e497/MZStorePlatform/src/main/java/com/apple/jingle/store/mediaapi/util/SFMediaAPIEditorialUtil.java#L633-L639 + validateGroupingId(objectGraph, intent.id); + } + const mediaApiRequest = makeMediaApiGroupingPageRequest(objectGraph, intent); + const response = await fetchData(objectGraph, mediaApiRequest); + const flattenedGrouping = flattenedGroupingFromDataContainer(objectGraph, response); + const groupingParseContext = groupingParseContextFromDataContainer(objectGraph, response); + if (isNothing(flattenedGrouping) || isNothing(groupingParseContext)) { + return null; + } + const page = groupingPageFromFlattenedGrouping(objectGraph, flattenedGrouping, groupingParseContext); + if ("id" in intent) { + page.canonicalURL = makeGroupingPageCanonicalURL(objectGraph, intent); + } + if (objectGraph.client.isWeb) { + injectWebNavigation(objectGraph, page, intent.platform); + injectSEOData(page, (_a = objectGraph.seo) === null || _a === void 0 ? void 0 : _a.getSEODataForGroupingPage(objectGraph, page, response)); + } + return page; + }); + }, +}; +//# sourceMappingURL=grouping-page-intent-controller.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/controllers/product-page/bundle-page-intent-controller.js b/node_modules/@jet-app/app-store/tmp/src/controllers/product-page/bundle-page-intent-controller.js new file mode 100644 index 0000000..382da66 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/controllers/product-page/bundle-page-intent-controller.js @@ -0,0 +1,61 @@ +import { makeBundlePageIntent as baseMakeBundlePageIntent, } from "../../api/intents/bundle-page-intent"; +import { injectSEOData } from "../../api/models/web-renderable-page"; +import { generateRoutes } from "../../common/util/generate-routes"; +import { makeBundlePageRequest } from "../../common/product-page/bundle-page-common"; +import { createProductPageFromResponse } from "../../common/product-page/shelf-based/shelf-based-product-page"; +import { inferPreviewPlatformFromDeviceFamilies } from "../../common/preview-platform"; +import * as mediaNetwork from "../../foundation/media/network"; +import { injectWebNavigation } from "../../common/web-navigation/inject-web-navigation"; +import { withActiveIntent } from "../../foundation/dependencies/active-intent"; +import { URL } from "../../foundation/network/urls"; +import { validateAdamId } from "../../foundation/media/util"; +function makeBundlePageIntent(opts) { + const { ...clone } = opts; + // The `{bundleName}` dynamic segment is part of the URL for SEO purposes, but is not actually part + // of the `Intent` since it is not necessary for data-fetching. This data must not end up as + // part of the `Intent` as the value cannot be consistently normalized, which can break important + // caching behavior within the `web` client + delete clone["bundleName"]; + return baseMakeBundlePageIntent(clone); +} +// @ts-expect-error `generateRoutes` does not handle a typed `Platform` value correctly +const { routes } = generateRoutes(makeBundlePageIntent, "/app-bundle/{bundleName}/{id}", [], { + optionalQuery: ["platform", "lic"], +}); +export const BundlePageIntentController = { + $intentKind: "BundlePageIntent", + routes, + async perform(intent, objectGraphWithoutActiveIntent) { + return await withActiveIntent(objectGraphWithoutActiveIntent, intent, async (objectGraph) => { + var _a, _b; + // See: https://github.pie.apple.com/its/Jingle/blob/ce14e21b6ac3dd4be884aa12b26d4e79c6d8aa7a/MZStorePlatform/src/main/java/com/apple/jingle/store/mediaapi/resource/SFMediaAPICommonResourceType.java#L47 + validateAdamId(objectGraph, intent.id); + const mediaApiRequest = makeBundlePageRequest(objectGraph, intent); + const response = await mediaNetwork.fetchData(objectGraph, mediaApiRequest); + const previewPlatform = (_a = intent.platform) !== null && _a !== void 0 ? _a : inferPreviewPlatformFromDeviceFamilies(objectGraph, response); + if (!objectGraph.activeIntent.previewPlatform) { + objectGraph.activeIntent.setInferredPreviewPlatform(previewPlatform); + } + const page = await createProductPageFromResponse(objectGraph, response); + // The page URL (`.canonicalURL` property on the view-model) from Media API does not account for the `platform` + // query param; we need to add that in ourselves. We can't rely on `makeCanonicalUrl` as that does not handle the + // `bundleName` segment; we need to take the Media API URL and append the `platform` ourselves + if (page.canonicalURL) { + const pageURL = new URL(page.canonicalURL); + if (intent.platform) { + pageURL.param("platform", intent.platform); + } + if (intent.lic) { + pageURL.param("lic", intent.lic); + } + page.canonicalURL = pageURL.toString(); + } + if (objectGraph.client.isWeb) { + injectWebNavigation(objectGraph, page, previewPlatform); + injectSEOData(page, (_b = objectGraph.seo) === null || _b === void 0 ? void 0 : _b.getSEODataForBundlePage(objectGraph, page, response)); + } + return page; + }); + }, +}; +//# sourceMappingURL=bundle-page-intent-controller.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/controllers/product-page/eula-page-intent-controller.js b/node_modules/@jet-app/app-store/tmp/src/controllers/product-page/eula-page-intent-controller.js new file mode 100644 index 0000000..af1d155 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/controllers/product-page/eula-page-intent-controller.js @@ -0,0 +1,38 @@ +import { withActiveIntent } from "../../foundation/dependencies/active-intent"; +import { isNothing } from "@jet/environment"; +import * as mediaNetwork from "../../foundation/media/network"; +import * as mediaDataFetching from "../../foundation/media/data-fetching"; +import * as models from "../../api/models"; +import * as serverData from "../../foundation/json-parsing/server-data"; +export const EulaPageIntentController = { + $intentKind: "EulaPageIntent", + async perform(intent, objectGraphWithoutActiveIntent) { + return await withActiveIntent(objectGraphWithoutActiveIntent, intent, async (objectGraph) => { + const { resourceId, resourceType } = intent; + if (isNothing(resourceId) || isNothing(resourceType)) { + const notFoundError = new mediaNetwork.NetworkError("content not found"); + notFoundError.statusCode = 404; + throw notFoundError; + } + const mediaApiRequest = new mediaDataFetching.Request(objectGraph).withIdOfType(resourceId, "eula"); + mediaApiRequest.targetResourceType = resourceType; + const response = await mediaNetwork.fetchData(objectGraph, mediaApiRequest); + const fullText = serverData.asString(response, "results.eula.text"); + const textParagraphs = fullText.split(/\n{1,2}/); // Split by one or two newlines. 3+ are respected. + const paragraphs = []; + for (const text of textParagraphs) { + const paragraph = new models.Paragraph(text); + paragraph.wantsCollapsedNewlines = false; + paragraph.suppressVerticalMargins = true; + paragraphs.push(paragraph); + } + const shelf = new models.Shelf("paragraph"); + shelf.isHorizontal = false; + shelf.items = paragraphs; + const page = new models.GenericPage([shelf]); + page.title = objectGraph.loc.string("LICENSE_AGREEMENT"); + return page; + }); + }, +}; +//# sourceMappingURL=eula-page-intent-controller.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/controllers/product-page/product-page-intent-controller.js b/node_modules/@jet-app/app-store/tmp/src/controllers/product-page/product-page-intent-controller.js new file mode 100644 index 0000000..c13c360 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/controllers/product-page/product-page-intent-controller.js @@ -0,0 +1,63 @@ +import { withActiveIntent } from "../../foundation/dependencies/active-intent"; +import { FlowAction } from "../../api/models"; +import { productPageRoutes, makeProductPageURLWithoutAppSlug, } from "../../common/product-page/intent-controller-routing"; +import { injectSEOData } from "../../api/models/web-renderable-page"; +import { lookupURLForProductId as makeProductPageRequest } from "../../common/builders/url-mapping"; +import { createProductPageFromResponse } from "../../common/product-page/product-page-util"; +import { injectWebNavigation } from "../../common/web-navigation/inject-web-navigation"; +import * as mediaNetwork from "../../foundation/media/network"; +import { URL } from "../../foundation/network/urls"; +import { inferPreviewPlatformFromDeviceFamilies } from "../../common/preview-platform"; +import { validateAdamId } from "../../foundation/media/util"; +export const ProductPageIntentController = { + $intentKind: "ProductPageIntent", + routes: productPageRoutes, + async perform(intent, objectGraphWithoutActiveIntent) { + return await withActiveIntent(objectGraphWithoutActiveIntent, intent, async (objectGraph) => { + var _a, _b; + // See: https://github.pie.apple.com/its/Jingle/blob/ce14e21b6ac3dd4be884aa12b26d4e79c6d8aa7a/MZStorePlatform/src/main/java/com/apple/jingle/store/mediaapi/resource/SFMediaAPICommonResourceType.java#L46 + validateAdamId(objectGraph, intent.id); + const mediaApiRequest = makeProductPageRequest(objectGraph, intent.id, false, intent.ppid, false, false); + const response = await mediaNetwork.fetchData(objectGraph, mediaApiRequest); + // If the `Intent` does not specify a platform that the user requested, we need to select one of the options + // available in the Media API response to use instead. This will ensure we use the correct platform attributes + // when rendering the view-model + const inferredPlatform = inferPreviewPlatformFromDeviceFamilies(objectGraph, response); + if (!objectGraph.activeIntent.previewPlatform) { + const platformToSet = (_a = intent.platform) !== null && _a !== void 0 ? _a : inferredPlatform; + objectGraph.activeIntent.setInferredPreviewPlatform(platformToSet); + } + const previewPlatform = objectGraph.activeIntent.previewPlatform; + const productPage = (await createProductPageFromResponse(objectGraph, response)); + // The page URL (`.canonicalURL` property on the view-model) from Media API does not account for + // the `platform` and `ppid` query param; we need to add that in ourselves. + // We can't rely on `makeCanonicalUrl` as that does not handle the `appName` segment; + // We need to take the Media API URL and append the `platform` and `ppid` ourselves + if (productPage.canonicalURL) { + const pageURL = new URL(productPage.canonicalURL); + if (typeof intent.platform === "string" && intent.platform !== inferredPlatform) { + pageURL.param("platform", intent.platform); + } + if (typeof intent.ppid === "string" && intent.ppid) { + pageURL.param("ppid", intent.ppid); + } + if (typeof intent.lic === "string" && intent.lic) { + pageURL.param("lic", intent.lic); + } + productPage.canonicalURL = pageURL.toString(); + } + if (objectGraph.client.isWeb) { + injectWebNavigation(objectGraph, productPage, previewPlatform); + injectSEOData(productPage, (_b = objectGraph.seo) === null || _b === void 0 ? void 0 : _b.getSEODataForProductPage(objectGraph, productPage, response)); + } + return productPage; + }); + }, + actionFor(intent, objectGraph) { + const action = new FlowAction("product"); + action.destination = intent; + action.pageUrl = makeProductPageURLWithoutAppSlug(objectGraph, intent); + return action; + }, +}; +//# sourceMappingURL=product-page-intent-controller.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/controllers/product-page/see-all-intent-controller.js b/node_modules/@jet-app/app-store/tmp/src/controllers/product-page/see-all-intent-controller.js new file mode 100644 index 0000000..618bf5d --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/controllers/product-page/see-all-intent-controller.js @@ -0,0 +1,39 @@ +import { injectSEOData } from "../../api/models/web-renderable-page"; +import { withActiveIntent } from "../../foundation/dependencies/active-intent"; +import * as mediaNetwork from "../../foundation/media/network"; +import { validateAdamId } from "../../foundation/media/util"; +import { mediaApiProductSeeAllRequest } from "../../common/builders/url-mapping"; +import { createProductPageFromResponse } from "../../common/product-page/product-page-util"; +import { seeAllPageRoutes } from "../../common/product-page/intent-controller-routing"; +import { injectWebNavigation } from "../../common/web-navigation/inject-web-navigation"; +export const SeeAllPageIntentController = { + $intentKind: "SeeAllPageIntent", + routes: seeAllPageRoutes, + async perform(intent, objectGraphWithoutActiveIntent) { + return await withActiveIntent(objectGraphWithoutActiveIntent, intent, async (objectGraph) => { + var _a, _b; + validateAdamId(objectGraph, intent.id); + const seeAllRequest = mediaApiProductSeeAllRequest(objectGraph, intent.id, intent["see-all"]); + const seeAllResponse = await mediaNetwork.fetchData(objectGraph, seeAllRequest); + const page = (await createProductPageFromResponse(objectGraph, seeAllResponse)); + if (objectGraph.client.isWeb) { + // Synthesize the URL for the page based on the product page URL and the expected query param + if (page.canonicalURL) { + const pageURL = new URL(page.canonicalURL); + const params = pageURL.searchParams; + params.set("see-all", intent["see-all"]); + params.set("platform", intent.platform); + if (intent.platform) { + params.set("platform", intent.platform); + } + page.canonicalURL = pageURL.toString(); + } + page.seeAllType = intent["see-all"]; + injectWebNavigation(objectGraph, page, intent.platform); + injectSEOData(page, (_b = (_a = objectGraph.seo) === null || _a === void 0 ? void 0 : _a.getSEODataForSeeAllPage) === null || _b === void 0 ? void 0 : _b.call(_a, objectGraph, page, seeAllResponse)); + } + return page; + }); + }, +}; +//# sourceMappingURL=see-all-intent-controller.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/controllers/room/room-page-intent-controller.js b/node_modules/@jet-app/app-store/tmp/src/controllers/room/room-page-intent-controller.js new file mode 100644 index 0000000..a414856 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/controllers/room/room-page-intent-controller.js @@ -0,0 +1,29 @@ +import { withActiveIntent } from "../../foundation/dependencies/active-intent"; +import { fetchData } from "../../foundation/media/network"; +import { roomPageRoutes as routes, makeCanonicalRoomPageUrl } from "../../common/room/room-common"; +import { createRoomRequest } from "../../common/room/room-request"; +import { renderRoomPage } from "../../common/room/room-page"; +import { injectWebNavigation } from "../../common/web-navigation/inject-web-navigation"; +import { injectSEOData } from "../../api/models/web-renderable-page"; +import { validateFcId } from "../../foundation/media/util"; +export const RoomPageIntentController = { + $intentKind: "RoomPageIntent", + routes, + async perform(intent, objectGraphWithoutActiveIntent) { + return await withActiveIntent(objectGraphWithoutActiveIntent, intent, async (objectGraph) => { + var _a; + // See: https://github.pie.apple.com/its/Jingle/blob/d2d051c9ef2891f72d5f02f5bbbf2d7748afa7b9/MZStoreComponents/src/main/java/com/apple/jingle/app/store/editorial/SFEditorialHelper.java#L293 + validateFcId(objectGraph, intent.id); + const mediaApiRequest = createRoomRequest(objectGraph, intent.id); + const response = await fetchData(objectGraph, mediaApiRequest); + const page = renderRoomPage(objectGraph, response); + page.canonicalURL = makeCanonicalRoomPageUrl(objectGraph, intent); + if (objectGraph.client.isWeb) { + injectWebNavigation(objectGraph, page, intent.platform); + injectSEOData(page, (_a = objectGraph.seo) === null || _a === void 0 ? void 0 : _a.getSEODataForRoomPage(objectGraph, page, response)); + } + return page; + }); + }, +}; +//# sourceMappingURL=room-page-intent-controller.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/controllers/search/search-landing-page-intent-controller.js b/node_modules/@jet-app/app-store/tmp/src/controllers/search/search-landing-page-intent-controller.js new file mode 100644 index 0000000..aafca37 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/controllers/search/search-landing-page-intent-controller.js @@ -0,0 +1,34 @@ +import { isNothing } from "@jet/environment"; +import { makeSearchLandingPageIntent, } from "../../api/intents/search/search-landing-page-intent"; +import { generateRoutes } from "../../common/util/generate-routes"; +import { fetchPage } from "../../common/search/search-landing-page-utils"; +import { injectWebNavigation } from "../../common/web-navigation/inject-web-navigation"; +import { withActiveIntent } from "../../foundation/dependencies/active-intent"; +import { injectSEOData } from "../../api/models/web-renderable-page"; +import { validateNeedsVisionRestriction } from "../../foundation/media/util"; +const { routes, makeCanonicalUrl } = generateRoutes(makeSearchLandingPageIntent, "/{platform}/search"); +export const SearchLandingPageIntentController = { + $intentKind: "SearchLandingPageIntent", + routes, + async perform(intent, objectGraphWithoutActiveIntent) { + if (objectGraphWithoutActiveIntent.client.isWeb && !intent.platform) { + // Search on the "web" client requires a platform; if we don't have one from the `Intent`, we should + // assume we want `iphone` results + intent.platform = "iphone"; + } + return await withActiveIntent(objectGraphWithoutActiveIntent, intent, async (objectGraph) => { + var _a; + validateNeedsVisionRestriction(objectGraph); + const searchLandingPage = await fetchPage(objectGraph); + if (isNothing(searchLandingPage.canonicalURL)) { + searchLandingPage.canonicalURL = makeCanonicalUrl(objectGraph, intent); + } + if (objectGraph.client.isWeb) { + injectWebNavigation(objectGraph, searchLandingPage, intent.platform); + injectSEOData(searchLandingPage, (_a = objectGraph.seo) === null || _a === void 0 ? void 0 : _a.getSEODataForSearchLandingPage(objectGraph, searchLandingPage, null)); + } + return searchLandingPage; + }); + }, +}; +//# sourceMappingURL=search-landing-page-intent-controller.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/controllers/search/search-results-controller.js b/node_modules/@jet-app/app-store/tmp/src/controllers/search/search-results-controller.js new file mode 100644 index 0000000..d263c24 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/controllers/search/search-results-controller.js @@ -0,0 +1,152 @@ +import { isNothing } from "@jet/environment"; +import * as models from "../../api/models"; +import { FetchTimingMetricsBuilder } from "@jet/environment/metrics/fetch-timing-metrics-builder"; +import { injectSEOData } from "../../api/models/web-renderable-page"; +import { AbstractPageBuilder } from "../../common/builders/abstract-page-builder"; +import * as search from "../../common/search/search"; +import { makeCanonicalSearchResultsPageUrl, searchResultsPageRoutes } from "../../common/search/search-page-url"; +import * as searchResultsFetching from "../../common/search/search-results-fetching"; +import { injectWebNavigation } from "../../common/web-navigation/inject-web-navigation"; +import { replacePlatformSelectorDestinationWithSearchResults } from "../../common/web-navigation/search-results-platform-selection"; +import { withActiveIntent } from "../../foundation/dependencies/active-intent"; +import { validateNeedsVisionRestriction } from "../../foundation/media/util"; +/** Handles `SearchResultsIntent` to fetch the search results. This is for shelves v1. */ +export class SearchResultsIntentController { + constructor() { + this.$intentKind = "SearchResultsIntent"; + } + async perform(intent, objectGraph) { + return await fetchSearchResults(objectGraph, intent.requestDescriptor); + } +} +/** Handles `SearchResultsMoreIntent` to fetch more of the search results. This is for shelves v1. */ +export class SearchResultsMoreIntentController { + constructor() { + this.$intentKind = "SearchResultsMoreIntent"; + } + async perform(intent, objectGraph) { + return await fetchMoreSearchResults(objectGraph, intent.pageToken); + } +} +/** Handles `SegmentedSearchResultsPageIntent` to fetch the segmented search results. */ +export class SegmentedSearchResultsPageIntentController { + constructor() { + this.$intentKind = "SegmentedSearchResultsPageIntent"; + } + async perform(intent, objectGraph) { + return await fetchSegmentedSearchResults(objectGraph, intent.requestDescriptor); + } +} +/** Handles `SearchResultsPageIntent` to fetch the search results. This is for shelves 2.0. */ +export class SearchResultsPageIntentController { + constructor() { + this.$intentKind = "SearchResultsPageIntent"; + this.routes = searchResultsPageRoutes; + } + async perform(intent, objectGraphWithoutActiveIntnet) { + if (objectGraphWithoutActiveIntnet.client.isWeb && !intent.platform) { + // Search on the "web" client requires a platform; if we don't have one from the `Intent`, we should + // assume we want `iphone` results + intent.platform = "iphone"; + } + return await withActiveIntent(objectGraphWithoutActiveIntnet, intent, async (objectGraph) => { + var _a; + validateNeedsVisionRestriction(objectGraph); + const searchResultsPage = await fetchSearchResultsPage(objectGraph, intent); + if (isNothing(searchResultsPage.canonicalURL)) { + searchResultsPage.canonicalURL = makeCanonicalSearchResultsPageUrl(objectGraph, intent); + } + if (objectGraph.client.isWeb) { + const webNavigation = injectWebNavigation(objectGraph, searchResultsPage, intent.platform); + webNavigation.searchAction.destination.term = intent.term; + replacePlatformSelectorDestinationWithSearchResults(objectGraph, webNavigation, intent); + injectSEOData(searchResultsPage, (_a = objectGraph.seo) === null || _a === void 0 ? void 0 : _a.getSEODataForSearchResultsPage(objectGraph, searchResultsPage, null)); + } + return searchResultsPage; + }); + } + actionFor(intent, objectGraph) { + const flowAction = new models.FlowAction("search"); + flowAction.destination = intent; + flowAction.pageUrl = makeCanonicalSearchResultsPageUrl(objectGraph, intent); + return flowAction; + } +} +/** Handles `SearchResultsPageMoreIntent` to fetch more of the search results. This is for shelves 2.0. */ +export class SearchResultsPageMoreIntentController { + constructor() { + this.$intentKind = "SearchResultsPageMoreIntent"; + } + async perform(intent, objectGraph) { + return await fetchMoreSearchResultsPage(objectGraph, intent.pageToken); + } +} +/** Fetches search results for shelves v1. */ +async function fetchSearchResults(objectGraph, requestDescriptor) { + const fetchTimingMetricsBuilder = new FetchTimingMetricsBuilder(); + const modifiedObjectGraph = objectGraph.addingFetchTimingMetricsBuilder(fetchTimingMetricsBuilder); + const combinedSearchData = await searchResultsFetching.fetchSequentialSearchResultsData(modifiedObjectGraph, requestDescriptor); + if (combinedSearchData === null) { + return search.emptyResults(modifiedObjectGraph, requestDescriptor.facets); + } + return await search.searchResultsFromResponse(modifiedObjectGraph, combinedSearchData); +} +/** Fetches search results for segmented search results on visionOS */ +async function fetchSegmentedSearchResults(objectGraph, requestDescriptor) { + const combinedSearchData = await searchResultsFetching.fetchSegmentedSearchResults(objectGraph, requestDescriptor); + if (combinedSearchData === null) { + return search.emptySegmentedResultsPage(objectGraph); + } + return await search.segmentedSearchResultsPageFromResponse(objectGraph, combinedSearchData); +} +/** Fetches more search results for shelves v1. */ +async function fetchMoreSearchResults(objectGraph, token) { + return await search.paginatedSearchResultsWithToken(objectGraph, token); +} +/** Fetches search results for shelves 2.0. */ +async function fetchSearchResultsPage(objectGraph, requestDescriptor) { + const fetchTimingMetricsBuilder = new FetchTimingMetricsBuilder(); + const modifiedObjectGraph = objectGraph.addingFetchTimingMetricsBuilder(fetchTimingMetricsBuilder); + const combinedSearchData = await searchResultsFetching.fetchSequentialSearchResultsData(modifiedObjectGraph, requestDescriptor); + if (combinedSearchData === null) { + return search.emptyResultsPage(objectGraph, requestDescriptor.facets); + } + return await search.searchResultsPageFromResponse(modifiedObjectGraph, combinedSearchData); +} +/** Fetches more search results for shelves 2.0. */ +async function fetchMoreSearchResultsPage(objectGraph, token) { + return await search.paginatedSearchResultsPageWithToken(objectGraph, token); +} +export class SearchResultsBuilder extends AbstractPageBuilder { + constructor(objectGraph, runtime) { + super("SearchResultsBuilder"); + runtime.exportingService("SearchData", { + /** Fetches search results for shelves v1. */ + async fetchResults(options) { + return await fetchSearchResults(objectGraph, options); + }, + /** Fetches more search results for shelves v1. */ + async fetchMoreResults(options) { + return await fetchMoreSearchResults(objectGraph, options.pageToken); + }, + /** Fetches search results for shelves 2.0. */ + async fetchSearchResultsPage(options) { + return await fetchSearchResultsPage(objectGraph, options); + }, + /** Fetches more search results for shelves 2.0. */ + async fetchMoreOfSearchResultsPage(options) { + return await fetchMoreSearchResultsPage(objectGraph, options.pageToken); + }, + }); + } + async handlePage(objectGraph, url, parameters, matchedRuleIdentifier, referrerData, isIncomingURL) { + throw new Error("No page routes specified"); + } + pageRoute(objectGraph) { + return []; + } + pageType() { + return "unknown"; + } +} +//# sourceMappingURL=search-results-controller.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/controllers/today/routable-article-page-intent-controller.js b/node_modules/@jet-app/app-store/tmp/src/controllers/today/routable-article-page-intent-controller.js new file mode 100644 index 0000000..9ab32b1 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/controllers/today/routable-article-page-intent-controller.js @@ -0,0 +1,53 @@ +import { withActiveIntent } from "../../foundation/dependencies/active-intent"; +import { injectSEOData } from "../../api/models/web-renderable-page"; +import { buildArticlePageRequest } from "../../common/today/article-request"; +import { fetchData } from "../../foundation/media/network"; +import { articlePageFromResponse, ArticleParseContext } from "../../common/today/article"; +import { routableArticlePageWithPlatformRoutes, routableArticlePageWithoutPlatformRoutes, makeRoutableArticlePageCanonicalUrl, } from "../../common/today/routable-article-page-url-utils"; +import { injectWebNavigation } from "../../common/web-navigation/inject-web-navigation"; +import { inferPreviewPlatformFromEditorialPlatforms } from "../../common/preview-platform"; +import { validateAdamId } from "../../foundation/media/util"; +export const RoutableArticlePageIntentController = { + $intentKind: "RoutableArticlePageIntent", + routes(objectGraph) { + return [ + // Note: order here is important! Both URL patterns provide an option where there is a + // single dynamic segment in front of the stable `/story` segment; it could be either + // the platform without an explicit storefront, or a storefront without an explicit platform. + // We want to assume the latter, so the URLs without a platform must be defined first + ...routableArticlePageWithoutPlatformRoutes(objectGraph), + ...routableArticlePageWithPlatformRoutes(objectGraph), + ]; + }, + async perform(intent, objectGraphWithoutActiveIntent) { + return await withActiveIntent(objectGraphWithoutActiveIntent, intent, async (objectGraph) => { + var _a, _b, _c; + // See: https://github.pie.apple.com/its/Jingle/blob/ce14e21b6ac3dd4be884aa12b26d4e79c6d8aa7a/MZStorePlatform/src/main/java/com/apple/jingle/store/mediaapi/resource/SFMediaAPICommonResourceType.java#L89 + validateAdamId(objectGraph, intent.id); + const mediaApiRequest = buildArticlePageRequest(objectGraph, intent, false); + const response = await fetchData(objectGraph, mediaApiRequest); + // If the user visits a "story" without an explicit "platform" specified, we need to infer a + // desirable default so that we can look up the correct platform attributes when rendering + // the view-model + const previewPlatform = (_a = intent.platform) !== null && _a !== void 0 ? _a : inferPreviewPlatformFromEditorialPlatforms(objectGraph, response); + if (!objectGraph.activeIntent.previewPlatform) { + objectGraph.activeIntent.setInferredPreviewPlatform(previewPlatform); + } + const articleParseContext = new ArticleParseContext(); + const page = articlePageFromResponse(objectGraph, response, articleParseContext); + // The Media API-provided `.canonicalURL` will not contain the explicit `{platform}` + // segment, if required; we need to override their value to make use of the URL + // factory provided by our routing utils instead + page.canonicalURL = makeRoutableArticlePageCanonicalUrl(objectGraph, intent); + if (objectGraph.client.isWeb) { + const webNavigation = injectWebNavigation(objectGraph, page, previewPlatform); + if ((_b = page.card) === null || _b === void 0 ? void 0 : _b.title) { + webNavigation.title = page.card.title; + } + injectSEOData(page, (_c = objectGraph.seo) === null || _c === void 0 ? void 0 : _c.getSEODataForArticlePage(objectGraph, page, response)); + } + return page; + }); + }, +}; +//# sourceMappingURL=routable-article-page-intent-controller.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/controllers/today/routable-today-page-intent-controller.js b/node_modules/@jet-app/app-store/tmp/src/controllers/today/routable-today-page-intent-controller.js new file mode 100644 index 0000000..d12e4ef --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/controllers/today/routable-today-page-intent-controller.js @@ -0,0 +1,21 @@ +import { withActiveIntent } from "../../foundation/dependencies/active-intent"; +import { buildTodayPageFromRequest, generateTodayPageRequest, prepareRequest as prepareTodayPageRequest, } from "../../common/today/today-controller-util"; +import { injectWebNavigation } from "../../common/web-navigation/inject-web-navigation"; +import { injectSEOData } from "../../api/models/web-renderable-page"; +export const RoutableTodayPageIntentController = { + $intentKind: "RoutableTodayPageIntent", + async perform(intent, objectGraphWithoutActiveIntent) { + return await withActiveIntent(objectGraphWithoutActiveIntent, intent, async (objectGraph) => { + var _a; + const mediaApiRequest = generateTodayPageRequest(objectGraph); + prepareTodayPageRequest(objectGraph, mediaApiRequest, true); + const page = await buildTodayPageFromRequest(objectGraph, mediaApiRequest); + if (objectGraph.client.isWeb) { + injectWebNavigation(objectGraph, page, intent.platform); + injectSEOData(page, (_a = objectGraph.seo) === null || _a === void 0 ? void 0 : _a.getSEODataForTodayPage(objectGraph, page, null)); + } + return page; + }); + }, +}; +//# sourceMappingURL=routable-today-page-intent-controller.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/controllers/top-charts/charts-hub-page-intent-controller.js b/node_modules/@jet-app/app-store/tmp/src/controllers/top-charts/charts-hub-page-intent-controller.js new file mode 100644 index 0000000..0f1a16e --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/controllers/top-charts/charts-hub-page-intent-controller.js @@ -0,0 +1,39 @@ +import { makeChartsPageIntent } from "../../api/intents/charts-page-intent"; +import { withActiveIntent } from "../../foundation/dependencies/active-intent"; +import { renderChartsHub } from "../../common/charts/charts-hub"; +import { chartsHubPageRoutes, makeChartsHubPageURL } from "../../common/charts/charts-page-url"; +import { injectWebNavigation } from "../../common/web-navigation/inject-web-navigation"; +import { injectSEOData } from "../../api/models/web-renderable-page"; +const APPS_GENRE_ID_STATIC = "36"; +const GAMES_GENRE_ID_STATIC = "6014"; +export const ChartsHubPageIntentController = { + $intentKind: "ChartsHubPageIntent", + routes: chartsHubPageRoutes, + async perform(intent, objectGraphWithoutActiveIntent) { + return await withActiveIntent(objectGraphWithoutActiveIntent, intent, async (objectGraph) => { + var _a; + const [appsPage, gamesPage] = await Promise.all([ + objectGraph.dispatcher.dispatch(makeChartsPageIntent({ + storefront: intent.storefront, + language: intent.language, + platform: intent.platform, + genreId: APPS_GENRE_ID_STATIC, + }), objectGraph), + objectGraph.dispatcher.dispatch(makeChartsPageIntent({ + storefront: intent.storefront, + language: intent.language, + platform: intent.platform, + genreId: GAMES_GENRE_ID_STATIC, + }), objectGraph), + ]); + const hubPage = renderChartsHub(objectGraph, appsPage, gamesPage); + hubPage.canonicalURL = makeChartsHubPageURL(objectGraph, intent); + if (objectGraph.client.isWeb) { + injectWebNavigation(objectGraph, hubPage, intent.platform); + injectSEOData(hubPage, (_a = objectGraph.seo) === null || _a === void 0 ? void 0 : _a.getSEODataForChartsHubPage(objectGraph, hubPage, null)); + } + return hubPage; + }); + }, +}; +//# sourceMappingURL=charts-hub-page-intent-controller.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/controllers/top-charts/charts-page-intent-controller.js b/node_modules/@jet-app/app-store/tmp/src/controllers/top-charts/charts-page-intent-controller.js new file mode 100644 index 0000000..79a0667 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/controllers/top-charts/charts-page-intent-controller.js @@ -0,0 +1,25 @@ +import { injectSEOData } from "../../api/models/web-renderable-page"; +import { withActiveIntent } from "../../foundation/dependencies/active-intent"; +import { chartsPageRoutes, makeChartsPageURL } from "../../common/charts/charts-page-url"; +import { injectWebNavigation } from "../../common/web-navigation/inject-web-navigation"; +import { fetchAndRenderTopChartPage } from "../../common/top-charts/top-charts-page"; +import { validateGenreId } from "../../foundation/media/util"; +export const ChartsPageIntentController = { + $intentKind: "ChartsPageIntent", + routes: chartsPageRoutes, + async perform(intent, objectGraphWithoutActiveIntent) { + return await withActiveIntent(objectGraphWithoutActiveIntent, intent, async (objectGraph) => { + var _a; + // See: https://github.pie.apple.com/its/Jingle/blob/aaccec936f1feed227fd171ae66bb160cf38e497/MZStorePlatform/src/main/clojure/jingle/store/platform/api/controller/categories.clj#L135 + validateGenreId(objectGraph, intent.genreId); + const page = await fetchAndRenderTopChartPage(objectGraph, intent.genreId, null, intent.chart, intent.ageBandId); + if (objectGraph.client.isWeb) { + injectWebNavigation(objectGraph, page, intent.platform); + injectSEOData(page, (_a = objectGraph.seo) === null || _a === void 0 ? void 0 : _a.getSEODataForChartsPage(objectGraph, page, null)); + } + page.canonicalURL = makeChartsPageURL(objectGraph, intent); + return page; + }); + }, +}; +//# sourceMappingURL=charts-page-intent-controller.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/controllers/web-navigation/category-tabs-intent-controller.js b/node_modules/@jet-app/app-store/tmp/src/controllers/web-navigation/category-tabs-intent-controller.js new file mode 100644 index 0000000..02487d2 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/controllers/web-navigation/category-tabs-intent-controller.js @@ -0,0 +1,62 @@ +import { isSome } from "@jet/environment/types/optional"; +import { dataCollectionFromDataContainer } from "../../foundation/media/data-structure"; +import { withActiveIntent } from "../../foundation/dependencies/active-intent"; +import { asDictionary, isDefinedNonNullNonEmpty } from "../../foundation/json-parsing/server-data"; +import { attributeAsDictionary } from "../../foundation/media/attributes"; +import { Request } from "../../foundation/media/data-fetching"; +import { fetchData } from "../../foundation/media/network"; +import { relationshipData } from "../../foundation/media/relationships"; +import { artworkFromApiArtwork } from "../../common/content/content"; +import { actionFromData } from "../../common/lockups/lockups"; +import { newLocationTracker } from "../../common/metrics/helpers/location"; +export class CategoryTabsIntentController { + constructor() { + this.$intentKind = "CategoryTabsIntent"; + } + async perform(intent, objectGraphWithoutActiveIntent) { + return await withActiveIntent(objectGraphWithoutActiveIntent, intent, async (objectGraph) => { + const editorialItemId = objectGraph.bag.webNavigationCategoryTabsEditorialItemId; + if (!isSome(editorialItemId)) { + return []; + } + const attributes = ["editorialArtwork"]; + const request = new Request(objectGraph) + .withIdOfType(editorialItemId, "collections") + .includingScopedAttributes("editorial-items", attributes) + .includingScopedAttributes("editorial-pages", attributes) + .addingQuery("sparseLimit[card-contents]", "0") + .addingQuery("platform", "web") + .addingQuery("previewPlatform", objectGraph.activeIntent.platform); + const response = await fetchData(objectGraph, request); + const contents = dataCollectionFromDataContainer(response); + return contents.map((item) => { + const primaryContent = relationshipData(objectGraph, item, "primary-content"); + const dataSource = primaryContent || item; + const metricsBase = { + pageInformation: null, + locationTracker: newLocationTracker(), + id: item.id, + }; + const formattedArtwork = getFormattedArtwork(objectGraph, item); + return { + action: actionFromData(objectGraph, dataSource, metricsBase, objectGraph.host.clientIdentifier), + isActive: intent.id === dataSource.id, + artwork: formattedArtwork, + }; + }); + }); + } +} +function getFormattedArtwork(objectGraph, data) { + const artwork = attributeAsDictionary(data, "artwork", null) || attributeAsDictionary(data, "editorialArtwork"); + if (!isDefinedNonNullNonEmpty(artwork)) { + return undefined; + } + const editorialArtwork = attributeAsDictionary(data, "editorialArtwork"); + const icon = asDictionary(editorialArtwork, "brandLogo", null); + return artworkFromApiArtwork(objectGraph, icon, { + allowingTransparency: true, + useCase: 20 /* ArtworkUseCase.CategoryIcon */, + }); +} +//# sourceMappingURL=category-tabs-intent-controller.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/foundation/amp-localization/amp-localization.js b/node_modules/@jet-app/app-store/tmp/src/foundation/amp-localization/amp-localization.js new file mode 100644 index 0000000..df04983 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/foundation/amp-localization/amp-localization.js @@ -0,0 +1,371 @@ +/** + * A type encapsulating localization logic for strings + * returned by localization server. + * + * Replacement for 'its' namespace from @onyx/localization package. + */ +export class AmpLocalization { + constructor() { + // region Properties + /** + * Localization JSON dictionary loaded from localization file. + */ + this.locData = {}; + /** + * The 2-letter code for current device language. + */ + this.language = "en"; + // endregion + } + // endregion + // region API + /** + * Updates the localization data for the device. + * @param localizations A JSON dictionary representing the localized strings. + * @param language Language code for current device language. + */ + updateLocalizationData(localizations, language) { + this.locData = localizations; + this.language = language; + } + /** + * Localizes a string replacing placehoders in key with values in the parameters dictionary + * @param key The loc key to look up + * @param params Parameters to replace in the loc string + * @return The localized string + */ + localize(key, params) { + let value = this.locData[key]; + if (value === undefined || typeof value !== "string") { + value = key; + } + if (params) { + value = this.replaceTokens(value, params); + } + value = this.replaceMarkupTokens(value, params); + return value; + } + /** + * Localize with appropriate plural form based on the count. + * + * Some languages have more plural forms than others. + * The full set of categories is "zero", "one", "two", "few", "many", and "other". + * + * The base loc key is used for "other" (the default). + * Otherwise the category is appended to the base loc key with a "." separator. + * + * http://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html + * http://cldr.unicode.org/index/cldr-spec/plural-rules#TOC-Determining-Plural-Categories + * http://unicode.org/repos/cldr/trunk/specs/ldml/tr35-numbers.html#Language_Plural_Rules + * + * English loc keys + * key = "@@count@@ dogs"; // default, aka "plural" + * key.zero = "no dogs"; // used when count is 0 + * key.one = "@@count@@ dog"; // used when count is 1, aka "singular" + * + * @param key base loc key + * @param count number used to determine the plural form + * @param params (optional) substitution keys and values, "count" will be added if it's not already present + * @return localized string + */ + localizeWithCount(objectGraph, key, count, params) { + let keyToUse = null; + let pluralFormKey; + let category; + if (count === 0) { + // Special case zero for all languages so we can have strings like + // "you have no messages" instead of "you have 0 messages". + pluralFormKey = `${key}.zero`; + if (this.isLocalized(objectGraph, pluralFormKey)) { + keyToUse = pluralFormKey; + } + } + if (keyToUse === null) { + keyToUse = key; + category = this.pluralCategory(objectGraph, count); + if (category !== "other") { + pluralFormKey = `${key}.${category}`; + if (this.isLocalized(objectGraph, pluralFormKey)) { + keyToUse = pluralFormKey; + } + } + } + if (!params) { + params = {}; + } + // @@count@@ is the standard, but @@number@@ is frequently used too. + if (params.count === undefined) { + params.count = this.formatNumber(count.toString()); + } + if (params.number === undefined) { + params.number = this.formatNumber(count.toString()); + } + return this.localize(keyToUse !== null && keyToUse !== void 0 ? keyToUse : key, params); + } + // endregion + // region Private Methods + replaceTokens(text, values) { + Object.entries(values).forEach(([key, value]) => { + const subKey = "@@" + key + "@@"; + text = this.replaceSubstring(text, subKey, value); + }); + return text; + } + replaceMarkupTokens(text, values) { + if (text.indexOf("##") <= -1) { + return text; + } + // Resolve token properties. + let markupParams; + if (values) { + // Create a copy to avoid mutating defaults. + markupParams = { ...AmpLocalization.MARKUP_PARAMS }; + Object.entries(values).forEach(([key, value]) => { + markupParams[key] = value; + }); + } + else { + markupParams = AmpLocalization.MARKUP_PARAMS; + } + Object.entries(markupParams).forEach(([key, value]) => { + const token = "##" + key + "##"; + text = text.replace(new RegExp(token, "gi"), value); + }); + // Replace any remaining standard markup tags like <br> etc. + text = text.replace(/##([^##]+)##/gi, "<$1>"); + return text; + } + /** + * Searches "str" for "substr" and replaces each occurrence with "replacement" + * @param str input string + * @param substr the string to search for in the input string + * @param replacement value to use for the replacement + * + * JavaScript String.replace has a misfeature where "$" in the replacement is always interpreted as a meta char, + * i.e. you have to use "$$" in the replacement to get a single "$" in the resulting string + * which lead to <rdar://problem/16848063> tv episode title containing "$&" not displaying correctly + * Using split and join is faster than substituting $$ for $ in the replacement. + */ + replaceSubstring(str, substr, replacement) { + return str.split(substr).join(replacement); + } + formatNumber(value) { + let decimalSeparator = this.locData["_decimalSeparator"]; + if (decimalSeparator === undefined || typeof decimalSeparator !== "string") { + decimalSeparator = "."; + } + let thousandsSeparator = this.locData["_thousandsSeparator"]; + if (thousandsSeparator === undefined || typeof thousandsSeparator !== "string") { + thousandsSeparator = "."; + } + const parts = parseFloat(value).toString().split("."); + const chars = parts[0].split(""); + for (let i = chars.length - 3; i > 0; i -= 3) { + chars.splice(i, 0, thousandsSeparator); + } + parts[0] = chars.join(""); + return parts.join(decimalSeparator); + } + /** + * Check whether the given key is localized or not. + * @param key The key to check. + */ + isLocalized(objectGraph, key) { + const value = this.locData[key]; + if (value === undefined || typeof value !== "string") { + return false; + } + else if (key.indexOf(".") === -1) { + // Simple localization keys such as "OK". + return true; + } + else if (value === key || (value.indexOf("**") === 0 && value.lastIndexOf("**") === value.length - 2)) { + objectGraph.console.error("Unlocalized key in keys dictionary", key); + return false; + } + return true; + } + /** + * Returns localization plural category for given number. + * @param num The number to return plural category for. + * @returns Plural category for specified number or "other" + * if there's no plural category function available for current language. + * + * @see: + * - http://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html + * - http://cldr.unicode.org/index/cldr-spec/plural-rules#TOC-Determining-Plural-Categories + * - http://unicode.org/repos/cldr/trunk/specs/ldml/tr35-numbers.html#Language_Plural_Rules + */ + pluralCategory(objectGraph, num) { + const categoryFn = AmpLocalization.pluralCategoryFnByLanguage[this.language]; + if (categoryFn !== undefined) { + return categoryFn(num); + } + else { + objectGraph.console.warn("Missing plural category function for: " + this.language); + return "other"; + } + } +} +/** + * Markup parameters used for replacing markup tokens in text. + */ +AmpLocalization.MARKUP_PARAMS = { nbsp: " ", gt: ">", lt: "<", copy: "\u00a9" }; +// endregion +// regions Plurals +AmpLocalization.pluralCategoryDefault = function (num) { + return "other"; +}; +AmpLocalization.pluralCategoryOne = function (num) { + if (num === 1) { + return "one"; + } + return "other"; +}; +AmpLocalization.pluralCategoryArabic = function (num) { + const n = num >> 0; + if (n !== num) { + // non integer + return "other"; + } + if (n === 0) { + return "zero"; + } + if (n === 1) { + return "one"; + } + if (n === 2) { + return "two"; + } + const m100 = n % 100; + if (m100 >= 11) { + // n mod 100 in 11..99 + return "many"; + } + if (m100 >= 3) { + // n mod 100 in 3..10 + return "few"; + } + return "other"; +}; +AmpLocalization.pluralCategoryFrench = function (num) { + // Do not check for integer, French includes fractional values! + if (num < 2 && num >= 0) { + return "one"; + } + return "other"; +}; +AmpLocalization.pluralCategoryHebrew = function (num) { + const n = num >> 0; + if (n !== num) { + // non integer + return "other"; + } + if (n === 1) { + return "one"; + } + if (n === 2) { + return "two"; + } + const m10 = n % 10; + if (m10 === 0 && n > 10) { + // n mod 10 is 0 and n != 0..10 + return "many"; + } + return "other"; +}; +AmpLocalization.pluralCategoryPolish = function (num) { + const n = num >> 0; + if (n !== num) { + // non integer + return "other"; + } + if (n === 1) { + return "one"; + } + const m10 = n % 10; + if (m10 <= 4 && m10 >= 2) { + const m100 = n % 100; + if (m100 > 14 || m100 < 12) { + // n mod 10 in 2..4 and n mod 100 not in 12..14 + return "few"; + } + } + return "many"; +}; +AmpLocalization.pluralCategoryRomanian = function (num) { + const n = num >> 0; + if (n !== num) { + // non integer + return "few"; + } + if (n === 0) { + return "few"; + } + if (n === 1) { + return "one"; + } + const m100 = num % 100; + if (m100 <= 19 && m100 >= 1) { + // n mod 100 in 1..19 + return "few"; + } + return "other"; +}; +AmpLocalization.pluralCategoryRussian = function (num) { + const n = num >> 0; + if (n !== num) { + // non integer + return "other"; + } + const m10 = n % 10; + if (m10 >= 5 || m10 === 0) { + // n mod 10 is 0 or n mod 10 in 5..9 + return "many"; + } + const m100 = n % 100; + if (m100 <= 14 && m100 >= 11) { + // n mod 100 in 11..14 + return "many"; + } + if (m10 === 1) { + // n mod 10 is 1 and n mod 100 is not 11 + return "one"; + } + // n mod 10 in 2..4 and n mod 100 not in 12..14 + return "few"; +}; +/** + * Mapping language code to plural category function. + */ +AmpLocalization.pluralCategoryFnByLanguage = { + zh: AmpLocalization.pluralCategoryDefault, + id: AmpLocalization.pluralCategoryDefault, + ja: AmpLocalization.pluralCategoryDefault, + ko: AmpLocalization.pluralCategoryDefault, + ms: AmpLocalization.pluralCategoryDefault, + th: AmpLocalization.pluralCategoryDefault, + vi: AmpLocalization.pluralCategoryDefault, + en: AmpLocalization.pluralCategoryOne, + ca: AmpLocalization.pluralCategoryOne, + da: AmpLocalization.pluralCategoryOne, + nl: AmpLocalization.pluralCategoryOne, + de: AmpLocalization.pluralCategoryOne, + el: AmpLocalization.pluralCategoryOne, + fi: AmpLocalization.pluralCategoryOne, + hu: AmpLocalization.pluralCategoryOne, + it: AmpLocalization.pluralCategoryOne, + nb: AmpLocalization.pluralCategoryOne, + no: AmpLocalization.pluralCategoryOne, + pt: AmpLocalization.pluralCategoryOne, + es: AmpLocalization.pluralCategoryOne, + sv: AmpLocalization.pluralCategoryOne, + tr: AmpLocalization.pluralCategoryOne, + ar: AmpLocalization.pluralCategoryArabic, + fr: AmpLocalization.pluralCategoryFrench, + iw: AmpLocalization.pluralCategoryHebrew, + pl: AmpLocalization.pluralCategoryPolish, + ro: AmpLocalization.pluralCategoryRomanian, + ru: AmpLocalization.pluralCategoryRussian, // Russian +}; +//# sourceMappingURL=amp-localization.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/foundation/dependencies/active-intent.js b/node_modules/@jet-app/app-store/tmp/src/foundation/dependencies/active-intent.js new file mode 100644 index 0000000..5270dd4 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/foundation/dependencies/active-intent.js @@ -0,0 +1,121 @@ +import { isSome } from "@jet/environment/types/optional"; +import { makeMetatype } from "@jet/environment/util/metatype"; +import { unreachable } from "../util/errors"; +export const ActiveIntentMetaType = makeMetatype("app-store:active-intent"); +/** + * Executes {@linkcode callback} with an {@linkcode AppStoreObjectGraph} that has been prepared with an + * {@linkcode ActiveIntent} dependency + * + * This will allow code running within {@linkcode callback} to access meta-data about the active + * {@linkcode Intent} without needing that information to be passed through every layer of the code + */ +export function withActiveIntent(objectGraph, intent, callback) { + const objectGraphWithActiveIntent = objectGraph.addingActiveIntent({ + previewPlatform: intent.platform, + }); + return callback(objectGraphWithActiveIntent); +} +/** + * Ephemeral storage of `Intent`-specific details that need to be accessible + * throughout the process of building that `Intent`'s result + */ +export class ActiveIntent { + constructor(implementation) { + this.implementation = implementation; + this.inferredPreviewPlatform = undefined; + } + /** + * Explicitly set a {@linkcode PreviewPlatform} for the active intent + * + * This might be a value derrived from an API response, in cases where the original `Intent` + * was not specific about which platform to display + */ + setInferredPreviewPlatform(platform) { + this.inferredPreviewPlatform = platform; + } + /** + * The {@linkcode PreviewPlatform} value of the current {@linkcode Intent} + */ + get previewPlatform() { + var _a; + return (_a = this.inferredPreviewPlatform) !== null && _a !== void 0 ? _a : this.implementation.previewPlatform; + } + /** + * The {@linkcode Platform} equivalent of the active {@linkcode PreviewPlatform} + * + * This translates the user-facing {@linkcode PreviewPlatform} into the "internal" + * Media API {@linkcode Platform} + */ + get platform() { + if (isSome(this.previewPlatform)) { + switch (this.previewPlatform) { + case "ipad": + case "iphone": + case "mac": + case "watch": + return this.previewPlatform; + case "tv": + return "appletv"; + case "vision": + return "realityDevice"; + default: + unreachable(this.previewPlatform); + } + } + return undefined; + } + /** + * The {@linkcode AttributePlatform} equivalent of the active {@linkcode PreviewPlatform} + * + * This translates the user-facing {@linkcode PreviewPlatform} into the "internal" + * {@linkcode AttributePlatform} + */ + get attributePlatform() { + if (isSome(this.previewPlatform)) { + switch (this.previewPlatform) { + case "iphone": + case "ipad": + return "ios"; + case "mac": + return "osx"; + case "tv": + return "appletvos"; + case "vision": + return "xros"; + case "watch": + return "watch"; + default: + unreachable(this.previewPlatform); + } + } + return undefined; + } + /** + * The {@linkcode AppPlatform} equivalent of the active {@linkcode PreviewPlatform} + * + * This translates the user-facing {@linkcode PreviewPlatform} into the "internal" + * {@linkcode AppPlatform} + */ + get appPlatform() { + if (isSome(this.previewPlatform)) { + switch (this.previewPlatform) { + case "ipad": + return "pad"; + case "iphone": + return "phone"; + case "mac": + return "mac"; + case "tv": + return "tv"; + case "vision": + return "vision"; + case "watch": + return "watch"; + default: + unreachable(this.previewPlatform); + } + } + return undefined; + } +} +//# sourceMappingURL=active-intent.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/foundation/dependencies/locale/locale-from-bag.js b/node_modules/@jet-app/app-store/tmp/src/foundation/dependencies/locale/locale-from-bag.js new file mode 100644 index 0000000..7790008 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/foundation/dependencies/locale/locale-from-bag.js @@ -0,0 +1,41 @@ +export class LocaleFromBag { + constructor(objectGraph) { + this.objectGraph = objectGraph; + } + /** + * The current {@link NormalizedStorefront | storefront} for the client + * + * This reads directly from the `Bag`, as that is the source of truth + * for locale information in the "native" clients + */ + get activeStorefront() { + // The type-cast here reflects the implicit assumption that any values + // belonging to the `Bag` have already been normalized + return this.objectGraph.bag.mediaCountryCode; + } + /** + * The current {@link NormalizedLanguage | language} for the client + * + * This reads directly from the `Bag`, as that is the source of truth + * for locale information in the "native" clients + */ + get activeLanguage() { + // The type-cast here reflects the implicit assumption that any values + // belonging to the `Bag` have already been normalized + return this.objectGraph.bag.language; + } + setActiveLocale(_locale) { + // Intentional no-op; "native" clients that use the `Bag` as the source + // of "locale" information do not allow for mutation of the "locale" + } + normalize(_unnormalizedLocale) { + return { + storefront: this.activeStorefront, + language: this.activeLanguage, + }; + } + deriveLocaleForUrl(locale) { + return locale; + } +} +//# sourceMappingURL=locale-from-bag.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/foundation/dependencies/locale/locale.js b/node_modules/@jet-app/app-store/tmp/src/foundation/dependencies/locale/locale.js new file mode 100644 index 0000000..d00bacc --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/foundation/dependencies/locale/locale.js @@ -0,0 +1,3 @@ +import { makeMetatype } from "@jet/environment/util/metatype"; +export const LocaleMetaType = makeMetatype("app-store:locale"); +//# sourceMappingURL=locale.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/foundation/dependencies/seo.js b/node_modules/@jet-app/app-store/tmp/src/foundation/dependencies/seo.js new file mode 100644 index 0000000..9def945 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/foundation/dependencies/seo.js @@ -0,0 +1,3 @@ +import { makeMetatype } from "@jet/environment/util/metatype"; +export const SEOMetaType = makeMetatype("app-store:seo"); +//# sourceMappingURL=seo.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/foundation/experimentation/app-store-experiments.js b/node_modules/@jet-app/app-store/tmp/src/foundation/experimentation/app-store-experiments.js new file mode 100644 index 0000000..e073481 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/foundation/experimentation/app-store-experiments.js @@ -0,0 +1,7 @@ +import { ExperimentCache } from "./experiment-cache"; +export function currentTreatmentIdForArea(objectGraph, experimentAreaId) { + var _a; + const experimentCache = objectGraph.optional(ExperimentCache.metatype); + return (_a = experimentCache === null || experimentCache === void 0 ? void 0 : experimentCache.currentTreatmentForExperiment(experimentAreaId)) === null || _a === void 0 ? void 0 : _a.identifier; +} +//# sourceMappingURL=app-store-experiments.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/foundation/experimentation/experiment-area-id.js b/node_modules/@jet-app/app-store/tmp/src/foundation/experimentation/experiment-area-id.js new file mode 100644 index 0000000..435c196 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/foundation/experimentation/experiment-area-id.js @@ -0,0 +1,10 @@ +export var ExperimentAreaId; +(function (ExperimentAreaId) { + ExperimentAreaId["ArcadeDownloadPackOnboarding"] = "Tf5Kjqz"; + ExperimentAreaId["CondensedTodayAds"] = "tBc9hUt"; + ExperimentAreaId["ProductPagePreloading"] = "m0henFo"; + ExperimentAreaId["ProductPageVariants"] = "fNPb5Km"; + ExperimentAreaId["ProductPageYMALRowCount"] = "isj11bm"; + ExperimentAreaId["SearchLandingPage"] = "WqjkRLH"; +})(ExperimentAreaId || (ExperimentAreaId = {})); +//# sourceMappingURL=experiment-area-id.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/foundation/experimentation/experiment-cache.js b/node_modules/@jet-app/app-store/tmp/src/foundation/experimentation/experiment-cache.js new file mode 100644 index 0000000..c0384ea --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/foundation/experimentation/experiment-cache.js @@ -0,0 +1,63 @@ +import { makeMetatype } from "@jet/environment/util/metatype"; +import { ExperimentAreaId } from "./experiment-area-id"; +export class ExperimentCache { + constructor() { + this.cachedTreatments = {}; + this.cachedRawTreatments = {}; + } + async loadTreatments(objectGraph) { + const experimentAreas = this.experimentAreasForPlatform(objectGraph); + if (experimentAreas.length > 0) { + try { + this.cachedRawTreatments = await objectGraph.treatmentStore.treatmentsForAreas(experimentAreas); + for (const [experimentAreaId, treatment] of Object.entries(this.cachedRawTreatments)) { + /// AMS Adds some debug metadata after the treatment identifier in internal builds we do not want this + /// for app store usage + const rawIdentifier = treatment.identifier; + this.cachedTreatments[experimentAreaId] = { + ...treatment, + identifier: rawIdentifier.split(":")[0], + }; + } + } + catch (error) { + objectGraph.console.error("Failed to load treatments", error); + } + } + } + currentTreatmentForExperiment(experimentAreaId) { + return this.cachedTreatments[experimentAreaId]; + } + createAb2Data() { + const ab2Data = []; + for (const [experimentAreaId, treatment] of Object.entries(this.cachedRawTreatments)) { + ab2Data.push({ + areaId: experimentAreaId, + bucket: -2, + treatmentId: treatment.identifier, + }); + } + return ab2Data; + } + experimentAreasForPlatform(objectGraph) { + const experimentAreas = []; + switch (objectGraph.client.deviceType) { + case "phone": + case "pad": + experimentAreas.push(...[ + ExperimentAreaId.ArcadeDownloadPackOnboarding, + ExperimentAreaId.CondensedTodayAds, + ExperimentAreaId.ProductPagePreloading, + ExperimentAreaId.ProductPageVariants, + ExperimentAreaId.ProductPageYMALRowCount, + ExperimentAreaId.SearchLandingPage, + ]); + break; + default: + break; + } + return experimentAreas; + } +} +ExperimentCache.metatype = makeMetatype("app-store:experimentCache"); +//# sourceMappingURL=experiment-cache.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/foundation/experimentation/product-page-experiments.js b/node_modules/@jet-app/app-store/tmp/src/foundation/experimentation/product-page-experiments.js new file mode 100644 index 0000000..7af0b43 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/foundation/experimentation/product-page-experiments.js @@ -0,0 +1,21 @@ +import * as appStoreExperiments from "./app-store-experiments"; +import { ExperimentAreaId } from "./experiment-area-id"; +export function shouldSkipProductPagePreload(objectGraph) { + const treatmentId = appStoreExperiments.currentTreatmentIdForArea(objectGraph, ExperimentAreaId.ProductPagePreloading); + return treatmentId === "3NepyQj01" /* ProductPagePreloadingExperimentTypes.SkipPreload */; +} +// --- Product Page Variants -- +/// Returns the treatment group id based on xp_ab value as string. +export function productVariantTreatmentId(objectGraph) { + return appStoreExperiments.currentTreatmentIdForArea(objectGraph, ExperimentAreaId.ProductPageVariants); +} +/** + * The number of rows that the YMAL shelf should use. + * @param objectGraph The object graph. + * @returns A number representing the number of rows in the YMAL shelf. + */ +export function ymalShelfNumberOfRows(objectGraph) { + const treatmentId = appStoreExperiments.currentTreatmentIdForArea(objectGraph, ExperimentAreaId.ProductPageYMALRowCount); + return treatmentId === "2S6U3Dq01" /* ProductPageYMALShelfRowNumberExperimentTypes.ThreeRow */ ? 3 : 2; +} +//# sourceMappingURL=product-page-experiments.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/foundation/experimentation/search-results-experiments.js b/node_modules/@jet-app/app-store/tmp/src/foundation/experimentation/search-results-experiments.js new file mode 100644 index 0000000..54654ee --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/foundation/experimentation/search-results-experiments.js @@ -0,0 +1,10 @@ +import * as serverData from "../../foundation/json-parsing/server-data"; +/** + * Returns the current treatment for editorial collections based on the meta blob. + * @param meta Meta blob in initial response. + */ +export function currentEditorialCollectionTreatment(objectGraph, meta) { + const showGridCard = serverData.asBooleanOrFalse(meta, "experiments.showGridCard"); + return showGridCard ? 0 /* EditorialCollectionExperimentType.Card */ : 1 /* EditorialCollectionExperimentType.Swoosh */; +} +//# sourceMappingURL=search-results-experiments.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/foundation/experimentation/today-ad-experiments.js b/node_modules/@jet-app/app-store/tmp/src/foundation/experimentation/today-ad-experiments.js new file mode 100644 index 0000000..52468f9 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/foundation/experimentation/today-ad-experiments.js @@ -0,0 +1,15 @@ +import * as appStoreExperiments from "./app-store-experiments"; +import { ExperimentAreaId } from "./experiment-area-id"; +/** + * Check whether the condensed treatment should be applied to the Today ad. + * @param objectGraph The object graph. + * @returns A boolean indicating if the treatment should be applied. + */ +export function shouldTodayAdBeCondensed(objectGraph) { + if (!objectGraph.client.isPhone) { + return false; + } + const treatmentId = appStoreExperiments.currentTreatmentIdForArea(objectGraph, ExperimentAreaId.CondensedTodayAds); + return treatmentId === "5pdfhju01" /* TodayAdCondensedDisplayExperimentTypes.Condensed */; +} +//# sourceMappingURL=today-ad-experiments.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/foundation/json-parsing/derived-data.js b/node_modules/@jet-app/app-store/tmp/src/foundation/json-parsing/derived-data.js new file mode 100644 index 0000000..fdf1f4e --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/foundation/json-parsing/derived-data.js @@ -0,0 +1,37 @@ +import * as validation from "@jet/environment/json/validation"; +import * as serverData from "./server-data"; +const DERIVED_CACHE_KEY = "_jet-internal:derived-data"; +export function value(data, key, generator) { + if (!data) { + return null; + } + return validation.context(key, () => { + let computedDataStore = data[DERIVED_CACHE_KEY]; + let returnValue = null; + if (!computedDataStore) { + computedDataStore = {}; + data[DERIVED_CACHE_KEY] = computedDataStore; + returnValue = generateValue(computedDataStore, key, generator); + } + else { + returnValue = serverData.traverse(computedDataStore, key); + if (!returnValue) { + returnValue = generateValue(computedDataStore, key, generator); + } + } + return returnValue; + }); +} +/** + * Computes an unknown value and saves it back into computedDataStore + * @param {JSONData} computedDataStore The dictionary in which to store the data + * @param {string} key The key to store it with + * @param {() => T} generator A function that will generate the value + * @returns {T} The value that was generated + */ +function generateValue(computedDataStore, key, generator) { + const generatedValue = generator(); + computedDataStore[key] = generatedValue; + return generatedValue; +} +//# sourceMappingURL=derived-data.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/foundation/json-parsing/server-data.js b/node_modules/@jet-app/app-store/tmp/src/foundation/json-parsing/server-data.js new file mode 100644 index 0000000..2ff6394 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/foundation/json-parsing/server-data.js @@ -0,0 +1,464 @@ +// +// server-data.ts +// AppStoreKit +// +// Created by Kevin MacWhinnie on 8/17/16. +// Copyright (c) 2016 Apple Inc. All rights reserved. +// +import * as validation from "@jet/environment/json/validation"; +import { isNothing } from "@jet/environment/types/optional"; +/** + * Returns the string representation of a given object path. + * @param path The object path to coerce to a string. + * @returns A string representation of `path`. + */ +export function objectPathToString(path) { + if (isNull(path)) { + return null; + } + else if (Array.isArray(path)) { + return path.join("."); + } + else { + return path; + } +} +const PARSED_PATH_CACHE = {}; +/** + * Traverse a nested JSON object structure, short-circuiting + * when finding `undefined` or `null` values. Usage: + * + * const object = {x: {y: {z: 42}}}; + * const meaningOfLife = serverData.traverse(object, 'x.y.z'); + * + * @param object The JSON object to traverse. + * @param path The path to search. If falsy, `object` will be returned without being traversed. + * @param defaultValue The object to return if the path search fails. + * @return The value at `path` if found; default value otherwise. + */ +export function traverse(object, path, defaultValue) { + if (object === undefined || object === null) { + return defaultValue; + } + if (!path) { + return object; + } + let components; + if (typeof path === "string") { + components = PARSED_PATH_CACHE[path]; + if (!components) { + // Fast Path: If the path contains only a single component, we can skip + // all of the work below here and speed up storefronts that + // don't have JIT compilation enabled. + if (!path.includes(".")) { + const value = object[path]; + if (value !== undefined && value !== null) { + return value; + } + else { + return defaultValue; + } + } + components = path.split("."); + PARSED_PATH_CACHE[path] = components; + } + } + else { + components = path; + } + let current = object; + for (const component of components) { + current = current[component]; + if (current === undefined || current === null) { + return defaultValue; + } + } + return current; +} +// endregion +// region Nullability +/** + * Returns a bool indicating whether or not a given object null or undefined. + * @param object The object to test. + * @return true if the object is null or undefined; false otherwise. + */ +export function isNull(object) { + return object === null || object === undefined; +} +/** + * Returns a bool indicating whether or not a given object is null or empty. + * @param object The object to test + * @return true if object is null or empty; false otherwise. + */ +export function isNullOrEmpty(object) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return isNull(object) || Object.keys(object).length === 0; +} +/** + * Returns a bool indicating whether or not a given object is non-null. + * @param object The object to test. + * @return true if the object is not null or undefined; false otherwise. + */ +export function isDefinedNonNull(object) { + return typeof object !== "undefined" && object !== null; +} +/** + * Returns a bool indicating whether or not a given object is non-null or empty. + * @param object The object to test. + * @return true if the object is not null or undefined and not empty; false otherwise. + */ +export function isDefinedNonNullNonEmpty(object) { + if (isNothing(object)) { + return false; + } + if (typeof object === "string") { + return object.length > 0; + } + else if (Array.isArray(object)) { + return object.length > 0; + } + else { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return Object.keys(object).length !== 0; + } +} +/** + * Returns a bool indicating whether or not a given object is non-null or empty. + * @param object The object to test. + * @return true if the object is not null or undefined and not empty; false otherwise. + */ +export function isSetDefinedNonNullNonEmpty(object) { + return isDefinedNonNull(object) && object.size > 0; +} +/** + * Checks if the passed string or number is a number + * + * @param value The value to check + * @return True if the value is an number, false if not + */ +export function isNumber(value) { + if (isNull(value)) { + return false; + } + let valueToCheck; + if (typeof value === "string") { + valueToCheck = parseInt(value); + } + else { + valueToCheck = value; + } + return !Number.isNaN(valueToCheck); +} +/** + * Checks if the value is a string + * + * @param value The value to check + * @return True if the value is a string, false if not + */ +export function isString(value) { + if (isNothing(value)) { + return false; + } + return typeof value === "string"; +} +/** + * Returns a bool indicating whether or not a given object is defined but empty. + * @param object The object to test. + * @return true if the object is not null and empty; false otherwise. + */ +export function isArrayDefinedNonNullAndEmpty(object) { + return isDefinedNonNull(object) && object.length === 0; +} +// endregion +// region Defaulting Casts +/** + * Check that a given object is an array, substituting an empty array if not. + * @param object The object to coerce. + * @param path The path to traverse on `object` to find an array. + * Omit this parameter if `object` is itself an array. + * @returns An untyped array. + */ +export function asArrayOrEmpty(object, path) { + var _a; + return (_a = asArray(object, path, [])) !== null && _a !== void 0 ? _a : []; +} +/** + * Check that a given object is a boolean, substituting the value `false` if not. + * @param object The object to coerce. + * @param path The path to traverse on `object` to find a boolean. + * Omit this parameter if `object` is itself a boolean. + * @returns A boolean from `object`, or defaults to `false`. + */ +export function asBooleanOrFalse(object, path) { + return asBooleanWithDefault(object, false, path); +} +/** + * Check that a given object is a boolean, substituting the value `false` if not. + * @param object The object to coerce. + * @param path The path to traverse on `object` to find a boolean. + * Omit this parameter if `object` is itself a boolean. + * @returns A boolean from `object`, or the provided default. + */ +export function asBooleanWithDefault(object, defaultValue, path) { + const target = traverse(object, path, null); + if (typeof target === "boolean") { + return target; + } + else { + if (!isNull(target)) { + validation.context("asBooleanWithDefault", () => { + validation.unexpectedType("defaultValue", "boolean", target, objectPathToString(path)); + }); + } + return defaultValue; + } +} +/** + * Safely coerce an object into a string. + * @param object The object to coerce. + * @param path The path to traverse on `object` to find a string. + * Omit this parameter if `object` is itself a string. + * @param policy The validation policy to use when resolving this value + * @returns A string from `object`, or `null` if `object` is null. + */ +export function asString(object, path, policy = "coercible") { + const target = traverse(object, path, null); + if (isNull(target)) { + return target; + } + else if (typeof target === "string") { + return target; + } + else { + // We don't consider arbitrary objects as convertable to strings even through they will result in some value + const coercedValue = typeof target === "object" ? null : String(target); + switch (policy) { + case "strict": { + validation.context("asString", () => { + validation.unexpectedType("coercedValue", "string", target, objectPathToString(path)); + }); + break; + } + case "coercible": { + if (isNull(coercedValue)) { + validation.context("asString", () => { + validation.unexpectedType("coercedValue", "string", target, objectPathToString(path)); + }); + } + break; + } + case "none": + default: { + break; + } + } + return coercedValue; + } +} +/** + * Safely coerce an object into a number. + * @param object The object to coerce. + * @param path The path to traverse on `object` to find a number. + * Omit this parameter if `object` is itself a number. + * @param policy The validation policy to use when resolving this value + * @returns A number from `object`, or `null` if `object` is null. + */ +export function asNumber(object, path, policy = "coercible") { + const target = traverse(object, path, null); + if (isNull(target) || typeof target === "number") { + return target; + } + else { + const coercedValue = Number(target); + switch (policy) { + case "strict": { + validation.context("asNumber", () => { + validation.unexpectedType("coercedValue", "number", target, objectPathToString(path)); + }); + break; + } + case "coercible": { + if (isNaN(coercedValue)) { + validation.context("asNumber", () => { + validation.unexpectedType("coercedValue", "number", target, objectPathToString(path)); + }); + return null; + } + break; + } + case "none": + default: { + break; + } + } + return coercedValue; + } +} +/** + * Safely coerce an object into a dictionary. + * @param object The object to coerce. + * @param path The path to traverse on `object` to find the dictionary. + * Omit this parameter if `object` is itself a dictionary. + * @param defaultValue The object to return if the path search fails. + * @returns A sub-dictionary from `object`, or `null` if `object` is null. + */ +export function asDictionary(object, path, defaultValue) { + const target = traverse(object, path, null); + if (target instanceof Object && !Array.isArray(target)) { + // Note: It's too expensive to actually validate this is a dictionary of { string : Type } at run time + return target; + } + else { + if (!isNull(target)) { + validation.context("asDictionary", () => { + validation.unexpectedType("defaultValue", "object", target, objectPathToString(path)); + }); + } + if (isDefinedNonNull(defaultValue)) { + return defaultValue; + } + return null; + } +} +/** + * Coerce an object into an array + * @param object The object to coerce. + * @param path The path to traverse on `object` to find an array. + * Omit this parameter if `object` is itself an array. + * @returns An untyped array. + */ +export function asArray(object, path, defaultValue) { + const target = traverse(object, path, null); + if (Array.isArray(target)) { + // Note: This is kind of a nasty cast, but I don't think we want to validate that everything is of type T + return target; + } + else { + if (!isNull(target)) { + validation.context("asArray", () => { + validation.unexpectedType("defaultValue", "array", target, objectPathToString(path)); + }); + } + if (isDefinedNonNull(defaultValue)) { + return defaultValue; + } + return null; + } +} +/** + * Safely coerce an object into a given interface. + * @param object The object to coerce. + * @param path The path to traverse on `object` to find a string. + * Omit this parameter if `object` is itself a string. + * @param defaultValue The object to return if the path search fails. + * @returns A sub-dictionary from `object`, or `null` if `object` is null. + */ +export function asInterface(object, path, defaultValue) { + return asDictionary(object, path, defaultValue); +} +/** + * Coerce an object into a boolean. + * @param object The object to coerce. + * @param path The path to traverse on `object` to find a boolean. + * Omit this parameter if `object` is itself a boolean. + * @param policy The validation policy to use when resolving this value + * @returns A boolean from `object`, or `null` if `object` is null. + * @note This is distinct from `asBooleanOrFalse` in that it doesn't default to false, + * and it tries to convert string boolean values into actual boolean types + */ +export function asBoolean(object, path, policy = "coercible") { + const target = traverse(object, path, null); + // Value was null + if (isNull(target)) { + return null; + } + // Value was boolean. + if (typeof target === "boolean") { + return target; + } + // Value was string. + if (typeof target === "string") { + if (target === "true") { + return true; + } + else if (target === "false") { + return false; + } + } + // Else coerce. + const coercedValue = Boolean(target); + switch (policy) { + case "strict": { + validation.context("asBoolean", () => { + validation.unexpectedType("coercedValue", "number", target, objectPathToString(path)); + }); + break; + } + case "coercible": { + if (isNull(coercedValue)) { + validation.context("asBoolean", () => { + validation.unexpectedType("coercedValue", "number", target, objectPathToString(path)); + }); + return null; + } + break; + } + case "none": + default: { + break; + } + } + return coercedValue; +} +/** + * Attempts to coerce the passed value to a JSONValue + * + * Note: due to performance concerns this does not perform a deep inspection of Objects or Arrays. + * + * @param value The value to coerce + * @return A JSONValue or null if value is not a valid JSONValue type + */ +export function asJSONValue(value) { + if (value === null || value === undefined) { + return null; + } + switch (typeof value) { + case "string": + case "number": + case "boolean": + return value; + case "object": + // Note: It's too expensive to actually validate this is an array of JSONValues at run time + if (Array.isArray(value)) { + return value; + } + // Note: It's too expensive to actually validate this is a dictionary of { string : JSONValue } at run time + return value; + default: + validation.context("asJSONValue", () => { + validation.unexpectedType("defaultValue", "JSONValue", typeof value); + }); + return null; + } +} +/** + * Attempts to coerce the passed value to JSONData + * + * @param value The value to coerce + * @return A JSONData or null if the value is not a valid JSONData object + */ +export function asJSONData(value) { + if (value === null || value === undefined) { + return null; + } + if (value instanceof Object && !Array.isArray(value)) { + // Note: It's too expensive to actually validate this is a dictionary of { string : Type } at run time + return value; + } + validation.context("asJSONValue", () => { + validation.unexpectedType("defaultValue", "object", typeof value); + }); + return null; +} +// endregion +//# sourceMappingURL=server-data.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/foundation/media/associations.js b/node_modules/@jet-app/app-store/tmp/src/foundation/media/associations.js new file mode 100644 index 0000000..7e2b088 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/foundation/media/associations.js @@ -0,0 +1,17 @@ +import * as serverData from "../json-parsing/server-data"; +/** + * @param data The media api data to find teh card in + * @returns The editorial-card data for this media api item, this will return the first associated card + */ +export function editorialCardsFromData(data) { + const editorialCards = serverData.asArrayOrEmpty(data, "meta.associations.editorial-cards.data"); + return editorialCards; +} +/** + * @param data The media api data to find teh card in + * @returns The editorial-card data for this media api item, this will return the first associated card + */ +export function editorialCardFromData(data) { + return serverData.asInterface(editorialCardsFromData(data)[0]); +} +//# sourceMappingURL=associations.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/foundation/media/attributes.js b/node_modules/@jet-app/app-store/tmp/src/foundation/media/attributes.js new file mode 100644 index 0000000..992a56e --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/foundation/media/attributes.js @@ -0,0 +1,149 @@ +import { isNothing } from "@jet/environment/types/optional"; +import * as serverData from "../json-parsing/server-data"; +// region Generic Attribute retrieval +// region Attribute retrieval +/** + * Retrieve the specified attribute from the data, coercing it to a JSONData dictionary + * + * @param data The data from which to retrieve the attribute. + * @param attributePath The path of the attribute. + * @param defaultValue The object to return if the path search fails. + * @returns The dictionary of data + */ +export function attributeAsDictionary(data, attributePath, defaultValue) { + if (isNothing(data)) { + return null; + } + return serverData.asDictionary(data.attributes, attributePath, defaultValue); +} +/** + * Retrieve the specified attribute from the data, coercing it to an Interface + * + * @param data The data from which to retrieve the attribute. + * @param attributePath The path of the attribute. + * @param defaultValue The object to return if the path search fails. + * @returns The dictionary of data as an interface + */ +export function attributeAsInterface(data, attributePath, defaultValue) { + return attributeAsDictionary(data, attributePath, defaultValue); +} +/** + * Retrieve the specified attribute from the data as an array, coercing to a JSONValue array + * + * @param data The data from which to retrieve the attribute. + * @param attributePath The path of the attribute. + * @returns {any[]} The attribute value as an array. + */ +export function attributeAsArray(data, attributePath) { + if (serverData.isNull(data)) { + return []; + } + return serverData.asArray(data.attributes, attributePath); +} +/** + * Retrieve the specified attribute from the data as an array, coercing to an empty array if the object is not an array. + * + * @param data The data from which to retrieve the attribute. + * @param attributePath The path of the attribute. + * @returns {any[]} The attribute value as an array. + */ +export function attributeAsArrayOrEmpty(data, attributePath) { + var _a; + return (_a = attributeAsArray(data, attributePath)) !== null && _a !== void 0 ? _a : []; +} +/** + * Retrieve the specified attribute from the data as a string. + * + * @param data The data from which to retrieve the attribute. + * @param attributePath The object path for the attribute. + * @param policy The validation policy to use when resolving this value. + * @returns {string} The attribute value as a string. + */ +export function attributeAsString(data, attributePath, policy = "coercible") { + if (serverData.isNull(data)) { + return null; + } + return serverData.asString(data.attributes, attributePath, policy); +} +/** + * Retrieve the specified attribute from the data as a boolean. + * + * @param data The data from which to retrieve the attribute. + * @param attributePath The path of the attribute. + * @param policy The validation policy to use when resolving this value. + * @returns {boolean} The attribute value as a boolean. + */ +export function attributeAsBoolean(data, attributePath, policy = "coercible") { + if (serverData.isNull(data)) { + return null; + } + return serverData.asBoolean(data.attributes, attributePath, policy); +} +/** + * Retrieve the specified attribute from the data as a boolean, which will be `false` if the attribute does not exist. + * + * @param data The data from which to retrieve the attribute. + * @param attributePath The path of the attribute. + * @returns {boolean} The attribute value as a boolean, coercing to `false` if the value is not present.. + */ +export function attributeAsBooleanOrFalse(data, attributePath) { + if (serverData.isNull(data)) { + return false; + } + return serverData.asBooleanOrFalse(data.attributes, attributePath); +} +/** + * Retrieve the specified attribute from the data as a number. + * + * @param data The data from which to retrieve the attribute. + * @param attributePath The path of the attribute. + * @param policy The validation policy to use when resolving this value. + * @returns {boolean} The attribute value as a number. + */ +export function attributeAsNumber(data, attributePath, policy = "coercible") { + if (serverData.isNull(data)) { + return null; + } + return serverData.asNumber(data.attributes, attributePath, policy); +} +export function hasAttributes(data) { + return !serverData.isNull(serverData.asDictionary(data, "attributes")); +} +/** + * The canonical way to detect if an item from Media API is hydrated or not. + * + * @param data The data from which to retrieve the attributes. + */ +export function isNotHydrated(data) { + return !hasAttributes(data); +} +// region Custom Attributes +/** + * Performs conversion for a custom variant of given attribute, if any are available. + * @param attribute Attribute to get custom attribute key for, if any. + */ +export function attributeKeyAsCustomAttributeKey(attribute) { + return customAttributeMapping[attribute]; +} +/** + * Whether or not given custom attributes key allows fallback to default page with AB testing treatment within a nondefault page. + * This is to allow AB testing to affect only icons within custom product pages. + */ +export function attributeAllowsNonDefaultTreatmentInNonDefaultPage(customAttribute) { + return customAttribute === "customArtwork" || customAttribute === "customIconArtwork"; // Only the icon artwork. +} +/** + * Defines mapping of attribute to custom attribute. + */ +const customAttributeMapping = { + artwork: "customArtwork", + iconArtwork: "customIconArtwork", + screenshotsByType: "customScreenshotsByType", + promotionalText: "customPromotionalText", + videoPreviewsByType: "customVideoPreviewsByType", + customScreenshotsByTypeForAd: "customScreenshotsByTypeForAd", + customVideoPreviewsByTypeForAd: "customVideoPreviewsByTypeForAd", + customDeepLink: "customDeepLink", +}; +// endregion +//# sourceMappingURL=attributes.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/foundation/media/data-fetching.js b/node_modules/@jet-app/app-store/tmp/src/foundation/media/data-fetching.js new file mode 100644 index 0000000..445f42f --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/foundation/media/data-fetching.js @@ -0,0 +1,631 @@ +import { isNothing, isSome } from "@jet/environment/types/optional"; +import * as serverData from "../json-parsing/server-data"; +import * as client from "../wrappers/client"; +export function allPlatforms(objectGraph, platformsToExclude) { + const platforms = new Set(); + platforms.add("iphone"); + platforms.add("ipad"); + platforms.add("appletv"); + platforms.add("mac"); + platforms.add("watch"); + if (objectGraph.client.isVision || objectGraph.bag.enableVisionPlatform) { + platforms.add("realityDevice"); + } + if (isSome(platformsToExclude)) { + for (const platform of platformsToExclude) { + platforms.delete(platform); + } + } + return Array.from(platforms); +} +export function defaultPlatformForClient(objectGraph) { + if (objectGraph.client.isCompanionVisionApp) { + // The Vision companion app always prefers visionOS assets + return "realityDevice"; + } + switch (objectGraph.client.deviceType) { + case "phone": + return "iphone"; + case "pad": + return "ipad"; + case "tv": + return "appletv"; + case "mac": + return "mac"; + case "watch": + return "watch"; + case "vision": + return "realityDevice"; + case "web": + return "web"; + default: + return null; + } +} +/** + * Returns the layout size controlling the number of rows to display. + * @param {AppStoreObjectGraph} objectGraph Object graph to get the client device type. + * @returns {number} Layout size. + */ +export function defaultLayoutSize(objectGraph) { + return objectGraph.client.isPhone ? 2 : 1; +} +/** + * Returns the sparse count controlling the number of shelves to hydrate. + * @param {AppStoreObjectGraph} objectGraph Object graph to get the client device type. + * @returns {number} Sparse count. + */ +export function defaultSparseCountForClient(objectGraph) { + switch (objectGraph.client.deviceType) { + case "phone": + return 4; + case "pad": + return 5; + case "tv": + return 6; + case "mac": + return 5; + case "watch": + return 10; + default: + return null; + } +} +/** + * Returns the sparse limit controlling the number of items to hydrate per shelve. + * @param {AppStoreObjectGraph} objectGraph Object graph to get the client device type. + * @returns {number} Sparse limit. + */ +export function defaultSparseLimitForClient(objectGraph) { + switch (objectGraph.client.deviceType) { + case "phone": + return 9; + case "pad": + return 12; + case "tv": + return 15; + case "mac": + return 15; + case "watch": + return 3; + case "web": + return 12; + default: + return null; + } +} +/** + * Get the list of all platforms excluding the current platform + * @returns {Platform[]} + */ +export function defaultAdditionalPlatformsForClient(objectGraph) { + switch (objectGraph.host.clientIdentifier) { + case "com.apple.TVAppStore.AppStoreTopShelfExtension": + case "com.apple.Arcade.ArcadeTopShelfExtension": + case "com.apple.AppStore.Widgets": + // Skip additional platforms for top shelf extension due to memory constraint. + return []; + default: { + const currentPlatform = defaultPlatformForClient(objectGraph); + return allPlatforms(objectGraph, isSome(currentPlatform) ? new Set([currentPlatform]) : undefined); + } + } +} +/** + * Get the product page reviews limit for a client. + * @returns {number} The limit to use. + */ +export function defaultProductPageReviewsLimitForClient(objectGraph) { + switch (objectGraph.client.deviceType) { + case "phone": + return 6; + case "pad": + return 10; + case "mac": + return 12; + case "vision": + return 10; + default: + return 8; + } +} +/// Returns the context param to use for a given grouping request. +/// This is leveraged for switching grouping tab root in specific configurations. +export function defaultGroupingContextForClient(objectGraph) { + let context = null; + if (objectGraph.host.clientIdentifier === client.messagesIdentifier) { + // Messages Grouping (iOS) + context = "messages"; + } + else if (objectGraph.host.clientIdentifier === client.watchIdentifier) { + // Bridge App Store (iOS) + context = "watch"; + } + else if (objectGraph.client.isWatch && objectGraph.client.isTinkerWatch) { + // Tinker App Store (watchOS) + context = "tinker"; + } + return context; +} +// Nothing on the App Store is rated 1000+, so a value this high essentially +// indicates that content restrictions are disabled. +export const ageRestrictionsDisabledThreshold = 1000; +export class Request { + constructor(objectGraph, param, enableMixedCatalog, supplementaryMetadataAssociations) { + var _a; + /// The resource types used in a mixed contents call + this.contentsResourceTypes = new Set(); + /// The ID(s) associated with the resource type + this.ids = new Set(); + /// IDs associated with specific resource types, used for mixed catalog requests + this.idsByResourceType = new Map(); + /// The original ordering of the ids in this reqeust if there is a mixed catalog request + this.originalOrdering = []; + this.relationshipIncludes = new Set(); + // Contents of `extend` param + this.attributeIncludes = new Set(); + this.platform = null; + /// The paths to additional metadata that need to be fetched for mixed media requests + this.supplementaryMetadataAssociations = []; + this.additionalPlatforms = new Set(); + this.additionalQuery = {}; + this.relationshipLimits = {}; + this.searchTerm = null; + this.searchTypes = []; + /// Whether we are searching watch or messages store. + this.context = null; + /// Whether or not to use custom extend attributes, instead of using `attributeIncludes` as-is. + this.useCustomAttributes = false; + /// An optional country code override for constructing the URL. Used in very specific regulatory scenarios where the bag country code cannot be relied upon. + this.countryCodeOverride = undefined; + this.objectGraph = objectGraph; + this.platform = defaultPlatformForClient(objectGraph); + this.isMixedMediaRequest = enableMixedCatalog !== null && enableMixedCatalog !== void 0 ? enableMixedCatalog : false; + this.supplementaryMetadataAssociations = supplementaryMetadataAssociations !== null && supplementaryMetadataAssociations !== void 0 ? supplementaryMetadataAssociations : []; + this.includeAppBinaryTraitsAttribute = objectGraph.client.isiOS; + // By default, the `platform` for web defaults to whatever the `activeIntent` is (e.g. "mac"), + // but this can be overridden when calling `setPreviewPlatform` with the request, which + // will set the `platform` to `web` and `previewPlatform` will be the `activeIntent` platform. + if ((_a = objectGraph.activeIntent) === null || _a === void 0 ? void 0 : _a.platform) { + this.platform = objectGraph.activeIntent.platform; + } + if (serverData.isNullOrEmpty(param)) { + return; + } + if (typeof param === "string") { + this.href = param; + } + else if (Array.isArray(param)) { + this.withDataItems(param, supplementaryMetadataAssociations, enableMixedCatalog); + } + } + /** + * Adds a data object to an idsByResourceType mapping. + * @param data The data object + * @returns An updated {resource-type: IDs} map, which includes the data object + */ + addDataToIDsByResourceType(data) { + const resourceType = data.type; + const id = data.id; + let ids = this.idsByResourceType.get(resourceType); + if (isNothing(ids)) { + ids = new Set(); + } + ids.add(id); + this.idsByResourceType.set(resourceType, ids); + } + forType(type) { + this.resourceType = type; + return this; + } + /** + * This method is used to add data items to the request. It is used to build the request for the mixed catalog / catalog + * + * @param dataItems The data items to add to the request + * @param supplementaryMetadataAssociations The metadata paths to add to the request + * @param enableMixedCatalog Whether to allow mixed catalog requests + * @returns The modified Request + */ + withDataItems(dataItems, supplementaryMetadataAssociations, enableMixedCatalog) { + if (dataItems.length === 0) { + return this; + } + this.isMixedMediaRequest = this.isMixedMediaRequest || (enableMixedCatalog !== null && enableMixedCatalog !== void 0 ? enableMixedCatalog : false); + for (const data of dataItems) { + // Track all IDs in a single set, for the case where we use the contents endpoint. + this.ids.add(data.id); + // Track all IDs by resource type, for either the case where we ultimately have just + // one resource type, or where we used the mixed catalog endpoint. + this.addDataToIDsByResourceType(data); + // If we have any additional metadata paths that need fetching, we now add them + // as additional resource types for the mixed catalog endpoint. + if (isSome(enableMixedCatalog) && + enableMixedCatalog && + isSome(supplementaryMetadataAssociations) && + supplementaryMetadataAssociations.length > 0) { + for (const association of supplementaryMetadataAssociations) { + const metadataItems = extractMetaAssociationFromData(association, data); + if (isSome(metadataItems) && metadataItems.length > 0) { + metadataItems.forEach((metadataItem) => { + this.addDataToIDsByResourceType(metadataItem); + }); + } + } + } + } + // Now that we have collated all IDs and resource types, we can assign them to different + // properties depending on the number of resource types we need. + if (this.idsByResourceType.size === 1) { + // We only have one list of IDs for a single resource type, so we just take the first one + this.resourceType = this.idsByResourceType.keys().next().value; + this.isMixedMediaRequest = false; + } + else if (this.idsByResourceType.size > 1 && !this.isMixedMediaRequest) { + this.resourceType = "contents"; + this.contentsResourceTypes = new Set(Array.from(this.idsByResourceType.keys())); + } + this.originalOrdering.push(...[...dataItems]); + return this; + } + /** + * Add a single id to this request, this should only be used when fetching a single resource + * @param id The single ID to add to the request + * @param type The type of the resource + * @returns The modified Request + */ + withIdOfType(id, type) { + return this.withDataItems([{ id, type }]); + } + /** + * Add a list of ids to this request, this will add these ids to the mapping of resource types + * @param ids The IDs to add to the request + * @param type The type of the resource + * @returns The modified Request + */ + withIdsOfType(ids, type) { + return this.withDataItems(ids.map((id) => ({ id, type }))); + } + includingRelationships(relationships) { + for (const relationship of relationships) { + this.relationshipIncludes.add(relationship); + } + return this; + } + includingScopedRelationships(scopedDataType, relationshipsToAdd) { + // Lazy init top-level property + if (!this.scopedRelationshipIncludes) { + this.scopedRelationshipIncludes = new Map(); + } + // Retrieve existing scoped relationship. Lazy init if needed. + let scopedRelationship = this.scopedRelationshipIncludes.get(scopedDataType); + if (!scopedRelationship) { + scopedRelationship = new Set(); + } + // Update + for (const newRelationship of relationshipsToAdd) { + scopedRelationship.add(newRelationship); + } + this.scopedRelationshipIncludes.set(scopedDataType, scopedRelationship); + return this; + } + includingMetaKeys(scopedDataType, keysToAdd) { + // Lazy init top-level property + if (!this.metaIncludes) { + this.metaIncludes = new Map(); + } + // Retrieve existing inclusion. Lazy init if needed. + let scopedMeta = this.metaIncludes.get(scopedDataType); + if (!scopedMeta) { + scopedMeta = new Set(); + } + // Update + for (const newKey of keysToAdd) { + scopedMeta.add(newKey); + } + this.metaIncludes.set(scopedDataType, scopedMeta); + return this; + } + includingViews(viewsToAdd) { + // Lazy init top-level property + if (!this.viewsIncludes) { + this.viewsIncludes = new Set(); + } + for (const view of viewsToAdd) { + this.viewsIncludes.add(view); + } + return this; + } + includingKindsKeys(scopedDataType, keysToAdd) { + // Lazy init top-level property + if (!this.kindIncludes) { + this.kindIncludes = new Map(); + } + // Retrieve existing inclusion. Lazy init if needed. + let scopedMeta = this.kindIncludes.get(scopedDataType); + if (!scopedMeta) { + scopedMeta = new Set(); + } + // Update + for (const newKey of keysToAdd) { + scopedMeta.add(newKey); + } + this.kindIncludes.set(scopedDataType, scopedMeta); + return this; + } + includingAssociateKeys(scopedDataType, keysToAdd) { + // Lazy init top-level property + if (!this.associateIncludes) { + this.associateIncludes = new Map(); + } + // Retrieve existing inclusion. Lazy init if needed. + let scopedAssociate = this.associateIncludes.get(scopedDataType); + if (!scopedAssociate) { + scopedAssociate = new Set(); + } + // Update + for (const newKey of keysToAdd) { + scopedAssociate.add(newKey); + } + this.associateIncludes.set(scopedDataType, scopedAssociate); + return this; + } + /** + * Include the relationships needed for an upsell. + * @param requiresScopedInclude A flag indicating whether the include should be "scoped". This will need to be the + * case when the relationships we are including is on a request for a separate primary resource type. For example, + * if we are looking to have the upsell relationship included in an EI (Today card, for example), this needs to be + * scoped. However, if we are fetching a grouping directly and need the upsell included, this should *not* be scoped. + */ + includingRelationshipsForUpsell(requiresScopedRelationshipInclude) { + const relationship = "marketing-items"; + if (requiresScopedRelationshipInclude) { + // Lazy init top-level property + if (!this.scopedRelationshipIncludes) { + this.scopedRelationshipIncludes = new Map(); + } + // Retrieve existing scoped relationship. Lazy init if needed. + let scopedRelationship = this.scopedRelationshipIncludes.get("editorial-items"); + if (!scopedRelationship) { + scopedRelationship = new Set(); + } + // Update + scopedRelationship.add(relationship); + this.scopedRelationshipIncludes.set("editorial-items", scopedRelationship); + } + else { + this.relationshipIncludes.add(relationship); + } + // In order to get metrics metadata stiched in to the marketing item relationship, we have to include it in our + // request. + if (relationship === "marketing-items") { + if (!this.metaIncludes) { + this.metaIncludes = new Map(); + } + // Retrieve existing inclusion. Lazy init if needed. + let scopedMeta = this.metaIncludes.get("marketing-items"); + if (!scopedMeta) { + scopedMeta = new Set(); + } + scopedMeta.add("metrics"); + this.metaIncludes.set("marketing-items", scopedMeta); + } + return this; + } + includingAttributes(attributes) { + for (const attribute of attributes) { + this.attributeIncludes.add(attribute); + } + return this; + } + includingScopedAttributes(resourceType, attributesToAdd) { + // Lazy init top-level property + if (!this.scopedAttributeIncludes) { + this.scopedAttributeIncludes = new Map(); + } + // Retrieve existing scoped relationship. Lazy init if needed. + let attributesForResourceType = this.scopedAttributeIncludes.get(resourceType); + if (!attributesForResourceType) { + attributesForResourceType = new Set(); + } + // Update + for (const attribute of attributesToAdd) { + attributesForResourceType.add(attribute); + } + this.scopedAttributeIncludes.set(resourceType, attributesForResourceType); + return this; + } + /** + * Adds an age restriction to the request. ManagedConfiguration provides + * this value on the client, which maps to JRPurpleRating.clientIdentifier. + * + * @returns {Request} The updated request + */ + includingAgeRestrictions() { + const maxAppContentRating = this.objectGraph.client.maxAppContentRating; + if (maxAppContentRating < ageRestrictionsDisabledThreshold) { + this.ageRestriction = maxAppContentRating; + } + return this; + } + includingAdditionalPlatforms(additionalPlatforms) { + for (const platform of additionalPlatforms) { + this.additionalPlatforms.add(platform); + } + return this; + } + includingScopedAvailableIn(resourceType, valuesToAdd) { + // Lazy init top-level property + if (!this.scopedAvailableInIncludes) { + this.scopedAvailableInIncludes = new Map(); + } + // Retrieve existing scoped relationship. Lazy init if needed. + let valuesForResourceType = this.scopedAvailableInIncludes.get(resourceType); + if (!valuesForResourceType) { + valuesForResourceType = new Set(); + } + // Update + for (const value of valuesToAdd) { + valuesForResourceType.add(value); + } + this.scopedAvailableInIncludes.set(resourceType, valuesForResourceType); + return this; + } + /** + * Include the sparse limit needed for a specific resource type. + * @param {media.Type} resourceType Resource type to use for the sparse limit. + * @param {number} value Value to set as the sparse limit. + * @returns {Request} Updated request. + */ + includingScopedSparseLimit(resourceType, value) { + // Lazy init top-level property + if (!this.scopedSparseLimit) { + this.scopedSparseLimit = new Map(); + } + this.scopedSparseLimit.set(resourceType, value); + return this; + } + addingQuery(key, value) { + if (isSome(value)) { + this.additionalQuery[key] = value; + } + else { + delete this.addingQuery[key]; + } + return this; + } + /** + * @param query The additional query values to add + * @returns The modified Reqeust + */ + addingQueryValues(query) { + this.additionalQuery = { + ...this.addingQuery, + ...query, + }; + return this; + } + addingRelationshipLimit(relationship, limit) { + this.relationshipLimits[relationship] = limit; + return this; + } + withSearchTerm(term) { + this.searchTerm = term; + return this; + } + searchingOverTypes(types) { + for (const type of types) { + this.searchTypes.push(type); + } + return this; + } + addingContext(context) { + this.context = context; + return this; + } + includingMacOSCompatibleIOSAppsWhenSupported(verifiedBadgeOnly = false) { + if (this.objectGraph.appleSilicon.isSupportEnabled) { + if (!verifiedBadgeOnly) { + this.enablingFeature("macOSCompatibleIOSApps"); + } + this.includingScopedAttributes("apps", ["isVerifiedForAppleSiliconMac"]); + } + return this; + } + /// Mark a request to include (or exclude) appBinaryTraits attributes. + includingAppBinaryTraitsAttribute(includeAppBinaryTraitsAttribute = true) { + this.includeAppBinaryTraitsAttribute = includeAppBinaryTraitsAttribute; + return this; + } + /** + * Mark a request to use custom attributes. + * This triggers usage of `customXYZ` extend attributes in url-builder for specific extend params + * e.g. `artwork`, `customArtwork`. + */ + usingCustomAttributes(useCustomAttributes) { + this.useCustomAttributes = useCustomAttributes; + return this; + } + alwaysUseIdsAsQueryParam(value) { + this.useIdsAsQueryParam = value; + return this; + } + attributingTo(canonicalUrl) { + this.canonicalUrl = canonicalUrl; + return this; + } + withFilter(type, value) { + this.filterType = type; + this.filterValue = value; + return this; + } + withLimit(limit) { + this.limit = limit; + return this; + } + withSparseLimit(sparseLimit) { + if (sparseLimit !== null) { + this.sparseLimit = sparseLimit; + } + return this; + } + withSparseCount(sparseCount) { + if (sparseCount !== null) { + this.sparseCount = sparseCount; + } + return this; + } + enablingFeature(feature) { + if (!this.enabledFeatures) { + this.enabledFeatures = []; + } + this.enabledFeatures.push(feature); + return this; + } + enablingFeatures(features) { + if (!this.enabledFeatures) { + this.enabledFeatures = []; + } + this.enabledFeatures.push(...features); + return this; + } + /** + * Limit the request to include only certain fields. + * This should only be used in very select use-cases, where: + * - There is no possibility of data being used for other purposes, e.g. sidpack + * - All consumer of this data use it in the same narrow scope, e.g. requesting a set of icons for displaying without metadata only. + * @param fields Set of fields to limit to + */ + asPartialResponseLimitedToFields(fields) { + this.fields = fields; + return this; + } + includesResourceType(resourceType) { + if (this.resourceType === resourceType) { + return true; + } + if (serverData.isDefinedNonNull(this.contentsResourceTypes)) { + return this.contentsResourceTypes.has(resourceType); + } + return false; + } + withCountryCodeOverride(countryCodeOverride) { + this.countryCodeOverride = countryCodeOverride; + return this; + } +} +/** + * Extracts the metadata association from the data. + * @param association The association to extract + * @param data The data to extract from + */ +export function extractMetaAssociationFromData(association, data) { + if (serverData.isNullOrEmpty(data)) { + return null; + } + const associationData = serverData.asArrayOrEmpty(data, `meta.associations.${association}.data`); + if (isNothing(associationData)) { + return null; + } + return [...associationData]; +} +//# sourceMappingURL=data-fetching.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/foundation/media/data-structure.js b/node_modules/@jet-app/app-store/tmp/src/foundation/media/data-structure.js new file mode 100644 index 0000000..cafcce8 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/foundation/media/data-structure.js @@ -0,0 +1,84 @@ +/** + * Created by joel on 1/25/18. + */ +import * as serverData from "../json-parsing/server-data"; +import { ResponseMetadata } from "../network/network"; +export function dataFromDataContainer(objectGraph, dataContainer) { + const dataArray = serverData.asArrayOrEmpty(dataContainer, "data"); + if (dataArray.length > 1) { + objectGraph.console.warn("tried to extract data from container but more than one member present"); + } + if (dataArray.length !== 1) { + return null; + } + return dataArray[0]; +} +export function dataCollectionFromDataContainer(dataContainer) { + return serverData.asArrayOrEmpty(dataContainer, "data"); +} +/** + * Check whether or not a server vended `Data` object is hydrated or not. + * @param {Data} data to check if hydrated. + */ +export function isDataHydrated(data) { + return serverData.isDefinedNonNull(data.attributes); +} +/** + * Check whether or not a entire data collection contains elements that are fully hydrated. + * @param {Data[]} dataArray Data array to check. + */ +export function isDataCollectionHydrated(dataCollection) { + // Iterate from the back to determine if fully hydrated faster - unhydrated elements tend to be latter elements. + const lastIndex = dataCollection.length - 1; + for (let index = lastIndex; index >= 0; index--) { + const data = dataCollection[index]; + if (!isDataHydrated(data)) { + return false; + } + } + return true; +} +/** + * Check whether or not a entire data collection at least has 1 element that is fully hydrated. + * @param {Data[]} dataArray Data array to check. + */ +export function isDataCollectionPartiallyHydrated(dataCollection) { + for (const data of dataCollection) { + if (isDataHydrated(data)) { + return true; + } + } + return false; +} +/** + * Check whether or not a today data module represents a today page on the heuristic that: + * - The "Today" modules have labels with value 'TodayForApps' as opposed to `WhatYouMissed` + * - Date marker "Today" modules have a 'date' field. + */ +export function isModuleTodayForApps(todayModule) { + const todayModuleId = "TodayForApps"; + const date = serverData.traverse(todayModule, "date"); + return todayModule.label === todayModuleId || serverData.isDefinedNonNull(date); +} +/** + * Get chart results from a server response. Always returns chart segments with valid data + */ +export function chartResultsFromServerResponse(response) { + const resultsArray = serverData.asArrayOrEmpty(response, "results.apps"); + return resultsArray.filter((segment) => { + return !serverData.isNull(segment.data); + }); +} +export function dataCollectionFromResultsListContainer(resultsListContainer) { + return serverData.asArrayOrEmpty(resultsListContainer, "results.contents"); +} +/** + * Get the metrics dictionary included in the meta data of a mediaApi Data object. + * @param {MetaDataProviding} metaDataProvidingObject + * @returns {MapLike<string> | null} + */ +export function metricsFromMediaApiObject(metaDataProvidingObject) { + return serverData.asDictionary(metaDataProvidingObject, "meta.metrics"); +} +// endregion +//# sourceMappingURL=data-structure.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/foundation/media/network.js b/node_modules/@jet-app/app-store/tmp/src/foundation/media/network.js new file mode 100644 index 0000000..2b01d40 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/foundation/media/network.js @@ -0,0 +1,304 @@ +/** + * Created by joel on 20/2/2018. + * + * This `network.ts` is the Media API arm of the netwok fetch requests. + * It is built on `Network` object and provides standard functionality specific for MAPI. + * + * @see `src/network.ts` for fetching from non-Media API endpoints + */ +import { isNothing, isSome } from "@jet/environment/types/optional"; +import * as serverData from "../json-parsing/server-data"; +import { MetricsIdentifierType } from "../metrics/metrics-identifiers-cache"; +import { ResponseMetadata } from "../network/network"; +import * as urls from "../network/urls"; +import { extractMetaAssociationFromData } from "./data-fetching"; +import * as urlBuilder from "./url-builder"; +const secondaryUserIdHeaderName = "X-Apple-AppStore-UserId-Secondary"; +/** + * Implements the MAPI fetch, building URL from MAPI Request and opaquely managing initial token request and refreshes. + * + * @param {Request} request MAPI Request to fetch with. + * @param {FetchOptions} options? FetchOptions for the MAPI request. + * @returns {Promise<Type>} Promise resolving to some type for given MAPI request. + */ +export async function fetchData(objectGraph, request, options) { + var _a; + const startTime = Date.now(); + const token = await objectGraph.mediaToken.refreshToken(); + const fetchTimingMetricsBuilder = objectGraph.fetchTimingMetricsBuilder; + const requestOptions = options !== null && options !== void 0 ? options : {}; + const personalizationIdentifier = (_a = objectGraph.personalizationMetricsIdentifiersCache) === null || _a === void 0 ? void 0 : _a.getMetricsIdForType(MetricsIdentifierType.user); + if (isSome(personalizationIdentifier)) { + if (isSome(requestOptions === null || requestOptions === void 0 ? void 0 : requestOptions.headers)) { + requestOptions.headers[secondaryUserIdHeaderName] = personalizationIdentifier; + } + else { + requestOptions.headers = { + [secondaryUserIdHeaderName]: personalizationIdentifier, + }; + } + } + const response = await fetchWithToken(objectGraph, request, token, requestOptions, false, fetchTimingMetricsBuilder); + const endTime = Date.now(); + if (request.canonicalUrl) { + response[ResponseMetadata.requestedUrl] = request.canonicalUrl; + } + const roundTripTimeIncludingWaiting = endTime - startTime; + if (roundTripTimeIncludingWaiting > 500) { + const longFetchUrl = urlBuilder.buildURLFromRequest(objectGraph, request).toString(); + objectGraph.console.warn("Fetch took too long (" + roundTripTimeIncludingWaiting.toString() + "ms) " + longFetchUrl); + } + return response; +} +export function redirectParametersInUrl(objectGraph, url) { + const redirectURLParams = objectGraph.bag.redirectUrlWhitelistedQueryParams; + return redirectURLParams.filter((param) => { var _a; return serverData.isDefinedNonNull((_a = url.query) === null || _a === void 0 ? void 0 : _a[param]); }); +} +/** + * Given a built URL, token, and options, calls into native networking APIs to fetch content. + * + * @param {string} url URL to fetch data from. + * @param {string} request The original request used to build this url. + * @param {string} token MAPI token key. + * @param {FetchOptions} options Fetch options for MAPI requests. + * @param {boolean} isRetry flag indicating whether this is a fetch retry following a 401 request, and media token was refreshed. + * @returns {Promise<Type>} Promise resolving to some type for given MAPI request. + */ +async function fetchWithToken(objectGraph, request, token, options = {}, isRetry = false, fetchTimingMetricsBuilder) { + var _a, _b; + const originalUrl = urlBuilder.buildURLFromRequest(objectGraph, request).toString(); + // Removes all affiliate/redirect params for caching (https://connectme.apple.com/docs/DOC-577671) + const filteredURL = new urls.URL(originalUrl); + const redirectParameters = redirectParametersInUrl(objectGraph, filteredURL); + for (const param of redirectParameters) { + filteredURL.removeParam(param); + } + const filteredUrlString = filteredURL.toString(); + let headers = options.headers; + if (!headers) { + headers = {}; + } + headers["Authorization"] = "Bearer " + token; + const response = await objectGraph.network.fetch({ + url: filteredUrlString, + headers: headers, + method: options.method, + body: options.requestBodyString, + timeout: options.timeout, + }); + try { + if (response.status === 401 || response.status === 403) { + if (isRetry) { + throw Error("We refreshed the token but we still get 401 from the API"); + } + objectGraph.mediaToken.resetToken(); + return await objectGraph.mediaToken.refreshToken().then(async (newToken) => { + // Explicitly re-fetch with the original request so logging and metrics are correct + return await fetchWithToken(objectGraph, request, newToken, options, true, fetchTimingMetricsBuilder); + }); + } + else if (response.status === 404) { + // item is not available in this storefront or perhaps not at all + throw noContentError(); + } + else if (!response.ok) { + const error = new NetworkError(`Bad Status code ${response.status} for ${filteredUrlString}, original ${originalUrl}`); + error.statusCode = response.status; + throw error; + } + const parser = (resp) => { + var _a; + const parseStartTime = Date.now(); + let result; + if (serverData.isNull(resp.body) || resp.body === "") { + if (resp.status === 204) { + // 204 indicates a success, but the response will typically be empty + // Create a fake result so that we don't throw an error when JSON parsing + const emptyData = {}; + result = emptyData; + } + else { + throw noContentError(); + } + } + else { + try { + result = JSON.parse(resp.body); + } + catch (e) { + let errorMessage = e.message; + if (["debug", "internal"].includes(objectGraph.client.buildType)) { + errorMessage = `${e.message}, body: ${resp.body}`; + } + throw new JSONParseError(errorMessage); + } + } + const parseEndTime = Date.now(); + if (result) { + result[ResponseMetadata.pageInformation] = serverData.asJSONData(getPageInformationFromResponse(objectGraph, resp)); + if (resp.metrics.length > 0) { + const metrics = { + ...resp.metrics[0], + parseStartTime: parseStartTime, + parseEndTime: parseEndTime, + }; + result[ResponseMetadata.timingValues] = metrics; + } + else { + const fallbackMetrics = { + pageURL: resp.url, + parseStartTime, + parseEndTime, + }; + result[ResponseMetadata.timingValues] = fallbackMetrics; + } + result[ResponseMetadata.contentMaxAge] = getContentTimeToLiveFromResponse(objectGraph, resp); + // If we have an empty data object, throw a 204 (No Content). + if (Array.isArray(result.data) && + serverData.isArrayDefinedNonNullAndEmpty(result.data) && + !serverData.asBooleanOrFalse(options.allowEmptyDataResponse)) { + throw noContentError(); + } + if (Array.isArray(result.data) && request.originalOrdering.length > 1) { + result.data = buildHydratedDataListResponse(objectGraph, request.originalOrdering, (_a = result.data) !== null && _a !== void 0 ? _a : [], request.supplementaryMetadataAssociations); + } + result[ResponseMetadata.requestedUrl] = originalUrl; + } + return result; + }; + if (isSome(fetchTimingMetricsBuilder)) { + return fetchTimingMetricsBuilder.measureParsing(response, parser); + } + else { + return parser(response); + } + } + catch (e) { + if (e instanceof NetworkError) { + throw e; + } + const correlationKey = (_a = response.headers["x-apple-jingle-correlation-key"]) !== null && _a !== void 0 ? _a : (_b = response.metrics[0]) === null || _b === void 0 ? void 0 : _b.clientCorrelationKey; + throw new Error(`Error Fetching - filtered: ${filteredUrlString}, original: ${originalUrl}, correlationKey: ${correlationKey !== null && correlationKey !== void 0 ? correlationKey : "N/A"}, ${e.name}, ${e.message}`); + } +} +export class NetworkError extends Error { +} +class JSONParseError extends Error { +} +export function noContentError() { + const error = new NetworkError(`No content`); + error.statusCode = 204; + return error; +} +export function notFoundError() { + const error = new NetworkError("Not found"); + error.statusCode = 404; + return error; +} +const serverInstanceHeader = "x-apple-application-instance"; +const environmentDataCenterHeader = "x-apple-application-site"; +function getPageInformationFromResponse(objectGraph, response) { + const storeFrontHeader = objectGraph.client.storefrontIdentifier; + let storeFront = null; + if ((storeFrontHeader === null || storeFrontHeader === void 0 ? void 0 : storeFrontHeader.length) > 0) { + const storeFrontHeaderComponents = storeFrontHeader.split("-"); + if (serverData.isDefinedNonNullNonEmpty(storeFrontHeaderComponents)) { + storeFront = storeFrontHeaderComponents[0]; + } + } + return { + serverInstance: response.headers[serverInstanceHeader], + storeFrontHeader: storeFrontHeader, + language: objectGraph.bag.language, + storeFront: storeFront, + environmentDataCenter: response.headers[environmentDataCenterHeader], + }; +} +function getContentTimeToLiveFromResponse(objectGraph, response) { + const cacheControlHeaderKey = Object.keys(response.headers).find((key) => key.toLowerCase() === "cache-control"); + if (serverData.isNull(cacheControlHeaderKey) || cacheControlHeaderKey === "") { + return null; + } + const headerValue = response.headers[cacheControlHeaderKey]; + if (serverData.isNullOrEmpty(headerValue)) { + return null; + } + const matches = headerValue.match(/max-age=(\d+)/); + if (serverData.isNull(matches) || matches.length < 2) { + return null; + } + return serverData.asNumber(matches[1]); +} +/** + * Builds the hydrated list of shelf items, based on the data in the response, and the original unhydated items. + */ +function buildHydratedDataListResponse(objectGraph, unhydratedData, hydratedDataCollection, associations = []) { + // Create a map of all the hydrated data items from the response, using the resource type and ID as the key + const hydratedDataMap = {}; + for (const dataItem of hydratedDataCollection) { + const key = dataMapKey(objectGraph, dataItem.type, dataItem.id); + hydratedDataMap[key] = dataItem; + } + // Next iterate through the unhyrated items in their original order, and swap in the hydrated item. + const hydratedItems = []; + for (const unhydratedItem of unhydratedData) { + const key = dataMapKey(objectGraph, unhydratedItem.type, unhydratedItem.id); + const hydratedItem = hydratedDataMap[key]; + if (isSome(hydratedItem)) { + /// Start with a base of what was there already, this is to ensure we don't lose any existing meta data + /// if some of the requested associations are not present in the response. + if (serverData.isDefinedNonNullNonEmpty(associations)) { + hydratedItem.meta = { + ...unhydratedItem.meta, + }; + for (const association of associations) { + hydrateMetaAssociationIfNecessary(objectGraph, association, hydratedItem, unhydratedItem, hydratedDataMap); + } + } + hydratedItems.push(hydratedItem); + } + } + return hydratedItems; +} +/** + * Creates a key to use in a mapping of data items. + * @param objectGraph Current object graph + * @param data The data item + * @returns A string composed from the data item ID and resource type + */ +function dataMapKey(objectGraph, resourceType, id) { + return `${resourceType}_${id}`; +} +/** + * @param objectGraph The current object graph + * @param association The association we'd like to hydrate in the meta object + * @param unhydratedItem The original MAPI data item to look for editorial card, and fill in with a fetched card if there is one + * @param hydratedDataMap The data map of all the fetched items keyed by id / type + */ +function hydrateMetaAssociationIfNecessary(objectGraph, association, hydratedItem, unhydratedItem, hydratedDataMap) { + var _a; + if (isNothing(hydratedItem.meta)) { + hydratedItem.meta = { + associations: {}, + }; + } + else if (isNothing(hydratedItem.meta.associations)) { + hydratedItem.meta.associations = {}; + } + const unhydratedAssociationsData = extractMetaAssociationFromData(association, unhydratedItem); + if (serverData.isDefinedNonNullNonEmpty(unhydratedAssociationsData)) { + const hydratedAssociationData = []; + for (const unhydratedAssociation of unhydratedAssociationsData) { + const associationDataKey = dataMapKey(objectGraph, unhydratedAssociation.type, unhydratedAssociation.id); + const associationData = hydratedDataMap[associationDataKey]; + if (isSome(associationData)) { + hydratedAssociationData.push(associationData); + } + } + const hydratedAssociations = (_a = serverData.asDictionary(hydratedItem.meta.associations)) !== null && _a !== void 0 ? _a : {}; + hydratedAssociations[association] = { + data: hydratedAssociationData, + }; + } +} +//# sourceMappingURL=network.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/foundation/media/platform-attributes.js b/node_modules/@jet-app/app-store/tmp/src/foundation/media/platform-attributes.js new file mode 100644 index 0000000..3849624 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/foundation/media/platform-attributes.js @@ -0,0 +1,143 @@ +import { isNothing } from "@jet/environment/types/optional"; +import * as serverData from "../json-parsing/server-data"; +import * as attributes from "./attributes"; +/** + * There are nested attributes that are platform-specific. If provided with a platform for which the response data has + * platform-specific attributes, this function will return those attributes. + * @param {Data} data The data for which to determine attributes. + * @param {AttributePlatform} platform The platform to fetch the attributes for + * @returns {any} If a platform is provided, returns `true` exactly when platform-specific attributes exist for that + * platform. If no platform is provided, it simply returns the data's top-level attributes. + */ +function attributesForPlatform(data, platform) { + const allPlatformAttributes = attributes.attributeAsDictionary(data, "platformAttributes"); + return serverData.traverse(allPlatformAttributes, platform !== null && platform !== void 0 ? platform : undefined); +} +/** + * Determines if attributes exist for a given platform + * @param {Data} data The data to check + * @param {AttributePlatform} platform The platform to check + * @returns {boolean} True if the platform exists in the data's platform attributes. False if not. + */ +export function hasPlatformAttribute(data, platform) { + const platformAttributes = attributesForPlatform(data, platform); + return serverData.isDefinedNonNullNonEmpty(platformAttributes); +} +/** + * Retrieve the specified attribute from the data as a dictionary + * + * @param data The data from which to retrieve the attribute. + * @param platform The platform to look up + * @param attributePath The path of the attribute. + * @returns The value for the requested attribute + */ +export function platformAttributeAsDictionary(data, platform, attributePath) { + const platformAttributes = attributesForPlatform(data, platform); + if (serverData.isNull(platformAttributes)) { + return null; + } + return serverData.asDictionary(platformAttributes, attributePath); +} +/** + * Retrieve the specified attribute from the data as an array. + * + * @param data The data from which to retrieve the attribute. + * @param platform The platform to look up + * @param attributePath The path of the attribute. + * @returns {any[]} The attribute value as an array. + */ +export function platformAttributeAsArray(data, platform, attributePath) { + const platformAttributes = attributesForPlatform(data, platform); + if (isNothing(platformAttributes)) { + return null; + } + return serverData.asArray(platformAttributes, attributePath); +} +/** + * Retrieve the specified attribute from the data as an array, coercing to an empty array if the object is not an array. + * + * @param data The data from which to retrieve the attribute. + * @param platform The platform to look up + * @param attributePath The path of the attribute. + * @returns {any[]} The attribute value as an array. + */ +export function platformAttributeAsArrayOrEmpty(data, platform, attributePath) { + const platformAttributes = attributesForPlatform(data, platform); + if (serverData.isNull(platformAttributes)) { + return []; + } + return serverData.asArrayOrEmpty(platformAttributes, attributePath); +} +/** + * Retrieve the specified attribute from the data as a string. + * + * If the attribute lives under the platform-specific attributes, then a platform may be provided to properly call in to + * the nested structure. + * @param data The data from which to retrieve the attribute. + * @param platform The platform to look up + * @param attributePath The object path for the attribute. + * @param policy The validation policy to use when resolving this value. + * @returns {string} The attribute value as a string. + */ +export function platformAttributeAsString(data, platform, attributePath, policy = "coercible") { + const platformAttributes = attributesForPlatform(data, platform); + if (serverData.isNull(platformAttributes)) { + return null; + } + return serverData.asString(platformAttributes, attributePath, policy); +} +/** + * Retrieve the specified attribute from the data as a boolean. + * + * If the attribute lives under the platform-specific attributes, then a platform may be provided to properly call in to + * the nested structure. + * @param data The data from which to retrieve the attribute. + * @param platform The platform to look up + * @param attributePath The path of the attribute. + * @param policy The validation policy to use when resolving this value. + * @returns {boolean} The attribute value as a boolean. + */ +export function platformAttributeAsBoolean(data, platform, attributePath, policy = "coercible") { + const platformAttributes = attributesForPlatform(data, platform); + if (serverData.isNull(platformAttributes)) { + return null; + } + return serverData.asBoolean(platformAttributes, attributePath, policy); +} +/** + * Retrieve the specified attribute from the data as a boolean, which will be `false` if the attribute does not exist. + * + * If the attribute lives under the platform-specific attributes, then a platform may be provided to properly call in to + * the nested structure. + * @param data The data from which to retrieve the attribute. + * @param platform The platform to look up + * @param attributePath The path of the attribute. + * @returns {boolean} The attribute value as a boolean, coercing to `false` if the value is not present.. + */ +export function platformAttributeAsBooleanOrFalse(data, platform, attributePath) { + const platformAttributes = attributesForPlatform(data, platform); + if (serverData.isNull(platformAttributes)) { + return false; + } + return serverData.asBooleanOrFalse(platformAttributes, attributePath); +} +/** + * Retrieve the specified attribute from the data as a number. + * + * If the attribute lives under the platform-specific attributes, then a platform may be provided to properly call in to + * the nested structure. + * @param data The data from which to retrieve the attribute. + * @param platform The platform to look up + * @param attributePath The path of the attribute. + * @param policy The validation policy to use when resolving this value. + * @returns {boolean} The attribute value as a number. + */ +export function platformAttributeAsNumber(data, platform, attributePath, policy = "coercible") { + const platformAttributes = attributesForPlatform(data, platform); + if (serverData.isNull(platformAttributes)) { + return null; + } + return serverData.asNumber(platformAttributes, attributePath, policy); +} +// endregion +//# sourceMappingURL=platform-attributes.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/foundation/media/relationships.js b/node_modules/@jet-app/app-store/tmp/src/foundation/media/relationships.js new file mode 100644 index 0000000..0f96574 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/foundation/media/relationships.js @@ -0,0 +1,43 @@ +import * as serverData from "../json-parsing/server-data"; +export function hasRelationship(data, relationshipType, checkForContent = true) { + const relationshipDataContainer = relationship(data, relationshipType); + if (!relationshipDataContainer) { + return false; + } + if (!relationshipDataContainer.data || (checkForContent && relationshipDataContainer.data.length === 0)) { + return false; + } + return true; +} +export function relationship(data, relationshipType) { + if (serverData.isDefinedNonNull(data)) { + return serverData.asInterface(data.relationships, relationshipType); + } + return null; +} +export function relationshipViewsContainer(data, relationshipType) { + return serverData.asInterface(data.views, relationshipType); +} +export function relationshipData(objectGraph, data, relationshipType) { + const relationshipDataArray = serverData.asArrayOrEmpty(data.relationships, [ + relationshipType, + "data", + ]); + if (relationshipDataArray.length === 0) { + return null; + } + if (relationshipDataArray.length > 1) { + objectGraph.console.warn(`there was an array of relationships when only the first was asked for in relationship ${relationshipType}`); + } + return relationshipDataArray[0]; +} +export function relationshipCollection(data, relationshipType, allowNulls = false) { + if (!hasRelationship(data, relationshipType, false) && allowNulls) { + return null; + } + return serverData.asArrayOrEmpty(data.relationships, [relationshipType, "data"]); +} +export function relationshipViewsCollection(data, relationshipType) { + return serverData.asArrayOrEmpty(data.views, [relationshipType, "data"]); +} +//# sourceMappingURL=relationships.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/foundation/media/url-builder.js b/node_modules/@jet-app/app-store/tmp/src/foundation/media/url-builder.js new file mode 100644 index 0000000..955529c --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/foundation/media/url-builder.js @@ -0,0 +1,381 @@ +/** + * Created by joel on 11/4/2018. + */ +import { isNothing, isSome } from "@jet/environment/types/optional"; +import * as serverData from "../json-parsing/server-data"; +import * as urls from "../network/urls"; +import * as attributes from "./attributes"; +/// this is exposed for compatibility. If you find yourself needing to use this outside of the media api module you +/// probably have code smell. DO NOT USE. +export function buildURLFromRequest(objectGraph, request) { + var _a, _b; + const baseURL = request.href && request.href.length > 0 + ? baseURLForHref(request.href) + : baseURLForResourceType(objectGraph, request.isMixedMediaRequest, request.resourceType, request.countryCodeOverride); + const mediaApiURL = new urls.URL(baseURL); + if (serverData.isDefinedNonNullNonEmpty(request.resourceType)) { + for (const pathComponent of pathComponentsForRequest(request.resourceType, request.targetResourceType)) { + mediaApiURL.append("pathname", pathComponent); + } + } + if (request.isMixedMediaRequest) { + for (const [resourceType, ids] of request.idsByResourceType.entries()) { + mediaApiURL.param(`ids[${resourceType}]`, Array.from(ids).sort().join(",")); + } + } + else if (request.ids.size > 1 || request.useIdsAsQueryParam) { + mediaApiURL.param("ids", Array.from(request.ids).sort().join(",")); + } + else if (request.ids.size === 1) { + const id = request.ids.values().next().value; + mediaApiURL.append("pathname", id); + } + if (request.resourceType !== undefined) { + const trailingPathComponent = trailingPathComponentForResourceType(request.resourceType); + if (serverData.isDefinedNonNullNonEmpty(trailingPathComponent)) { + mediaApiURL.append("pathname", trailingPathComponent); + } + } + mediaApiURL.param("platform", (_a = request.platform) !== null && _a !== void 0 ? _a : undefined); + if (request.additionalPlatforms.size > 0) { + mediaApiURL.param("additionalPlatforms", Array.from(request.additionalPlatforms).sort().join(",")); + } + /** + * Add `extend` attributes. + * Note that when `useCustomAttributes` is true, there is `customArtwork` param even when `attributeIncludes` is initially empty. + * This due MAPI auto-extend for `artwork`, and lack of auto-extend for `customArtwork` + */ + if (request.attributeIncludes.size > 0 || request.useCustomAttributes) { + let extendAttributes = Array.from(request.attributeIncludes); + if (request.useCustomAttributes) { + extendAttributes = convertRequestAttributesToCustomAttributes(objectGraph, extendAttributes); + } + extendAttributes.sort(); + mediaApiURL.param("extend", extendAttributes.join(",")); + } + // Add age restriction if present. + if (serverData.isDefinedNonNull(request.ageRestriction) && objectGraph.bag.enableAgeRatingFilter) { + mediaApiURL.param("restrict[ageRestriction]", request.ageRestriction.toString()); + } + // Automatically extend iOS catalog requests for apps to include appBinaryTraits. + if (request.includeAppBinaryTraitsAttribute) { + request.includingScopedAttributes("apps", ["appBinaryTraits"]); + } + if (serverData.isDefinedNonNull(request.scopedAttributeIncludes)) { + for (const [dataType, scopedIncludes] of request.scopedAttributeIncludes.entries()) { + mediaApiURL.param(`extend[${dataType}]`, Array.from(scopedIncludes).sort().join(",")); + } + } + if (request.relationshipIncludes.size > 0) { + mediaApiURL.param("include", Array.from(request.relationshipIncludes).sort().join(",")); + } + if (serverData.isDefinedNonNull(request.scopedRelationshipIncludes)) { + for (const [dataType, scopedIncludes] of request.scopedRelationshipIncludes.entries()) { + mediaApiURL.param(`include[${dataType}]`, Array.from(scopedIncludes).sort().join(",")); + } + } + if (serverData.isDefinedNonNull(request.metaIncludes)) { + for (const [dataType, scopedMeta] of request.metaIncludes.entries()) { + mediaApiURL.param(`meta[${dataType}]`, Array.from(scopedMeta).sort().join(",")); + } + } + if (serverData.isSetDefinedNonNullNonEmpty(request.viewsIncludes)) { + mediaApiURL.param("views", Array.from(request.viewsIncludes).sort().join(",")); + } + if (serverData.isDefinedNonNull(request.kindIncludes)) { + for (const [dataType, scopedMeta] of request.kindIncludes.entries()) { + mediaApiURL.param(`kinds[${dataType}]`, Array.from(scopedMeta).sort().join(",")); + } + } + if (serverData.isDefinedNonNull(request.associateIncludes)) { + for (const [dataType, scopedAssociate] of request.associateIncludes.entries()) { + mediaApiURL.param(`associate[${dataType}]`, Array.from(scopedAssociate).sort().join(",")); + } + } + if (serverData.isDefinedNonNull(request.scopedAvailableInIncludes)) { + for (const [dataType, scopedAvailableIn] of request.scopedAvailableInIncludes.entries()) { + mediaApiURL.param(`availableIn[${dataType}]`, Array.from(scopedAvailableIn).sort().join(",")); + } + } + if (serverData.isDefinedNonNullNonEmpty(request.fields)) { + let extendedFields = Array.from(request.fields); + if (request.useCustomAttributes) { + extendedFields = convertRequestFieldsToCustomFields(extendedFields); + } + request.fields.sort(); + mediaApiURL.param("fields", extendedFields.join(",")); + } + if (serverData.isDefinedNonNull(request.limit) && request.limit > 0) { + mediaApiURL.param(`limit`, `${request.limit}`); + } + if (serverData.isDefinedNonNull(request.sparseLimit)) { + mediaApiURL.param(`sparseLimit`, `${request.sparseLimit}`); + } + if (serverData.isDefinedNonNull(request.scopedSparseLimit)) { + for (const [dataType, scopedLimit] of request.scopedSparseLimit.entries()) { + mediaApiURL.param(`sparseLimit[${dataType}]`, String(scopedLimit)); + } + } + if (serverData.isDefinedNonNull(request.sparseCount)) { + mediaApiURL.param(`sparseCount`, `${request.sparseCount}`); + } + for (const relationshipID of Object.keys(request.relationshipLimits).sort()) { + const limit = request.relationshipLimits[relationshipID]; + mediaApiURL.param(`limit[${relationshipID}]`, `${limit}`); + } + if (serverData.isDefinedNonNullNonEmpty(request.additionalQuery)) { + mediaApiURL.append("query", request.additionalQuery); + } + if (serverData.isDefinedNonNullNonEmpty(request.searchTerm)) { + // Search hints shouldn't add `search` to the end of the path name as the correct final path + // is `v1/catalog/us/search/suggestions`, which is handled by `trailingPathComponentForResourceType()` + // Search hints also shouldn't have the bubble param + if (isNothing(request.resourceType) || request.resourceType !== "search-hints") { + mediaApiURL.append("pathname", "search"); + mediaApiURL.param("bubble[search]", request.searchTypes.join(",")); + } + mediaApiURL.param("term", request.searchTerm); + } + if (serverData.isDefinedNonNullNonEmpty(request.enabledFeatures)) { + mediaApiURL.param("with", request.enabledFeatures.join(",")); + } + if (serverData.isDefinedNonNullNonEmpty(request.context)) { + mediaApiURL.param("contexts", request.context); + } + if (serverData.isDefinedNonNullNonEmpty(request.filterType) && + serverData.isDefinedNonNullNonEmpty(request.filterValue)) { + mediaApiURL.param(`filter[${request.filterType}]`, request.filterValue); + } + const language = objectGraph.bag.mediaApiLanguage; + // Only attach the language query param if: + // - there is a language available in the bag, and + // - a language has not been manually attached to the request. This is used for special situations to override the language for particular content, + // so it should take precedence over the default. + if (serverData.isDefinedNonNull(language) && serverData.isNull(request.additionalQuery["l"])) { + mediaApiURL.param("l", language); + } + mediaApiURL.host = (_b = hostForUrl(objectGraph, mediaApiURL, request)) !== null && _b !== void 0 ? _b : undefined; + mediaApiURL.protocol = "https"; + return mediaApiURL; +} +/** + * Get the media api base url for all requests. + * @param objectGraph Current object graph + * @param isMixedCatalogRequest Whether the request intends to use mixed catalog + * @param type The request resource type + * @param overrideCountryCode A country code to override the bag value. + * @returns A built base URL string + */ +function baseURLForResourceType(objectGraph, isMixedCatalogRequest, type, overrideCountryCode) { + switch (type) { + case "personalization-data": + case "reviews": + case "app-distribution": + return `/v1/${endpointTypeForResourceType(type)}/`; + default: + const countryCode = isSome(overrideCountryCode) && overrideCountryCode.length > 0 + ? overrideCountryCode + : objectGraph.bag.mediaCountryCode; + const baseURL = `/v1/${endpointTypeForResourceType(type)}/${countryCode}`; + return isMixedCatalogRequest ? baseURL : `${baseURL}/`; + } +} +/** + * Get the media api base url for all requests that already have an href. + * @return {string} + */ +function baseURLForHref(href) { + return href; +} +function endpointTypeForResourceType(type) { + switch (type) { + case "apps": + case "app-events": + case "arcade-apps": + case "app-bundles": + case "charts": + case "contents": + case "developers": + case "eula": + case "in-apps": + case "multiple-system-operators": + case "user-reviews": + case "customers-also-bought-apps-with-download-intent": + return "catalog"; + case "categories": + case "editorial-pages": + case "editorial-items": + case "editorial-item-groups": + case "editorial-elements": + case "groupings": + case "multiplex": + case "multirooms": + case "rooms": + case "today": + case "collections": + return "editorial"; + case "ratings": + return "ratings"; + case "personalization-data": + case "reviews": + return "me"; + case "upsellMarketingItem": + case "landing": + return "engagement"; + case "landing:new-protocol": + return "recommendations"; + case "personal-recommendations": + return "recommendations"; + case "engagement-data": + return "engagement"; + case "app-distribution": + return "listing"; + default: + return "catalog"; + } +} +/** + * The path component to add for the given resource + */ +function pathComponentsForRequest(resourceType, targetResourceType) { + switch (resourceType) { + case "eula": + if (targetResourceType === undefined) { + return [resourceType]; // Might be modelled better as an error. + } + else { + return [resourceType, targetResourceType]; + } + case "landing:new-protocol": + return []; + case "landing": + if (targetResourceType === undefined) { + return ["search", resourceType]; // Might be modelled better as an error. + } + else { + return ["search", resourceType, targetResourceType]; + } + case "user-reviews": + return ["apps"]; + case "reviews": + return ["reviews", "apps"]; + case "multiplex": + return ["multiplex"]; + case "upsellMarketingItem": + return ["upsell", "marketing-items"]; + case "trending-contents": + return ["search", resourceType]; + case "customers-also-bought-apps-with-download-intent": + return ["apps"]; + case "searchLanding:see-all": + return []; + case "search-hints": + return []; + case "app-distribution": + return ["apps"]; + default: + return [resourceType]; + } +} +/** + * Add a component to the end of the path for the given resource + */ +function trailingPathComponentForResourceType(type) { + switch (type) { + case "user-reviews": + return "reviews"; + case "customers-also-bought-apps-with-download-intent": + return "view/customers-also-bought-apps-with-download-intent"; + case "collections": + return "contents"; + case "searchLanding:see-all": + return "view/see-all"; + case "search-hints": + return "search/suggestions"; + default: + return null; + } +} +function hostForUrl(objectGraph, url, request) { + var _a; + const path = (_a = url.pathname) !== null && _a !== void 0 ? _a : ""; + let host = null; + if (request.isStorePreviewRequest) { + host = objectGraph.bag.mediaPreviewHost; + } + else if (request.isMediaRealmRequest) { + host = objectGraph.bag.mediaRealmHost; + } + else if (path.includes("search/landing")) { + // Special case <rdar://problem/50185140> RFW3: Use bag key "apps-media-api-edge-end-points" for "search/landing" end-point + // until we figure out a better way to test the paths + const useEdgeForSearchLanding = objectGraph.bag.edgeEndpoints.indexOf("landing") !== -1; + host = useEdgeForSearchLanding ? objectGraph.bag.mediaEdgeHost(objectGraph) : objectGraph.bag.mediaHost; + } + else if (request.resourceType === "app-distribution" && isSome(objectGraph.bag.appDistributionMediaAPIHost)) { + host = objectGraph.bag.appDistributionMediaAPIHost; + } + else if (request.isMixedMediaRequest && objectGraph.bag.mediaAPICatalogMixedShouldUseEdge) { + // CatalogMixed endpoint should be routed to edge when the bag is enabled. + host = objectGraph.bag.mediaEdgeHost(objectGraph); + } + else if (objectGraph.bag.edgeEndpoints.map((endpoint) => path.includes(endpoint)).reduce(truthReducer, false)) { + if (path.includes("search") && !path.includes("view/see-all")) { + host = objectGraph.bag.mediaEdgeSearchHost; + } + else { + host = objectGraph.bag.mediaEdgeHost(objectGraph); + } + } + else { + host = objectGraph.bag.mediaHost; + } + if (serverData.isNull(host)) { + host = "api.apps.apple.com"; + } + return host; +} +const truthReducer = (accumulator, current) => accumulator || current; +/** + * Performs a conversion for given attribute to fetch the customAttribute variant of it. + * @param objectGraph The object graph + * @param attribute Attribute to convert if needed, e.g. `artwork` + * @returns `string` attribute that is custom equivalent of `attribute`, or `attribute` unmodified. + */ +function convertRequestAttributesToCustomAttributes(objectGraph, requestAttributes) { + const convertedAttributes = requestAttributes.map((attribute) => { + var _a; + return (_a = attributes.attributeKeyAsCustomAttributeKey(attribute)) !== null && _a !== void 0 ? _a : attribute; + }); + /** + * `artwork` is an autoincluded resources, so `attributes` usually doesn't contain this explicitly :( + * Per MAPI contract, we "autoinclude" `customArtwork` explicitly for requests with custon attributes. + */ + convertedAttributes.push("customArtwork"); + /** + * `iconArtwork` is not autoincluded, but we need to ensure it is always requested even alongside its + * custom counterpart, `customIconArtwork`. This is because we might be viewing a macOS only app on iOS, + * where custom attributes are supported, but not available for macOS apps. + */ + if (requestAttributes.includes("iconArtwork")) { + convertedAttributes.push("iconArtwork"); + } + /** + * `customDeepLink` is always desired as an included resource in case an app decides to use a custom tap destination. + * Per MAPI contract, we "autoinclude" `customDeepLink` explicitly for all requests with custom attributes. + */ + convertedAttributes.push("customDeepLink"); + return convertedAttributes; +} +/** + * Performs the conversion for given field value (which may specify `attributes` keys) to customAttribute variant of it. + */ +function convertRequestFieldsToCustomFields(requestFields) { + const convertedFields = requestFields.map((fieldName) => { + var _a; + return (_a = attributes.attributeKeyAsCustomAttributeKey(fieldName)) !== null && _a !== void 0 ? _a : fieldName; + }); + // DON'T include `customArtwork` for request `field` conversion. Only specify if `artwork` was initially in `requestFields`. + return convertedFields; +} +//# sourceMappingURL=url-builder.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/foundation/media/util.js b/node_modules/@jet-app/app-store/tmp/src/foundation/media/util.js new file mode 100644 index 0000000..77ed5c2 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/foundation/media/util.js @@ -0,0 +1,185 @@ +import { noContentError, notFoundError } from "./network"; +/** + * Validate an untrusted adam ID without contacting the server. + * + * This allows avoiding the work of calling the server with completely bogus + * IDs. It's scoped to web since ID formats can change and web can be updated + * easily (whereas backporting to older native clients is trickier). + * + * @param {AppStoreObjectGraph} objectGraph + * @param {string} id - the Adam ID to validate + * @returns {void} undefined if valid, throws a `NetworkError` (204) otherwise + */ +export function validateAdamId(objectGraph, id) { + if (objectGraph.client.isWeb && !isAdamId(id)) { + throw noContentError(); + } +} +/** + * Check if an ID is a valid Adam ID. + * + * @param {string} id - string to validate + * @return {boolean} true if valid, false otherwise + */ +function isAdamId(id) { + // Media API actually validates if the number <= 2^63-1. But doubles in + // JavaScript cannot precisely represent this (the closest number is >2^63) + // so checking this requires BigInt, which might not be available. At the + // end of the day, checking this is likely not worth the complexity. 2^63-1 + // is 9223372036854775807 so we just restrict the number of digits. + // + // See: https://github.pie.apple.com/its/amp-enums/blob/f75500b44f871f35ba3ce459a5ff4c9f225e71b0/src/main/java/com/apple/jingle/store/IdSpace.java#L131-L140 + // See: https://github.com/google/guava/blob/869a75a1e3ff85d36672a3cd154772dc90c7b3d2/guava/src/com/google/common/primitives/Longs.java#L400-L440 + // See: https://docs.oracle.com/en/java/javase/24/docs/api/java.base/java/lang/Long.html#MAX_VALUE + return /^\d{1,19}$/.test(id); +} +/** + * Validate an untrusted Featured Content ID without contacting the server. + * + * This allows avoiding the work of calling the server with completely bogus + * IDs. It's scoped to web since ID formats can change and web can be updated + * easily (whereas backporting to older native clients is trickier). + * + * @param {AppStoreObjectGraph} objectGraph + * @param {string} id the FC ID to validate + * @returns {void} undefined if valid, throws a `NetworkError` (204) otherwise + */ +export function validateFcId(objectGraph, id) { + if (objectGraph.client.isWeb && !isFcId(id)) { + throw noContentError(); + } +} +/** + * Check if an ID is a valid Featured Content (FC) ID. + * + * @param {string} id - string to validate + * @return {boolean} true if valid, false otherwise + */ +function isFcId(id) { + // FcIds are actually Adam IDs under the hood. + // See: https://github.pie.apple.com/its/Jingle/blob/d2d051c9ef2891f72d5f02f5bbbf2d7748afa7b9/MZStoreComponents/src/main/java/com/apple/jingle/app/store/editorial/SFEditorialHelper.java#L293 + return isAdamId(id); +} +/** + * Validate an untrusted (Chart) Genre ID without contacting the server. + * + * This allows avoiding the work of calling the server with completely bogus + * IDs. It's scoped to web since ID formats can change and web can be updated + * easily (whereas backporting to older native clients is trickier). + * + * @param {AppStoreObjectGraph} objectGraph + * @param {string} id the genre ID to validate + * @returns {void} undefined if valid, throws a `NetworkError` (204) otherwise + */ +export function validateGenreId(objectGraph, id) { + if (objectGraph.client.isWeb && !isGenreId(id)) { + throw noContentError(); + } +} +/** + * Check if an ID is a valid (Chart) Genre ID. + * + * @param {string} id - string to validate + * @return {boolean} true if valid, false otherwise + */ +function isGenreId(id) { + // Genre IDs are Java int (signed 32-bit). + // + // Technically negative is not excluded, but all charts are positive, so + // filter out negative. + // + // See: https://github.pie.apple.com/its/Jingle/blob/main/shared/reference-data-logic/MZReferenceDataLogic/src/main/java/com/apple/jingle/eo/MZGenreService.java#L555 + return isPositiveJavaInt(id); +} +/** + * Validate an untrusted Grouping ID without contacting the server. + * + * This allows avoiding the work of calling the server with completely bogus + * IDs. It's scoped to web since ID formats can change and web can be updated + * easily (whereas backporting to older native clients is trickier). + * + * @param {AppStoreObjectGraph} objectGraph + * @param {string} id the grouping ID to validate + * @returns {void} undefined if valid, throws a `NetworkError` (204) otherwise + */ +export function validateGroupingId(objectGraph, id) { + if (objectGraph.client.isWeb && !isGroupingId(id)) { + throw noContentError(); + } +} +/** + * Check if an ID is a valid Grouping ID. + * + * @param {string} id - string to validate + * @return {boolean} true if valid, false otherwise + */ +function isGroupingId(id) { + // Grouping IDs are Java int (signed 32-bit) + // + // Technically negative does not fail to parse, but they're all positive in + // practice. + // + // See: https://github.pie.apple.com/its/Jingle/blob/aaccec936f1feed227fd171ae66bb160cf38e497/MZStorePlatform/src/main/java/com/apple/jingle/store/mediaapi/util/SFMediaAPIEditorialUtil.java#L634 + return isPositiveJavaInt(id); +} +/** + * Test if a string contains a Java int. + * + * @param {string} s - the string to test + * @returns {boolean} true if the string is a stringified Java int, false otherwise + */ +function isPositiveJavaInt(s) { + // Java int is 32-bit signed. We can check bounds since signed integers are + // exactly representable as doubles (actually up to 2^53 is representable + // exactly). + // + // See: https://docs.oracle.com/en/java/javase/24/docs/api/java.base/java/lang/Integer.html#MAX_VALUE + return /^\d+$/.test(s) && parseInt(s, 10) <= 2147483647; +} +/** + * Validate an untrusted Editorial Shelf Collection ID without contacting the server. + * + * This allows avoiding the work of calling the server with completely bogus + * IDs. It's scoped to web since ID formats can change and web can be updated + * easily (whereas backporting to older native clients is trickier). + * + * @param {AppStoreObjectGraph} objectGraph + * @param {string} id the editorial shelf collection ID to validate + * @returns {void} undefined if valid, throws a `NetworkError` (204) otherwise + */ +export function validateEditorialShelfCollectionId(objectGraph, id) { + if (objectGraph.client.isWeb && !isEditorialShelfCollectionId(id)) { + throw noContentError(); + } +} +/** + * Check if an ID is a valid editorial shelf collection ID. + * + * @param {string} id - string to validate + * @return {boolean} true if valid, false otherwise + */ +function isEditorialShelfCollectionId(id) { + // Ids are prefixed with "eds.". Beyond that they do have a UUID-type + // identifier that follows, but that seems more liable to change. + // + // See: https://github.pie.apple.com/its/amp-enums/blob/f75500b44f871f35ba3ce459a5ff4c9f225e71b0/src/main/java/com/apple/jingle/store/IdSpace.java#L43C5-L43C20 + // See: https://github.pie.apple.com/its/amp-enums/blob/f75500b44f871f35ba3ce459a5ff4c9f225e71b0/src/main/java/com/apple/jingle/store/IdSpace.java#L250-L252 + return id.startsWith("eds."); +} +/** + * Validates if a request can be performed for `vision` platform content, based on + * the feature flag is disabled. + * + * @param {AppStoreObjectGraph} objectGraph The application's state graph. + * @throws {NetworkError} Throws a 404 error if access is restricted. + * @returns {void} + */ +export function validateNeedsVisionRestriction(objectGraph) { + var _a; + if (objectGraph.client.isWeb && + ((_a = objectGraph.activeIntent) === null || _a === void 0 ? void 0 : _a.previewPlatform) === "vision" && + !objectGraph.bag.enableVisionPlatform) { + throw notFoundError(); + } +} +//# sourceMappingURL=util.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/foundation/metrics/buy-parameters.js b/node_modules/@jet-app/app-store/tmp/src/foundation/metrics/buy-parameters.js new file mode 100644 index 0000000..a48e886 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/foundation/metrics/buy-parameters.js @@ -0,0 +1,112 @@ +import { isNothing, isSome } from "@jet/environment/types/optional"; +/** + * An object which allows buy parameters to be operated on like a hash map. + */ +export class BuyParameters { + /** + * Create a buy parameters object with an optional string representation. + * + * @param buyParams A string containing buy parameters. + */ + constructor(buyParams) { + this._values = {}; + if (isSome(buyParams) && buyParams.length > 0) { + const pairs = buyParams.split("&"); + for (const pair of pairs) { + const [encodedKey, encodedValue] = pair.split("="); + const key = decodeURIComponent(encodedKey); + const value = isNothing(encodedValue) ? "" : decodeURIComponent(encodedValue); + this._values[key] = value; + } + } + } + /** + * Determine the search key to use for the given parameters. + * + * @param key The key to determine the search value for. + * @param prefix An optional prefix to prepend to `key`. + * @returns A key which can be used to store and retrieve + * values in this buy parameters object. + */ + _searchKey(key, prefix) { + if (key.length === 0) { + throw new Error("key may not be zero length"); + } + if (isNothing(prefix) || prefix.length === 0) { + return key; + } + else { + return `${prefix}${key.charAt(0).toUpperCase()}${key.slice(1)}`; + } + } + /** + * Returns the value associated with a given key. + * + * @param key The key to retrieve the associated value for. + * @param keyPrefix The prefix to add to the key. Defaults to `mt`. + * @returns The value associated with `key`, or `undefined` is none is found. + */ + get(key, keyPrefix = "mt") { + const searchKey = this._searchKey(key, keyPrefix); + return this._values[searchKey]; + } + /** + * Add a given key-value pair to this buy parameters. + * + * If a value was previously associated with `key`, + * it will be replaced with `value`. + * + * @param key The key to associate `value` with in these buy parameters. + * @param value The value to add to the buy parameters. If value is `undefined` + * or `null`, any values previously associated with `key` will be removed. + * @param keyPrefix The prefix to add to the key. Defaults to `mt`. + * @returns `this` + */ + set(key, value, keyPrefix = "mt") { + const searchKey = this._searchKey(key, keyPrefix); + if (isNothing(value)) { + delete this._values[searchKey]; + } + else { + this._values[searchKey] = value; + } + return this; + } + /** + * Convert this buy parameters to its string representation. + */ + toString() { + let buyParams = ""; + for (const key of Object.keys(this._values)) { + const value = this._values[key]; + if (buyParams.length > 0) { + buyParams += "&"; + } + buyParams += encodeURIComponent(key); + buyParams += "="; + buyParams += encodeURIComponent(value); + } + return buyParams; + } + /** + * Convert the buy params into a map representation, in which + * the keys and values are encoded. + */ + toEncodedMap() { + const map = {}; + for (const key of Object.keys(this._values)) { + const value = this._values[key]; + const encodedKey = encodeURIComponent(key); + const encodedValue = encodeURIComponent(value); + map[encodedKey] = encodedValue; + } + return map; + } + /** + * Buy params in a raw (non url encoded) map representation. + */ + toRawMap() { + return { ...this._values }; + } +} +//# sourceMappingURL=buy-parameters.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/foundation/metrics/cookies.js b/node_modules/@jet-app/app-store/tmp/src/foundation/metrics/cookies.js new file mode 100644 index 0000000..5005b72 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/foundation/metrics/cookies.js @@ -0,0 +1,27 @@ +import { isNothing } from "@jet/environment/types/optional"; +/** + * Iterate the cookies contained in a string. + * + * @param cookie A string containing zero or more cookies. + */ +export function* cookiesOf(cookie) { + if (isNothing(cookie)) { + return; + } + const rawEntries = cookie.split(";"); + for (const rawEntry of rawEntries) { + const keyEndIndex = rawEntry.indexOf("="); + if (keyEndIndex === -1) { + // If there's no splitter, treat the whole raw + // entry as the key and provide an empty value. + const key = decodeURIComponent(rawEntry).trim(); + yield { key, value: "" }; + } + else { + const key = decodeURIComponent(rawEntry.substring(0, keyEndIndex)).trim(); + const value = decodeURIComponent(rawEntry.substring(keyEndIndex + 1)).trim(); + yield { key, value }; + } + } +} +//# sourceMappingURL=cookies.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/foundation/metrics/metrics-identifiers-cache.js b/node_modules/@jet-app/app-store/tmp/src/foundation/metrics/metrics-identifiers-cache.js new file mode 100644 index 0000000..96a487f --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/foundation/metrics/metrics-identifiers-cache.js @@ -0,0 +1,245 @@ +import { isNothing, isSome } from "@jet/environment"; +import * as validation from "@jet/environment/json/validation"; +import { makeMetatype } from "@jet/environment/util/metatype"; +/** + * @public + * Type to define which type of metrics identifier key to use to fetch identifiers, fields, etc. + */ +export var MetricsIdentifierType; +(function (MetricsIdentifierType) { + MetricsIdentifierType["client"] = "clientId"; + MetricsIdentifierType["user"] = "userId"; + MetricsIdentifierType["canonical"] = "canonicalAccountIdentifierOverride"; +})(MetricsIdentifierType || (MetricsIdentifierType = {})); +/** + * @public + * The default namespaces to provide to the native `MetricsIdStore` for fetching and caching + */ +export var MetricsIdentifiersDefaultBagNamespaces; +(function (MetricsIdentifiersDefaultBagNamespaces) { + MetricsIdentifiersDefaultBagNamespaces["client"] = "APPSTORE_ENGAGEMENT_CLIENT"; + MetricsIdentifiersDefaultBagNamespaces["user"] = "APPSTORE_ENGAGEMENT"; +})(MetricsIdentifiersDefaultBagNamespaces || (MetricsIdentifiersDefaultBagNamespaces = {})); +export var MetricsIdentifiersPaymentBagNamespaces; +(function (MetricsIdentifiersPaymentBagNamespaces) { + MetricsIdentifiersPaymentBagNamespaces["client"] = "APPSTORE_PAYMENTS_ENGAGEMENT_CLIENT"; + MetricsIdentifiersPaymentBagNamespaces["user"] = "APPSTORE_PAYMENTS_ENGAGEMENT"; +})(MetricsIdentifiersPaymentBagNamespaces || (MetricsIdentifiersPaymentBagNamespaces = {})); +export var MetricsIdentifiersPersonalizationBagNamespaces; +(function (MetricsIdentifiersPersonalizationBagNamespaces) { + MetricsIdentifiersPersonalizationBagNamespaces["user"] = "APPSTORE_PERSONALIZATION"; +})(MetricsIdentifiersPersonalizationBagNamespaces || (MetricsIdentifiersPersonalizationBagNamespaces = {})); +/** + * The payment context mappings for a metrics identifier key. + */ +export const paymentIdentifierContextMapping = { + [MetricsIdentifierType.client]: { + keyType: MetricsIdentifierType.client, + bagNamespace: MetricsIdentifiersPaymentBagNamespaces.client, + crossSyncDevice: false, + }, + [MetricsIdentifierType.user]: { + keyType: MetricsIdentifierType.user, + bagNamespace: MetricsIdentifiersPaymentBagNamespaces.user, + crossSyncDevice: true, + }, +}; +/** + * The default context mappings for a metrics identifier key. + */ +const defaultIdentifierContextMapping = { + [MetricsIdentifierType.client]: { + keyType: MetricsIdentifierType.client, + bagNamespace: MetricsIdentifiersDefaultBagNamespaces.client, + crossSyncDevice: false, + }, + [MetricsIdentifierType.user]: { + keyType: MetricsIdentifierType.user, + bagNamespace: MetricsIdentifiersDefaultBagNamespaces.user, + crossSyncDevice: true, + }, +}; +/** + * The personalization context mappings for a metrics identifier key. + */ +export const personalizationIdentifierContextMapping = { + [MetricsIdentifierType.user]: { + keyType: MetricsIdentifierType.user, + bagNamespace: MetricsIdentifiersPersonalizationBagNamespaces.user, + crossSyncDevice: true, + }, +}; +/** + * Represents a cache for metrics identifiers and fields. + */ +export class MetricsIdentifiersCache { + /** + * Constructs a new instance of the MetricsIdentifiersCache class. + * @param identifierContextMapping - The mapping of identifier types to key contexts. + */ + constructor(identifierContextMapping = defaultIdentifierContextMapping) { + this.cachedMetricsIds = {}; + this.cachedMetricsFields = {}; + /** + * A flag indicating whether a DSID fallback field should be added to the event fields. + */ + this.shouldAddDsIdFallbackField = false; + /** + * The DSID of the currently logged in user on this JS request. + */ + this.userDsId = null; + this.identifierContextMapping = identifierContextMapping; + } + /** + * Loads the values for the specified identifier types. + * @param objectGraph - The object graph. + * @param idTypes - The identifier types. + * @returns A promise that resolves to an array of loaded values. + */ + async loadValues(objectGraph, idTypes) { + if (objectGraph.bag.isMetricsUserIdFallbackEnabled && !objectGraph.user.isUnderThirteen) { + this.shouldAddDsIdFallbackField = true; + this.userDsId = objectGraph.user.dsid; + } + else { + this.shouldAddDsIdFallbackField = false; + this.userDsId = null; + } + const loadedValues = []; + for (const idType of idTypes) { + const idTypeLoadedValues = await this.loadValuesForIdType(objectGraph, idType); + loadedValues.push(idTypeLoadedValues); + } + for (const { idType, id, fields } of loadedValues) { + if (id) { + this.cachedMetricsIds[idType] = id; + } + if (fields) { + this.cachedMetricsFields[idType] = fields; + } + } + } + /** + * @param objectGraph The app store object graph for all our native dependencies + * @param idType The type of id we're loading values for + * @returns The MetricsIdStore values for this id type + */ + async loadValuesForIdType(objectGraph, idType) { + const returnValues = { + idType, + }; + const context = this.identifierContextMapping[idType]; + if (context) { + try { + const fields = await validation.context("MetricsIdentifiersCache:loadValues:metricsFields", async () => { + return await objectGraph.metricsIdentifiers.getMetricsFieldsForContexts([context]); + }); + if (isSome(fields)) { + returnValues["fields"] = fields; + const extractedIdValue = fields[idType]; + if (isSome(extractedIdValue) && + typeof extractedIdValue === "string" && + extractedIdValue.length > 0) { + returnValues["id"] = extractedIdValue; + } + } + } + catch (error) { + objectGraph.console.error(`Unable to fetch metrics fields for idType ${idType}`, error); + } + if (isNothing(returnValues["id"])) { + try { + const id = await validation.context("MetricsIdentifiersCache:loadValues:metricsIdentifier", async () => { + return await objectGraph.metricsIdentifiers.getIdentifierForContext(context); + }); + if (isSome(id)) { + returnValues["id"] = id; + } + } + catch (error) { + objectGraph.console.error(`Unable to fetch metrics identifier for idType ${idType}`, error); + } + } + } + return returnValues; + } + /** + * Gets the metrics identifier for the specified identifier type. + * @param idType - The identifier type. + * @returns The metrics identifier, or undefined if not found. + */ + getMetricsIdForType(idType) { + return this.cachedMetricsIds[idType]; + } + /** + * Gets the metrics fields for the specified identifier types. + * @param idTypes - The identifier types. + * @returns The metrics fields, or undefined if not found. + */ + getMetricsFieldsForTypes(idTypes) { + const fieldsForTypes = idTypes.map((idType) => { var _a; return (_a = this.cachedMetricsFields[idType]) !== null && _a !== void 0 ? _a : {}; }); + const eventFields = Object.assign({}, ...fieldsForTypes); + if (this.shouldAddDsIdFallbackField && idTypes.indexOf(MetricsIdentifierType.user) !== -1) { + this.addDsIdFallbackFieldIfNecessary(eventFields); + } + return eventFields; + } + /** + * Adds a DSID fallback field to the event fields if necessary. + * @param eventFields - The event fields. + */ + addDsIdFallbackFieldIfNecessary(eventFields) { + const existingUserId = eventFields[MetricsIdentifierType.user]; + const isExistingUserIdInvalid = isNothing(existingUserId) || + typeof existingUserId !== "string" || + existingUserId.length === 0 || + existingUserId.length === MetricsIdentifiersCache.clientGeneratedUserIdLength; + if (isExistingUserIdInvalid && isSome(this.userDsId) && this.userDsId.length > 0) { + eventFields["dsId"] = this.userDsId; + } + } + /** + * Allows the setting of an app-level canonical account override for downstream metrics computations + * @param canonicalAccountIdentifier - The identifier to use when operated with canonical account overides + */ + setCanonicalAccountIdentifierOverride(canonicalAccountIdentifier) { + if (canonicalAccountIdentifier.length >= 0) { + this.cachedMetricsFields[MetricsIdentifierType.canonical] = { + [MetricsIdentifierType.canonical]: canonicalAccountIdentifier, + }; + } + } +} +/** + * The metatype of the MetricsIdentifiersCache class. + */ +MetricsIdentifiersCache.defaultMetatype = makeMetatype("app-store:metricsIdentifiersCache"); +MetricsIdentifiersCache.paymentMetatype = makeMetatype("app-store:paymentMetricsIdentifiersCache"); +MetricsIdentifiersCache.personalizationMetatype = makeMetatype("app-store:personalizationMetricsIdentifiersCache"); +/** + * The length of a client generated user ID. + */ +MetricsIdentifiersCache.clientGeneratedUserIdLength = 24; +export class MockMetricsIdentifiersCache extends MetricsIdentifiersCache { + constructor() { + super(); + this.cachedMetricsIds = { + [MetricsIdentifierType.client]: "1a2b3c4d5e", + [MetricsIdentifierType.user]: "1a2b3c4d5e", + }; + this.cachedMetricsFields = { + [MetricsIdentifierType.client]: { + clientId: "1a2b3c4d5e", + }, + [MetricsIdentifierType.user]: { + userNs: "mockuserNs", + userId: "1a2b3c4d5e", + metricsId: 2, + }, + [MetricsIdentifierType.canonical]: { + canonicalAccountIdentifierOverride: "mockCanonicalAccountIdentifier", + }, + }; + } +} +//# sourceMappingURL=metrics-identifiers-cache.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/foundation/network/http.js b/node_modules/@jet-app/app-store/tmp/src/foundation/network/http.js new file mode 100644 index 0000000..34c6a9a --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/foundation/network/http.js @@ -0,0 +1,42 @@ +/** + * Created by keithpk on 12/3/16. + */ +/** + * The `FormBuilder` class can be used to construct HTTP + * the form parameters for use with an HTTP post operation. + */ +export class FormBuilder { + /** + * The content type to use for form parameters. + */ + static get contentType() { + return "application/x-www-form-urlencoded"; + } + /** + * Construct an empty form builder. + */ + constructor() { + this._params = ""; + } + /** + * Append a parameter to the builder's form parameters. + * @param key The key of the parameter. + * @param value The value of the parameter. + * @return The builder. + */ + param(key, value) { + if (key && value) { + const separator = this._params.length > 0 ? "&" : ""; + this._params += `${separator}${encodeURIComponent(key)}=${encodeURIComponent(value)}`; + } + return this; + } + /** + * Create and return a form parameters string based on the contents of the builder. + * @return The form parameters string. + */ + build() { + return this._params; + } +} +//# sourceMappingURL=http.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/foundation/network/network.js b/node_modules/@jet-app/app-store/tmp/src/foundation/network/network.js new file mode 100644 index 0000000..db302e7 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/foundation/network/network.js @@ -0,0 +1,123 @@ +/** + * Created by ls on 9/7/2018. + * + * This `network.ts` is the NON-MEDIA API arm of network fetch requests. + * It is built on `Network` object and provides standard functionality, such as: + * 1. Parsing the body into specific format. + * 2. Adding timing metrics onto blob. + * + * This should *only* be used for objects that should have timing metrics, i.e. requests to Non-MediaAPI endpoints + * that will ultimately render some whole page. Otherwise, use `objectGraph.network.fetch` directly. + * + * @see `src/media/network.ts` for fetching from Media API endpoints + */ +import { isSome } from "@jet/environment/types/optional"; +import * as serverData from "../json-parsing/server-data"; +/** @public */ +// eslint-disable-next-line @typescript-eslint/no-namespace +export var ResponseMetadata; +(function (ResponseMetadata) { + ResponseMetadata.requestedUrl = "_jet-internal:metricsHelpers_requestedUrl"; + /** + * Symbol used to place timing metrics values onto fetch responses + * without interfering with the data returned by the server. + */ + ResponseMetadata.timingValues = "_jet-internal:metricsHelpers_timingValues"; + /** + * Key used to access the page information gathered from a response's headers + */ + ResponseMetadata.pageInformation = "_jet-internal:metricsHelpers_pageInformation"; + /** + * Key used to access the content max-age gathered from a response's headers. + */ + ResponseMetadata.contentMaxAge = "_jet-internal:responseMetadata_contentMaxAge"; +})(ResponseMetadata || (ResponseMetadata = {})); +/** + * Module's private fetch implementation built off `net` global. + * + * @param {FetchRequest} request describes fetch request. + * @param {(value: string) => Type} parser Some function parsing response body `string` into specific type. + * @returns {Promise<Type>} Promise resolving to specific object. + * @throws {Error} Throws error if status code of request is not 200. + * + * @note Similar to `fetchWithToken` in `media` module, but excludes media token specific functionality. + * Top level data fetches to endpoints that don't do redirects, and can benefit from metrics should + * call methods that build off of this instead of calling `objectGraph.network.fetch(...)` directly. + */ +async function fetch(objectGraph, request, parser) { + const response = await objectGraph.network.fetch(request); + if (!response.ok) { + throw Error(`Bad Status code ${response.status} for ${request.url}`); + } + const parseStartTime = Date.now(); + const result = parser(response.body); + const parseEndTime = Date.now(); + if (result) { + // Build full network timing metrics. + const completeTimingMetrics = networkTimingMetricsWithParseTime(response.metrics, parseStartTime, parseEndTime); + if (serverData.isDefinedNonNull(completeTimingMetrics)) { + result[ResponseMetadata.timingValues] = completeTimingMetrics; + } + } + result[ResponseMetadata.requestedUrl] = request.url.toString(); + return result; +} +/** + * Fetch from an endpoint with JSON response body. + * + * @param {FetchRequest} request to fetch from endpoint with JSON response.. + * @returns {Promise<Type>} Promise resolving to body of response parsed as `Type`. + * @throws {Error} Throws error if status code of request is not 200. + */ +export async function fetchJSON(objectGraph, request) { + return await fetch(objectGraph, request, (body) => { + if (isSome(body)) { + return JSON.parse(body); + } + else { + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + return {}; + } + }); +} +/** + * Fetch from an endpoint with XML response body. + * + * @param {FetchRequest} request to fetch from endpoint with XML response. + * @returns {Promise<Type>} Promise resolving to body of response parsed as `Type`. + * @throws {Error} Throws error if status code of request is not 200. + */ +export async function fetchPlist(objectGraph, request) { + return await fetch(objectGraph, request, (body) => { + if (isSome(body)) { + return objectGraph.plist.parse(body); + } + else { + throw new Error(`Could not fetch Plist, response body was not defined for ${request.url}`); + } + }); +} +/** + * With network requests now being created and parsed in JS, different timing metrics are measured in both Native and JS. + * This function populates the missing values from `HTTPTimingMetrics`'s native counterpart, `JSNetworkPerformanceMetrics`. + * + * @param {HTTPTimingMetrics[] | null} responseMetrics Array of response metrics provided by native. + * @param {number} parseStartTime Time at which response body string parse began in JS. + * @param {number} parseEndTime Time at which response body string parse ended in JS. + * @returns {HTTPTimingMetrics | null} Fully populated timing metrics, or `null` if native response provided no metrics events to build off of. + */ +function networkTimingMetricsWithParseTime(responseMetrics, parseStartTime, parseEndTime) { + // No metrics events to build from. + if (serverData.isNull(responseMetrics) || responseMetrics.length === 0) { + return null; + } + // Append parse times to first partial timing metrics from native. + const firstPartialTimingMetrics = { + ...responseMetrics[0], + parseStartTime: parseStartTime, + parseEndTime: parseEndTime, + }; + // Timing metrics with all properties populated. + return firstPartialTimingMetrics; +} +//# sourceMappingURL=network.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/foundation/network/url-constants.js b/node_modules/@jet-app/app-store/tmp/src/foundation/network/url-constants.js new file mode 100644 index 0000000..f25d9cb --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/foundation/network/url-constants.js @@ -0,0 +1,452 @@ +/** + * Copied from SKUIUrl these action types come in urls like scheme:/?action=[UrlAction] + */ +/* tslint:disable:variable-name */ +export const Protocol = { + /// Protocol used for internal app store routing + internal: "x-as3-internal", + /// Protocol used for routing HTTP URLs + http: "http", + /// Protocol used for routing standard HTTPS URLs + https: "https", + /// The protocol used for iTunes app urls + itms: "itms", + /// The protocol used for secure iTunes app urls + itmss: "itmss", + /// Protocol used to target the app store + itmsAppss: "itms-appss", + /// Protocol used to target the app store + itmsApps: "itms-apps", + /// Protocol used to target the messages app store + itmsMessagess: "itms-messagess", + /// Protocol used to target the messages app store + itmsMessages: "itms-messages", + /// Protocol used to target the watch app store + itmsWatchs: "itms-watchs", + /// Protocol used to target the watch app store + itmsWatch: "itms-watch", + /// Protocol used for internal file routing + file: "file", + /// Protocol used for referencing resources in the host app's bundles. + resource: "resource", + /// Protocol used to target the mac app store + macappstore: "macappstore", + /// Protocol used to target the mac app store + macappstores: "macappstores", + /// Legacy protocol used to target the tv app store, + /// kept to maintain backwards compatibility. + tvappstoreLegacy: "com.apple.tvappstore", + /// Protocol used to target the tv app store + tvappstore: "com.apple.TVAppStore", + /// Protocol used to target Arcade + arcade: "com.apple.Arcade", + /// Protocol used to target StoreKitUI App Store service + storeKitUIServiceAppStore: "appstore-ui", +}; +export const Path = { + // Path for loading a shelf + shelf: "shelf", + // Path for MZStore requests + store: "WebObjects/MZStore.woa/wa", + // Path for MZStoreElements requests + storeElements: "WebObjects/MZStoreElements.woa/wa", + // Path for performing a lookup + lookup: "lookup", + // Path for performing a docTypeLookup + docTypeLookup: "docTypeLookup", + // Path for performing an install sheet + install: "install", + // Path to the grouping page + grouping: "viewGrouping", + // Path to the artist page + viewArtist: "viewArtist", + // Path to the product page + viewSoftware: "viewSoftware", + // Path to the today page + today: "today", + // Path to the arcade main page + arcade: "arcade", + arcadeUpsellPreview: "arcadeUpsellPreview", + // Path to Arcade See All + arcadeSeeAllGames: "arcadeSeeAllGames", + // Pagination path for Arcade See All + arcadeSeeAllGamesLoadMore: "arcadeSeeAllGamesLoadMore", + // Path to the genre page (which is a grouping page) + genre: "genre", + // Path to the genre page (which is a grouping page) + viewGenre: "viewGenre", + recommendationsSeeAll: "recommendationsSeeAll", + // Path for a screenshots lookup. + screenshots: "screenshots", + // Path to a single room + room: "viewRoom", + // Path to a programmed multi-room + multiRoom: "viewMultiRoom", + // Path to the main top charts page + charts: "charts", + // Path to the product page + product: "app", + // Path to the game page + game: "game", + // Path to siri product page deep links on tvOS App Store. + siri: "siri", + // Path to the product page for an app bundle. + productBundle: "app-bundle", + // Path component for the developer page + developer: "developer", + artist: "artist", + // Path component for the ratings shelf + ratings: "ratings", + // Path component for the reviews shelf + reviews: "reviews", + // Path to view reviews for MZStore + viewReviews: "viewContentsUserReviews", + // Path component for the personalized review shelf + personalizedReviews: "personalizedReviews", + // Path to editorial items (articles) + article: "article", + story: "story", + editorialItem: "editorialItem", + viewEditorialItem: "viewEditorialItem", + todayCardPreview: "todayCardPreview", + // Path component for MSO rooms + mso: "mso", + // Path for resetting the storefront + resetAndRedirect: "resetAndRedirect", + // Account + account: "account", + personalizationTransparency: "personalizationTransparency", + // Path component for an href-driven route. + href: "href", + // Path component for the EULA. + eula: "eula", + // Path component for the EULA on tvOS. + tvEula: "tv-eula", + // Path component for the Privacy Policy on tvOS. + privacyPolicy: "privacyPolicy", + // Path component for the Safety and Compliance Page on tvOS. + safetyCompliance: "safety-compliance", + // Path component for recommendations + appsForYou: "apps-for-you", + // Path for the storeFront + storeFront: "storeFront", + // Path for the tab + tab: "tab", + // Path component for the search landing page. + searchLandingPage: "searchLandingPage", + // Path component for search trending content page. + searchTrendingApps: "searchTrendingApps", + // Path for showing product privacy detail page + privacyDetail: "privacyDetail", + // Path for showing product privacy definitions page + privacyDefinitions: "privacyDefinitions", + // Path for the report a problem link + reportAProblem: "reportaproblem.apple.com/store", + // Path for showing a game center player profile + gameCenterProfile: "gameCenterProfile", + // Path for showing the ODP page + onDeviceRecommendations: "onDeviceRecommendations", + // Path for editorial pages + editorialPage: "editorialPage", + // Path to acessibility details + accessibilityDetails: "accessibilityDetails", + // ---------------------- + // Debug + // ---------------------- + test: "test", + shelfTypes: "shelfTypes", + groupingTest: "grouping", + builtIn: "builtIn", + shelfTextTest: "shelfText", + shelfLockupsTest: "shelfLockups", + shelfMiscTest: "shelfMisc", + lockupTest: "lockupTest", + articleTestArtwork: "articleArtworkTest", + articleTestSingleAppIcon: "articleSingleAppIconTest", + articleTestSingleAppIconHeroArt: "articleSingleAppIconHeroArtTest", + articleTestBrandedApp: "articleBrandedAppTest", + articleTestMultiAppTwo: "articleMultiAppTwoTest", + articleTestMultiAppThree: "articleMultiAppThreeTest", + articleTestGrid: "articleGridTest", + articleTestInAppPurchase: "articleInAppPurchaseTest", +}; +export const Host = { + // Legacy Mac Software Update URL + showUpdatesPage: "showUpdatesPage", + // Legacy Deep Link Actions + showAccountPage: "showAccountPage", + showPurchasesPage: "showPurchasesPage", + showSubscriptionsPage: "showSubscriptionsPage", + // Legacy MAS Safari Extension Search + searchExtensions: "searchExtensions", + // Legacy search.itunes.apple.com redirected urls + searchItunes: "search.itunes.apple.com", + // Legacy itunes.apple.com redirected urls + iTunes: "itunes.apple.com", + // Urls allowing programming to preview programmed groupings + storePreview: "storepreview.apple.com", + // Urls allowing programming to preview programmed stories + appsPreview: "preview.apps.apple.com", + // Legacy product url for tvOS App Store + product: "product", + // Host for arcade subscribe page + arcadeSubscribePage: "arcadeSubscribePage", + // Host for an upsell that must necessarily go through our own Arcade subscribe page, + // rather than through an AMS marketing item page. + arcadeSubscribePageCustomContext: "arcadeSubscribePageCustomContext", + // Host for arcade welcome page + arcadeWelcomePage: "arcadeWelcomePage", + // Host for Arcade See All page for standalone case. + arcadeSeeAllPage: "arcadeSeeAllGames", + // Host for the deep link from an Arcade app clip. + appClipSubscribe: "appClipSubscribe", + familyCircle: "familyCircle", + // Host for showing spam blocking extensions + spamBlockingExtensions: "spamBlockingExtensions", + // Host for showing Safari extensions + safariExtensions: "safariExtensions", + // Host for app launch trampoline + launchApp: "launchApp", + // Host for SharePlay more deeplink + sharePlayApps: "sharePlayApps", + // Host for opening a deeplink at the end of Buddy + buddyOnboarding: "buddyOnboarding", + // Host for Arcade download (starter) pack page + arcadeDownloadPackPage: "arcadeDownloadPack", + // Host for showing settings page + showSettingsPage: "showSettingsPage", + // Host for showing settings page + showHiddenPurchasesPage: "showHiddenPurchases", +}; +/// The different preview hosts that require the media api preview endpoint +export const previewHosts = new Set([Host.storePreview, Host.appsPreview]); +export const Parameters = { + // Generic ID + id: "id", + // Generic IDs + ids: "ids", + // Product variant ID for given `id` + productVariantID: "ppid", + // Country Code + countryCode: "cc", + // Language override + language: "l", + // Featured Content ID + featuredContentId: "fcId", + fetchData: "fetchData", + isTodaySection: "isTodaySection", + isTodayFeedPreview: "isTodayFeedPreview", + // Genre + genre: "genre", + // Bundle Identifier + bundleIdentifier: "bundleIdentifier", + // BundleID + bundleId: "bundleId", + // Spotlight In-App Purchase Identifier + offerName: "offerName", + // Top Charts + charts: "charts", + ages: "ages", + chart: "chart", + types: "types", + // v0 urls (used e.g. when redirected from MZContentLink URLs). + v0: "v0", + // Action urls + action: "action", + // Type parameter + type: "type", + context: "context", + isArcade: "isArcade", + isSubscribed: "isSubscribed", + isTrialAvailable: "isTrialAvailable", + isTrialEnrolled: "isTrialEnrolled", + groupingFeaturedContentId: "groupingFeaturedContentId", + editorialPageShelfType: "editorialPageShelfType", + nativeGroupingShelfId: "nativeGroupingShelfId", + isArcadeSeeAllGamesShelf: "isArcadeSeeAllGamesShelf", + isGameCenterActivityFeedShelf: "isGameCenterActivityFeedShelf", + isGameCenterPlayerShelf: "isGameCenterPlayerShelf", + isGameCenterPlayerRibbonItem: "isGameCenterPlayerRibbonItem", + isGameCenterAchievementsShelf: "isGameCenterAchievementsShelf", + isGameCenterContinuePlayingShelf: "isGameCenterContinuePlayingShelf", + isGameCenterPopularWithYourFriendsShelf: "isGameCenterPopularWithYourFriendsShelf", + isGameCenterSuggestedFriendsShelf: "isGameCenterSuggestedFriendsShelf", + isGameCenterReengagementShelf: "isGameCenterReengagementShelf", + isOnDeviceRecommendationsShelf: "isOnDeviceRecommendationsShelf", + isOnDeviceSearchHistoryShelf: "isOnDeviceSearchHistoryShelf", + isSearchFocusHeaderShelf: "isSearchFocusHeaderShelf", + isArcadeDownloadPackShelfPlaceholder: "isArcadeDownloadPackShelfPlaceholder", + onDeviceRecommendationsUseCase: "onDeviceRecommendationsUseCase", + onDevicePersonalizationUseCase: "onDevicePersonalizationUseCase", + // Denotes that this url is coming from the purchases page. + isPurchasesApp: "isPurchasesApp", + // Used for single sign on, the parameter is passed from store kit + isViewOnly: "isViewOnly", + // Determines whether unlisted apps should be returned in the response. + // Note that for direct lookups, MAPI allows the App Store to view unlisted + // apps without this parameter, although they would like to eventually gate + // the behavior behind this parameter. + includeUnlistedApps: "includeUnlistedApps", + enabled: "enabled", + href: "href", + recoMetrics: "recoMetrics", + // Used on article urls when the TodayCard is showing fallback media + showingFallbackMedia: "showingFallbackMedia", + path: "path", + useReleaseId: "useReleaseId", + // The client identifier that should be used to display the item, regardless of host device. + clientIdentifierOverride: "clientIdentifierOverride", + // The message attached to the subscribe page, for ATB. + subscribePageMessage: "message", + // The ID for a specific editorial item, in the case of a subscribe page. + editorialItem: "editorialItem", + // The identifer for the ATB request. + askToBuyId: "askToBuyId", + // Indicates an ID, when the 'app' disambiguation is necessary. + appId: "appId", + // Whether the URL has been launched from a PPT (Purple Performance Test). + isPPT: "isPPT", + // How the shelf is sorted, represented by a value of `ArcadeSeeAllGamesPageSort` + sort: "sort", + // Featured Content ID + grouping: "grouping", + // Code Parameter, e.g. for Redeem code + code: "code", + // Parameter to include attribution on offer action for post subcribge + includePostSubscribeAttribution: "includePostSubscribeAttribution", + // Campaign Token + campaignToken: "ct", + // Provider Token + providerToken: "pt", + // Q Token + qToken: "its_qt", + // Advertisement ID + advertisementId: "adId", + // The token used to pass arbitrary data in a url + token: "token", + // The current parse context for the a page + parseContext: "parseContext", + // An ID for a privacy type + privacyTypeId: "privacyTypeId", + // The token used to pass arbitrary data in a url + requestDescriptor: "requestDescriptor", + // Page Facet + ageRating: "ageRating", + // Page Facet + controllerSupport: "controllerSupport", + // Page Facet + multiplayerSupport: "multiplayerSupport", + // Page Facet + comingSoon: "comingSoon", + // Page Facet + binCompatGames: "binCompatGames", + // Page Facet + gamePreviews: "gamePreviews", + // Upsell (marketing item) marketing hint + offerHints: "offerHints", + // Indicates that a 204 network error should invalidate the App Store's widgets. + invalidateWidgetsOnFailure: "invalidateWidgetsOnFailure", + // Metrics + metrics: "metrics", + // App event ID + appEventId: "eventid", + // Offer Item ID + offerItemId: "offerItemId", + // App event deep link + appEventDeepLink: "deepLink", + // The use case param for collection deeplinking + useCaseShort: "uc", + // The collection id param for collection deeplinking + collectionId: "collection-id", + // The seed id param for collection deeplinking + seedId: "seed-id", + // Whether the shelf is the product page similar items shelf. + isShelfWithAd: "isShelfWithAd", + // The ad placement type for the shelf. Only used if `isShelfWithAd` is true. + shelfWithAdPlacementType: "shelfWithAdPlacementType", + // Provides a refresh type to route a refresh request for a shelf. + // See `refreshUrl` on `Shelf`. + shelfRefreshType: "refreshType", + // Param to denote that this url originated for preloading purposes. + isPreloading: "isPreloading", + // The name parameter for editorial pages with known names + name: "name", + // The editorial page shelf type + shelfType: "shelfType", + // The id of the shelf in a url + shelfId: "shelfId", + // Parameter for the onboarding cards for the today tab + onbaordingCardIds: "onboardingCardIds", + // Parameter for the today card previews to provide a future date and time + preview: "preview", + // The card config for used for the card on a today page + todayCardConfig: "todayCardConfig", + // The parameter that is expanded into the header used for today previews. + experimentId: "experimentId", + shortEditorialNotes: "shortEditorialNotes", + // User subscription status defined by Mercury and passed to Arcade download pack onboarding. + // It is used to select the right copy variant. + arcadeSubscriptionStatus: "subscriptionStatus", + // Used to indicate a web browser is being displayed in the web browser context via App Store Components. + webBrowser: "webBrowser", + // The id of the editorial-page that was used to create a url + editorialPageId: "editorialPageId", + // A filter ID to add to a editorial item request in order to fetch a certain version of the story. + editorialCardId: "filter[canvas:cardId]", + // A filter to add to the hydration call for the Today tab recommended candidates to fetch only recommendable apps. + filterRecommendable: "filter[recommendable]", + // A device family, used for accessibility labels + deviceFamily: "deviceFamily", +}; +// Install Page-specific parameters +export const InAppPurchaseInstallPageParameters = { + // The adamId for the IAP. + inAppPurchaseId: "inAppPurchaseId", +}; +// Product Page-specific parameters +export const ProductPageParameters = { + // Product URL + url: "productUrl", + // Whether an iAP is a subscription. + isSubscription: "isSubscription", + // The minimum version of the app to display + // Typically used to prevent older cached versions being displayed + // when deep-linking from an app clip. + minExternalVersionId: "minExternalVersionId", +}; +// Review Page-specific parameters +export const ReviewsPageParameters = { + // Product adamId + adamId: "adamId", + // Sort index for the reviews request + sort: "sort", +}; +export const MSOPageParameters = { + // Ids of available apps. + availableAdamIds: "availableAdamIds", +}; +export const ShelfParameters = { + // Shelf Title + title: "shelfTitle", + // Whether the shelf should auto-configure a see-all on fetch + shouldInferSeeAllFromFetchedItems: "shelfShouldInferSeeAllFromFetchedItems", + // Shelf content type + contentType: "shelfContentType", + // Shelf offer them + offerTheme: "shelfOfferTheme", + // metrics page information + metricsPageInformation: "metricsPage", +}; +export const ShareURLParameters = { + // Which client should be the link be opened with + clientSpecifier: "app", +}; +export const Hashes = { + // Reviews action anchor (#reviews) + reviews: "reviews", +}; +export const ShelfRefreshType = { + productPageSimilarItems: "productPageSimilarItems", +}; +/* tslint:enable:variable-name */ +//# sourceMappingURL=url-constants.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/foundation/network/urls.js b/node_modules/@jet-app/app-store/tmp/src/foundation/network/urls.js new file mode 100644 index 0000000..6abbd37 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/foundation/network/urls.js @@ -0,0 +1,382 @@ +/** + * Created by keithpk on 12/2/16. + */ +import { isNothing } from "@jet/environment/types/optional"; +const protocolRegex = /^([a-z][a-z0-9.+-]*:)(\/\/)?([\S\s]*)/i; +const queryParamRegex = /([^=?&]+)=?([^&]*)/g; +const componentOrder = ["hash", "query", "pathname", "host"]; +function splitUrlComponent(input, marker, style) { + const index = input.indexOf(marker); + let result; + let remainder = input; + if (index !== -1) { + const prefix = input.slice(0, index); + const suffix = input.slice(index + marker.length, input.length); + if (style === "prefix") { + result = prefix; + remainder = suffix; + } + else { + result = suffix; + remainder = prefix; + } + } + // log("Token: " + marker + " String: " + input, " Result: " + result + " Remainder: " + remainder) + return { + result: result, + remainder: remainder, + }; +} +export class URL { + constructor(url) { + this.query = {}; + if (!url) { + return; + } + // Split the protocol from the rest of the urls + let remainder = url; + const match = protocolRegex.exec(url); + if (match) { + // Pull out the protocol + let protocol = match[1]; + if (protocol) { + protocol = protocol.split(":")[0]; + } + this.protocol = protocol; + // Save the remainder + remainder = match[3]; + } + // Then match each component in a specific order + let parse = { remainder: remainder, result: undefined }; + for (const component of componentOrder) { + if (!parse.remainder) { + break; + } + switch (component) { + case "hash": { + parse = splitUrlComponent(parse.remainder, "#", "suffix"); + this.hash = parse.result; + break; + } + case "query": { + parse = splitUrlComponent(parse.remainder, "?", "suffix"); + if (parse.result) { + this.query = URL.queryFromString(parse.result); + } + break; + } + case "pathname": { + parse = splitUrlComponent(parse.remainder, "/", "suffix"); + if (parse.result) { + // Replace the initial /, since paths require it + this.pathname = "/" + parse.result; + } + break; + } + case "host": { + if (parse.remainder) { + const authorityParse = splitUrlComponent(parse.remainder, "@", "prefix"); + const userInfo = authorityParse.result; + const hostPort = authorityParse.remainder; + if (userInfo) { + const userInfoSplit = userInfo.split(":"); + this.username = decodeURIComponent(userInfoSplit[0]); + this.password = decodeURIComponent(userInfoSplit[1]); + } + if (hostPort) { + const hostPortSplit = hostPort.split(":"); + this.host = hostPortSplit[0]; + this.port = hostPortSplit[1]; + } + } + break; + } + default: { + throw new Error("Unhandled case!"); + } + } + } + } + set(component, value) { + if (!value) { + return this; + } + if (component === "query") { + if (typeof value === "string") { + value = URL.queryFromString(value); + } + } + switch (component) { + // Exhaustive match to make sure TS property minifiers and other + // transformer plugins do not break this code. + case "protocol": + this.protocol = value; + break; + case "username": + this.username = value; + break; + case "password": + this.password = value; + break; + case "port": + this.port = value; + break; + case "pathname": + this.pathname = value; + break; + case "query": + this.query = value; + break; + case "hash": + this.hash = value; + break; + default: + // The fallback for component which is not a property of URL object. + this[component] = value; + break; + } + return this; + } + get(component) { + switch (component) { + // Exhaustive match to make sure TS property minifiers and other + // transformer plugins do not break this code. + case "protocol": + return this.protocol; + case "username": + return this.username; + case "password": + return this.password; + case "port": + return this.port; + case "pathname": + return this.pathname; + case "query": + return this.query; + case "hash": + return this.hash; + default: + // The fallback for component which is not a property of URL object. + return this[component]; + } + } + append(component, value) { + const existingValue = this.get(component); + let newValue; + if (component === "query") { + if (typeof value === "string") { + value = URL.queryFromString(value); + } + if (typeof existingValue === "string") { + newValue = { existingValue, ...value }; + } + else { + newValue = { ...existingValue, ...value }; + } + } + else { + let existingValueString = existingValue; + if (!existingValueString) { + existingValueString = ""; + } + let newValueString = existingValueString; + if (component === "pathname") { + const pathLength = existingValueString.length; + if (!pathLength || existingValueString[pathLength - 1] !== "/") { + newValueString += "/"; + } + } + // eslint-disable-next-line @typescript-eslint/restrict-plus-operands, @typescript-eslint/no-base-to-string + newValueString += value; + newValue = newValueString; + } + return this.set(component, newValue); + } + param(key, value) { + if (!key) { + return this; + } + if (!this.query) { + this.query = {}; + } + this.query[key] = value; + return this; + } + removeParam(key) { + if (!key || !this.query) { + return this; + } + if (this.query[key] !== undefined) { + delete this.query[key]; + } + return this; + } + /** + * Push a new string value onto the path for this url + * @returns URL this object with the updated path. + */ + path(value) { + return this.append("pathname", value); + } + pathExtension() { + var _a; + // Extract path extension if one exists + if (isNothing(this.pathname)) { + return null; + } + const lastFilenameComponents = (_a = this.pathname + .split("/") + .filter((item) => item.length > 0) // Remove any double or trailing slashes + .pop()) === null || _a === void 0 ? void 0 : _a.split("."); + if (lastFilenameComponents === undefined) { + return null; + } + if (lastFilenameComponents.filter((part) => { + return part !== ""; + }).length < 2 // Remove any empty parts (e.g. .ssh_config -> ["ssh_config"]) + ) { + return null; + } + return lastFilenameComponents.pop(); + } + /** + * Returns the path components of the URL + * @returns An array of non-empty path components from `urls`. + */ + pathComponents() { + if (!this.pathname) { + return []; + } + return this.pathname.split("/").filter((component) => component.length > 0); + } + /** + * Returns the last path component from this url, updating the url to not include this path component + * @returns String the last path component from this url. + */ + popPathComponent() { + if (!this.pathname) { + return null; + } + const lastPathComponent = this.pathname.slice(this.pathname.lastIndexOf("/") + 1); + if (lastPathComponent.length === 0) { + return null; + } + this.pathname = this.pathname.slice(0, this.pathname.lastIndexOf("/")); + return lastPathComponent; + } + /** + * Same as toString + * + * @returns {string} A string representation of the URL + */ + build() { + return this.toString(); + } + /** + * Converts the URL to a string + * + * @returns {string} A string representation of the URL + */ + toString() { + let url = ""; + if (this.protocol) { + url += this.protocol + "://"; + } + if (this.username) { + url += encodeURIComponent(this.username); + if (this.password) { + url += ":" + encodeURIComponent(this.password); + } + url += "@"; + } + if (this.host) { + url += this.host; + if (this.port) { + url += ":" + this.port; + } + } + if (this.pathname) { + url += this.pathname; + /// Trim off trailing path separators when we have a valid path + /// We don't do this unless pathname has elements otherwise we will trim the `://` + if (url.endsWith("/") && this.pathname.length > 0) { + url = url.slice(0, -1); + } + } + if (this.query && Object.keys(this.query).length) { + url += "?" + URL.toQueryString(this.query); + } + if (this.hash) { + url += "#" + this.hash; + } + return url; + } + // ---------------- + // Static API + // ---------------- + /** + * Converts a string into a query dictionary + * @param query The string to parse + * @returns The query dictionary containing the key-value pairs in the query string + */ + static queryFromString(query) { + const result = {}; + let parseResult = queryParamRegex.exec(query); + while (parseResult) { + const key = decodeURIComponent(parseResult[1]); + const value = decodeURIComponent(parseResult[2]); + result[key] = value; + parseResult = queryParamRegex.exec(query); + } + return result; + } + /** + * Converts a query dictionary into a query string + * + * @param query The query dictionary + * @returns {string} The string representation of the query dictionary + */ + static toQueryString(query) { + let queryString = ""; + let first = true; + for (const key of Object.keys(query)) { + if (!first) { + queryString += "&"; + } + first = false; + queryString += encodeURIComponent(key); + const value = query[key]; + if (value && value.length) { + queryString += "=" + encodeURIComponent(value); + } + } + return queryString; + } + /** + * Convenience method to instantiate a URL from a string + * @param url The URL string to parse + * @returns {URL} The new URL object representing the URL + */ + static from(url) { + return new URL(url); + } + /** + * Convenience method to instantiate a URL from numerous (optional) components + * @param protocol The protocol type + * @param host The host name + * @param path The path + * @param query The query + * @param hash The hash + * @returns {URL} The new URL object representing the URL + */ + static fromComponents(protocol, host, path, query, hash) { + const url = new URL(); + url.protocol = protocol; + url.host = host; + url.pathname = path; + url.query = query; + url.hash = hash; + return url; + } +} +//# sourceMappingURL=urls.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/foundation/routing/routing-components.js b/node_modules/@jet-app/app-store/tmp/src/foundation/routing/routing-components.js new file mode 100644 index 0000000..28fdc48 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/foundation/routing/routing-components.js @@ -0,0 +1,318 @@ +/** + * Created by km on 11/16/16. + */ +import * as urls from "../network/urls"; +import * as urlUtil from "./url-util"; +// endregion +// region private URLRule helpers. +/** + * Checks whether or not a given _pathComponents component contains a parameter. + * @param pathComponent The _pathComponents component to check. + * @returns true if the _pathComponents component is surrounded by curly braces; false otherwise. + */ +function isPathComponentParameter(pathComponent) { + const parameterStartIndex = pathComponent.indexOf("{"); + const parameterEndIndex = pathComponent.indexOf("}"); + return parameterStartIndex >= 0 && parameterEndIndex > parameterStartIndex + 1; +} +/** + * Extracts the parameter contained in a _pathComponents component. + * @param pathComponent A _pathComponents component surrounded by curly braces. + * @returns The parameter contained in the component. + */ +function getPathComponentParameter(pathComponent) { + const parameterStartIndex = pathComponent.indexOf("{"); + const parameterEndIndex = pathComponent.indexOf("}"); + const pathHasRequiredCurlyBraces = parameterStartIndex >= 0 && parameterEndIndex > parameterStartIndex; + return pathHasRequiredCurlyBraces + ? pathComponent.substring(parameterStartIndex + 1, parameterEndIndex) + : pathComponent; +} +/** + * Extracts the value from a path component, for a given internal key example: + * Path Component: "id123456" + * Internal Key: "id{id}" + * Return Value: "123456" + * @param pathComponent A _pathComponents component surrounded by curly braces. + * @returns The parameter contained in the component. + */ +export function getPathComponentParameterValueUsingInternalKey(pathComponent, internalKey) { + const valueStartIndex = internalKey.indexOf("{"); + const valueEndIndex = pathComponent.length - (internalKey.length - (internalKey.indexOf("}") + 1)); + const pathHasRequiredCurlyBraces = valueStartIndex >= 0 && valueEndIndex > valueStartIndex; + return pathHasRequiredCurlyBraces ? pathComponent.substring(valueStartIndex, valueEndIndex) : pathComponent; +} +/** + * Creates a mapping from key to _pathComponents component index + * for efficiently extracting parameters from a _pathComponents. + * @param rulePath The _pathComponents to create a mapping for. + * @returns A map of keys to _pathComponents component indexes. + */ +function makePathParameterMapping(rulePath) { + const mapping = {}; + rulePath.forEach((ruleComponent, index) => { + if (isPathComponentParameter(ruleComponent)) { + mapping[ruleComponent] = index; + } + }); + return mapping; +} +/** + * Normalizes a given protocol string for matching. + * @param protocol The protocol to match against. + * @returns The `protocol` with colon added if needed. + */ +function normalizeProtocol(protocol) { + // An empty string is falsy. + if (protocol === null || protocol === undefined) { + return null; + } + return protocol; +} +/** + * Creates `UrlRouteQuery` objects from substring of url. + * ? = optional + * -caseInsensitive = case insensitive + * @param parameters strings of form `<key>[?][-i]=<value>`. + * @returns Array of `UrlRouteQuery` objects. + */ +function parseQuery(parameters) { + const parsedQuery = []; + if (!parameters) { + return parsedQuery; + } + for (const param of parameters) { + const parts = param.split("="); + let key = parts[0]; + const optional = key.indexOf("?") !== -1; + key = key.replace("?", ""); + const caseInsensitive = key.indexOf("-caseInsensitive") !== -1; + key = key.replace("-caseInsensitive", ""); + let value = null; + if (parts.length > 1) { + value = decodeURIComponent(parts[1]); + } + parsedQuery.push({ + key, + value, + optional, + caseInsensitive, + }); + } + return parsedQuery; +} +// endregion +// region Url Rule +/** + * The `UrlRule` class extracts the pattern format from `UrlRuleDefinition`s, and encapsulates + * the information needed to match against a candidate URL and extract parameters from it. + * + * The terminology here is: + * - rule: A specific url pattern. + * - route: A group of rules that together form a single route, i.e. UrlRule[]. + */ +export class UrlRule { + /** + * Construct the route with all required properties. + * @param rule The rule to match. + */ + constructor(rule) { + this.identifier = rule.identifier; + this._protocol = normalizeProtocol(rule.protocol); + this._hostName = rule.hostName; + if (rule.path) { + this._pathComponents = rule.path.split("/").filter((component) => component.length > 0); + this._pathParameterMap = makePathParameterMapping(this._pathComponents); + } + else { + this._pathComponents = null; + this._pathParameterMap = null; + } + this._pathExtension = rule.pathExtension; + this._query = parseQuery(rule.query); + this._hash = rule.hash; + this._regex = rule.regex; + if (rule.exclusions) { + this._exclusions = rule.exclusions.map(function (ex) { + return new UrlRule(ex); + }); + } + else { + this._exclusions = null; + } + } + /** + * Checks whether or not the route matches a given URL. + * @param url The URL to check against. + * @returns true if the route matches `urls`; false otherwise. + */ + matches(url) { + var _a, _b; + if (this._regex) { + if (!this._regex.length) { + // If the rule specifies regex but does not supply patterns, we need to return false. Otherwise, we will + // risk matching against everything. This is because an empty regex with no other rule parameters will + // cause us to fallthrough to the end and match against all URLs. + return false; + } + let didMatchRegex = false; + for (const regexPattern of this._regex) { + if (regexPattern.test(url.toString())) { + // If we match against any of regex patterns, then we should proceed. + // If no matches are found, then this rule is not matched. + didMatchRegex = true; + break; + } + } + if (!didMatchRegex) { + return false; + } + } + if (this._protocol && url.protocol !== this._protocol) { + return false; + } + if (this._hostName && url.host !== this._hostName) { + return false; + } + if (this._pathComponents) { + const rulePathComponents = this._pathComponents; + const urlPathComponents = url.pathComponents(); + if (rulePathComponents.length !== urlPathComponents.length) { + return false; + } + // We're iterating two arrays here, an old style for-loop is appropriate + const length = rulePathComponents.length; + for (let i = 0; i < length; i++) { + const ruleComponent = rulePathComponents[i]; + if (isPathComponentParameter(ruleComponent)) { + // component parameters always match + continue; + } + const urlComponent = urlPathComponents[i]; + if (ruleComponent !== urlComponent) { + return false; + } + } + } + if (this._pathExtension) { + if (url.pathExtension() !== this._pathExtension) { + return false; + } + } + if (this._query) { + for (const param of this._query) { + let value; + if (param.caseInsensitive) { + for (const [queryKey, queryValue] of Object.entries((_a = url.query) !== null && _a !== void 0 ? _a : {})) { + if (param.key.toLocaleLowerCase() === queryKey.toLocaleLowerCase()) { + value = queryValue; + } + } + } + else { + value = (_b = url.query) === null || _b === void 0 ? void 0 : _b[param.key]; + } + if (!value && !param.optional) { + return false; + } + if (param.value && param.value !== value) { + return false; + } + } + } + if (this._hash && url.hash !== this._hash) { + return false; + } + if (this._exclusions) { + for (const exclusionRule of this._exclusions) { + if (exclusionRule._exclusions) { + throw Error("Matching exclusion rules with further exclusion rules may introduce significant code-complexity and/or reduce the ease with which developers are able to reason about your desired goals. Are there any simpler options?"); + } + if (exclusionRule.matches(url)) { + return false; + } + } + } + return true; + } + /** + * Extract information from a matching url. + * @param matchingUrl The url to extract parameters from. + * @returns `Parameters` extracted from `matchingUrl` + * @note This function is only valid when `this.matches(matchingUrl) === true`. + */ + extractParameters(matchingUrl) { + var _a, _b; + const parameters = {}; + if (this._pathComponents !== null && this._pathParameterMap !== null) { + const urlPathComponents = matchingUrl.pathComponents(); + for (const internalKey of Object.keys(this._pathParameterMap)) { + const externalKey = getPathComponentParameter(internalKey); + const index = this._pathParameterMap[internalKey]; + const parameterValue = getPathComponentParameterValueUsingInternalKey(urlPathComponents[index], internalKey); + parameters[externalKey] = decodeURIComponent(parameterValue); + } + } + if (this._query) { + for (const param of this._query) { + parameters[param.key] = (_b = (_a = matchingUrl.query) === null || _a === void 0 ? void 0 : _a[param.key]) !== null && _b !== void 0 ? _b : undefined; + } + } + return parameters; + } +} +/** + * `UrlRouter` manages a set of url rule templates to allow `urls` to serve as keys for different associated objects (like Builders). + * + * @note This is replaces old `UrlRouter` as a synchronous way match route URLs to handlers. In contrast to the previous implementation, + * it maps entire objects (containing related async handlers and properties) to urls. + */ +export class UrlRouter { + /// Constructs an empty URL router object. + constructor() { + this._routeMappings = []; + } + /** + * Register a new route defined by a set of definitions and object on the router. + * @param routeDefinitions The definitions of rules to register. + * @param object The object for the rule. + */ + associate(routeDefinitions, object) { + const route = []; + for (const definition of routeDefinitions) { + route.push(new UrlRule(definition)); + } + this._routeMappings.push({ route: route, object: object }); + } + /** + * Resolve given url to associated object, if any exist. + * @param urlOrString URL or string representation of url to resolve objects for. + * @returns `UrlRouterResult` containing url, extracted parameters, and associated object, or `null` if no match was found. + */ + routedObjectForUrl(urlOrString) { + let url = typeof urlOrString === "string" ? new urls.URL(urlOrString) : urlOrString; + url = urlUtil.normalizedAppStoreUrl(url); + url = urlUtil.normalizedActionUrl(url); + for (const mapping of this._routeMappings) { + for (const rule of mapping.route) { + if (rule.matches(url)) { + return { + normalizedUrl: url, + parameters: rule.extractParameters(url), + object: mapping.object, + matchedRuleIdentifier: rule.identifier, + }; + } + } + } + // No match. Still return a result with normalized url. + return { + normalizedUrl: url, + parameters: null, + object: null, + matchedRuleIdentifier: null, + }; + } +} +// endregion +//# sourceMappingURL=routing-components.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/foundation/runtime/action-provider.js b/node_modules/@jet-app/app-store/tmp/src/foundation/runtime/action-provider.js new file mode 100644 index 0000000..e0038da --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/foundation/runtime/action-provider.js @@ -0,0 +1,13 @@ +import { isBaseActionProvider } from "@jet/environment/dispatching"; +/** + * Retrieve an {@linkcode ActionModel} for the given {@linkcode intent} through + * any registered `IntentController`s that implement `ActionProvider` + */ +export function actionFor(intent, objectGraph, options = {}) { + const resolvedController = objectGraph.dispatcher.controller(intent); + if (isBaseActionProvider(resolvedController)) { + return resolvedController.actionFor(intent, objectGraph, options); + } + return null; +} +//# sourceMappingURL=action-provider.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/foundation/runtime/app-store-intent-dispatcher.js b/node_modules/@jet-app/app-store/tmp/src/foundation/runtime/app-store-intent-dispatcher.js new file mode 100644 index 0000000..0a88ada --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/foundation/runtime/app-store-intent-dispatcher.js @@ -0,0 +1,62 @@ +import { IntentDispatcher } from "@jet/environment/dispatching"; +import { ExperimentCache } from "../experimentation/experiment-cache"; +import { MetricsIdentifiersCache, MetricsIdentifierType, paymentIdentifierContextMapping, personalizationIdentifierContextMapping, } from "../metrics/metrics-identifiers-cache"; +import { AppStoreObjectGraph } from "./app-store-object-graph"; +export class AppStoreIntentDispatcher { + constructor() { + this.dispatcher = new IntentDispatcher(); + } + // MARK: - IBaseIntentDispatcher + register(controller) { + this.dispatcher.register(controller); + } + async dispatch(intent, objectGraph) { + const intentObjectGraph = await this.createIntentObjectGraphWithAsyncValues(objectGraph); + return await this.dispatcher.dispatch(intent, intentObjectGraph); + } + controller(intent) { + return this.dispatcher.controller(intent); + } + get registeredControllers() { + return this.dispatcher.registeredControllers; + } + async createIntentObjectGraphWithAsyncValues(objectGraph) { + const experimentCache = new ExperimentCache(); + const metricsIdentifiersCache = new MetricsIdentifiersCache(); + const personalizationMetricsIdentifierCache = new MetricsIdentifiersCache(personalizationIdentifierContextMapping); + let paymentMetricsIdentifiersCache; + // Doing these in sequence to help minimize the liklihood of hitting the treatment store locking timeout + // rdar://147518835 (AppStore Ad Regression Performance - Metrics Identifier blocking network requests) + if (objectGraph instanceof AppStoreObjectGraph) { + let appStoreObjectGraph = objectGraph; + // reassigning the bag creates a fresh cache + appStoreObjectGraph = appStoreObjectGraph.addingBag(appStoreObjectGraph.bag.underlyingBag); + await experimentCache.loadTreatments(appStoreObjectGraph); + await metricsIdentifiersCache.loadValues(appStoreObjectGraph, [ + MetricsIdentifierType.client, + MetricsIdentifierType.user, + ]); + // Only load the values if the namespace is in the bag + if (objectGraph.bag.personalizationUserIdEnabled) { + await personalizationMetricsIdentifierCache.loadValues(objectGraph, [MetricsIdentifierType.user]); + } + // The payment topic uses a different metrics identifier cache as it has a separate namespace. + if (appStoreObjectGraph.props.enabled("paymentTopicFromBag") && + appStoreObjectGraph.bag.metricsPaymentNamespaceEnabled) { + paymentMetricsIdentifiersCache = new MetricsIdentifiersCache(paymentIdentifierContextMapping); + // Only add this promise if paymentMetricsIdentifiersCache is actually created + await paymentMetricsIdentifiersCache.loadValues(appStoreObjectGraph, [ + MetricsIdentifierType.client, + MetricsIdentifierType.user, + ]); + } + objectGraph = appStoreObjectGraph; + } + return objectGraph + .adding(ExperimentCache.metatype, experimentCache) + .adding(MetricsIdentifiersCache.defaultMetatype, metricsIdentifiersCache) + .adding(MetricsIdentifiersCache.paymentMetatype, paymentMetricsIdentifiersCache || metricsIdentifiersCache) + .adding(MetricsIdentifiersCache.personalizationMetatype, personalizationMetricsIdentifierCache); + } +} +//# sourceMappingURL=app-store-intent-dispatcher.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/foundation/runtime/app-store-object-graph.js b/node_modules/@jet-app/app-store/tmp/src/foundation/runtime/app-store-object-graph.js new file mode 100644 index 0000000..a5102b2 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/foundation/runtime/app-store-object-graph.js @@ -0,0 +1,380 @@ +import { ObjectGraph } from "@jet/environment/dependencies"; +import { fetchTimingMetricsBuilderType, } from "@jet/environment/metrics/fetch-timing-metrics-builder"; +import * as jetTypes from "@jet/environment/types/globals/types"; +import * as askTypes from "../../api/typings/constants"; +import { isDefinedNonNull } from "../json-parsing/server-data"; +import { AppleSiliconWrapper } from "../wrappers/apple-silicon"; +import { BagWrapper } from "../wrappers/bag"; +import { ClientWrapper } from "../wrappers/client"; +import { ClientOrderingWrapper } from "../wrappers/client-ordering"; +import { ConsoleWrapper } from "../wrappers/console"; +import { HostWrapper } from "../wrappers/host"; +import { LocalizationWrapper } from "../wrappers/localization"; +import { PropertiesWrapper } from "../wrappers/properties"; +import { StorageWrapper } from "../wrappers/storage"; +import { ExperimentCache } from "../experimentation/experiment-cache"; +import { MetricsIdentifiersCache } from "../metrics/metrics-identifiers-cache"; +import { LocaleMetaType } from "../dependencies/locale/locale"; +import { SEOMetaType } from "../dependencies/seo"; +import { ActiveIntentMetaType, ActiveIntent } from "../dependencies/active-intent"; +import { LocaleFromBag } from "../dependencies/locale/locale-from-bag"; +import { ObjectGraphType } from "../../gameservicesui/src/foundation/object-graph-types"; +export class AppStoreObjectGraph extends ObjectGraph { + configureDefaults( + // Jet Types + aBag, aCryptography, aHost, aNetwork, aPlatform, aPlist, aRandom, aServices, aCookieProvider, aConsole, + // Ask Types + aStoreMetrics, aAMSEngagement, aLocalization, aAdsLocalizer, aDevice, aClient, aProperties, aUser, aPlayer, aMetricsIdentifiers, aClientOrdering, aArcade, aGameCenter, aResilientDeepLinks, aAppleSilicon, aStorage, aAds, aOnDeviceRecommendationsManager, aOnDeviceSearchHistoryManager, aFeatureFlags, aMediaToken, aAppDistribution, timeoutManager, treatmentStore, userDefaults) { + let objectGraph = this + // Jet Types + .addingCryptography(aCryptography) + .addingHost(aHost) + .addingNetwork(aNetwork) + .addingPlatform(aPlatform) + .addingPlist(aPlist) + .addingRandom(aRandom) + .addingServices(aServices) + .addingCookieProvider(aCookieProvider) + .addingBag(aBag) // Bag depends on having Host + .addingConsole(aConsole) + // ASK types + .addingStoreMetrics(aStoreMetrics) + .addingAMSEngagement(aAMSEngagement) + .addingLoc(aLocalization) + .addingAdsLoc(aAdsLocalizer) + .addingDevice(aDevice) + .addingClient(aClient) + .addingProperties(aProperties) + .addingUser(aUser) + .addingPlayer(aPlayer) + .addingMetricsIdentifiers(aMetricsIdentifiers) + .addingClientOrdering(aClientOrdering) + .addingArcade(aArcade) + .addingGameCenter(aGameCenter) + .addingDeepLinks(aResilientDeepLinks) + .addingAppleSilicon(aAppleSilicon) + .addingStorage(aStorage) + .addingAds(aAds) + .addingOnDeviceRecommendationsManager(aOnDeviceRecommendationsManager) + .addingOnDeviceSearchHistoryManager(aOnDeviceSearchHistoryManager) + .addingFeatureFlags(aFeatureFlags) + .addingMediaToken(aMediaToken) + .addingAppDistribution(aAppDistribution) + .addingTimeoutManager(timeoutManager) + .addingAdsLoc(aAdsLocalizer) + .addingTreatmentStore(treatmentStore) + .addingUserDefaults(userDefaults); + objectGraph.loc.load(objectGraph); + // `LocaleFromBag` requires that the `bag` is already present on the Object Graph + objectGraph = objectGraph.addingLocale(new LocaleFromBag(objectGraph)); + return objectGraph; + } + // Jet Types + get bag() { + return this.required(BagWrapper.type); + } + addingBag(bag) { + return this.addingBagWrapper(new BagWrapper(bag, this.host)).adding(jetTypes.bag, bag); + } + addingBagWrapper(bag) { + return this.adding(BagWrapper.type, bag); + } + get console() { + return this.required(ConsoleWrapper.type); + } + addingConsole(console) { + return this.addingConsoleWrapper(new ConsoleWrapper(console)); + } + addingConsoleWrapper(console) { + return this.adding(ConsoleWrapper.type, console); + } + get cryptography() { + return this.required(jetTypes.cryptography); + } + addingCryptography(cryptography) { + return this.adding(jetTypes.cryptography, cryptography); + } + get host() { + return this.required(HostWrapper.type); + } + addingHost(host) { + return this.addingHostWrapper(new HostWrapper(host)); + } + addingHostWrapper(host) { + return this.adding(HostWrapper.type, host); + } + get locale() { + return this.required(LocaleMetaType); + } + addingLocale(locale) { + return this.adding(LocaleMetaType, locale); + } + get network() { + return this.required(jetTypes.net); + } + addingNetwork(network) { + return this.adding(jetTypes.net, network); + } + get platform() { + return this.required(jetTypes.platform); + } + addingPlatform(platform) { + return this.adding(jetTypes.platform, platform); + } + get plist() { + return this.required(jetTypes.plist); + } + addingPlist(plist) { + return this.adding(jetTypes.plist, plist); + } + get random() { + return this.required(jetTypes.random); + } + addingRandom(random) { + return this.adding(jetTypes.random, random); + } + get services() { + return this.required(jetTypes.services); + } + addingServices(services) { + return this.adding(jetTypes.services, services); + } + get cookieProvider() { + return this.required(jetTypes.cookieProvider); + } + addingCookieProvider(cookieProvider) { + return this.adding(jetTypes.cookieProvider, cookieProvider); + } + get fetchTimingMetricsBuilder() { + return this.optional(fetchTimingMetricsBuilderType); + } + addingFetchTimingMetricsBuilder(fetchTimingMetricsBuilder) { + return this.adding(fetchTimingMetricsBuilderType, fetchTimingMetricsBuilder); + } + // Ask Types + get storeMetrics() { + return this.required(askTypes.storeMetrics); + } + addingStoreMetrics(storeMetrics) { + return this.adding(askTypes.storeMetrics, storeMetrics); + } + get amsEngagement() { + return this.optional(askTypes.amsEngagement); + } + addingAMSEngagement(amsEngagement) { + return this.adding(askTypes.amsEngagement, amsEngagement); + } + get loc() { + return this.required(LocalizationWrapper.type); + } + addingLoc(loc) { + return this.addingLocWrapper(new LocalizationWrapper(loc, this)); + } + addingLocWrapper(loc) { + return this.adding(LocalizationWrapper.type, loc); + } + get adsLoc() { + return this.required(askTypes.adsLocalizer); + } + addingAdsLoc(loc) { + return this.adding(askTypes.adsLocalizer, loc); + } + get device() { + return this.required(askTypes.device); + } + addingDevice(device) { + return this.adding(askTypes.device, device); + } + get client() { + return this.required(ClientWrapper.type); + } + addingClient(client) { + return this.addingClientWrapper(new ClientWrapper(client)); + } + addingClientWrapper(client) { + return this.adding(ClientWrapper.type, client); + } + get props() { + return this.required(PropertiesWrapper.type); + } + addingProperties(properties) { + return this.addingPropertiesWrapper(new PropertiesWrapper(properties)); + } + addingPropertiesWrapper(properties) { + return this.adding(PropertiesWrapper.type, properties); + } + get user() { + return this.required(askTypes.user); + } + addingUser(user) { + return this.adding(askTypes.user, user); + } + /// Returns the current GameCenter player, only available for GAMES_TARGET + get player() { + if (preprocessor.GAMES_TARGET && this.props.enabled("157263806-add-playerBridge-askGlobal")) { + return this.required(askTypes.player); + } + else { + return undefined; + } + } + addingPlayer(player) { + return this.adding(askTypes.player, player); + } + get metricsIdentifiers() { + return this.required(askTypes.metricsIdentifiers); + } + addingMetricsIdentifiers(metricsIdentifiers) { + return this.adding(askTypes.metricsIdentifiers, metricsIdentifiers); + } + get clientOrdering() { + return this.required(ClientOrderingWrapper.type); + } + addingClientOrdering(clientOrdering) { + return this.addingClientOrderingWrapper(new ClientOrderingWrapper(clientOrdering)); + } + addingClientOrderingWrapper(clientOrdering) { + return this.adding(ClientOrderingWrapper.type, clientOrdering); + } + get arcade() { + return this.required(askTypes.arcade); + } + addingArcade(arcade) { + return this.adding(askTypes.arcade, arcade); + } + get gameCenter() { + return this.required(askTypes.gameCenter); + } + addingGameCenter(gameCenter) { + return this.adding(askTypes.gameCenter, gameCenter); + } + get deepLinks() { + return this.required(askTypes.resilientDeepLinks); + } + addingDeepLinks(resilientDeepLinks) { + return this.adding(askTypes.resilientDeepLinks, resilientDeepLinks); + } + get appleSilicon() { + return this.required(AppleSiliconWrapper.type); + } + addingAppleSilicon(appleSilicon) { + return this.addingAppleSiliconWrapper(new AppleSiliconWrapper(appleSilicon)); + } + addingAppleSiliconWrapper(appleSilicon) { + return this.adding(AppleSiliconWrapper.type, appleSilicon); + } + get storage() { + return this.required(StorageWrapper.type); + } + addingStorage(storage) { + return this.addingStorageWrapper(new StorageWrapper(storage)); + } + addingStorageWrapper(storage) { + return this.adding(StorageWrapper.type, storage); + } + get ads() { + return this.required(askTypes.ads); + } + addingAds(ads) { + return this.adding(askTypes.ads, ads); + } + get onDeviceRecommendationsManager() { + return this.required(askTypes.onDeviceRecommendationsManager); + } + addingOnDeviceRecommendationsManager(onDeviceRecommendationsManager) { + return this.adding(askTypes.onDeviceRecommendationsManager, onDeviceRecommendationsManager); + } + get onDeviceSearchHistoryManager() { + return this.required(askTypes.onDeviceSearchHistoryManager); + } + addingOnDeviceSearchHistoryManager(onDeviceSearchHistoryManager) { + return this.adding(askTypes.onDeviceSearchHistoryManager, onDeviceSearchHistoryManager); + } + get featureFlags() { + return this.required(askTypes.featureFlags); + } + addingFeatureFlags(featureFlags) { + return this.adding(askTypes.featureFlags, featureFlags); + } + get mediaToken() { + return this.required(askTypes.mediaToken); + } + addingMediaToken(mediaToken) { + return this.adding(askTypes.mediaToken, mediaToken); + } + get appDistribution() { + return this.required(askTypes.appDistribution); + } + addingAppDistribution(appDistribution) { + return this.adding(askTypes.appDistribution, appDistribution); + } + get timeoutManager() { + return this.required(askTypes.timeoutManager); + } + addingTimeoutManager(timeoutManager) { + return this.adding(askTypes.timeoutManager, timeoutManager); + } + get treatmentStore() { + return this.required(askTypes.treatmentStore); + } + addingTreatmentStore(treatmentStore) { + return this.adding(askTypes.treatmentStore, treatmentStore); + } + get experimentCache() { + return this.optional(ExperimentCache.metatype); + } + get metricsIdentifiersCache() { + return this.optional(MetricsIdentifiersCache.defaultMetatype); + } + get paymentMetricsIdentifiersCache() { + return this.optional(MetricsIdentifiersCache.paymentMetatype); + } + get personalizationMetricsIdentifiersCache() { + return this.optional(MetricsIdentifiersCache.personalizationMetatype); + } + get userDefaults() { + if (!this.client.isiOS) { + return undefined; + } + return this.required(askTypes.userDefaults); + } + addingUserDefaults(userDefaults) { + return this.adding(askTypes.userDefaults, userDefaults); + } + isAvailable(type) { + return isDefinedNonNull(this.optional(type)); + } + // "Web" client dependencies + get activeIntent() { + return this.optional(ActiveIntentMetaType); + } + addingActiveIntent(implementation) { + return this.adding(ActiveIntentMetaType, new ActiveIntent(implementation)); + } + get seo() { + return this.optional(SEOMetaType); + } + addingSEO(implementation) { + return this.adding(SEOMetaType, implementation); + } + // GameStoreKit dependencies + get dispatcher() { + return this.required(ObjectGraphType.dispatcher); + } + get nativeIntentDispatcher() { + return this.required(ObjectGraphType.nativeIntentDispatcher); + } + get debugSettings() { + return this.required(ObjectGraphType.debugSettings); + } + get router() { + return this.required(ObjectGraphType.router); + } + get localizer() { + return this.required(ObjectGraphType.localizer); + } + get personNameComponentsFormatter() { + return this.required(ObjectGraphType.personNameComponentsFormatter); + } +} +//# sourceMappingURL=app-store-object-graph.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/foundation/runtime/runtime.js b/node_modules/@jet-app/app-store/tmp/src/foundation/runtime/runtime.js new file mode 100644 index 0000000..d3b5904 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/foundation/runtime/runtime.js @@ -0,0 +1,52 @@ +import * as validation from "@jet/environment/json/validation"; +import { LegacyRuntime } from "@jet/environment/runtime"; +export class AppStoreRuntime extends LegacyRuntime { + constructor(dispatcher, objectGraph) { + super(dispatcher, objectGraph, {}); + } + exportingService(name, service) { + this.wrapServiceInValidation(service); + const existingService = this.serviceWithName(name) || {}; + const newService = { + ...existingService, + ...service, + }; + return super.exportingService(name, newService); + } + // eslint-disable-next-line @typescript-eslint/ban-types + exportingServiceName(name, functionName, implementation) { + const service = {}; + service[functionName] = implementation; + this.exportingService(name, service); + } + wrapServiceInValidation(service) { + for (const memberName of Object.keys(service)) { + const serviceFunction = service[memberName]; + // For every service route function that we find, we're going + // to wrap it in a thunk so that we can attach validation + // incidents to it before it's returned back to native code. + if (serviceFunction instanceof Function) { + // Using a regular function here because we want to forward + // `this` as well as our arguments to the wrapped function. + service[memberName] = function validationThunk(...args) { + // Execute the function + const returnValue = serviceFunction.apply(this, args); + // Ensure we record validation incidents after the fact + if (returnValue instanceof Promise) { + return returnValue.then((value) => { + validation.recordValidationIncidents(value); + return value; + }); + } + else { + // This would be a violation of the calling convention, + // but I guess we might as well consume the incidents. + validation.recordValidationIncidents(returnValue); + return returnValue; + } + }; + } + } + } +} +//# sourceMappingURL=runtime.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/foundation/util/array-util.js b/node_modules/@jet-app/app-store/tmp/src/foundation/util/array-util.js new file mode 100644 index 0000000..57c8e84 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/foundation/util/array-util.js @@ -0,0 +1,32 @@ +/** + * Creates an array containing multiple copies of the elements of another array. + * + * For example, if A = [Q, W, E, R, T, Y], then calling repeating(A, 3) would return + * [Q, W, E, R, T, Y, Q, W, E, R, T, Y, Q, W, E, R, T, Y] + * + * @param elements The elements that will be repeated + * @param times The number of times all of the elements should appear in the new array. + */ +export function arrayByRepeating(elements, times) { + if (times <= 0) { + return []; + } + let newArray = []; + for (let i = 0; i < times; i++) { + newArray = newArray.concat(elements); + } + return newArray; +} +export function partition(array, predicate) { + const part1 = []; + const part2 = []; + array.forEach((item) => (predicate(item) ? part1.push(item) : part2.push(item))); + return [part1, part2]; +} +export function isEmpty(elements) { + return elements.length === 0; +} +export function isNotEmpty(elements) { + return !isEmpty(elements); +} +//# sourceMappingURL=array-util.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/foundation/util/color-util.js b/node_modules/@jet-app/app-store/tmp/src/foundation/util/color-util.js new file mode 100644 index 0000000..84e3815 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/foundation/util/color-util.js @@ -0,0 +1,231 @@ +import { isNothing, isSome } from "@jet/environment/types/optional"; +const HEX_PARSER = new RegExp("#?([0-9,a-f,A-F][0-9,a-f,A-F])([0-9,a-f,A-F][0-9,a-f,A-F])([0-9,a-f,A-F][0-9,a-f,A-F])([0-9,a-f,A-F][0-9,a-f,A-F])?"); +export function fromHex(hexString) { + if (isNothing(hexString) || hexString === "") { + return null; + } + const results = HEX_PARSER.exec(hexString); + if (results === null || !(results.length === 4 || results.length === 5)) { + return null; + } + const red = parseInt(results[1], 16) / 255; + const green = parseInt(results[2], 16) / 255; + const blue = parseInt(results[3], 16) / 255; + const alpha = isSome(results[4]) ? parseInt(results[4], 16) / 255 : undefined; + const newColor = { + red: red, + green: green, + blue: blue, + alpha: alpha, + type: "rgb", + }; + return newColor; +} +export const black = rgbWith(0, 0, 0); +export const white = rgbWith(1, 1, 1); +export function rgbWith(red, green, blue, alpha) { + const newColor = { + red: red, + green: green, + blue: blue, + alpha: alpha, + type: "rgb", + }; + return newColor; +} +export function luminanceFrom(rgbColor) { + // Note: This is lifted from UIColor_Private + // Using RGB color components, calculates and returns (0.2126 * r) + (0.7152 * g) + (0.0722 * b). + return rgbColor.red * 0.2126 + rgbColor.green * 0.7152 + rgbColor.blue * 0.0722; +} +/** + * Determines whether the given color is considered dark. + * @param color The color to test. + * @param darkColorCutoff The cutoff value, which ought to be between 0 and 100. + */ +export function isDarkColor(color, darkColorCutoff = 84) { + if (isNothing(color)) { + return false; + } + switch (color.type) { + case "rgb": + // Multiplying 100 because the rest of the codebase now works off darkColorCutoff 0-100. + // Before this change it used to be darkColorCutoff 0.0-1.0 + return luminanceFrom(color) * 100 < darkColorCutoff; + case "named": + return isNamedColorDarkColor(color); + default: + return false; + } +} +function isNamedColorDarkColor(namedColor) { + switch (namedColor.name) { + case "black": + return true; + default: + return false; + } +} +export function named(name) { + const newColor = { + name: name, + type: "named", + }; + return newColor; +} +export function dynamicWith(lightColor, darkColor) { + const newColor = { + lightColor: lightColor, + darkColor: darkColor, + type: "dynamic", + }; + return newColor; +} +/** + * A convenience for comparing two colors to see if they are equal. + * @param firstColor The first color to compare + * @param secondColor The second color to compare + * @returns True if the colors are considered equal + */ +export function isColorEqualToColor(firstColor, secondColor) { + if (isNothing(firstColor) || isNothing(secondColor)) { + return false; + } + if (firstColor.type === "rgb" && secondColor.type === "rgb") { + return (firstColor.red === secondColor.red && + firstColor.green === secondColor.green && + firstColor.blue === secondColor.blue && + firstColor.alpha === secondColor.alpha); + } + else if (firstColor.type === "named" && secondColor.type === "named") { + return firstColor.name === secondColor.name; + } + else if (firstColor.type === "dynamic" && secondColor.type === "dynamic") { + return (isColorEqualToColor(firstColor.lightColor, secondColor.lightColor) && + isColorEqualToColor(firstColor.darkColor, secondColor.darkColor)); + } + return false; +} +/** + * Type guard to check if a value is a ColorBucketSaturationBrightnessCriteria. + * @param value The value to check. + * @returns True if the value is a ColorBucketSaturationBrightnessCriteria, false otherwise. + */ +function isSaturationBrightnessCriteria(value) { + if (typeof value !== "object" || value === null) { + return false; + } + const obj = value; // Cast for easier property access + // Check if all required properties exist and have the correct types + return ("colorHex" in obj && + typeof obj.colorHex === "string" && + "maxSaturation" in obj && + typeof obj.maxSaturation === "number" && + "maxBrightness" in obj && + typeof obj.maxBrightness === "number"); +} +/** + * Finds the closest matching color from a list of hex colors using LAB color space + * for more perceptually accurate matching + */ +export function findColorBucketForColor(targetRGB, saturationBrightnessBasedBuckets, hueBasedBuckets) { + if (isNothing(targetRGB) || (saturationBrightnessBasedBuckets.length === 0 && hueBasedBuckets.length === 0)) { + return null; + } + const targetColor = targetRGB; + if (isNothing(targetColor)) { + return null; + } + const targetHsb = rgbToHsb(targetColor); + for (const saturationBrightnessBasedBucket of saturationBrightnessBasedBuckets) { + if (doesColorMeetCriteria(targetHsb, saturationBrightnessBasedBucket)) { + return fromHex(saturationBrightnessBasedBucket.colorHex); + } + } + for (const hueBasedBucket of hueBasedBuckets) { + if (doesColorMeetCriteria(targetHsb, hueBasedBucket)) { + return fromHex(hueBasedBucket.colorHex); + } + } + return null; +} +/** + * Determines whether a color meets the specified criteria. + * + * For saturation/brightness criteria, the color passes if either its saturation or brightness + * is less than or equal to the maximum allowed values. + * + * For hue criteria, the color passes if its hue falls within the specified range (inclusive). + * + * @param color The RGB color to evaluate + * @param criteria The criteria to check against (either hue-based or saturation/brightness-based) + * @returns True if the color passes the criteria, false otherwise or if color is null/undefined + */ +export function doesColorMeetCriteria(color, criteria) { + if (isNothing(color)) { + return false; + } + const hsbColor = isHSBColor(color) ? color : rgbToHsb(color); + // Check saturation / brightness buckets first, using <= for inclusive range checks + if (isSaturationBrightnessCriteria(criteria)) { + return hsbColor.saturation <= criteria.maxSaturation || hsbColor.brightness <= criteria.maxBrightness; + } + else { + // Check if the hue falls within the min/max bounds (inclusive) + // Special handling for the Pink range potentially including 360 if needed, + // but mapping 360->0 covers the Coral case correctly. + return hsbColor.hue >= criteria.minHue && hsbColor.hue <= criteria.maxHue; + } +} +function isHSBColor(value) { + if (typeof value !== "object" || value === null) { + return false; + } + const obj = value; // Cast for easier property access + // Check if the type property exists and has the correct value + return "type" in obj && obj.type === "hsb"; +} +/** + * Converts RGB color to HSB color space. + * HSL is particularly good at representing relationships between colors + * as humans perceive them, especially for determining similar hues + * across different brightness levels. + */ +function rgbToHsb(rgb) { + const r = rgb.red; + const g = rgb.green; + const b = rgb.blue; + const max = Math.max(r, g, b); + const min = Math.min(r, g, b); + const delta = max - min; + // Calculate brightness (value in HSV) + const brightness = max * 100; + // Calculate saturation + const saturation = max === 0 ? 0 : (delta / max) * 100; + // Calculate hue + let hue = 0; + if (delta !== 0) { + switch (max) { + case r: + hue = ((g - b) / delta + (g < b ? 6 : 0)) * 60; + break; + case g: + hue = ((b - r) / delta + 2) * 60; + break; + case b: + hue = ((r - g) / delta + 4) * 60; + break; + default: + break; + } + } + // Ensure hue is in [0, 360) + hue = (hue + 360) % 360; + return { + type: "hsb", + hue: hue, + saturation: saturation, + brightness: brightness, + }; +} +//# sourceMappingURL=color-util.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/foundation/util/constants.js b/node_modules/@jet-app/app-store/tmp/src/foundation/util/constants.js new file mode 100644 index 0000000..86522d5 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/foundation/util/constants.js @@ -0,0 +1,26 @@ +/** + * Created by keithpk on 2/9/17. + */ +/** + * These are the developer IDs for apps that Apple works on internally + * The IDs in the list below are CPIDs (content provider IDs), which are not exposed + * to the client. So, we use the developer.id field that we have available instead. + * Note that Dark Sky both the same developer ID as Apple Inc. + * 2003 Apple Inc + * 120076162 Apple Health Research + * 244848 The Dark Sky Company LLC + * 115216 Claris International Inc. + * 14275 Shazam Entertainment Limited + * 118988565 AC Wellness Network LLC + */ +export const appleOwnedDeveloperIds = [ + "284417353", + "1464590764", + "314638464", + "284993479", + "1351056256", // AC Wellness Network LLC +]; +export const iAdRequestDataHeaderName = "X-Apple-iAd-Request-Data"; +export const appStoreClientRequestIdName = "X-Apple-App-Store-Client-Request-Id"; +export const iAdRoutingInfoHeaderName = "X-Apple-iAd-Env-Name"; +//# sourceMappingURL=constants.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/foundation/util/date-util.js b/node_modules/@jet-app/app-store/tmp/src/foundation/util/date-util.js new file mode 100644 index 0000000..182dd63 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/foundation/util/date-util.js @@ -0,0 +1,127 @@ +// +// date.ts +// AppStoreKit +// +// Created by Sam Vafaee on 10/25/17. +// Copyright (c) 2017 Apple Inc. All rights reserved. +// +import * as serverData from "../json-parsing/server-data"; +/** + * Returns the date representation for a string ignoring the time. For preorders, it + * is vital that any date string be passed through this before being used. + * @param dateString The date string to parse + * @returns A date in local time zone. + */ +export function parseDateOmittingTimeFromString(dateString) { + // Sanity check + if (serverData.isNull(dateString) || dateString === "") { + return null; + } + // Only consider date + if (dateString.indexOf("T") !== -1) { + dateString = dateString.split("T")[0]; + } + // This string replacement is needed to ensure midnight in the local time zone + // is used, rather than UTC-midnight. + return new Date(dateString.replace(/-/g, "/")); +} +/** + * Strips time from the given date and returns it as midnight UTC of the same day. + * @param date Local date. + * @returns UTC date + */ +export function convertLocalDateToUTCMidnight(date) { + // Sanity check + if (serverData.isNull(date)) { + return null; + } + return new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate())); +} +/** + * Returns the number of milliseconds from epoch to UTC midnight of the given date. + * @param date Local date. + * @returns Milliseconds from epoch. + */ +export function millisecondsToUTCMidnightFromLocalDate(date) { + // Sanity check + if (serverData.isNull(date)) { + return null; + } + const utcDate = convertLocalDateToUTCMidnight(date); + if (serverData.isNull(utcDate)) { + return null; + } + return utcDate.getTime(); +} +/** + * Removes the time portion from the given date and returns it as midnight of the same day. + * @param date Local date + * @returns Local date + */ +export function convertLocalDateToLocalMidnight(date) { + // Sanity check + if (serverData.isNull(date)) { + return null; + } + const midnight = new Date(date); + midnight.setHours(0, 0, 0, 0); + return midnight; +} +/** + * Sets the minutes, seconds and milliseconds of the given date to 0. + * @param date Local date + * @returns Local date + */ +export function convertLocalDateByFlooringToHour(date) { + // Sanity check + if (serverData.isNull(date)) { + return null; + } + return dateFlooredToHour(date); +} +/** + * Creates a new date with the minutes, seconds and milliseconds of the given date set to 0. + * @param date The date to remove minutes, seconds, and milliseconds from. + * @returns The new date with minutes, seconds, and milliseconds removed using local device time zone. + */ +export function dateFlooredToHour(date) { + const dateCopy = new Date(date.getTime()); + dateCopy.setMinutes(0); + dateCopy.setSeconds(0); + dateCopy.setMilliseconds(0); + return dateCopy; +} +/** + * Returns the number of hours between two dates, or null if either date is null. + * @param fromDate The date to calculate from. + * @param toDate The date to calculate to. + */ +export function numberOfHoursBetween(fromDate, toDate) { + // Sanity check + if (serverData.isNull(fromDate) || serverData.isNull(toDate)) { + return null; + } + return Math.ceil((toDate.getTime() - dateFlooredToHour(fromDate).getTime()) / (60 * 60 * 1000)); +} +/** + * Returns the number of days between two dates, or null if either date is null. + * @param fromDate The date to calculate from. + * @param toDate The date to calculate to. + */ +export function numberOfDaysBetween(fromDate, toDate) { + // Sanity check + if (serverData.isNull(fromDate) || serverData.isNull(toDate)) { + return null; + } + return Math.floor((toDate.getTime() - fromDate.getTime()) / (60 * 60 * 1000 * 24)); +} +/** + * Whether or not two dates occur on the same day. + * @param date A date that will be compared to the second input. + * @param otherDate A date that will be compared to the first input. + */ +export function areLocalDatesSameDay(date, otherDate) { + var _a, _b; + return ((_a = convertLocalDateToLocalMidnight(date)) === null || _a === void 0 ? void 0 : _a.getTime()) === ((_b = convertLocalDateToLocalMidnight(otherDate)) === null || _b === void 0 ? void 0 : _b.getTime()); +} +//# sourceMappingURL=date-util.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/foundation/util/errors.js b/node_modules/@jet-app/app-store/tmp/src/foundation/util/errors.js new file mode 100644 index 0000000..d1a3482 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/foundation/util/errors.js @@ -0,0 +1,13 @@ +/** + * Created by Ben Williams on 3/13/19. + */ +/** + * Enables compile time detection for code that should be unreachable + * For example, calling this function in the default case of a switch statement + * will provide compile time checking that the switch is exhaustive + * @return never + */ +export function unreachable(value) { + throw new Error(`This method should never be called with value: ${value}`); +} +//# sourceMappingURL=errors.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/foundation/util/math-util.js b/node_modules/@jet-app/app-store/tmp/src/foundation/util/math-util.js new file mode 100644 index 0000000..92b4992 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/foundation/util/math-util.js @@ -0,0 +1,32 @@ +/** + * Given an integer m > 1, called a modulus, two integers are said to be congruent modulo m, + * if m is a divisor of their difference (i.e., if there is an integer k such that a \u2212 b = km). + * + * Unlike JavaScript's remainder operator (%), this mod function never returns negative values. + * + * @param n An integer + * @param m The modulous + */ +export function mod(n, m) { + return ((n % m) + m) % m; +} +/** + * Reduce sig fig of `value` to `significantDigits`. + * @param value Value to reduce precision of. + * @param significantDigits Significant digits to keep. + */ +export function reduceSignificantDigits(value, significantDigits) { + const roundFactor = Math.pow(10.0, significantDigits); + const roundingFunction = value > 0.0 ? Math.floor : Math.ceil; + return roundingFunction(value / roundFactor) * roundFactor; +} +/** + * Clamps a value between an upper and lower bound. + * @param value The preferred value. + * @param min The minimum allowed value. + * @param max The maximum allowed value. + */ +export function clamp(value, min, max) { + return Math.max(min, Math.min(max, value)); +} +//# sourceMappingURL=math-util.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/foundation/util/objects.js b/node_modules/@jet-app/app-store/tmp/src/foundation/util/objects.js new file mode 100644 index 0000000..6740572 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/foundation/util/objects.js @@ -0,0 +1,33 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/** + * Created by km on 3/6/17. + */ +/** + * Creates and returns a shallow copy of a given object. + * @param object The object to create a shallow copy of. + * @returns The new copy of object. + */ +export function shallowCopyOf(object) { + if (object === null || object === undefined) { + return object; + } + const copy = Object.create(Object.getPrototypeOf(object)); + Object.assign(copy, object); + return copy; +} +/** + * Returns whether or not a given object is an instance of + * a specified type, modifying the type of the object in + * TypeScript's type system. + * + * Prefer this function to using `instanceof` directly as it + * will retype the passed in object if the check succeeds. + * + * @param object The object whose nominal type will be checked. + * @param type The metatype that `object` is expected to be an instance of. + * @returns Whether object is an instance of type, updating the type of object accordingly. + */ +export function isTypeOf(object, type) { + return object instanceof type; +} +//# sourceMappingURL=objects.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/foundation/util/promise-util.js b/node_modules/@jet-app/app-store/tmp/src/foundation/util/promise-util.js new file mode 100644 index 0000000..d661559 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/foundation/util/promise-util.js @@ -0,0 +1,215 @@ +/** + * This module provides enhanced promise handling capabilities beyond native JavaScript Promises. + * It focuses on distinguishing between required and optional promises, standardizing results, + * and simplifying error handling for complex async operations. + */ +/** + * Symbol used to uniquely identify PromiseResult objects. + * This allows for reliable type checking. + */ +export const PROMISE_RESULT_SYMBOL = Symbol("PromiseResult"); +/** + * Type guard to check if a value is a PromiseResult. + * + * @param value - The value to check + * @returns True if the value is a PromiseResult, false otherwise + */ +function isPromiseResult(value) { + return value !== null && typeof value === "object" && PROMISE_RESULT_SYMBOL in value; +} +/** + * Custom error class that aggregates multiple promise failures. + * Provides access to all underlying errors while presenting a combined error message. + */ +export class MultiPromiseError extends Error { + constructor(reasons) { + super(errorMessageForReasons(reasons)); + this.reasons = reasons; + this.reasons = reasons; + } +} +/** + * Helper function to create a standardized fulfilled result object. + * Ensures the result has the PROMISE_RESULT_SYMBOL for type checking. + * + * @param value - The value to wrap in a fulfilled result + * @returns A standardized fulfilled result object + */ +export function makeFulfilledResult(value) { + return { + success: true, + value, + error: null, + [PROMISE_RESULT_SYMBOL]: true, + }; +} +/** + * Helper function to create a standardized rejected result object. + * Ensures the result has the PROMISE_RESULT_SYMBOL for type checking. + * + * @param error - The error to wrap in a rejected result + * @returns A standardized rejected result object + */ +export function makeRejectedResult(error) { + const normalizedError = error instanceof Error ? error : new Error(String(error)); + return { + success: false, + value: null, + error: normalizedError, + [PROMISE_RESULT_SYMBOL]: true, + }; +} +/** + * Custom error class that indicates that a required promise failed. + */ +export class RequiredPromiseError extends Error { + constructor(reason) { + super(reason.message); + this.reason = reason; + this.reason = reason; + } +} +/** + * Transforms a promise into one that never throws, instead returning a standardized result. + * Use this for optional operations that shouldn't cause the overall process to fail. + * This is a convenience alias for tryAwait with a more semantic name. + * + * @param promise - The promise to make optional + * @returns A promise that resolves to a PromiseResult instead of throwing + */ +export async function optional(promise) { + return await tryAwait(promise); +} +/** + * Marks a promise as required (will throw on rejection). + * This is mainly for code clarity as regular promises are already required by default. + * + * @param promise - The promise to mark as required + * @returns The original promise (identity function) + */ +export async function required(promise) { + return await promise; +} +/** + * Executes a mix of required and optional promises, returning standardized results. + * Regular promises are treated as required and will cause this function to throw if they fail. + * Promises wrapped with optional() will never cause this function to throw. + * + * This implementation handles both regular promises and promises that resolve to PromiseResult. + * + * @param promises - Array of promises (both required and optional) + * @returns Promise resolving to an array of standardized results + * @throws Original error if a single required promise fails + * @throws MultiPromiseError if multiple required promises fail + * @note For homogeneous promise arrays, prefer using more specific functions: + * - Use allRequired() when all promises should be treated as required + * - Use allOptional() when all promises should be treated as optional + * - Only use allMixed() when you need to combine both required and optional promises + */ +export async function allMixed(promises) { + // Process each promise + const results = await Promise.all(promises.map(async (promise) => { + try { + const value = await promise; + // If it's a PromiseResult (from optional()/tryAwait), use it directly + if (isPromiseResult(value)) { + // Handle the case where T is already a PromiseResult + // This fixes the double-wrapping issue when all promises are optional + return value; + } + else { + // It's a regular promise that succeeded + return makeFulfilledResult(value); + } + } + catch (error) { + // This was a required promise that failed + return makeRejectedResult(error instanceof Error ? error : new Error(String(error))); + } + })); + const requiredFailures = []; + results.forEach((result) => { + if (!result.success) { + requiredFailures.push(result.error); + } + }); + // If any required promises failed, throw an error + if (requiredFailures.length > 0) { + if (requiredFailures.length === 1) { + throw requiredFailures[0]; + } + else { + throw new MultiPromiseError(requiredFailures); + } + } + return results; +} +/** + * Executes all promises and returns their values. + * All promises are treated as required and will cause this function to throw if any fail. + * + * @param promises - Array of promises + * @returns Promise resolving to an array of values + * @throws Original error if a single promise fails + * @throws MultiPromiseError if multiple promises fail + */ +export async function allRequired(promises) { + return await Promise.all(promises); +} +/** + * Makes all promises optional and then executes them, returning standardized results. + * This function is specifically designed for cases where you want all promises to be treated as optional. + * It avoids the double-wrapping issue that can occur when using allMixed with arrays of optional promises. + * + * @param promises - Array of promises to make optional + * @returns Promise resolving to an array of standardized results + * @example + * ``` + * const promises = urls.map(fetchData); + * const results = await allOptional(promises); + * // Each result is a PromiseResult<T> that won't cause the overall operation to fail + * ``` + */ +export async function allOptional(promises) { + // Use Promise.all with optional() for each promise + return await Promise.all(promises.map(optional)); +} +/** + * Creates an error message from multiple rejection reasons. + * Formats multiple error reasons into a single error message string, + * handling both Error objects and other rejection values. + * + * @param reasons - Array of error reasons that caused promise rejections + * @returns Concatenated error message string + */ +function errorMessageForReasons(reasons) { + return reasons + .map((reason) => { + if (reason instanceof Error) { + return reason.message; + } + else { + return JSON.stringify(reason); + } + }) + .join(""); +} +/** + * Safely awaits a promise and returns a standardized result object. + * Eliminates the need for repetitive try/catch blocks throughout the codebase. + * + * @param promiseOrFn - Either a promise or a function that returns a promise + * @returns A standardized PromiseResult object indicating success or failure + */ +export async function tryAwait(promiseOrFn) { + try { + // Handle both promise and function that returns a promise + const promise = typeof promiseOrFn === "function" ? promiseOrFn() : promiseOrFn; + const value = await promise; + return makeFulfilledResult(value); + } + catch (error) { + return makeRejectedResult(error); + } +} +//# sourceMappingURL=promise-util.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/foundation/util/string-util.js b/node_modules/@jet-app/app-store/tmp/src/foundation/util/string-util.js new file mode 100644 index 0000000..affeba3 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/foundation/util/string-util.js @@ -0,0 +1,127 @@ +/** + * Created by dabolfathi on 4/26/17. + */ +/** + * Returns a single string, which is the result of joining together all + * the strings in the list with the provided separator. + * @param strings The list of strings to be joined. + * @param separator The separator to use when joining them together. + * @returns {string} The joined string. + */ +export function join(strings, separator) { + if (strings == null || separator == null) { + return null; + } + if (strings.length === 0) { + return ""; + } + let stringCount = strings.length; + let joinedString = ""; + strings.forEach((element, index) => { + if (element === null) { + stringCount -= 1; + } + else { + joinedString += element; + if (index < stringCount - 1) { + joinedString += separator; + } + } + }); + return joinedString; +} +/** + * Generate a normalized string for robust multilingual search that properly handles + * CJK (Chinese, Japanese, Korean), Arabic, Cyrillic, and Latin-based scripts. + * Preserves Unicode characters while normalizing diacritics and case appropriately. + * + * @param input The input string to normalize + * @returns A normalized string suitable for search matching + */ +export function normalizeForSearch(input) { + if (!input) { + return ""; + } + try { + // Remove special characters + // \u2122 -> (Trade Mark Sign) + // \u2120 -> (Service Mark) + // \u03a9 -> (Greek Capital Letter Omega) + // \u00a9 -> (Copyright Sign) + // \u00ae -> (Registered Sign) + // \u30fc -> (Katakana-Hiragana Prolonged Sound Mark) + // \u03c9 -> (Greek Small Letter Omega) + const removedSpecialUnicodesRegex = /[\u2122\u2120\u03a9\u00a9\u00ae\u30fc\u03c9]/g; + return (input + .toLowerCase() // Case insensitivity + .replace(removedSpecialUnicodesRegex, "") + // Apply normalization only to Latin characters to preserve CJK integrity + // Handle Latin characters safely with basic ranges A-Za-z + // \u00C0-\u00FF - Latin-1 Supplement (Upper Half) + // \u0100-\u017F - Latin Extended-A (includes dotless i \u0131) + // \u0180-\u024F - Latin Extended-B + // \u1E00-\u1EFF - Latin Extended Additional + .replace(/[A-Za-z\u00A0-\u00FF\u0100-\u017F\u0180-\u024F\u1E00-\u1EFF\p{Diacritic}]+/gu, (latinText) => { + return (latinText + .normalize("NFKD") + .replace(/\p{Diacritic}/gu, "") + // Convert specific characters that don't match basic Latin + .replace(/\u0131/g, "i") // Convert dotless i to regular i + // Keep only actual Latin characters and numbers after normalization + .replace(/[^A-Za-z0-9]/g, "")); + }) + // Remove punctuation, symbols, and control characters but preserve: + // - Letters from all writing systems (\p{L}) + // - Numbers (\p{N}) + // - Whitespace (\s) + // - Underscores (_) + .replace(/[^\p{L}\p{N}\s_]/gu, "") + // Normalize multiple whitespace to single spaces + .replace(/\s+/g, " ") + // Trim leading/trailing whitespace + .trim()); + } + catch (error) { + // Fallback: use basic character classes + return (input + .toLowerCase() + // Remove punctuation, symbols, and control characters but preserve: + // - Letters from all writing systems (\p{L}) + // - Numbers (\p{N}) + // - Whitespace (\s) + // - Underscores (_) + .replace(/[^\p{L}\p{N}\s_]/gu, "") + // Normalize multiple whitespace to single spaces + .replace(/\s+/g, " ") + // Trim leading/trailing whitespace + .trim()); + } +} +/** + * Whether or not the input string is contains search term using normalized string which comparing using case insensitive, locale insensitive and ignore all special characters. + * + * @param input + * @param normalizedTerm + * @returns + */ +export function containsSearchTerm(input, normalizedTerm) { + const normalizedInput = normalizeForSearch(input); + return normalizedInput.includes(normalizedTerm); +} +/** + * Wraps a string with bidirectional isolate characters to ensure proper text direction handling. + * This is particularly useful for user-generated content like display names that may contain + * mixed left-to-right and right-to-left text. + * + * @param text The string to wrap with bidi isolates + * @returns The string wrapped with Left-to-Right Isolate (U+2066) and Pop Directional Isolate (U+2069) + */ +export function withBidiIsolates(text) { + if (!text) { + return text; + } + // U+2068: First Strong Isolate + // U+2069: Pop Directional Isolate + return "\u2068" + text + "\u2069"; +} +//# sourceMappingURL=string-util.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/foundation/util/validation-util.js b/node_modules/@jet-app/app-store/tmp/src/foundation/util/validation-util.js new file mode 100644 index 0000000..fe9bd93 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/foundation/util/validation-util.js @@ -0,0 +1,30 @@ +import { isSome } from "@jet/environment"; +import * as validation from "@jet/environment/json/validation"; +import { tryAwait } from "./promise-util"; +/** + * Creates a validation context for async operations and ensures proper cleanup. + * + * Wraps an async operation in a validation context, automatically handling + * context cleanup even when errors occur. Optionally processes errors through + * a provided error handler. + * + * @param name - The name for the validation context + * @param producer - Async function that produces the result + * @param errorHandler - Optional function to handle errors from the producer + * @returns The result of the async operation + */ +export async function withAsyncValidationContext(name, producer, errorHandler) { + validation.beginContext(name); + let result = await tryAwait(producer()); + if (!result.success && isSome(errorHandler)) { + result = await tryAwait(errorHandler(result.error)); + } + validation.endContext(); + if (!result.success) { + throw result.error; + } + else { + return result.value; + } +} +//# sourceMappingURL=validation-util.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/foundation/wrappers/apple-silicon.js b/node_modules/@jet-app/app-store/tmp/src/foundation/wrappers/apple-silicon.js new file mode 100644 index 0000000..4371e77 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/foundation/wrappers/apple-silicon.js @@ -0,0 +1,13 @@ +import { makeMetatype } from "@jet/environment/util/metatype"; +import * as serverData from "../../foundation/json-parsing/server-data"; +import { Wrapper } from "./wrapper"; +export class AppleSiliconWrapper extends Wrapper { + get isSupportEnabled() { + return serverData.isDefinedNonNull(this.implementation) && this.implementation.isSupportEnabled; + } + get isRosettaAvailable() { + return serverData.isDefinedNonNull(this.implementation) && this.implementation.isRosettaAvailable; + } +} +AppleSiliconWrapper.type = makeMetatype("app-store:as-wrapper"); +//# sourceMappingURL=apple-silicon.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/foundation/wrappers/bag.js b/node_modules/@jet-app/app-store/tmp/src/foundation/wrappers/bag.js new file mode 100644 index 0000000..ad76b76 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/foundation/wrappers/bag.js @@ -0,0 +1,1126 @@ +/** + * Created by km on 1/9/17. + */ +import { isSome } from "@jet/environment"; +import { makeMetatype } from "@jet/environment/util/metatype"; +import * as serverData from "../json-parsing/server-data"; +import { Wrapper } from "./wrapper"; +import { CachedBag } from "./cached-bag"; +export class BagWrapper extends Wrapper { + constructor(bag, host) { + super(new CachedBag(bag)); + this.underlyingBag = bag; + } + /// The boolean for whether today ad medium lockup screenshots are enabled + get todayAdMediumLockupScreenshotEnabled() { + var _a; + return (_a = this.implementation.boolean("today-ad-medium-lockup-screenshots-enabled")) !== null && _a !== void 0 ? _a : false; + } + get todayAdMediumLockupScreenshotAnimationEnabled() { + var _a; + return (_a = this.implementation.boolean("today-ad-medium-lockup-screenshots-animation-enabled")) !== null && _a !== void 0 ? _a : true; + } + /// The URL for the trending searches endpoint. + get trendingSearchesURL() { + return this.implementation.url("trending-searches"); + } + /// The URL for the search hints endpoint. + get searchHintsURL() { + return this.implementation.url("searchHints"); + } + /// The URL for fetching a personalized review + get personalizedUserReviewURL() { + return this.implementation.url("personalizedUserReviewUrl"); + } + /// The boolean for whether personalized reviews are enabled + get personalizedUserReviewEnabled() { + return this.implementation.boolean("personalizedUserReviewEnabled"); + } + /// The URL for posting tap-to-rate requests. + get userRateURL() { + return this.implementation.url("p2-application-user-rate-content"); + } + /// The URL for posting write review requests. + get writeReviewURL() { + return this.implementation.url("p2-application-user-write-review"); + } + /// The base URL for accessory rooms + get accessoryRoomURL() { + return this.implementation.url("p2-accessory-room"); + } + /// The URL for passbook main room + get passbookMainURL() { + return this.implementation.url("passbook"); + } + /// The URL for library-link room + get libraryLinkURL() { + return this.implementation.url("library-link"); + } + /// The metrics configuration for mt-metrics kit integration + get metricsConfiguration() { + return serverData.asJSONData(this.implementation.dictionary("metrics")); + } + // When the metrics payment topic is enabled the clientId and userId are on a separate namespace. + get metricsPaymentNamespaceEnabled() { + if (serverData.isNullOrEmpty(this.metricsPaymentTopic)) { + return false; + } + const identifiers = serverData.asJSONData(this.implementation.dictionary("metrics-identifiers")); + const metricsNameSpace = serverData.asDictionary(identifiers, "APPSTORE_PAYMENTS_ENGAGEMENT"); + const metricsClientIdSpace = serverData.asDictionary(identifiers, "APPSTORE_PAYMENTS_ENGAGEMENT_CLIENT"); + return (isSome(metricsNameSpace) && + metricsNameSpace.length !== 0 && + isSome(metricsClientIdSpace) && + metricsClientIdSpace.length !== 0); + } + // The Payment topic if one is provided by the bag. Otherwise the default one will be used + get metricsPaymentTopic() { + var _a, _b; + if (preprocessor.GAMES_TARGET) { + return (_a = serverData.asString(this.metricsConfiguration, "topics.GAMES_PAYMENTS_ENGAGEMENT_TOPIC")) !== null && _a !== void 0 ? _a : null; + } + else { + return (_b = serverData.asString(this.metricsConfiguration, "topics.APPSTORE_PAYMENTS_ENGAGEMENT_TOPIC")) !== null && _b !== void 0 ? _b : null; + } + } + // Whether the personalization user id has been inlcuded in the metrics identifiers bag entry + get personalizationUserIdEnabled() { + const identifiers = serverData.asJSONData(this.implementation.dictionary("metrics-identifiers")); + const personalizationIdNameSpace = serverData.asDictionary(identifiers, "APPSTORE_PERSONALIZATION"); + return isSome(personalizationIdNameSpace) && Object.keys(personalizationIdNameSpace).length !== 0; + } + /// The language for the users storefront + get language() { + return this.implementation.string("language"); + } + /// The language for media API + get mediaApiLanguage() { + const languageTag = this.implementation.string("language-tag"); + if (languageTag) { + return languageTag; + } + return this.implementation.string("language"); + } + /// The URL for terms and conditions page + get termsAndConditionsURL() { + return this.implementation.url("p2-service-terms-url"); + } + /// whether we should send iAd data as a post request + get usePostForAppStoreSearch() { + return this.implementation.boolean("usePostForAppStoreSearch"); + } + /// Whether or not monetary gifting is enabled. + get isMonetaryGiftingEnabled() { + return this.implementation.boolean("isBuyingScheduledGiftCertificateEnabled"); + } + /// The URL for Account Top-Up in the finance sheet. + get accountTopUpURL() { + return this.implementation.url("AddFundsUrl"); + } + /// The title for Account Top-Up in the finance sheet. + get accountTopUpTitle() { + return this.implementation.string("account-top-up-title"); + } + /// Whether or not content gifting is enabled. + get isContentGiftingEnabled() { + return this.implementation.boolean("isScheduledGiftingEnabled"); + } + /// The URL for buy button metadata. + get buyButtonMetadataURL() { + return this.implementation.url("personalized-buy-buttons/software"); + } + /// Whether or not the current storefront supports the TV App. + get isTVAppEnabled() { + return this.implementation.boolean("uvSearch/nowplaying-enabled"); + } + /// the url to send people to to email support + get emailSupportLinkURL() { + return this.implementation.url("supportLinkUrl"); + } + /// The URL to report a review item helpful or not. + get voteUrl() { + return this.implementation.url("voteUrl"); + } + /// Enables the Review Summary module + get enableReviewSummarization() { + return this.implementation.boolean("enable-review-summarization"); + } + /// Provides configuration for Review Summary Report A Concern + get reviewSummaryReportConcernData() { + return serverData.asJSONData(this.implementation.dictionary("review-summarization-report-concern")); + } + /// The URL to report a concern about a review item. + get reportConcernUrl() { + return this.implementation.url("reportConcernUrl"); + } + /// The explanation for the report a concern screen for a review. + get reportConcernExplanation() { + return this.implementation.string("reportConcernExplanation"); + } + /// An array of reasons for reporting a concern for a review. + get reportConcernReasons() { + return serverData.asArrayOrEmpty(serverData.asJSONValue(this.implementation.array("reportConcernReasons"))); + } + /// The boolean that enables report a problem. + get reportProblemEnabled() { + var _a; + return (_a = this.implementation.boolean("product-page-report-problem-enabled")) !== null && _a !== void 0 ? _a : false; + } + /// The bag key that stores the report a problem URL. + get productPageReportProblemURL() { + return this.implementation.string("product-page-report-problem-url"); + } + /// The string array of adamIDs of SAD apps with subscriptions. + get productPageReportProblemSADSubscriptionArray() { + return serverData.asArrayOrEmpty(serverData.asJSONValue(this.implementation.array("product-page-report-problem-sad-subscriptions"))); + } + /// The string array of second party apps to not show the report a problem link for. + /// We have added the array of IDs while we wait for bag key support. Removal is tracked by rdar://82606581 (Remove array of second party app ids from JS bag.) + get productPageReportProblemSecondPartyAppArray() { + const secondPartyAppArray = serverData.asArrayOrEmpty(serverData.asJSONValue(this.implementation.array("product-page-report-problem-second-party-apps"))); + const defaultSecondPartyAppArray = [ + "1473505534", + "1416238567", + "640199958", + "1529498570", + "915061776", + "1130498044", + "1070072560", + ]; + if (serverData.isNullOrEmpty(secondPartyAppArray)) { + return defaultSecondPartyAppArray; + } + return secondPartyAppArray; + } + /// The URL to create a new account. + get createAccountUrl() { + var _a; + return ((_a = this.implementation.url("createAccountUrl")) !== null && _a !== void 0 ? _a : "https://buy.itunes.apple.com/WebObjects/MZFinance.woa/wa/signupWizard"); + } + get mediaCountryCode() { + return this.implementation.string("countryCode"); + } + get mediaHost() { + return this.implementation.url("apps-media-api-host"); + } + mediaEdgeHost(objectGraph) { + // The key has different type for AppStoreComponents + // see <rdar://71290056>. + if (objectGraph.host.clientIdentifier === "com.apple.appstorecomponentsd") { + return this.implementation.url("apps-media-api-edge-host"); + } + else { + return this.implementation.string("apps-media-api-edge-host"); + } + } + get mediaAPICatalogMixedShouldUseEdge() { + var _a; + return (_a = this.implementation.boolean("apps-media-api-catalog-mixed-should-use-edge")) !== null && _a !== void 0 ? _a : false; + } + get mediaEdgeSearchHost() { + return this.implementation.string("apps-media-api-search-edge-host"); + } + get mediaPreviewHost() { + return this.implementation.string("apps-media-api-preview-host"); + } + get mediaRealmHost() { + return this.implementation.string("notification-settings-media-api-host"); + } + get edgeEndpoints() { + return serverData.asArrayOrEmpty(serverData.asJSONValue(this.implementation.array("apps-media-api-edge-end-points"))); + } + get mediaAdvertRequestLimit() { + var _a; + return (_a = this.implementation.double("apps-media-api-search-ads-limit")) !== null && _a !== void 0 ? _a : 4; + } + get searchSortOptions() { + return serverData.asArrayOrEmpty(serverData.asJSONValue(this.implementation.array("searchSortOptions"))); + } + get ageBands() { + return serverData.asArrayOrEmpty(serverData.asJSONValue(this.implementation.array("ageBands"))); + } + get redirectUrlWhitelistedQueryParams() { + let params = serverData.asArrayOrEmpty(serverData.asJSONValue(this.implementation.array("processRedirectUrl/whitelistedQueryParams"))); + if (serverData.isNullOrEmpty(params)) { + params = [ + "affC", + "adId", + "advp", + "at", + "ct", + "itsct", + "itscg", + "itscc", + "itcCt", + "its_qt", + "ls", + "partnerId", + "pt", + "qtkid", + "uo", + ]; + } + return params; + } + get redirectUrlEndpoint() { + var _a; + return ((_a = this.implementation.string("processRedirectUrl/endpoint")) !== null && _a !== void 0 ? _a : "https://itunes.apple.com/WebObjects/MZStoreServices.woa/wa/processRedirectUrl"); + } + get aristotleParentAppAdamId() { + var _a; + return (_a = this.implementation.string("aristotle-app-id")) !== null && _a !== void 0 ? _a : "383941000"; + } + /// The AdamId of the App Store app which owns the IAP for Arcade subscriptions + get arcadeAppAdamId() { + return this.implementation.string("app-store-app-id"); + } + /// The product family of the Arcade subscription IAP. This is for multiple IAPs for the same service not family sharing. + get arcadeProductFamilyId() { + var _a; + return (_a = this.implementation.string("arcade-iap-family-id")) !== null && _a !== void 0 ? _a : this.implementation.string("ocelot-iap-family-id"); + } + /// The Identifier (developer supplied reverse dns style name) of the IAP for Arcade subscriptions + get arcadeProductId() { + var _a; + return ((_a = this.implementation.string("arcade-iap-offer-name")) !== null && _a !== void 0 ? _a : this.implementation.string("ocelot-iap-offer-name")); + } + /// The percentage of users that will see the Arcade category bar See All Games uplift. + get arcadeCategoryBarSAGUpliftDisplayRate() { + var _a; + return (_a = this.implementation.double("arcade-category-bar-see-all-games-display-rate")) !== null && _a !== void 0 ? _a : 0.0; + } + get isArcadeEnabled() { + var _a; + return (_a = this.implementation.boolean("arcade-enabled")) !== null && _a !== void 0 ? _a : false; + } + get isAppsGroupingTagsEnabled() { + var _a; + return (_a = this.implementation.boolean("apps-groupings-tags-enabled")) !== null && _a !== void 0 ? _a : false; + } + get isAppsProductPageTagsEnabled() { + var _a; + return (_a = this.implementation.boolean("apps-product-page-tags-enabled")) !== null && _a !== void 0 ? _a : false; + } + get isAppsSlpTagsEnabled() { + var _a; + return (_a = this.implementation.boolean("apps-slp-tags-enabled")) !== null && _a !== void 0 ? _a : false; + } + get searchResultsLearnMoreEditorialId() { + return this.implementation.string("transparencyLawEditorialItemId"); + } + /// An array containing a mapping of system app bundle IDs and adam IDs. + get systemApps() { + return serverData.asArrayOrEmpty(serverData.asJSONValue(this.implementation.array("hideableSystemApps"))); + } + /// An array containing a mapping of system app bundle IDs and adam IDs + /// Should only be used for watchOS as a temporary fix for rdar://100234873 + get nonDeletableSystemApps() { + const apps = serverData.asJSONValue(this.implementation.array("nonDeletableSystemApps")); + if (serverData.isDefinedNonNullNonEmpty(apps)) { + return serverData.asArrayOrEmpty(apps); + } + else { + return [ + { "id": 1635387927, "bundle-id": "com.apple.Depth" }, + { "id": 1635862301, "bundle-id": "com.apple.Mandrake" }, + { "id": 1584216343, "bundle-id": "com.apple.findmy.finddevices" }, + { "id": 1584215960, "bundle-id": "com.apple.NanoWorldClock" }, + { "id": 1584215812, "bundle-id": "com.apple.HeartRate" }, + { "id": 1584215851, "bundle-id": "com.apple.SessionTrackerApp" }, + { "id": 1146562108, "bundle-id": "com.apple.NanoPhone" }, + { "id": 1146560473, "bundle-id": "com.apple.MobileSMS" }, + { "id": 1584215428, "bundle-id": "com.apple.NanoPhotos" }, + { "id": 1459455352, "bundle-id": "com.apple.DeepBreathing" }, + { "id": 1067456176, "bundle-id": "com.apple.NanoCompass.watchkitapp" }, // Compass + ]; + } + } + /// The standard tabs for the current storefront. + get tabsStandard() { + return serverData.asArrayOrEmpty(serverData.asJSONValue(this.implementation.array("tabs/standard"))); + } + // URL to sub-in for deep links specifying the watch category. Note that this can be a multiplexing url. + get watchAppsGroupingURL() { + var _a; + return ((_a = this.implementation.url("watchAppsGrouping")) !== null && _a !== void 0 ? _a : "https://apps.apple.com/WebObjects/MZStore.woa/wa/viewFeature?id=1472048385"); + } + get requireAgeVerification() { + return this.implementation.boolean("requireAgeVerification"); + } + get ageRatingLearnMoreEditorialItemId() { + return this.implementation.string("ageRatingLearnMoreEditorialItemId"); + } + get appleSiliconMacUnverifiedBadgeEditorialItemId() { + return this.implementation.string("appleSiliconMacUnverifiedBadgeEditorialItemId"); + } + get safariExtensionsGroupingURL() { + return this.implementation.url("safariExtensionsGrouping"); + } + get familySubscriptionsLearnMoreEditorialItemId() { + return this.implementation.string("familySubscriptionsLearnMoreEditorialItemId"); + } + get dynamicUIRegexStrings() { + return serverData.asArrayOrEmpty(serverData.asJSONValue(this.implementation.array("commerce-ui-urls/dynamic-url-patterns"))); + } + get financeUIRegexStrings() { + return serverData.asArrayOrEmpty(serverData.asJSONValue(this.implementation.array("commerce-ui-urls/url-patterns"))); + } + get webViewRegexStrings() { + return serverData.asArrayOrEmpty(serverData.asJSONValue(this.implementation.array("commerce-ui-urls/v2-url-patterns"))); + } + get arcadePreOrderUpsellLimitSeconds() { + var _a; + return (_a = this.implementation.double("arcadePreOrderUpsellLimitSeconds")) !== null && _a !== void 0 ? _a : 86400; + } + get recentlyPlayedGamesWindowInSeconds() { + var _a; + return (_a = this.implementation.double("recentlyPlayedGamesWindowInSeconds")) !== null && _a !== void 0 ? _a : 7776000; // 90 days + } + get gamesFriendsPlayedWindowInSeconds() { + var _a; + return (_a = this.implementation.integer("games-friends-played-window-in-seconds")) !== null && _a !== void 0 ? _a : 15778800; // Half a year + } + get enableComingSoonToggle() { + return this.implementation.boolean("enableComingSoonToggle"); + } + /// Whether or not to show the app acessibility labels + get enableAppAccessibilityLabels() { + var _a; + return (_a = this.implementation.boolean("enable-app-accessibility-labels")) !== null && _a !== void 0 ? _a : false; + } + /// Whether or not to show the app privacy labels + get enablePrivacyNutritionLabels() { + var _a; + return (_a = this.implementation.boolean("enable-privacy-nutrition-labels")) !== null && _a !== void 0 ? _a : false; + } + /// Whether or not to show the seller info + get enableSellerInfo() { + var _a; + return (_a = this.implementation.boolean("enable-seller-info")) !== null && _a !== void 0 ? _a : false; + } + // Whether or not to show the seller ICP annotation + get enableSellerICPAnnotation() { + var _a; + return (_a = this.implementation.boolean("enable-seller-icp")) !== null && _a !== void 0 ? _a : false; + } + /// Whether to display enhanced category features (bar, breakout, and swoosh) on apps and games tabs + get enableFeaturedCategoriesOnGroupings() { + var _a; + return (_a = this.implementation.boolean("enable-featured-categories-on-groupings")) !== null && _a !== void 0 ? _a : false; + } + /// Whether to display enhanced category bricks on apps and games tabs + get enableCategoryBricksOnGroupings() { + var _a; + return (_a = this.implementation.boolean("enable-category-bricks-on-groupings")) !== null && _a !== void 0 ? _a : false; + } + get arcadeOfferEditorialItemId() { + return this.implementation.string("arcadeOfferEditorialItemId"); + } + get sponsoredSearchODMLTimeout() { + var _a; + return (_a = this.implementation.double("sponsored-search-odml-timeout")) !== null && _a !== void 0 ? _a : 3; + } + get isSearchLandingAdsEnabled() { + var _a; + return (_a = this.implementation.boolean("isSearchLandingAdsEnabled")) !== null && _a !== void 0 ? _a : false; + } + get isLLMSearchTagsEnabled() { + var _a; + return (_a = this.implementation.boolean("apps-search-tags-enabled")) !== null && _a !== void 0 ? _a : false; + } + get searchLandingAdFetchTimeout() { + var _a; + return (_a = this.implementation.double("search-landing-ad-fetch-timeout")) !== null && _a !== void 0 ? _a : 0.175; + } + // Time that SLP needs to be offscreen for it to be refreshed in seconds. + get searchLandingPageOffscreenRefreshInterval() { + var _a; + return (_a = this.implementation.double("search-landing-offscreen-refresh-interval-in-seconds")) !== null && _a !== void 0 ? _a : 60; // default is minute + } + // Time to delay the data fetch for SLP when refreshing + get searchLandingPageRefreshUpdateDelayInterval() { + var _a; + return (_a = this.implementation.double("search-landing-page-update-delay-interval-in-seconds")) !== null && _a !== void 0 ? _a : 0.3; // default is 0.3s + } + get appPrivacyLearnMoreEditorialItemId() { + return this.implementation.string("appPrivacyLearnMoreEditorialItemId"); + } + // The editorial id for the article page showing the transparency info for ratings and reviews + get ratingsAndReviewsLearnMoreEditorialId() { + return this.implementation.string("ratings-and-reviews-learn-more-editorial-item-id"); + } + // The editorial id for the article page showing info about the review summary + get reviewSummarizationLearnMoreEditorialItemId() { + return this.implementation.string("review-summarization-learn-more-editorial-item-id"); + } + /// An array containing a list of app bundle IDs and adam IDs for apps that should not display + /// any app privacy shelves + get suppressedPrivacyAppIds() { + return serverData.asArrayOrEmpty(serverData.asJSONValue(this.implementation.array("suppressedPrivacyLabels"))); + } + /// An array containing a list of app bundle IDs and adam IDs for apps that should not display + /// any app accessibility shelves + get suppressedAccessibilityAppIds() { + return serverData.asArrayOrEmpty(serverData.asJSONValue(this.implementation.array("suppressed-accessibility-labels"))); + } + get appPrivacyDefinitionsEditorialItemId() { + return this.implementation.string("appPrivacyDefinitionsEditorialItemId"); + } + // The EI ID for the web's category tabs shown in the sidebar + get webNavigationCategoryTabsEditorialItemId() { + return this.implementation.string("web-navigation-category-tabs-editorial-item-id"); + } + /// The percentage of users that will see a live preview of the widget in the Widget Gallery + get todayWidgetLivePreviewRolloutRate() { + var _a; + return (_a = this.implementation.double("todayWidgetLivePreviewRolloutRate")) !== null && _a !== void 0 ? _a : 1.0; + } + /// The percentage of users that will see the new carousel item styles under Hero 3.0 + get hero3RolloutRate() { + var _a; + return (_a = this.implementation.double("arcade-hero-shelf-tagline-style-rollout-rate")) !== null && _a !== void 0 ? _a : 1.0; + } + /// The percentage of users that will see the trial enrolled (subscriber) tab + get arcadeTrialEnrolledStateRate() { + var _a; + return (_a = this.implementation.double("arcade-trial-enrolled-state-rate")) !== null && _a !== void 0 ? _a : 0.0; + } + get marketingItemSelectionTimeout() { + var _a; + return (_a = this.implementation.double("marketing-item-selection-timeout")) !== null && _a !== void 0 ? _a : 1.0; + } + /// Whether or not to enable app events + get enableAppEvents() { + var _a; + return (_a = this.implementation.boolean("enableAppEvents")) !== null && _a !== void 0 ? _a : false; + } + /// Whether or not product page variants is enabled (feature-gate) + get enableProductPageVariants() { + var _a; + return (_a = this.implementation.boolean("enableProductPageVariants")) !== null && _a !== void 0 ? _a : false; + } + get enableArcadeTrialEligibleBadging() { + return this.implementation.boolean("enable-arcade-trial-eligible-badging"); + } + /// The duration we should use for the hero carousel auto scrolling + get heroCarouselAutoScrollDuration() { + var _a; + return (_a = this.implementation.double("heroCarouselAutoScrollDuration")) !== null && _a !== void 0 ? _a : 7.0; + } + /// Enable additional JS logging for Product Page Variants + get enableAdditionalLoggingForPPV() { + var _a; + return (_a = this.implementation.boolean("enableAdditionalLoggingForPPV")) !== null && _a !== void 0 ? _a : false; + } + /// Whether or not to enable on device personalization + get enableOnDevicePersonalization() { + const enableOnDevicePersonalization = this.implementation.boolean("enable-on-device-personalization"); + if (serverData.isNull(enableOnDevicePersonalization)) { + return true; + } + return enableOnDevicePersonalization; + } + /// Whether or not automatic page refreshing is enabled + get enableAutomaticPageRefresh() { + var _a; + return (_a = this.implementation.boolean("enable-automatic-page-refresh")) !== null && _a !== void 0 ? _a : true; + } + /// Rollout rate for smart stack suggestions for the today widget. + get widgetSuggestionsFromTodayTabRolloutRate() { + var _a; + return (_a = this.implementation.double("today-widget-suggestions-from-today-tab-rollout-rate")) !== null && _a !== void 0 ? _a : 1.0; + } + /// The upper bound for how many extra minutes we delay the smart stack from suggesting the Today Widget (to protect MAPI). + get todayWidgetSmartStackJitterMinutes() { + var _a; + return (_a = this.implementation.double("today-widget-smart-stack-jitter-minutes")) !== null && _a !== void 0 ? _a : 45; + } + get enableSystemAppReviews() { + var _a; + return (_a = this.implementation.boolean("enable-system-app-reviews")) !== null && _a !== void 0 ? _a : false; + } + /// Whether or not CPPs are enabled for Search Ads. + get enableCPPInSearchAds() { + return this.implementation.boolean("enableCPPsInSearchAds") || false; + } + get cancelPreorderItemSrv() { + return (this.implementation.url("cancelPreorderItemSrv") || "https://buy.itunes.apple.com/commerce/preorders/cancel"); + } + get getCancellablePreorderItemsSrv() { + return (this.implementation.url("getCancellablePreorderItemsSrv") || + "https://buy.itunes.apple.com/commerce/preorders/cancellable"); + } + /// An array of the enabled ad placements. + get enabledAdPlacements() { + return serverData.asArrayOrEmpty(serverData.asJSONValue(this.implementation.array("enabled-ad-placements"))); + } + /// A dictionary of timeout values for the ad placements. + get adPlacementTimeouts() { + return serverData.asDictionary(serverData.asJSONData(this.implementation.dictionary("ad-placement-timeouts"))); + } + /// The editorial story ID for In App Purchases + get inAppPurchasesLearnMoreEditorialItemId() { + return this.implementation.string("in-app-purchases-learn-more-editorial-item-id"); + } + /// Determines whether external purchases are enabled. + get enableExternalPurchases() { + var _a; + return (_a = this.implementation.boolean("enable-external-purchase")) !== null && _a !== void 0 ? _a : false; + } + /// An array of the enabled external purchases placements + get enabledExternalPurchasesPlacements() { + return serverData.asArrayOrEmpty(serverData.asJSONValue(this.implementation.array("enabled-external-purchase-placements"))); + } + /// The editorial item ID for the external purchases story + get externalPurchasesLearnMoreEditorialItemId() { + return this.implementation.string("external-purchase-learn-more-editorial-item-id"); + } + /// The editorial item ID for the external browser story + get externalBrowserLearnMoreEditorialItemId() { + return this.implementation.string("external-browser-learn-more-editorial-item-id"); + } + /// The SharePlay groupings url + get sharePlayAppsEditorialItemId() { + return this.implementation.string("share-play-apps-editorial-item-id"); + } + /// Determines whether to show the icon on the external purchases product page banner + get externalPurchasesIncludeProductPageBannerIcon() { + var _a; + return (_a = this.implementation.boolean("external-purchase-product-page-banner-include-icon")) !== null && _a !== void 0 ? _a : false; + } + /// Determines whether to use a variant of the external purchases product page annotation copy + get externalPurchasesProductPageAnnotationVariant() { + return this.implementation.string("external-purchase-product-page-annotation-variant"); + } + /// Tells us whether we want new events for ODJ to be enabled or not. + get newEventsForODJAreEnabled() { + var _a; + return (_a = this.implementation.boolean("new-events-for-odj-are-enabled")) !== null && _a !== void 0 ? _a : false; + } + get defaultChart() { + return serverData.asJSONData(this.implementation.dictionary("default-chart")); + } + // The url to display the user account + get accountUrl() { + var _a; + return ((_a = this.implementation.url("accountUrl")) !== null && _a !== void 0 ? _a : "https://buy.itunes.apple.com/WebObjects/MZFinance.woa/wa/accountSummary"); + } + // The url to display the user account + get redeemUrl() { + var _a; + return ((_a = this.implementation.url("redeemUrl")) !== null && _a !== void 0 ? _a : "https://buy.itunes.apple.com/WebObjects/MZFinance.woa/wa/redeemLandingPage"); + } + // The url to display the user account + get charityUrl() { + var _a; + return ((_a = this.implementation.url("charityUrl")) !== null && _a !== void 0 ? _a : "https://buy.itunes.apple.com/WebObjects/MZFinance.woa/wa/buyCharityGiftWizard"); + } + // The url to display the manage subs page + get manageSubscriptionsUrl() { + var _a; + return ((_a = this.implementation.url("manageSubscriptionsUrl")) !== null && _a !== void 0 ? _a : "https://finance-app.itunes.apple.com/subscriptions/manage?context=deeplink"); + } + // The url to display the manage subs page + get manageSubscriptionsV2Url() { + var _a; + return ((_a = this.implementation.url("manageSubscriptionsV2Url")) !== null && _a !== void 0 ? _a : "https://apps.mzstatic.com/content/54a1317a0ad442d3965d64ef6bfaae1c/"); + } + /// The language value to use for ad language content filtering. + /// This may not necessarily match the user's language setting - in some regions ads are required + /// to be localized to the country's primary language, regardless of the user's own preferences. + get adsOverrideLanguage() { + return this.implementation.string("ads-override-language"); + } + /// The percentage of users that will see the controller recommended or required treatment + /// (as opposed to supported) on supported platforms. + get gameControllerRecommendedRolloutRate() { + var _a; + return (_a = this.implementation.double("game-controller-recommended-rollout-rate")) !== null && _a !== void 0 ? _a : 0; + } + /// The ID of the editorial item that provides information about game controllers. + get gameControllerLearnMoreEditorialItemId() { + return this.implementation.string("game-controller-learn-more-editorial-item-id"); + } + /// The ID of the editorial item that provides information about spatial controllers. + get spatialControlsLearnMoreEditorialItemId() { + return this.implementation.string("spatial-controllers-learn-more-editorial-item-id"); + } + // Whether the user can use MAPI based search focus + get mediaAPISearchFocusEnabled() { + var _a; + return (_a = this.implementation.boolean("apps-search-focus-suggestions-enabled")) !== null && _a !== void 0 ? _a : false; + } + /// Indicates whether the Search Landing Page supports the V2 protocol + get supportsSearchLandingPageV2() { + var _a; + return (_a = this.implementation.boolean("supports-apps-slp-v2")) !== null && _a !== void 0 ? _a : false; + } + get enableSearchLandingPageV2ByTreatment() { + var _a; + return (_a = this.implementation.boolean("enable-apps-slp-v2-by-treatment")) !== null && _a !== void 0 ? _a : false; + } + /// The percentage of users that will see the new Search Landing Page V2 protocol + get searchLandingPageV2RolloutRate() { + var _a; + return (_a = this.implementation.double("apps-slp-v2-rollout-rate")) !== null && _a !== void 0 ? _a : 0; + } + /// The percentage of users that will see the Today tab Arcade personalization + get todayTabArcadePersonalizationRate() { + var _a; + return (_a = this.implementation.double("today-tab-arcade-personalization-rate")) !== null && _a !== void 0 ? _a : 0; + } + /// Whether to enable extending supported Game Center features. + get gameCenterExtendSupportedFeatures() { + var _a; + return (_a = this.implementation.boolean("game-center-extend-supported-features")) !== null && _a !== void 0 ? _a : false; + } + get adPlacementEligibleSlotPositions() { + const eligibleSlotPositions = serverData.asJSONData(this.implementation.dictionary("ad-placement-eligible-slot-positions")); + if (serverData.isDefinedNonNullNonEmpty(eligibleSlotPositions)) { + return eligibleSlotPositions; + } + return { + "today": [ + { + shelfIdentifier: "0", + slot: 0, + }, + { + shelfIdentifier: "0", + slot: 1, + }, + ], + "product-page-ymal": [ + { + shelfIdentifier: "customers-also-bought-apps", + slot: 0, + }, + ], + }; + } + // The url to display the manage preorders page + get managePreordersUrl() { + var _a; + return (_a = this.implementation.url("preordersUrl")) !== null && _a !== void 0 ? _a : "https://finance-app.itunes.apple.com/preorders"; + } + // A url to modify the user's account. + get modifyAccount() { + var _a; + return ((_a = this.implementation.url("modifyAccount")) !== null && _a !== void 0 ? _a : "https://buy.itunes.apple.com/WebObjects/MZFinance.woa/wa/accountSummary"); + } + // The url to display the purchase history page + get purchaseHistoryUrl() { + return this.implementation.url("purchaseHistoryUrl"); + } + // The url to display the ratings and reviews page + get ratingsReviewsUrl() { + var _a; + return ((_a = this.implementation.url("ratingsReviewsUrl")) !== null && _a !== void 0 ? _a : "https://apps.mzstatic.com/content/54a1317a0ad442d3965d64ef6bfaae1c/ratings-reviews"); + } + // A url to sign up for a new account. + get signup() { + var _a; + return ((_a = this.implementation.url("signup")) !== null && _a !== void 0 ? _a : "https://buy.itunes.apple.com/WebObjects/MZFinance.woa/wa/signupWizard"); + } + // The landing page to redeem a code. + get redeemCodeLanding() { + var _a; + return ((_a = this.implementation.url("redeemCodeLanding")) !== null && _a !== void 0 ? _a : "https://buy.itunes.apple.com/WebObjects/MZFinance.woa/wa/redeemLandingPage"); + } + /// The bag key that stores the generic report a problem URL. + get reportProblemUrl() { + return this.implementation.string("reportProblemUrl"); + } + // Whether the app store should allow unrestricted arcade tab badging by server side marketing journeys + get unrestrictedServerSideTabBadging() { + var _a; + return (_a = this.implementation.boolean("unrestricted-server-side-tab-badging")) !== null && _a !== void 0 ? _a : false; + } + // Whether all users should receive the "condensed" today ad. + get todayAdCondensedEnabled() { + var _a; + return (_a = this.implementation.boolean("today-ad-condensed-enabled")) !== null && _a !== void 0 ? _a : false; + } + // Whether to enable bin-compat checks for the visionOS App Store + get enableVisionAppStoreBinCompatChecks() { + var _a; + return (_a = this.implementation.boolean("enable-vision-app-store-bincompat-checks")) !== null && _a !== void 0 ? _a : false; + } + // The ID for an editorial page containing Safari Extensions + get safariExtensionsEditorialPageId() { + return this.implementation.url("safari-extensions-editorial-page-id"); + } + /// The ID of the editorial item to show when deeplinking to buddy onboarding + get buddyOnboardingEditorialItemId() { + return this.implementation.string("buddy-onboarding-editorial-item-id"); + } + // Whether click events should be sent for the friends playing shelf on the product page + get productPageFriendsPlayingClickEventsEnabled() { + var _a; + return (_a = this.implementation.boolean("product-page-friends-playing-click-events-enabled")) !== null && _a !== void 0 ? _a : false; + } + // ID of the editorial item to show for more information about the "High Motion" annotation. + get highMotionLearnMoreEditorialItemId() { + return this.implementation.string("high-motion-learn-more-editorial-item-id"); + } + // The speed of the river in the medium lockup with screenshots ad. + // This is temporary, and only for testing purposes. + get todayAdMediumLockupScreenshotsRiverSpeed() { + return this.implementation.double("today-ad-medium-lockup-screenshots-river-speed"); + } + // Collection ID of the game categories for Arcade download/starter pack onboarding. + get arcadeDownloadPackCategoriesCollectionId() { + return this.implementation.string("arcade-download-packs-onboarding-collection-id"); + } + // Time interval in seconds where Arcade download pack shelf is visible after user has gone through the onboarding flow. + get arcadeDownloadPackShelfTTLInSeconds() { + var _a; + return (_a = this.implementation.integer("arcade-starter-pack-ttl-in-seconds")) !== null && _a !== void 0 ? _a : 0; + } + // The feature flag as whether or not we should display IAP offers (eg winback offers). + get enableOfferItems() { + var _a; + return (_a = this.implementation.boolean("enable-winback-offers")) !== null && _a !== void 0 ? _a : false; + } + // The host for App Distribution Media API requests. + get appDistributionMediaAPIHost() { + return this.implementation.string("app-distribution-media-api-host"); + } + // The language tag to use for App Distribution Media API requests. + get appDistributionLanguageTag() { + return this.implementation.string("app-distribution-language-tag"); + } + // Whether App Distribution is supported. + get supportsAppDistribution() { + var _a; + return (_a = this.implementation.boolean("supports-app-distribution")) !== null && _a !== void 0 ? _a : false; + } + // Whether Arcade download/starter pack onboarding should be triggered after Arcade purchase from the page offer button. + // When Mercury supports this flow the flag should be disabled. + get arcadeDownloadPackPostSubscribeTrigger() { + var _a; + return (_a = this.implementation.boolean("arcade-download-packs-post-subscribe-trigger")) !== null && _a !== void 0 ? _a : true; + } + // The feature flag as whether or not we should display contingent offers. + get enableContingentOffers() { + var _a; + return (_a = this.implementation.boolean("enable-contingent-offers")) !== null && _a !== void 0 ? _a : false; + } + /// The percentage of users that will see Arcade packs onboarding experience. + get arcadeDownloadPackRolloutRate() { + var _a; + return (_a = this.implementation.double("arcade-download-packs-rollout-rate")) !== null && _a !== void 0 ? _a : 0; + } + // Whether to enable including the Vision platform in fetch requests + get enableVisionPlatform() { + var _a; + return (_a = this.implementation.boolean("enable-vision-platform")) !== null && _a !== void 0 ? _a : false; + } + // Dictionary containing the use cases that are supported for mixed media requests. + get supportedMixedMediaRequestUsecases() { + var _a; + return ((_a = serverData.asDictionary(serverData.asJSONData(this.implementation.dictionary("supported-mixed-media-request-usecases")))) !== null && _a !== void 0 ? _a : {}); + } + // Whether the page and click events are enabled for Arcade download pack. + get arcadeDownloadPacksMetricsEventsEnabled() { + var _a; + return (_a = this.implementation.boolean("arcade-download-packs-metrics-events-enabled")) !== null && _a !== void 0 ? _a : true; + } + // Whether the impression events are enabled for Arcade download pack. + get arcadeDownloadPacksImpressionEventsEnabled() { + var _a; + return (_a = this.implementation.boolean("arcade-download-packs-impression-events-enabled")) !== null && _a !== void 0 ? _a : true; + } + /// The learn more EI ID for when purchasing a visionOS only app on iOS + get visionOnlyAppLearnMoreEditorialItemId() { + return this.implementation.string("vision-only-app-learn-more-editorial-item-id"); + } + // Whether Arcade download pack onboarding will be shown after successful CIP carrier link. + get arcadeDownloadPacksCIPDeeplinkIntegrationEnabled() { + var _a; + return (_a = this.implementation.boolean("arcade-download-packs-cip-deeplink-trigger")) !== null && _a !== void 0 ? _a : false; + } + // Whether Arcade download pack onboarding will be shown after Hardware upsell successful purchase. + // Only for tab badge upsell. + get arcadeDownloadPacksHardwareTabBadgeUpsellIntegrationEnabled() { + var _a; + return (_a = this.implementation.boolean("arcade-download-packs-hw-tabbadge-trigger")) !== null && _a !== void 0 ? _a : false; + } + // A URL that links to the About the App Store website + get aboutAppStoreUrl() { + return this.implementation.string("about-app-store-url"); + } + // The EI ID for the About In App Purchases article + get aboutInAppPurchasesEditorialItemId() { + return this.implementation.string("about-in-app-purchases-editorial-item-id"); + } + // A URL that links to the Request A Refund website + get requestARefundUrl() { + return this.implementation.string("request-a-refund-url"); + } + // Whether the personalized recommendations toggle should be displayed in the personalized + // recommendations screen. + get personalizedRecommendationsToggleEnabled() { + var _a; + return (_a = this.implementation.boolean("enable-personalized-recommendations-toggle")) !== null && _a !== void 0 ? _a : false; + } + /// Whether we should be using metricsId exclusively and remove DSID in events + get metricsIdMigrationEnabled() { + var _a; + return (_a = this.implementation.boolean("metrics-id-migration-enabled")) !== null && _a !== void 0 ? _a : true; + } + // Whether natural language search is enabled for all features, e.g. What's New, Bubble Tip, Hints, and Results. + get isNaturalLanguageSearchEnabled() { + var _a; + return (_a = this.implementation.boolean("apps-natural-language-search-enabled")) !== null && _a !== void 0 ? _a : false; + } + // Whether natural language search is partially enabled, e.g. Bubble Tip and Results only, excludes What's New and Hints. + get isNaturalLanguageSearchResultsEnabled() { + var _a; + return (_a = this.implementation.boolean("apps-natural-language-search-results-enabled")) !== null && _a !== void 0 ? _a : false; + } + /// Whether we should be using metricsId exclusively and remove DSID in events + get metricsIdentifiersShouldCache() { + var _a; + return (_a = this.implementation.boolean("metrics-identifiers-should-cache")) !== null && _a !== void 0 ? _a : true; + } + // A URL that links to the Change Your Payment Method support article + get changePaymentMethodUrl() { + return this.implementation.string("change-payment-method-url"); + } + // The EI ID for the Information about the French App Store article + get aboutFrenchAppStoreEditorialItemId() { + return this.implementation.string("about-app-store-editorial-item-id"); + } + // A bag override which can disable product page on demand shelf fetching if necessary, without + // a JS release + get isOnDemandShelfFetchingEnabled() { + var _a; + return (_a = this.implementation.boolean("on-demand-product-shelf-fetching-enabled")) !== null && _a !== void 0 ? _a : true; + } + // A bag override which can enable the addition of the dsId field to metrics events in the case of + // missing userId + get isMetricsUserIdFallbackEnabled() { + var _a; + return (_a = this.implementation.boolean("metrics-user-id-fallback-enabled")) !== null && _a !== void 0 ? _a : false; + } + // A bag override which can enable the addition of the alt_ab2_data field to metrics events in the case of + // missing ab2_data + get isMetricsAb2DataFallbackEnabled() { + var _a; + return (_a = this.implementation.boolean("metrics-ab2data-fallback-enabled")) !== null && _a !== void 0 ? _a : !preprocessor.GAMES_TARGET; + } + /// Whether or not to enable on device reco reordering. Default to true while in dev. + get enableRecoOnDeviceReordering() { + var _a; + return (_a = this.implementation.boolean("enable-on-device-reco-reordering")) !== null && _a !== void 0 ? _a : false; + } + // The EI IDs for any Vision Pro ribbon bar items + get ribbonBarVisionEditorialItemIds() { + return serverData.asArrayOrEmpty(serverData.asJSONValue(this.implementation.array("ribbon-bar-vision-editorial-item-ids"))); + } + // The IDs for any EIs that should be filtered from search results + get searchFilterEditorialItemIds() { + const array = serverData.asArrayOrEmpty(serverData.asJSONValue(this.implementation.array("search-filter-editorial-item-ids"))); + return new Set(array); + } + // The ID for the accessibility learn more EI on the product page + get accessibilityLearnMoreEditorialItemId() { + return this.implementation.string("accessibility-learn-more-editorial-item-id"); + } + /// Whether discovery content based on ownership of a Vision Pro device is enabled + get enableDeviceDrivenDiscoveryContent() { + var _a; + return (_a = this.implementation.boolean("enable-device-driven-discovery-content")) !== null && _a !== void 0 ? _a : false; + } + /// Whether to show install size for apps on the product page + get enableProductPageInstallSize() { + var _a; + return (_a = this.implementation.boolean("enable-product-page-install-size")) !== null && _a !== void 0 ? _a : false; + } + // Whether we show the prerendered icon artwork + get enableIconArtwork() { + var _a; + return (_a = this.implementation.boolean("enable-icon-artwork")) !== null && _a !== void 0 ? _a : false; + } + // The rollout rate associated with `enable-icon-artwork` + get iconArtworkRolloutRate() { + var _a; + return (_a = this.implementation.double("icon-artwork-rollout-rate")) !== null && _a !== void 0 ? _a : 0.0; + } + // Whether the new age rating system should be used + get enableUpdatedAgeRatings() { + var _a; + return (_a = this.implementation.boolean("enable-app-store-age-ratings")) !== null && _a !== void 0 ? _a : false; + } + // Whether age rating should be sent to MAPI to filter returned content + get enableAgeRatingFilter() { + var _a; + return (_a = this.implementation.boolean("enable-age-rating-filter")) !== null && _a !== void 0 ? _a : false; + } + // Whether two-phase buy is enabled. This is expected to always be false, and only exists + // as an escape hatch. + get enableTwoPhaseOfferConfirmation() { + var _a; + return (_a = this.implementation.boolean("enable-two-phase-offer-confirmation")) !== null && _a !== void 0 ? _a : false; + } + /// Determines whether to use a variant of the external purchases product page banner text copy + get externalPurchasesProductPageBannerTextVariant() { + return this.implementation.string("external-purchase-product-page-banner-text-variant"); + } + /// Describes the variant of the external purchases product page banner icon to use + get externalPurchasesProductPageBannerIconVariant() { + return this.implementation.string("external-purchase-product-page-banner-icon-variant"); + } + /** ******* GameStoreKit *********/ + // A default maximum number of games used for query events & updates. + get maxGamesForFetchingEvents() { + var _a; + return (_a = this.implementation.integer("max-games-for-fetching-events")) !== null && _a !== void 0 ? _a : 30; + } + // The mock home feed url for testing + get mockHomeFeedURL() { + return this.implementation.string("mock-home-feed-url"); + } + // Disable play-together endpoint + get disablePlayTogetherEndpoint() { + var _a; + return (_a = this.implementation.boolean("disable-play-together-endpoint")) !== null && _a !== void 0 ? _a : false; + } + // Metrics topic for the current bundle. + get metricsTopic() { + var _a, _b; + if (preprocessor.GAMES_TARGET) { + const defaultTopic = "xp_amp_gc_cs"; + const metricsTopic = (_a = serverData.asString(this.metricsConfiguration, "topics.GAMES_CLICKSTREAM_TOPIC")) !== null && _a !== void 0 ? _a : null; + return metricsTopic !== null && metricsTopic !== void 0 ? metricsTopic : defaultTopic; + } + else { + const defaultTopic = "xp_ase_appstore_ue"; + return (_b = this.implementation.string("metrics_topic")) !== null && _b !== void 0 ? _b : defaultTopic; + } + } + // A default collectionID of the game recommendations for arcade subscriber. + get playTogetherGameRecommendationsArcade() { + var _a; + return (_a = this.implementation.string("play-together-arcade-game-recommendations")) !== null && _a !== void 0 ? _a : "1803255513"; + } + // A default collectionID of the game recommendations for users with no arcade subscription. + get playTogetherGameRecommendationsNonArcade() { + var _a; + return (_a = this.implementation.string("play-together-non-arcade-game-recommendations")) !== null && _a !== void 0 ? _a : "1804480915"; + } + /// A collectionID of arcade game recommendations for multiplayer activity + get multiplayerActivityGameRecommendationsArcade() { + var _a; + return (_a = this.implementation.string("multiplayer-activity-arcade-game-recommendations")) !== null && _a !== void 0 ? _a : "1821553042"; + } + /// A collectionID of non-arcade game recommendations for multiplayer activity + get multiplayerActivityGameRecommendationsNonArcade() { + var _a; + return (_a = this.implementation.string("multiplayer-activity-non-arcade-game-recommendations")) !== null && _a !== void 0 ? _a : "1821553152"; + } + // Whether to show the Subscriber Access Arcade Badge + get showArcadeSubscriberAccessBadge() { + var _a; + return (_a = this.implementation.boolean("show-arcade-subscriber-access-badge")) !== null && _a !== void 0 ? _a : false; + } + // Whether to show the Non-Subscriber Arcade Badge + get showArcadeNonSubscriberBadge() { + var _a; + return (_a = this.implementation.boolean("show-arcade-non-subscriber-badge")) !== null && _a !== void 0 ? _a : false; + } + /// The URL for the learn more link on the Cross Use Consent screen. + get gamesCrossUseConsentLearnMoreURL() { + return this.implementation.string("games-crossuse-consent-learn-more-url"); + } + /// The duration for the auto advance interval for the Play Now feed hero carousel + get gamesPlayNowHeroCarouselAutoAdvanceInterval() { + var _a; + return (_a = this.implementation.double("games-play-now-hero-carousel-auto-advance-interval")) !== null && _a !== void 0 ? _a : 10.0; + } + /// The max duration for the auto advance interval for the Play Now feed hero carousel + get gamesPlayNowHeroCarouselAutoAdvanceMaxInterval() { + var _a; + return (_a = this.implementation.double("games-play-now-hero-carousel-auto-advance-max-interval")) !== null && _a !== void 0 ? _a : 30.0; + } + /// The duration for the auto advance interval for the Editorial hero carousel + get gamesEditorialHeroCarouselAutoAdvanceInterval() { + var _a; + return (_a = this.implementation.double("games-editorial-hero-carousel-auto-advance-interval")) !== null && _a !== void 0 ? _a : 10.0; + } + /// The max duration for the auto advance interval for the Editorial hero carousel + get gamesEditorialHeroCarouselAutoAdvanceMaxInterval() { + var _a; + return (_a = this.implementation.double("games-editorial-hero-carousel-auto-advance-max-interval")) !== null && _a !== void 0 ? _a : 30.0; + } + get enablePreviewPlatformForWeb() { + var _a; + return (_a = this.implementation.boolean("enable-preview-platform-for-web")) !== null && _a !== void 0 ? _a : false; + } + // How long to look back for completed challenges to show in active challenges shelf + get completedChallengesInActiveShelfTimeThreshold() { + var _a; + return (_a = this.implementation.integer("completed-challenges-in-active-shelf-time-threshold")) !== null && _a !== void 0 ? _a : 86400; // default to 1 day + } + // Whether we should request for a review when we have a chance. + get requestReviewEnabled() { + var _a; + return (_a = this.implementation.boolean("request-review-enabled")) !== null && _a !== void 0 ? _a : true; + } + // The minimum number of app launches before we consider requesting for a review. + get requestReviewMinAppLaunchCount() { + var _a; + return (_a = this.implementation.integer("request-review-min-app-launch-count")) !== null && _a !== void 0 ? _a : 15; + } + // The minimum number of game launches from the app before we consider requesting a review. + get requestReviewMinGameLaunchCount() { + var _a; + return (_a = this.implementation.integer("request-review-min-game-launch-count")) !== null && _a !== void 0 ? _a : 5; + } + // Whether we should request licenses in product information + get enableLicenses() { + var _a; + return (_a = this.implementation.boolean("enable-licenses")) !== null && _a !== void 0 ? _a : false; + } + // The number of apps to display in the chart detail screen. + get chartDetailPageItemCount() { + var _a; + return (_a = this.implementation.integer("chart-detail-page-item-count")) !== null && _a !== void 0 ? _a : 25; + } + /// https://quip-apple.com/G5EGABrSh1YO#temp:C:UdDa344becf5aac473db8085f35a + /// URL query parameters that are permitted to be included in the pageUrl field of page metrics events. + get metricsAllowedListURLParams() { + let params = serverData.asArrayOrEmpty(serverData.asJSONValue(this.implementation.array("metrics-allowed-list-url-params"))); + if (serverData.isNullOrEmpty(params)) { + params = [ + "itsct", + "itscg", + "itcCt", + "ct", + "pt", + "advp", + "mttn3pid", + "mttnagencyid", + "mttncc", + "mttnpid", + "mttnsiteid", + "mttnsub1", + "mttnsub2", + "mttnsubad", + "mttnsubkw", + "mttnsubplmnt", + "id", + "term", + "salableAdamId", + "ign-itscg", + "ign-itsct", + "utm_campaign", + "clusterId", + ]; + } + return params; + } +} +BagWrapper.type = makeMetatype("app-store:bag-wrapper"); +//# sourceMappingURL=bag.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/foundation/wrappers/cached-bag.js b/node_modules/@jet-app/app-store/tmp/src/foundation/wrappers/cached-bag.js new file mode 100644 index 0000000..e7ac2db --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/foundation/wrappers/cached-bag.js @@ -0,0 +1,82 @@ +import { isSome } from "@jet/environment"; +import { Wrapper } from "./wrapper"; +/** + * A bag wrapper that implements caching. All accessed values are cached until this object is reinitialized via rebootstrap or recreated. + * + */ +export class CachedBag extends Wrapper { + constructor() { + super(...arguments); + /** + * A cache of entries + * + * Note that `Opt<T>` values which are nil are stored, representing that the bag has no value for a key, which + * is coalesced in the JS. This prevents infinite cache misses when the bag has no value for a key, resulting in + * unnecessary calls to the native client + */ + this.cache = {}; + } + registerBagKeys(keys) { + this.implementation.registerBagKeys(keys); + } + string(key, forceDisableCache = false) { + return this.fromCache(key, forceDisableCache, () => { + return this.implementation.string(key); + }); + } + double(key, forceDisableCache = false) { + return this.fromCache(key, forceDisableCache, () => { + return this.implementation.double(key); + }); + } + integer(key, forceDisableCache = false) { + return this.fromCache(key, forceDisableCache, () => { + return this.implementation.integer(key); + }); + } + boolean(key, forceDisableCache = false) { + return this.fromCache(key, forceDisableCache, () => { + return this.implementation.boolean(key); + }); + } + array(key, forceDisableCache = false) { + return this.fromCache(key, forceDisableCache, () => { + return this.implementation.array(key); + }); + } + dictionary(key, forceDisableCache = false) { + return this.fromCache(key, forceDisableCache, () => { + return this.implementation.dictionary(key); + }); + } + url(key, forceDisableCache = false) { + return this.fromCache(key, forceDisableCache, () => { + return this.implementation.url(key); + }); + } + /** + * Returns a cached value if possible to do so, otherwise uses the closure to fetch a new value which is cached and returned + * + * @param key The key on which to cache + * @param forceDisableCache Disables the cache to ensure a fresh value from the bag + * @param fetchValue A closure to fetch new values with + * @returns A previously or newly cached value + */ + fromCache(key, forceDisableCache = false, fetchValue) { + if (forceDisableCache) { + return fetchValue(); + } + else { + const cacheEntry = this.cache[key]; + if (isSome(cacheEntry)) { + return cacheEntry.value; + } + else { + const newValue = fetchValue(); + this.cache[key] = { value: newValue }; + return newValue; + } + } + } +} +//# sourceMappingURL=cached-bag.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/foundation/wrappers/client-ordering.js b/node_modules/@jet-app/app-store/tmp/src/foundation/wrappers/client-ordering.js new file mode 100644 index 0000000..683d81c --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/foundation/wrappers/client-ordering.js @@ -0,0 +1,43 @@ +/** + * Created by dersu on 6/23/17. + */ +import { makeMetatype } from "@jet/environment/util/metatype"; +import { Wrapper } from "./wrapper"; +export class ClientOrderingWrapper extends Wrapper { + async orderedVisibleIAPs(appBundleId, defaultOrdering, defaultVisibleIdentifiers, spotlightIdentifier) { + return await new Promise((resolve, reject) => { + // Collections that are parameters over the bridge cannot have `nil` constituents on the other side, so we + // should make sure we don't give such inputs or else we'll crash the app. + const cleanedDefaultOrdering = defaultOrdering.filter((item) => { + return item !== null && item !== undefined; + }); + const cleanedDefaultVisibleIdentifiers = defaultVisibleIdentifiers.filter((item) => { + return item !== null && item !== undefined; + }); + this.implementation.orderedVisibleIAPs(appBundleId, cleanedDefaultOrdering, cleanedDefaultVisibleIdentifiers, spotlightIdentifier, (ordering, error) => { + if (error) { + reject(error); + } + else { + resolve(ordering); + } + }); + }); + } + async visibilityForIAPs(productMap) { + return await new Promise((resolve, reject) => { + this.implementation.visibilityForIAPs(productMap, (visibilities, error) => { + if (error) { + // Do not reject; we should gracefully fail, since this ordering + // data isn't strictly necessary to render search results. + resolve({}); + } + else { + resolve(visibilities); + } + }); + }); + } +} +ClientOrderingWrapper.type = makeMetatype("app-store:client-ordering-wrapper"); +//# sourceMappingURL=client-ordering.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/foundation/wrappers/client.js b/node_modules/@jet-app/app-store/tmp/src/foundation/wrappers/client.js new file mode 100644 index 0000000..5a53dac --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/foundation/wrappers/client.js @@ -0,0 +1,204 @@ +/** + * Created by Pete Hare on 2/7/17. + */ +import { makeMetatype } from "@jet/environment/util/metatype"; +import { Size } from "../../api/models/base"; +import * as serverData from "../json-parsing/server-data"; +import { Wrapper } from "./wrapper"; +// region API +/** + * Client identifier for the App Store. + */ +export const appStoreIdentifier = "com.apple.AppStore" /* ClientIdentifier.AppStore */; +/** + * Client identifier for the Bridge (Watch) store. + */ +export const watchIdentifier = "com.apple.AppStore.BridgeStoreExtension" /* ClientIdentifier.AppStore_BridgeStoreExtension */; +/** + * Client identifier for the iMessages store. + */ +export const messagesIdentifier = "com.apple.MobileSMS" /* ClientIdentifier.MobileSMS */; +/** + * Client identifier for the arcade app. + */ +export const arcadeIdentifier = "com.apple.Arcade" /* ClientIdentifier.Arcade */; +/** + * Client identifier indicating ATV. + */ +export const tvIdentifier = "com.apple.TVAppStore" /* ClientIdentifier.TVAppStore */; +/** + * Client identifier for the Arcade launch repair SubscribePageExtension (iOS). + */ +export const productPageExtensionIdentifier = "com.apple.AppStore.ProductPageExtension" /* ClientIdentifier.AppStore_ProductPageExtension */; +/** + * Client identifier for the Arcade launch repair SubscribePageExtension (iOS). + */ +export const subscribePageExtensionIdentifier = "com.apple.AppStore.SubscribePageExtension" /* ClientIdentifier.AppStore_SubscribePageExtension */; +export class ClientWrapper extends Wrapper { + get buildType() { + return this.implementation.buildType; + } + // This is only supported in Luckier client builds from AppStoreJet 11.1.7 & GamesUI 2.1.5 onwards. Any previous + // clients will return undefined. + get buildVersion() { + return this.implementation.buildVersion; + } + get deviceType() { + return this.implementation.deviceType; + } + get guid() { + return this.implementation.guid; + } + get isActivityAvailable() { + return this.implementation.isActivityAvailable; + } + get isElectrocardiogramInstallationAllowed() { + return this.implementation.isElectrocardiogramInstallationAllowed; + } + get isScandiumInstallationAllowed() { + return this.implementation.isScandiumInstallationAllowed; + } + get isSidepackingEnabled() { + return this.implementation.isSidepackingEnabled; + } + get isTinkerWatch() { + return this.implementation.isTinkerWatch; + } + get screenCornerRadius() { + return this.implementation.screenCornerRadius; + } + get screenSize() { + return Size.fromNativeSize(this.implementation.screenSize); + } + get storefrontIdentifier() { + return this.implementation.storefrontIdentifier; + } + get supportsHEIF() { + return this.implementation.supportsHEIF; + } + get thinnedApplicationVariantIdentifier() { + return this.implementation.thinnedApplicationVariantIdentifier; + } + get isMandrakeSupported() { + return this.implementation.isMandrakeSupported; + } + get isCharonSupported() { + return this.implementation.isCharonSupported; + } + get isIconArtworkCapable() { + return this.implementation.isIconArtworkCapable; + } + get maxAppContentRating() { + return this.implementation.maxAppContentRating; + } + get hostBundleId() { + return this.implementation.hostBundleId; + } + isPairedSystemVersionAtLeast(version) { + var _a, _b, _c; + return (_c = (_b = (_a = this.implementation).isPairedSystemVersionAtLeast) === null || _b === void 0 ? void 0 : _b.call(_a, version)) !== null && _c !== void 0 ? _c : false; + } + deletableSystemAppCanBeInstalledOnWatchWithBundleID(bundleId) { + return this.implementation.deletableSystemAppCanBeInstalledOnWatchWithBundleID(bundleId); + } + deviceHasCapabilities(capabilities) { + return this.implementation.deviceHasCapabilities(capabilities); + } + deviceHasCapabilitiesIncludingCompatibilityCheckIsVisionOSCompatibleIOSApp(capabilities, supportsVisionOSCompatibleIOSBinary) { + if (this.isPad && capabilities.includes("healthkit")) { + // Workaround for: rdar://116905381 (J517/21C16: Unable to download AC Wellness even though App Store page says its compatible with my iPad) + return false; + } + return this.implementation.deviceHasCapabilitiesIncludingCompatibilityCheckIsVisionOSCompatibleIOSApp(capabilities, supportsVisionOSCompatibleIOSBinary); + } + isActivePairedWatchSystemVersionAtLeastMajorVersionMinorVersionPatchVersion(majorVersion, minorVersion, patchVersion) { + return this.implementation.isActivePairedWatchSystemVersionAtLeastMajorVersionMinorVersionPatchVersion(majorVersion, minorVersion, patchVersion); + } + canDevicePerformAppActionWithAppCapabilities(appAction, appCapabilities) { + return this.implementation.canDevicePerformAppActionWithAppCapabilities(appAction, appCapabilities); + } + isAutomaticDownloadingEnabled() { + return this.implementation.isAutomaticDownloadingEnabled(); + } + isAuthorizedForUserNotifications() { + return this.implementation.isAuthorizedForUserNotifications(); + } + /** + * Check whether the active paired device's OS is below a given version. + */ + isActivePairedWatchSystemVersionBelow(version) { + // We want to use isActivePairedWatchSystemVersionAtLeastMajorVersionMinorVersionPatchVersion + // rather than isPairedSystemVersionAtLeast here, as the latter is only available from YukonB onwards + const versionComponents = version.split("."); + const majorVersion = serverData.asNumber(versionComponents[0]) || 0; + const minorVersion = serverData.asNumber(versionComponents[1]) || 0; + const patchVersion = serverData.asNumber(versionComponents[2]) || 0; + return !this.implementation.isActivePairedWatchSystemVersionAtLeastMajorVersionMinorVersionPatchVersion(majorVersion, minorVersion, patchVersion); + } + /** Returns `true` for phone-factor iOS devices. */ + get isPhone() { + return this.deviceType === "phone"; + } + /** Returns `true` for pad-factor iOS devices. */ + get isPad() { + return this.deviceType === "pad"; + } + /** + * Returns `true` for iOS devices. + * + * Note: this property might be replaced with a build-time macro in "production" + */ + get isiOS() { + return this.isPhone || this.isPad; + } + /** + * Returns `true` for Mac devices + + * Note: this property might be replaced with a build-time macro in "production" + */ + get isMac() { + return this.deviceType === "mac"; + } + /** + * Returns `true` for TV devices. + + * Note: this property might be replaced with a build-time macro in "production" + */ + get isTV() { + return this.deviceType === "tv"; + } + /** + * Returns `true` for Watch devices. + + * Note: this property might be replaced with a build-time macro in "production" + */ + get isWatch() { + return this.deviceType === "watch"; + } + /** + * Returns `true` for Vision devices. + * + * Note: this property might be replaced with a build-time macro in "production" + */ + get isVision() { + return this.deviceType === "vision"; + } + /** + * Returns `true` for the Web + * + * Note: this property might be replaced with a build-time macro in "production" + */ + get isWeb() { + return this.deviceType === "web"; + } + get isCompanionVisionApp() { + return this.hostBundleId === "com.apple.visionproapp"; + } + /** Returns the list of remote identifiers to download any purchases to. */ + get remoteDownloadIdentifiers() { + return this.implementation.remoteDownloadIdentifiers; + } +} +ClientWrapper.type = makeMetatype("app-store:client-wrapper"); +// endregion +//# sourceMappingURL=client.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/foundation/wrappers/console.js b/node_modules/@jet-app/app-store/tmp/src/foundation/wrappers/console.js new file mode 100644 index 0000000..1bbfac0 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/foundation/wrappers/console.js @@ -0,0 +1,18 @@ +import { makeMetatype } from "@jet/environment/util/metatype"; +import { Wrapper } from "./wrapper"; +export class ConsoleWrapper extends Wrapper { + info(...args) { + return this.implementation.info(...args); + } + error(...args) { + return this.implementation.error(...args); + } + log(...args) { + return this.implementation.log(...args); + } + warn(...args) { + return this.implementation.warn(...args); + } +} +ConsoleWrapper.type = makeMetatype("app-store:console-wrapper"); +//# sourceMappingURL=console.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/foundation/wrappers/host.js b/node_modules/@jet-app/app-store/tmp/src/foundation/wrappers/host.js new file mode 100644 index 0000000..001a1e3 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/foundation/wrappers/host.js @@ -0,0 +1,64 @@ +import { makeMetatype } from "@jet/environment/util/metatype"; +import { Wrapper } from "./wrapper"; +export class HostWrapper extends Wrapper { + get clientIdentifier() { + return this.implementation.clientIdentifier; + } + get clientVersion() { + return this.implementation.clientVersion; + } + get deviceLocalizedModel() { + return this.implementation.deviceLocalizedModel; + } + get deviceModel() { + return this.implementation.deviceModel; + } + get deviceModelFamily() { + return this.implementation.deviceModelFamily; + } + get devicePhysicalModel() { + return this.implementation.devicePhysicalModel; + } + get deviceMarketingFamilyName() { + return this.implementation.deviceMarketingFamilyName; + } + get osBuild() { + return this.implementation.osBuild; + } + get platform() { + return this.implementation.platform; + } + isOSAtLeast(majorVersion, minorVersion, patchVersion) { + return this.implementation.isOSAtLeast(majorVersion, minorVersion, patchVersion); + } + /** Returns `true` for iOS host. */ + get isiOS() { + return this.platform === "iOS"; + } + /** Returns `true` for macOS host. */ + get isMac() { + return this.platform === "macOS"; + } + /** Returns `true` for tvOS host. */ + get isTV() { + return this.platform === "tvOS"; + } + /** Returns `true` for watchOS host. */ + get isWatch() { + return this.platform === "watchOS"; + } + /** Returns `true` for web host. */ + get isWeb() { + return this.platform === "web"; + } + /** Returns `true` for Windows host. */ + get isWindows() { + return this.platform === "Windows"; + } + /** Returns `true` for visionOS host. */ + get isVision() { + return this.platform === "xrOS"; + } +} +HostWrapper.type = makeMetatype("app-store:host-wrapper"); +//# sourceMappingURL=host.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/foundation/wrappers/localization.js b/node_modules/@jet-app/app-store/tmp/src/foundation/wrappers/localization.js new file mode 100644 index 0000000..d7e0ee8 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/foundation/wrappers/localization.js @@ -0,0 +1,437 @@ +import { makeMetatype } from "@jet/environment/util/metatype"; +import { AmpLocalization } from "../amp-localization/amp-localization"; +import { isNullOrEmpty } from "../json-parsing/server-data"; +import { Wrapper } from "./wrapper"; +import { isSome } from "@jet/environment/types/optional"; +import * as validation from "@jet/environment/json/validation"; +/** + * Wrapper around an object or `Localization` type. + * The wrapped object is implemented as part of a native app. + */ +export class LocalizationWrapper extends Wrapper { + constructor(loc, objectGraph) { + super(loc); + /** + * Path to localization file. + * The path is set when the localization file is loaded + * for the first time. + */ + this.locFile = null; + /** + * Instance of `AMPLocalization` providing server-side + * values for localized stirngs. + */ + this.ampLoc = new AmpLocalization(); + /** + * Localization strings cache. + * Cache and reuse values returned by + * wrapped `Localization` implementation. + */ + this.LOC_STRING_CACHE = {}; + this.objectGraph = objectGraph; + } + /** + * Returns the wrapped localization's identifier. + */ + get identifier() { + return this.implementation.identifier; + } + /** + * Returns the wrapped localization's safe identifier. + */ + get safeIdentifier() { + return this.implementation.identifier.split("_")[0]; + } + // endregion + // region Localization + /** + * Localizes a string replacing placehoders in key with values in the parameters dictionary + * @param key The loc key to look up + * @param params Parameters to replace in the loc string + * @return The localized string + */ + string(key, defaultValue) { + return this.implementation.string(key); + } + /** + * Localizes a string, and logs & throws an error if the key is a screamer. + * @param key The loc key to look up + * @return The localized string + */ + tryString(key) { + const value = this.implementation.string(key); + if (value === key || value === `**${key}**`) { + validation.context("tryString", () => { + validation.unexpectedType("coercedValue", "Localization key", key, null); + }); + throw new Error(`No value exists for localization key '${key}'`); + } + return value; + } + /** + * Localizes a string, and logs an error if the key is a screamer. + * @param key The loc key to look up + * @param fallback The fallback value for unlocalized keys, e.g. English value + * @return The localized string + */ + stringWithFallback(key, fallback) { + const value = this.implementation.string(key); + return value === `**AppStore.${key}**` ? fallback : value; + } + /** + * Localizes a string using a preferred locale. + * + * The implementation of this function is similar to `string(key:)` above, but with the option + * to prefer a particular locale. If one is provided, we augment the lookup key in the cache + * with the `locale` value. If one is not provided, we fallback to `string(key:)`. This makes it + * much easier to use this function directly, in place of `string(key:)` where necessary. + + * @param objectGraph The object graph, used to forward this call on if the required native function is unavailable. + * Can be removed for 2023. + * @param key The loc key to look up. + * @param locale The preferred locale to use for look up. Falls back to default, if unavailable. + * @param defaultValue A default value to use if nothing is found. + */ + stringForPreferredLocale(objectGraph, key, locale, defaultValue) { + if (isNullOrEmpty(locale)) { + return this.string(key, defaultValue); + } + const cacheKey = `${key}_${locale}`; + let value = this.LOC_STRING_CACHE[cacheKey]; + if (!value) { + value = this.implementation.stringForPreferredLocale(key, locale); + if (value && value !== key) { + this.LOC_STRING_CACHE[cacheKey] = value; + } + else { + const serverValue = this.ampLoc.localize(key); + if (serverValue !== key) { + value = serverValue; + } + else if (defaultValue) { + value = defaultValue; + } + else { + value = key; + } + } + } + return value; + } + /** + * Localize with appropriate plural form based on the count. + * + * Some languages have more plural forms than others. + * The full set of categories is "zero", "one", "two", "few", "many", and "other". + * + * The base loc key is used for "other" (the default). + * Otherwise the category is appended to the base loc key with a "." separator. + * + * http://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html + * http://cldr.unicode.org/index/cldr-spec/plural-rules#TOC-Determining-Plural-Categories + * http://unicode.org/repos/cldr/trunk/specs/ldml/tr35-numbers.html#Language_Plural_Rules + * + * English loc keys + * key = "@@count@@ dogs"; // default, aka "plural" + * key.zero = "no dogs"; // used when count is 0 + * key.one = "@@count@@ dog"; // used when count is 1, aka "singular" + * + * @param key base loc key + * @param count number used to determine the plural form + * @param params (optional) substitution keys and values, "count" will be added if it's not already present + * @return localized string + */ + stringWithCount(key, count, params) { + let value = this.implementation.stringWithCount(key, count); + if (!value || value === key) { + const serverValue = this.ampLoc.localizeWithCount(this.objectGraph, key, count, params); + if (serverValue) { + value = serverValue; + } + } + return value; + } + /** + * A variation of `stringWithCount` that supports multiple plural forms. + * @param key base loc key + * @param counts numbers used to determine the plural forms + * @param params (optional) substitution keys and values, "count" will be added if it's not already present + * @return localized string + */ + stringWithCounts(key, counts, params) { + return this.implementation.stringWithCounts(key, counts); + } + /** + * Converts a string into its uppercased form. + * @param value The string to apply the uppercase to. + * @returns {string} The loacle-specific, uppercased form of the string. If for whatever reason the upper-casing cannot + * be applied, this function simply returns the input value. + */ + uppercased(value) { + if (!value) { + return null; + } + return value.toLocaleUpperCase(this.safeIdentifier); + } + /** + * Converts a number into a localized string + * + * @param n The number to convert + * @param decimalPlaces The number of decimal places to include. + * @return {string} The localized version of the number + */ + decimal(n, decimalPlaces) { + let value = this.implementation.decimal(n, decimalPlaces); + if (!value) { + if (typeof n === "number") { + value = `* ${n.toString()} *`; + } + else { + value = this.nullString(); + } + } + return value; + } + /** + * Converts a number of bytes into a localized file size string + * + * @param bytes The number of bytes to convert + * @return The localized file size string + */ + fileSize(bytes) { + let value = this.implementation.fileSize(bytes); + if (!value) { + value = this.nullString(); + } + return value; + } + /** + * Converts a number of bytes into a localized file size string + * + * @param count The number of bytes to convert + * @return The localized file size string + */ + formattedCount(count) { + let value = this.implementation.formattedCount(count); + if (!value) { + value = this.nullString(); + } + return value; + } + /** + * Converts a number into a formatted string representation using the preferred locale identifier. + * + * @param count The number to format. + * @param locale The locale identifier to prefer for lookup. + * @return The localized string. + */ + formattedCountForPreferredLocale(objectGraph, count, locale) { + if (isNullOrEmpty(locale)) { + return this.formattedCount(count); + } + let value = this.implementation.formattedCountForPreferredLocale(count, locale); + if (!value) { + value = this.nullString(); + } + return value; + } + /** + * Converts a date into a time ago label, showing how long ago + * the date occurred + * + * @param date The date object to convert + * @param context The context in which the date should be formatted + * @return The localized string representing the amount of time that has passed + */ + timeAgoWithContext(date, context) { + let value = this.implementation.timeAgoWithContext(date, context); + if (!value) { + value = this.nullString(); + } + return value; + } + /** + * Converts a date into a localized date string using the provided format + * + * @param format The format string describing how the date should be formatted + * @param date The date object to convert + * @return The localized string representing the date + */ + formatDate(format, date) { + let value = this.implementation.formatDate(format, date); + if (!value) { + value = this.nullString(); + } + return value; + } + formatDateWithContext(format, date, context) { + let value = this.implementation.formatDateWithContext(format, date, context); + if (!value) { + value = this.nullString(); + } + return value; + } + formatDateInSentence(sentence, format, date) { + let value = this.implementation.formatDateInSentence(sentence, format, date); + if (!value) { + value = this.nullString(); + } + return value; + } + /** + * Converts a date into a relative date, showing how long ago + * the date occurred + * + * @param date The date object to convert + * @return The localized string representing the amount of time that has passed + */ + relativeDate(date) { + let value = this.implementation.relativeDate(date); + if (!value) { + value = this.nullString(); + } + return value; + } + formatDuration(value, unit) { + let result = this.implementation.formatDuration(value, unit); + if (!result) { + result = this.nullString(); + } + return result; + } + // endregion + // region Private Methods + /** + * Applies a new localization file and localization dictionary as the new set of localizations + * + * @param file The file name of the file that is loaded + * @param localizations The localizations dictionary to use + * @param locale Current normalized locale which includes language code + */ + applyLocalizations(file, localizations, locale) { + if (this.isLocFileLoaded(file)) { + return; + } + this.locFile = file; + // The first 2 characters of normalized locale are the language code. + this.ampLoc.updateLocalizationData(localizations, locale.slice(0, 2)); + } + /** + * Checks if the named loc file is already loaded + * @param file The file name to check + * @return {boolean} True or false depending on whether the file name is loaded + */ + isLocFileLoaded(file) { + return this.locFile === file; + } + /** + * Normalizes the given bag language into a locale. + * @param language The language to normalize. + * @param storefrontIdentifier The storefront identifier to use. + * @return A locale to use. + */ + normalizedLocale(objectGraph, language, storefrontIdentifier) { + language = language.toLowerCase(); + switch (language) { + case "yue-hant": { + // Country code from the bag is not available in JS, so we use the storefront. + const macauStoreFrontIdentifier = objectGraph.props.asString("macauStorefrontIdentifier"); + if (typeof storefrontIdentifier === "string" && + typeof macauStoreFrontIdentifier === "string" && + storefrontIdentifier.indexOf(macauStoreFrontIdentifier) !== -1) { + return "zh-ma"; + } + else { + return "zh-hk"; + } + } + default: + return language; + } + } + nullString() { + return "* null *"; + } + // endregion + // region API + /** + * Loads the localizations from the JetPack. + */ + load(objectGraph) { + // Sanity check + if (objectGraph.bag.language === undefined || objectGraph.bag.language === null) { + throw new Error("Bag language is not available. Unable to load localizations."); + } + const locale = this.normalizedLocale(objectGraph, objectGraph.bag.language, objectGraph.client.storefrontIdentifier); + const locName = `local/${locale}`; + // Load localizations if needed + if (!this.isLocFileLoaded(locName)) { + const localizations = objectGraph.props.asDictionary(`localizations.${locale}`); + if (localizations !== undefined && localizations !== null) { + this.applyLocalizations(locName, localizations, locale); + } + else { + // Fallback to english + const fallbackLocalizations = objectGraph.props.asDictionary(`localizations.en-us`); + if (fallbackLocalizations !== undefined && fallbackLocalizations !== null) { + this.applyLocalizations(locName, fallbackLocalizations, locale); + } + } + } + } + /** + * Returns the localized name for the provided device type. + * The company policy seems to be to always use the branded, + * non-localized text, but this is just-in-case. + * @returns {string} The display name for the device. + */ + deviceDisplayName(objectGraph) { + if (objectGraph.client.isVision && isSome(objectGraph.host.deviceMarketingFamilyName)) { + return objectGraph.host.deviceMarketingFamilyName; + } + if (objectGraph.host.deviceLocalizedModel) { + return objectGraph.host.deviceLocalizedModel; + } + // TODO: <rdar://problem/33575833> Jet: Remove brand loc fallbacks + switch (objectGraph.client.deviceType) { + case "phone": + const localizedPhoneName = this.string("IPHONE_BRAND_NAME"); + if (localizedPhoneName === "IPHONE_BRAND_NAME") { + return "iPhone"; + } + return localizedPhoneName; + case "pad": + const localizedPadName = this.string("IPAD_BRAND_NAME"); + if (localizedPadName === "IPAD_BRAND_NAME") { + return "iPad"; + } + return localizedPadName; + case "tv": + const localizedTvName = this.string("APPLE_TV_BRAND_NAME"); + if (localizedTvName === "APPLE_TV_BRAND_NAME") { + return "Apple\u00a0TV"; + } + return localizedTvName; + case "watch": + const localizedWatchName = this.string("APPLE_WATCH_BRAND_NAME"); + if (localizedWatchName === "APPLE_WATCH_BRAND_NAME") { + return "Apple\u00a0Watch"; + } + return localizedWatchName; + case "mac": + const localizedMacName = this.string("MAC_BRAND_NAME"); + if (localizedMacName === "MAC_BRAND_NAME") { + return "Mac"; + } + return localizedMacName; + default: + return null; + } + } +} +// region Properties +/** + * Localization wrapper metatype to use with object graph. + */ +LocalizationWrapper.type = makeMetatype("app-store:loc-wrapper"); +//# sourceMappingURL=localization.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/foundation/wrappers/properties.js b/node_modules/@jet-app/app-store/tmp/src/foundation/wrappers/properties.js new file mode 100644 index 0000000..fa8b4e1 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/foundation/wrappers/properties.js @@ -0,0 +1,74 @@ +/** + * Created by keithpk on 4/2/17. + */ +import { makeMetatype } from "@jet/environment/util/metatype"; +import * as serverData from "../json-parsing/server-data"; +import { Wrapper } from "./wrapper"; +export class PropertiesWrapper extends Wrapper { + /** + * Returns the raw underlying value for the key path + * @param path The path of the property to lookup + * @return {JSONValue | null} The resulting value or null + */ + value(path) { + return serverData.traverse(this.implementation, path); + } + /** + * Returns whether a property is enabled or not. This also + * looks in the clientFeatures key as well for client-enabled + * features + * + * @param key The key to look up + * @return {boolean} The boolean result or false if no value was found for the key + */ + enabled(key) { + const propertyValue = this.value(key); + if (typeof propertyValue !== "undefined") { + return Boolean(propertyValue); + } + return Boolean(this.implementation.clientFeatures[key]); + } + /** + * Syntactic sugar for !enabled(key). + * + * @param key The key to look up + * @return {boolean} The boolean result or true if no value was found for the key + */ + isNotEnabled(key) { + return !this.enabled(key); + } + /** + * Returns the value of the path coerced as a dictionary + * @param path The path to look up + * @return {JSONData|null} The dictionary or null if not found + */ + asDictionary(path) { + return serverData.asDictionary(this.implementation, path); + } + /** + * Returns the value of the path coerced as a string + * @param path The path to look up + * @return {string|null} The string or null if not found + */ + asString(path) { + return serverData.asString(this.implementation, path); + } + /** + * Returns the value of the path coerced as a number + * @param path The path to look up + * @return {string|null} The number or null if not found + */ + asNumber(path) { + return serverData.asNumber(this.implementation, path); + } + /** + * Returns the value of the path coerced as an array + * @param path The path to look up + * @return {string|null} The array or empty if not found + */ + asArray(path) { + return serverData.asArrayOrEmpty(this.implementation, path); + } +} +PropertiesWrapper.type = makeMetatype("app-store:props-wrapper"); +//# sourceMappingURL=properties.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/foundation/wrappers/storage.js b/node_modules/@jet-app/app-store/tmp/src/foundation/wrappers/storage.js new file mode 100644 index 0000000..2643e0d --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/foundation/wrappers/storage.js @@ -0,0 +1,22 @@ +/** + * Creates and returns a mock Storage object. + * @return A new mock Storage object. + */ +import { makeMetatype } from "@jet/environment/util/metatype"; +import { Wrapper } from "./wrapper"; +export class StorageWrapper extends Wrapper { + retrieveString(key) { + const value = this.implementation.retrieveString(key); + if ((value === null || value === void 0 ? void 0 : value.length) > 0 && value !== "<null>") { + return value; + } + else { + return null; + } + } + storeString(key, value) { + this.implementation.storeString(value, key); // flip is deliberate. + } +} +StorageWrapper.type = makeMetatype("app-store:storage-wrapper"); +//# sourceMappingURL=storage.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/foundation/wrappers/wrapper.js b/node_modules/@jet-app/app-store/tmp/src/foundation/wrappers/wrapper.js new file mode 100644 index 0000000..f03e835 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/foundation/wrappers/wrapper.js @@ -0,0 +1,6 @@ +export class Wrapper { + constructor(implementation) { + this.implementation = implementation; + } +} +//# sourceMappingURL=wrapper.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/gameservicesui/src/foundation/media-api/requests/recommendation-request-types.js b/node_modules/@jet-app/app-store/tmp/src/gameservicesui/src/foundation/media-api/requests/recommendation-request-types.js new file mode 100644 index 0000000..7005cf7 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/gameservicesui/src/foundation/media-api/requests/recommendation-request-types.js @@ -0,0 +1,49 @@ +export const AllPlayNowFeedContent = [ + "players", + "apps", + "achievements", + "leaderboards", + "contents", + "challenges", + "challenge-instances", + "activities", +]; +export const AllPlayNowFeedContentTypes = [ + "friend-request-modules", + "friend-playing-modules", + "achievement-modules", + "leaderboard-modules", + "chart-modules", + "content-modules", + "challenge-invite-modules", + "challenge-modules", + "challenge-suggestion-modules", + "on-device-modules", + "activities", + "activity-invite-modules", + "activity-modules", + "activity-suggestion-modules", +]; +export const PlayNowFeedAppsAttributes = [ + "compatibilityControllerRequirement", + "editorialArtwork", + "editorialClientParams", + "editorialVideo", + "expectedReleaseDateDisplayFormat", + "isAppleWatchSupported", + "minimumOSVersion", + "screenshotsByType", + "videoPreviewsByType", + "gameDisplayName", + "miniGamesDeepLink", +]; +export const PlayNowFeedEditorialItemsAttributes = [ + "editorialArtwork", + "editorialClientParams", + "editorialVideo", + "expectedReleaseDateDisplayFormat", + "showExpectedReleaseDate", +]; +export const PlayNowFeedEditorialPagesAttributes = ["editorialArtwork", "editorialClientParams", "editorialVideo"]; +export const AppEventsAttributes = ["description", "productArtwork", "productVideo"]; +//# sourceMappingURL=recommendation-request-types.js.map
\ No newline at end of file diff --git a/node_modules/@jet-app/app-store/tmp/src/gameservicesui/src/foundation/object-graph-types.js b/node_modules/@jet-app/app-store/tmp/src/gameservicesui/src/foundation/object-graph-types.js new file mode 100644 index 0000000..26d7621 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/gameservicesui/src/foundation/object-graph-types.js @@ -0,0 +1,20 @@ +import { makeMetatype } from "@jet/environment/util/metatype"; +import * as jetTypes from "@jet/environment/types/globals/types"; +/** + * Known dependencies that injectable from ObjectGraph. + */ +export const ObjectGraphType = { + bag: jetTypes.bag, + dispatcher: makeMetatype("dispatcher"), + host: jetTypes.host, + localizer: jetTypes.localizer, + network: jetTypes.net, + plist: jetTypes.plist, + platform: jetTypes.platform, + router: makeMetatype("router"), + client: makeMetatype("client"), + debugSettings: makeMetatype("debugSettings"), + nativeIntentDispatcher: makeMetatype("nativeIntentDispatcher"), + personNameComponentsFormatter: makeMetatype("personNameComponentsFormatter"), +}; +//# sourceMappingURL=object-graph-types.js.map
\ No newline at end of file |
