// // sharing.ts // AppStoreKit // // Created by Sam Vafaee on 6/16/17. // Copyright (c) 2016 Apple Inc. All rights reserved. // import { isNothing, isSome } from "@jet/environment"; import * as validation from "@jet/environment/json/validation"; import * as models from "../api/models"; import * as serverData from "../foundation/json-parsing/server-data"; import * as mediaAttributes from "../foundation/media/attributes"; import { Parameters, ShareURLParameters } from "../foundation/network/url-constants"; import * as urls from "../foundation/network/urls"; import * as dateUtil from "../foundation/util/date-util"; import * as client from "../foundation/wrappers/client"; import * as contentArtwork from "./content/artwork/artwork"; import * as contentAttributes from "./content/attributes"; import * as content from "./content/content"; import * as metricsHelpersClicks from "./metrics/helpers/clicks"; import * as offers from "./offers/offers"; import { productVariantDataForData, productVariantIDForVariantData } from "./product-page/product-page-variants"; import { cardDisplayStyleFromData, editorialArtKeyPathForCardDisplayStyle, todayCardArtworkFromArtworkData, todayCardHeroArtForData, } from "./today/today-card-util"; import { HeroMediaDisplayContext } from "./today/today-types"; import { dataHasDeviceFamily, dataOnlyHasDeviceFamily } from "./content/device-family"; // region Data helpers export function adamIdForShareSheetGiftActivityFromProductData(objectGraph, data) { if (!objectGraph.bag.isContentGiftingEnabled) { return null; } if (serverData.isNull(data)) { return null; } const offer = offers.offerDataFromData(objectGraph, data); if (serverData.isNull(offer)) { return null; } // Disable gifting for pre-orders const isPreorder = mediaAttributes.attributeAsBoolean(data, "isPreorder"); if (isPreorder) { return null; } const price = serverData.asNumber(offer, "price"); if (price > 0) { return data.id; } return null; } /** * Return the url for sharing for given product * @param objectGraph Dependency soup * @param data Apps Resource data for product */ function shareUrlForProductData(objectGraph, data, urlKey) { const rawUrl = mediaAttributes.attributeAsString(data, urlKey); if (serverData.isNullOrEmpty(rawUrl)) { return null; } const url = new urls.URL(rawUrl); // Add client specifier to url so apps are shown in the same client they were shared from let clientSpecifier = null; const clientIdentifier = objectGraph.host.clientIdentifier; switch (clientIdentifier) { case client.messagesIdentifier: clientSpecifier = "messages"; break; case client.watchIdentifier: clientSpecifier = "watch"; break; default: break; } if (clientSpecifier) { url.param(ShareURLParameters.clientSpecifier, clientSpecifier); } // Custom product page variant id must be added to `url` for MAPI resource caching constraints. const productVariantData = productVariantDataForData(objectGraph, data); const productVariantID = productVariantIDForVariantData(productVariantData); if (serverData.isDefinedNonNull(productVariantID)) { url.param(Parameters.productVariantID, productVariantID); } return url.toString(); } function notesMetadataFromProductData(objectGraph, data) { return validation.context("notesMetadataFromProductData", () => { var _a; if (serverData.isNull(data)) { return null; } const itemName = mediaAttributes.attributeAsString(data, "name"); // Require name if (isNothing(itemName) || itemName.length === 0) { return null; } const url = shareUrlForProductData(objectGraph, data, "url"); const developer = mediaAttributes.attributeAsString(data, "artistName"); const category = mediaAttributes.attributeAsString(data, "genreNames.0"); const fileSize = (_a = content.combinedFileSizeFromData(objectGraph, data)) === null || _a === void 0 ? void 0 : _a.fileSizeByDevice; let mediaType; switch (data.type) { case "apps": { mediaType = "app"; break; } case "app-bundles": { mediaType = "bundle"; break; } case "in-apps": { mediaType = "iap"; break; } default: { mediaType = null; } } return new models.ShareSheetNotesMetadata(itemName, url, developer, category, fileSize, mediaType); }); } function copyLinkShareSheetActivityForURL(objectGraph, url, title) { if (serverData.isNullOrEmpty(url)) { return null; } const copyTextAction = new models.CopyTextAction(url); copyTextAction.title = title !== null && title !== void 0 ? title : objectGraph.loc.string("ShareSheet.CopyLink.Title"); copyTextAction.artwork = contentArtwork.createArtworkForResource(objectGraph, "systemimage://link"); const copyLinkActionActivity = new models.ShareSheetActivity("com.apple.AppStore.copyLinkActivity", copyTextAction); return copyLinkActionActivity; } function openInGameCenterShareSheetActivityForURL(url, title) { if (serverData.isNullOrEmpty(url)) { return undefined; } const openURLAction = new models.ExternalUrlAction(url); openURLAction.title = title; const activity = new models.ShareSheetActivity("com.apple.AppStore.openInGameCenterActivity", openURLAction); return activity; } // endregion // region Article export function shareSheetDataForArticle(objectGraph, text, url, shortUrl, articleArtwork, data) { // Sharing is currently not supported on watchOS. if (serverData.isNull(data) || objectGraph.client.isWatch) { return null; } return validation.context("shareSheetDataForArticle", () => { let artwork = articleArtwork; if (isNothing(artwork) && isSome(data)) { artwork = shareSheetArtForData(objectGraph, data); } const subtitle = objectGraph.loc.string("ShareSheet.Story.Subtitle"); const articleMetadata = new models.ShareSheetArticleMetadata(data.id, text, subtitle, artwork); return new models.ShareSheetData(articleMetadata, url, shortUrl); }); } /** * Determines the artwork to use in share sheets. * @param {Data} data The data from which to retrieve the artwork. * @returns {Artwork} The artwork suitable for display in the share sheet. */ export function shareSheetArtForData(objectGraph, data) { const cardDisplayStyle = cardDisplayStyleFromData(data); if (!objectGraph.client.isVision && !preprocessor.GAMES_TARGET) { const heroArt = todayCardHeroArtForData(objectGraph, data, HeroMediaDisplayContext.Article, cardDisplayStyle); if (serverData.isDefinedNonNull(heroArt)) { return heroArt; } } let cropCode; if (objectGraph.client.isVision) { cropCode = "SCS.ApDPCS01"; } const artKeyPath = editorialArtKeyPathForCardDisplayStyle(objectGraph, cardDisplayStyle); return todayCardArtworkFromArtworkData(objectGraph, mediaAttributes.attributeAsDictionary(data, artKeyPath), cropCode); } export function shareSheetActivitiesForArticle(objectGraph, shareUrl, todayCardPreviewUrl, storyID) { const shareSheetActivities = []; if ((shareUrl === null || shareUrl === void 0 ? void 0 : shareUrl.length) > 0) { const copyLinkActivity = copyLinkShareSheetActivityForURL(objectGraph, shareUrl); if (serverData.isDefinedNonNull(copyLinkActivity)) { shareSheetActivities.push(copyLinkActivity); } } if ((todayCardPreviewUrl === null || todayCardPreviewUrl === void 0 ? void 0 : todayCardPreviewUrl.length) > 0) { const copyCardPreviewLinkActivity = copyLinkShareSheetActivityForURL(objectGraph, todayCardPreviewUrl, "Copy Card Preview Link"); if (serverData.isDefinedNonNull(copyCardPreviewLinkActivity)) { shareSheetActivities.push(copyCardPreviewLinkActivity); } } if (isSome(storyID)) { let activity; if (objectGraph.featureFlags.isGSEUIEnabled("de7bbd8e") && objectGraph.featureFlags.isEnabled("open_in_story_share_sheet")) { activity = openInGameCenterShareSheetActivityForURL(`games:///story/id${storyID}`, objectGraph.loc.string("ShareSheet.OpenInGameCenter.Title")); } if (serverData.isDefinedNonNull(activity)) { shareSheetActivities.push(activity); } } return shareSheetActivities; } // endregion // region App Events export function shareSheetDataForAppEvent(objectGraph, text, subtitle, url, shortUrl, appEventArtwork) { return validation.context("shareSheetDataForAppEvent", () => { const artwork = appEventArtwork; const appEventMetadata = new models.ShareSheetAppEventMetadata(text, subtitle, artwork); return new models.ShareSheetData(appEventMetadata, url, shortUrl); }); } export function shareSheetActivitiesForAppEvent(objectGraph, appEvent, shareUrl) { var _a; const shareSheetActivities = []; // Copy link action if ((shareUrl === null || shareUrl === void 0 ? void 0 : shareUrl.length) > 0) { const copyLinkActivity = copyLinkShareSheetActivityForURL(objectGraph, shareUrl); if (serverData.isDefinedNonNull) { shareSheetActivities.push(copyLinkActivity); } } // If the event has already started, remove the option to create a calendar event if (appEvent.startDate.getTime() <= Date.now()) { return shareSheetActivities; } // Creating events is not supported in the product page extension if (objectGraph.host.clientIdentifier === client.productPageExtensionIdentifier) { return shareSheetActivities; } // Not authorized action const notAuthorizedAction = new models.AlertAction("default"); notAuthorizedAction.title = objectGraph.loc.string("APP_EVENTS_CALENDAR_NOT_AUTHORIZED_TITLE"); notAuthorizedAction.message = objectGraph.loc.string("APP_EVENTS_CALENDAR_NOT_AUTHORIZED_DETAIL"); notAuthorizedAction.isCancelable = true; notAuthorizedAction.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. notAuthorizedAction.buttonActions = [new models.ExternalUrlAction("prefs:root=Privacy&path=CALENDARS", true)]; let isAllDay = false; if (serverData.isDefinedNonNull(appEvent.endDate)) { // If the start and end date are > 6 hours apart, and spans over multiple days, then mark as all-day const startMidnight = dateUtil.convertLocalDateToLocalMidnight(appEvent.startDate); const endMidnight = dateUtil.convertLocalDateToLocalMidnight(appEvent.endDate); const difference = appEvent.endDate.getTime() - appEvent.startDate.getTime(); const sixHoursDifference = 1000 * 60 * 60 * 6; if (endMidnight.getTime() > startMidnight.getTime() && difference > sixHoursDifference) { isAllDay = true; } // If the start and end date are on the same day, and the event runs for 23 hours & 59 mins // then mark as all-day. This effectively ignores the seconds portion as calendar ignores // this anyway. const fullDayDifference = 1000 * 60 * 60 * 23 + 1000 * 60 * 59; if (startMidnight.getTime() === endMidnight.getTime() && difference >= fullDayDifference) { isAllDay = true; } } // Create calendar event activity 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"); createCalendarEventAction.title = objectGraph.loc.string("SHARE_SHEET_ADD_TO_CALENDAR"); createCalendarEventAction.artwork = contentArtwork.createArtworkForResource(objectGraph, "systemimage://calendar.circle"); const createCalendarEventActivity = new models.ShareSheetActivity("com.apple.AppStore.createCalendarEventActivity", createCalendarEventAction); // Create Calendar Event should be at the beginning. shareSheetActivities.unshift(createCalendarEventActivity); return shareSheetActivities; } // endregion // region Products export function shareSheetDataForProductFromProductData(objectGraph, data, clientIdentifierOverride) { return validation.context("shareSheetDataForProductFromProductData", () => { // Sharing is currently not supported on watchOS or the web client. if (serverData.isNull(data) || objectGraph.client.isWatch || objectGraph.client.isWeb) { return null; } // required attributes const url = shareUrlForProductData(objectGraph, data, "url"); const title = mediaAttributes.attributeAsString(data, "name"); const developerName = mediaAttributes.attributeAsString(data, "artistName"); const adamId = data.id; const storeFrontIdentifier = objectGraph.client.storefrontIdentifier; // Sanity check if (!url || !title || !developerName || !adamId) { return null; } // optional attributes const shortUrl = shareUrlForProductData(objectGraph, data, "shortUrl"); let artwork = null; let notesMetadata = null; const screenshots = content.screenshotsFromData(objectGraph, data, 4 /* content.ArtworkUseCase.LockupScreenshots */); const videos = content.videoPreviewsFromData(objectGraph, data); const subtitle = contentAttributes.contentAttributeAsString(objectGraph, data, "subtitle") || developerName; const genreName = null; const isMessagesOnlyApp = false; const messagesAppIcon = null; // Platform let platform; const isMacOnlyApp = dataOnlyHasDeviceFamily(objectGraph, data, "mac"); const isMacApp = dataHasDeviceFamily(objectGraph, data, "mac"); if (isMacOnlyApp || (objectGraph.client.isMac && isMacApp)) { platform = "Mac"; } else { platform = "iOS"; } // Add product info if (serverData.isDefinedNonNull(data) && mediaAttributes.attributeAsString(data, "url")) { artwork = content.iconFromData(objectGraph, data, { useCase: 1 /* content.ArtworkUseCase.LockupIconSmall */, }, clientIdentifierOverride); notesMetadata = notesMetadataFromProductData(objectGraph, data); } const productMetadata = new models.ShareSheetProductMetadata(adamId, storeFrontIdentifier, title, platform, artwork, screenshots, videos, isMessagesOnlyApp, subtitle, genreName, messagesAppIcon, notesMetadata); return new models.ShareSheetData(productMetadata, url, shortUrl); }); } export function shareProductActionFromData(objectGraph, data, metricsPageInformation, metricsLocationTracker, clientIdentifierOverride) { return validation.context(`shareActionFromData: ${data.type}`, () => { var _a; const id = data.id; switch (objectGraph.client.deviceType) { case "mac": { const shareSheetData = shareSheetDataForProductFromProductData(objectGraph, data); // Share action if (shareSheetData) { const shareAction = new models.ShareSheetAction(shareSheetData, []); metricsHelpersClicks.addClickEventToAction(objectGraph, shareAction, { targetType: "button", id: id, actionType: "share", pageInformation: metricsPageInformation, locationTracker: metricsLocationTracker, }); return shareAction; } break; } case "phone": case "pad": case "vision": { const shareSheetData = shareSheetDataForProductFromProductData(objectGraph, data, clientIdentifierOverride); const shareSheetActivities = []; // Copy link action if (((_a = shareSheetData === null || shareSheetData === void 0 ? void 0 : shareSheetData.url) === null || _a === void 0 ? void 0 : _a.length) > 0) { const copyLinkActivity = copyLinkShareSheetActivityForURL(objectGraph, shareSheetData.url); if (serverData.isDefinedNonNull) { shareSheetActivities.push(copyLinkActivity); } } // Gift action const giftAdamId = adamIdForShareSheetGiftActivityFromProductData(objectGraph, data); if (giftAdamId) { const giftAction = new models.FlowAction("finance"); giftAction.presentationContext = "presentModal"; giftAction.title = objectGraph.loc.string("SHARE_GIFT_APP"); giftAction.artwork = contentArtwork.createArtworkForResource(objectGraph, "systemimage://app.gift"); giftAction.pageUrl = `gift/${giftAdamId}`; metricsHelpersClicks.addClickEventToAction(objectGraph, giftAction, { targetType: "button", id: id, actionType: "gift", actionContext: "shareSheet", pageInformation: metricsPageInformation, locationTracker: metricsLocationTracker, }); const giftActionActivity = new models.ShareSheetActivity("com.apple.AppStore.giftActivity", giftAction); shareSheetActivities.push(giftActionActivity); } if (shareSheetData) { // Share action const shareSheetStyle = "expanded"; const shareAction = new models.ShareSheetAction(shareSheetData, shareSheetActivities, shareSheetStyle); shareAction.title = objectGraph.loc.string("SHARE_APP"); shareAction.artwork = contentArtwork.createArtworkForResource(objectGraph, "systemimage://square.and.arrow.up"); metricsHelpersClicks.addClickEventToAction(objectGraph, shareAction, { targetType: "button", id: id, actionType: "share", actionContext: "shareSheet", pageInformation: metricsPageInformation, locationTracker: metricsLocationTracker, }); return shareAction; } else if (shareSheetActivities.length > 0) { // Map ActionActivity[] to Action[] as we're not dealing with a share sheet const sheetActions = shareSheetActivities.map((activity) => activity.action); // Create action sheet instead of share sheet const sheetAction = new models.SheetAction(sheetActions); sheetAction.isCancelable = true; sheetAction.isCustom = true; metricsHelpersClicks.addClickEventToAction(objectGraph, sheetAction, { targetType: "button", id: id, actionType: "actionSheet", pageInformation: metricsPageInformation, locationTracker: metricsLocationTracker, }); return sheetAction; } break; } default: { // These device types do not support sharing, so we don't create any share actions. break; } } return null; }); } // region Generic Pages export function shareSheetDataForGenericPage(objectGraph, text, url, subtitle, shortUrl, artwork) { return validation.context("shareSheetDataForGenericPage", () => { if (serverData.isNullOrEmpty(url)) { return null; } const metadata = new models.ShareSheetGenericMetadata(text, subtitle, artwork !== null && artwork !== void 0 ? artwork : undefined); return new models.ShareSheetData(metadata, url, shortUrl); }); } export function shareSheetActivitiesForGenericPage(objectGraph, shareUrl) { const shareSheetActivities = []; if ((shareUrl === null || shareUrl === void 0 ? void 0 : shareUrl.length) > 0) { const copyLinkActivity = copyLinkShareSheetActivityForURL(objectGraph, shareUrl); if (serverData.isDefinedNonNull) { shareSheetActivities.push(copyLinkActivity); } } return shareSheetActivities; } // endregion //# sourceMappingURL=sharing.js.map