Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Monthly prices #3187

Merged
merged 1 commit into from
Jul 1, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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