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