mirror of
https://github.com/ProtonMail/ios-mail.git
synced 2026-05-15 09:50:39 +00:00
ET-5956: Added unlimited upsell design in UpsellScreen
This commit is contained in:
@@ -100,6 +100,8 @@ public extension DS.Icon {
|
||||
static let icCode = ImageResource.icCode
|
||||
static let icDiamond = ImageResource.icDiamond
|
||||
static let icInfinity = ImageResource.icInfinity
|
||||
static let icInfinityUpsellHeader = ImageResource.icInfinityUpsellHeader
|
||||
static let icInfinityUpsellRow = ImageResource.icInfinityUpsellRow
|
||||
static let icEnvelopeDot = ImageResource.icEnvelopeDot
|
||||
static let icEnvelopeOpen = ImageResource.icEnvelopeOpen
|
||||
static let icFileLines = ImageResource.icFileLines
|
||||
|
||||
@@ -41,6 +41,7 @@ public extension DS.Images {
|
||||
public static let logoMobileSignature = ImageResource.upsellLogoMobileSignature
|
||||
public static let logoScheduleSend = ImageResource.upsellLogoScheduleSend
|
||||
public static let logoSnooze = ImageResource.upsellLogoSnooze
|
||||
public static let logoUnlimited = ImageResource.upsellLogoUnlimited
|
||||
|
||||
public enum BlackFriday {
|
||||
public static let background = ImageResource.upsellBlackFridayBackground
|
||||
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "Plan icon.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
Binary file not shown.
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "Icon.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
Binary file not shown.
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "upsell_logo_unlimited.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
Binary file not shown.
@@ -95,7 +95,7 @@ public final class UpsellCoordinator: ObservableObject {
|
||||
let availablePlans = try await fetchAvailablePlans()
|
||||
|
||||
let model = try upsellScreenFactory.upsellScreenModel(
|
||||
showingPlan: configuration.regularPlan,
|
||||
showingPlan: upsellType.planVariant,
|
||||
basedOn: availablePlans,
|
||||
entryPoint: entryPoint,
|
||||
upsellType: upsellType
|
||||
|
||||
@@ -114,6 +114,12 @@ enum L10n {
|
||||
return .init("\(measurement.formatted()) storage", bundle: .module, comment: "Amount of storage space available in a given plan, for example: 1 GB storage")
|
||||
}
|
||||
|
||||
static let hideMyEmailAliases = LocalizedStringResource("Hide My Email aliases", bundle: .module, comment: "Description of a feature of a paid subscription")
|
||||
static let foldersAndLabels = LocalizedStringResource("Folders and labels", bundle: .module, comment: "Description of a feature of a paid subscription")
|
||||
static let premiumVpnPasswordManagerCloudStorage = LocalizedStringResource(
|
||||
"Premium VPN, password manager and cloud storage", bundle: .module, comment: "Description of a feature of a paid subscription")
|
||||
static let darkWebMonitoring = LocalizedStringResource("Dark Web Monitoring", bundle: .module, comment: "Description of a feature of a paid subscription")
|
||||
|
||||
static func numberOfEmailAddresses(_ amount: UInt) -> LocalizedStringResource {
|
||||
.init("\(amount) email addresses", bundle: .module, comment: "Number of email addresses available in a given plan")
|
||||
}
|
||||
|
||||
@@ -2626,6 +2626,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Dark Web Monitoring" : {
|
||||
"comment" : "Description of a feature of a paid subscription"
|
||||
},
|
||||
"Deletes spam and trash after 30 days. Get this and more with %@." : {
|
||||
"comment" : "Subtitle of the upsell page",
|
||||
"localizations" : {
|
||||
@@ -3169,6 +3172,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Folders and labels" : {
|
||||
"comment" : "Description of a feature of a paid subscription"
|
||||
},
|
||||
"Free" : {
|
||||
"comment" : "Name of the free plan",
|
||||
"localizations" : {
|
||||
@@ -3742,6 +3748,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Hide My Email aliases" : {
|
||||
"comment" : "Description of a feature of a paid subscription"
|
||||
},
|
||||
"Make your mobile signature your own. Enjoy this and more with %@." : {
|
||||
"comment" : "Subtitle of the upsell page",
|
||||
"localizations" : {
|
||||
@@ -5037,6 +5046,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Premium VPN, password manager and cloud storage" : {
|
||||
"comment" : "Description of a feature of a paid subscription"
|
||||
},
|
||||
"Priority customer support" : {
|
||||
"comment" : "Description of a feature of a paid subscription",
|
||||
"localizations" : {
|
||||
|
||||
@@ -16,8 +16,10 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail. If not, see https://www.gnu.org/licenses/.
|
||||
|
||||
import SwiftUI
|
||||
|
||||
enum ComparisonItemType {
|
||||
case boolean
|
||||
case string(free: String, plus: String)
|
||||
case integer(free: Int, plus: Int)
|
||||
case string(free: String, plan: String)
|
||||
case stringAndIcon(free: String, plan: Image)
|
||||
}
|
||||
|
||||
@@ -20,22 +20,24 @@ import InboxDesignSystem
|
||||
import SwiftUI
|
||||
|
||||
struct PlanComparisonGrid: View {
|
||||
private let items: [ComparisonItem] = [
|
||||
.init(title: \.storage, type: .string(free: gigabytesString(1), plus: gigabytesString(15))),
|
||||
.init(title: \.emailAddresses, type: .integer(free: 1, plus: 10)),
|
||||
.init(title: \.customEmailDomain, type: .boolean),
|
||||
.init(title: \.accessToDesktopApp, type: .boolean),
|
||||
.init(title: \.unlimitedFoldersAndLabels, type: .boolean),
|
||||
.init(title: \.priorityCustomerSupport, type: .boolean),
|
||||
]
|
||||
struct Configuration {
|
||||
enum ColumnHeader {
|
||||
case text(LocalizedStringResource)
|
||||
case icon(Image)
|
||||
}
|
||||
|
||||
let planColumnHeader: ColumnHeader
|
||||
let headerStroke: (any ShapeStyle)?
|
||||
let items: [ComparisonItem]
|
||||
}
|
||||
|
||||
private let configuration: Configuration
|
||||
private let highlightBorderWidth: CGFloat = 2
|
||||
private let highlightStroke: any ShapeStyle
|
||||
|
||||
@State private var highlightedColumnWidth: CGFloat = 0
|
||||
|
||||
init(highlightStroke: (any ShapeStyle)? = nil) {
|
||||
self.highlightStroke = highlightStroke ?? LinearGradient.highlight
|
||||
init(configuration: Configuration) {
|
||||
self.configuration = configuration
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
@@ -45,13 +47,15 @@ struct PlanComparisonGrid: View {
|
||||
|
||||
Text(L10n.PlanName.free)
|
||||
|
||||
Text(L10n.PlanName.plus)
|
||||
planColumnHeaderView
|
||||
.padding(.vertical, DS.Spacing.compact)
|
||||
.padding(.horizontal, DS.Spacing.standard)
|
||||
.overlay {
|
||||
RoundedRectangle(cornerRadius: DS.Radius.medium)
|
||||
.stroke(AnyShapeStyle(highlightStroke), lineWidth: highlightBorderWidth)
|
||||
.padding(highlightBorderWidth / 2)
|
||||
if let headerStroke = configuration.headerStroke {
|
||||
RoundedRectangle(cornerRadius: DS.Radius.medium)
|
||||
.stroke(AnyShapeStyle(headerStroke), lineWidth: highlightBorderWidth)
|
||||
.padding(highlightBorderWidth / 2)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, DS.Spacing.small)
|
||||
.coordinatedMinWidth(using: _highlightedColumnWidth)
|
||||
@@ -59,10 +63,10 @@ struct PlanComparisonGrid: View {
|
||||
.font(.callout)
|
||||
.fontWeight(.semibold)
|
||||
|
||||
ForEach(items.indices, id: \.self) { itemIndex in
|
||||
gridRow(for: items[itemIndex])
|
||||
ForEach(configuration.items.indices, id: \.self) { itemIndex in
|
||||
gridRow(for: configuration.items[itemIndex])
|
||||
|
||||
if itemIndex != items.indices.last {
|
||||
if itemIndex != configuration.items.indices.last {
|
||||
Divider()
|
||||
.overlay(.white.opacity(0.12))
|
||||
}
|
||||
@@ -82,6 +86,19 @@ struct PlanComparisonGrid: View {
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var planColumnHeaderView: some View {
|
||||
switch configuration.planColumnHeader {
|
||||
case .text(let resource):
|
||||
Text(resource)
|
||||
case .icon(let image):
|
||||
image
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(height: 32)
|
||||
}
|
||||
}
|
||||
|
||||
private func gridRow(for item: ComparisonItem) -> some View {
|
||||
GridRow {
|
||||
Text(L10n.Perk.self[keyPath: item.title])
|
||||
@@ -97,25 +114,24 @@ struct PlanComparisonGrid: View {
|
||||
.symbolRenderingMode(.palette)
|
||||
.foregroundStyle(.white, .black.opacity(0.2))
|
||||
.coordinatedMinWidth(using: _highlightedColumnWidth)
|
||||
case .integer(let valueForFreePlan, let valueForPlus):
|
||||
Text("\(valueForFreePlan)")
|
||||
|
||||
Text("\(valueForPlus)")
|
||||
.coordinatedMinWidth(using: _highlightedColumnWidth)
|
||||
case .string(let valueForFreePlan, let valueForPlus):
|
||||
case .string(let valueForFreePlan, let valueForPlan):
|
||||
Text(valueForFreePlan)
|
||||
|
||||
Text(valueForPlus)
|
||||
Text(valueForPlan)
|
||||
.padding(.horizontal, DS.Spacing.small)
|
||||
.coordinatedMinWidth(using: _highlightedColumnWidth)
|
||||
case .stringAndIcon(let valueForFreePlan, let iconForPlan):
|
||||
Text(valueForFreePlan)
|
||||
|
||||
iconForPlan
|
||||
.font(.system(size: 20))
|
||||
.padding(.horizontal, DS.Spacing.small)
|
||||
.coordinatedMinWidth(using: _highlightedColumnWidth)
|
||||
}
|
||||
}
|
||||
.fontWeight(.semibold)
|
||||
}
|
||||
}
|
||||
|
||||
private static func gigabytesString(_ value: Double) -> String {
|
||||
Measurement<UnitInformationStorage>(value: value, unit: .gigabytes).formatted()
|
||||
}
|
||||
}
|
||||
|
||||
private extension View {
|
||||
@@ -128,7 +144,16 @@ private extension View {
|
||||
|
||||
#Preview {
|
||||
ScrollView {
|
||||
PlanComparisonGrid()
|
||||
PlanComparisonGrid(
|
||||
configuration: .init(
|
||||
planColumnHeader: .text(L10n.PlanName.plus),
|
||||
headerStroke: LinearGradient.highlight,
|
||||
items: [
|
||||
.init(title: \.storage, type: .string(free: "1 GB", plan: "15 GB")),
|
||||
.init(title: \.customEmailDomain, type: .boolean),
|
||||
]
|
||||
)
|
||||
)
|
||||
}
|
||||
.background(LinearGradient.screenBackground)
|
||||
.preferredColorScheme(.dark)
|
||||
|
||||
@@ -148,7 +148,7 @@ public struct UpsellScreen: View {
|
||||
Spacer.exactly(DS.Spacing.huge)
|
||||
}
|
||||
|
||||
PlanComparisonGrid(highlightStroke: model.highlightStroke)
|
||||
PlanComparisonGrid(configuration: model.comparisonConfiguration)
|
||||
}
|
||||
.padding(.top, headerHeight)
|
||||
.padding(.bottom, DS.Spacing.extraLarge)
|
||||
|
||||
@@ -27,8 +27,16 @@ extension UpsellScreenModel {
|
||||
planInstances = DisplayablePlanInstance.previews
|
||||
}
|
||||
|
||||
let planName: String
|
||||
switch upsellType {
|
||||
case .mailPlus:
|
||||
planName = "Mail Plus"
|
||||
case .unlimited:
|
||||
planName = "Proton Unlimited"
|
||||
}
|
||||
|
||||
return .init(
|
||||
planName: "Mail Plus",
|
||||
planName: planName,
|
||||
planInstances: planInstances,
|
||||
entryPoint: entryPoint,
|
||||
upsellType: upsellType,
|
||||
|
||||
@@ -40,11 +40,17 @@ public final class UpsellScreenModel: Identifiable {
|
||||
|
||||
var logo: ImageResource {
|
||||
switch upsellType {
|
||||
case .mailPlus, .unlimited:
|
||||
case .mailPlus:
|
||||
entryPoint.logo
|
||||
case .unlimited:
|
||||
upsellType.logo
|
||||
}
|
||||
}
|
||||
|
||||
var comparisonConfiguration: PlanComparisonGrid.Configuration {
|
||||
upsellType.comparisonConfiguration
|
||||
}
|
||||
|
||||
var logoHeight: CGFloat? {
|
||||
isPromo ? nil : 118
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
//
|
||||
// Copyright (c) 2026 Proton Technologies AG
|
||||
//
|
||||
// This file is part of Proton Mail.
|
||||
//
|
||||
// Proton Mail is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail. If not, see https://www.gnu.org/licenses/.
|
||||
|
||||
import InboxDesignSystem
|
||||
import SwiftUI
|
||||
import proton_app_uniffi
|
||||
|
||||
extension UpsellType {
|
||||
var planVariant: String {
|
||||
switch self {
|
||||
case .mailPlus:
|
||||
SubscriptionPlanVariant.plus
|
||||
case .unlimited:
|
||||
SubscriptionPlanVariant.unlimited
|
||||
}
|
||||
}
|
||||
|
||||
var logo: ImageResource {
|
||||
switch self {
|
||||
case .mailPlus:
|
||||
DS.Images.Upsell.logoDefault
|
||||
case .unlimited:
|
||||
DS.Images.Upsell.logoUnlimited
|
||||
}
|
||||
}
|
||||
|
||||
var comparisonConfiguration: PlanComparisonGrid.Configuration {
|
||||
switch self {
|
||||
case .mailPlus:
|
||||
.init(
|
||||
planColumnHeader: .text(L10n.PlanName.plus),
|
||||
headerStroke: LinearGradient.highlight,
|
||||
items: Self.mailPlusComparisonItems
|
||||
)
|
||||
case .unlimited:
|
||||
.init(
|
||||
planColumnHeader: .icon(Image(DS.Icon.icInfinityUpsellHeader)),
|
||||
headerStroke: nil,
|
||||
items: Self.unlimitedComparisonItems
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private static let mailPlusComparisonItems: [ComparisonItem] = [
|
||||
.init(title: \.storage, type: .string(free: gigabytesString(1), plan: gigabytesString(15))),
|
||||
.init(title: \.emailAddresses, type: .string(free: "1", plan: "10")),
|
||||
.init(title: \.customEmailDomain, type: .boolean),
|
||||
.init(title: \.accessToDesktopApp, type: .boolean),
|
||||
.init(title: \.unlimitedFoldersAndLabels, type: .boolean),
|
||||
.init(title: \.priorityCustomerSupport, type: .boolean),
|
||||
]
|
||||
|
||||
private static let unlimitedComparisonItems: [ComparisonItem] = [
|
||||
.init(title: \.storage, type: .string(free: gigabytesString(1), plan: gigabytesString(500))),
|
||||
.init(title: \.hideMyEmailAliases, type: .stringAndIcon(free: "10", plan: Image(DS.Icon.icInfinityUpsellRow))),
|
||||
.init(title: \.foldersAndLabels, type: .stringAndIcon(free: "3", plan: Image(DS.Icon.icInfinityUpsellRow))),
|
||||
.init(title: \.premiumVpnPasswordManagerCloudStorage, type: .boolean),
|
||||
.init(title: \.darkWebMonitoring, type: .boolean),
|
||||
.init(title: \.customEmailDomain, type: .boolean),
|
||||
.init(title: \.accessToDesktopApp, type: .boolean),
|
||||
.init(title: \.priorityCustomerSupport, type: .boolean),
|
||||
]
|
||||
|
||||
private static func gigabytesString(_ value: Double) -> String {
|
||||
Measurement<UnitInformationStorage>(value: value, unit: .gigabytes).formatted()
|
||||
}
|
||||
}
|
||||
@@ -69,4 +69,16 @@ final class UpsellCoordinatorTests {
|
||||
_ = try await self.sut.presentUpsellScreen(entryPoint: .mailboxTopBar)
|
||||
#expect(telemetryReporting.upsellButtonTappedCalls == 1)
|
||||
}
|
||||
|
||||
@Test
|
||||
func unlimitedUpsellTypeFetchesUnlimitedPlan() async throws {
|
||||
let model = try await sut.presentUpsellScreen(entryPoint: .mailboxTopBar, upsellType: .unlimited)
|
||||
#expect(model.planName == "Proton Unlimited")
|
||||
}
|
||||
|
||||
@Test
|
||||
func mailPlusUpsellTypeFetchesMailPlusPlan() async throws {
|
||||
let model = try await sut.presentUpsellScreen(entryPoint: .mailboxTopBar, upsellType: .mailPlus)
|
||||
#expect(model.planName == "Mail Plus")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,6 +57,30 @@ final class UpsellScreenModelTests {
|
||||
#expect(planPurchasing.purchaseInvocations.count == 1)
|
||||
}
|
||||
|
||||
@Test
|
||||
func mailPlusLogoComesFromEntryPoint() {
|
||||
let sut = makeSUT(upsellType: .mailPlus)
|
||||
#expect(sut.logo == UpsellEntryPoint.mailboxTopBar.logo)
|
||||
}
|
||||
|
||||
@Test
|
||||
func unlimitedLogoComesFromUpsellType() {
|
||||
let sut = makeSUT(upsellType: .unlimited)
|
||||
#expect(sut.logo == UpsellType.unlimited.logo)
|
||||
}
|
||||
|
||||
@Test
|
||||
func mailPlusComparisonConfigurationHasSixItems() {
|
||||
let sut = makeSUT(upsellType: .mailPlus)
|
||||
#expect(sut.comparisonConfiguration.items.count == 6)
|
||||
}
|
||||
|
||||
@Test
|
||||
func unlimitedComparisonConfigurationHasEightItems() {
|
||||
let sut = makeSUT(upsellType: .unlimited)
|
||||
#expect(sut.comparisonConfiguration.items.count == 8)
|
||||
}
|
||||
|
||||
private func makeSUT(upsellType: UpsellType = .mailPlus) -> UpsellScreenModel {
|
||||
.init(
|
||||
planName: "foo",
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
//
|
||||
// Copyright (c) 2026 Proton Technologies AG
|
||||
//
|
||||
// This file is part of Proton Mail.
|
||||
//
|
||||
// Proton Mail is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Proton Mail is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Proton Mail. If not, see https://www.gnu.org/licenses/.
|
||||
|
||||
import InboxDesignSystem
|
||||
import Testing
|
||||
import proton_app_uniffi
|
||||
|
||||
@testable import InboxIAP
|
||||
|
||||
struct UpsellTypeUpsellScreenTests {
|
||||
@Test
|
||||
func mailPlusPlanVariant() {
|
||||
#expect(UpsellType.mailPlus.planVariant == SubscriptionPlanVariant.plus)
|
||||
}
|
||||
|
||||
@Test
|
||||
func unlimitedPlanVariant() {
|
||||
#expect(UpsellType.unlimited.planVariant == SubscriptionPlanVariant.unlimited)
|
||||
}
|
||||
|
||||
@Test
|
||||
func mailPlusLogo() {
|
||||
#expect(UpsellType.mailPlus.logo == DS.Images.Upsell.logoDefault)
|
||||
}
|
||||
|
||||
@Test
|
||||
func unlimitedLogo() {
|
||||
#expect(UpsellType.unlimited.logo == DS.Images.Upsell.logoUnlimited)
|
||||
}
|
||||
|
||||
@Test
|
||||
func mailPlusComparisonConfigurationHasTextHeader() {
|
||||
let config = UpsellType.mailPlus.comparisonConfiguration
|
||||
guard case .text = config.planColumnHeader else {
|
||||
Issue.record("Expected .text header for mailPlus")
|
||||
return
|
||||
}
|
||||
#expect(config.items.count == 6)
|
||||
}
|
||||
|
||||
@Test
|
||||
func unlimitedComparisonConfigurationHasIconHeader() {
|
||||
let config = UpsellType.unlimited.comparisonConfiguration
|
||||
guard case .icon = config.planColumnHeader else {
|
||||
Issue.record("Expected .icon header for unlimited")
|
||||
return
|
||||
}
|
||||
#expect(config.items.count == 8)
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,7 @@ final class UpsellScreenFactoryTests {
|
||||
@Test
|
||||
func upsellScreenModelGeneration() throws {
|
||||
let upsellScreenModel = try sut.upsellScreenModel(
|
||||
showingPlan: configuration.regularPlan,
|
||||
showingPlan: SubscriptionPlanVariant.plus,
|
||||
basedOn: availablePlans,
|
||||
entryPoint: entryPoint,
|
||||
upsellType: .mailPlus
|
||||
@@ -43,6 +43,18 @@ final class UpsellScreenFactoryTests {
|
||||
#expect(upsellScreenModel.planInstances == DisplayablePlanInstance.previews)
|
||||
}
|
||||
|
||||
@Test
|
||||
func unlimitedUpsellScreenModelGeneration() throws {
|
||||
let upsellScreenModel = try sut.upsellScreenModel(
|
||||
showingPlan: SubscriptionPlanVariant.unlimited,
|
||||
basedOn: availablePlans,
|
||||
entryPoint: entryPoint,
|
||||
upsellType: .unlimited
|
||||
)
|
||||
|
||||
#expect(upsellScreenModel.planName == "Proton Unlimited")
|
||||
}
|
||||
|
||||
@Test
|
||||
func onboardingUpsellScreenModelGeneration() throws {
|
||||
let upsellScreenModel = try sut.onboardingUpsellScreenModel(
|
||||
|
||||
Reference in New Issue
Block a user