summaryrefslogtreecommitdiff
path: root/node_modules/@jet-app/app-store/tmp/src/common/arcade/arcade-common.js
blob: 440d3503b3b68a735e4073c20d8198be4d5cbe7e (plain)
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
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
/**
 * Created by ls on 5/15/18.
 */
import { Color, isSome } from "@jet/environment";
import * as models from "../../api/models/index";
import * as serverData from "../../foundation/json-parsing/server-data";
import { isDefinedNonNull } from "../../foundation/json-parsing/server-data";
import * as mediaFetching from "../../foundation/media/data-fetching";
import * as mediaNetwork from "../../foundation/media/network";
import * as mediaRelationships from "../../foundation/media/relationships";
import { Path, Protocol } from "../../foundation/network/url-constants";
import * as urls from "../../foundation/network/urls";
import * as artworkBuilder from "../content/artwork/artwork";
import * as metricsHelpersClicks from "../metrics/helpers/clicks";
import * as productPageVariants from "../product-page/product-page-variants";
import { MetricsIdentifierType } from "../../foundation/metrics/metrics-identifiers-cache";
import { named } from "../../foundation/util/color-util";
import { makeRoutableArcadeSeeAllPageIntent } from "../../api/intents/routable-arcade-see-all-page-intent";
import { getPlatform } from "../preview-platform";
import { getLocale } from "../locale";
import { makeArcadeSeeAllCanonicalUrl } from "./arcade-see-all-routing";
import { shouldUsePrerenderedIconArtwork } from "../content/content";
// endregion
// region Arcade Navigation Actions
/**
 * Creates a flow action for going to the page to see all Arcade games.
 * Defaults to sorting by release date (descending)
 * @param {ArcadeSeeAllGamesPageSort} sort The order which the games in response will be sorted in.
 * @param metricsPageInformation
 * @param metricsLocationTracker
 * @returns {FlowAction} Flow action to Arcade see all games page.
 */
export function seeAllArcadeGamesPageFlowAction(objectGraph, sort = "releaseDate", metricsPageInformation, metricsLocationTracker, title = undefined, id = undefined, idType = undefined, targetType = "button") {
    const seeAllGamesUrl = urls.URL.fromComponents(Protocol.internal, null, `/${Path.arcadeSeeAllGames}`, {
        sort: sort,
    });
    const flowAction = new models.FlowAction("arcadeSeeAllGames", seeAllGamesUrl.build());
    flowAction.title = title !== null && title !== void 0 ? title : objectGraph.loc.string("Arcade.SeeAllGames.Button.Title");
    if (objectGraph.client.isWeb) {
        const destination = makeRoutableArcadeSeeAllPageIntent({
            ...getLocale(objectGraph),
            ...getPlatform(objectGraph),
        });
        const pageUrl = makeArcadeSeeAllCanonicalUrl(objectGraph, destination);
        flowAction.destination = destination;
        flowAction.pageUrl = pageUrl;
    }
    const itemId = id !== null && id !== void 0 ? id : (objectGraph.client.isVision ? "SeeAllGames" : "arcade-see-all-games-button");
    const seeAllClickOptions = {
        id: itemId,
        idType: idType,
        targetType: targetType,
        actionType: "navigate",
        actionContext: "Arcade",
        pageInformation: metricsPageInformation,
        locationTracker: metricsLocationTracker,
    };
    metricsHelpersClicks.addClickEventToAction(objectGraph, flowAction, seeAllClickOptions);
    return flowAction;
}
/**
 * Create a flow action for opening the Arcade Subscribe page with optional parameters
 * @param context The context in which the Arcade Subscribe page is being opened for, e.g. where the flow action was initiated from.
 * @param contextualAppId Optional app ID to associate with flow, if any. This is used for flow into contextual upsell sheet.
 * @returns {FlowAction} Flow action to `arcadeSubscribe` page.
 */
export function arcadeSubscribePageFlowAction(objectGraph, context, contextualAppId, purchaseSuccessAction, options) {
    var _a, _b, _c, _d;
    const upsellRequestInfo = new models.MarketingItemRequestInfo("arcade", context, objectGraph.bag.metricsTopic, contextualAppId);
    upsellRequestInfo.purchaseSuccessAction = purchaseSuccessAction;
    upsellRequestInfo.carrierLinkSuccessAction = purchaseSuccessAction;
    const action = new models.FlowAction("upsellMarketingItem");
    if (isSome((_b = (_a = options === null || options === void 0 ? void 0 : options.pageInformation) === null || _a === void 0 ? void 0 : _a.searchTermContext) === null || _b === void 0 ? void 0 : _b.term)) {
        upsellRequestInfo.metricsOverlay["searchTerm"] = (_c = options.pageInformation.searchTermContext) === null || _c === void 0 ? void 0 : _c.term;
    }
    const metricsIdentifierFields = (_d = objectGraph.metricsIdentifiersCache) === null || _d === void 0 ? void 0 : _d.getMetricsFieldsForTypes([
        MetricsIdentifierType.user,
        MetricsIdentifierType.client,
    ]);
    if (isSome(metricsIdentifierFields)) {
        upsellRequestInfo.metricsOverlay = {
            ...upsellRequestInfo.metricsOverlay,
            ...metricsIdentifierFields,
        };
    }
    action.pageData = upsellRequestInfo;
    if (serverData.isDefinedNonNull(options)) {
        metricsHelpersClicks.addClickEventToArcadeBuyInitiateAction(objectGraph, action, options);
    }
    return action;
}
/**
 * Action to open the main Arcade page on each platform.
 * Depending on the platform, this can be a tab change action or open action to separate Arcade app.
 */
export function openArcadeMainAction(objectGraph, metricsPageInformation, metricsLocationTracker, popToRoot) {
    if (objectGraph.client.isTV) {
        return openTVArcadeAppAction(objectGraph);
    }
    else {
        const arcadeTabChangeAction = new models.TabChangeAction("arcade");
        if (serverData.isDefinedNonNull(popToRoot)) {
            arcadeTabChangeAction.popToRoot = popToRoot;
        }
        /*
         * Presidio / Yukon timeframe workaround for <rdar://problem/53600942> Allow deserialized TabChangeAction to have use `title` property from JS instead of always using `nil`
         * We're wrapping a single `TabChangeAction` within `CompoundAction` since `TabChangeAction` deserialization drops the JS provided title.
         *
         * Tracking removing this workaround in:
         * <rdar://problem/53601182> Arcade: Remove workaround for having a tab change action with a title
         */
        return new models.CompoundAction([arcadeTabChangeAction]);
    }
}
/**
 * Creates an action to open Arcade app on tvOS.
 */
export function openTVArcadeAppAction(objectGraph) {
    const url = "com.apple.Arcade://";
    return new models.ExternalUrlAction(url);
}
// endregion
/**
 * Creates an action to open GamesUI.
 */
export function openGamesUIAction(objectGraph, target = { playNow: {} }) {
    return new models.OpenGamesUIAction(target);
}
/**
 * Creates Game Center header.
 */
export function makeGameCenterHeader(objectGraph, title = undefined, subtitle = undefined, useTitleArtwork = undefined) {
    let eyebrowArtwork;
    if (objectGraph.client.isTV) {
        eyebrowArtwork = artworkBuilder.createArtworkForResource(objectGraph, "systemimage://gamecenter.fill", 16, 16);
    }
    else {
        eyebrowArtwork = artworkBuilder.createArtworkForResource(objectGraph, "resource://GameCenterEyebrow", 16, 16);
    }
    const isShelfHeaderEnabled = objectGraph.featureFlags.isEnabled("shelf_header");
    const isGameCenterShelfHeaderEnabled = objectGraph.featureFlags.isEnabled("game_center_shelf_header");
    const isGSEUIEnabled = objectGraph.featureFlags.isGSEUIEnabled("de7bbd8e");
    if (isGSEUIEnabled) {
        const configuration = {
            eyebrowColor: named("secondaryText"),
            includeSeparator: !isShelfHeaderEnabled,
            prefersShelfHeader: isGameCenterShelfHeaderEnabled,
        };
        const eyebrow = objectGraph.loc.string("GAME_CENTER");
        const shelfHeader = {
            eyebrow: eyebrow,
            eyebrowArtwork: eyebrowArtwork,
            eyebrowArtworkType: models.ShelfHeaderArtworkType.Icon,
            title: title,
            subtitle: subtitle,
            configuration: configuration,
        };
        return shelfHeader;
    }
    else {
        const configuration = {
            eyebrowColor: isGSEUIEnabled ? named("systemBlue") : undefined,
            includeSeparator: !isShelfHeaderEnabled,
            prefersShelfHeader: isGameCenterShelfHeaderEnabled,
        };
        if (isSome(useTitleArtwork) && useTitleArtwork) {
            const shelfHeader = {
                title: title,
                titleArtwork: eyebrowArtwork,
                titleArtworkType: models.ShelfHeaderArtworkType.Icon,
                subtitle: subtitle,
                configuration: configuration,
            };
            return shelfHeader;
        }
        else {
            const eyebrow = objectGraph.loc.uppercased(objectGraph.loc.string("GAME_CENTER"));
            const shelfHeader = {
                eyebrow: eyebrow,
                eyebrowArtwork: eyebrowArtwork,
                eyebrowArtworkType: models.ShelfHeaderArtworkType.Icon,
                title: title,
                subtitle: subtitle,
                configuration: configuration,
            };
            return shelfHeader;
        }
    }
}
// region Arcade Catalog MAPI Requests
/**
 * Base request for all MAPI requests fetching a set of Arcade games from catalog.
 * This request has a server-defined implicit limit value (e.g. 100).
 * This request should be bare-bones. If additional attributes are needed, it should be chained to this request.
 *
 * @returns {mediaFetching.Request} Request object for fetching Arcade apps from MAPI catalog endpoint.
 */
export function arcadeAppsRequest(objectGraph, includeComingSoon = false) {
    let request = new mediaFetching.Request(objectGraph).forType("arcade-apps").includingAgeRestrictions();
    if (includeComingSoon) {
        request = request.addingQuery("with", "comingSoonApps");
    }
    // For visionOS, we need icons for bincompat games.
    if (objectGraph.client.isVision) {
        request = request.includingAdditionalPlatforms(["iphone", "ipad"]);
        request.attributeIncludes.add("compatibilityControllerRequirement");
    }
    return request;
}
/**
 * Request for fetching a set of arcade apps for displaying a set of Arcade Icons, e.g. for Arcade Grouping Footer, Contextual Upsell Icon Grid, and iOS Arcade Showcase.
 * This request is a very special request with MAPI data containing only `artwork` attribute. Data is meant to be used *as-is* for showing a set of icons only.
 *
 * Requirements:
 * - Icon Artwork for apps only. Additional metadata should be pruned if possible.
 *
 * @param limit Limit of apps. This should be configured to fit the view's needs.
 */
export function arcadeAppsRequestForIcons(objectGraph, limit) {
    return arcadeAppsRequest(objectGraph)
        .withSparseLimit(limit)
        .asPartialResponseLimitedToFields(["artwork"])
        .usingCustomAttributes(productPageVariants.shouldFetchCustomAttributes(objectGraph));
}
// endregion
// region Arcade Upsell Request
export function arcadeUpsellRequest(objectGraph, context, contextualAppId) {
    return arcadeUpsellMarketingItemRequest(objectGraph, context, contextualAppId);
}
export function arcadeUpsellMarketingItemRequest(objectGraph, context, contextualAppId) {
    // We always want `context` to be provided, but some callers will provide this value from an messy source, e.g. extracted from URL param. Fall back to `generic` just in case.
    if (serverData.isNullOrEmpty(context)) {
        context = models.marketingItemContextFromString("generic");
    }
    const request = new mediaFetching.Request(objectGraph)
        .forType("upsellMarketingItem")
        .addingQuery("serviceType", "arcade")
        .addingQuery("placement", context)
        .includingMetaKeys("marketing-items", ["metrics"])
        .includingRelationships(["contents"])
        .includingAttributes(["marketingArtwork", "marketingVideo"])
        .includingAgeRestrictions();
    // Append app id that is promoted, if any.
    if (serverData.isDefinedNonNull(contextualAppId)) {
        request.addingQuery("seed", contextualAppId);
    }
    return request;
}
// endregion
/**
 * Grab recently played games.
 * @param objectGraph The object graph.
 * @param {number} limit The number of recently played games to return.
 * @param {boolean} shouldHydrateAppsData Whether the apps data should be fetched from media api.
 * @param {number} timeout A timeout in seconds.
 * @returns {DataContainer} The media api data container with the recently played games.
 */
export async function getRecentlyPlayedGames(objectGraph, limit = null, shouldHydrateAppsData = false, timeout = null) {
    return await new Promise((resolve, reject) => {
        const isRecentlyPlayedGamesSupported = objectGraph.host.isiOS || objectGraph.host.isMac || objectGraph.host.isTV;
        // Check if the client supports recently played games.
        if (!isRecentlyPlayedGamesSupported) {
            resolve(null);
            return;
        }
        const getRecentlyPlayGamesPromise = objectGraph.arcade.getRecentlyPlayedGamesWithTimeout(timeout);
        // Get recently played games
        getRecentlyPlayGamesPromise
            .then((adamIds) => {
            // Only perform request when there are recently played games.
            if (serverData.isNull(adamIds) || adamIds.length === 0) {
                resolve(null);
                return;
            }
            // Enforce limit (if any).
            if (serverData.isNumber(limit) && adamIds.length > limit) {
                adamIds = adamIds.slice(0, limit);
            }
            if (shouldHydrateAppsData) {
                // Fetch data for recently played games.
                const attributes = [
                    "editorialArtwork",
                    "editorialVideo",
                    "description",
                    "minimumOSVersion",
                    "minPlayers",
                    "maxPlayers",
                    "remoteControllerRequirement",
                    "requiresGameController",
                    "supportsSharePlay",
                ];
                if (objectGraph.client.isVision) {
                    attributes.push("compatibilityControllerRequirement");
                }
                if (objectGraph.client.isMac) {
                    attributes.push("hasMacIPAPackage");
                }
                if (objectGraph.bag.enableUpdatedAgeRatings) {
                    attributes.push("ageRating");
                }
                if (shouldUsePrerenderedIconArtwork(objectGraph)) {
                    attributes.push("iconArtwork");
                }
                const mediaApiRequest = new mediaFetching.Request(objectGraph)
                    .withIdsOfType(adamIds, "apps")
                    .includingAttributes(attributes);
                const fetchDataPromise = mediaNetwork.fetchData(objectGraph, mediaApiRequest);
                fetchDataPromise.then((dataContainer) => resolve(dataContainer), (reason) => {
                    objectGraph.console.log(`getRecentlyPlayedGames() failed when calling mediaNetwork.fetchData() with reason: ${reason}`);
                    resolve(null);
                });
            }
            else {
                // Create an incomplete data container to be fetched later.
                const dataContainer = {
                    data: [],
                };
                adamIds.forEach((adamId) => {
                    dataContainer.data.push({
                        id: adamId,
                        type: "apps",
                    });
                });
                resolve(dataContainer);
            }
        })
            .catch((reason) => {
            objectGraph.console.log(`getRecentlyPlayedGames() failed with: ${reason}`);
            resolve(null);
        });
    });
}
/**
 * Convenience function to build a `ArcadeUpsellData` representation from upsell relationship joined to some data.
 * @seealso `upsellFromContentsOfUpsellResponse`
 * @param objectGraph The object graph
 * @param data Data containing `upsell` relationship to build `ArcadeUpsellData` with
 */
export function upsellFromRelationshipOf(objectGraph, data) {
    // Data to extract.
    let marketingItemData = null;
    const upsellDataContainer = objectGraph.client.isVision || preprocessor.GAMES_TARGET
        ? mediaRelationships.relationship(data, "contents")
        : mediaRelationships.relationship(data, "upsell") ||
            mediaRelationships.relationship(data, "marketing-items");
    if (serverData.isNullOrEmpty(upsellDataContainer) || serverData.isNullOrEmpty(upsellDataContainer.data)) {
        return null;
    }
    // Create marketing items array from data container.
    const marketingItems = upsellDataContainer.data
        .map((item) => {
        if (item.type === "marketing-items") {
            return item;
        }
        else {
            return null;
        }
    })
        .filter((item) => isDefinedNonNull(item));
    // Return null if there are NOT any marketing items.
    if (serverData.isNullOrEmpty(marketingItems)) {
        return null;
    }
    const timeout = objectGraph.bag.marketingItemSelectionTimeout;
    // If there is only one marketing item or timeout is zero, set this as the data.
    // Otherwise get a single marketing item by calling AMS.
    if (marketingItems.length === 1 || timeout === 0) {
        marketingItemData = marketingItems[0];
    }
    else {
        try {
            marketingItemData = objectGraph.arcade.getMarketingItemWithTimeout(marketingItems, timeout);
        }
        catch {
            // Default to first item if the call was timed out.
            marketingItemData = marketingItems[0];
        }
    }
    // Return null if marketing item is null.
    if (serverData.isNull(marketingItemData)) {
        return null;
    }
    return {
        marketingItemData: marketingItemData,
    };
}
/**
 * Convenience function to build a `ArcadeUpsellData` representation from contents of a engagement/upsell response.
 * This doesn't really belong in
 * @seealso `upsellFromRelationshipOf`
 * @param arcadeUpsellResponse Response from the engagement/upsell endpoint.
 */
export function upsellFromContentsOfUpsellResponse(objectGraph, arcadeUpsellResponse) {
    if (!arcadeUpsellResponse) {
        return null;
    }
    let marketingItemData = null;
    const responseDataArray = serverData.asArrayOrEmpty(arcadeUpsellResponse, "results.data");
    if (responseDataArray.length > 0) {
        marketingItemData = responseDataArray[0];
    }
    /**
     * `arcadeUpsellResponse` is expected of form:
     * {
     *      content: { mediaDataStructure.Data }
     * }
     * matching what is provided via the appropriate `action=<usage>` param from engagement/upsell endpoint.
     */
    if (!serverData.isDefinedNonNull(marketingItemData)) {
        return null;
    }
    return {
        marketingItemData: marketingItemData,
    };
}
// endregion
// region Arcade Games For You Request
/**
 * Request for fetching list of recommended Arcade apps from recommendations API.
 * @param objectGraph The App Store object graph.
 * @param limit Maximum number of apps to fetch.
 * @returns {Request} Request object for fetching recommended Arcade apps from
 * personalized recommendations endpoint.
 */
export function arcadeGamesForYouRequest(objectGraph, limit) {
    let request = new mediaFetching.Request(objectGraph)
        .forType("personal-recommendations")
        .addingQuery("sparseLimit[contents]", `${limit}`)
        .addingQuery("include[personal-recommendations]", "contents")
        .addingQuery("filter[kind]", "arcadeGamesForYou")
        .includingAgeRestrictions();
    // For visionOS, we need to include bincompat apps.
    if (objectGraph.client.isVision) {
        request = request.includingAdditionalPlatforms(["iphone", "ipad"]);
    }
    return request;
}
// endregion
// The color to use for Arcade content.
export const arcadeColor = Color.fromRGB(1, 90 / 255, 80 / 255);
//# sourceMappingURL=arcade-common.js.map