From bce557cc2dc767628bed6aac87301a1be7c5431b Mon Sep 17 00:00:00 2001 From: rxliuli Date: Tue, 4 Nov 2025 05:03:50 +0800 Subject: init commit --- .../src/common/app-promotions/app-event-detail.js | 131 +++++++ .../tmp/src/common/app-promotions/app-event.js | 243 +++++++++++++ .../tmp/src/common/app-promotions/app-promotion.js | 120 +++++++ .../common/app-promotions/app-promotions-common.js | 385 +++++++++++++++++++++ .../common/app-promotions/app-promotions-shelf.js | 120 +++++++ .../app-promotions/contingent-offer-detail.js | 106 ++++++ .../src/common/app-promotions/contingent-offer.js | 189 ++++++++++ .../src/common/app-promotions/offer-item-detail.js | 147 ++++++++ .../tmp/src/common/app-promotions/offer-item.js | 206 +++++++++++ 9 files changed, 1647 insertions(+) create mode 100644 node_modules/@jet-app/app-store/tmp/src/common/app-promotions/app-event-detail.js create mode 100644 node_modules/@jet-app/app-store/tmp/src/common/app-promotions/app-event.js create mode 100644 node_modules/@jet-app/app-store/tmp/src/common/app-promotions/app-promotion.js create mode 100644 node_modules/@jet-app/app-store/tmp/src/common/app-promotions/app-promotions-common.js create mode 100644 node_modules/@jet-app/app-store/tmp/src/common/app-promotions/app-promotions-shelf.js create mode 100644 node_modules/@jet-app/app-store/tmp/src/common/app-promotions/contingent-offer-detail.js create mode 100644 node_modules/@jet-app/app-store/tmp/src/common/app-promotions/contingent-offer.js create mode 100644 node_modules/@jet-app/app-store/tmp/src/common/app-promotions/offer-item-detail.js create mode 100644 node_modules/@jet-app/app-store/tmp/src/common/app-promotions/offer-item.js (limited to 'node_modules/@jet-app/app-store/tmp/src/common/app-promotions') 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 = "" + mediaAttributes.attributeAsString(branch, "name") + ""; + const skuDescription = mediaAttributes.attributeAsString(branch, "description.standard") + "
"; + const termsTitle = "" + objectGraph.loc.string("ContingentOffer.Terms.Title") + ""; + const terms = mediaAttributes.attributeAsString(data, "additionalTerms"); + return new models.Paragraph([skuNameTitle, skuDescription, termsTitle, terms].join("
"), "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) ? "" + skuName + "" : undefined; + const skuDescription = mediaAttributes.attributeAsString(iapData, "description.standard"); + const skuDescriptionFormatted = isSome(skuDescription) ? skuDescription + "
" : undefined; + const termsTitle = "" + objectGraph.loc.string("Promotion.Terms.Title") + ""; + 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("
"); + 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 -- cgit v1.2.3