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