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

Fix the next year price being shown if it is the same price as the first year in the sign up wizard #7936

Closed
wants to merge 2 commits into from
Closed
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
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"
}
}
2 changes: 2 additions & 0 deletions src/common/api/worker/invoicegen/InvoiceTexts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export default {
yourVatId: "Your VAT identification number:",
netPricesNoVatInGermany: "All prices are net prices and not subject to value added tax in Germany.",
noVatInGermany: "Prices are not subject to value added tax in Germany.",
reverseChargeAffiliate: "All prices are net prices. We are liable for VAT under the reverse charge mechanism",

paymentInvoiceDue1: "The payment is due 7 days after the invoice date without any deduction. Please",
paymentInvoiceDue2: "transfer the grand total with reference to the invoice number to our account:",
Expand Down Expand Up @@ -102,6 +103,7 @@ export default {
yourVatId: "Ihre Umsatzsteuer-Identifikationsnummer:",
netPricesNoVatInGermany: "Alle Beträge sind netto und werden nicht in Deutschland versteuert.",
noVatInGermany: "Beträge werden nicht in Deutschland versteuert.",
reverseChargeAffiliate: "Alle Beträge sind netto. Die Steuerschuldnerschaft geht auf uns als Leistungsempfänger über.",

quantity: "Menge",
item: "Item",
Expand Down
19 changes: 16 additions & 3 deletions src/common/api/worker/invoicegen/PdfInvoiceGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const enum VatType {
ADD_VAT = "1",
VAT_INCLUDED_SHOWN = "2",
VAT_INCLUDED_HIDDEN = "3",
NO_VAT_REVERSE_CHARGE = "4",
NO_VAT_CHARGE_TUTAO = "4",
}

const enum InvoiceType {
Expand Down Expand Up @@ -223,7 +223,8 @@ export class PdfInvoiceGenerator {
"",
"",
InvoiceTexts[this.languageCode].grandTotal,
this.formatInvoiceCurrency(this.invoice.grandTotal),
// in case of NO_VAT_CHARGE_TUTAO we must not show the VAT in the invoice, but we pay the taxes ourselves, so they need to be existing on the invoice
this.formatInvoiceCurrency(this.invoice.vatType == VatType.NO_VAT_CHARGE_TUTAO ? this.invoice.subTotal : this.invoice.grandTotal),
])
}

Expand All @@ -239,7 +240,6 @@ export class PdfInvoiceGenerator {
case VatType.VAT_INCLUDED_SHOWN:
break
case VatType.NO_VAT:
case VatType.NO_VAT_REVERSE_CHARGE:
if (this.invoice.vatIdNumber != null) {
this.doc
.addText(InvoiceTexts[this.languageCode].reverseChargeVatIdNumber1)
Expand All @@ -253,6 +253,19 @@ export class PdfInvoiceGenerator {
this.doc.addText(InvoiceTexts[this.languageCode].netPricesNoVatInGermany)
}
break
case VatType.NO_VAT_CHARGE_TUTAO:
this.doc
.addText(InvoiceTexts[this.languageCode].reverseChargeAffiliate)
.addLineBreak()
.addText(InvoiceTexts[this.languageCode].reverseChargeVatIdNumber2)
if (this.invoice.vatIdNumber != null) {
this.doc
.addLineBreak()
.addText(`${InvoiceTexts[this.languageCode].yourVatId} `)
.changeFont(PDF_FONTS.BOLD, 11)
.addText(`${this.invoice.vatIdNumber}`)
}
break
case VatType.VAT_INCLUDED_HIDDEN:
this.doc.addText(InvoiceTexts[this.languageCode].noVatInGermany)
break
Expand Down
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
Loading
Loading