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