import * as models from "../../api/models"; 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 appPromotionCommon from "./app-promotions-common"; import * as dateUtil from "../../foundation/util/date-util"; import { isNothing, isSome } from "@jet/environment/types/optional"; import * as offerFormatting from "../offers/offer-formatting"; import * as offers from "../offers/offers"; import * as content from "../content/content"; import * as contentArtwork from "../content/artwork/artwork"; /** * Creates an offer item object. * @param offerItemData The data blob * @param parentAppData The related parent app of this app event. If not provided will be derived from `data`. * @param offerEnvironment The preferred environment for the offer * @param offerStyle The preferred style of the offer * @param baseMetricsOptions The base metrics options * @param allowEndedEvents Whether events in the past are allowed * @param includeLockupClickAction Whether to include the click action for the lockup * @param isArcadePage Whether or not this is presented on the Arcade page * @returns an Offer Item or null */ export function offerItemFromData(objectGraph, offerItemData, parentAppData, offerEnvironment, offerStyle, baseMetricsOptions, includeLockupClickAction, isArcadePage) { var _a, _b, _c, _d, _e, _f; if (offerItemData.type !== "offer-items") { return null; } const kind = mediaAttributes.attributeAsString(offerItemData, "kind"); if (kind !== "resubscription" && kind !== "winback") { // Only resubscriptions are supported return null; } const isArtworkDark = false; const mediaOverlayStyle = isArtworkDark ? "dark" : "light"; // The Offers const discountedIAP = mediaRelationship.relationshipData(objectGraph, offerItemData, "salables"); if (isNothing(discountedIAP)) { return null; } const discountedOffer = serverData.asDictionary(discountedIAP, "meta.discountOffer"); const regularOffer = offers.offerDataFromData(objectGraph, discountedIAP); if (isNothing(discountedOffer)) { return null; } const resolvedParentApp = (_a = parentAppData !== null && parentAppData !== void 0 ? parentAppData : mediaRelationship.relationshipData(objectGraph, offerItemData, "app")) !== null && _a !== void 0 ? _a : mediaRelationship.relationshipData(objectGraph, discountedIAP, "app"); const supportsStreamlinedBuy = mediaAttributes.attributeAsBooleanOrFalse(resolvedParentApp, "supportsStreamlinedBuy"); // Offer Validity const endDateString = mediaAttributes.attributeAsString(offerItemData, "redemptionExpirationDate"); if (isNothing(endDateString) || !isOfferValid(endDateString)) { return null; } const endDate = new Date(endDateString); let formattedExpiryDateShortString = formattedExpiryDate(objectGraph, endDate); if (isNothing(formattedExpiryDateShortString)) { formattedExpiryDateShortString = objectGraph.loc.string("OfferItems.Available.Now"); } // IAP Artwork let iapArtwork = content.iconFromData(objectGraph, discountedIAP, { useCase: 3 /* content.ArtworkUseCase.LockupIconLarge */, withJoeColorPlaceholder: true, overrideTextColorKey: "textColor4", }); /// A fallback artwork for rare cases where no artwork is found. /// This should not happen in production if (serverData.isNullOrEmpty(iapArtwork)) { const blackColor = { type: "rgb", red: 0.0 / 255, green: 0.0 / 255, blue: 0.0 / 255, alpha: 1, }; iapArtwork = contentArtwork.createArtworkForResource(objectGraph, "systemimage://questionmark.circle", 200, 200, blackColor); iapArtwork.style = "iap"; } const subscriptionRecurrence = offerFormatting.subscriptionRecurrenceForServerRecurrence(objectGraph, serverData.asString(discountedOffer, "recurringSubscriptionPeriod"), serverData.asNumber(discountedOffer, "numOfPeriods")); const discountDuration = isSome(subscriptionRecurrence) ? offerFormatting.durationCountString(objectGraph, subscriptionRecurrence.type, subscriptionRecurrence.periodDuration * subscriptionRecurrence.periodCount) : undefined; // Price Strings // Adding \u{2060} (Word Joiner) prevents the string from breaking on the slash character const regularPrice = (_b = formattedPrice(objectGraph, serverData.asString(regularOffer, "recurringSubscriptionPeriod"), serverData.asNumber(regularOffer, "numOfPeriods"), serverData.asString(regularOffer, "priceFormatted"))) === null || _b === void 0 ? void 0 : _b.replace("/", "/\u{2060}"); const discountedPrice = (_c = formattedPrice(objectGraph, serverData.asString(discountedOffer, "recurringSubscriptionPeriod"), serverData.asNumber(discountedOffer, "numOfPeriods"), serverData.asString(discountedOffer, "priceFormatted"))) === null || _c === void 0 ? void 0 : _c.replace("/", "/\u{2060}"); /// A string map of the templated string in an offer /// Fallback to the templated string if the string does not exist const redemptionDateFormat = objectGraph.loc.string("OfferItems.FormattedDate.RedemptionDate.DateFormat"); const templateKeyMap = { "@@redemptionDate@@": objectGraph.loc.formatDate(redemptionDateFormat, endDate), "@@skuName@@": (_d = mediaAttributes.attributeAsString(discountedIAP, "name")) !== null && _d !== void 0 ? _d : "@@skuName@@", "@@discountedPrice@@": discountedPrice !== null && discountedPrice !== void 0 ? discountedPrice : "@@discountedPrice@@", "@@regularPricePerDuration@@": regularPrice !== null && regularPrice !== void 0 ? regularPrice : "@@regularPricePerDuration@@", "@@discountDuration@@": discountDuration !== null && discountDuration !== void 0 ? discountDuration : "@@discountDuration@@", "@@payUpfrontPrice@@": (_e = serverData.asString(discountedOffer, "priceFormatted")) !== null && _e !== void 0 ? _e : "@@payUpfrontPrice@@", }; // Labels const badge = mediaAttributes.attributeAsString(offerItemData, "badge"); if (isNothing(badge)) { return null; } const title = appPromotionCommon.replacingTemplatedKeys(mediaAttributes.attributeAsString(offerItemData, "title"), templateKeyMap); const formattedTitle = new models.Paragraph(title, "text/x-apple-as3-nqml", "appPromotionTitle"); const subtitle = appPromotionCommon.replacingTemplatedKeys(mediaAttributes.attributeAsString(offerItemData, "subtitle"), templateKeyMap); const description = appPromotionCommon.replacingTemplatedKeys((_f = mediaAttributes.attributeAsString(offerItemData, "details")) !== null && _f !== void 0 ? _f : "", templateKeyMap); // Offer Lockup let offerLockup; if (serverData.isDefinedNonNull(resolvedParentApp)) { offerLockup = appPromotionCommon.lockupFromData(objectGraph, offerItemData, resolvedParentApp, "", offerEnvironment, offerStyle, false, baseMetricsOptions, includeLockupClickAction, null, isArcadePage, true); } if (isNothing(offerLockup)) { return null; } const offerItem = new models.OfferItem(null, null, mediaOverlayStyle, supportsStreamlinedBuy, formattedTitle, formattedExpiryDateShortString, subtitle, description, badge, endDate, iapArtwork, offerLockup); offerItem.title = title; // Detail page click action if (serverData.isDefinedNonNull(resolvedParentApp)) { offerItem.clickAction = appPromotionCommon.detailPageClickActionFromData(objectGraph, offerItemData, resolvedParentApp, offerItem, baseMetricsOptions, includeLockupClickAction); } return offerItem; } /** * Creates a readable subscription price * @param recurringSubscriptionPeriod The subscription recurrence in the server format. (eg P1M) * @param numberOfPeriods The number of periods the subscription recurs for. * @param price A price string in a readable format like $3.99. * @returns A formatted string eg $3.99/month */ function formattedPrice(objectGraph, recurringSubscriptionPeriod, numberOfPeriods, price) { const subscriptionRecurrence = offerFormatting.subscriptionRecurrenceForServerRecurrence(objectGraph, recurringSubscriptionPeriod, numberOfPeriods); if (isNothing(subscriptionRecurrence) || isNothing(price)) { return null; } return offerFormatting.priceDurationString(objectGraph, subscriptionRecurrence === null || subscriptionRecurrence === void 0 ? void 0 : subscriptionRecurrence.type, subscriptionRecurrence === null || subscriptionRecurrence === void 0 ? void 0 : subscriptionRecurrence.periodDuration, price); } /** * Checks the validity of a app promotion based on end date * @param endDateString A string representation of the end date * @returns bool of if the offer is valid or not */ function isOfferValid(endDateString) { if (isNothing(endDateString) || serverData.isNullOrEmpty(endDateString)) { return false; } const endDate = new Date(endDateString); if (serverData.isNull(endDate)) { return false; } const todayDate = new Date(); const hasEventEnded = endDate.getTime() <= todayDate.getTime(); if (hasEventEnded) { return false; } return true; } /** * Creates the availability label for a app promotion * @param startDate The start date * @param endDate The end date * @returns string of the format the date should be shown */ function formattedExpiryDate(objectGraph, endDate) { if (isNothing(endDate)) { return null; } const endMidnight = dateUtil.convertLocalDateToLocalMidnight(endDate); const currentDate = new Date(); const daysLeft = dateUtil.numberOfDaysBetween(currentDate, endMidnight); if (isNothing(daysLeft)) { return null; } const isSameDate = dateUtil.areLocalDatesSameDay(currentDate, endDate); if (daysLeft > 90) { return objectGraph.loc.string("OfferItems.Available.Now"); } // Event ends 5+ days from now // Example: EXPIRES DEC 31 if (daysLeft > 5) { const expiryDateFormat = currentDate.getFullYear() !== endDate.getFullYear() ? objectGraph.loc.string("OfferItems.FormattedDate.NextYear.DateFormat") : objectGraph.loc.string("OfferItems.FormattedDate.FiveDaysOrMore.DateFormat"); const expiryDateString = objectGraph.loc.uppercased(objectGraph.loc.formatDate(expiryDateFormat, endDate)); if (isNothing(expiryDateString)) { return null; } return objectGraph.loc .string("OfferItems.FormattedDate.FiveDaysOrMore.Title") .replace("@@date@@", expiryDateString); } // Event ends 2-5 days from now // Example: EXPIRES IN 3 Days if (daysLeft > 1) { const fiveDaysPriorDateFormat = objectGraph.loc .string("OfferItems.FormattedDate.FiveDaysOrLess.Title") .replace("@@count@@", objectGraph.loc.formattedCount(daysLeft)); return fiveDaysPriorDateFormat; } // Event ends 1 day from now // Example: EXPIRES TOMORROW if (daysLeft === 1 && !isSameDate) { return objectGraph.loc.string("OfferItems.FormattedDate.Tomorrow.Title"); } // Event ends 1 day from now // Example: EXPIRES TONIGHT return objectGraph.loc.string("OfferItems.FormattedDate.Today.Title"); } //# sourceMappingURL=offer-item.js.map