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
386
387
388
389
390
391
392
393
394
395
|
import { isSome } from "@jet/environment";
import * as models from "../../../api/models";
import * as serverData from "../../../foundation/json-parsing/server-data";
import * as mediaDataFetching from "../../../foundation/media/data-fetching";
import * as mediaNetwork from "../../../foundation/media/network";
import { ResponseMetadata } from "../../../foundation/network/network";
import { Parameters, Path, Protocol } from "../../../foundation/network/url-constants";
import * as urls from "../../../foundation/network/urls";
import * as mediaUrlMapping from "../../builders/url-mapping";
import { shouldUsePrerenderedIconArtwork } from "../../content/content";
import * as metricsHelpersImpressions from "../../metrics/helpers/impressions";
import * as metricsHelpersLocation from "../../metrics/helpers/location";
import * as impressionDemotion from "../../personalization/on-device-impression-demotion";
import * as productVariants from "../../product-page/product-page-variants";
import * as refresh from "../../refresh/page-refresh-controller";
import * as groupingShelfControllerCommon from "./grouping-shelf-controller-common";
/**
* A GroupingShelfController is responsible for all the logic around parsing and rending
* a single grouping page shelf.
*
* The `ShelfMetadata` is a type the is specific for a given shelf and has some additional data needed to render
* that shelf.
*/
export class GroupingShelfController {
// endregion
// region AnyGroupingShelfController
/**
* Indicates whether this grouping shelf controller can create a shelf for the given mediaApiData.
* @param objectGraph The App Store dependency graph
* @param mediaApiData The outer data object containing the FC properties and data
* @param featuredContentId The featured content id for this shelf data
* @param nativeGroupingShelfId The id of the custom shelf type, one not defined on the server
*/
supports(objectGraph, mediaApiData, featuredContentId, nativeGroupingShelfId) {
return this._supports(objectGraph, mediaApiData, featuredContentId, nativeGroupingShelfId);
}
/**
* Indicates whether this grouping shelf controller can create a shelf for the given mediaApiData.
* @param objectGraph The App Store dependency graph
* @param mediaApiData The outer data object containing the FC properties and data
* @param featuredContentId The featured content id for this shelf data
* @param nativeGroupingShelfId The id of the custom shelf type, one not defined on the server
*/
_supports(objectGraph, mediaApiData, featuredContentId, nativeGroupingShelfId) {
const isFeaturedContentIdSupported = this.supportedFeaturedContentIds.has(featuredContentId);
let isNativeGroupingShelfIdSupported;
if (serverData.isDefinedNonNull(nativeGroupingShelfId)) {
isNativeGroupingShelfIdSupported = this.supportedNativeGroupingShelfIds.has(nativeGroupingShelfId);
}
else {
isNativeGroupingShelfIdSupported = true;
}
return isFeaturedContentIdSupported && isNativeGroupingShelfIdSupported;
}
/**
* This method will return a grouping page shelf regardless of the type of controller
* @param objectGraph The App Store dependency graph
* @param groupingParseContext The parse context for the grouping page so far
* @param mediaApiData The outer data object containing the FC properties and data
* @param baseShelfToken The base grouping shelf token created by the grouping-controller
* @param baseMetricsOptions The minimum set of metrics options for this shelf, created by the
* grouping page controller
*/
createShelf(objectGraph, mediaApiData, groupingParseContext, baseShelfToken, baseMetricsOptions) {
var _a, _b, _c;
const typedMediaApiData = mediaApiData;
const shelfData = this.initialShelfDataFromGroupingMediaApiData(objectGraph, typedMediaApiData);
const shelfToken = this.shelfTokenFromBaseTokenAndMediaApiData(objectGraph, typedMediaApiData, baseShelfToken, groupingParseContext);
const shelfMetricsOptions = this.shelfMetricsOptionsFromBaseMetricsOptions(objectGraph, shelfToken, baseMetricsOptions);
const hasShelfMetricsOptions = serverData.isDefinedNonNullNonEmpty(shelfMetricsOptions);
if (hasShelfMetricsOptions && this.shouldImpressShelf()) {
metricsHelpersLocation.pushContentLocation(objectGraph, shelfMetricsOptions, shelfToken.title);
}
/// Reorder the shelf contents based on the impression data if available
if (serverData.isDefinedNonNullNonEmpty(shelfData.shelfContents)) {
shelfData.shelfContents = impressionDemotion.personalizeDataItems(shelfData.shelfContents, (_a = groupingParseContext.recoImpressionData) !== null && _a !== void 0 ? _a : {}, (_b = baseMetricsOptions === null || baseMetricsOptions === void 0 ? void 0 : baseMetricsOptions.recoMetricsData) !== null && _b !== void 0 ? _b : {});
}
const shelf = this._createShelf(objectGraph, shelfToken, shelfData, groupingParseContext);
if (hasShelfMetricsOptions && this.shouldImpressShelf()) {
metricsHelpersLocation.popLocation(shelfMetricsOptions.locationTracker);
if (serverData.isDefinedNonNull(shelf)) {
metricsHelpersImpressions.addImpressionFields(objectGraph, shelf, shelfMetricsOptions);
// rdar://84952935 (Placeholder shelves are not being impressed
// For placeholder shelves we end up replacing the entire shelf, so we need to make sure the original
// impression metrics are included in the token, so they can be added when the real content is fetched
// We're doing this here because this is where we decide whether the original shelf should be impressed
if (((_c = shelf.url) === null || _c === void 0 ? void 0 : _c.length) > 0 &&
serverData.isDefinedNonNullNonEmpty(shelf.impressionMetrics) &&
shelfToken.showingPlaceholders) {
const originalShelfUrlString = shelf.url;
try {
// Extract the token from the URL.
// Note: Although we have access to the shelfToken here, we do not know that
// the url was constructed from token in its current state. To be safe,
// if not efficient, we reverse engineer the URL to get the token.
const originalShelfUrl = urls.URL.from(originalShelfUrlString);
const encodedToken = originalShelfUrl.pathComponents().pop();
const shelfTokenFromUrl = JSON.parse(decodeURIComponent(encodedToken));
// Modify the token to include the impressions metrics.
shelfTokenFromUrl.originalPlaceholderShelfImpressionMetrics = shelf.impressionMetrics;
groupingShelfControllerCommon.updateShelfUrlWithNewToken(objectGraph, shelf, shelfTokenFromUrl);
}
catch {
shelf.url = originalShelfUrlString;
}
}
}
}
this.finalizeInitialShelfForDisplay(objectGraph, shelf, shelfToken, shelfData, groupingParseContext);
if (hasShelfMetricsOptions && this.shouldPrepareLocationTrackerForNextPosition()) {
metricsHelpersLocation.nextPosition(groupingParseContext.metricsLocationTracker);
}
return shelf;
}
/**
* Initialize a builder with globally unique name.
*
* @param {string} builderClass Globally unique name.
*/
constructor(builderClass) {
// region Supported Types
this.supportedFeaturedContentIds = new Set([]);
this.supportedNativeGroupingShelfIds = new Set([]);
this.builderClass = builderClass;
}
/**
* Determines the strategy for fetching incomplete shelves based on feature flags and shelf type
*
* @param objectGraph - The application store object graph.
* @returns The strategy for fetching incomplete shelves, either on shelf appearance or on page load.
*/
incompleteShelfFetchStrategy(objectGraph) {
if (objectGraph.client.isiOS) {
return models.IncompleteShelfFetchStrategy.OnShelfWillAppear;
}
else {
return models.IncompleteShelfFetchStrategy.OnPageLoad;
}
}
// endregion
// region Metrics
/**
* Return the shelf metrics options to use for this specific shelf. Using the base options from the grouping
* page controller
*
* @param objectGraph The App Store dependency graph
* @param shelfToken The shelf shelfToken for this current shelf creation request
* @param baseMetricsOptions The minimum set of metrics options for this shelf, created by the
* grouping page controller
*/
shelfMetricsOptionsFromBaseMetricsOptions(objectGraph, shelfToken, baseMetricsOptions) {
return baseMetricsOptions;
}
/**
* Whether the shelf itself should be impressed, there are some cases where the shelf itself
* does not get impressed, just the contents.
*/
shouldImpressShelf() {
return true;
}
/**
* Whether we should move the location tracker to the next position after creating our shelf
*/
shouldPrepareLocationTrackerForNextPosition() {
return true;
}
// endregion
// Shelf Finalization
/**
* This method will set any required fields on our shelf once it has created as part of the initial page rendering.
* This includes things like timing metrics, hiding empty shelves etc.
*
* @param objectGraph The App Store dependency graph
* @param shelf The created shelf
* @param shelfToken The shelf shelfToken for this current shelf creation request
* @param shelfData The media api shelfContents array for this shelf
* @param groupingParseContext The parse context for the grouping page so far
* @private
*/
finalizeInitialShelfForDisplay(objectGraph, shelf, shelfToken, shelfData, groupingParseContext) {
var _a, _b;
if (serverData.isNullOrEmpty(shelf)) {
return;
}
// Should not show see all links on search groupings
if (shelfToken.isSearchLandingPage) {
groupingShelfControllerCommon.modifyShelfForSearchLandingGrouping(objectGraph, shelf, shelfToken);
}
if (((_a = shelf.url) === null || _a === void 0 ? void 0 : _a.length) > 0 &&
serverData.isDefinedNonNullNonEmpty(groupingParseContext.additionalShelfParameters)) {
shelf.url = urls.URL.from(shelf.url)
.append("query", groupingParseContext.additionalShelfParameters)
.build();
}
// If we're on iOS and no prior fetch strategy has been defined, set the fetchStrategy to OnShelfWillAppear.
shelf.fetchStrategy = this.incompleteShelfFetchStrategy(objectGraph);
// Shelf will fetch content after sending follow-up fetch request.
const willFetchShelfContent = isSome(shelf) && ((_b = shelf.url) === null || _b === void 0 ? void 0 : _b.length) > 0;
if (serverData.isNullOrEmpty(shelf.items) && !willFetchShelfContent) {
shelf.isHidden = true;
}
shelf.accessibilityMetadata = createShelfAccessibilityMetadata(objectGraph, shelf);
}
/**
* This method will set any required fields on our shelf once it has been fetched as a result of a secondary fetch.
* This includes things like timing metrics, hiding empty shelves etc.
*
* @param objectGraph The AppStore dependency graph
* @param shelf The created shelf
* @param shelfToken The shelf shelfToken for this current shelf creation request
* @param shelfData The media api shelfContents array for this shelf
* @private
*/
finalizeSecondaryShelfForDisplay(objectGraph, shelf, shelfToken, shelfData) {
if (serverData.isNullOrEmpty(shelf)) {
return;
}
if (shelfToken.remainingItems.length) {
const remainingIds = shelfToken.remainingItems.map((data) => {
return data.id;
});
objectGraph.console.warn("Could not load items for: " + remainingIds.join(","));
}
if (shelf) {
shelf.mergeWhenFetched = groupingShelfControllerCommon.shelfFetchShouldMergeWhenFetched(objectGraph, shelfToken);
shelf.networkTimingMetrics = shelfData.responseTimingValues;
shelf.nextPreferredContentRefreshDate = refresh.nextPreferredContentRefreshDateForController(refresh.newPageRefreshController());
}
// Merge the original impression metrics with the newly created impression metrics.
if (serverData.isDefinedNonNullNonEmpty(shelfToken.originalPlaceholderShelfImpressionMetrics)) {
// If the `shelf.impressionMetrics` is null, we just defer to the original metrics.
if (serverData.isNull(shelf.impressionMetrics)) {
shelf.impressionMetrics = shelfToken.originalPlaceholderShelfImpressionMetrics;
}
else {
for (const key in shelfToken.originalPlaceholderShelfImpressionMetrics.fields) {
if (Object.prototype.hasOwnProperty.call(shelfToken.originalPlaceholderShelfImpressionMetrics.fields, key)) {
shelf.impressionMetrics.fields[key] =
shelfToken.originalPlaceholderShelfImpressionMetrics.fields[key];
}
}
}
}
if (!shelfToken.hasExistingContent && serverData.isNullOrEmpty(shelf.items)) {
shelf.isHidden = true;
}
// Should not show see all links on search groupings
if (shelfToken.isSearchLandingPage) {
groupingShelfControllerCommon.modifyShelfForSearchLandingGrouping(objectGraph, shelf, shelfToken);
}
shelf.accessibilityMetadata = createShelfAccessibilityMetadata(objectGraph, shelf);
}
// endregion
// region ShelfBuilder
async handleShelf(objectGraph, url, parameters, matchedRuleIdentifier) {
const tokenJson = parameters["token"];
const shelfToken = JSON.parse(tokenJson);
shelfToken.isFirstRender = false;
try {
const shelfData = await this.secondaryShelfDataForShelfUrl(objectGraph, url, shelfToken, parameters);
const shelf = this._createShelf(objectGraph, shelfToken, shelfData, null);
this.finalizeSecondaryShelfForDisplay(objectGraph, shelf, shelfToken, shelfData);
return shelf;
}
catch (error) {
if (shelfToken && !shelfToken.hasExistingContent) {
const hiddenShelf = new models.Shelf(shelfToken.shelfStyle);
hiddenShelf.isHidden = true;
return hiddenShelf;
}
else {
throw error;
}
}
}
shelfRoute(objectGraph) {
if (serverData.isDefinedNonNullNonEmpty(this.supportedNativeGroupingShelfIds)) {
return routesForNativeGroupingShelfIds(this.supportedNativeGroupingShelfIds);
}
else {
return routesForFeaturedContentIds(this.supportedFeaturedContentIds);
}
}
// endregion
// region Static Base Helpers
/**
* This is a standard default implementation for the secondary shelf data fetch. This can be used for all the
* grouping shelf controls that dont implement a custom ShelfDataType
* @param objectGraph
* @param shelfUrl
* @param parameters
*/
static async secondaryGroupingShelfDataForShelfUrl(objectGraph, shelfUrl, shelfToken, parameters) {
return await GroupingShelfController.secondaryGroupingShelfMediaApiData(objectGraph, shelfUrl, shelfToken, parameters).then((mediaApiData) => {
const hydratedItems = groupingShelfControllerCommon.hydratedRemainingItemsForShelfTokenFromMediaApiData(objectGraph, shelfToken, mediaApiData);
return {
shelfContents: hydratedItems,
responseTimingValues: mediaApiData[ResponseMetadata.timingValues],
};
});
}
/**
* This is a standard default implementation for the media api request, for an incomplete grouping shelf.
*
* @param objectGraph
* @param shelfUrl
* @param parameters
*/
static async secondaryGroupingShelfMediaApiData(objectGraph, shelfUrl, shelfToken, parameters) {
const urlString = shelfUrl.build();
let request;
if (mediaUrlMapping.isMediaUrl(objectGraph, shelfUrl)) {
request = new mediaDataFetching.Request(objectGraph, urlString);
}
else {
request = groupingShelfControllerCommon.generateShelfRequest(objectGraph, shelfToken, parameters);
}
if (!request) {
return await Promise.reject(new Error(`Could not construct media API request for: ${shelfUrl}`));
}
request.includingAdditionalPlatforms(defaultMediaApiPlatforms(objectGraph));
request.includingAttributes(defaultMediaApiAttributes(objectGraph));
request.usingCustomAttributes(productVariants.shouldFetchCustomAttributes(objectGraph));
request.attributingTo(shelfUrl.build());
return await mediaNetwork.fetchData(objectGraph, request).then((mediaApiData) => {
groupingShelfControllerCommon.flushRequestedItemsFromShelfToken(shelfToken, request.ids);
return mediaApiData;
});
}
}
export function createShelfAccessibilityMetadata(objectGraph, shelf) {
var _a;
let accessibilityLabel = objectGraph.loc.string("Shelves.Accessibility.Label");
if (isSome(shelf.title)) {
accessibilityLabel = `${shelf.title}, ${accessibilityLabel}`;
}
else if (isSome((_a = shelf.header) === null || _a === void 0 ? void 0 : _a.title)) {
accessibilityLabel = `${shelf.header.title}, ${accessibilityLabel}`;
}
const accessibilityRoleDescription = objectGraph.loc.string("Shelves.Accessibility.RoleDescription");
return {
label: accessibilityLabel,
roleDescription: accessibilityRoleDescription,
};
}
function routeForFeaturedContentId(featuredContentId, nativeGroupingShelfId, additionalQueryParams) {
const query = serverData.isDefinedNonNullNonEmpty(additionalQueryParams)
? [...additionalQueryParams]
: [];
query.push(`${Parameters.groupingFeaturedContentId}=${featuredContentId}`);
if (serverData.isDefinedNonNullNonEmpty(nativeGroupingShelfId)) {
query.push(`${Parameters.nativeGroupingShelfId}=${nativeGroupingShelfId}`);
}
return {
protocol: Protocol.internal,
path: `/${Path.grouping}/${Path.shelf}/{token}`,
query: query,
};
}
export function routesForFeaturedContentIds(featuredContentIds, additonalQueryParams) {
const routes = [];
for (const featuredContentId of featuredContentIds) {
routes.push(routeForFeaturedContentId(featuredContentId, null, additonalQueryParams));
}
return routes;
}
export function routesForNativeGroupingShelfIds(nativeGroupingShelfIds, additonalQueryParams) {
const routes = [];
for (const nativeGroupingShelfId of nativeGroupingShelfIds) {
routes.push(routeForFeaturedContentId(-1 /* FeaturedContentID.Native_GroupingShelf */, nativeGroupingShelfId, additonalQueryParams));
}
return routes;
}
// region Media Api Attributes
function defaultMediaApiPlatforms(objectGraph) {
return mediaDataFetching.defaultAdditionalPlatformsForClient(objectGraph);
}
function defaultMediaApiAttributes(objectGraph) {
const attributes = ["editorialArtwork", "isAppleWatchSupported", "requiredCapabilities", "badge-content"];
if (objectGraph.appleSilicon.isSupportEnabled) {
attributes.push("macRequiredCapabilities");
}
if (objectGraph.client.isMac) {
attributes.push("hasMacIPAPackage");
}
if (objectGraph.bag.enableUpdatedAgeRatings) {
attributes.push("ageRating");
}
if (shouldUsePrerenderedIconArtwork(objectGraph)) {
attributes.push("iconArtwork");
}
return attributes;
}
// endregion
//# sourceMappingURL=grouping-shelf-controller.js.map
|