From bce557cc2dc767628bed6aac87301a1be7c5431b Mon Sep 17 00:00:00 2001 From: rxliuli Date: Tue, 4 Nov 2025 05:03:50 +0800 Subject: init commit --- .../common/app-promotions/app-promotions-common.js | 385 +++++++++++++++++++++ 1 file changed, 385 insertions(+) create mode 100644 node_modules/@jet-app/app-store/tmp/src/common/app-promotions/app-promotions-common.js (limited to 'node_modules/@jet-app/app-store/tmp/src/common/app-promotions/app-promotions-common.js') 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 -- cgit v1.2.3