// // offer-formatting.ts // AppStoreKit // // Created by Dersu Abolfathi on 8/8/19. // Copyright (c) 2019 Apple Inc. All rights reserved. // import { isNothing } from "@jet/environment"; import * as serverData from "../../foundation/json-parsing/server-data"; export class SubscriptionRecurrence { constructor(periodDuration, periodCount, type) { this.periodDuration = periodDuration; this.periodCount = periodCount; this.type = type; } isEqualTo(otherRecurrence) { return (otherRecurrence.periodDuration === this.periodDuration && otherRecurrence.periodCount === this.periodCount && otherRecurrence.type === this.type); } } const DAYS_PER_WEEK = 7; /** * Determines the recurrence type for the server-vended subscription recurrence string. * @param serverRecurrence The string denoting the recurring subscription recurrence (i.e. 'P1M', 'P1Y', etc.). * @param numberOfPeriods - The number of periods for the subscription. * @returns {SubscriptionRecurrence} The recurrence value. */ export function subscriptionRecurrenceForServerRecurrence(objectGraph, serverRecurrence, numberOfPeriods) { if (isNothing(serverRecurrence)) { return null; } const match = serverRecurrence.match(/P(\d+)([A-Z]+)/); if (!match || match.length !== 3) { return null; } let periodDuration = serverData.asNumber(match[1]); let type = match[2]; if (!periodDuration || !type) { return null; } // Unfortunately, the server will give us weekly recurrences in days. So, we need to transform a days-based // recurrence that lands on week boundaries to a week-based recurrence. if (type === "D" && periodDuration > 0 && periodDuration % DAYS_PER_WEEK === 0) { type = "W"; periodDuration = periodDuration / DAYS_PER_WEEK; } return new SubscriptionRecurrence(periodDuration, numberOfPeriods !== null && numberOfPeriods !== void 0 ? numberOfPeriods : 1, type); } // endregion // region IAP Subscription Trials /** * The pre-install description for a subscription trial offer. * @param inAppOfferData The overall data for the subscription, including the discount. * @returns {string} The fully-localized formatted string for the subscription trial description. */ export function installPagePreInstallTrialDescription(objectGraph, inAppOfferData) { const discountData = serverData.asArrayOrEmpty(inAppOfferData, "discounts")[0]; if (!discountData) { return null; } const trialRecurrencePeriod = serverData.asString(discountData, "recurringSubscriptionPeriod"); const trialNumberOfPeriods = serverData.asNumber(discountData, "numOfPeriods"); const postTrialRecurrencePeriod = serverData.asString(inAppOfferData, "recurringSubscriptionPeriod"); const postTrialNumberOfPeriods = serverData.asNumber(inAppOfferData, "numOfPeriods"); if (!trialRecurrencePeriod || !postTrialRecurrencePeriod) { return null; } const trialType = serverData.asString(discountData, "modeType"); const trialRecurrence = subscriptionRecurrenceForServerRecurrence(objectGraph, trialRecurrencePeriod, trialNumberOfPeriods); const postTrialRecurrence = subscriptionRecurrenceForServerRecurrence(objectGraph, postTrialRecurrencePeriod, postTrialNumberOfPeriods); // Replace any spaces within the formatted prices with non-breaking spaces to avoid prices breaking across multiple lines const trialPriceFormatted = serverData.asString(discountData, "priceFormatted").replace(/ /g, "\u00a0"); const postTrialPriceFormatted = serverData.asString(inAppOfferData, "priceFormatted").replace(/ /g, "\u00a0"); let postTrialPriceDuration = priceDurationString(objectGraph, postTrialRecurrence.type, postTrialRecurrence.periodDuration, postTrialPriceFormatted); // Update any slash (/) characters to be non-breaking by wrapping with u2060 word-joiner unicode characters. postTrialPriceDuration = postTrialPriceDuration.replace(/\//g, "\u2060/\u2060"); switch (trialType) { case "FreeTrial": // template: "Free for @@durationCount@@, then @@postTrialPriceDuration@@ after trial." // result: "Free for 6 months, then $4.99/month after trial." const freeTrialDurationCount = durationCountString(objectGraph, trialRecurrence.type, trialRecurrence.periodDuration * trialRecurrence.periodCount); if (freeTrialDurationCount && postTrialPriceDuration) { return objectGraph.loc .string("InAppOfferPage.Description.FreeTrialTemplate") .replace("@@durationCount@@", tokenReplacer(freeTrialDurationCount)) .replace("@@postTrialPriceDuration@@", tokenReplacer(postTrialPriceDuration)); } break; case "PayUpFront": // template: "@@durationCount@@ for @@trialPrice@@, then @@postTrialPriceDuration@@ after trial." // result: "3 months for $9.99, then $19.99/year after trial." const payUpFrontDurationCount = durationCountString(objectGraph, trialRecurrence.type, trialRecurrence.periodDuration * trialRecurrence.periodCount); if (payUpFrontDurationCount && postTrialPriceDuration) { return objectGraph.loc .string("InAppOfferPage.Description.PaidUpFrontTemplate") .replace("@@durationCount@@", tokenReplacer(payUpFrontDurationCount)) .replace("@@trialPrice@@", tokenReplacer(trialPriceFormatted)) .replace("@@postTrialPriceDuration@@", tokenReplacer(postTrialPriceDuration)); } break; case "PayAsYouGo": // template: "@@trialPriceDuration@@ for @@durationCount@@, then @@postTrialPriceDuration@@ after trial." // result: "$1.99/week for 3 weeks, then $9.99/month after trial." const trialPriceDuration = priceDurationString(objectGraph, trialRecurrence.type, trialRecurrence.periodDuration, trialPriceFormatted); const durationCount = durationCountString(objectGraph, trialRecurrence.type, trialRecurrence.periodDuration * trialRecurrence.periodCount); if (durationCount && postTrialPriceDuration) { return objectGraph.loc .string("InAppOfferPage.Description.PaidTrialTemplate") .replace("@@trialPriceDuration@@", tokenReplacer(trialPriceDuration)) .replace("@@durationCount@@", tokenReplacer(durationCount)) .replace("@@postTrialPriceDuration@@", tokenReplacer(postTrialPriceDuration)); } break; default: return null; } return null; } // Arcade grouping upsell text showing pricing token // We use a function replacer instead of a string replacer, since `$` is interpreted as a special substitution token. // Function replacers have less special semantics. function tokenReplacer(replacementString) { return replacementString; } /** * Returns a string representing a formatted price per duration of time. * e.g. "$3.99/day" if the recurrenceType is `days` and durationCount is `1`. * e.g. "$3.99 every 3 months" the recurrenceType is `months` and durationCount is `3`. * @param recurrenceType: The recurrence type - e.g. days, weeks, months, years. * @param durationCount The count of the duration that will be represented in the string. * @param formattedPrice The formatted price that will be substituted in the returned string. * @returns {string} The localized string for the price per duration. */ export function priceDurationString(objectGraph, recurrenceType, durationCount, formattedPrice) { // template: "@@price@@ every @@count@@ days" // result: "$1.99 every 3 days" let template; switch (recurrenceType) { // NOTE: Below we have added a workaround to enforce use of the .one plural string variation when count is exactly 1, as jet localizer doesn't do this by default for all languages. // In rdar://113586253 we will adopt newer API from jet that allows us to do this globally, and then this workaround can be removed. case "D": if (durationCount === 1) { template = objectGraph.loc .string("InAppOfferPage.Description.PriceDuration.Days.one") .replace("@@count@@", objectGraph.loc.formattedCount(durationCount)); } else { template = objectGraph.loc.stringWithCount("InAppOfferPage.Description.PriceDuration.Days", durationCount); } break; case "W": if (durationCount === 1) { template = objectGraph.loc .string("InAppOfferPage.Description.PriceDuration.Weeks.one") .replace("@@count@@", objectGraph.loc.formattedCount(durationCount)); } else { template = objectGraph.loc.stringWithCount("InAppOfferPage.Description.PriceDuration.Weeks", durationCount); } break; case "M": if (durationCount === 1) { template = objectGraph.loc .string("InAppOfferPage.Description.PriceDuration.Months.one") .replace("@@count@@", objectGraph.loc.formattedCount(durationCount)); } else { template = objectGraph.loc.stringWithCount("InAppOfferPage.Description.PriceDuration.Months", durationCount); } break; case "Y": if (durationCount === 1) { template = objectGraph.loc .string("InAppOfferPage.Description.PriceDuration.Years.one") .replace("@@count@@", objectGraph.loc.formattedCount(durationCount)); } else { template = objectGraph.loc.stringWithCount("InAppOfferPage.Description.PriceDuration.Years", durationCount); } break; default: break; } return template.replace("@@price@@", tokenReplacer(formattedPrice)); } /** * Returns a string representing the duration count of the specified recurrence type. * e.g. "3 days" if recurrenceType is `days` and durationCount is `3`. * @param recurrenceType: The recurrence type - e.g. days, weeks, months, years. * @param durationCount The count of the duration that will be represented in the string. * @returns {string} The localized duration count string. */ export function durationCountString(objectGraph, recurrenceType, durationCount) { switch (recurrenceType) { // NOTE: Below we have added a workaround to enforce use of the .one plural string variation when count is exactly 1, as jet localizer doesn't do this by default for all languages. // In rdar://113586253 we will adopt newer API from jet that allows us to do this globally, and then this workaround can be removed. case "D": if (durationCount === 1) { return objectGraph.loc .string("InAppOfferPage.Description.DurationCount.Days.one") .replace("@@count@@", objectGraph.loc.formattedCount(durationCount)); } return objectGraph.loc.stringWithCount("InAppOfferPage.Description.DurationCount.Days", durationCount); case "W": if (durationCount === 1) { return objectGraph.loc .string("InAppOfferPage.Description.DurationCount.Weeks.one") .replace("@@count@@", objectGraph.loc.formattedCount(durationCount)); } return objectGraph.loc.stringWithCount("InAppOfferPage.Description.DurationCount.Weeks", durationCount); case "M": if (durationCount === 1) { return objectGraph.loc .string("InAppOfferPage.Description.DurationCount.Months.one") .replace("@@count@@", objectGraph.loc.formattedCount(durationCount)); } return objectGraph.loc.stringWithCount("InAppOfferPage.Description.DurationCount.Months", durationCount); case "Y": if (durationCount === 1) { return objectGraph.loc .string("InAppOfferPage.Description.DurationCount.Years.one") .replace("@@count@@", objectGraph.loc.formattedCount(durationCount)); } return objectGraph.loc.stringWithCount("InAppOfferPage.Description.DurationCount.Years", durationCount); default: break; } return null; } //# sourceMappingURL=offer-formatting.js.map