Skip to content

Commit

Permalink
Fix the next year price label
Browse files Browse the repository at this point in the history
Co-authored-by: arm <[email protected]>
Co-authored-by: jat <[email protected]>
Co-authored-by: jug <[email protected]>
  • Loading branch information
3 people committed Dec 12, 2024
1 parent e9ed745 commit 354bccb
Show file tree
Hide file tree
Showing 12 changed files with 158 additions and 103 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ import kotlinx.serialization.json.*
@Serializable
data class MobilePlanPrice(
val name: String,
val monthlyPerMonth: String,
val yearlyPerYear: String,
val yearlyPerMonth: String,
val rawMonthlyPerMonth: String,
val rawYearlyPerYear: String,
val rawYearlyPerMonth: String,
val displayMonthlyPerMonth: String,
val displayYearlyPerYear: String,
val displayYearlyPerMonth: String,
)
27 changes: 18 additions & 9 deletions app-ios/TutanotaSharedFramework/GeneratedIpc/MobilePlanPrice.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,26 @@
public struct MobilePlanPrice : Codable {
public init(
name: String,
monthlyPerMonth: String,
yearlyPerYear: String,
yearlyPerMonth: String
rawMonthlyPerMonth: String,
rawYearlyPerYear: String,
rawYearlyPerMonth: String,
displayMonthlyPerMonth: String,
displayYearlyPerYear: String,
displayYearlyPerMonth: String
) {
self.name = name
self.monthlyPerMonth = monthlyPerMonth
self.yearlyPerYear = yearlyPerYear
self.yearlyPerMonth = yearlyPerMonth
self.rawMonthlyPerMonth = rawMonthlyPerMonth
self.rawYearlyPerYear = rawYearlyPerYear
self.rawYearlyPerMonth = rawYearlyPerMonth
self.displayMonthlyPerMonth = displayMonthlyPerMonth
self.displayYearlyPerYear = displayYearlyPerYear
self.displayYearlyPerMonth = displayYearlyPerMonth
}
public let name: String
public let monthlyPerMonth: String
public let yearlyPerYear: String
public let yearlyPerMonth: String
public let rawMonthlyPerMonth: String
public let rawYearlyPerYear: String
public let rawYearlyPerMonth: String
public let displayMonthlyPerMonth: String
public let displayYearlyPerYear: String
public let displayYearlyPerMonth: String
}
91 changes: 58 additions & 33 deletions app-ios/calendar/Sources/Payment/IosMobilePaymentsFacade.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,39 +27,64 @@ public class IosMobilePaymentsFacade: MobilePaymentsFacade {
return currentResult
}

public func getPlanPrices() async throws -> [MobilePlanPrice] {
struct TempMobilePlanPrice {
var monthlyPerMonth: String?
var yearlyPerYear: String?
var yearlyPerMonth: String?
}
let plans: [String] = ALL_PURCHASEABLE_PLANS.flatMap { plan in
[self.formatPlanType(plan, withInterval: 1), self.formatPlanType(plan, withInterval: 12)]
}
let products: [Product] = try await Product.products(for: plans)
var result = [String: TempMobilePlanPrice]()
for product in products {
let productName = String(product.id.split(separator: ".")[2])
var plan = result[productName] ?? TempMobilePlanPrice(monthlyPerMonth: nil, yearlyPerYear: nil, yearlyPerMonth: nil)

let unit = product.subscription!.subscriptionPeriod.unit
switch unit {
case .year:
let formatStyle = product.priceFormatStyle
let priceDivided = product.price / 12
let yearlyPerMonthPrice = priceDivided.formatted(formatStyle)
let yearlyPerYearPrice = product.displayPrice
plan.yearlyPerYear = yearlyPerYearPrice
plan.yearlyPerMonth = yearlyPerMonthPrice
case .month: plan.monthlyPerMonth = product.displayPrice
default: fatalError("unexpected subscription period unit \(unit)")
}
result[productName] = plan
}
return result.map { name, prices in
MobilePlanPrice(name: name, monthlyPerMonth: prices.monthlyPerMonth!, yearlyPerYear: prices.yearlyPerYear!, yearlyPerMonth: prices.yearlyPerMonth!)
}
}
public func getPlanPrices() async throws -> [MobilePlanPrice] {
// TODO: Handle promotions (first year discount etc.)
struct TempMobilePlanPrice {
var rawMonthlyPerMonth: String?
var rawYearlyPerYear: String?
var rawYearlyPerMonth: String?
var displayMonthlyPerMonth: String?
var displayYearlyPerYear: String?
var displayYearlyPerMonth: String?
}
let plans: [String] = ALL_PURCHASEABLE_PLANS.flatMap { plan in
[self.formatPlanType(plan, withInterval: 1), self.formatPlanType(plan, withInterval: 12)]
}
let products: [Product] = try await Product.products(for: plans)
var result = [String: TempMobilePlanPrice]()
for product in products {
let productName = String(product.id.split(separator: ".")[2])
var plan =
result[productName]
?? TempMobilePlanPrice(
rawMonthlyPerMonth: nil,
rawYearlyPerYear: nil,
rawYearlyPerMonth: nil,
displayMonthlyPerMonth: nil,
displayYearlyPerYear: nil,
displayYearlyPerMonth: nil
)

let unit = product.subscription!.subscriptionPeriod.unit
switch unit {
case .year:
let formatStyle = product.priceFormatStyle
let priceDivided = product.price / 12
let yearlyPerMonthPrice = priceDivided.formatted(formatStyle)
let yearlyPerYearPrice = product.displayPrice
plan.rawYearlyPerYear = String(describing: product.price)
plan.rawYearlyPerMonth = String(describing: priceDivided)
plan.displayYearlyPerYear = yearlyPerYearPrice
plan.displayYearlyPerMonth = yearlyPerMonthPrice
case .month:
plan.rawMonthlyPerMonth = String(describing: product.price)
plan.displayMonthlyPerMonth = product.displayPrice
default: fatalError("unexpected subscription period unit \(unit)")
}
result[productName] = plan
}
return result.map { name, prices in
MobilePlanPrice(
name: name,
rawMonthlyPerMonth: prices.rawMonthlyPerMonth!,
rawYearlyPerYear: prices.rawYearlyPerYear!,
rawYearlyPerMonth: prices.rawYearlyPerMonth!,
displayMonthlyPerMonth: prices.displayMonthlyPerMonth!,
displayYearlyPerYear: prices.displayYearlyPerYear!,
displayYearlyPerMonth: prices.displayYearlyPerMonth!
)
}
}
public func showSubscriptionConfigView() async throws {
let window = await UIApplication.shared.connectedScenes.first
try await AppStore.showManageSubscriptions(in: window as! UIWindowScene)
Expand Down
41 changes: 33 additions & 8 deletions app-ios/tutanota/Sources/Payment/IosMobilePaymentsFacade.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,14 @@ public class IosMobilePaymentsFacade: MobilePaymentsFacade {
}

public func getPlanPrices() async throws -> [MobilePlanPrice] {
// TODO: Handle promotions (first year discount etc.)
struct TempMobilePlanPrice {
var monthlyPerMonth: String?
var yearlyPerYear: String?
var yearlyPerMonth: String?
var rawMonthlyPerMonth: String?
var rawYearlyPerYear: String?
var rawYearlyPerMonth: String?
var displayMonthlyPerMonth: String?
var displayYearlyPerYear: String?
var displayYearlyPerMonth: String?
}
let plans: [String] = ALL_PURCHASEABLE_PLANS.flatMap { plan in
[self.formatPlanType(plan, withInterval: 1), self.formatPlanType(plan, withInterval: 12)]
Expand All @@ -40,7 +44,16 @@ public class IosMobilePaymentsFacade: MobilePaymentsFacade {
var result = [String: TempMobilePlanPrice]()
for product in products {
let productName = String(product.id.split(separator: ".")[1])
var plan = result[productName] ?? TempMobilePlanPrice(monthlyPerMonth: nil, yearlyPerYear: nil, yearlyPerMonth: nil)
var plan =
result[productName]
?? TempMobilePlanPrice(
rawMonthlyPerMonth: nil,
rawYearlyPerYear: nil,
rawYearlyPerMonth: nil,
displayMonthlyPerMonth: nil,
displayYearlyPerYear: nil,
displayYearlyPerMonth: nil
)

let unit = product.subscription!.subscriptionPeriod.unit
switch unit {
Expand All @@ -49,15 +62,27 @@ public class IosMobilePaymentsFacade: MobilePaymentsFacade {
let priceDivided = product.price / 12
let yearlyPerMonthPrice = priceDivided.formatted(formatStyle)
let yearlyPerYearPrice = product.displayPrice
plan.yearlyPerYear = yearlyPerYearPrice
plan.yearlyPerMonth = yearlyPerMonthPrice
case .month: plan.monthlyPerMonth = product.displayPrice
plan.rawYearlyPerYear = String(describing: product.price)
plan.rawYearlyPerMonth = String(describing: priceDivided)
plan.displayYearlyPerYear = yearlyPerYearPrice
plan.displayYearlyPerMonth = yearlyPerMonthPrice
case .month:
plan.rawMonthlyPerMonth = String(describing: product.price)
plan.displayMonthlyPerMonth = product.displayPrice
default: fatalError("unexpected subscription period unit \(unit)")
}
result[productName] = plan
}
return result.map { name, prices in
MobilePlanPrice(name: name, monthlyPerMonth: prices.monthlyPerMonth!, yearlyPerYear: prices.yearlyPerYear!, yearlyPerMonth: prices.yearlyPerMonth!)
MobilePlanPrice(
name: name,
rawMonthlyPerMonth: prices.rawMonthlyPerMonth!,
rawYearlyPerYear: prices.rawYearlyPerYear!,
rawYearlyPerMonth: prices.rawYearlyPerMonth!,
displayMonthlyPerMonth: prices.displayMonthlyPerMonth!,
displayYearlyPerYear: prices.displayYearlyPerYear!,
displayYearlyPerMonth: prices.displayYearlyPerMonth!
)
}
}
public func showSubscriptionConfigView() async throws {
Expand Down
9 changes: 6 additions & 3 deletions ipc-schema/types/MobilePlanPrice.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@
"type": "struct",
"fields": {
"name": "string",
"monthlyPerMonth": "string",
"yearlyPerYear": "string",
"yearlyPerMonth": "string"
"rawMonthlyPerMonth": "string",
"rawYearlyPerYear": "string",
"rawYearlyPerMonth": "string",
"displayMonthlyPerMonth": "string",
"displayYearlyPerYear": "string",
"displayYearlyPerMonth": "string"
}
}
9 changes: 6 additions & 3 deletions src/common/native/common/generatedipc/MobilePlanPrice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@

export interface MobilePlanPrice {
readonly name: string
readonly monthlyPerMonth: string
readonly yearlyPerYear: string
readonly yearlyPerMonth: string
readonly rawMonthlyPerMonth: string
readonly rawYearlyPerYear: string
readonly rawYearlyPerMonth: string
readonly displayMonthlyPerMonth: string
readonly displayYearlyPerYear: string
readonly displayYearlyPerMonth: string
}
2 changes: 1 addition & 1 deletion src/common/subscription/InvoiceAndPaymentDataPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ export class InvoiceAndPaymentDataPage implements WizardPageN<UpgradeSubscriptio
a.data.paymentData,
null,
a.data.upgradeType === UpgradeType.Signup,
a.data.price,
neverNull(a.data.price?.rawPrice),
neverNull(a.data.accountingInfo),
).then((success) => {
if (success) {
Expand Down
20 changes: 8 additions & 12 deletions src/common/subscription/PriceUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ export function getPriceFromPriceData(priceData: PriceData | null, featureType:
export type SubscriptionPrice = {
// The locale formatted price of a description in the local currency on iOS and in Euro elsewhere
displayPrice: string
// The raw price in Euro as a float
// The raw price in the local currency on iOS and in Euro elsewhere as a float
rawPrice: string
}

Expand Down Expand Up @@ -173,16 +173,18 @@ export class PriceAndConfigProvider {
: this.getMonthlySubscriptionPrice(subscription, type)
}

// Returns the subscription price with the currency formatting on iOS and as a plain period seperated number on other platforms
/**
* Returns the subscription price with the currency formatting on iOS and as a plain period seperated number on other platforms
*/
getSubscriptionPriceWithCurrency(paymentInterval: PaymentInterval, subscription: PlanType, type: UpgradePriceType): SubscriptionPrice {
const price = this.getSubscriptionPrice(paymentInterval, subscription, type)
const rawPrice = price.toString()

if (isIOSApp()) {
return this.getAppStorePaymentsSubscriptionPrice(subscription, paymentInterval, rawPrice, type)
} else {
const displayPrice = formatPrice(price, true)
return { displayPrice, rawPrice }
const price = this.getSubscriptionPrice(paymentInterval, subscription, type)
return { displayPrice: formatPrice(price, true), rawPrice: price.toString() }
}
}

Expand All @@ -198,15 +200,9 @@ export class PriceAndConfigProvider {

switch (paymentInterval) {
case PaymentInterval.Monthly:
return { displayPrice: applePrices.monthlyPerMonth, rawPrice }
return { displayPrice: applePrices.displayMonthlyPerMonth, rawPrice: applePrices.rawMonthlyPerMonth }
case PaymentInterval.Yearly:
if (isCyberMonday && subscription === PlanType.Legend && type === UpgradePriceType.PlanActualPrice) {
const revolutionaryYearlyPerYear = this.getMobilePrices().get(PlanTypeToName[PlanType.Revolutionary].toLowerCase())?.yearlyPerYear ?? NBSP

return { displayPrice: revolutionaryYearlyPerYear, rawPrice }
}

return { displayPrice: applePrices.yearlyPerYear, rawPrice }
return { displayPrice: applePrices.displayYearlyPerYear, rawPrice: applePrices.rawYearlyPerYear }
}
}

Expand Down
10 changes: 5 additions & 5 deletions src/common/subscription/SubscriptionSelector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -264,19 +264,19 @@ export class SubscriptionSelector implements Component<SubscriptionSelectorAttr>
if (prices != null) {
if (isCyberMonday && targetSubscription === PlanType.Legend && interval == PaymentInterval.Yearly) {
const revolutionaryPrice = priceAndConfigProvider.getMobilePrices().get(PlanTypeToName[PlanType.Revolutionary].toLowerCase())
priceStr = revolutionaryPrice?.yearlyPerMonth ?? NBSP
priceStr = revolutionaryPrice?.displayYearlyPerMonth ?? NBSP
// if there is a discount for this plan we show the original price as reference
referencePriceStr = prices?.yearlyPerMonth
referencePriceStr = prices?.displayYearlyPerMonth
} else {
switch (interval) {
case PaymentInterval.Monthly:
priceStr = prices.monthlyPerMonth
priceStr = prices.displayMonthlyPerMonth
break
case PaymentInterval.Yearly:
priceStr = prices.yearlyPerMonth
priceStr = prices.displayYearlyPerMonth
if (!isCyberMonday) {
// if there is no discount for any plan then we show the monthly price as reference
referencePriceStr = prices.monthlyPerMonth
referencePriceStr = prices.displayMonthlyPerMonth
}
break
}
Expand Down
8 changes: 4 additions & 4 deletions src/common/subscription/UpgradeConfirmSubscriptionPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,8 +168,8 @@ export class UpgradeConfirmSubscriptionPage implements WizardPageN<UpgradeSubscr
isReadOnly: true,
}),
m(TextField, {
label: isYearly ? "priceFirstYear_label" : "price_label",
value: buildPriceString(attrs.data.displayPrice, attrs.data.options),
label: isYearly && attrs.data.nextYearPrice ? "priceFirstYear_label" : "price_label",
value: buildPriceString(attrs.data.price?.displayPrice ?? "0", attrs.data.options),
isReadOnly: true,
}),
this.renderPriceNextYear(attrs),
Expand Down Expand Up @@ -204,10 +204,10 @@ export class UpgradeConfirmSubscriptionPage implements WizardPageN<UpgradeSubscr
}

private renderPriceNextYear(attrs: WizardPageAttrs<UpgradeSubscriptionData>) {
return attrs.data.priceNextYear
return attrs.data.nextYearPrice
? m(TextField, {
label: "priceForNextYear_label",
value: buildPriceString(attrs.data.priceNextYear, attrs.data.options),
value: buildPriceString(attrs.data.nextYearPrice.displayPrice, attrs.data.options),
isReadOnly: true,
})
: null
Expand Down
14 changes: 6 additions & 8 deletions src/common/subscription/UpgradeSubscriptionPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,8 @@ export class UpgradeSubscriptionPage implements WizardPageN<UpgradeSubscriptionD
// Confirmation of free/business dialog (click on ok)
this.__signupFreeTest?.getStage(1).complete()
data.type = PlanType.Free
data.price = "0"
data.priceNextYear = "0"
data.price = null
data.nextYearPrice = null
this.showNextPage()
}
})
Expand Down Expand Up @@ -207,12 +207,10 @@ export class UpgradeSubscriptionPage implements WizardPageN<UpgradeSubscriptionD
data.type = planType
const { planPrices, options } = data
try {
// `data.price` is used for the amount parameter in the Braintree credit card verification call, so we do not include currency locale outside iOS.
const subscriptionPrice = planPrices.getSubscriptionPriceWithCurrency(options.paymentInterval(), data.type, UpgradePriceType.PlanActualPrice)
data.price = subscriptionPrice.rawPrice
data.displayPrice = subscriptionPrice.displayPrice
const nextYear = planPrices.getSubscriptionPriceWithCurrency(options.paymentInterval(), data.type, UpgradePriceType.PlanNextYearsPrice).displayPrice
data.priceNextYear = data.price !== nextYear ? nextYear : null
// `data.price.rawPrice` is used for the amount parameter in the Braintree credit card verification call, so we do not include currency locale outside iOS.
data.price = planPrices.getSubscriptionPriceWithCurrency(options.paymentInterval(), data.type, UpgradePriceType.PlanActualPrice)
const nextYear = planPrices.getSubscriptionPriceWithCurrency(options.paymentInterval(), data.type, UpgradePriceType.PlanNextYearsPrice)
data.nextYearPrice = data.price.rawPrice !== nextYear.rawPrice ? nextYear : null
} catch (e) {
console.error(e)
Dialog.message("appStoreNotAvailable_msg")
Expand Down
Loading

0 comments on commit 354bccb

Please sign in to comment.