1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
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
|