this repo has no description
1//
2// sharing.ts
3// AppStoreKit
4//
5// Created by Sam Vafaee on 6/16/17.
6// Copyright (c) 2016 Apple Inc. All rights reserved.
7//
8import { isNothing, isSome } from "@jet/environment";
9import * as validation from "@jet/environment/json/validation";
10import * as models from "../api/models";
11import * as serverData from "../foundation/json-parsing/server-data";
12import * as mediaAttributes from "../foundation/media/attributes";
13import { Parameters, ShareURLParameters } from "../foundation/network/url-constants";
14import * as urls from "../foundation/network/urls";
15import * as dateUtil from "../foundation/util/date-util";
16import * as client from "../foundation/wrappers/client";
17import * as contentArtwork from "./content/artwork/artwork";
18import * as contentAttributes from "./content/attributes";
19import * as content from "./content/content";
20import * as metricsHelpersClicks from "./metrics/helpers/clicks";
21import * as offers from "./offers/offers";
22import { productVariantDataForData, productVariantIDForVariantData } from "./product-page/product-page-variants";
23import { cardDisplayStyleFromData, editorialArtKeyPathForCardDisplayStyle, todayCardArtworkFromArtworkData, todayCardHeroArtForData, } from "./today/today-card-util";
24import { HeroMediaDisplayContext } from "./today/today-types";
25import { dataHasDeviceFamily, dataOnlyHasDeviceFamily } from "./content/device-family";
26// region Data helpers
27export function adamIdForShareSheetGiftActivityFromProductData(objectGraph, data) {
28 if (!objectGraph.bag.isContentGiftingEnabled) {
29 return null;
30 }
31 if (serverData.isNull(data)) {
32 return null;
33 }
34 const offer = offers.offerDataFromData(objectGraph, data);
35 if (serverData.isNull(offer)) {
36 return null;
37 }
38 // Disable gifting for pre-orders
39 const isPreorder = mediaAttributes.attributeAsBoolean(data, "isPreorder");
40 if (isPreorder) {
41 return null;
42 }
43 const price = serverData.asNumber(offer, "price");
44 if (price > 0) {
45 return data.id;
46 }
47 return null;
48}
49/**
50 * Return the url for sharing for given product
51 * @param objectGraph Dependency soup
52 * @param data Apps Resource data for product
53 */
54function shareUrlForProductData(objectGraph, data, urlKey) {
55 const rawUrl = mediaAttributes.attributeAsString(data, urlKey);
56 if (serverData.isNullOrEmpty(rawUrl)) {
57 return null;
58 }
59 const url = new urls.URL(rawUrl);
60 // Add client specifier to url so apps are shown in the same client they were shared from
61 let clientSpecifier = null;
62 const clientIdentifier = objectGraph.host.clientIdentifier;
63 switch (clientIdentifier) {
64 case client.messagesIdentifier:
65 clientSpecifier = "messages";
66 break;
67 case client.watchIdentifier:
68 clientSpecifier = "watch";
69 break;
70 default:
71 break;
72 }
73 if (clientSpecifier) {
74 url.param(ShareURLParameters.clientSpecifier, clientSpecifier);
75 }
76 // Custom product page variant id must be added to `url` for MAPI resource caching constraints.
77 const productVariantData = productVariantDataForData(objectGraph, data);
78 const productVariantID = productVariantIDForVariantData(productVariantData);
79 if (serverData.isDefinedNonNull(productVariantID)) {
80 url.param(Parameters.productVariantID, productVariantID);
81 }
82 return url.toString();
83}
84function notesMetadataFromProductData(objectGraph, data) {
85 return validation.context("notesMetadataFromProductData", () => {
86 var _a;
87 if (serverData.isNull(data)) {
88 return null;
89 }
90 const itemName = mediaAttributes.attributeAsString(data, "name");
91 // Require name
92 if (isNothing(itemName) || itemName.length === 0) {
93 return null;
94 }
95 const url = shareUrlForProductData(objectGraph, data, "url");
96 const developer = mediaAttributes.attributeAsString(data, "artistName");
97 const category = mediaAttributes.attributeAsString(data, "genreNames.0");
98 const fileSize = (_a = content.combinedFileSizeFromData(objectGraph, data)) === null || _a === void 0 ? void 0 : _a.fileSizeByDevice;
99 let mediaType;
100 switch (data.type) {
101 case "apps": {
102 mediaType = "app";
103 break;
104 }
105 case "app-bundles": {
106 mediaType = "bundle";
107 break;
108 }
109 case "in-apps": {
110 mediaType = "iap";
111 break;
112 }
113 default: {
114 mediaType = null;
115 }
116 }
117 return new models.ShareSheetNotesMetadata(itemName, url, developer, category, fileSize, mediaType);
118 });
119}
120function copyLinkShareSheetActivityForURL(objectGraph, url, title) {
121 if (serverData.isNullOrEmpty(url)) {
122 return null;
123 }
124 const copyTextAction = new models.CopyTextAction(url);
125 copyTextAction.title = title !== null && title !== void 0 ? title : objectGraph.loc.string("ShareSheet.CopyLink.Title");
126 copyTextAction.artwork = contentArtwork.createArtworkForResource(objectGraph, "systemimage://link");
127 const copyLinkActionActivity = new models.ShareSheetActivity("com.apple.AppStore.copyLinkActivity", copyTextAction);
128 return copyLinkActionActivity;
129}
130function openInGameCenterShareSheetActivityForURL(url, title) {
131 if (serverData.isNullOrEmpty(url)) {
132 return undefined;
133 }
134 const openURLAction = new models.ExternalUrlAction(url);
135 openURLAction.title = title;
136 const activity = new models.ShareSheetActivity("com.apple.AppStore.openInGameCenterActivity", openURLAction);
137 return activity;
138}
139// endregion
140// region Article
141export function shareSheetDataForArticle(objectGraph, text, url, shortUrl, articleArtwork, data) {
142 // Sharing is currently not supported on watchOS.
143 if (serverData.isNull(data) || objectGraph.client.isWatch) {
144 return null;
145 }
146 return validation.context("shareSheetDataForArticle", () => {
147 let artwork = articleArtwork;
148 if (isNothing(artwork) && isSome(data)) {
149 artwork = shareSheetArtForData(objectGraph, data);
150 }
151 const subtitle = objectGraph.loc.string("ShareSheet.Story.Subtitle");
152 const articleMetadata = new models.ShareSheetArticleMetadata(data.id, text, subtitle, artwork);
153 return new models.ShareSheetData(articleMetadata, url, shortUrl);
154 });
155}
156/**
157 * Determines the artwork to use in share sheets.
158 * @param {Data} data The data from which to retrieve the artwork.
159 * @returns {Artwork} The artwork suitable for display in the share sheet.
160 */
161export function shareSheetArtForData(objectGraph, data) {
162 const cardDisplayStyle = cardDisplayStyleFromData(data);
163 if (!objectGraph.client.isVision && !preprocessor.GAMES_TARGET) {
164 const heroArt = todayCardHeroArtForData(objectGraph, data, HeroMediaDisplayContext.Article, cardDisplayStyle);
165 if (serverData.isDefinedNonNull(heroArt)) {
166 return heroArt;
167 }
168 }
169 let cropCode;
170 if (objectGraph.client.isVision) {
171 cropCode = "SCS.ApDPCS01";
172 }
173 const artKeyPath = editorialArtKeyPathForCardDisplayStyle(objectGraph, cardDisplayStyle);
174 return todayCardArtworkFromArtworkData(objectGraph, mediaAttributes.attributeAsDictionary(data, artKeyPath), cropCode);
175}
176export function shareSheetActivitiesForArticle(objectGraph, shareUrl, todayCardPreviewUrl, storyID) {
177 const shareSheetActivities = [];
178 if ((shareUrl === null || shareUrl === void 0 ? void 0 : shareUrl.length) > 0) {
179 const copyLinkActivity = copyLinkShareSheetActivityForURL(objectGraph, shareUrl);
180 if (serverData.isDefinedNonNull(copyLinkActivity)) {
181 shareSheetActivities.push(copyLinkActivity);
182 }
183 }
184 if ((todayCardPreviewUrl === null || todayCardPreviewUrl === void 0 ? void 0 : todayCardPreviewUrl.length) > 0) {
185 const copyCardPreviewLinkActivity = copyLinkShareSheetActivityForURL(objectGraph, todayCardPreviewUrl, "Copy Card Preview Link");
186 if (serverData.isDefinedNonNull(copyCardPreviewLinkActivity)) {
187 shareSheetActivities.push(copyCardPreviewLinkActivity);
188 }
189 }
190 if (isSome(storyID)) {
191 let activity;
192 if (objectGraph.featureFlags.isGSEUIEnabled("de7bbd8e") &&
193 objectGraph.featureFlags.isEnabled("open_in_story_share_sheet")) {
194 activity = openInGameCenterShareSheetActivityForURL(`games:///story/id${storyID}`, objectGraph.loc.string("ShareSheet.OpenInGameCenter.Title"));
195 }
196 if (serverData.isDefinedNonNull(activity)) {
197 shareSheetActivities.push(activity);
198 }
199 }
200 return shareSheetActivities;
201}
202// endregion
203// region App Events
204export function shareSheetDataForAppEvent(objectGraph, text, subtitle, url, shortUrl, appEventArtwork) {
205 return validation.context("shareSheetDataForAppEvent", () => {
206 const artwork = appEventArtwork;
207 const appEventMetadata = new models.ShareSheetAppEventMetadata(text, subtitle, artwork);
208 return new models.ShareSheetData(appEventMetadata, url, shortUrl);
209 });
210}
211export function shareSheetActivitiesForAppEvent(objectGraph, appEvent, shareUrl) {
212 var _a;
213 const shareSheetActivities = [];
214 // Copy link action
215 if ((shareUrl === null || shareUrl === void 0 ? void 0 : shareUrl.length) > 0) {
216 const copyLinkActivity = copyLinkShareSheetActivityForURL(objectGraph, shareUrl);
217 if (serverData.isDefinedNonNull) {
218 shareSheetActivities.push(copyLinkActivity);
219 }
220 }
221 // If the event has already started, remove the option to create a calendar event
222 if (appEvent.startDate.getTime() <= Date.now()) {
223 return shareSheetActivities;
224 }
225 // Creating events is not supported in the product page extension
226 if (objectGraph.host.clientIdentifier === client.productPageExtensionIdentifier) {
227 return shareSheetActivities;
228 }
229 // Not authorized action
230 const notAuthorizedAction = new models.AlertAction("default");
231 notAuthorizedAction.title = objectGraph.loc.string("APP_EVENTS_CALENDAR_NOT_AUTHORIZED_TITLE");
232 notAuthorizedAction.message = objectGraph.loc.string("APP_EVENTS_CALENDAR_NOT_AUTHORIZED_DETAIL");
233 notAuthorizedAction.isCancelable = true;
234 notAuthorizedAction.buttonTitles = [objectGraph.loc.string("ACTION_SETTINGS")];
235 // NOTE: This URL only works on iOS. If this feature is expanded beyond iOS, this code will need to be split per-platform.
236 notAuthorizedAction.buttonActions = [new models.ExternalUrlAction("prefs:root=Privacy&path=CALENDARS", true)];
237 let isAllDay = false;
238 if (serverData.isDefinedNonNull(appEvent.endDate)) {
239 // If the start and end date are > 6 hours apart, and spans over multiple days, then mark as all-day
240 const startMidnight = dateUtil.convertLocalDateToLocalMidnight(appEvent.startDate);
241 const endMidnight = dateUtil.convertLocalDateToLocalMidnight(appEvent.endDate);
242 const difference = appEvent.endDate.getTime() - appEvent.startDate.getTime();
243 const sixHoursDifference = 1000 * 60 * 60 * 6;
244 if (endMidnight.getTime() > startMidnight.getTime() && difference > sixHoursDifference) {
245 isAllDay = true;
246 }
247 // If the start and end date are on the same day, and the event runs for 23 hours & 59 mins
248 // then mark as all-day. This effectively ignores the seconds portion as calendar ignores
249 // this anyway.
250 const fullDayDifference = 1000 * 60 * 60 * 23 + 1000 * 60 * 59;
251 if (startMidnight.getTime() === endMidnight.getTime() && difference >= fullDayDifference) {
252 isAllDay = true;
253 }
254 }
255 // Create calendar event activity
256 const createCalendarEventAction = new models.CreateCalendarEventAction(appEvent.startDate, appEvent.endDate, isAllDay, appEvent.title, (_a = appEvent.lockup) === null || _a === void 0 ? void 0 : _a.title, appEvent.detail, shareUrl, notAuthorizedAction, "free");
257 createCalendarEventAction.title = objectGraph.loc.string("SHARE_SHEET_ADD_TO_CALENDAR");
258 createCalendarEventAction.artwork = contentArtwork.createArtworkForResource(objectGraph, "systemimage://calendar.circle");
259 const createCalendarEventActivity = new models.ShareSheetActivity("com.apple.AppStore.createCalendarEventActivity", createCalendarEventAction);
260 // Create Calendar Event should be at the beginning.
261 shareSheetActivities.unshift(createCalendarEventActivity);
262 return shareSheetActivities;
263}
264// endregion
265// region Products
266export function shareSheetDataForProductFromProductData(objectGraph, data, clientIdentifierOverride) {
267 return validation.context("shareSheetDataForProductFromProductData", () => {
268 // Sharing is currently not supported on watchOS or the web client.
269 if (serverData.isNull(data) || objectGraph.client.isWatch || objectGraph.client.isWeb) {
270 return null;
271 }
272 // required attributes
273 const url = shareUrlForProductData(objectGraph, data, "url");
274 const title = mediaAttributes.attributeAsString(data, "name");
275 const developerName = mediaAttributes.attributeAsString(data, "artistName");
276 const adamId = data.id;
277 const storeFrontIdentifier = objectGraph.client.storefrontIdentifier;
278 // Sanity check
279 if (!url || !title || !developerName || !adamId) {
280 return null;
281 }
282 // optional attributes
283 const shortUrl = shareUrlForProductData(objectGraph, data, "shortUrl");
284 let artwork = null;
285 let notesMetadata = null;
286 const screenshots = content.screenshotsFromData(objectGraph, data, 4 /* content.ArtworkUseCase.LockupScreenshots */);
287 const videos = content.videoPreviewsFromData(objectGraph, data);
288 const subtitle = contentAttributes.contentAttributeAsString(objectGraph, data, "subtitle") || developerName;
289 const genreName = null;
290 const isMessagesOnlyApp = false;
291 const messagesAppIcon = null;
292 // Platform
293 let platform;
294 const isMacOnlyApp = dataOnlyHasDeviceFamily(objectGraph, data, "mac");
295 const isMacApp = dataHasDeviceFamily(objectGraph, data, "mac");
296 if (isMacOnlyApp || (objectGraph.client.isMac && isMacApp)) {
297 platform = "Mac";
298 }
299 else {
300 platform = "iOS";
301 }
302 // Add product info
303 if (serverData.isDefinedNonNull(data) && mediaAttributes.attributeAsString(data, "url")) {
304 artwork = content.iconFromData(objectGraph, data, {
305 useCase: 1 /* content.ArtworkUseCase.LockupIconSmall */,
306 }, clientIdentifierOverride);
307 notesMetadata = notesMetadataFromProductData(objectGraph, data);
308 }
309 const productMetadata = new models.ShareSheetProductMetadata(adamId, storeFrontIdentifier, title, platform, artwork, screenshots, videos, isMessagesOnlyApp, subtitle, genreName, messagesAppIcon, notesMetadata);
310 return new models.ShareSheetData(productMetadata, url, shortUrl);
311 });
312}
313export function shareProductActionFromData(objectGraph, data, metricsPageInformation, metricsLocationTracker, clientIdentifierOverride) {
314 return validation.context(`shareActionFromData: ${data.type}`, () => {
315 var _a;
316 const id = data.id;
317 switch (objectGraph.client.deviceType) {
318 case "mac": {
319 const shareSheetData = shareSheetDataForProductFromProductData(objectGraph, data);
320 // Share action
321 if (shareSheetData) {
322 const shareAction = new models.ShareSheetAction(shareSheetData, []);
323 metricsHelpersClicks.addClickEventToAction(objectGraph, shareAction, {
324 targetType: "button",
325 id: id,
326 actionType: "share",
327 pageInformation: metricsPageInformation,
328 locationTracker: metricsLocationTracker,
329 });
330 return shareAction;
331 }
332 break;
333 }
334 case "phone":
335 case "pad":
336 case "vision": {
337 const shareSheetData = shareSheetDataForProductFromProductData(objectGraph, data, clientIdentifierOverride);
338 const shareSheetActivities = [];
339 // Copy link action
340 if (((_a = shareSheetData === null || shareSheetData === void 0 ? void 0 : shareSheetData.url) === null || _a === void 0 ? void 0 : _a.length) > 0) {
341 const copyLinkActivity = copyLinkShareSheetActivityForURL(objectGraph, shareSheetData.url);
342 if (serverData.isDefinedNonNull) {
343 shareSheetActivities.push(copyLinkActivity);
344 }
345 }
346 // Gift action
347 const giftAdamId = adamIdForShareSheetGiftActivityFromProductData(objectGraph, data);
348 if (giftAdamId) {
349 const giftAction = new models.FlowAction("finance");
350 giftAction.presentationContext = "presentModal";
351 giftAction.title = objectGraph.loc.string("SHARE_GIFT_APP");
352 giftAction.artwork = contentArtwork.createArtworkForResource(objectGraph, "systemimage://app.gift");
353 giftAction.pageUrl = `gift/${giftAdamId}`;
354 metricsHelpersClicks.addClickEventToAction(objectGraph, giftAction, {
355 targetType: "button",
356 id: id,
357 actionType: "gift",
358 actionContext: "shareSheet",
359 pageInformation: metricsPageInformation,
360 locationTracker: metricsLocationTracker,
361 });
362 const giftActionActivity = new models.ShareSheetActivity("com.apple.AppStore.giftActivity", giftAction);
363 shareSheetActivities.push(giftActionActivity);
364 }
365 if (shareSheetData) {
366 // Share action
367 const shareSheetStyle = "expanded";
368 const shareAction = new models.ShareSheetAction(shareSheetData, shareSheetActivities, shareSheetStyle);
369 shareAction.title = objectGraph.loc.string("SHARE_APP");
370 shareAction.artwork = contentArtwork.createArtworkForResource(objectGraph, "systemimage://square.and.arrow.up");
371 metricsHelpersClicks.addClickEventToAction(objectGraph, shareAction, {
372 targetType: "button",
373 id: id,
374 actionType: "share",
375 actionContext: "shareSheet",
376 pageInformation: metricsPageInformation,
377 locationTracker: metricsLocationTracker,
378 });
379 return shareAction;
380 }
381 else if (shareSheetActivities.length > 0) {
382 // Map ActionActivity[] to Action[] as we're not dealing with a share sheet
383 const sheetActions = shareSheetActivities.map((activity) => activity.action);
384 // Create action sheet instead of share sheet
385 const sheetAction = new models.SheetAction(sheetActions);
386 sheetAction.isCancelable = true;
387 sheetAction.isCustom = true;
388 metricsHelpersClicks.addClickEventToAction(objectGraph, sheetAction, {
389 targetType: "button",
390 id: id,
391 actionType: "actionSheet",
392 pageInformation: metricsPageInformation,
393 locationTracker: metricsLocationTracker,
394 });
395 return sheetAction;
396 }
397 break;
398 }
399 default: {
400 // These device types do not support sharing, so we don't create any share actions.
401 break;
402 }
403 }
404 return null;
405 });
406}
407// region Generic Pages
408export function shareSheetDataForGenericPage(objectGraph, text, url, subtitle, shortUrl, artwork) {
409 return validation.context("shareSheetDataForGenericPage", () => {
410 if (serverData.isNullOrEmpty(url)) {
411 return null;
412 }
413 const metadata = new models.ShareSheetGenericMetadata(text, subtitle, artwork !== null && artwork !== void 0 ? artwork : undefined);
414 return new models.ShareSheetData(metadata, url, shortUrl);
415 });
416}
417export function shareSheetActivitiesForGenericPage(objectGraph, shareUrl) {
418 const shareSheetActivities = [];
419 if ((shareUrl === null || shareUrl === void 0 ? void 0 : shareUrl.length) > 0) {
420 const copyLinkActivity = copyLinkShareSheetActivityForURL(objectGraph, shareUrl);
421 if (serverData.isDefinedNonNull) {
422 shareSheetActivities.push(copyLinkActivity);
423 }
424 }
425 return shareSheetActivities;
426}
427// endregion
428//# sourceMappingURL=sharing.js.map