Merge branch 'main' into tech/ET-6087
# Conflicts: # Modules/App/Tests/Tests/Snapshots/Screens/Sidebar/SidebarScreenSnapshotTests.swift
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"project": "apple-mail-new",
|
||||
"locale": "0d39f2e91da281e1a30dfc158cc6c4257d0a7d5a"
|
||||
"locale": "c3c613cfc201210c3d95bdda446c74a329ffc290"
|
||||
}
|
||||
@@ -61,7 +61,7 @@ final class SenderImageAPIDataSource: Sendable, SenderImageDataSource {
|
||||
address: params.address,
|
||||
bimiSelector: params.bimiSelector,
|
||||
displaySenderImage: params.displaySenderImage,
|
||||
size: 128,
|
||||
size: .s128,
|
||||
mode: colorScheme == .dark ? "dark" : "light",
|
||||
format: "png"
|
||||
)
|
||||
|
||||
@@ -46,7 +46,7 @@ extension SystemLabel {
|
||||
L10n.Mailbox.SystemFolder.scheduled
|
||||
case .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")
|
||||
}
|
||||
}
|
||||
@@ -75,7 +75,7 @@ extension SystemLabel {
|
||||
DS.Icon.icClockPaperPlane.image
|
||||
case .snoozed:
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,8 +32,4 @@ extension UpsellConfiguration {
|
||||
false
|
||||
#endif
|
||||
}
|
||||
|
||||
var humanReadableUpsoldPlanName: String {
|
||||
"Mail Plus"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57066,7 +57066,7 @@
|
||||
"sl" : {
|
||||
"stringUnit" : {
|
||||
"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" : {
|
||||
|
||||
@@ -397,6 +397,8 @@ extension MailboxModel {
|
||||
showScrollerErrorIfNotNetwork(error: error)
|
||||
let isLastPage = await !conversationScrollerHasMore()
|
||||
paginatedDataSource.handle(update: .init(isLastPage: isLastPage, value: .error(error), completion: nil))
|
||||
case .categoryViewChanged:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@@ -443,6 +445,8 @@ extension MailboxModel {
|
||||
showScrollerErrorIfNotNetwork(error: error)
|
||||
let isLastPage = await !messageScrollerHasMore()
|
||||
paginatedDataSource.handle(update: .init(isLastPage: isLastPage, value: .error(error), completion: nil))
|
||||
case .categoryViewChanged:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -160,7 +160,7 @@ extension DraftPresenter {
|
||||
undoScheduleSendProvider: UndoScheduleSendProvider = .mockInstance
|
||||
) -> DraftPresenter {
|
||||
.init(
|
||||
userSession: .init(noHandle: .init()),
|
||||
userSession: .init(noPointer: .init()),
|
||||
draftProvider: .dummy,
|
||||
undoSendProvider: undoSendProvider,
|
||||
undoScheduleSendProvider: undoScheduleSendProvider
|
||||
|
||||
@@ -28,6 +28,6 @@ extension DraftProvider {
|
||||
}
|
||||
|
||||
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 {
|
||||
static var dummy: Undo {
|
||||
Undo(noHandle: .init())
|
||||
Undo(noPointer: .init())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ enum LabelAsSheetPreviewProvider {
|
||||
static func testData() -> LabelAsSheetModel {
|
||||
.init(
|
||||
input: .init(sheetType: .labelAs, ids: [], mailboxItem: .message(isLastMessageInCurrentLocation: false)),
|
||||
mailbox: .init(noHandle: .init()),
|
||||
mailbox: .init(noPointer: .init()),
|
||||
availableLabelAsActions: .init(
|
||||
message: { _, _ in .ok(testLabels()) },
|
||||
conversation: { _, _ in .ok([]) }
|
||||
|
||||
@@ -19,6 +19,6 @@ import proton_app_uniffi
|
||||
|
||||
extension MailUserSession {
|
||||
static var dummy: MailUserSession {
|
||||
.init(noHandle: .init())
|
||||
.init(noPointer: .init())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,6 @@ import proton_app_uniffi
|
||||
|
||||
extension Mailbox {
|
||||
static var dummy: Mailbox {
|
||||
.init(noHandle: .init())
|
||||
.init(noPointer: .init())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,8 +126,8 @@ struct AppProtectionSelectionScreen: View {
|
||||
.init(type: .faceID, isSelected: true),
|
||||
]
|
||||
),
|
||||
appSettingsRepository: MailSession(noHandle: .init()),
|
||||
appProtectionConfigurator: MailSession(noHandle: .init())
|
||||
appSettingsRepository: MailSession(noPointer: .init()),
|
||||
appProtectionConfigurator: MailSession(noPointer: .init())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,7 +199,7 @@ struct AppSettingsScreen: View {
|
||||
NavigationStack {
|
||||
AppSettingsScreen(
|
||||
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 {
|
||||
switch self {
|
||||
case .inbox, .allDrafts, .allSent, .sent, .trash, .spam, .allMail, .archive, .drafts, .starred, .scheduled,
|
||||
.almostAllMail, .snoozed, .categorySocial, .categoryPromotions, .catergoryUpdates, .categoryForums,
|
||||
.categoryDefault, .blocked, .pinned:
|
||||
.almostAllMail, .snoozed, .categorySocial, .categoryPromotions, .categoryUpdates, .categoryForums,
|
||||
.categoryDefault, .blocked, .pinned, .categoryNewsletter, .categoryTransactions:
|
||||
.mailbox(isUnreadFilterOn: isUnreadFilterOn)
|
||||
case .outbox:
|
||||
.outbox
|
||||
|
||||
@@ -197,5 +197,5 @@ struct ReportProblemScreen: View {
|
||||
}
|
||||
|
||||
#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)
|
||||
let isLastPage = await !searchScrollerHasMore()
|
||||
paginatedDataSource.handle(update: .init(isLastPage: isLastPage, value: .error(error), completion: nil))
|
||||
case .categoryViewChanged:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -324,7 +324,7 @@ private extension SettingsPreference {
|
||||
#Preview {
|
||||
NavigationStack {
|
||||
SettingsScreen(
|
||||
mailUserSession: MailUserSession(noHandle: .init()),
|
||||
mailUserSession: MailUserSession(noPointer: .init()),
|
||||
accountAuthCoordinator: .mock(),
|
||||
upsellCoordinator: .init(
|
||||
mailUserSession: .dummy,
|
||||
|
||||
@@ -232,8 +232,6 @@ struct SidebarScreen: View {
|
||||
|
||||
@ViewBuilder
|
||||
private func upsellSidebarItem(item: SidebarItem, upsellType: UpsellType) -> some View {
|
||||
let planName = UpsellConfiguration.mail.humanReadableUpsoldPlanName
|
||||
|
||||
SidebarItemButton(
|
||||
item: item,
|
||||
isTappable: isButtonTappable,
|
||||
@@ -241,7 +239,7 @@ struct SidebarScreen: View {
|
||||
content: {
|
||||
HStack(spacing: .zero) {
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -370,11 +368,11 @@ struct SidebarScreen: View {
|
||||
.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)
|
||||
.font(.subheadline)
|
||||
.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)
|
||||
.accessibilityIdentifier(SidebarScreenIdentifiers.textItem)
|
||||
}
|
||||
@@ -455,24 +453,25 @@ private extension SidebarOtherItem {
|
||||
}
|
||||
|
||||
private extension UpsellType {
|
||||
var icon: ImageResource {
|
||||
var planName: String {
|
||||
switch self {
|
||||
case .mailPlus, .unlimited:
|
||||
DS.Icon.icDiamond
|
||||
case .mailPlus:
|
||||
"Mail Plus"
|
||||
case .unlimited:
|
||||
"Unlimited"
|
||||
}
|
||||
}
|
||||
|
||||
func title(planName: String) -> String {
|
||||
var icon: ImageResource {
|
||||
switch self {
|
||||
case .mailPlus, .unlimited:
|
||||
case .mailPlus:
|
||||
DS.Icon.icDiamond
|
||||
case .unlimited:
|
||||
DS.Icon.icInfinity
|
||||
}
|
||||
}
|
||||
|
||||
var title: String {
|
||||
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 {
|
||||
ToolbarItem(placement: .topBarTrailing) {
|
||||
upsellButton(for: upsellType)
|
||||
.frame(width: 26, height: 26)
|
||||
.clipShape(.circle)
|
||||
}
|
||||
}
|
||||
ToolbarItem(placement: .topBarTrailing) {
|
||||
@@ -111,10 +113,10 @@ struct MainToolbar<AvatarView: View>: ViewModifier {
|
||||
if !selectionMode.hasItems {
|
||||
ToolbarItemGroup(placement: .topBarTrailing) {
|
||||
HStack(spacing: DS.Spacing.standard) {
|
||||
searchButton
|
||||
if case .eligible(let upsellType) = upsellEligibility {
|
||||
upsellButton(for: upsellType)
|
||||
}
|
||||
searchButton
|
||||
avatarView()
|
||||
}
|
||||
}
|
||||
@@ -141,8 +143,29 @@ struct MainToolbar<AvatarView: View>: ViewModifier {
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func upsellButton(for upsellType: UpsellType) -> some View {
|
||||
switch upsellType {
|
||||
case .mailPlus:
|
||||
Button(L10n.MainToolbar.upgrade, image: upsellType.icon) {
|
||||
performUpsellAction(upsellType: upsellType)
|
||||
}
|
||||
case .unlimited:
|
||||
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)
|
||||
@@ -153,7 +176,6 @@ struct MainToolbar<AvatarView: View>: ViewModifier {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
@MainActor
|
||||
@@ -203,8 +225,10 @@ enum MainToolbarEvent {
|
||||
private extension UpsellType {
|
||||
var icon: ImageResource {
|
||||
switch self {
|
||||
case .mailPlus, .unlimited:
|
||||
case .mailPlus:
|
||||
DS.Icon.icBrandProtonMailUpsellBlackAndWhite
|
||||
case .unlimited:
|
||||
DS.Icon.icBrandProtonUnlimitedUpsellHeader
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,11 +26,11 @@ final class MailboxStub: Mailbox, @unchecked Sendable {
|
||||
|
||||
init(viewMode: ViewMode) {
|
||||
self._viewMode = viewMode
|
||||
super.init(noHandle: .init())
|
||||
super.init(noPointer: .init())
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init(unsafeFromHandle handle: UInt64) {
|
||||
fatalError("init(unsafeFromHandle:) has not been implemented")
|
||||
required init(unsafeFromRawPointer pointer: UnsafeMutableRawPointer) {
|
||||
fatalError("init(unsafeFromRawPointer:) has not been implemented")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,6 +67,6 @@ final class SidebarSpy: SidebarProtocol, @unchecked Sendable {
|
||||
let mappedIndex: CallbackIndex = lastIndex == -1 ? .folder : .init(rawValue: lastIndex + 1)!
|
||||
spiedWatchers[mappedIndex] = callback
|
||||
|
||||
return .ok(WatchHandleDummy(noHandle: .init()))
|
||||
return .ok(WatchHandleDummy(noPointer: .init()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,6 @@ import proton_app_uniffi
|
||||
|
||||
extension MailUserSession {
|
||||
static var dummy: MailUserSession {
|
||||
.init(noHandle: .init())
|
||||
.init(noPointer: .init())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,6 @@ import proton_app_uniffi
|
||||
|
||||
extension Mailbox {
|
||||
static var dummy: Mailbox {
|
||||
.init(noHandle: .init())
|
||||
.init(noPointer: .init())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ final class MailboxItemCellSnapshotTests {
|
||||
("expiration_time_message", .makeSimpleMessage(type: .expirationTime)),
|
||||
])
|
||||
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
|
||||
class SettingsScreenSnapshotTests: BaseTestCase {
|
||||
func testSettingsScreenLayoutsCorrectOnIphoneX() {
|
||||
let store = AppAppearanceStore(mailSession: { MailSession(noHandle: .init()) })
|
||||
let store = AppAppearanceStore(mailSession: { MailSession(noPointer: .init()) })
|
||||
let mailUserSession = MailUserSessionSpy(id: "")
|
||||
mailUserSession.stubbedAccountDetails = .testData
|
||||
mailUserSession.stubbedUser = .testData
|
||||
|
||||
@@ -37,9 +37,10 @@ final class SidebarScreenSnapshotTests {
|
||||
createFolder: .createFolder
|
||||
)
|
||||
|
||||
@Test(arguments: [UIUserInterfaceStyle.light, .dark])
|
||||
func testSidebarWithDataLayoutsCorrectOnIphoneX(style: UIUserInterfaceStyle) {
|
||||
@Test(arguments: [UIUserInterfaceStyle.light, .dark], [UpsellType.mailPlus, .unlimited])
|
||||
func testSidebarWithDataLayoutsCorrectOnIphoneX(style: UIUserInterfaceStyle, upsellType: UpsellType) {
|
||||
var state = self.state
|
||||
state.upsell = .upsell(upsellType)
|
||||
|
||||
state.folders = [SidebarCustomFolder.topSecretFolder].map(\.sidebarFolder)
|
||||
state.system = [PMSystemLabel.inbox, .sent, .outbox].compactMap(\.sidebarSystemFolder)
|
||||
@@ -48,7 +49,7 @@ final class SidebarScreenSnapshotTests {
|
||||
let screenModel = SidebarModel(
|
||||
state: state,
|
||||
sidebar: SidebarSpy(),
|
||||
upsellEligibilityPublisher: .init(constant: .eligible(.mailPlus))
|
||||
upsellEligibilityPublisher: .init(constant: .eligible(upsellType))
|
||||
)
|
||||
let sidebarScreen = SidebarScreen(
|
||||
screenModel: screenModel,
|
||||
@@ -57,7 +58,7 @@ final class SidebarScreenSnapshotTests {
|
||||
)
|
||||
.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])
|
||||
|
||||
|
Before Width: | Height: | Size: 149 KiB After Width: | Height: | Size: 149 KiB |
|
Before Width: | Height: | Size: 156 KiB After Width: | Height: | Size: 156 KiB |
|
After Width: | Height: | Size: 149 KiB |
|
After Width: | Height: | Size: 156 KiB |
@@ -25,12 +25,24 @@ import proton_app_uniffi
|
||||
@testable import InboxIAP
|
||||
|
||||
@MainActor
|
||||
@Suite(.disabled("Tests are failing on CI only, needs more investigation."))
|
||||
struct UpsellScreenSnapshotTests {
|
||||
enum SelectedCycle: String {
|
||||
case yearly
|
||||
case monthly
|
||||
|
||||
var lengthInMonths: Int {
|
||||
switch self {
|
||||
case .yearly: 12
|
||||
case .monthly: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct TestCase {
|
||||
let label: String
|
||||
let config: ViewImageConfig
|
||||
let upsellType: UpsellType
|
||||
let selectedCycle: SelectedCycle
|
||||
}
|
||||
|
||||
nonisolated private static let testCases: [TestCase] = {
|
||||
@@ -42,28 +54,40 @@ struct UpsellScreenSnapshotTests {
|
||||
]
|
||||
|
||||
let upsellTypes: [UpsellType] = [.mailPlus, .unlimited]
|
||||
let selectedCycles: [SelectedCycle] = [.yearly, .monthly]
|
||||
|
||||
return orientations.flatMap { orientation in
|
||||
devices.flatMap { device in
|
||||
upsellTypes.map { upsellType in
|
||||
upsellTypes.flatMap { upsellType in
|
||||
selectedCycles.map { selectedCycle in
|
||||
.init(
|
||||
label: "\(device.label)_\(orientation)_\(upsellType.label)",
|
||||
label: "\(device.label)_\(orientation)_\(upsellType.label)_\(selectedCycle.rawValue)",
|
||||
config: device.configFactory(orientation),
|
||||
upsellType: upsellType
|
||||
upsellType: upsellType,
|
||||
selectedCycle: selectedCycle
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
@Test(arguments: testCases)
|
||||
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)
|
||||
viewController.view.backgroundColor = .black
|
||||
viewController.overrideUserInterfaceStyle = .dark
|
||||
|
||||
let strategy: Snapshotting<UIViewController, UIImage> = .image(
|
||||
on: testCase.config,
|
||||
drawHierarchyInKeyWindow: true
|
||||
drawHierarchyInKeyWindow: true,
|
||||
precision: 0.99
|
||||
)
|
||||
|
||||
assertSnapshot(of: viewController, as: strategy, named: testCase.label)
|
||||
|
||||
|
Before Width: | Height: | Size: 995 KiB |
|
After Width: | Height: | Size: 995 KiB |
|
After Width: | Height: | Size: 998 KiB |
|
Before Width: | Height: | Size: 995 KiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.7 MiB After Width: | Height: | Size: 1.8 MiB |
|
Before Width: | Height: | Size: 1.7 MiB After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 2.3 MiB |
|
After Width: | Height: | Size: 2.3 MiB |
|
Before Width: | Height: | Size: 812 KiB |
|
After Width: | Height: | Size: 818 KiB |
|
After Width: | Height: | Size: 818 KiB |
|
Before Width: | Height: | Size: 812 KiB |
|
After Width: | Height: | Size: 885 KiB |
|
After Width: | Height: | Size: 887 KiB |
|
Before Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
Before Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 1.8 MiB |
@@ -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)
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 150 KiB |
|
After Width: | Height: | Size: 146 KiB |
|
After Width: | Height: | Size: 165 KiB |
|
After Width: | Height: | Size: 160 KiB |
|
After Width: | Height: | Size: 149 KiB |
|
After Width: | Height: | Size: 142 KiB |
@@ -35,7 +35,7 @@ final class SceneDelegateTests: BaseTestCase {
|
||||
sut = .init()
|
||||
mailSessionSpy = .init()
|
||||
sut.appProtectionStore = .init(mailSession: { self.mailSessionSpy })
|
||||
sut.mailSessionFactory = { .init(noHandle: .init()) }
|
||||
sut.mailSessionFactory = { .init(noPointer: .init()) }
|
||||
sut.checkAutoLockSetting = { completion in completion(self.shouldAutoLockStub) }
|
||||
sut.transitionAnimation = { _, _, _, animation, completion in
|
||||
animation?()
|
||||
|
||||
@@ -33,7 +33,7 @@ final class DeviceTokenRegistrarTests {
|
||||
var lastCreatedHandle: RegisterDeviceTaskHandleSpy?
|
||||
|
||||
mailSession.stubbedRegisterDeviceTaskHandleFactory = {
|
||||
let newHandle = RegisterDeviceTaskHandleSpy(noHandle: .init())
|
||||
let newHandle = RegisterDeviceTaskHandleSpy(noPointer: .init())
|
||||
lastCreatedHandle = newHandle
|
||||
return newHandle
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ final class DraftPresenterTests: BaseTestCase, @unchecked Sendable {
|
||||
|
||||
@MainActor
|
||||
func testOpenDraftWithContact_ItCreatesEmptyDraftAddRecipientAndOpensDraft() async throws {
|
||||
let draftSpy = DraftSpy(noHandle: .init())
|
||||
let draftSpy = DraftSpy(noPointer: .init())
|
||||
sut = makeSUT(stubbedNewDraftResult: .ok(draftSpy))
|
||||
|
||||
var capturedDraftToPresent: [DraftToPresent] = []
|
||||
@@ -123,7 +123,7 @@ final class DraftPresenterTests: BaseTestCase, @unchecked Sendable {
|
||||
|
||||
@MainActor
|
||||
func testOpenDraftWithContactGroup_ItCreatesEmptyDraftAddGroupAndOpensDraft() async throws {
|
||||
let draftSpy = DraftSpy(noHandle: .init())
|
||||
let draftSpy = DraftSpy(noPointer: .init())
|
||||
sut = makeSUT(stubbedNewDraftResult: .ok(draftSpy))
|
||||
|
||||
var capturedDraftToPresent: [DraftToPresent] = []
|
||||
@@ -292,13 +292,13 @@ extension DraftPresenterTests {
|
||||
}
|
||||
|
||||
private extension Draft {
|
||||
static var dummyDraft: Draft { .init(noHandle: .init()) }
|
||||
static var dummyDraft: Draft { .init(noPointer: .init()) }
|
||||
}
|
||||
|
||||
private class DraftSpy: Draft, @unchecked Sendable {
|
||||
let toRecipientsCalls: ComposerRecipientListSpy = .init(noHandle: .init())
|
||||
let ccRecipientsCalls: ComposerRecipientListSpy = .init(noHandle: .init())
|
||||
let bccRecipientsCalls: ComposerRecipientListSpy = .init(noHandle: .init())
|
||||
let toRecipientsCalls: ComposerRecipientListSpy = .init(noPointer: .init())
|
||||
let ccRecipientsCalls: ComposerRecipientListSpy = .init(noPointer: .init())
|
||||
let bccRecipientsCalls: ComposerRecipientListSpy = .init(noPointer: .init())
|
||||
private(set) var setSubjectCalls: [String] = []
|
||||
private(set) var setBodyCalls: [String] = []
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ final class LabelAsSheetModelTests {
|
||||
private var invokedAvailableActionsWithConversationIDs: [ID] = []
|
||||
private var invokedDismissCount = 0
|
||||
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 invokedLabelMessage: [LabelAsExecutedWithData] = []
|
||||
@@ -175,7 +175,7 @@ final class LabelAsSheetModelTests {
|
||||
private func sut(ids: [ID], type: MailboxItemType) -> LabelAsSheetModel {
|
||||
LabelAsSheetModel(
|
||||
input: .init(sheetType: .labelAs, ids: ids, mailboxItem: type.mailboxItem),
|
||||
mailbox: .init(noHandle: .init()),
|
||||
mailbox: .init(noPointer: .init()),
|
||||
availableLabelAsActions: .init(
|
||||
message: { _, ids in
|
||||
self.invokedAvailableActionsWithMessagesIDs = ids
|
||||
@@ -220,7 +220,7 @@ final class LabelAsSheetModelTests {
|
||||
itemType: MailboxItemType,
|
||||
spyToVerify: () -> [LabelAsExecutedWithData]
|
||||
) async throws {
|
||||
let undoSpy = UndoSpy(noHandle: .init())
|
||||
let undoSpy = UndoSpy(noPointer: .init())
|
||||
let selectedLabelID: ID = .init(value: 2)
|
||||
let partiallySelectedLabelID: ID = .init(value: 4)
|
||||
stubbedLabelAsActions = [
|
||||
@@ -257,7 +257,7 @@ final class LabelAsSheetModelTests {
|
||||
spyToVerify: () -> [LabelAsExecutedWithData],
|
||||
expectToastMessage: LocalizedStringResource
|
||||
) async throws {
|
||||
let undoSpy = UndoSpy(noHandle: .init())
|
||||
let undoSpy = UndoSpy(noPointer: .init())
|
||||
let selectedLabelID: ID = .init(value: 2)
|
||||
let partiallySelectedLabelID: ID = .init(value: 4)
|
||||
stubbedLabelAsActions = [
|
||||
|
||||
@@ -259,7 +259,7 @@ class ListActionsToolbarStoreTests {
|
||||
func action_WhenMoveToInboxIsTappedUndoIsAvailbleAndTapped_ItTriggersUndoAndDismissesToast() async throws {
|
||||
let ids: [ID] = [.init(value: 7), .init(value: 77)]
|
||||
let systemFolder = MovableSystemFolderAction.testInbox
|
||||
let undoSpy = UndoSpy(noHandle: .init())
|
||||
let undoSpy = UndoSpy(noPointer: .init())
|
||||
let viewMode = ViewMode.messages
|
||||
moveToActionsSpy.stubbedMoveMessagesToOkResult = undoSpy
|
||||
sut = makeSUT(viewMode: viewMode)
|
||||
|
||||
@@ -29,7 +29,7 @@ final class MoveToActionPerformerTests: BaseTestCase {
|
||||
super.setUp()
|
||||
|
||||
sut = .init(
|
||||
mailbox: .init(noHandle: .init()),
|
||||
mailbox: .init(noPointer: .init()),
|
||||
moveToActions: .init(
|
||||
moveMessagesTo: { [unowned self] _, _, _ in stubbedResult },
|
||||
moveConversationsTo: { [unowned self] _, _, _ in stubbedResult }
|
||||
|
||||
@@ -108,7 +108,7 @@ final class MoveToSheetStateStoreTests {
|
||||
|
||||
@Test
|
||||
func action_WhenInboxIsTappedUndoIsAvailableAndTapped_ItTriggersUndoAndDismissesToast() async throws {
|
||||
let undoSpy = UndoSpy(noHandle: .init())
|
||||
let undoSpy = UndoSpy(noPointer: .init())
|
||||
moveToActionsSpy.stubbedMoveMessagesToOkResult = undoSpy
|
||||
|
||||
let sut = sut(
|
||||
@@ -155,7 +155,7 @@ final class MoveToSheetStateStoreTests {
|
||||
.init(
|
||||
state: .initial,
|
||||
input: input,
|
||||
mailbox: .init(noHandle: .init()),
|
||||
mailbox: .init(noPointer: .init()),
|
||||
availableMoveToActions: .init(
|
||||
message: { _, ids in
|
||||
self.invokedAvailableActionsWithMessagesIDs = ids
|
||||
|
||||
@@ -566,12 +566,12 @@ private final class DecryptedMessageSpy: DecryptedMessage, @unchecked Sendable {
|
||||
|
||||
init(stubbedOptions: TransformOpts) {
|
||||
self.stubbedOptions = stubbedOptions
|
||||
super.init(noHandle: .init())
|
||||
super.init(noPointer: .init())
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init(unsafeFromHandle handle: UInt64) {
|
||||
fatalError("init(unsafeFromHandle:) has not been implemented")
|
||||
required init(unsafeFromRawPointer pointer: UnsafeMutableRawPointer) {
|
||||
fatalError("init(unsafeFromRawPointer:) has not been implemented")
|
||||
}
|
||||
|
||||
private(set) var bodyWithOptionsCalls: [TransformOpts] = []
|
||||
|
||||
@@ -333,7 +333,7 @@ final class MessageAddressActionViewStateStoreTests {
|
||||
senderUnblocker: .init(
|
||||
mailbox: .dummy,
|
||||
wrapper: .init(
|
||||
messageBody: { _, _ in .ok(.init(noHandle: .init())) },
|
||||
messageBody: { _, _ in .ok(.init(noPointer: .init())) },
|
||||
markMessageHam: { _, _ in .ok },
|
||||
unblockSender: { _, emailAddress in
|
||||
await self.unblockSpy.result(for: emailAddress)
|
||||
|
||||
@@ -235,12 +235,12 @@ class BackgroundExecutionHandleStub: BackgroundExecutionHandle, @unchecked Senda
|
||||
private(set) var abortCalls: [Bool] = []
|
||||
|
||||
init() {
|
||||
super.init(noHandle: .init())
|
||||
super.init(noPointer: .init())
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init(unsafeFromHandle handle: UInt64) {
|
||||
fatalError("init(unsafeFromHandle:) has not been implemented")
|
||||
required init(unsafeFromRawPointer pointer: UnsafeMutableRawPointer) {
|
||||
fatalError("init(unsafeFromRawPointer:) has not been implemented")
|
||||
}
|
||||
|
||||
// MARK: - BackgroundExecutionHandle
|
||||
|
||||
@@ -127,7 +127,7 @@ struct ComposerLoadingView: View {
|
||||
ComposerScreen(
|
||||
draft: .emptyMock,
|
||||
draftOrigin: .new,
|
||||
dependencies: .init(contactProvider: .mockInstance, userSession: .init(noHandle: .init())),
|
||||
dependencies: .init(contactProvider: .mockInstance, userSession: .init(noPointer: .init())),
|
||||
onDismiss: { _ in }
|
||||
)
|
||||
.environmentObject(toastStateStore)
|
||||
|
||||
@@ -307,6 +307,6 @@ final class MockAttachmentList: AttachmentListProtocol, @unchecked Sendable {
|
||||
}
|
||||
|
||||
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 {
|
||||
static func empty() -> MailUserSession {
|
||||
MailUserSession(noHandle: .init())
|
||||
MailUserSession(noPointer: .init())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,12 +150,12 @@ private class ContactSuggestionsStub: ContactSuggestions, @unchecked Sendable {
|
||||
|
||||
init(all: [ContactSuggestion]) {
|
||||
_all = all
|
||||
super.init(noHandle: .init())
|
||||
super.init(noPointer: .init())
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init(unsafeFromHandle handle: UInt64) {
|
||||
fatalError("init(unsafeFromHandle:) has not been implemented")
|
||||
required init(unsafeFromRawPointer pointer: UnsafeMutableRawPointer) {
|
||||
fatalError("init(unsafeFromRawPointer:) has not been implemented")
|
||||
}
|
||||
|
||||
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 {
|
||||
func get() throws(ActionError) -> Id {
|
||||
switch self {
|
||||
|
||||
@@ -139,7 +139,7 @@ public struct ContactsScreen: View {
|
||||
#Preview {
|
||||
ContactsScreen(
|
||||
apiConfig: .debugPreview,
|
||||
mailUserSession: .init(noHandle: .init()),
|
||||
mailUserSession: .init(noPointer: .init()),
|
||||
contactsProvider: .previewInstance(),
|
||||
contactsWatcher: .previewInstance(),
|
||||
draftPresenter: ContactsDraftPresenterDummy()
|
||||
|
||||
@@ -19,6 +19,6 @@ import proton_app_uniffi
|
||||
|
||||
extension ContactsWatcher {
|
||||
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 {
|
||||
static func testInstance() -> MailUserSession {
|
||||
.init(noHandle: .init())
|
||||
.init(noPointer: .init())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ final class ContactSuggestionsRepositoryTests {
|
||||
let result = ContactSuggestionsStub(all: self.stubbedAllContacts)
|
||||
return .ok(result)
|
||||
}),
|
||||
mailUserSession: MailUserSession(noHandle: .init())
|
||||
mailUserSession: MailUserSession(noPointer: .init())
|
||||
)
|
||||
}
|
||||
|
||||
@@ -349,12 +349,12 @@ private class ContactSuggestionsStub: ContactSuggestions, @unchecked Sendable {
|
||||
|
||||
init(all: [ContactSuggestion]) {
|
||||
_all = all
|
||||
super.init(noHandle: .init())
|
||||
super.init(noPointer: .init())
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init(unsafeFromHandle handle: UInt64) {
|
||||
fatalError("init(unsafeFromHandle:) has not been implemented")
|
||||
required init(unsafeFromRawPointer pointer: UnsafeMutableRawPointer) {
|
||||
fatalError("init(unsafeFromRawPointer:) has not been implemented")
|
||||
}
|
||||
|
||||
override func all() -> [ContactSuggestion] {
|
||||
|
||||
@@ -26,7 +26,7 @@ import proton_app_uniffi
|
||||
final class ContactViewFactoryTests {
|
||||
let sut = ContactViewFactory(
|
||||
apiConfig: .testData,
|
||||
mailUserSession: .init(noHandle: .init()),
|
||||
mailUserSession: .init(noPointer: .init()),
|
||||
draftPresenter: ContactsDraftPresenterDummy()
|
||||
)
|
||||
|
||||
|
||||
@@ -627,7 +627,7 @@ final class ContactsStateStoreTests {
|
||||
},
|
||||
contactsWatcher: .init(watch: { [unowned self] _, callback in
|
||||
watchContactsCallback = callback
|
||||
return WatchContactListResult.ok(.init(contactList: [], handle: .init(noHandle: .init())))
|
||||
return WatchContactListResult.ok(.init(contactList: [], handle: .init(noPointer: .init())))
|
||||
})
|
||||
),
|
||||
makeContactsLiveQuery: { [unowned self] in
|
||||
|
||||
@@ -23,7 +23,7 @@ import proton_app_uniffi
|
||||
|
||||
final class GroupedContactsRepositoryTests {
|
||||
private lazy var sut: GroupedContactsRepository = .init(
|
||||
mailUserSession: MailUserSession(noHandle: .init()),
|
||||
mailUserSession: MailUserSession(noPointer: .init()),
|
||||
contactsProvider: .init(allContacts: { _ in .ok(self.stubbedContacts) })
|
||||
)
|
||||
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 {
|
||||
func get() throws(MailScrollerError) {
|
||||
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 {
|
||||
func get() throws(MailScrollerError) {
|
||||
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 {
|
||||
func get() throws(ProtonError) -> SystemLabel? {
|
||||
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 {
|
||||
func get() throws(UserSessionError) {
|
||||
switch self {
|
||||
|
||||
@@ -4852,7 +4852,7 @@
|
||||
"sl" : {
|
||||
"stringUnit" : {
|
||||
"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" : {
|
||||
@@ -5001,7 +5001,7 @@
|
||||
"sl" : {
|
||||
"stringUnit" : {
|
||||
"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" : {
|
||||
@@ -5150,7 +5150,7 @@
|
||||
"sl" : {
|
||||
"stringUnit" : {
|
||||
"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" : {
|
||||
@@ -5293,7 +5293,7 @@
|
||||
"sl" : {
|
||||
"stringUnit" : {
|
||||
"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" : {
|
||||
@@ -5442,7 +5442,7 @@
|
||||
"sl" : {
|
||||
"stringUnit" : {
|
||||
"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" : {
|
||||
@@ -5591,7 +5591,7 @@
|
||||
"sl" : {
|
||||
"stringUnit" : {
|
||||
"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" : {
|
||||
@@ -5883,7 +5883,7 @@
|
||||
"sl" : {
|
||||
"stringUnit" : {
|
||||
"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" : {
|
||||
@@ -7016,7 +7016,7 @@
|
||||
"sl" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Šifrirano brez dostopa za preverjenega prejemnika"
|
||||
"value" : "Šifrirano brez dostopa s preverjenim prejemnikom"
|
||||
}
|
||||
},
|
||||
"sv" : {
|
||||
@@ -7159,7 +7159,7 @@
|
||||
"sl" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Šifrirano brez dostopa za preverjene prejemnike"
|
||||
"value" : "Šifrirano brez dostopa s preverjenimi prejemniki"
|
||||
}
|
||||
},
|
||||
"sv" : {
|
||||
|
||||
@@ -87,6 +87,7 @@ public extension DS.Icon {
|
||||
public extension DS.Icon {
|
||||
static let icBrandProtonMailUpsell = ImageResource.icBrandProtonMailUpsell
|
||||
static let icBrandProtonMailUpsellBlackAndWhite = ImageResource.icBrandProtonMailUpsellBw
|
||||
static let icBrandProtonUnlimitedUpsellHeader = ImageResource.icBrandProtonUnlimitedUpsellHeader
|
||||
static let upsellBlackFridayHeaderButtonWave1 = ImageResource.upsellBlackFridayHeaderButtonWave1
|
||||
static let upsellBlackFridayHeaderButtonWave2 = ImageResource.upsellBlackFridayHeaderButtonWave2
|
||||
static let upsellBlackFridaySidebarItemWave1 = ImageResource.upsellBlackFridaySidebarItemWave1
|
||||
@@ -98,6 +99,9 @@ public extension DS.Icon {
|
||||
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
|
||||
|
||||
@@ -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 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")
|
||||
}
|
||||
@@ -130,6 +136,15 @@ enum L10n {
|
||||
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 {
|
||||
|
||||
@@ -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" : {
|
||||
"comment" : "Notice at the bottom",
|
||||
"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 %@." : {
|
||||
"comment" : "Subtitle of the upsell page",
|
||||
"localizations" : {
|
||||
@@ -3169,6 +3175,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Folders and labels" : {
|
||||
"comment" : "Description of a feature of a paid subscription"
|
||||
},
|
||||
"Free" : {
|
||||
"comment" : "Name of the free plan",
|
||||
"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 %@." : {
|
||||
"comment" : "Subtitle of the upsell page",
|
||||
"localizations" : {
|
||||
@@ -5037,6 +5049,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" : {
|
||||
|
||||
@@ -64,7 +64,12 @@ final class TelemetryReporter: TelemetryReporting {
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||