Merge branch 'main' into tech/ET-6087

# Conflicts:
#	Modules/App/Tests/Tests/Snapshots/Screens/Sidebar/SidebarScreenSnapshotTests.swift
This commit is contained in:
Maciej Gomółka
2026-05-06 08:56:43 +02:00
120 changed files with 731 additions and 383 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
{ {
"project": "apple-mail-new", "project": "apple-mail-new",
"locale": "0d39f2e91da281e1a30dfc158cc6c4257d0a7d5a" "locale": "c3c613cfc201210c3d95bdda446c74a329ffc290"
} }
@@ -61,7 +61,7 @@ final class SenderImageAPIDataSource: Sendable, SenderImageDataSource {
address: params.address, address: params.address,
bimiSelector: params.bimiSelector, bimiSelector: params.bimiSelector,
displaySenderImage: params.displaySenderImage, displaySenderImage: params.displaySenderImage,
size: 128, size: .s128,
mode: colorScheme == .dark ? "dark" : "light", mode: colorScheme == .dark ? "dark" : "light",
format: "png" format: "png"
) )
@@ -46,7 +46,7 @@ extension SystemLabel {
L10n.Mailbox.SystemFolder.scheduled L10n.Mailbox.SystemFolder.scheduled
case .snoozed: case .snoozed:
L10n.Mailbox.SystemFolder.snoozed L10n.Mailbox.SystemFolder.snoozed
case .categorySocial, .categoryPromotions, .catergoryUpdates, .categoryForums, .categoryDefault, .blocked, .pinned: case .categorySocial, .categoryPromotions, .categoryUpdates, .categoryForums, .categoryDefault, .blocked, .pinned, .categoryNewsletter, .categoryTransactions:
fatalError("Not implemented") fatalError("Not implemented")
} }
} }
@@ -75,7 +75,7 @@ extension SystemLabel {
DS.Icon.icClockPaperPlane.image DS.Icon.icClockPaperPlane.image
case .snoozed: case .snoozed:
DS.Icon.icClock.image DS.Icon.icClock.image
case .categorySocial, .categoryPromotions, .catergoryUpdates, .categoryForums, .categoryDefault, .blocked, .pinned: case .categorySocial, .categoryPromotions, .categoryUpdates, .categoryForums, .categoryDefault, .blocked, .pinned, .categoryNewsletter, .categoryTransactions:
fatalError("Not implemented") fatalError("Not implemented")
} }
} }
@@ -32,8 +32,4 @@ extension UpsellConfiguration {
false false
#endif #endif
} }
var humanReadableUpsoldPlanName: String {
"Mail Plus"
}
} }
@@ -57066,7 +57066,7 @@
"sl" : { "sl" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
"value" : "Aktivno vas ščitimo pred sledenjem. Tukaj si lahko ogledate vohunske slikovne pike, ki smo jih blokirali, in povezave za sledenje, ki smo jih očistili v tem sporočilu. [Več o tem](https://proton.me/support/email-tracker-protection)" "value" : "Aktivno vas ščitimo pred sledenjem. Tukaj si lahko ogledate vohunske slikovne pike, ki smo jih blokirali, in sledilne povezave, ki smo jih očistili v tem sporočilu. [Več o tem](https://proton.me/support/email-tracker-protection)"
} }
}, },
"tr" : { "tr" : {
@@ -397,6 +397,8 @@ extension MailboxModel {
showScrollerErrorIfNotNetwork(error: error) showScrollerErrorIfNotNetwork(error: error)
let isLastPage = await !conversationScrollerHasMore() let isLastPage = await !conversationScrollerHasMore()
paginatedDataSource.handle(update: .init(isLastPage: isLastPage, value: .error(error), completion: nil)) paginatedDataSource.handle(update: .init(isLastPage: isLastPage, value: .error(error), completion: nil))
case .categoryViewChanged:
break
} }
} }
@@ -443,6 +445,8 @@ extension MailboxModel {
showScrollerErrorIfNotNetwork(error: error) showScrollerErrorIfNotNetwork(error: error)
let isLastPage = await !messageScrollerHasMore() let isLastPage = await !messageScrollerHasMore()
paginatedDataSource.handle(update: .init(isLastPage: isLastPage, value: .error(error), completion: nil)) paginatedDataSource.handle(update: .init(isLastPage: isLastPage, value: .error(error), completion: nil))
case .categoryViewChanged:
break
} }
} }
@@ -160,7 +160,7 @@ extension DraftPresenter {
undoScheduleSendProvider: UndoScheduleSendProvider = .mockInstance undoScheduleSendProvider: UndoScheduleSendProvider = .mockInstance
) -> DraftPresenter { ) -> DraftPresenter {
.init( .init(
userSession: .init(noHandle: .init()), userSession: .init(noPointer: .init()),
draftProvider: .dummy, draftProvider: .dummy,
undoSendProvider: undoSendProvider, undoSendProvider: undoSendProvider,
undoScheduleSendProvider: undoScheduleSendProvider undoScheduleSendProvider: undoScheduleSendProvider
@@ -28,6 +28,6 @@ extension DraftProvider {
} }
static var dummy: Self { static var dummy: Self {
.init(makeDraft: { _, _ in NewDraftResult.ok(.init(noHandle: .init())) }) .init(makeDraft: { _, _ in NewDraftResult.ok(.init(noPointer: .init())) })
} }
} }
@@ -48,6 +48,6 @@ extension LabelAsActions {
extension Undo { extension Undo {
static var dummy: Undo { static var dummy: Undo {
Undo(noHandle: .init()) Undo(noPointer: .init())
} }
} }
@@ -22,7 +22,7 @@ enum LabelAsSheetPreviewProvider {
static func testData() -> LabelAsSheetModel { static func testData() -> LabelAsSheetModel {
.init( .init(
input: .init(sheetType: .labelAs, ids: [], mailboxItem: .message(isLastMessageInCurrentLocation: false)), input: .init(sheetType: .labelAs, ids: [], mailboxItem: .message(isLastMessageInCurrentLocation: false)),
mailbox: .init(noHandle: .init()), mailbox: .init(noPointer: .init()),
availableLabelAsActions: .init( availableLabelAsActions: .init(
message: { _, _ in .ok(testLabels()) }, message: { _, _ in .ok(testLabels()) },
conversation: { _, _ in .ok([]) } conversation: { _, _ in .ok([]) }
@@ -19,6 +19,6 @@ import proton_app_uniffi
extension MailUserSession { extension MailUserSession {
static var dummy: MailUserSession { static var dummy: MailUserSession {
.init(noHandle: .init()) .init(noPointer: .init())
} }
} }
@@ -19,6 +19,6 @@ import proton_app_uniffi
extension Mailbox { extension Mailbox {
static var dummy: Mailbox { static var dummy: Mailbox {
.init(noHandle: .init()) .init(noPointer: .init())
} }
} }
@@ -126,8 +126,8 @@ struct AppProtectionSelectionScreen: View {
.init(type: .faceID, isSelected: true), .init(type: .faceID, isSelected: true),
] ]
), ),
appSettingsRepository: MailSession(noHandle: .init()), appSettingsRepository: MailSession(noPointer: .init()),
appProtectionConfigurator: MailSession(noHandle: .init()) appProtectionConfigurator: MailSession(noPointer: .init())
) )
} }
} }
@@ -199,7 +199,7 @@ struct AppSettingsScreen: View {
NavigationStack { NavigationStack {
AppSettingsScreen( AppSettingsScreen(
state: .initial(isDiscreetAppIconEnabled: false), state: .initial(isDiscreetAppIconEnabled: false),
customSettings: CustomSettings(noHandle: .init()) customSettings: CustomSettings(noPointer: .init())
) )
} }
} }
@@ -185,8 +185,8 @@ private extension SystemLabel {
func emptyScreenVariant(isUnreadFilterOn: Bool) -> NoResultsView.Variant { func emptyScreenVariant(isUnreadFilterOn: Bool) -> NoResultsView.Variant {
switch self { switch self {
case .inbox, .allDrafts, .allSent, .sent, .trash, .spam, .allMail, .archive, .drafts, .starred, .scheduled, case .inbox, .allDrafts, .allSent, .sent, .trash, .spam, .allMail, .archive, .drafts, .starred, .scheduled,
.almostAllMail, .snoozed, .categorySocial, .categoryPromotions, .catergoryUpdates, .categoryForums, .almostAllMail, .snoozed, .categorySocial, .categoryPromotions, .categoryUpdates, .categoryForums,
.categoryDefault, .blocked, .pinned: .categoryDefault, .blocked, .pinned, .categoryNewsletter, .categoryTransactions:
.mailbox(isUnreadFilterOn: isUnreadFilterOn) .mailbox(isUnreadFilterOn: isUnreadFilterOn)
case .outbox: case .outbox:
.outbox .outbox
@@ -197,5 +197,5 @@ struct ReportProblemScreen: View {
} }
#Preview { #Preview {
ReportProblemScreen(reportProblemService: MailUserSession(noHandle: .init())) ReportProblemScreen(reportProblemService: MailUserSession(noPointer: .init()))
} }
@@ -231,6 +231,8 @@ final class SearchModel: ObservableObject {
AppLogger.log(error: error, category: .mailbox) AppLogger.log(error: error, category: .mailbox)
let isLastPage = await !searchScrollerHasMore() let isLastPage = await !searchScrollerHasMore()
paginatedDataSource.handle(update: .init(isLastPage: isLastPage, value: .error(error), completion: nil)) paginatedDataSource.handle(update: .init(isLastPage: isLastPage, value: .error(error), completion: nil))
case .categoryViewChanged:
break
} }
} }
@@ -324,7 +324,7 @@ private extension SettingsPreference {
#Preview { #Preview {
NavigationStack { NavigationStack {
SettingsScreen( SettingsScreen(
mailUserSession: MailUserSession(noHandle: .init()), mailUserSession: MailUserSession(noPointer: .init()),
accountAuthCoordinator: .mock(), accountAuthCoordinator: .mock(),
upsellCoordinator: .init( upsellCoordinator: .init(
mailUserSession: .dummy, mailUserSession: .dummy,
@@ -232,8 +232,6 @@ struct SidebarScreen: View {
@ViewBuilder @ViewBuilder
private func upsellSidebarItem(item: SidebarItem, upsellType: UpsellType) -> some View { private func upsellSidebarItem(item: SidebarItem, upsellType: UpsellType) -> some View {
let planName = UpsellConfiguration.mail.humanReadableUpsoldPlanName
SidebarItemButton( SidebarItemButton(
item: item, item: item,
isTappable: isButtonTappable, isTappable: isButtonTappable,
@@ -241,7 +239,7 @@ struct SidebarScreen: View {
content: { content: {
HStack(spacing: .zero) { HStack(spacing: .zero) {
sidebarItemImage(icon: upsellType.icon.image, isSelected: false, renderingMode: .original) sidebarItemImage(icon: upsellType.icon.image, isSelected: false, renderingMode: .original)
itemNameLabel(name: upsellType.title(planName: planName), isSelected: false, foregroundColor: upsellType.tint) itemNameLabel(name: upsellType.title, isSelected: false)
Spacer() Spacer()
} }
} }
@@ -370,11 +368,11 @@ struct SidebarScreen: View {
.accessibilityIdentifier(SidebarScreenIdentifiers.badgeIcon) .accessibilityIdentifier(SidebarScreenIdentifiers.badgeIcon)
} }
private func itemNameLabel(name: String, isSelected: Bool, foregroundColor: Color? = nil) -> some View { private func itemNameLabel(name: String, isSelected: Bool) -> some View {
Text(name) Text(name)
.font(.subheadline) .font(.subheadline)
.fontWeight(isSelected ? .bold : .regular) .fontWeight(isSelected ? .bold : .regular)
.foregroundStyle(foregroundColor ?? (isSelected ? DS.Color.Sidebar.textSelected : DS.Color.Sidebar.textNorm)) .foregroundStyle(isSelected ? DS.Color.Sidebar.textSelected : DS.Color.Sidebar.textNorm)
.lineLimit(1) .lineLimit(1)
.accessibilityIdentifier(SidebarScreenIdentifiers.textItem) .accessibilityIdentifier(SidebarScreenIdentifiers.textItem)
} }
@@ -455,24 +453,25 @@ private extension SidebarOtherItem {
} }
private extension UpsellType { private extension UpsellType {
var planName: String {
switch self {
case .mailPlus:
"Mail Plus"
case .unlimited:
"Unlimited"
}
}
var icon: ImageResource { var icon: ImageResource {
switch self { switch self {
case .mailPlus, .unlimited: case .mailPlus:
DS.Icon.icDiamond DS.Icon.icDiamond
case .unlimited:
DS.Icon.icInfinity
} }
} }
func title(planName: String) -> String { var title: String {
switch self { L10n.Sidebar.upgrade(to: planName).string
case .mailPlus, .unlimited:
L10n.Sidebar.upgrade(to: planName).string
}
}
var tint: Color? {
switch self {
case .mailPlus, .unlimited:
nil
}
} }
} }
@@ -90,6 +90,8 @@ struct MainToolbar<AvatarView: View>: ViewModifier {
if case .eligible(let upsellType) = upsellEligibility { if case .eligible(let upsellType) = upsellEligibility {
ToolbarItem(placement: .topBarTrailing) { ToolbarItem(placement: .topBarTrailing) {
upsellButton(for: upsellType) upsellButton(for: upsellType)
.frame(width: 26, height: 26)
.clipShape(.circle)
} }
} }
ToolbarItem(placement: .topBarTrailing) { ToolbarItem(placement: .topBarTrailing) {
@@ -111,10 +113,10 @@ struct MainToolbar<AvatarView: View>: ViewModifier {
if !selectionMode.hasItems { if !selectionMode.hasItems {
ToolbarItemGroup(placement: .topBarTrailing) { ToolbarItemGroup(placement: .topBarTrailing) {
HStack(spacing: DS.Spacing.standard) { HStack(spacing: DS.Spacing.standard) {
searchButton
if case .eligible(let upsellType) = upsellEligibility { if case .eligible(let upsellType) = upsellEligibility {
upsellButton(for: upsellType) upsellButton(for: upsellType)
} }
searchButton
avatarView() avatarView()
} }
} }
@@ -141,15 +143,35 @@ struct MainToolbar<AvatarView: View>: ViewModifier {
} }
} }
@ViewBuilder
private func upsellButton(for upsellType: UpsellType) -> some View { private func upsellButton(for upsellType: UpsellType) -> some View {
Button(L10n.MainToolbar.upgrade, image: upsellType.icon) { switch upsellType {
Task { case .mailPlus:
do { Button(L10n.MainToolbar.upgrade, image: upsellType.icon) {
let upsellScreenModel = try await upsellCoordinator.presentUpsellScreen(entryPoint: .mailboxTopBar, upsellType: upsellType) performUpsellAction(upsellType: upsellType)
onEvent(.onUpsell(upsellScreenModel)) }
} catch { case .unlimited:
toastStateStore.present(toast: .error(message: error.localizedDescription)) Button {
} performUpsellAction(upsellType: upsellType)
} label: {
Image(upsellType.icon)
.renderingMode(.original)
.resizable()
.scaledToFit()
.frame(width: 32, height: 32)
.clipShape(RoundedRectangle(cornerRadius: Theme.radius.large))
}
.accessibilityLabel(L10n.MainToolbar.upgrade)
}
}
private func performUpsellAction(upsellType: UpsellType) {
Task {
do {
let upsellScreenModel = try await upsellCoordinator.presentUpsellScreen(entryPoint: .mailboxTopBar, upsellType: upsellType)
onEvent(.onUpsell(upsellScreenModel))
} catch {
toastStateStore.present(toast: .error(message: error.localizedDescription))
} }
} }
} }
@@ -203,8 +225,10 @@ enum MainToolbarEvent {
private extension UpsellType { private extension UpsellType {
var icon: ImageResource { var icon: ImageResource {
switch self { switch self {
case .mailPlus, .unlimited: case .mailPlus:
DS.Icon.icBrandProtonMailUpsellBlackAndWhite DS.Icon.icBrandProtonMailUpsellBlackAndWhite
case .unlimited:
DS.Icon.icBrandProtonUnlimitedUpsellHeader
} }
} }
} }
+3 -3
View File
@@ -26,11 +26,11 @@ final class MailboxStub: Mailbox, @unchecked Sendable {
init(viewMode: ViewMode) { init(viewMode: ViewMode) {
self._viewMode = viewMode self._viewMode = viewMode
super.init(noHandle: .init()) super.init(noPointer: .init())
} }
@available(*, unavailable) @available(*, unavailable)
required init(unsafeFromHandle handle: UInt64) { required init(unsafeFromRawPointer pointer: UnsafeMutableRawPointer) {
fatalError("init(unsafeFromHandle:) has not been implemented") fatalError("init(unsafeFromRawPointer:) has not been implemented")
} }
} }
+1 -1
View File
@@ -67,6 +67,6 @@ final class SidebarSpy: SidebarProtocol, @unchecked Sendable {
let mappedIndex: CallbackIndex = lastIndex == -1 ? .folder : .init(rawValue: lastIndex + 1)! let mappedIndex: CallbackIndex = lastIndex == -1 ? .folder : .init(rawValue: lastIndex + 1)!
spiedWatchers[mappedIndex] = callback spiedWatchers[mappedIndex] = callback
return .ok(WatchHandleDummy(noHandle: .init())) return .ok(WatchHandleDummy(noPointer: .init()))
} }
} }
@@ -19,6 +19,6 @@ import proton_app_uniffi
extension MailUserSession { extension MailUserSession {
static var dummy: MailUserSession { static var dummy: MailUserSession {
.init(noHandle: .init()) .init(noPointer: .init())
} }
} }
@@ -19,6 +19,6 @@ import proton_app_uniffi
extension Mailbox { extension Mailbox {
static var dummy: Mailbox { static var dummy: Mailbox {
.init(noHandle: .init()) .init(noPointer: .init())
} }
} }
@@ -40,7 +40,7 @@ final class MailboxItemCellSnapshotTests {
("expiration_time_message", .makeSimpleMessage(type: .expirationTime)), ("expiration_time_message", .makeSimpleMessage(type: .expirationTime)),
]) ])
func mailboxItemCell(testName: String, model: MailboxItemCellUIModel) { func mailboxItemCell(testName: String, model: MailboxItemCellUIModel) {
assertSnapshotsOnIPhoneX(of: MailboxItemCell.testCell(model: model), testName: testName) assertSnapshotsOnIPhoneX(of: MailboxItemCell.testCell(model: model), precision: 0.99, testName: testName)
} }
} }
@@ -27,7 +27,7 @@ import XCTest
@MainActor @MainActor
class SettingsScreenSnapshotTests: BaseTestCase { class SettingsScreenSnapshotTests: BaseTestCase {
func testSettingsScreenLayoutsCorrectOnIphoneX() { func testSettingsScreenLayoutsCorrectOnIphoneX() {
let store = AppAppearanceStore(mailSession: { MailSession(noHandle: .init()) }) let store = AppAppearanceStore(mailSession: { MailSession(noPointer: .init()) })
let mailUserSession = MailUserSessionSpy(id: "") let mailUserSession = MailUserSessionSpy(id: "")
mailUserSession.stubbedAccountDetails = .testData mailUserSession.stubbedAccountDetails = .testData
mailUserSession.stubbedUser = .testData mailUserSession.stubbedUser = .testData
@@ -37,9 +37,10 @@ final class SidebarScreenSnapshotTests {
createFolder: .createFolder createFolder: .createFolder
) )
@Test(arguments: [UIUserInterfaceStyle.light, .dark]) @Test(arguments: [UIUserInterfaceStyle.light, .dark], [UpsellType.mailPlus, .unlimited])
func testSidebarWithDataLayoutsCorrectOnIphoneX(style: UIUserInterfaceStyle) { func testSidebarWithDataLayoutsCorrectOnIphoneX(style: UIUserInterfaceStyle, upsellType: UpsellType) {
var state = self.state var state = self.state
state.upsell = .upsell(upsellType)
state.folders = [SidebarCustomFolder.topSecretFolder].map(\.sidebarFolder) state.folders = [SidebarCustomFolder.topSecretFolder].map(\.sidebarFolder)
state.system = [PMSystemLabel.inbox, .sent, .outbox].compactMap(\.sidebarSystemFolder) state.system = [PMSystemLabel.inbox, .sent, .outbox].compactMap(\.sidebarSystemFolder)
@@ -48,7 +49,7 @@ final class SidebarScreenSnapshotTests {
let screenModel = SidebarModel( let screenModel = SidebarModel(
state: state, state: state,
sidebar: SidebarSpy(), sidebar: SidebarSpy(),
upsellEligibilityPublisher: .init(constant: .eligible(.mailPlus)) upsellEligibilityPublisher: .init(constant: .eligible(upsellType))
) )
let sidebarScreen = SidebarScreen( let sidebarScreen = SidebarScreen(
screenModel: screenModel, screenModel: screenModel,
@@ -57,7 +58,7 @@ final class SidebarScreenSnapshotTests {
) )
.environmentObject(AppUIStateStore(sidebarState: .init(zIndex: .zero, visibleWidth: 320))) .environmentObject(AppUIStateStore(sidebarState: .init(zIndex: .zero, visibleWidth: 320)))
assertSnapshotsOnIPhoneX(of: sidebarScreen, styles: [style]) assertSnapshotsOnIPhoneX(of: sidebarScreen, named: "\(upsellType)", styles: [style])
} }
@Test(arguments: [UIUserInterfaceStyle.light, .dark]) @Test(arguments: [UIUserInterfaceStyle.light, .dark])
@@ -25,12 +25,24 @@ import proton_app_uniffi
@testable import InboxIAP @testable import InboxIAP
@MainActor @MainActor
@Suite(.disabled("Tests are failing on CI only, needs more investigation."))
struct UpsellScreenSnapshotTests { struct UpsellScreenSnapshotTests {
enum SelectedCycle: String {
case yearly
case monthly
var lengthInMonths: Int {
switch self {
case .yearly: 12
case .monthly: 1
}
}
}
struct TestCase { struct TestCase {
let label: String let label: String
let config: ViewImageConfig let config: ViewImageConfig
let upsellType: UpsellType let upsellType: UpsellType
let selectedCycle: SelectedCycle
} }
nonisolated private static let testCases: [TestCase] = { nonisolated private static let testCases: [TestCase] = {
@@ -42,15 +54,19 @@ struct UpsellScreenSnapshotTests {
] ]
let upsellTypes: [UpsellType] = [.mailPlus, .unlimited] let upsellTypes: [UpsellType] = [.mailPlus, .unlimited]
let selectedCycles: [SelectedCycle] = [.yearly, .monthly]
return orientations.flatMap { orientation in return orientations.flatMap { orientation in
devices.flatMap { device in devices.flatMap { device in
upsellTypes.map { upsellType in upsellTypes.flatMap { upsellType in
.init( selectedCycles.map { selectedCycle in
label: "\(device.label)_\(orientation)_\(upsellType.label)", .init(
config: device.configFactory(orientation), label: "\(device.label)_\(orientation)_\(upsellType.label)_\(selectedCycle.rawValue)",
upsellType: upsellType config: device.configFactory(orientation),
) upsellType: upsellType,
selectedCycle: selectedCycle
)
}
} }
} }
} }
@@ -58,12 +74,20 @@ struct UpsellScreenSnapshotTests {
@Test(arguments: testCases) @Test(arguments: testCases)
func upsellScreen(testCase: TestCase) { func upsellScreen(testCase: TestCase) {
let sut = UpsellScreen(model: .preview(entryPoint: .mailboxTopBar, upsellType: testCase.upsellType)) let model = UpsellScreenModel.preview(entryPoint: .mailboxTopBar, upsellType: testCase.upsellType)
if let instance = model.planInstances.first(where: { $0.cycleInMonths == testCase.selectedCycle.lengthInMonths }) {
model.selectedInstanceId = instance.storeKitProductId
}
let sut = UpsellScreen(model: model)
let viewController = UIHostingController(rootView: sut) let viewController = UIHostingController(rootView: sut)
viewController.view.backgroundColor = .black
viewController.overrideUserInterfaceStyle = .dark
let strategy: Snapshotting<UIViewController, UIImage> = .image( let strategy: Snapshotting<UIViewController, UIImage> = .image(
on: testCase.config, on: testCase.config,
drawHierarchyInKeyWindow: true drawHierarchyInKeyWindow: true,
precision: 0.99
) )
assertSnapshot(of: viewController, as: strategy, named: testCase.label) assertSnapshot(of: viewController, as: strategy, named: testCase.label)
@@ -0,0 +1,69 @@
// Copyright (c) 2025 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 InboxIAP
import InboxSnapshotTesting
import InboxTesting
import ProtonUIFoundations
import SwiftUI
import Testing
import UIKit
import proton_app_uniffi
@testable import ProtonMail
@MainActor
final class MainToolbarSnapshotTests {
@Test(arguments: [UpsellType.mailPlus, .unlimited])
func mainToolbarWithUpsell(upsellType: UpsellType) {
let view = makeToolbarView(upsellEligibility: .eligible(upsellType))
assertSnapshotsOnIPhoneX(
of: view,
named: "upsellType.\(upsellType)",
drawHierarchyInKeyWindow: true,
precision: 0.99
)
}
@Test
func mainToolbarWithoutUpsell() {
let view = makeToolbarView(upsellEligibility: .notEligible)
assertSnapshotsOnIPhoneX(of: view, drawHierarchyInKeyWindow: true)
}
private func makeToolbarView(upsellEligibility: UpsellEligibility) -> some View {
NavigationStack {
Color.clear
.mainToolbar(
title: "Inbox",
onEvent: { _ in },
avatarView: {
Text("R")
.font(.subheadline.weight(.semibold))
.frame(width: 32, height: 32)
.foregroundStyle(.white)
.background(.blue)
.clipShape(RoundedRectangle(cornerRadius: Theme.radius.large))
}
)
}
.environment(\.upsellEligibility, upsellEligibility)
.environmentObject(ToastStateStore(initialState: .initial))
.environmentObject(UpsellCoordinator.dummy)
}
}
@@ -35,7 +35,7 @@ final class SceneDelegateTests: BaseTestCase {
sut = .init() sut = .init()
mailSessionSpy = .init() mailSessionSpy = .init()
sut.appProtectionStore = .init(mailSession: { self.mailSessionSpy }) sut.appProtectionStore = .init(mailSession: { self.mailSessionSpy })
sut.mailSessionFactory = { .init(noHandle: .init()) } sut.mailSessionFactory = { .init(noPointer: .init()) }
sut.checkAutoLockSetting = { completion in completion(self.shouldAutoLockStub) } sut.checkAutoLockSetting = { completion in completion(self.shouldAutoLockStub) }
sut.transitionAnimation = { _, _, _, animation, completion in sut.transitionAnimation = { _, _, _, animation, completion in
animation?() animation?()
@@ -33,7 +33,7 @@ final class DeviceTokenRegistrarTests {
var lastCreatedHandle: RegisterDeviceTaskHandleSpy? var lastCreatedHandle: RegisterDeviceTaskHandleSpy?
mailSession.stubbedRegisterDeviceTaskHandleFactory = { mailSession.stubbedRegisterDeviceTaskHandleFactory = {
let newHandle = RegisterDeviceTaskHandleSpy(noHandle: .init()) let newHandle = RegisterDeviceTaskHandleSpy(noPointer: .init())
lastCreatedHandle = newHandle lastCreatedHandle = newHandle
return newHandle return newHandle
} }
@@ -86,7 +86,7 @@ final class DraftPresenterTests: BaseTestCase, @unchecked Sendable {
@MainActor @MainActor
func testOpenDraftWithContact_ItCreatesEmptyDraftAddRecipientAndOpensDraft() async throws { func testOpenDraftWithContact_ItCreatesEmptyDraftAddRecipientAndOpensDraft() async throws {
let draftSpy = DraftSpy(noHandle: .init()) let draftSpy = DraftSpy(noPointer: .init())
sut = makeSUT(stubbedNewDraftResult: .ok(draftSpy)) sut = makeSUT(stubbedNewDraftResult: .ok(draftSpy))
var capturedDraftToPresent: [DraftToPresent] = [] var capturedDraftToPresent: [DraftToPresent] = []
@@ -123,7 +123,7 @@ final class DraftPresenterTests: BaseTestCase, @unchecked Sendable {
@MainActor @MainActor
func testOpenDraftWithContactGroup_ItCreatesEmptyDraftAddGroupAndOpensDraft() async throws { func testOpenDraftWithContactGroup_ItCreatesEmptyDraftAddGroupAndOpensDraft() async throws {
let draftSpy = DraftSpy(noHandle: .init()) let draftSpy = DraftSpy(noPointer: .init())
sut = makeSUT(stubbedNewDraftResult: .ok(draftSpy)) sut = makeSUT(stubbedNewDraftResult: .ok(draftSpy))
var capturedDraftToPresent: [DraftToPresent] = [] var capturedDraftToPresent: [DraftToPresent] = []
@@ -292,13 +292,13 @@ extension DraftPresenterTests {
} }
private extension Draft { private extension Draft {
static var dummyDraft: Draft { .init(noHandle: .init()) } static var dummyDraft: Draft { .init(noPointer: .init()) }
} }
private class DraftSpy: Draft, @unchecked Sendable { private class DraftSpy: Draft, @unchecked Sendable {
let toRecipientsCalls: ComposerRecipientListSpy = .init(noHandle: .init()) let toRecipientsCalls: ComposerRecipientListSpy = .init(noPointer: .init())
let ccRecipientsCalls: ComposerRecipientListSpy = .init(noHandle: .init()) let ccRecipientsCalls: ComposerRecipientListSpy = .init(noPointer: .init())
let bccRecipientsCalls: ComposerRecipientListSpy = .init(noHandle: .init()) let bccRecipientsCalls: ComposerRecipientListSpy = .init(noPointer: .init())
private(set) var setSubjectCalls: [String] = [] private(set) var setSubjectCalls: [String] = []
private(set) var setBodyCalls: [String] = [] private(set) var setBodyCalls: [String] = []
@@ -31,7 +31,7 @@ final class LabelAsSheetModelTests {
private var invokedAvailableActionsWithConversationIDs: [ID] = [] private var invokedAvailableActionsWithConversationIDs: [ID] = []
private var invokedDismissCount = 0 private var invokedDismissCount = 0
private var stubbedLabelAsActions: [LabelAsAction] = [] private var stubbedLabelAsActions: [LabelAsAction] = []
private var stubbedLabelAsOutput: LabelAsOutput = .init(inputLabelIsEmpty: false, undo: UndoSpy(noHandle: .init())) private var stubbedLabelAsOutput: LabelAsOutput = .init(inputLabelIsEmpty: false, undo: UndoSpy(noPointer: .init()))
private var toastStateStore = ToastStateStore(initialState: .initial) private var toastStateStore = ToastStateStore(initialState: .initial)
private var invokedLabelMessage: [LabelAsExecutedWithData] = [] private var invokedLabelMessage: [LabelAsExecutedWithData] = []
@@ -175,7 +175,7 @@ final class LabelAsSheetModelTests {
private func sut(ids: [ID], type: MailboxItemType) -> LabelAsSheetModel { private func sut(ids: [ID], type: MailboxItemType) -> LabelAsSheetModel {
LabelAsSheetModel( LabelAsSheetModel(
input: .init(sheetType: .labelAs, ids: ids, mailboxItem: type.mailboxItem), input: .init(sheetType: .labelAs, ids: ids, mailboxItem: type.mailboxItem),
mailbox: .init(noHandle: .init()), mailbox: .init(noPointer: .init()),
availableLabelAsActions: .init( availableLabelAsActions: .init(
message: { _, ids in message: { _, ids in
self.invokedAvailableActionsWithMessagesIDs = ids self.invokedAvailableActionsWithMessagesIDs = ids
@@ -220,7 +220,7 @@ final class LabelAsSheetModelTests {
itemType: MailboxItemType, itemType: MailboxItemType,
spyToVerify: () -> [LabelAsExecutedWithData] spyToVerify: () -> [LabelAsExecutedWithData]
) async throws { ) async throws {
let undoSpy = UndoSpy(noHandle: .init()) let undoSpy = UndoSpy(noPointer: .init())
let selectedLabelID: ID = .init(value: 2) let selectedLabelID: ID = .init(value: 2)
let partiallySelectedLabelID: ID = .init(value: 4) let partiallySelectedLabelID: ID = .init(value: 4)
stubbedLabelAsActions = [ stubbedLabelAsActions = [
@@ -257,7 +257,7 @@ final class LabelAsSheetModelTests {
spyToVerify: () -> [LabelAsExecutedWithData], spyToVerify: () -> [LabelAsExecutedWithData],
expectToastMessage: LocalizedStringResource expectToastMessage: LocalizedStringResource
) async throws { ) async throws {
let undoSpy = UndoSpy(noHandle: .init()) let undoSpy = UndoSpy(noPointer: .init())
let selectedLabelID: ID = .init(value: 2) let selectedLabelID: ID = .init(value: 2)
let partiallySelectedLabelID: ID = .init(value: 4) let partiallySelectedLabelID: ID = .init(value: 4)
stubbedLabelAsActions = [ stubbedLabelAsActions = [
@@ -259,7 +259,7 @@ class ListActionsToolbarStoreTests {
func action_WhenMoveToInboxIsTappedUndoIsAvailbleAndTapped_ItTriggersUndoAndDismissesToast() async throws { func action_WhenMoveToInboxIsTappedUndoIsAvailbleAndTapped_ItTriggersUndoAndDismissesToast() async throws {
let ids: [ID] = [.init(value: 7), .init(value: 77)] let ids: [ID] = [.init(value: 7), .init(value: 77)]
let systemFolder = MovableSystemFolderAction.testInbox let systemFolder = MovableSystemFolderAction.testInbox
let undoSpy = UndoSpy(noHandle: .init()) let undoSpy = UndoSpy(noPointer: .init())
let viewMode = ViewMode.messages let viewMode = ViewMode.messages
moveToActionsSpy.stubbedMoveMessagesToOkResult = undoSpy moveToActionsSpy.stubbedMoveMessagesToOkResult = undoSpy
sut = makeSUT(viewMode: viewMode) sut = makeSUT(viewMode: viewMode)
@@ -29,7 +29,7 @@ final class MoveToActionPerformerTests: BaseTestCase {
super.setUp() super.setUp()
sut = .init( sut = .init(
mailbox: .init(noHandle: .init()), mailbox: .init(noPointer: .init()),
moveToActions: .init( moveToActions: .init(
moveMessagesTo: { [unowned self] _, _, _ in stubbedResult }, moveMessagesTo: { [unowned self] _, _, _ in stubbedResult },
moveConversationsTo: { [unowned self] _, _, _ in stubbedResult } moveConversationsTo: { [unowned self] _, _, _ in stubbedResult }
@@ -108,7 +108,7 @@ final class MoveToSheetStateStoreTests {
@Test @Test
func action_WhenInboxIsTappedUndoIsAvailableAndTapped_ItTriggersUndoAndDismissesToast() async throws { func action_WhenInboxIsTappedUndoIsAvailableAndTapped_ItTriggersUndoAndDismissesToast() async throws {
let undoSpy = UndoSpy(noHandle: .init()) let undoSpy = UndoSpy(noPointer: .init())
moveToActionsSpy.stubbedMoveMessagesToOkResult = undoSpy moveToActionsSpy.stubbedMoveMessagesToOkResult = undoSpy
let sut = sut( let sut = sut(
@@ -155,7 +155,7 @@ final class MoveToSheetStateStoreTests {
.init( .init(
state: .initial, state: .initial,
input: input, input: input,
mailbox: .init(noHandle: .init()), mailbox: .init(noPointer: .init()),
availableMoveToActions: .init( availableMoveToActions: .init(
message: { _, ids in message: { _, ids in
self.invokedAvailableActionsWithMessagesIDs = ids self.invokedAvailableActionsWithMessagesIDs = ids
@@ -566,12 +566,12 @@ private final class DecryptedMessageSpy: DecryptedMessage, @unchecked Sendable {
init(stubbedOptions: TransformOpts) { init(stubbedOptions: TransformOpts) {
self.stubbedOptions = stubbedOptions self.stubbedOptions = stubbedOptions
super.init(noHandle: .init()) super.init(noPointer: .init())
} }
@available(*, unavailable) @available(*, unavailable)
required init(unsafeFromHandle handle: UInt64) { required init(unsafeFromRawPointer pointer: UnsafeMutableRawPointer) {
fatalError("init(unsafeFromHandle:) has not been implemented") fatalError("init(unsafeFromRawPointer:) has not been implemented")
} }
private(set) var bodyWithOptionsCalls: [TransformOpts] = [] private(set) var bodyWithOptionsCalls: [TransformOpts] = []
@@ -333,7 +333,7 @@ final class MessageAddressActionViewStateStoreTests {
senderUnblocker: .init( senderUnblocker: .init(
mailbox: .dummy, mailbox: .dummy,
wrapper: .init( wrapper: .init(
messageBody: { _, _ in .ok(.init(noHandle: .init())) }, messageBody: { _, _ in .ok(.init(noPointer: .init())) },
markMessageHam: { _, _ in .ok }, markMessageHam: { _, _ in .ok },
unblockSender: { _, emailAddress in unblockSender: { _, emailAddress in
await self.unblockSpy.result(for: emailAddress) await self.unblockSpy.result(for: emailAddress)
@@ -235,12 +235,12 @@ class BackgroundExecutionHandleStub: BackgroundExecutionHandle, @unchecked Senda
private(set) var abortCalls: [Bool] = [] private(set) var abortCalls: [Bool] = []
init() { init() {
super.init(noHandle: .init()) super.init(noPointer: .init())
} }
@available(*, unavailable) @available(*, unavailable)
required init(unsafeFromHandle handle: UInt64) { required init(unsafeFromRawPointer pointer: UnsafeMutableRawPointer) {
fatalError("init(unsafeFromHandle:) has not been implemented") fatalError("init(unsafeFromRawPointer:) has not been implemented")
} }
// MARK: - BackgroundExecutionHandle // MARK: - BackgroundExecutionHandle
@@ -127,7 +127,7 @@ struct ComposerLoadingView: View {
ComposerScreen( ComposerScreen(
draft: .emptyMock, draft: .emptyMock,
draftOrigin: .new, draftOrigin: .new,
dependencies: .init(contactProvider: .mockInstance, userSession: .init(noHandle: .init())), dependencies: .init(contactProvider: .mockInstance, userSession: .init(noPointer: .init())),
onDismiss: { _ in } onDismiss: { _ in }
) )
.environmentObject(toastStateStore) .environmentObject(toastStateStore)
@@ -307,6 +307,6 @@ final class MockAttachmentList: AttachmentListProtocol, @unchecked Sendable {
} }
func watcherStream() async -> AttachmentListWatcherStreamResult { func watcherStream() async -> AttachmentListWatcherStreamResult {
.ok(DraftAttachmentListUpdateStream.init(noHandle: .init())) .ok(DraftAttachmentListUpdateStream.init(noPointer: .init()))
} }
} }
@@ -141,7 +141,7 @@ private extension Array where Element == ContactSuggestion {
private extension MailUserSession { private extension MailUserSession {
static func empty() -> MailUserSession { static func empty() -> MailUserSession {
MailUserSession(noHandle: .init()) MailUserSession(noPointer: .init())
} }
} }
@@ -150,12 +150,12 @@ private class ContactSuggestionsStub: ContactSuggestions, @unchecked Sendable {
init(all: [ContactSuggestion]) { init(all: [ContactSuggestion]) {
_all = all _all = all
super.init(noHandle: .init()) super.init(noPointer: .init())
} }
@available(*, unavailable) @available(*, unavailable)
required init(unsafeFromHandle handle: UInt64) { required init(unsafeFromRawPointer pointer: UnsafeMutableRawPointer) {
fatalError("init(unsafeFromHandle:) has not been implemented") fatalError("init(unsafeFromRawPointer:) has not been implemented")
} }
override func all() -> [ContactSuggestion] { override func all() -> [ContactSuggestion] {
@@ -394,6 +394,16 @@ public extension PasswordFlowSubmitTotpResult {
} }
} }
} }
public extension ResolveMessageFromPushNotificationResult {
func get() throws(ActionError) -> Message {
switch self {
case .ok(let value):
value
case .error(let error):
throw error
}
}
}
public extension ResolveMessageIdResult { public extension ResolveMessageIdResult {
func get() throws(ActionError) -> Id { func get() throws(ActionError) -> Id {
switch self { switch self {
@@ -139,7 +139,7 @@ public struct ContactsScreen: View {
#Preview { #Preview {
ContactsScreen( ContactsScreen(
apiConfig: .debugPreview, apiConfig: .debugPreview,
mailUserSession: .init(noHandle: .init()), mailUserSession: .init(noPointer: .init()),
contactsProvider: .previewInstance(), contactsProvider: .previewInstance(),
contactsWatcher: .previewInstance(), contactsWatcher: .previewInstance(),
draftPresenter: ContactsDraftPresenterDummy() draftPresenter: ContactsDraftPresenterDummy()
@@ -19,6 +19,6 @@ import proton_app_uniffi
extension ContactsWatcher { extension ContactsWatcher {
static func previewInstance() -> Self { static func previewInstance() -> Self {
.init(watch: { _, _ in .ok(.init(contactList: [], handle: .init(noHandle: .init()))) }) .init(watch: { _, _ in .ok(.init(contactList: [], handle: .init(noPointer: .init()))) })
} }
} }
@@ -19,6 +19,6 @@ import proton_app_uniffi
extension MailUserSession { extension MailUserSession {
static func testInstance() -> MailUserSession { static func testInstance() -> MailUserSession {
.init(noHandle: .init()) .init(noPointer: .init())
} }
} }
@@ -39,7 +39,7 @@ final class ContactSuggestionsRepositoryTests {
let result = ContactSuggestionsStub(all: self.stubbedAllContacts) let result = ContactSuggestionsStub(all: self.stubbedAllContacts)
return .ok(result) return .ok(result)
}), }),
mailUserSession: MailUserSession(noHandle: .init()) mailUserSession: MailUserSession(noPointer: .init())
) )
} }
@@ -349,12 +349,12 @@ private class ContactSuggestionsStub: ContactSuggestions, @unchecked Sendable {
init(all: [ContactSuggestion]) { init(all: [ContactSuggestion]) {
_all = all _all = all
super.init(noHandle: .init()) super.init(noPointer: .init())
} }
@available(*, unavailable) @available(*, unavailable)
required init(unsafeFromHandle handle: UInt64) { required init(unsafeFromRawPointer pointer: UnsafeMutableRawPointer) {
fatalError("init(unsafeFromHandle:) has not been implemented") fatalError("init(unsafeFromRawPointer:) has not been implemented")
} }
override func all() -> [ContactSuggestion] { override func all() -> [ContactSuggestion] {
@@ -26,7 +26,7 @@ import proton_app_uniffi
final class ContactViewFactoryTests { final class ContactViewFactoryTests {
let sut = ContactViewFactory( let sut = ContactViewFactory(
apiConfig: .testData, apiConfig: .testData,
mailUserSession: .init(noHandle: .init()), mailUserSession: .init(noPointer: .init()),
draftPresenter: ContactsDraftPresenterDummy() draftPresenter: ContactsDraftPresenterDummy()
) )
@@ -627,7 +627,7 @@ final class ContactsStateStoreTests {
}, },
contactsWatcher: .init(watch: { [unowned self] _, callback in contactsWatcher: .init(watch: { [unowned self] _, callback in
watchContactsCallback = callback watchContactsCallback = callback
return WatchContactListResult.ok(.init(contactList: [], handle: .init(noHandle: .init()))) return WatchContactListResult.ok(.init(contactList: [], handle: .init(noPointer: .init())))
}) })
), ),
makeContactsLiveQuery: { [unowned self] in makeContactsLiveQuery: { [unowned self] in
@@ -23,7 +23,7 @@ import proton_app_uniffi
final class GroupedContactsRepositoryTests { final class GroupedContactsRepositoryTests {
private lazy var sut: GroupedContactsRepository = .init( private lazy var sut: GroupedContactsRepository = .init(
mailUserSession: MailUserSession(noHandle: .init()), mailUserSession: MailUserSession(noPointer: .init()),
contactsProvider: .init(allContacts: { _ in .ok(self.stubbedContacts) }) contactsProvider: .init(allContacts: { _ in .ok(self.stubbedContacts) })
) )
private var stubbedContacts: [GroupedContacts] = [] private var stubbedContacts: [GroupedContacts] = []
@@ -54,6 +54,26 @@ public extension ChallengeLoaderPutResult {
} }
} }
} }
public extension ConversationScrollerCategoryViewResult {
func get() throws(MailScrollerError) -> CategoryView {
switch self {
case .ok(let value):
value
case .error(let error):
throw error
}
}
}
public extension ConversationScrollerChangeCategoryViewResult {
func get() throws(MailScrollerError) {
switch self {
case .ok:
break
case .error(let error):
throw error
}
}
}
public extension ConversationScrollerChangeFilterResult { public extension ConversationScrollerChangeFilterResult {
func get() throws(MailScrollerError) { func get() throws(MailScrollerError) {
switch self { switch self {
@@ -1184,6 +1204,26 @@ public extension MailboxWatchUnreadCountResult {
} }
} }
} }
public extension MessageScrollerCategoryViewResult {
func get() throws(MailScrollerError) -> CategoryView {
switch self {
case .ok(let value):
value
case .error(let error):
throw error
}
}
}
public extension MessageScrollerChangeCategoryViewResult {
func get() throws(MailScrollerError) {
switch self {
case .ok:
break
case .error(let error):
throw error
}
}
}
public extension MessageScrollerChangeFilterResult { public extension MessageScrollerChangeFilterResult {
func get() throws(MailScrollerError) { func get() throws(MailScrollerError) {
switch self { switch self {
@@ -1344,96 +1384,6 @@ public extension NewMailboxResult {
} }
} }
} }
public extension PasswordFlowChangeMboxPassResult {
func get() throws(PasswordError) -> SimplePasswordState {
switch self {
case .ok(let value):
value
case .error(let error):
throw error
}
}
}
public extension PasswordFlowChangePassResult {
func get() throws(PasswordError) -> SimplePasswordState {
switch self {
case .ok(let value):
value
case .error(let error):
throw error
}
}
}
public extension PasswordFlowFidoDetailsResult {
func get() throws(PasswordError) -> Fido2ResponseFfi? {
switch self {
case .ok(let value):
value
case .error(let error):
throw error
}
}
}
public extension PasswordFlowHasFidoResult {
func get() throws(PasswordError) -> Bool {
switch self {
case .ok(let value):
value
case .error(let error):
throw error
}
}
}
public extension PasswordFlowHasMbpResult {
func get() throws(PasswordError) -> Bool {
switch self {
case .ok(let value):
value
case .error(let error):
throw error
}
}
}
public extension PasswordFlowHasTotpResult {
func get() throws(PasswordError) -> Bool {
switch self {
case .ok(let value):
value
case .error(let error):
throw error
}
}
}
public extension PasswordFlowStepBackResult {
func get() throws(PasswordError) -> SimplePasswordState {
switch self {
case .ok(let value):
value
case .error(let error):
throw error
}
}
}
public extension PasswordFlowSubmitFidoResult {
func get() throws(PasswordError) -> SimplePasswordState {
switch self {
case .ok(let value):
value
case .error(let error):
throw error
}
}
}
public extension PasswordFlowSubmitTotpResult {
func get() throws(PasswordError) -> SimplePasswordState {
switch self {
case .ok(let value):
value
case .error(let error):
throw error
}
}
}
public extension ResolveSystemLabelByIdResult { public extension ResolveSystemLabelByIdResult {
func get() throws(ProtonError) -> SystemLabel? { func get() throws(ProtonError) -> SystemLabel? {
switch self { switch self {
@@ -1564,116 +1514,6 @@ public extension SearchScrollerTotalResult {
} }
} }
} }
public extension SignupFlowAvailableCountriesResult {
func get() throws(SignupError) -> Countries {
switch self {
case .ok(let value):
value
case .error(let error):
throw error
}
}
}
public extension SignupFlowAvailableDomainsResult {
func get() throws(SignupError) -> [String] {
switch self {
case .ok(let value):
value
case .error(let error):
throw error
}
}
}
public extension SignupFlowCompleteResult {
func get() throws(SignupError) -> UserAddrId {
switch self {
case .ok(let value):
value
case .error(let error):
throw error
}
}
}
public extension SignupFlowCreateResult {
func get() throws(SignupError) -> SimpleSignupState {
switch self {
case .ok(let value):
value
case .error(let error):
throw error
}
}
}
public extension SignupFlowSkipRecoveryResult {
func get() throws(SignupError) -> SimpleSignupState {
switch self {
case .ok(let value):
value
case .error(let error):
throw error
}
}
}
public extension SignupFlowStepBackResult {
func get() throws(SignupError) -> SimpleSignupState {
switch self {
case .ok(let value):
value
case .error(let error):
throw error
}
}
}
public extension SignupFlowSubmitExternalUsernameResult {
func get() throws(SignupError) -> SimpleSignupState {
switch self {
case .ok(let value):
value
case .error(let error):
throw error
}
}
}
public extension SignupFlowSubmitInternalUsernameResult {
func get() throws(SignupError) -> SimpleSignupState {
switch self {
case .ok(let value):
value
case .error(let error):
throw error
}
}
}
public extension SignupFlowSubmitPasswordResult {
func get() throws(SignupError) -> SimpleSignupState {
switch self {
case .ok(let value):
value
case .error(let error):
throw error
}
}
}
public extension SignupFlowSubmitRecoveryEmailResult {
func get() throws(SignupError) -> SimpleSignupState {
switch self {
case .ok(let value):
value
case .error(let error):
throw error
}
}
}
public extension SignupFlowSubmitRecoveryPhoneResult {
func get() throws(SignupError) -> SimpleSignupState {
switch self {
case .ok(let value):
value
case .error(let error):
throw error
}
}
}
public extension UpdateNextMessageOnMoveResult { public extension UpdateNextMessageOnMoveResult {
func get() throws(UserSessionError) { func get() throws(UserSessionError) {
switch self { switch self {
@@ -4852,7 +4852,7 @@
"sl" : { "sl" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
"value" : "[Preverjanje pošiljatelja](https://proton.me/support/sender-verification-failed) je spodletelo, ker ni bilo podpisano. Prosimo, potrdite pristnost e-pošte pri svojemu stiku." "value" : "[Preverjanje pošiljatelja](https://proton.me/support/sender-verification-failed) je spodletelo, ker ni bilo podpisano. Potrdite pristnost e-pošte pri svojemu stiku."
} }
}, },
"sv" : { "sv" : {
@@ -5001,7 +5001,7 @@
"sl" : { "sl" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
"value" : "[Preverjanje pošiljatelja](https://proton.me/support/sender-verification-failed) ni uspelo. Prosimo, potrdite pristnost e-pošte pri svojemu stiku." "value" : "[Preverjanje pošiljatelja](https://proton.me/support/sender-verification-failed) ni uspelo. Potrdite pristnost e-pošte pri svojemu stiku."
} }
}, },
"sv" : { "sv" : {
@@ -5150,7 +5150,7 @@
"sl" : { "sl" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
"value" : "Sporočilo je na strežnikih Proton shranjeno z [šifriranjem brez dostopa](https://proton.me/blog/zero-access-encryption). Niti Proton niti nihče drug ga ne more prebrati." "value" : "Sporočilo je na strežnikih Proton shranjeno s [šifriranjem brez dostopa](https://proton.me/blog/zero-access-encryption). Niti Proton niti nihče drug ga ne more prebrati."
} }
}, },
"tr" : { "tr" : {
@@ -5293,7 +5293,7 @@
"sl" : { "sl" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
"value" : "Prejemnik je [preverjen stik](https://proton.me/support/address-verification), kateremu ste zaupali šifrirne ključe." "value" : "Prejemnik je [preverjen stik](https://proton.me/support/address-verification), njegove šifrirne ključe ste potrdili kot zaupanja vredne."
} }
}, },
"sv" : { "sv" : {
@@ -5442,7 +5442,7 @@
"sl" : { "sl" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
"value" : "Prejemniki so [preverjeni stiki](https://proton.me/support/address-verification), katerim ste zaupali šifrirne ključe." "value" : "Prejemniki so [preverjeni stiki](https://proton.me/support/address-verification), njihove šifrirne ključe ste potrdili kot zaupanja vredne."
} }
}, },
"sv" : { "sv" : {
@@ -5591,7 +5591,7 @@
"sl" : { "sl" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
"value" : "Pošiljatelj je [preverjen stik](https://proton.me/support/address-verification), kateremu ste zaupali šifrirne ključe." "value" : "Pošiljatelj je [preverjen stik](https://proton.me/support/address-verification), njegove šifrirne ključe ste potrdili kot zaupanja vredne."
} }
}, },
"sv" : { "sv" : {
@@ -5883,7 +5883,7 @@
"sl" : { "sl" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
"value" : "Ta e-pošta je na strežnikih Proton shranjena z [šifriranjem brez dostopa](https://proton.me/blog/zero-access-encryption). Niti Proton niti kdorkoli drug je ne more prebrati." "value" : "Ta e-pošta je na strežnikih Proton shranjena s [šifriranjem brez dostopa](https://proton.me/blog/zero-access-encryption). Niti Proton niti kdorkoli drug je ne more prebrati."
} }
}, },
"tr" : { "tr" : {
@@ -7016,7 +7016,7 @@
"sl" : { "sl" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
"value" : "Šifrirano brez dostopa za preverjenega prejemnika" "value" : "Šifrirano brez dostopa s preverjenim prejemnikom"
} }
}, },
"sv" : { "sv" : {
@@ -7159,7 +7159,7 @@
"sl" : { "sl" : {
"stringUnit" : { "stringUnit" : {
"state" : "translated", "state" : "translated",
"value" : "Šifrirano brez dostopa za preverjene prejemnike" "value" : "Šifrirano brez dostopa s preverjenimi prejemniki"
} }
}, },
"sv" : { "sv" : {
@@ -87,6 +87,7 @@ public extension DS.Icon {
public extension DS.Icon { public extension DS.Icon {
static let icBrandProtonMailUpsell = ImageResource.icBrandProtonMailUpsell static let icBrandProtonMailUpsell = ImageResource.icBrandProtonMailUpsell
static let icBrandProtonMailUpsellBlackAndWhite = ImageResource.icBrandProtonMailUpsellBw static let icBrandProtonMailUpsellBlackAndWhite = ImageResource.icBrandProtonMailUpsellBw
static let icBrandProtonUnlimitedUpsellHeader = ImageResource.icBrandProtonUnlimitedUpsellHeader
static let upsellBlackFridayHeaderButtonWave1 = ImageResource.upsellBlackFridayHeaderButtonWave1 static let upsellBlackFridayHeaderButtonWave1 = ImageResource.upsellBlackFridayHeaderButtonWave1
static let upsellBlackFridayHeaderButtonWave2 = ImageResource.upsellBlackFridayHeaderButtonWave2 static let upsellBlackFridayHeaderButtonWave2 = ImageResource.upsellBlackFridayHeaderButtonWave2
static let upsellBlackFridaySidebarItemWave1 = ImageResource.upsellBlackFridaySidebarItemWave1 static let upsellBlackFridaySidebarItemWave1 = ImageResource.upsellBlackFridaySidebarItemWave1
@@ -98,6 +99,9 @@ public extension DS.Icon {
public extension DS.Icon { public extension DS.Icon {
static let icCode = ImageResource.icCode static let icCode = ImageResource.icCode
static let icDiamond = ImageResource.icDiamond 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 icEnvelopeDot = ImageResource.icEnvelopeDot
static let icEnvelopeOpen = ImageResource.icEnvelopeOpen static let icEnvelopeOpen = ImageResource.icEnvelopeOpen
static let icFileLines = ImageResource.icFileLines static let icFileLines = ImageResource.icFileLines
@@ -41,6 +41,7 @@ public extension DS.Images {
public static let logoMobileSignature = ImageResource.upsellLogoMobileSignature public static let logoMobileSignature = ImageResource.upsellLogoMobileSignature
public static let logoScheduleSend = ImageResource.upsellLogoScheduleSend public static let logoScheduleSend = ImageResource.upsellLogoScheduleSend
public static let logoSnooze = ImageResource.upsellLogoSnooze public static let logoSnooze = ImageResource.upsellLogoSnooze
public static let logoUnlimited = ImageResource.upsellLogoUnlimited
public enum BlackFriday { public enum BlackFriday {
public static let background = ImageResource.upsellBlackFridayBackground public static let background = ImageResource.upsellBlackFridayBackground
@@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "Frame 944441499.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true
}
}
@@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "Plan icon.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "Icon.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "icon.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "upsell_logo_unlimited.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -95,7 +95,7 @@ public final class UpsellCoordinator: ObservableObject {
let availablePlans = try await fetchAvailablePlans() let availablePlans = try await fetchAvailablePlans()
let model = try upsellScreenFactory.upsellScreenModel( let model = try upsellScreenFactory.upsellScreenModel(
showingPlan: configuration.regularPlan, showingPlan: upsellType.planVariant,
basedOn: availablePlans, basedOn: availablePlans,
entryPoint: entryPoint, entryPoint: entryPoint,
upsellType: upsellType upsellType: upsellType
+15
View File
@@ -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") 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 { static func numberOfEmailAddresses(_ amount: UInt) -> LocalizedStringResource {
.init("\(amount) email addresses", bundle: .module, comment: "Number of email addresses available in a given plan") .init("\(amount) email addresses", bundle: .module, comment: "Number of email addresses available in a given plan")
} }
@@ -130,6 +136,15 @@ enum L10n {
comment: "Notice at the bottom of the upsell page" comment: "Notice at the bottom of the upsell page"
) )
} }
static func autoRenewalNotice(price: String, period: Product.SubscriptionPeriod.Unit) -> LocalizedStringResource {
let periodSuffix = "/\(period.localizedDescription.lowercased())"
return .init(
"Auto-renews at \(price)\(periodSuffix) unless canceled",
bundle: .module,
comment: "Disclaimer shown at the bottom of the upsell page; first placeholder is the localized price, second is the period suffix (/year or /month)"
)
}
} }
private extension LocalizedStringResource.BundleDescription { private extension LocalizedStringResource.BundleDescription {
@@ -758,6 +758,9 @@
} }
} }
}, },
"Auto-renews at %@%@ unless canceled" : {
"comment" : "Disclaimer shown at the bottom of the upsell page; first placeholder is the localized price, second is the period suffix (/year or /month)"
},
"Auto-renews at the same price and terms unless canceled" : { "Auto-renews at the same price and terms unless canceled" : {
"comment" : "Notice at the bottom", "comment" : "Notice at the bottom",
"localizations" : { "localizations" : {
@@ -2626,6 +2629,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 %@." : { "Deletes spam and trash after 30 days. Get this and more with %@." : {
"comment" : "Subtitle of the upsell page", "comment" : "Subtitle of the upsell page",
"localizations" : { "localizations" : {
@@ -3169,6 +3175,9 @@
} }
} }
}, },
"Folders and labels" : {
"comment" : "Description of a feature of a paid subscription"
},
"Free" : { "Free" : {
"comment" : "Name of the free plan", "comment" : "Name of the free plan",
"localizations" : { "localizations" : {
@@ -3742,6 +3751,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 %@." : { "Make your mobile signature your own. Enjoy this and more with %@." : {
"comment" : "Subtitle of the upsell page", "comment" : "Subtitle of the upsell page",
"localizations" : { "localizations" : {
@@ -5037,6 +5049,9 @@
} }
} }
}, },
"Premium VPN, password manager and cloud storage" : {
"comment" : "Description of a feature of a paid subscription"
},
"Priority customer support" : { "Priority customer support" : {
"comment" : "Description of a feature of a paid subscription", "comment" : "Description of a feature of a paid subscription",
"localizations" : { "localizations" : {
@@ -64,7 +64,12 @@ final class TelemetryReporter: TelemetryReporting {
} }
private func generalDimensions() -> GeneralDimensions { private func generalDimensions() -> GeneralDimensions {
.init(upsellEntryPoint: entryPoint!, planBeforeUpgrade: "free", modalVariant: .comparisonPlus) .init(
upsellEntryPoint: entryPoint!,
planBeforeUpgrade: "free",
modalVariant: .comparisonPlus,
upsellFeatureFlags: upsellFeatureFlagsForIos()
)
} }
private func planSpecificDimensions(storeKitProductID: String) -> PlanSpecificDimensions { private func planSpecificDimensions(storeKitProductID: String) -> PlanSpecificDimensions {

Some files were not shown because too many files have changed in this diff Show More