import * as validation from "@jet/environment/json/validation"; import * as models from "../../api/models"; import * as base from "../../api/models/base"; import * as serverData from "../../foundation/json-parsing/server-data"; import * as mediaAttributes from "../../foundation/media/attributes"; import * as mediaRelationship from "../../foundation/media/relationships"; import * as http from "../../foundation/network/http"; import * as urls from "../../foundation/network/urls"; import * as externalDeepLink from "../linking/external-deep-link"; import * as lockups from "../lockups/lockups"; import * as metricsUtil from "../metrics/helpers/util"; import * as offers from "../offers/offers"; import * as reviews from "../product-page/reviews"; import * as sharing from "../sharing"; import * as contentArtwork from "./artwork/artwork"; import * as content from "./content"; import * as contentDeviceFamily from "./device-family"; import { isSome } from "@jet/environment"; import { Request } from "../../foundation/media/data-fetching"; import { buildURLFromRequest } from "../../foundation/media/url-builder"; /** * Create and return the flow preview actions configuration for a product. * @param objectGraph * @param data The response data to read from. * @param includeOfferAction Whether to include an offer action, where possible * @param clientIdentifierOverride The suggested client identifier for the given product * @param clickAction The flow action for viewing the product * @param metricsOptions The metrics options to use for reporting metrics actions. * @param metricsClickOptions TThe metrics click options for the product * @param externalDeepLinkUrl * @returns A configuration object for flow preview actions that can be taken on the product */ export function flowPreviewActionsConfigurationForProductFromData(objectGraph, data, includeOfferAction, clientIdentifierOverride, clickAction, metricsOptions, metricsClickOptions, externalDeepLinkUrl, lockupSubtitle, lockupTitle) { return validation.context("flowPreviewActionsConfigurationForProductFromData", () => { // Flow preview is only supported on iOS if (objectGraph.client.deviceType !== "phone" && objectGraph.client.deviceType !== "pad") { return null; } const productData = productDataFromData(objectGraph, data); if (!serverData.isDefinedNonNullNonEmpty(productData)) { return null; } const actions = []; // Offer let offerActionIndex = null; let offerDisplayProperties = null; const isPreorder = mediaAttributes.attributeAsBooleanOrFalse(productData, "isPreorder"); if (includeOfferAction) { const isArcade = content.isArcadeSupported(objectGraph, productData); const offerType = lockups.offerTypeForMediaType(objectGraph, productData.type, isArcade); const offerAction = offerActionFromData(objectGraph, productData, isPreorder, isArcade, offerType, clientIdentifierOverride, metricsClickOptions); offerDisplayProperties = offers.displayPropertiesFromOfferAction(objectGraph, offerAction, offerType, productData, isPreorder, false, null, null, null, null, "flowPreview"); const wrappedOfferAction = wrappedOfferActionFromData(objectGraph, productData, offerAction, isPreorder, clientIdentifierOverride, metricsClickOptions, metricsOptions, externalDeepLinkUrl); if (serverData.isDefinedNonNull(wrappedOfferAction) && serverData.isDefinedNonNull(offerDisplayProperties)) { offerActionIndex = actions.length; wrappedOfferAction.artwork = contentArtwork.createArtworkForResource(objectGraph, "systemimage://arrow.down.circle"); actions.push(wrappedOfferAction); } } // Share const shareAction = shareActionFromData(objectGraph, productData, metricsOptions); if (serverData.isDefinedNonNull(shareAction)) { actions.push(shareAction); } // Reviews const shouldSuppressReviews = reviews.shouldSuppressReviews(objectGraph, productData); const shouldShowRatingsAndReviews = !isPreorder && !shouldSuppressReviews; if (shouldShowRatingsAndReviews) { // See ratings & reviews if (serverData.isDefinedNonNull(clickAction) && clickAction instanceof models.FlowAction && (clickAction.pageData instanceof models.ProductPage || clickAction.pageData instanceof models.ShelfBasedProductPage)) { const seeRatingsAndReviewsAction = seeRatingsAndReviewsActionFromData(objectGraph, productData, clickAction); if (serverData.isDefinedNonNull(seeRatingsAndReviewsAction)) { actions.push(seeRatingsAndReviewsAction); } } // Write a review const isTvOnlyApp = contentDeviceFamily.dataOnlyHasDeviceFamily(objectGraph, data, "tvos"); if (!isTvOnlyApp) { const writeReviewAction = writeReviewActionFromData(objectGraph, productData, lockupSubtitle, lockupTitle); if (serverData.isDefinedNonNull(writeReviewAction)) { actions.push(writeReviewAction); } } } return new models.FlowPreviewActionsConfiguration(actions, offerDisplayProperties, offerActionIndex); }); } /** * Create and return the flow preview configuration for a review object * @param data The response data to read from. * @param deviceId The UUID for the user's device. * @param adamId The adam id of the product associated with the review * @returns A configuration object for flow preview actions that can be taken on the review */ export function flowPreviewActionsConfigurationForReviewFromData(objectGraph, data, deviceId, adamId, reviewText) { var _a; if (serverData.isNullOrEmpty(data) || (objectGraph.client.deviceType !== "phone" && objectGraph.client.deviceType !== "pad" && objectGraph.client.deviceType !== "mac")) { return null; } const actions = [ voteActionFromData(objectGraph, data, deviceId, true), voteActionFromData(objectGraph, data, deviceId, false), ]; // Workaround for missing report concern url in bag if (((_a = objectGraph.bag.reportConcernUrl) === null || _a === void 0 ? void 0 : _a.length) > 0) { actions.push(reportConcernActionFromData(objectGraph, data, deviceId)); } if ((reviewText === null || reviewText === void 0 ? void 0 : reviewText.length) > 0) { actions.push(copyReviewAction(objectGraph, reviewText)); } return new models.FlowPreviewActionsConfiguration(actions); } /** * Create and return the flow preview configuration for a review summary object * @param data The response data to read from. * @param adamId The adam ID of the app * @param appName The name of the app * @param reviewSummaryId The ID of the review summary * @param reviewSummaryText The text of the review summary * @param deviceId The UUID for the customer's device. * @returns A configuration object for flow preview actions that can be taken on the review summary */ export function flowPreviewActionsConfigurationForReviewSummaryFromData(objectGraph, data, adamId, appName, reviewSummaryId, reviewSummaryText, deviceId) { if (!objectGraph.client.isiOS) { return null; } const actions = []; const reportConcernAction = reviewSummaryReportConcernActionFromData(objectGraph, data, reviewSummaryId, deviceId); if (isSome(reportConcernAction)) { actions.push(reportConcernAction); } const learnMoreAction = reviews.reviewSummaryLearnMoreAction(objectGraph); if (isSome(learnMoreAction)) { actions.push(learnMoreAction); } const fileRadarAction = fileReviewSummaryRadarAction(objectGraph, adamId, appName, reviewSummaryId, reviewSummaryText); if (isSome(fileRadarAction)) { actions.push(fileRadarAction); } return new models.FlowPreviewActionsConfiguration(actions); } /** * Creates a report a concern action for a given review * @param data The response data to read from. * @param deviceId The UUID for the user's device. * @returns A report concern action */ export function reviewSummaryReportConcernActionFromData(objectGraph, data, reviewSummaryId, deviceId) { return validation.context("reviewSummaryReportConcernActionFromApiRow", () => { const isEnabled = serverData.asBoolean(data, "enabled"); if (!isEnabled) { return null; } const sendAction = createReviewSummaryReportConcernHtmlTemplateAction(objectGraph, reviewSummaryId); const concerns = serverData.asArrayOrEmpty(data, "concerns"); const reviewSummaryConcerns = concerns.map((concernData) => { let name; let uppercaseName; const concernKind = serverData.asString(concernData, "kind"); switch (concernKind) { case "OFFENSIVE": case "OFFENSIVE_OR_HARMFUL": name = objectGraph.loc.string("ProductPage.ReviewSummary.ReportAConcern.Offensive.Name"); uppercaseName = objectGraph.loc.string("ProductPage.ReviewSummary.ReportAConcern.Offensive.UppercaseName"); break; case "MISREPRESENTING_THE_APP": case "MISREPRESENT": name = objectGraph.loc.string("ProductPage.ReviewSummary.ReportAConcern.Misrepresent.Name"); uppercaseName = objectGraph.loc.string("ProductPage.ReviewSummary.ReportAConcern.Misrepresent.UppercaseName"); break; case "SOMETHING_ELSE": name = objectGraph.loc.string("ProductPage.ReviewSummary.ReportAConcern.SomethingElse.Name"); uppercaseName = objectGraph.loc.string("ProductPage.ReviewSummary.ReportAConcern.SomethingElse.UppercaseName"); break; default: break; } return new base.ReportConcernReason(concernKind, name, uppercaseName); }); if (reviewSummaryConcerns.length === 0) { return null; } const title = objectGraph.loc.string("ACTION_REVIEW_REPORT"); const explanation = objectGraph.loc.string("ProductPage.ReviewSummary.ReportAConcern.Explanation"); const action = new models.ReviewSummaryReportConcernAction(reviewSummaryConcerns, title, explanation, sendAction); action.artwork = contentArtwork.createArtworkForResource(objectGraph, "systemimage://exclamationmark.circle"); return action; }); } function createReviewSummaryReportConcernHtmlTemplateAction(objectGraph, reviewSummaryId) { const request = new Request(objectGraph); request.includeAppBinaryTraitsAttribute = false; request.resourceType = "concerns"; const url = buildURLFromRequest(objectGraph, request); const sendAction = new models.HttpTemplateAction(url.toString()); sendAction.method = "POST"; sendAction.disableCache = true; sendAction.needsMediaToken = true; sendAction.headers = { "Content-Type": "application/json" }; sendAction.bodyDictionary = { report: { contentId: reviewSummaryId, contentKind: "review-summaries", concerns: null }, }; const successToast = new models.AlertAction("toast"); successToast.title = objectGraph.loc.string("TOAST_CONCERN_REPORTED_TITLE"); successToast.message = objectGraph.loc.string("TOAST_CONCERN_REPORTED_DESCRIPTION"); successToast.artwork = contentArtwork.createArtworkForResource(objectGraph, "systemimage://exclamationmark.circle"); sendAction.successAction = successToast; const failureAlert = new models.AlertAction("default"); failureAlert.title = objectGraph.loc.string("Alert.GenericError.Title"); failureAlert.message = objectGraph.loc.string("Alert.GenericError.Message"); failureAlert.isCancelable = true; sendAction.failureAction = failureAlert; return sendAction; } /** * Walks through the given data to extract the data blob for a product * @param data The response data to read from. * @returns Response data for a product */ function productDataFromData(objectGraph, data) { return validation.context(`productDataFromData: ${data.type}`, () => { switch (data.type) { case "apps": case "app-bundles": { return data; } case "editorial-items": { const cardContents = mediaRelationship.relationshipCollection(data, "card-contents"); if (serverData.isDefinedNonNullNonEmpty(cardContents) && cardContents.length === 1) { const cardContentsData = cardContents[0]; return productDataFromData(objectGraph, cardContentsData); } break; } case "editorial-elements": { const contents = mediaRelationship.relationshipCollection(data, "contents"); if (serverData.isDefinedNonNullNonEmpty(contents) && contents.length === 1) { const contentData = contents[0]; return productDataFromData(objectGraph, contentData); } break; } default: { return null; } } return null; }); } /** * Create an offer action for the provided product data * @param data The response data to read from. * @param isPreorder Whether the product is a pre-order * @param isArcade Whether the product is an Arcade product * @param offerType The type of offer for the product * @param clientIdentifierOverride The suggested client identifier for the given product * @param metricsClickOptions TThe metrics click options for the product * @returns An offer action */ function offerActionFromData(objectGraph, data, isPreorder, isArcade, offerType, clientIdentifierOverride, metricsClickOptions) { if (serverData.isNull(data) || data.type !== "apps") { return null; } const offerData = offers.offerDataFromData(objectGraph, data); const appIcon = content.iconFromData(objectGraph, data, null, clientIdentifierOverride); const metricsPlatformDisplayStyle = metricsUtil.metricsPlatformDisplayStyleFromData(objectGraph, data, appIcon, clientIdentifierOverride); const offerAction = offers.offerActionFromOfferData(objectGraph, offerData, data, isPreorder, false, metricsPlatformDisplayStyle, metricsClickOptions, "flowPreview"); return offerAction; } /** * Wraps an offer action if required * @param data The response data to read from. * @param offerAction The provided offer action to wrap * @param clientIdentifierOverride The suggested client identifier for the given product * @param metricsClickOptions TThe metrics click options for the product * @param metricsOptions The metrics options to use for reporting metrics actions. * @param externalDeepLink The promotional deep link url to use on the product's offer. * @returns A wrapped offer action */ function wrappedOfferActionFromData(objectGraph, data, offerAction, isPreorder, clientIdentifierOverride, metricsClickOptions, metricsOptions, externalDeepLinkUrl) { if (serverData.isNull(offerAction)) { return null; } let wrappedOfferAction = offers.wrapOfferActionIfNeeded(objectGraph, offerAction, data, isPreorder, metricsClickOptions, "flowPreview", clientIdentifierOverride); if ((externalDeepLinkUrl === null || externalDeepLinkUrl === void 0 ? void 0 : externalDeepLinkUrl.length) > 0) { // Configure cross link as well as deep link action. wrappedOfferAction = externalDeepLink.deepLinkActionWrappingAction(objectGraph, wrappedOfferAction, offerAction.adamId, null, externalDeepLinkUrl, false, metricsClickOptions); } return wrappedOfferAction; } /** * Creates and returns an action for sharing a product * @param data The response data to read from. * @param metricsOptions The metrics options to use for reporting metrics actions. * @returns A share action or null */ function shareActionFromData(objectGraph, data, metricsOptions) { const shareAction = sharing.shareProductActionFromData(objectGraph, data, metricsOptions.pageInformation, metricsOptions.locationTracker); if (serverData.isDefinedNonNull(shareAction)) { shareAction.title = objectGraph.loc.string("FLOW_PREVIEW_ACTION_SHARE"); shareAction.artwork = contentArtwork.createArtworkForResource(objectGraph, "systemimage://square.and.arrow.up"); return shareAction; } return shareAction; } /** * Creates and returns an action for navigating to a product's ratings & reviews * @param data The response data to read from. * @param clickAction The flow action for viewing the product * @returns An action or null */ function seeRatingsAndReviewsActionFromData(objectGraph, data, clickAction) { const seeRatingsAndReviewsAction = reviews.seeRatingsAndReviewsActionFromClickAction(objectGraph, data.id, clickAction); if (serverData.isDefinedNonNull(seeRatingsAndReviewsAction)) { seeRatingsAndReviewsAction.title = objectGraph.loc.string("FLOW_PREVIEW_ACTION_SEE_RATINGS_AND_REVIEWS"); seeRatingsAndReviewsAction.artwork = contentArtwork.createArtworkForResource(objectGraph, "systemimage://star"); seeRatingsAndReviewsAction.animationBehavior = "never"; } return seeRatingsAndReviewsAction; } /** * Creates and returns an action for writing a product review * @param data The response data to read from. * @returns An action or null */ function writeReviewActionFromData(objectGraph, data, lockupSubtitle, lockupTitle) { const writeReviewAction = reviews.writeReviewActionFromData(objectGraph, data, lockupSubtitle, lockupTitle); if (serverData.isDefinedNonNull(writeReviewAction)) { writeReviewAction.title = objectGraph.loc.string("FLOW_PREVIEW_ACTION_WRITE_REVIEW"); writeReviewAction.artwork = contentArtwork.createArtworkForResource(objectGraph, "systemimage://square.and.pencil"); } return writeReviewAction; } /** * Create and returns an action for voting whether a review was helpful or not * @param data The response data to read from. * @param deviceId The UUID for the user's device. * @param helpful Whether or not the vote is indicating the review was helpful, or not helpful. * @returns A new vote action. */ export function voteActionFromData(objectGraph, data, deviceId, helpful) { const baseVoteUrl = objectGraph.bag.voteUrl; const reviewId = serverData.asString(data, "id", "coercible"); const voteUrl = new urls.URL(baseVoteUrl).param("userReviewId", reviewId); if (objectGraph.client.isVision) { // We only attach this parameter for visionOS, as the other platforms have legacy handling in place // that we do not want to interrupt. voteUrl.param("version", "2"); } const action = new models.HttpAction(voteUrl.build()); const successToast = new models.AlertAction("toast"); if (helpful) { action.title = objectGraph.loc.string("ACTION_REVIEW_HELPFUL"); successToast.title = objectGraph.loc.string("TOAST_HELPFUL_TITLE"); successToast.message = objectGraph.loc.string("TOAST_HELPFUL_DESCRIPTION"); action.artwork = contentArtwork.createArtworkForResource(objectGraph, "systemimage://hand.thumbsup"); successToast.artwork = contentArtwork.createArtworkForResource(objectGraph, objectGraph.client.isVision ? "systemimage://hand.thumbsup.fill" : "systemimage://hand.thumbsup"); } else { action.title = objectGraph.loc.string("ACTION_REVIEW_NOT_HELPFUL"); successToast.title = objectGraph.loc.string("TOAST_NOT_HELPFUL_TITLE"); successToast.message = objectGraph.loc.string("TOAST_NOT_HELPFUL_DESCRIPTION"); action.artwork = contentArtwork.createArtworkForResource(objectGraph, "systemimage://hand.thumbsdown"); successToast.artwork = contentArtwork.createArtworkForResource(objectGraph, objectGraph.client.isVision ? "systemimage://hand.thumbsdown.fill" : "systemimage://hand.thumbsdown"); } action.method = "POST"; action.isStoreRequest = true; action.disableCache = true; action.headers = { "Content-Type": http.FormBuilder.contentType }; action.body = new http.FormBuilder() .param("vote", helpful ? "1" : "0") .param("guid", deviceId) .build(); action.successAction = successToast; return action; } /** * Creates a report a concern action for a given review * @param data The response data to read from. * @param deviceId The UUID for the user's device. * @returns A report concern action */ export function reportConcernActionFromData(objectGraph, data, deviceId) { return validation.context("reportConcernActionFromApiRow", () => { const reviewId = serverData.asString(data, "id", "coercible"); const reportConcernUrl = objectGraph.bag.reportConcernUrl; const sendAction = new models.HttpTemplateAction(reportConcernUrl); sendAction.method = "POST"; sendAction.isStoreRequest = true; sendAction.disableCache = true; sendAction.needsAuthentication = true; sendAction.headers = { "Content-Type": http.FormBuilder.contentType }; sendAction.body = new http.FormBuilder().param("userReviewId", reviewId).param("guid", deviceId).build(); const reasonParameter = new models.HttpTemplateParameter("selectedReason", "formBody", "decimalPad"); const explanationParameter = new models.HttpTemplateParameter("explanation", "formBody", "text"); sendAction.parameters = [reasonParameter, explanationParameter]; if (!objectGraph.client.isVision) { const successToast = new models.AlertAction("toast"); successToast.title = objectGraph.loc.string("TOAST_CONCERN_REPORTED_TITLE"); successToast.message = objectGraph.loc.string("TOAST_CONCERN_REPORTED_DESCRIPTION"); successToast.artwork = contentArtwork.createArtworkForResource(objectGraph, "systemimage://exclamationmark.circle"); sendAction.successAction = successToast; } const failureAlert = new models.AlertAction("default"); failureAlert.title = objectGraph.loc.string("Alert.GenericError.Title"); failureAlert.message = objectGraph.loc.string("Alert.GenericError.Message"); failureAlert.isCancelable = true; sendAction.failureAction = failureAlert; let concernReasons = objectGraph.bag.reportConcernReasons; if (serverData.isNullOrEmpty(concernReasons)) { // Not available until 18G concernReasons = [ { reasonId: "1", name: "It contains offensive material", upperCaseName: "IT CONTAINS OFFENSIVE MATERIAL", }, { reasonId: "8", name: "It's off-topic", upperCaseName: "IT\u2019S OFF-TOPIC", }, { reasonId: "111003", name: "It looks like spam", upperCaseName: "IT LOOKS LIKE SPAM", }, { reasonId: "7", name: "Something else", upperCaseName: "SOMETHING ELSE", }, ]; } const reasons = concernReasons.map((reasonData) => { return new base.ReportConcernReason(serverData.asString(reasonData, "reasonId"), serverData.asString(reasonData, "name"), serverData.asString(reasonData, "upperCaseName")); }); const action = new models.ReportConcernAction(reasons); action.title = objectGraph.loc.string("ACTION_REVIEW_REPORT"); action.explanation = objectGraph.bag.reportConcernExplanation; if (serverData.isNullOrEmpty(action.explanation)) { // Not available until 18G action.explanation = "Tell us a little more (Optional)"; } action.artwork = contentArtwork.createArtworkForResource(objectGraph, "systemimage://exclamationmark.circle"); action.sendAction = sendAction; return action; }); } export function copyReviewAction(objectGraph, reviewText) { const copyTextAction = new models.CopyTextAction(reviewText); copyTextAction.title = objectGraph.loc.string("ACTION_REVIEW_COPY"); copyTextAction.artwork = contentArtwork.createArtworkForResource(objectGraph, "systemimage://doc.on.doc"); return copyTextAction; } /** * Creates an action for filing a radar against a Review Summary * @param objectGraph Current object graph * @param adamId The ID of the app * @param appName The name of the app * @param reviewSummaryId The ID of the review summary * @param reviewSummaryText The text of the review summary * @returns An action that will deep link to Radar */ function fileReviewSummaryRadarAction(objectGraph, adamId, appName, reviewSummaryId, reviewSummaryText) { if (!["debug", "internal"].includes(objectGraph.client.buildType)) { return null; } const componentId = "999915"; // ASE App Store | Recommendations const title = `Review Summary Feedback: ${appName}`; const description = `App ID: ${adamId}\nApp name: ${appName}\nReview summary ID: ${reviewSummaryId}\nReview summary: ${reviewSummaryText}\n\nFeedback: `; const url = `tap-to-radar://new/problem?componentid=${componentId}&title=${title}&description=${description}`; const action = new models.ExternalUrlAction(url, true); action.title = objectGraph.loc.string("Action.ProvideFeedback"); action.artwork = contentArtwork.createArtworkForResource(objectGraph, "systemimage://ant.circle"); return action; } //# sourceMappingURL=flow-preview.js.map