Skip to content

Commit

Permalink
Show monthly prices at signup, close #3087
Browse files Browse the repository at this point in the history
Co-authored-by: bedhub
Co-authored-by: tih-tutao
  • Loading branch information
vitoreiji committed Jul 1, 2021
1 parent 226cee5 commit 4599d52
Show file tree
Hide file tree
Showing 12 changed files with 247 additions and 86 deletions.
4 changes: 3 additions & 1 deletion src/misc/TranslationKey.js
Original file line number Diff line number Diff line change
Expand Up @@ -1377,4 +1377,6 @@ export type TranslationKeyType = "about_label"
| "you_label"
| "emptyString_msg"
| "draftSaved_msg"
| "draftNotSaved_msg"
| "draftNotSaved_msg"
| "pricing.perMonthPaidYearly_label"
| "pricing.businessTemplates_msg"
38 changes: 23 additions & 15 deletions src/subscription/BuyOptionBox.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import type {SegmentControlItem} from "../gui/base/SegmentControl"
import {SegmentControl} from "../gui/base/SegmentControl"
import type {ButtonAttrs} from "../gui/base/ButtonN"
import {ButtonN} from "../gui/base/ButtonN"
import type {WorkerClient} from "../api/main/WorkerClient"
import type {BookingItemFeatureTypeEnum} from "../api/common/TutanotaConstants"
import {formatMonthlyPrice, getCountFromPriceData, getPriceFromPriceData, isYearlyPayment} from "./PriceUtils"

const PaymentIntervalItems: SegmentControlItem<number>[] = [
{name: lang.get("pricing.yearly_label"), value: 12},
Expand All @@ -23,7 +26,7 @@ export type BuyOptionBoxAttr = {|
// that doesn't occur when you pass in the attrs
actionButton: ?(Component | lazy<ButtonAttrs>),
price?: string,
originalPrice: string,
priceHint?: TranslationKey | lazy<string>,
helpLabel: TranslationKey | lazy<string>,
features: () => string[],
width: number,
Expand Down Expand Up @@ -64,7 +67,7 @@ export class BuyOptionBox implements MComponent<BuyOptionBoxAttr> {
'border-radius': '3px'
}
}, [
(vnode.attrs.showReferenceDiscount && vnode.attrs.price !== vnode.attrs.originalPrice)
(vnode.attrs.paymentInterval ? isYearlyPayment(vnode.attrs.paymentInterval()) : null)
? m(".ribbon-vertical", m(".text-center.b.h4", {style: {'padding-top': px(22)}}, "%"))
: null,
m(".h4.text-center.dialog-header.dialog-header-line-height.flex.col.center-horizontally", {
Expand All @@ -77,20 +80,8 @@ export class BuyOptionBox implements MComponent<BuyOptionBoxAttr> {
}, vnode.attrs.heading),
m(".text-center.pt.flex.center-vertically.center-horizontally", [
vnode.attrs.price ? m("span.h1", vnode.attrs.price) : null,
(vnode.attrs.showReferenceDiscount && vnode.attrs.price !== vnode.attrs.originalPrice)
? [
// This element is for the screen reader because they tend to not announce strikethrough.
m("span", {
style: {
opacity: "0",
width: "0",
height: "0",
},
}, lang.get("originalPrice_label") + ": "),
m("s.pl", "(" + vnode.attrs.originalPrice + ")"),
]
: null
]),
m(".small.text-center", vnode.attrs.priceHint ? lang.getMaybeLazy(vnode.attrs.priceHint) : lang.get("emptyString_msg")),
m(".small.text-center.pb-s", lang.getMaybeLazy(vnode.attrs.helpLabel)),
(vnode.attrs.paymentInterval) ? m(SegmentControl, {
selectedValue: vnode.attrs.paymentInterval,
Expand Down Expand Up @@ -127,3 +118,20 @@ export class BuyOptionBox implements MComponent<BuyOptionBoxAttr> {
}
}

/**
* Loads the price information for the given feature type/amount and updates the price information on the BuyOptionBox.
*/
export async function updateBuyOptionBoxPriceInformation(worker: WorkerClient, featureType: BookingItemFeatureTypeEnum, amount: number, attrs: BuyOptionBoxAttr): Promise<void> {
const newPrice = await worker.getPrice(featureType, amount, false)
if (amount === getCountFromPriceData(newPrice.currentPriceNextPeriod, featureType)) {
attrs.actionButton = getActiveSubscriptionActionButtonReplacement()
}
const futurePrice = newPrice.futurePriceNextPeriod
if (futurePrice) {
const paymentInterval = Number(futurePrice.paymentInterval)
const price = getPriceFromPriceData(futurePrice, featureType)
attrs.price = formatMonthlyPrice(price, paymentInterval)
attrs.helpLabel = isYearlyPayment(paymentInterval) ? "pricing.perMonthPaidYearly_label" : "pricing.perMonth_label"
m.redraw()
}
}
16 changes: 2 additions & 14 deletions src/subscription/EmailAliasOptionsDialog.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,9 @@ import m from "mithril"
import {lang} from "../misc/LanguageViewModel"
import {BookingItemFeatureType} from "../api/common/TutanotaConstants"
import type {BuyOptionBoxAttr} from "./BuyOptionBox"
import {BuyOptionBox, getActiveSubscriptionActionButtonReplacement} from "./BuyOptionBox"
import {BuyOptionBox, updateBuyOptionBoxPriceInformation} from "./BuyOptionBox"
import {load} from "../api/main/Entity"
import {worker} from "../api/main/WorkerClient"
import {formatPrice, getCountFromPriceData, getPriceFromPriceData} from "./PriceUtils"
import {neverNull} from "../api/common/utils/Utils"
import {CustomerTypeRef} from "../api/entities/sys/Customer"
import {CustomerInfoTypeRef} from "../api/entities/sys/CustomerInfo"
Expand Down Expand Up @@ -65,7 +64,6 @@ function createEmailAliasPackageBox(amount: number, freeAmount: number, buyActio
}
},
price: lang.get("emptyString_msg"),
originalPrice: lang.get("emptyString_msg"),
helpLabel: "emptyString_msg",
features: () => [],
width: 230,
Expand All @@ -74,16 +72,6 @@ function createEmailAliasPackageBox(amount: number, freeAmount: number, buyActio
showReferenceDiscount: false
}

worker.getPrice(BookingItemFeatureType.Alias, amount, false).then(newPrice => {
if (amount === getCountFromPriceData(newPrice.currentPriceNextPeriod, BookingItemFeatureType.Alias)) {
attrs.actionButton = getActiveSubscriptionActionButtonReplacement()
}
let price = formatPrice(getPriceFromPriceData(newPrice.futurePriceNextPeriod, BookingItemFeatureType.Alias), true)
attrs.price = price
attrs.originalPrice = price
attrs.helpLabel = (neverNull(newPrice.futurePriceNextPeriod).paymentInterval
=== "12") ? "pricing.perYear_label" : "pricing.perMonth_label"
m.redraw()
})
updateBuyOptionBoxPriceInformation(worker, BookingItemFeatureType.Alias, amount, attrs)
return {amount, buyOptionBoxAttr: attrs}
}
29 changes: 24 additions & 5 deletions src/subscription/PriceUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ export function formatPrice(value: number, includeCurrency: boolean): string {
}
}

/**
* Return actual price for given subscription data. In case of yearly subscription, the yearly value is returned, and monthly otherwise.
*/
export function getSubscriptionPrice(data: SubscriptionData, subscription: SubscriptionTypeEnum, type: UpgradePriceTypeEnum): number {
const prices = getPlanPrices(data.planPrices, subscription)
if (prices) {
Expand Down Expand Up @@ -84,14 +87,30 @@ export function getSubscriptionPrice(data: SubscriptionData, subscription: Subsc
}
}

export function getFormattedSubscriptionPrice(attrs: SubscriptionData, subscription: SubscriptionTypeEnum, type: UpgradePriceTypeEnum): string {
return formatPrice(getSubscriptionPrice(attrs, subscription, type), true)
/**
* Formats the monthly price of the subscription (even for yearly subscriptions).
*/
export function formatMonthlyPrice(subscriptionPrice: number, paymentInterval: number): string {
const monthlyPrice = isYearlyPayment(paymentInterval) ? subscriptionPrice / 12 : subscriptionPrice
return formatPrice(monthlyPrice, true)
}

export function isYearlyPayment(periods: number): boolean {
return periods === 12
}

export function formatPriceWithInfo(price: number, paymentInterval: number, taxIncluded: boolean): string {
const netOrGross = taxIncluded ? lang.get("gross_label") : lang.get("net_label")
const yearlyOrMonthly = paymentInterval === 12 ? lang.get("pricing.perYear_label") : lang.get("pricing.perMonth_label")
return formatPrice(price, true) + " " + yearlyOrMonthly + " (" + netOrGross + ")"
const netOrGross = taxIncluded
? lang.get("gross_label")
: lang.get("net_label")

const yearlyOrMonthly = isYearlyPayment(paymentInterval)
? lang.get("pricing.perYear_label")
: lang.get("pricing.perMonth_label")

const formattedPrice = formatPrice(price, true)

return `${formattedPrice} ${yearlyOrMonthly} (${netOrGross})`
}

/**
Expand Down
19 changes: 3 additions & 16 deletions src/subscription/StorageCapacityOptionsDialog.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,11 @@ import type {TranslationKey} from "../misc/LanguageViewModel"
import {lang} from "../misc/LanguageViewModel"
import {BookingItemFeatureType, Keys} from "../api/common/TutanotaConstants"
import type {BuyOptionBoxAttr} from "./BuyOptionBox"
import {BuyOptionBox, getActiveSubscriptionActionButtonReplacement} from "./BuyOptionBox"
import {BuyOptionBox, updateBuyOptionBoxPriceInformation} from "./BuyOptionBox"
import {load} from "../api/main/Entity"
import {worker} from "../api/main/WorkerClient"
import {formatPrice, getCountFromPriceData, getPriceFromPriceData} from "./PriceUtils"
import {neverNull} from "../api/common/utils/Utils"
import {buyStorage} from "../subscription/SubscriptionUtils"
import {buyStorage} from "./SubscriptionUtils"
import {CustomerTypeRef} from "../api/entities/sys/Customer"
import {CustomerInfoTypeRef} from "../api/entities/sys/CustomerInfo"
import {logins} from "../api/main/LoginController"
Expand Down Expand Up @@ -84,26 +83,14 @@ function createStorageCapacityBoxAttr(amount: number, freeAmount: number, buyAct
}
},
price: lang.get("emptyString_msg"),
originalPrice: lang.get("emptyString_msg"),
helpLabel: "emptyString_msg",
features: () => [],
width: 230,
height: 210,
paymentInterval: null,
showReferenceDiscount: false
}

worker.getPrice(BookingItemFeatureType.Storage, amount, false).then(newPrice => {
if (amount === getCountFromPriceData(newPrice.currentPriceNextPeriod, BookingItemFeatureType.Storage)) {
attrs.actionButton = getActiveSubscriptionActionButtonReplacement()
}
let price = formatPrice(getPriceFromPriceData(newPrice.futurePriceNextPeriod, BookingItemFeatureType.Storage), true)
attrs.price = price
attrs.originalPrice = price
attrs.helpLabel = (neverNull(newPrice.futurePriceNextPeriod).paymentInterval
=== "12") ? "pricing.perYear_label" : "pricing.perMonth_label"
m.redraw()
})
updateBuyOptionBoxPriceInformation(worker, BookingItemFeatureType.Storage, amount, attrs)
return {amount, buyOptionBoxAttr: attrs}
}

Expand Down
34 changes: 24 additions & 10 deletions src/subscription/SubscriptionSelector.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
} from "./SubscriptionUtils"
import type {SegmentControlItem} from "../gui/base/SegmentControl"
import {SegmentControl} from "../gui/base/SegmentControl"
import {formatPrice, getFormattedSubscriptionPrice} from "./PriceUtils"
import {formatMonthlyPrice, formatPrice, getSubscriptionPrice, isYearlyPayment} from "./PriceUtils"

const BusinessUseItems: SegmentControlItem<boolean>[] = [
{name: lang.get("pricing.privateUse_label"), value: false},
Expand Down Expand Up @@ -89,9 +89,9 @@ export class SubscriptionSelector implements MComponent<SubscriptionSelectorAttr

_getCurrentPlanInfo(selectorAttrs: SubscriptionSelectorAttr): ?string {
if (selectorAttrs.options.businessUse() && selectorAttrs.currentSubscriptionType && !selectorAttrs.currentlyBusinessOrdered) {
const price = getFormattedSubscriptionPrice(selectorAttrs, selectorAttrs.currentSubscriptionType, UpgradePriceType.PlanActualPrice)
const price = getSubscriptionPrice(selectorAttrs, selectorAttrs.currentSubscriptionType, UpgradePriceType.PlanActualPrice)
return lang.get("businessCustomerNeedsBusinessFeaturePlan_msg", {
"{price}": price,
"{price}": formatMonthlyPrice(price, selectorAttrs.options.paymentInterval()),
"{plan}": selectorAttrs.currentSubscriptionType
})
+ " " + lang.get("businessCustomerAutoBusinessFeature_msg")
Expand All @@ -106,7 +106,6 @@ export class SubscriptionSelector implements MComponent<SubscriptionSelectorAttr
? getActiveSubscriptionActionButtonReplacement()
: selectorAttrs.actionButtons.Free,
price: formatPrice(0, true),
originalPrice: formatPrice(0, true),
helpLabel: "pricing.upgradeLater_msg",
features: () => [
lang.get("pricing.comparisonUsersFree_msg"),
Expand All @@ -132,9 +131,11 @@ export class SubscriptionSelector implements MComponent<SubscriptionSelectorAttr
showAdditionallyBookedFeatures = !isDowngrade(targetSubscription, selectorAttrs.currentSubscriptionType)
}
const targetSubscriptionConfig = subscriptions[targetSubscription]

const additionalUserPrice = getSubscriptionPrice(selectorAttrs, targetSubscription, UpgradePriceType.AdditionalUserPrice)
const premiumFeatures = [
lang.get("pricing.comparisonAddUser_msg", {"{1}": getFormattedSubscriptionPrice(selectorAttrs, targetSubscription, UpgradePriceType.AdditionalUserPrice)}),
lang.get("pricing.comparisonAddUser_msg", {
"{1}": formatMonthlyPrice(additionalUserPrice, selectorAttrs.options.paymentInterval())
}),
lang.get("pricing.comparisonStorage_msg", {"{amount}": planPrices.includedStorage}),
lang.get(targetSubscriptionConfig.business || (selectorAttrs.currentlyBusinessOrdered && showAdditionallyBookedFeatures)
? "pricing.comparisonDomainBusiness_msg"
Expand All @@ -150,12 +151,15 @@ export class SubscriptionSelector implements MComponent<SubscriptionSelectorAttr
const sharingFeature = [lang.get("pricing.comparisonSharingCalendar_msg")]
const businessFeatures = [
lang.get("pricing.comparisonOutOfOffice_msg"),
lang.get("pricing.comparisonEventInvites_msg")
lang.get("pricing.comparisonEventInvites_msg"),
lang.get("pricing.businessTemplates_msg")
]

const contactFormPrice = getSubscriptionPrice(selectorAttrs, targetSubscription, UpgradePriceType.ContactFormPrice)
const whitelabelFeatures = [
lang.get("pricing.comparisonLoginPro_msg"),
lang.get("pricing.comparisonThemePro_msg"),
lang.get("pricing.comparisonContactFormPro_msg", {"{price}": getFormattedSubscriptionPrice(selectorAttrs, targetSubscription, UpgradePriceType.ContactFormPrice)})
lang.get("pricing.comparisonContactFormPro_msg", {"{price}": formatMonthlyPrice(contactFormPrice, selectorAttrs.options.paymentInterval())})
]
const featuresToBeOrdered = premiumFeatures
.concat(targetSubscriptionConfig.business || (showAdditionallyBookedFeatures
Expand All @@ -170,13 +174,15 @@ export class SubscriptionSelector implements MComponent<SubscriptionSelectorAttr
&& !selectorAttrs.options.businessUse()
&& (!selectorAttrs.currentSubscriptionType || selectorAttrs.currentSubscriptionType === SubscriptionType.Free)

const subscriptionPrice = getSubscriptionPrice(selectorAttrs, targetSubscription, UpgradePriceType.PlanActualPrice)
const formattedMonthlyPrice = formatMonthlyPrice(subscriptionPrice, selectorAttrs.options.paymentInterval())
return {
heading: getDisplayNameOfSubscriptionType(targetSubscription),
actionButton: selectorAttrs.currentSubscriptionType === targetSubscription
? getActiveSubscriptionActionButtonReplacement()
: getActionButtonBySubscription(selectorAttrs.actionButtons, targetSubscription),
price: getFormattedSubscriptionPrice(selectorAttrs, targetSubscription, UpgradePriceType.PlanActualPrice),
originalPrice: getFormattedSubscriptionPrice(selectorAttrs, targetSubscription, UpgradePriceType.PlanReferencePrice),
price: formattedMonthlyPrice,
priceHint: getPriceHint(subscriptionPrice, selectorAttrs.options.paymentInterval()),
helpLabel: selectorAttrs.options.businessUse() ? "pricing.basePriceExcludesTaxes_msg" : "pricing.basePriceIncludesTaxes_msg",
features: () => featuresToBeOrdered,
width: selectorAttrs.boxWidth,
Expand All @@ -187,3 +193,11 @@ export class SubscriptionSelector implements MComponent<SubscriptionSelectorAttr
}
}
}

function getPriceHint(subscriptionPrice: number, paymentInterval: number): TranslationKey {
if (subscriptionPrice > 0) {
return isYearlyPayment(paymentInterval) ? "pricing.perMonthPaidYearly_label" : "pricing.perMonth_label"
} else {
return "emptyString_msg"
}
}
39 changes: 16 additions & 23 deletions src/subscription/UpgradeConfirmPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import m from "mithril"
import stream from "mithril/stream/stream.js"
import {Dialog} from "../gui/base/Dialog"
import {lang} from "../misc/LanguageViewModel"
import {formatPrice, getPaymentMethodName} from "./PriceUtils"
import {formatPriceWithInfo, getPaymentMethodName, isYearlyPayment} from "./PriceUtils"
import {HabReminderImage} from "../gui/base/icons/Icons"
import {createSwitchAccountTypeData} from "../api/entities/sys/SwitchAccountTypeData"
import type {PaidSubscriptionTypeEnum} from "../api/common/TutanotaConstants"
Expand All @@ -18,7 +18,7 @@ import {deleteCampaign} from "./UpgradeSubscriptionWizard"
import {BadGatewayError, PreconditionFailedError} from "../api/common/error/RestError"
import {RecoverCodeField} from "../settings/RecoverCodeDialog"
import {logins} from "../api/main/LoginController"
import type {SubscriptionTypeEnum} from "./SubscriptionUtils"
import type {SubscriptionOptions, SubscriptionTypeEnum} from "./SubscriptionUtils"
import {getDisplayNameOfSubscriptionType, getPreconditionFailedPaymentMsg, SubscriptionType, UpgradeType} from "./SubscriptionUtils"
import {ButtonN, ButtonType} from "../gui/base/ButtonN"
import type {WizardPageAttrs, WizardPageN} from "../gui/base/WizardDialogN"
Expand All @@ -29,6 +29,7 @@ import {TextFieldN} from "../gui/base/TextFieldN"
export class UpgradeConfirmPage implements WizardPageN<UpgradeSubscriptionData> {
view(vnode: Vnode<WizardPageAttrs<UpgradeSubscriptionData>>): Children {
const attrs = vnode.attrs
const isYearly = isYearlyPayment(attrs.data.options.paymentInterval())
const newAccountData = attrs.data.newAccountData

const orderFieldAttrs = {
Expand All @@ -37,27 +38,19 @@ export class UpgradeConfirmPage implements WizardPageN<UpgradeSubscriptionData>
disabled: true,
}

const subscription =
(attrs.data.options.paymentInterval() === 12)
? lang.get("pricing.yearly_label")
: lang.get("pricing.monthly_label") + ", " + lang.get("automaticRenewal_label")
const subscription = (isYearly
? lang.get("pricing.yearly_label")
: lang.get("pricing.monthly_label")) + ", " + lang.get("automaticRenewal_label")
const subscriptionFieldAttrs = {
label: "subscriptionPeriod_label",
value: stream(subscription),
disabled: true,
}

const netOrGross = attrs.data.options.businessUse()
? lang.get("net_label")
: lang.get("gross_label")

const price = formatPrice(Number(attrs.data.price), true) + " " + (attrs.data.options.paymentInterval() === 12
? lang.get("pricing.perYear_label")
: lang.get("pricing.perMonth_label")) + " (" + netOrGross + ")"
const priceFieldAttrs = {
label: "priceFirstYear_label",
value: stream(price),
disabeld: true,
label: isYearly ? "priceFirstYear_label" : "price_label",
value: stream(buildPriceString(attrs.data.price, attrs.data.options)),
disabled: true,
}

const paymentMethodFieldAttrs = {
Expand Down Expand Up @@ -118,7 +111,7 @@ export class UpgradeConfirmPage implements WizardPageN<UpgradeSubscriptionData>
m(TextFieldN, orderFieldAttrs),
m(TextFieldN, subscriptionFieldAttrs),
m(TextFieldN, priceFieldAttrs),
attrs.data.priceNextYear ? m(TextFieldN, this._createPriceNextYearFieldAttrs(netOrGross, attrs.data)) : null,
attrs.data.priceNextYear ? m(TextFieldN, this._createPriceNextYearFieldAttrs(attrs.data.priceNextYear, attrs.data.options)) : null,
m(TextFieldN, paymentMethodFieldAttrs),
]),
m(".flex-grow-shrink-half.plr-l.flex-center.items-end",
Expand Down Expand Up @@ -159,19 +152,19 @@ export class UpgradeConfirmPage implements WizardPageN<UpgradeSubscriptionData>
})
}

_createPriceNextYearFieldAttrs(netOrGross: string, data: UpgradeSubscriptionData): TextFieldAttrs {
const priceNextyear = formatPrice(Number(data.priceNextYear), true) + " " + (data.options.paymentInterval() === 12
? lang.get("pricing.perYear_label")
: lang.get("pricing.perMonth_label")) + " (" + netOrGross + ")"

_createPriceNextYearFieldAttrs(price: NumberString, options: SubscriptionOptions): TextFieldAttrs {
return {
label: "priceForNextYear_label",
value: stream(priceNextyear),
value: stream(buildPriceString(price, options)),
disabled: true,
}
}
}

function buildPriceString(price: NumberString, options: SubscriptionOptions): string {
return formatPriceWithInfo(Number(price), options.paymentInterval(), !options.businessUse())
}

export class UpgradeConfirmPageAttrs implements WizardPageAttrs<UpgradeSubscriptionData> {

data: UpgradeSubscriptionData
Expand Down
Loading

0 comments on commit 4599d52

Please sign in to comment.