import * as validation from "@jet/environment/json/validation"; import { isNothing } from "@jet/environment/types/optional"; import * as models from "../../api/models"; import * as serverData from "../../foundation/json-parsing/server-data"; import * as mediaAttributes from "../../foundation/media/attributes"; import * as mediaRelationships from "../../foundation/media/relationships"; import { Path, Protocol } from "../../foundation/network/url-constants"; import * as urls from "../../foundation/network/urls"; import * as constants from "../../foundation/util/constants"; import * as contentAttributes from "../content/attributes"; import * as metricsHelpersClicks from "../metrics/helpers/clicks"; import * as linksShelf from "../product-page/shelves/links-shelf"; import * as privacyTypesShelf from "./privacy-types-shelf"; import { makeRoutableArticlePageCanonicalUrl } from "../today/routable-article-page-url-utils"; import { makeRoutableArticlePageIntent } from "../../api/intents/routable-article-page-intent"; import { getPlatform } from "../preview-platform"; import { getLocale } from "../locale"; /** * Builder for the privacy header shelf. This shelf is currently used * on both the product page, and the privacy detail page. */ export function create(objectGraph, data, pageInformation, locationTracker) { return validation.context("privacyShelf", () => { if (serverData.isNullOrEmpty(data)) { return null; } const shelf = new models.Shelf("privacyHeader"); shelf.title = objectGraph.loc.string("PRODUCT_PRIVACY_TITLE"); const privacyHeader = privacyHeaderFromData(objectGraph, data, false, false, pageInformation, locationTracker); shelf.items = [privacyHeader]; if (objectGraph.client.deviceType !== "watch" && objectGraph.client.deviceType !== "tv") { shelf.seeAllAction = privacyDetailActionFromData(objectGraph, data, "detailPage", pageInformation, locationTracker, null); } return shelf; }); } /** * Creates a privacy header object. * @param data The data blob * @param isDetailHeader Whether this header is intended to be displayed on the detail page * @param isDetailData Whether the included data is for the detail page */ export function privacyHeaderFromData(objectGraph, data, isDetailHeader, isDetailData, pageInformation, locationTracker) { return validation.context("createPrivacyHeaderFromData", () => { const bodyText = bodyTextFromData(objectGraph, data, isDetailHeader, isDetailData, pageInformation, locationTracker); let seeDetailsAction; let privacyPolicyAction; const privacyDefinitionsText = privacyDefinitionsTextFromData(objectGraph, data, isDetailHeader, pageInformation, locationTracker); let privacyDefinitionsAction; let learnMoreText; let learnMoreAction; if (objectGraph.client.isTV || objectGraph.client.isWatch) { if (!isDetailHeader) { const destinationPrivacyTypeStyle = privacyTypesShelf.isIntermediateDetailPageEnabled(objectGraph) ? "intermediateDetailPage" : "detailPage"; seeDetailsAction = privacyDetailActionFromData(objectGraph, data, destinationPrivacyTypeStyle, pageInformation, locationTracker, null); } privacyPolicyAction = privacyPolicyActionFromData(objectGraph, data, pageInformation, locationTracker); } if (isDetailHeader) { if (objectGraph.client.isWatch || objectGraph.client.isTV) { privacyDefinitionsAction = createPrivacyDefinitionsAction(objectGraph, pageInformation, locationTracker); } learnMoreText = learnMoreTextFromData(objectGraph, data, isDetailHeader, pageInformation, locationTracker); if (serverData.isDefinedNonNullNonEmpty(learnMoreText)) { learnMoreAction = createLearnMoreAction(objectGraph, pageInformation, locationTracker); } } const supplementaryItems = []; if (serverData.isDefinedNonNull(privacyDefinitionsText)) { const supplementaryItem = new models.PrivacyHeaderSupplementaryItem(privacyDefinitionsText, privacyDefinitionsAction); supplementaryItems.push(supplementaryItem); } if (serverData.isDefinedNonNull(learnMoreText)) { const supplementaryItem = new models.PrivacyHeaderSupplementaryItem(learnMoreText, learnMoreAction); supplementaryItems.push(supplementaryItem); } let privacyTypes = []; if ((objectGraph.client.isWatch || objectGraph.client.isTV) && !isDetailHeader) { privacyTypes = privacyTypesFromData(objectGraph, data, isDetailData, "productPage", pageInformation, locationTracker); } const bodyActions = []; if (objectGraph.client.isTV) { if (serverData.isDefinedNonNull(seeDetailsAction)) { bodyActions.push(seeDetailsAction); } if (serverData.isDefinedNonNull(privacyPolicyAction)) { bodyActions.push(privacyPolicyAction); } } if (objectGraph.client.isWatch) { if (serverData.isDefinedNonNull(privacyPolicyAction)) { bodyActions.push(privacyPolicyAction); } } return new models.PrivacyHeader(bodyText, isDetailHeader, privacyTypes, bodyActions, supplementaryItems, seeDetailsAction); }); } /** * Creates the main body text for the header. * @param data The data blob * @param isDetailHeader Whether this header is intended to be displayed on the detail page * @param isDetailData Whether the included data is for the detail page */ function bodyTextFromData(objectGraph, data, isDetailHeader, isDetailData, pageInformation, locationTracker) { let text; let textType = "text/x-apple-as3-nqml"; const developer = mediaRelationships.relationshipData(objectGraph, data, "developer"); const isAppleOwnedDeveloperId = serverData.isDefinedNonNullNonEmpty(developer) && constants.appleOwnedDeveloperIds.indexOf(developer.id) > -1; if (isDetailHeader && !isAppleOwnedDeveloperId) { text = objectGraph.loc.string("PRODUCT_PRIVACY_DETAIL_HEADER_TEMPLATE"); } else { text = objectGraph.loc.string("PRODUCT_PRIVACY_HEADER_TEMPLATE"); } const privacyTypes = privacyTypesFromData(objectGraph, data, isDetailData, "detailPage", pageInformation, locationTracker); const privacyDataNotProvided = (privacyTypes.length === 1 && privacyTypes[0].identifier === "DATA_NOT_PROVIDED") || privacyTypes.length === 0; if (privacyDataNotProvided) { text = objectGraph.loc.string("PRODUCT_PRIVACY_HEADER_NO_DETAILS_TEMPLATE"); } // Developer name const developerName = mediaAttributes.attributeAsString(data, "artistName"); if (serverData.isDefinedNonNull(developerName)) { text = text.replace("{developerName}", "" + developerName + ""); } else { // This shouldn't happen, but just in case, fallback to the text with no developer name placeholder if (privacyDataNotProvided) { text = objectGraph.loc.string("PRODUCT_PRIVACY_FALLBACK_HEADER_NO_DETAILS_TEMPLATE"); } else { if (isDetailHeader) { text = objectGraph.loc.string("PRODUCT_PRIVACY_FALLBACK_DETAIL_HEADER_TEMPLATE"); } else { text = objectGraph.loc.string("PRODUCT_PRIVACY_FALLBACK_HEADER_TEMPLATE"); } } textType = "text/plain"; } // Privacy policy link const privacyPolicyLinkText = objectGraph.loc.string("PRODUCT_PRIVACY_SUMMARY_PRIVACY_POLICY_LINK"); text = text.replace("{privacyPolicyLink}", privacyPolicyLinkText); const privacyPolicyAction = privacyPolicyActionFromData(objectGraph, data, pageInformation, locationTracker); const linkedSubstrings = {}; if (serverData.isDefinedNonNull(privacyPolicyAction)) { linkedSubstrings[privacyPolicyLinkText] = privacyPolicyAction; } // Manage choices if (isDetailHeader) { if (objectGraph.client.isiOS || objectGraph.client.isMac || objectGraph.client.isVision || objectGraph.client.isWeb) { const managePrivacyChoicesAction = managePrivacyChoicesActionFromData(objectGraph, data, pageInformation, locationTracker); if (serverData.isDefinedNonNull(managePrivacyChoicesAction)) { const managePrivacyChoicesLink = objectGraph.loc.string("PRODUCT_PRIVACY_MANAGE_CHOICES_LINK"); text += "

"; text += objectGraph.loc .string("PRODUCT_PRIVACY_MANAGE_CHOICES_TEMPLATE") .replace("{manageChoicesLink}", managePrivacyChoicesLink); managePrivacyChoicesAction.title = managePrivacyChoicesLink; linkedSubstrings[managePrivacyChoicesLink] = managePrivacyChoicesAction; } } else { text += "

"; text += objectGraph.loc.string("PRODUCT_PRIVACY_MANAGE_CHOICES_NO_LINK"); } } const styledText = new models.StyledText(text, textType); return new models.LinkableText(styledText, linkedSubstrings); } /** * Creates the action for linking to the developer's privacy policy. */ function privacyPolicyActionFromData(objectGraph, data, pageInformation, locationTracker) { let privacyPolicyLinkAction; if (objectGraph.client.isTV) { const hasPrivacyPolicy = contentAttributes.contentAttributeAsBooleanOrFalse(objectGraph, data, "hasPrivacyPolicyText"); if (!hasPrivacyPolicy) { return null; } const url = linksShelf.privacyPolicyUrlFromData(objectGraph, data); if (serverData.isNull(url)) { return null; } const action = new models.FlowAction("unknown"); action.pageUrl = url; privacyPolicyLinkAction = action; } else { const privacyPolicyUrl = contentAttributes.contentAttributeAsString(objectGraph, data, "privacyPolicyUrl"); if (isNothing(privacyPolicyUrl) || serverData.isNullOrEmpty(privacyPolicyUrl)) { return null; } privacyPolicyLinkAction = new models.ExternalUrlAction(privacyPolicyUrl, false); } privacyPolicyLinkAction.title = objectGraph.loc.string("PRODUCT_PRIVACY_SUMMARY_PRIVACY_POLICY_BUTTON_TITLE"); metricsHelpersClicks.addClickEventToAction(objectGraph, privacyPolicyLinkAction, { targetType: "link", id: "privacyPolicy", pageInformation: pageInformation, locationTracker: locationTracker, }); return privacyPolicyLinkAction; } /** * Creates the action for linking to the developer's manage privacy choices URL. */ function managePrivacyChoicesActionFromData(objectGraph, data, pageInformation, locationTracker) { const privacyDetailsData = mediaAttributes.attributeAsDictionary(data, "privacyDetails"); const managePrivacyChoicesUrl = serverData.asString(privacyDetailsData, "managePrivacyChoicesUrl"); if (isNothing(managePrivacyChoicesUrl) || serverData.isNullOrEmpty(managePrivacyChoicesUrl)) { return null; } const managePrivacyChoicesAction = new models.ExternalUrlAction(managePrivacyChoicesUrl, false); metricsHelpersClicks.addClickEventToAction(objectGraph, managePrivacyChoicesAction, { targetType: "link", id: "managePrivacyChoices", pageInformation: pageInformation, locationTracker: locationTracker, }); return managePrivacyChoicesAction; } /** * Creates the text for linking to the learn more page. * @param data The data blob * @param isDetailHeader Whether this header is intended to be displayed on the detail page */ function learnMoreTextFromData(objectGraph, data, isDetailHeader, pageInformation, locationTracker) { if (!isDetailHeader || objectGraph.client.isTV) { return null; } const learnMoreLink = objectGraph.loc.string("PRODUCT_PRIVACY_LEARN_MORE_LINK"); const learnMoreAction = createLearnMoreAction(objectGraph, pageInformation, locationTracker); let text; const linkedSubstrings = {}; if (serverData.isNull(learnMoreAction) || objectGraph.client.isWatch) { text = objectGraph.loc.string("PRODUCT_PRIVACY_LEARN_MORE_NO_LINK"); } else { text = objectGraph.loc.string("PRODUCT_PRIVACY_LEARN_MORE_TEMPLATE").replace("{learnMoreLink}", learnMoreLink); learnMoreAction.title = learnMoreLink; linkedSubstrings[learnMoreLink] = learnMoreAction; } const styledText = new models.StyledText(text, "text/plain"); return new models.LinkableText(styledText, linkedSubstrings); } /** * Creates the action for linking to the learn more URL. */ export function createLearnMoreAction(objectGraph, pageInformation, locationTracker) { const editorialItemId = objectGraph.bag.appPrivacyLearnMoreEditorialItemId; if (isNothing(editorialItemId) || editorialItemId.length === 0) { return null; } const learnMoreAction = new models.FlowAction("article"); learnMoreAction.title = objectGraph.loc.string("PRODUCT_PRIVACY_LEARN_MORE_LINK"); learnMoreAction.pageUrl = `https://apps.apple.com/story/id${editorialItemId}`; metricsHelpersClicks.addClickEventToAction(objectGraph, learnMoreAction, { targetType: "button", id: "privacyLearnMore", pageInformation: pageInformation, locationTracker: locationTracker, }); if (objectGraph.client.isVision) { learnMoreAction.presentation = "sheetPresent"; } if (objectGraph.client.isWeb) { const destination = makeRoutableArticlePageIntent({ ...getLocale(objectGraph), ...getPlatform(objectGraph), id: editorialItemId, }); const pageUrlString = makeRoutableArticlePageCanonicalUrl(objectGraph, destination); learnMoreAction.pageUrl = pageUrlString; learnMoreAction.destination = destination; } return learnMoreAction; } /** * Creates the text for linking to the privacy definitions EI. * @param data The data blob * @param isDetailHeader Whether this header is intended to be displayed on the detail page */ function privacyDefinitionsTextFromData(objectGraph, data, isDetailHeader, pageInformation, locationTracker) { if (!isDetailHeader) { return null; } const privacyDefinitionsLink = objectGraph.loc.string("PRODUCT_PRIVACY_DEFINITIONS_LINK"); const text = objectGraph.loc .string("PRODUCT_PRIVACY_DEFINITIONS_LINK_TEMPLATE") .replace("{privacyDefinitionsLink}", privacyDefinitionsLink); const privacyDefinitionsAction = createPrivacyDefinitionsAction(objectGraph, pageInformation, locationTracker); if (serverData.isNull(privacyDefinitionsAction)) { return null; } privacyDefinitionsAction.title = privacyDefinitionsLink; const linkedSubstrings = {}; linkedSubstrings[privacyDefinitionsLink] = privacyDefinitionsAction; const styledText = new models.StyledText(text, "text/plain"); return new models.LinkableText(styledText, linkedSubstrings); } /** * Creates the action for linking to the privacy definitions page. */ export function createPrivacyDefinitionsAction(objectGraph, pageInformation, locationTracker) { const editorialItemId = objectGraph.bag.appPrivacyDefinitionsEditorialItemId; if (isNothing(editorialItemId) || editorialItemId.length === 0) { return null; } const privacyDefinitionsAction = new models.FlowAction("article"); privacyDefinitionsAction.title = objectGraph.loc.string("PRODUCT_PRIVACY_DEFINITIONS_LINK"); privacyDefinitionsAction.pageUrl = `https://apps.apple.com/story/id${editorialItemId}`; if (objectGraph.client.isWeb) { const destination = makeRoutableArticlePageIntent({ ...getLocale(objectGraph), ...getPlatform(objectGraph), id: editorialItemId, }); const pageUrlString = makeRoutableArticlePageCanonicalUrl(objectGraph, destination); privacyDefinitionsAction.pageUrl = pageUrlString; privacyDefinitionsAction.destination = destination; } metricsHelpersClicks.addClickEventToAction(objectGraph, privacyDefinitionsAction, { targetType: "button", id: "privacyDefinitions", pageInformation: pageInformation, locationTracker: locationTracker, }); return privacyDefinitionsAction; } /** * Creates a list of privacy types, suitable for associating with the header. * In practice, this means that the categories will only be included when necessary, * to keep the data size as small as possible. */ export function privacyTypesFromData(objectGraph, data, isDetailData, destinationPrivacyTypeStyle, pageInformation, locationTracker) { var _a; let privacyTypes = []; const privacyDataKey = isDetailData ? "privacyDetails" : "privacy"; const privacyData = mediaAttributes.attributeAsDictionary(data, privacyDataKey); if (serverData.isDefinedNonNullNonEmpty(privacyData)) { const includeCategories = objectGraph.client.deviceType !== "watch" || destinationPrivacyTypeStyle === "intermediateDetailPage"; privacyTypes = (_a = privacyTypesShelf.privacyTypesFromData(objectGraph, privacyData, data, destinationPrivacyTypeStyle, includeCategories, pageInformation, locationTracker)) !== null && _a !== void 0 ? _a : []; // If we only have a single privacy type, with no categories, force it to display in `productPage` style // even on the intermediate detail page, as it has a nicer appearance for this case. if (privacyTypes.length === 1 && privacyTypes[0].categories.length === 0) { privacyTypes[0].style = "productPage"; } } return privacyTypes; } /** * Creates an incomplete privacy detail page, sidepacking the privacy header shelf. * On watchOS, when creating the `intermediateDetailPage`, we also sidepack the privacy types themselves, * resulting in a complete (intermediate) detail page. */ export function privacyDetailSidepackPageFromData(objectGraph, data, destinationPrivacyTypeStyle, pageInformation, locationTracker) { const shelves = []; // Always sidepack the header, unless this will be the detail page for the watch, // which does not display the header if (objectGraph.client.deviceType !== "watch" || destinationPrivacyTypeStyle !== "detailPage") { const headerShelf = new models.Shelf("privacyHeader"); const privacyHeader = privacyHeaderFromData(objectGraph, data, true, false, pageInformation, locationTracker); headerShelf.items = [privacyHeader]; headerShelf.presentationHints = { isFirstShelf: true }; shelves.push(headerShelf); } if ( // On the watch, we want to sidepack the privacy types if coming from the product page // as we already have all the information we need, and we can avoid having an incomplete page (objectGraph.client.isWatch && destinationPrivacyTypeStyle === "intermediateDetailPage") || // The "detailPage" on the "web" client is presented as a modal, so we want to sidepack all // of the data so it can display is immediately (objectGraph.client.isWeb && destinationPrivacyTypeStyle === "detailPage")) { const privacyTypes = privacyTypesFromData(objectGraph, data, objectGraph.client.isWeb, destinationPrivacyTypeStyle, pageInformation, locationTracker); const shelf = new models.Shelf("privacyType"); if (privacyTypes.length > 0) { shelf.items = privacyTypes; shelves.push(shelf); } } const page = new models.GenericPage(shelves); if (objectGraph.client.deviceType !== "watch" || destinationPrivacyTypeStyle === "detailPage") { page.isIncomplete = true; } page.title = objectGraph.loc.string("PRODUCT_PRIVACY_TITLE"); if (objectGraph.client.isMac) { page.presentationOptions = ["prefersLargeTitle"]; } return page; } /** * Creates the action for linking to the privacy detail page. */ export function privacyDetailActionFromData(objectGraph, data, destinationPrivacyTypeStyle, pageInformation, locationTracker, scrollFocusPrivacyTypeId) { if (serverData.isNull(data.id)) { return null; } const seeDetailsAction = new models.FlowAction("privacyDetail"); seeDetailsAction.title = objectGraph.loc.string("ACTION_SEE_DETAILS"); seeDetailsAction.pageData = privacyDetailSidepackPageFromData(objectGraph, data, destinationPrivacyTypeStyle, pageInformation, locationTracker); const productType = data.type === "app-bundles" ? Path.productBundle : Path.product; let query; if (serverData.isDefinedNonNullNonEmpty(scrollFocusPrivacyTypeId)) { query = { privacyTypeId: scrollFocusPrivacyTypeId }; } const pageUrl = urls.URL.fromComponents(Protocol.internal, null, `/${Path.privacyDetail}/${productType}/${data.id}`, query); seeDetailsAction.pageUrl = pageUrl.build(); const seeDetailsClickOptions = { targetType: "button", id: "seeDetails", pageInformation: pageInformation, locationTracker: locationTracker, }; if (serverData.isDefinedNonNull(scrollFocusPrivacyTypeId)) { seeDetailsClickOptions.targetType = "privacyCard"; seeDetailsClickOptions.id = scrollFocusPrivacyTypeId; } metricsHelpersClicks.addClickEventToAction(objectGraph, seeDetailsAction, seeDetailsClickOptions); return seeDetailsAction; } //# sourceMappingURL=privacy-header-shelf.js.map