diff options
| author | rxliuli <rxliuli@gmail.com> | 2025-11-04 05:03:50 +0800 |
|---|---|---|
| committer | rxliuli <rxliuli@gmail.com> | 2025-11-04 05:03:50 +0800 |
| commit | bce557cc2dc767628bed6aac87301a1be7c5431b (patch) | |
| tree | b51a051228d01fe3306cd7626d4a96768aadb944 /node_modules/@jet-app/app-store/tmp/src/common/privacy/privacy-header-shelf.js | |
init commit
Diffstat (limited to 'node_modules/@jet-app/app-store/tmp/src/common/privacy/privacy-header-shelf.js')
| -rw-r--r-- | node_modules/@jet-app/app-store/tmp/src/common/privacy/privacy-header-shelf.js | 427 |
1 files changed, 427 insertions, 0 deletions
diff --git a/node_modules/@jet-app/app-store/tmp/src/common/privacy/privacy-header-shelf.js b/node_modules/@jet-app/app-store/tmp/src/common/privacy/privacy-header-shelf.js new file mode 100644 index 0000000..b911d94 --- /dev/null +++ b/node_modules/@jet-app/app-store/tmp/src/common/privacy/privacy-header-shelf.js @@ -0,0 +1,427 @@ +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}", "<b>" + developerName + "</b>"); + } + 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 += "<br><br>"; + text += objectGraph.loc + .string("PRODUCT_PRIVACY_MANAGE_CHOICES_TEMPLATE") + .replace("{manageChoicesLink}", managePrivacyChoicesLink); + managePrivacyChoicesAction.title = managePrivacyChoicesLink; + linkedSubstrings[managePrivacyChoicesLink] = managePrivacyChoicesAction; + } + } + else { + text += "<br><br>"; + 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
\ No newline at end of file |
