This commit is contained in:
Robert Patchett
2025-10-23 17:51:26 +02:00
parent ccdf8a12dc
commit 9b7ddc5396
43 changed files with 1083 additions and 216 deletions
@@ -75,4 +75,13 @@ public enum ExternalFeatureFlag: String, CaseIterable, Codable {
// Payments
case driveiOSPaymentsV2 = "DriveiOSPaymentsV2"
// SDK
case driveiOSSDKUploadMain = "DriveiOSSDKUploadMain"
case driveiOSSDKUploadPhoto = "DriveiOSSDKUploadPhoto"
case driveiOSSDKDownloadMain = "DriveiOSSDKDownloadMain"
case driveiOSSDKDownloadPhoto = "DriveiOSSDKDownloadPhoto"
// Black Friday 2025
case driveIOSBlackFriday2025 = "DriveIOSBlackFriday2025"
}
@@ -38,21 +38,44 @@ public class EventStorageManager: NSObject, RecoverableStorage {
return model
}
#if RESOURCES_ARE_IMPORTED_BY_SPM
#if RESOURCES_ARE_IMPORTED_BY_SPM && !canImport(XCTest)
if let bundle = Bundle.module.url(forResource: databaseName, withExtension: "momd"),
let model = NSManagedObjectModel(contentsOf: bundle)
{
return model
}
#elseif RESOURCES_ARE_IMPORTED_BY_SPM && canImport(XCTest)
// Find the model manually in case we're running tests.
if let libraryPath = ProcessInfo.processInfo.environment["DYLD_LIBRARY_PATH"]?.split(separator: ":").first,
let resourceBundle = Bundle(path: libraryPath + "/PDCore_PDCore.bundle"),
let modelURL = resourceBundle.url(forResource: databaseName, withExtension: "momd"),
let model = NSManagedObjectModel(contentsOf: modelURL)
{
return model
}
#endif
// dynamic linking
if let bundle = Bundle(for: EventStorageManager.self).url(forResource: databaseName, withExtension: "momd"),
let model = NSManagedObjectModel(contentsOf: bundle)
{
return model
}
// Debug builds for real devices link XCTest in, causing problems when developing
// on an iOS device. This doesn't happen for macOS.
//
// We work around this by trying the SPM/application code path even in case we
// already tried looking for resources in DYLD_LIBRARY_PATH.
//
// We shouldn't remove the compile-time checks because checking Bundle.module while
// running macOS tests will crash as the resources aren't where it expects.
if let url = Bundle.module.url(forResource: databaseName, withExtension: "momd"),
let model = NSManagedObjectModel(contentsOf: url) {
return model
}
fatalError("Error loading EventStorageModel from bundle")
}()
@@ -187,6 +187,14 @@ class ExternalFeatureFlagsRepository: FeatureFlagsRepository {
case .driveiOSDebugMode: return .driveiOSDebugMode
// Payments
case .driveiOSPaymentsV2: return .driveiOSPaymentsV2
// SDK
case .driveiOSSDKUploadMain: return .driveiOSSDKUploadMain
case .driveiOSSDKUploadPhoto: return .driveiOSSDKUploadPhoto
case .driveiOSSDKDownloadMain: return .driveiOSSDKDownloadMain
case .driveiOSSDKDownloadPhoto: return .driveiOSSDKDownloadPhoto
// Black Friday 2025
case .driveIOSBlackFriday2025: return .driveIOSBlackFriday2025
}
}
}
@@ -77,6 +77,13 @@ extension LocalSettings: ExternalFeatureFlagsStore {
case .driveiOSDebugMode: driveiOSDebugMode = value
// Payments
case .driveiOSPaymentsV2: driveiOSPaymentsV2 = value
// SDK
case .driveiOSSDKUploadMain: driveiOSSDKUploadMain = value
case .driveiOSSDKUploadPhoto: driveiOSSDKUploadPhoto = value
case .driveiOSSDKDownloadMain: driveiOSSDKDownloadMain = value
case .driveiOSSDKDownloadPhoto: driveiOSSDKDownloadPhoto = value
// Black Friday 2025
case .driveIOSBlackFriday2025: driveIOSBlackFriday2025 = value
}
}
@@ -133,6 +140,13 @@ extension LocalSettings: ExternalFeatureFlagsStore {
case .driveiOSDebugMode: return driveiOSDebugMode
// Payments
case .driveiOSPaymentsV2: return driveiOSPaymentsV2
// SDK
case .driveiOSSDKUploadMain: return driveiOSSDKUploadMain
case .driveiOSSDKUploadPhoto: return driveiOSSDKUploadPhoto
case .driveiOSSDKDownloadMain: return driveiOSSDKDownloadMain
case .driveiOSSDKDownloadPhoto: return driveiOSSDKDownloadPhoto
// Black Friday 2025
case .driveIOSBlackFriday2025: return driveIOSBlackFriday2025
}
}
}
@@ -77,4 +77,13 @@ public enum FeatureAvailabilityFlag: CaseIterable {
// Payments
case driveiOSPaymentsV2
// SDK
case driveiOSSDKUploadMain
case driveiOSSDKUploadPhoto
case driveiOSSDKDownloadMain
case driveiOSSDKDownloadPhoto
// Black Friday 2025
case driveIOSBlackFriday2025
}
@@ -34,12 +34,22 @@ extension NSManagedObjectModel {
}
#endif
#if RESOURCES_ARE_IMPORTED_BY_SPM
#if RESOURCES_ARE_IMPORTED_BY_SPM && !canImport(XCTest)
// Looking for the bundle via Bundle.module works fine for SPM packages linked to applications.
if let bundle = Bundle.module.url(forResource: "Metadata", withExtension: "momd"),
let model = NSManagedObjectModel(contentsOf: bundle)
{
return model
}
#elseif RESOURCES_ARE_IMPORTED_BY_SPM && canImport(XCTest)
// But we need to do it manually for tests.
if let libraryPath = ProcessInfo.processInfo.environment["DYLD_LIBRARY_PATH"]?.split(separator: ":").first,
let resourceBundle = Bundle(path: libraryPath + "/PDCore_PDCore.bundle"),
let modelURL = resourceBundle.url(forResource: "Metadata", withExtension: "momd"),
let model = NSManagedObjectModel(contentsOf: modelURL)
{
return model
}
#endif
// dynamic linking
@@ -49,6 +59,19 @@ extension NSManagedObjectModel {
return model
}
// Debug builds for real devices link XCTest in, causing problems when developing
// on an iOS device. This doesn't happen for macOS.
//
// We work around this by trying the SPM/application code path even in case we
// already tried looking for it in DYLD_LIBRARY_PATH.
//
// We shouldn't remove the compile-time checks because checking Bundle.module while
// running macOS tests will crash as the resources aren't where it expects.
if let url = Bundle.module.url(forResource: "Metadata", withExtension: "momd"),
let model = NSManagedObjectModel(contentsOf: url) {
return model
}
fatalError("Error loading Metadata from bundle")
}
@@ -45,7 +45,7 @@ final class AsyncThumbnailLoader: CancellableThumbnailLoader {
extension AsyncThumbnailLoader {
func loadThumbnail(with id: Identifier) {
guard isIdAllowed(id) else {
Log.info("Load thumbnail not allowed: \(id)", domain: .thumbnails)
Log.debug("Load thumbnail not allowed: \(id)", domain: .thumbnails)
failedIdSubject.send(id)
return
}
+11 -10
View File
@@ -15,6 +15,7 @@
// You should have received a copy of the GNU General Public License
// along with Proton Drive. If not, see https://www.gnu.org/licenses/.
import Combine
import Foundation
import ProtonCoreNetworking
import ProtonCoreServices
@@ -23,18 +24,16 @@ import PMEventsManager
public typealias UserSettings = PMEventsManager.UserSettings
public final class GeneralSettings {
@SecureStorage(label: "userSettings") private(set) var userSettings: UserSettings?
@SecureStorage(label: "userSettings") public private(set) var currentUserSettings: UserSettings?
public private(set) var userSettings: CurrentValueSubject<UserSettings?, Never> = .init(nil)
private let network: ProtonCoreServices.APIService
private let localSettings: LocalSettings
init(mainKeyProvider: MainKeyProvider, network: ProtonCoreServices.APIService, localSettings: LocalSettings) {
self.network = network
self.localSettings = localSettings
self._userSettings.configure(with: mainKeyProvider)
}
public var currentUserSettings: UserSettings? {
userSettings
self._currentUserSettings.configure(with: mainKeyProvider)
}
public func fetchUserSettings() {
@@ -65,14 +64,16 @@ public final class GeneralSettings {
}
public func storeUserSettings(_ userSettings: UserSettings) {
self.userSettings = userSettings
self.currentUserSettings = userSettings
self.localSettings.optOutFromTelemetry = userSettings.optOutFromTelementry
self.localSettings.optOutFromCrashReports = userSettings.optOutFromCrashReports
self.userSettings.send(userSettings)
}
public func cleanUp() {
try? _userSettings.wipeValue()
try? _currentUserSettings.wipeValue()
}
}
+55 -1
View File
@@ -132,6 +132,15 @@ public class LocalSettings: NSObject {
@SettingsStorage("didFetchProtonUserSettings") public var didFetchProtonUserSettings: Bool?
@SettingsStorage("didFetchB2BStatus") public var didFetchB2BStatus: Bool?
// SDK FF
@SettingsStorage("DriveiOSSDKUploadMainValue") private var driveiOSSDKUploadMainValue: Bool?
@SettingsStorage("DriveiOSSDKUploadPhotoValue") private var driveiOSSDKUploadPhotoValue: Bool?
@SettingsStorage("DriveiOSSDKDownloadMainValue") private var driveiOSSDKDownloadMainValue: Bool?
@SettingsStorage("DriveiOSSDKDownloadPhotoValue") private var driveiOSSDKDownloadPhotoValue: Bool?
// Black Friday 2025
@SettingsStorage("driveIOSBlackFriday2025") private var driveIOSBlackFriday2025Value: Bool?
public let suite: SettingsStorageSuite
public init(suite: SettingsStorageSuite) {
@@ -243,6 +252,13 @@ public class LocalSettings: NSObject {
self._didFetchDriveUserSettings.configure(with: suite)
self._didFetchProtonUserSettings.configure(with: suite)
self._didFetchB2BStatus.configure(with: suite)
// SDK
self._driveiOSSDKUploadMainValue.configure(with: suite)
self._driveiOSSDKUploadPhotoValue.configure(with: suite)
self._driveiOSSDKDownloadMainValue.configure(with: suite)
self._driveiOSSDKDownloadPhotoValue.configure(with: suite)
// Black Friday 2025
self._driveIOSBlackFriday2025Value.configure(with: suite)
setDynamicVariables()
}
@@ -303,6 +319,13 @@ public class LocalSettings: NSObject {
self.driveSettingsDocsCommentsNotificationsEnabled = docsCommentsNotificationsEnabled ?? false
self.driveSettingsDocsCommentsNotificationsIncludeDocumentName = docsCommentsNotificationsIncludeDocumentName ?? false
self.driveSettingsPhotoTags = photoTags ?? [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
// SDK
driveiOSSDKUploadMain = driveiOSSDKUploadMainValue ?? false
driveiOSSDKUploadPhoto = driveiOSSDKUploadPhotoValue ?? false
driveiOSSDKDownloadMain = driveiOSSDKDownloadMainValue ?? false
driveiOSSDKDownloadPhoto = driveiOSSDKDownloadPhotoValue ?? false
// Black Friday 2025
driveIOSBlackFriday2025 = driveIOSBlackFriday2025Value ?? false
}
/// `cleanUserSpecificSettings`
@@ -389,9 +412,15 @@ public class LocalSettings: NSObject {
self.docsCommentsNotificationsEnabled = nil
self.docsCommentsNotificationsIncludeDocumentName = nil
self.photoTags = nil
self.didFetchDriveUserSettings = nil
self.didFetchProtonUserSettings = nil
// SDK
driveiOSSDKUploadMainValue = nil
driveiOSSDKUploadPhotoValue = nil
driveiOSSDKDownloadMainValue = nil
driveiOSSDKDownloadPhotoValue = nil
// Black Friday 2025
driveIOSBlackFriday2025Value = nil
setDynamicVariables()
}
@@ -753,6 +782,31 @@ public class LocalSettings: NSObject {
get { photoTags ?? [] }
set { photoTags = newValue }
}
public var driveiOSSDKUploadMain: Bool {
get { driveiOSSDKUploadMainValue ?? false }
set { driveiOSSDKUploadMainValue = newValue }
}
public var driveiOSSDKUploadPhoto: Bool {
get { driveiOSSDKUploadPhotoValue ?? false }
set { driveiOSSDKUploadPhotoValue = newValue }
}
public var driveiOSSDKDownloadMain: Bool {
get { driveiOSSDKDownloadMainValue ?? false }
set { driveiOSSDKDownloadMainValue = newValue }
}
public var driveiOSSDKDownloadPhoto: Bool {
get { driveiOSSDKDownloadPhotoValue ?? false }
set { driveiOSSDKDownloadPhotoValue = newValue }
}
@objc public dynamic var driveIOSBlackFriday2025: Bool {
get { driveIOSBlackFriday2025Value ?? false }
set { driveIOSBlackFriday2025Value = newValue }
}
}
#if DEBUG
@@ -43,6 +43,11 @@ public protocol FeatureFlagsControllerProtocol {
var hasProtonSheetCreation: Bool { get }
var hasDebugMode: Bool { get }
var hasPaymentsV2: Bool { get }
var hasSDKUploadMain: Bool { get }
var hasSDKUploadPhoto: Bool { get }
var hasSDKDownloadMain: Bool { get }
var hasSDKDownloadPhoto: Bool { get }
var hasIOSBlackFriday2025: Bool { get }
/// Makes current value publisher for the specific FF
func makePublisher(keyPath: KeyPath<FeatureFlagsControllerProtocol, Bool>) -> AnyPublisher<Bool, Never>
}
@@ -156,6 +161,26 @@ public final class FeatureFlagsController: FeatureFlagsControllerProtocol {
return featureFlagsStore.isFeatureEnabled(.driveiOSPaymentsV2)
}
public var hasSDKUploadMain: Bool {
return buildType.isQaOrBelow && featureFlagsStore.isFeatureEnabled(.driveiOSSDKUploadMain)
}
public var hasSDKUploadPhoto: Bool {
return buildType.isQaOrBelow && featureFlagsStore.isFeatureEnabled(.driveiOSSDKUploadPhoto)
}
public var hasSDKDownloadMain: Bool {
return buildType.isQaOrBelow && featureFlagsStore.isFeatureEnabled(.driveiOSSDKDownloadMain)
}
public var hasSDKDownloadPhoto: Bool {
return buildType.isQaOrBelow && featureFlagsStore.isFeatureEnabled(.driveiOSSDKDownloadPhoto)
}
public var hasIOSBlackFriday2025: Bool {
featureFlagsStore.isFeatureEnabled(.driveIOSBlackFriday2025)
}
public func makePublisher(keyPath: KeyPath<FeatureFlagsControllerProtocol, Bool>) -> AnyPublisher<Bool, Never> {
let currentValue = self[keyPath: keyPath]
let updatePublisher = updatePublisher
@@ -437,6 +437,12 @@
"value" : "Resynchronisation complète"
}
},
"hu" : {
"stringUnit" : {
"state" : "translated",
"value" : "Teljes újraszinkronizálás"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
@@ -45857,6 +45857,12 @@
"value" : "Accueil"
}
},
"hu" : {
"stringUnit" : {
"state" : "translated",
"value" : "Kezdőlap"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
@@ -47854,6 +47860,12 @@
"value" : "Nom du document"
}
},
"hu" : {
"stringUnit" : {
"state" : "translated",
"value" : "Dokumentum neve"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
@@ -47863,7 +47875,7 @@
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "文書の名"
"value" : "文書の名"
}
},
"ko" : {
@@ -47998,6 +48010,12 @@
"value" : "Nommer votre scan"
}
},
"hu" : {
"stringUnit" : {
"state" : "translated",
"value" : "Beolvasás elnevezése"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
@@ -78344,6 +78362,12 @@
"value" : "Numériser un document"
}
},
"hu" : {
"stringUnit" : {
"state" : "translated",
"value" : "Dokumentum szkennelése"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
@@ -112873,6 +112897,12 @@
"value" : "Faites glisser vers le bas pour actualiser"
}
},
"hu" : {
"stringUnit" : {
"state" : "translated",
"value" : "Frissítéshez húzza le"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
@@ -73,22 +73,36 @@
66E62A752D7B1865008222AC /* FullResyncCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66E62A732D7B185F008222AC /* FullResyncCoordinator.swift */; };
66E62A762D7B1865008222AC /* FullResyncCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66E62A732D7B185F008222AC /* FullResyncCoordinator.swift */; };
66FAB3332AD55EF000ADAB83 /* LaunchOnBootService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66FAB3322AD55EF000ADAB83 /* LaunchOnBootService.swift */; };
721D5AFC2E819C8000740E68 /* DBMeasurementCollector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72FFB9662E8146500013826F /* DBMeasurementCollector.swift */; };
7201DEB42E9405D0009FEC92 /* PerformanceMetricsReportAggregator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7201DEB32E9405C9009FEC92 /* PerformanceMetricsReportAggregator.swift */; };
7201DEB52E9405D0009FEC92 /* PerformanceMetricsReportAggregator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7201DEB32E9405C9009FEC92 /* PerformanceMetricsReportAggregator.swift */; };
721D72862E8A7C3000E45B33 /* FetchedResultObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3D7ED9B2CEF757A00A2FAB1 /* FetchedResultObserver.swift */; };
7240FEAA2EA7747000B47EA7 /* PromoCampaignDateResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7240FEA92EA7746B00B47EA7 /* PromoCampaignDateResource.swift */; };
7240FEAB2EA7747000B47EA7 /* PromoCampaignDateResource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7240FEA92EA7746B00B47EA7 /* PromoCampaignDateResource.swift */; };
726691C12E9FCACD00796513 /* PDCoreTestingToolkit in Frameworks */ = {isa = PBXBuildFile; productRef = 726691C02E9FCACD00796513 /* PDCoreTestingToolkit */; };
726A96822EA67D1B00F698F2 /* PerformanceMeasurementEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 726A96812EA67D1B00F698F2 /* PerformanceMeasurementEvent.swift */; };
726A96832EA67D1B00F698F2 /* PerformanceMeasurementEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 726A96812EA67D1B00F698F2 /* PerformanceMeasurementEvent.swift */; };
726A96842EA67D1B00F698F2 /* PerformanceMeasurementEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 726A96812EA67D1B00F698F2 /* PerformanceMeasurementEvent.swift */; };
726A96862EA67D2900F698F2 /* DBPerformanceMeasurementCollector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 726A96852EA67D2900F698F2 /* DBPerformanceMeasurementCollector.swift */; };
726A96872EA67D2900F698F2 /* DBPerformanceMeasurementCollector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 726A96852EA67D2900F698F2 /* DBPerformanceMeasurementCollector.swift */; };
726A96882EA67D2900F698F2 /* DBPerformanceMeasurementCollector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 726A96852EA67D2900F698F2 /* DBPerformanceMeasurementCollector.swift */; };
72ADD1892E8ECF4100A7ABB3 /* DBPerformanceMeasurementRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72ADD1882E8ECF4100A7ABB3 /* DBPerformanceMeasurementRepository.swift */; };
72ADD18A2E8ECF4100A7ABB3 /* DBPerformanceMeasurementRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72ADD1882E8ECF4100A7ABB3 /* DBPerformanceMeasurementRepository.swift */; };
72ADD18B2E8ECF4100A7ABB3 /* DBPerformanceMeasurementRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72ADD1882E8ECF4100A7ABB3 /* DBPerformanceMeasurementRepository.swift */; };
72ADD1902E8FBE0700A7ABB3 /* DBPerformanceMetricsReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72ADD18E2E8FBE0700A7ABB3 /* DBPerformanceMetricsReporter.swift */; };
72ADD1912E8FBE0700A7ABB3 /* DBPerformanceMetricsReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72ADD18E2E8FBE0700A7ABB3 /* DBPerformanceMetricsReporter.swift */; };
72FFB96A2E8146760013826F /* MeasurementEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72FFB9692E8146730013826F /* MeasurementEvent.swift */; };
72FFB96B2E8146760013826F /* MeasurementEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72FFB9692E8146730013826F /* MeasurementEvent.swift */; };
72BDED562EA0EF1500EC3302 /* PromoCampaignBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72BDED552EA0EF0900EC3302 /* PromoCampaignBanner.swift */; };
72BDED572EA0EF1500EC3302 /* PromoCampaignBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72BDED552EA0EF0900EC3302 /* PromoCampaignBanner.swift */; };
72BDED5A2EA0F53F00EC3302 /* PromoCampaignInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72BDED592EA0F53900EC3302 /* PromoCampaignInteractor.swift */; };
72BDED5B2EA0F53F00EC3302 /* PromoCampaignInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72BDED592EA0F53900EC3302 /* PromoCampaignInteractor.swift */; };
72BDED5D2EA0F6CC00EC3302 /* UserDefaults+PromoCampaigns.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72BDED5C2EA0F6C300EC3302 /* UserDefaults+PromoCampaigns.swift */; };
72BDED5E2EA0F6CC00EC3302 /* UserDefaults+PromoCampaigns.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72BDED5C2EA0F6C300EC3302 /* UserDefaults+PromoCampaigns.swift */; };
72C94D122E9D51FA00FA0038 /* PDCoreTestingToolkit in Frameworks */ = {isa = PBXBuildFile; productRef = 72C94D112E9D51FA00FA0038 /* PDCoreTestingToolkit */; };
72FFB9862E8179820013826F /* Metrics.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 72FFB9842E8179820013826F /* Metrics.xcdatamodeld */; };
72FFB9872E8179820013826F /* Metrics.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 72FFB9842E8179820013826F /* Metrics.xcdatamodeld */; };
72FFB9882E8179820013826F /* Metrics.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 72FFB9842E8179820013826F /* Metrics.xcdatamodeld */; };
72FFB98A2E8179D60013826F /* DBPerformanceMeasurement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72FFB9892E8179D10013826F /* DBPerformanceMeasurement.swift */; };
72FFB98B2E8179D60013826F /* DBPerformanceMeasurement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72FFB9892E8179D10013826F /* DBPerformanceMeasurement.swift */; };
72FFB98C2E8179D60013826F /* DBPerformanceMeasurement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72FFB9892E8179D10013826F /* DBPerformanceMeasurement.swift */; };
72FFB98D2E817CAD0013826F /* MeasurementEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72FFB9692E8146730013826F /* MeasurementEvent.swift */; };
76A2BBDA2AE25F26005B77A2 /* NotificationName+Window.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76A2BBD92AE25F26005B77A2 /* NotificationName+Window.swift */; };
76CEAF432A6CF67A000586F4 /* OnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76CEAF422A6CF67A000586F4 /* OnboardingView.swift */; };
76CEAF452A6CF695000586F4 /* OnboardingCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76CEAF442A6CF695000586F4 /* OnboardingCoordinator.swift */; };
@@ -254,6 +268,13 @@
remoteGlobalIDString = 6690C7FA2AD49DD60005FC8F;
remoteInfo = ProtonDriveMacLauncher;
};
7240FEAE2EA7927C00B47EA7 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = AB71531824274ED900543720 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 6690C7FA2AD49DD60005FC8F;
remoteInfo = ProtonDriveMacLauncher;
};
D4040203282018B9001D465B /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = AB71531824274ED900543720 /* Project object */;
@@ -372,10 +393,15 @@
66E62A732D7B185F008222AC /* FullResyncCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullResyncCoordinator.swift; sourceTree = "<group>"; };
66F061982B850C5600A6B067 /* stress-tests */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = "stress-tests"; sourceTree = "<group>"; };
66FAB3322AD55EF000ADAB83 /* LaunchOnBootService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LaunchOnBootService.swift; sourceTree = "<group>"; };
7201DEB32E9405C9009FEC92 /* PerformanceMetricsReportAggregator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PerformanceMetricsReportAggregator.swift; sourceTree = "<group>"; };
7240FEA92EA7746B00B47EA7 /* PromoCampaignDateResource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PromoCampaignDateResource.swift; sourceTree = "<group>"; };
726A96812EA67D1B00F698F2 /* PerformanceMeasurementEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PerformanceMeasurementEvent.swift; sourceTree = "<group>"; };
726A96852EA67D2900F698F2 /* DBPerformanceMeasurementCollector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBPerformanceMeasurementCollector.swift; sourceTree = "<group>"; };
72ADD1882E8ECF4100A7ABB3 /* DBPerformanceMeasurementRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBPerformanceMeasurementRepository.swift; sourceTree = "<group>"; };
72ADD18E2E8FBE0700A7ABB3 /* DBPerformanceMetricsReporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBPerformanceMetricsReporter.swift; sourceTree = "<group>"; };
72FFB9662E8146500013826F /* DBMeasurementCollector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBMeasurementCollector.swift; sourceTree = "<group>"; };
72FFB9692E8146730013826F /* MeasurementEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeasurementEvent.swift; sourceTree = "<group>"; };
72BDED552EA0EF0900EC3302 /* PromoCampaignBanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PromoCampaignBanner.swift; sourceTree = "<group>"; };
72BDED592EA0F53900EC3302 /* PromoCampaignInteractor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PromoCampaignInteractor.swift; sourceTree = "<group>"; };
72BDED5C2EA0F6C300EC3302 /* UserDefaults+PromoCampaigns.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserDefaults+PromoCampaigns.swift"; sourceTree = "<group>"; };
72FFB9852E8179820013826F /* Metrics.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Metrics.xcdatamodel; sourceTree = "<group>"; };
72FFB9892E8179D10013826F /* DBPerformanceMeasurement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBPerformanceMeasurement.swift; sourceTree = "<group>"; };
763AA3F129374F9F00AEE68E /* Config-Release-Store.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Config-Release-Store.xcconfig"; sourceTree = "<group>"; };
@@ -578,16 +604,27 @@
72ADD18F2E8FBE0700A7ABB3 /* Reporting */ = {
isa = PBXGroup;
children = (
7201DEB32E9405C9009FEC92 /* PerformanceMetricsReportAggregator.swift */,
72ADD18E2E8FBE0700A7ABB3 /* DBPerformanceMetricsReporter.swift */,
);
path = Reporting;
sourceTree = "<group>";
};
72BDED582EA0F51E00EC3302 /* PromoCampaigns */ = {
isa = PBXGroup;
children = (
7240FEA92EA7746B00B47EA7 /* PromoCampaignDateResource.swift */,
72BDED5C2EA0F6C300EC3302 /* UserDefaults+PromoCampaigns.swift */,
72BDED592EA0F53900EC3302 /* PromoCampaignInteractor.swift */,
);
path = PromoCampaigns;
sourceTree = "<group>";
};
72FFB9602E8140430013826F /* Metrics */ = {
isa = PBXGroup;
children = (
726A96812EA67D1B00F698F2 /* PerformanceMeasurementEvent.swift */,
72FFB9832E8179690013826F /* DB */,
72FFB9692E8146730013826F /* MeasurementEvent.swift */,
72FFB9722E814AED0013826F /* Storage */,
72FFB9642E8146420013826F /* Collection */,
);
@@ -597,7 +634,7 @@
72FFB9642E8146420013826F /* Collection */ = {
isa = PBXGroup;
children = (
72FFB9662E8146500013826F /* DBMeasurementCollector.swift */,
726A96852EA67D2900F698F2 /* DBPerformanceMeasurementCollector.swift */,
);
path = Collection;
sourceTree = "<group>";
@@ -657,6 +694,7 @@
A30308ED2CE74CC400087F2E /* Subviews */ = {
isa = PBXGroup;
children = (
72BDED552EA0EF0900EC3302 /* PromoCampaignBanner.swift */,
D744EE452E059A9F008B005A /* SpinningImage.swift */,
A30308E72CE74CC400087F2E /* FooterView.swift */,
A30308E82CE74CC400087F2E /* HeaderView.swift */,
@@ -792,6 +830,7 @@
AB71532224274ED900543720 /* ProtonDriveMac */ = {
isa = PBXGroup;
children = (
72BDED582EA0F51E00EC3302 /* PromoCampaigns */,
72ADD18F2E8FBE0700A7ABB3 /* Reporting */,
A3D773452D3AA84400769A8D /* TestRunner.swift */,
D4CC491828705C7F00C6E83F /* main.m */,
@@ -1113,14 +1152,14 @@
);
mainGroup = AB71531724274ED900543720;
packageReferences = (
D83C419C2C53A233002EF29C /* XCRemoteSwiftPackageReference "Sparkle" */,
D83C419C2C53A233002EF29C /* XCRemoteSwiftPackageReference "Sparkle.git" */,
D8AB30D62C6217B5006A5F7C /* XCRemoteSwiftPackageReference "OHHTTPStubs" */,
D8AB30D92C621957006A5F7C /* XCRemoteSwiftPackageReference "TrustKit" */,
D8AB30E32C621ABF006A5F7C /* XCRemoteSwiftPackageReference "apple-fusion" */,
D8AB30D92C621957006A5F7C /* XCRemoteSwiftPackageReference "TrustKit.git" */,
D8AB30E32C621ABF006A5F7C /* XCRemoteSwiftPackageReference "apple-fusion.git" */,
3E9137BE2CC77C0400651BC1 /* XCRemoteSwiftPackageReference "SwiftLintPlugins" */,
661DC23D2CB94C3C00DECBDE /* XCRemoteSwiftPackageReference "CryptoSwift" */,
66E09DFC2E7DA3C30082A1B0 /* XCRemoteSwiftPackageReference "Yams" */,
66E09E202E7DAD9F0082A1B0 /* XCRemoteSwiftPackageReference "protoncore_ios" */,
661DC23D2CB94C3C00DECBDE /* XCRemoteSwiftPackageReference "CryptoSwift.git" */,
66E09DFC2E7DA3C30082A1B0 /* XCRemoteSwiftPackageReference "Yams.git" */,
66E09E202E7DAD9F0082A1B0 /* XCRemoteSwiftPackageReference "protoncore_ios.git" */,
);
productRefGroup = AB71532124274ED900543720 /* Products */;
projectDirPath = "";
@@ -1261,8 +1300,10 @@
669776DC2AF15BE700929E9A /* AppDelegate.swift in Sources */,
72FFB98C2E8179D60013826F /* DBPerformanceMeasurement.swift in Sources */,
669776DD2AF15BE700929E9A /* PostLoginServicesBuilder.swift in Sources */,
72BDED5E2EA0F6CC00EC3302 /* UserDefaults+PromoCampaigns.swift in Sources */,
669776E12AF15BE700929E9A /* OnboardingCoordinator.swift in Sources */,
66835C4C2C7F0A2100B158CB /* RecoveryAttempter.swift in Sources */,
726A96882EA67D2900F698F2 /* DBPerformanceMeasurementCollector.swift in Sources */,
D44B81EE2C71CE170074AD58 /* MemoryWarningObserver.swift in Sources */,
669776E22AF15BE700929E9A /* DriveCoreAlertListener.swift in Sources */,
D4D84DCD2C300FBB007079B4 /* macOSURLCoordinator.swift in Sources */,
@@ -1287,6 +1328,7 @@
669776ED2AF15BE700929E9A /* main.m in Sources */,
D43A7D402AF4FDB100DCA64E /* NotificationName+Window.swift in Sources */,
D8D6FB922B503E2400FB71AE /* Dumper.swift in Sources */,
72BDED5B2EA0F53F00EC3302 /* PromoCampaignInteractor.swift in Sources */,
A30FD4332CEF2CEB0050E2ED /* SpinningProgressView.swift in Sources */,
A30309162CE74CC400087F2E /* ItemRowView.swift in Sources */,
D48A113D2D536B2A001618CE /* DeleteAlerter.swift in Sources */,
@@ -1298,12 +1340,14 @@
A303091A2CE74CC400087F2E /* SyncErrorWindow.swift in Sources */,
A318BBBF2D242F970006212A /* SubscriptionService.swift in Sources */,
72ADD1912E8FBE0700A7ABB3 /* DBPerformanceMetricsReporter.swift in Sources */,
72BDED562EA0EF1500EC3302 /* PromoCampaignBanner.swift in Sources */,
A303091C2CE74CC400087F2E /* SyncStateView.swift in Sources */,
A303091D2CE74CC400087F2E /* HeaderView.swift in Sources */,
A303091E2CE74CC400087F2E /* ItemListView.swift in Sources */,
A303091F2CE74CC400087F2E /* FooterView.swift in Sources */,
A30309202CE74CC400087F2E /* GlobalProgressStatusItem.swift in Sources */,
666D1DDB2D8426FE0048A02F /* FullResyncMetrics.swift in Sources */,
7201DEB42E9405D0009FEC92 /* PerformanceMetricsReportAggregator.swift in Sources */,
A30309212CE74CC400087F2E /* UserActions.swift in Sources */,
A30309232CE74CC400087F2E /* FileTypeAsset.swift in Sources */,
A30309242CE74CC400087F2E /* SyncDBObserver.swift in Sources */,
@@ -1322,8 +1366,9 @@
A3DFAA3F2CE7570D0050AE64 /* QAStateDebuggingView.swift in Sources */,
A3DFAA402CE7570D0050AE64 /* QASettingsWindowCoordinator.swift in Sources */,
D4989F872C18AADF003DFF5E /* ProtonFile.swift in Sources */,
72FFB96A2E8146760013826F /* MeasurementEvent.swift in Sources */,
726A96842EA67D1B00F698F2 /* PerformanceMeasurementEvent.swift in Sources */,
D744EE462E059AA3008B005A /* SpinningImage.swift in Sources */,
7240FEAB2EA7747000B47EA7 /* PromoCampaignDateResource.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -1347,8 +1392,10 @@
AB71532424274ED900543720 /* AppDelegate.swift in Sources */,
72FFB98A2E8179D60013826F /* DBPerformanceMeasurement.swift in Sources */,
D458EEE5286DA94200C325C1 /* PostLoginServicesBuilder.swift in Sources */,
72BDED5D2EA0F6CC00EC3302 /* UserDefaults+PromoCampaigns.swift in Sources */,
66835C4B2C7F0A2100B158CB /* RecoveryAttempter.swift in Sources */,
D44B81ED2C71CE170074AD58 /* MemoryWarningObserver.swift in Sources */,
726A96872EA67D2900F698F2 /* DBPerformanceMeasurementCollector.swift in Sources */,
76CEAF452A6CF695000586F4 /* OnboardingCoordinator.swift in Sources */,
D4D84DCC2C300FBB007079B4 /* macOSURLCoordinator.swift in Sources */,
D4F28D2A29E97AAE00065CD3 /* DriveCoreAlertListener.swift in Sources */,
@@ -1373,6 +1420,7 @@
76A2BBDA2AE25F26005B77A2 /* NotificationName+Window.swift in Sources */,
D8D6FB912B503E2400FB71AE /* Dumper.swift in Sources */,
A30FD4342CEF2CEB0050E2ED /* SpinningProgressView.swift in Sources */,
72BDED5A2EA0F53F00EC3302 /* PromoCampaignInteractor.swift in Sources */,
A303092D2CE74CC400087F2E /* ItemRowView.swift in Sources */,
A35169742D1041F200651795 /* Constants.swift in Sources */,
D48A113C2D536B2A001618CE /* DeleteAlerter.swift in Sources */,
@@ -1384,12 +1432,14 @@
A30309312CE74CC400087F2E /* SyncErrorWindow.swift in Sources */,
A318BBBE2D242F970006212A /* SubscriptionService.swift in Sources */,
72ADD1902E8FBE0700A7ABB3 /* DBPerformanceMetricsReporter.swift in Sources */,
72BDED572EA0EF1500EC3302 /* PromoCampaignBanner.swift in Sources */,
A30309332CE74CC400087F2E /* SyncStateView.swift in Sources */,
A30309342CE74CC400087F2E /* HeaderView.swift in Sources */,
A30309352CE74CC400087F2E /* ItemListView.swift in Sources */,
A30309362CE74CC400087F2E /* FooterView.swift in Sources */,
A30309372CE74CC400087F2E /* GlobalProgressStatusItem.swift in Sources */,
666D1DDA2D8426FE0048A02F /* FullResyncMetrics.swift in Sources */,
7201DEB52E9405D0009FEC92 /* PerformanceMetricsReportAggregator.swift in Sources */,
A30309382CE74CC400087F2E /* UserActions.swift in Sources */,
A35169762D10423300651795 /* RuntimeConfiguration.swift in Sources */,
A303093A2CE74CC400087F2E /* FileTypeAsset.swift in Sources */,
@@ -1408,8 +1458,9 @@
A3DFAA432CE7570D0050AE64 /* QAStateDebuggingView.swift in Sources */,
A3DFAA442CE7570D0050AE64 /* QASettingsWindowCoordinator.swift in Sources */,
D4989F862C18AADF003DFF5E /* ProtonFile.swift in Sources */,
72FFB96B2E8146760013826F /* MeasurementEvent.swift in Sources */,
726A96832EA67D1B00F698F2 /* PerformanceMeasurementEvent.swift in Sources */,
D744EE472E059AA3008B005A /* SpinningImage.swift in Sources */,
7240FEAA2EA7747000B47EA7 /* PromoCampaignDateResource.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -1419,15 +1470,15 @@
files = (
A35169832D1045A700651795 /* RuntimeConfiguration.swift in Sources */,
721D72862E8A7C3000E45B33 /* FetchedResultObserver.swift in Sources */,
72FFB98D2E817CAD0013826F /* MeasurementEvent.swift in Sources */,
A351697F2D1045A200651795 /* Log+Configuration.swift in Sources */,
A3ED69FD2D53488D00FC2A7C /* Log+TestRunner.swift in Sources */,
72FFB9882E8179820013826F /* Metrics.xcdatamodeld in Sources */,
721D5AFC2E819C8000740E68 /* DBMeasurementCollector.swift in Sources */,
72FFB98B2E8179D60013826F /* DBPerformanceMeasurement.swift in Sources */,
ABD1909025A30FF7005D3E29 /* FileProviderExtension.swift in Sources */,
A351697B2D10458C00651795 /* Constants.swift in Sources */,
726A96822EA67D1B00F698F2 /* PerformanceMeasurementEvent.swift in Sources */,
72ADD18B2E8ECF4100A7ABB3 /* DBPerformanceMeasurementRepository.swift in Sources */,
726A96862EA67D2900F698F2 /* DBPerformanceMeasurementCollector.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -1774,7 +1825,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 2.9.0;
MARKETING_VERSION = 2.10.0;
OTHER_CODE_SIGN_FLAGS = "";
PRODUCT_BUNDLE_IDENTIFIER = ch.protonmail.drive;
PRODUCT_MODULE_NAME = ProtonDriveMac;
@@ -1861,7 +1912,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 2.9.0;
MARKETING_VERSION = 2.10.0;
OTHER_CODE_SIGN_FLAGS = "";
PRODUCT_BUNDLE_IDENTIFIER = ch.protonmail.drive;
PRODUCT_MODULE_NAME = ProtonDriveMac;
@@ -1932,7 +1983,7 @@
"@executable_path/../../../../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 2.9.0;
MARKETING_VERSION = 2.10.0;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
OTHER_CODE_SIGN_FLAGS = "";
@@ -2635,7 +2686,7 @@
version = 0.57.0;
};
};
661DC23D2CB94C3C00DECBDE /* XCRemoteSwiftPackageReference "CryptoSwift" */ = {
661DC23D2CB94C3C00DECBDE /* XCRemoteSwiftPackageReference "CryptoSwift.git" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/krzyzanowskim/CryptoSwift.git";
requirement = {
@@ -2643,7 +2694,7 @@
minimumVersion = 1.8.3;
};
};
66E09DFC2E7DA3C30082A1B0 /* XCRemoteSwiftPackageReference "Yams" */ = {
66E09DFC2E7DA3C30082A1B0 /* XCRemoteSwiftPackageReference "Yams.git" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/jpsim/Yams.git";
requirement = {
@@ -2651,7 +2702,7 @@
minimumVersion = 5.0.0;
};
};
66E09E202E7DAD9F0082A1B0 /* XCRemoteSwiftPackageReference "protoncore_ios" */ = {
66E09E202E7DAD9F0082A1B0 /* XCRemoteSwiftPackageReference "protoncore_ios.git" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/ProtonMail/protoncore_ios.git";
requirement = {
@@ -2659,7 +2710,7 @@
version = 33.2.0;
};
};
D83C419C2C53A233002EF29C /* XCRemoteSwiftPackageReference "Sparkle" */ = {
D83C419C2C53A233002EF29C /* XCRemoteSwiftPackageReference "Sparkle.git" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/sparkle-project/Sparkle.git";
requirement = {
@@ -2676,7 +2727,7 @@
minimumVersion = 0.0.0;
};
};
D8AB30D92C621957006A5F7C /* XCRemoteSwiftPackageReference "TrustKit" */ = {
D8AB30D92C621957006A5F7C /* XCRemoteSwiftPackageReference "TrustKit.git" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/ProtonMail/TrustKit.git";
requirement = {
@@ -2685,7 +2736,7 @@
minimumVersion = 0.0.0;
};
};
D8AB30E32C621ABF006A5F7C /* XCRemoteSwiftPackageReference "apple-fusion" */ = {
D8AB30E32C621ABF006A5F7C /* XCRemoteSwiftPackageReference "apple-fusion.git" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/ProtonMail/apple-fusion.git";
requirement = {
@@ -2715,7 +2766,7 @@
};
661DC23E2CB94C3C00DECBDE /* CryptoSwift */ = {
isa = XCSwiftPackageProductDependency;
package = 661DC23D2CB94C3C00DECBDE /* XCRemoteSwiftPackageReference "CryptoSwift" */;
package = 661DC23D2CB94C3C00DECBDE /* XCRemoteSwiftPackageReference "CryptoSwift.git" */;
productName = CryptoSwift;
};
667B32052C69F4E500D15C95 /* PDCore */ = {
@@ -2736,49 +2787,57 @@
};
66E09DFD2E7DA3C30082A1B0 /* Yams */ = {
isa = XCSwiftPackageProductDependency;
package = 66E09DFC2E7DA3C30082A1B0 /* XCRemoteSwiftPackageReference "Yams" */;
package = 66E09DFC2E7DA3C30082A1B0 /* XCRemoteSwiftPackageReference "Yams.git" */;
productName = Yams;
};
66E09DFF2E7DA42D0082A1B0 /* Yams */ = {
isa = XCSwiftPackageProductDependency;
package = 66E09DFC2E7DA3C30082A1B0 /* XCRemoteSwiftPackageReference "Yams" */;
package = 66E09DFC2E7DA3C30082A1B0 /* XCRemoteSwiftPackageReference "Yams.git" */;
productName = Yams;
};
66E09E012E7DA4490082A1B0 /* Yams */ = {
isa = XCSwiftPackageProductDependency;
package = 66E09DFC2E7DA3C30082A1B0 /* XCRemoteSwiftPackageReference "Yams" */;
package = 66E09DFC2E7DA3C30082A1B0 /* XCRemoteSwiftPackageReference "Yams.git" */;
productName = Yams;
};
66E09E282E7DB0A40082A1B0 /* ProtonCoreCryptoMultiversionPatchedGoImplementation */ = {
isa = XCSwiftPackageProductDependency;
package = 66E09E202E7DAD9F0082A1B0 /* XCRemoteSwiftPackageReference "protoncore_ios" */;
package = 66E09E202E7DAD9F0082A1B0 /* XCRemoteSwiftPackageReference "protoncore_ios.git" */;
productName = ProtonCoreCryptoMultiversionPatchedGoImplementation;
};
66E09E2A2E7DB0CA0082A1B0 /* ProtonCoreCryptoMultiversionPatchedGoImplementation */ = {
isa = XCSwiftPackageProductDependency;
package = 66E09E202E7DAD9F0082A1B0 /* XCRemoteSwiftPackageReference "protoncore_ios" */;
package = 66E09E202E7DAD9F0082A1B0 /* XCRemoteSwiftPackageReference "protoncore_ios.git" */;
productName = ProtonCoreCryptoMultiversionPatchedGoImplementation;
};
66E09E2C2E7DB0D00082A1B0 /* ProtonCoreCryptoMultiversionPatchedGoImplementation */ = {
isa = XCSwiftPackageProductDependency;
package = 66E09E202E7DAD9F0082A1B0 /* XCRemoteSwiftPackageReference "protoncore_ios" */;
package = 66E09E202E7DAD9F0082A1B0 /* XCRemoteSwiftPackageReference "protoncore_ios.git" */;
productName = ProtonCoreCryptoMultiversionPatchedGoImplementation;
};
66E09E2E2E7DB2F60082A1B0 /* ProtonCoreQuarkCommands */ = {
isa = XCSwiftPackageProductDependency;
package = 66E09E202E7DAD9F0082A1B0 /* XCRemoteSwiftPackageReference "protoncore_ios" */;
package = 66E09E202E7DAD9F0082A1B0 /* XCRemoteSwiftPackageReference "protoncore_ios.git" */;
productName = ProtonCoreQuarkCommands;
};
66E09E302E7DB2FC0082A1B0 /* ProtonCoreQuarkCommands */ = {
isa = XCSwiftPackageProductDependency;
package = 66E09E202E7DAD9F0082A1B0 /* XCRemoteSwiftPackageReference "protoncore_ios" */;
package = 66E09E202E7DAD9F0082A1B0 /* XCRemoteSwiftPackageReference "protoncore_ios.git" */;
productName = ProtonCoreQuarkCommands;
};
66E09E322E7DB3030082A1B0 /* ProtonCoreQuarkCommands */ = {
isa = XCSwiftPackageProductDependency;
package = 66E09E202E7DAD9F0082A1B0 /* XCRemoteSwiftPackageReference "protoncore_ios" */;
package = 66E09E202E7DAD9F0082A1B0 /* XCRemoteSwiftPackageReference "protoncore_ios.git" */;
productName = ProtonCoreQuarkCommands;
};
726691C02E9FCACD00796513 /* PDCoreTestingToolkit */ = {
isa = XCSwiftPackageProductDependency;
productName = PDCoreTestingToolkit;
};
72C94D112E9D51FA00FA0038 /* PDCoreTestingToolkit */ = {
isa = XCSwiftPackageProductDependency;
productName = PDCoreTestingToolkit;
};
D8262F302C40364B00867704 /* PDLocalization */ = {
isa = XCSwiftPackageProductDependency;
productName = PDLocalization;
@@ -2793,7 +2852,7 @@
};
D83C419D2C53A233002EF29C /* Sparkle */ = {
isa = XCSwiftPackageProductDependency;
package = D83C419C2C53A233002EF29C /* XCRemoteSwiftPackageReference "Sparkle" */;
package = D83C419C2C53A233002EF29C /* XCRemoteSwiftPackageReference "Sparkle.git" */;
productName = Sparkle;
};
D85AB8362C539D5600FFDC10 /* PDFileProvider */ = {
@@ -2854,17 +2913,17 @@
};
D8AB30DA2C621957006A5F7C /* TrustKit */ = {
isa = XCSwiftPackageProductDependency;
package = D8AB30D92C621957006A5F7C /* XCRemoteSwiftPackageReference "TrustKit" */;
package = D8AB30D92C621957006A5F7C /* XCRemoteSwiftPackageReference "TrustKit.git" */;
productName = TrustKit;
};
D8AB30E12C621A2F006A5F7C /* TrustKit */ = {
isa = XCSwiftPackageProductDependency;
package = D8AB30D92C621957006A5F7C /* XCRemoteSwiftPackageReference "TrustKit" */;
package = D8AB30D92C621957006A5F7C /* XCRemoteSwiftPackageReference "TrustKit.git" */;
productName = TrustKit;
};
D8AB30E42C621ABF006A5F7C /* fusion */ = {
isa = XCSwiftPackageProductDependency;
package = D8AB30E32C621ABF006A5F7C /* XCRemoteSwiftPackageReference "apple-fusion" */;
package = D8AB30E32C621ABF006A5F7C /* XCRemoteSwiftPackageReference "apple-fusion.git" */;
productName = fusion;
};
D8AB30E62C621B7F006A5F7C /* OHHTTPStubs */ = {
@@ -27,15 +27,6 @@
"version" : "1.8.3"
}
},
{
"identity" : "ellipticcurvekeypair",
"kind" : "remoteSourceControl",
"location" : "https://github.com/agens-no/EllipticCurveKeyPair",
"state" : {
"revision" : "944ae5c89ca045e9f1a113b736706c73fc51d1c2",
"version" : "2.0.0"
}
},
{
"identity" : "lottie-ios",
"kind" : "remoteSourceControl",
@@ -59,8 +50,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/ProtonMail/protoncore_ios.git",
"state" : {
"revision" : "1f7d0eb2c3feb4f28ea0251a4470c44731484637",
"version" : "25.3.4"
"revision" : "0fdf4c672d898ab8b9048c3b1c36c44a047dd694",
"version" : "33.2.0"
}
},
{
@@ -86,8 +77,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/getsentry/sentry-cocoa.git",
"state" : {
"revision" : "c9a692ba837f4832ec7ce6378c071cb91cb55f92",
"version" : "8.29.0"
"revision" : "9e193ac0b71760603aa666bad7e9e303dd7031a8",
"version" : "8.56.2"
}
},
{
@@ -143,6 +134,15 @@
"revision" : "d107d7cc825f38ae2d6dc7c54af71d58145c3506",
"version" : "1.0.3"
}
},
{
"identity" : "yams",
"kind" : "remoteSourceControl",
"location" : "https://github.com/jpsim/Yams.git",
"state" : {
"revision" : "3d6871d5b4a5cd519adf233fbb576e0a2af71c17",
"version" : "5.4.0"
}
}
],
"version" : 2
@@ -38,7 +38,7 @@ import ProtonCoreCryptoMultiversionPatchedGoImplementation
/// `ApplicationEventObserver` - observes all changes relevant to the state of the status window (sync in progress? user logged in?, network reachable?, update available?), and propagates them to the menu bar status item and status window.
/// `ApplicationState` - shared with `AppCoordinator`.
/// `NetworkStateInteractor` - provides updates on whether the network is reachable.
/// `AppUpdateServiceProtocol` - provides updates on whether an app update is available.
/// `AppUpdateServiceProtocol` - provides updaes on whether an app update is available.
/// `SessionVault` - provides updates on when a user logs in.
/// `LoggedInStateReporter` - provides updates on when a user logs in.
/// `GlobalProgressObserver` - provides updates on the state of uploading or downloading operations from the File Provider extension.
@@ -48,6 +48,7 @@ import ProtonCoreCryptoMultiversionPatchedGoImplementation
/// `SyncStateDelegate` - updates the `isPaused` and `isOffline` status of `EventsSystemManager` and `DomainOperationsService`.
/// `PDCore.EventsSystemManager` - CoreData (Tower).
/// `DomainOperationsService` Events from the FileProvider.
/// `PromoCampaignInteractor` - provides information about active promo campaigns, used for the banner within the tray app.
/// `MenuBarCoordinator` - logic related to then menu icon and dropdown menu.
/// `DBPerformanceMetricsReporter` - logic related to watching the performance metrics DB and sending
/// signals to observability system.
@@ -104,6 +105,7 @@ class AppCoordinator: NSObject, ObservableObject {
private var menuBarCoordinator: MenuBarCoordinator?
private var applicationEventObserver: ApplicationEventObserver?
private var globalProgressObserver: GlobalProgressObserver?
private var promoCampaignInteractor: PromoCampaignInteractorProtocol
private var initializationCoordinator: InitializationCoordinator?
private var onboardingCoordinator: OnboardingCoordinator?
@@ -168,6 +170,7 @@ class AppCoordinator: NSObject, ObservableObject {
accountInfoProvider: initialServices.sessionVault,
featureFlags: { featureFlagsAccessor() },
fileProviderManagerFactory: SystemFileProviderManagerFactory())
let promoCampaignInteractor = PromoCampaignInteractor.shared
#if HAS_BUILTIN_UPDATER
let appUpdateService = SparkleAppUpdateService()
@@ -175,16 +178,19 @@ class AppCoordinator: NSObject, ObservableObject {
let appUpdateService: AppUpdateServiceProtocol? = nil
#endif
self.init(initialServices: initialServices,
networkStateService: networkStateService,
driveCoreAlertListener: driveCoreAlertListener,
loginBuilder: loginBuilder,
postLoginServicesBuilder: postLoginServicesBuilder,
logContentLoader: logContentLoader,
launchOnBoot: launchOnBoot,
appUpdateService: appUpdateService,
domainOperationsService: domainOperationsService,
ddkSessionCommunicator: ddkSessionCommunicator)
self.init(
initialServices: initialServices,
networkStateService: networkStateService,
driveCoreAlertListener: driveCoreAlertListener,
loginBuilder: loginBuilder,
postLoginServicesBuilder: postLoginServicesBuilder,
logContentLoader: logContentLoader,
launchOnBoot: launchOnBoot,
appUpdateService: appUpdateService,
domainOperationsService: domainOperationsService,
ddkSessionCommunicator: ddkSessionCommunicator,
promoCampaignInteractor: promoCampaignInteractor
)
featureFlagsAccessor = { [weak self] in self?.featureFlags }
await ddkSessionCommunicator.performInitialSetup()
@@ -195,25 +201,31 @@ class AppCoordinator: NSObject, ObservableObject {
}
}
#if DEBUG && !canImport(XCTest)
#if DEBUG
static var counter = 0
#endif
required init(initialServices: InitialServices,
networkStateService: NetworkStateInteractor,
driveCoreAlertListener: DriveCoreAlertListener,
loginBuilder: LoginManagerBuilder,
postLoginServicesBuilder: PostLoginServicesBuilder,
logContentLoader: LogContentLoader,
launchOnBoot: any LaunchOnBootServiceProtocol,
appUpdateService: AppUpdateServiceProtocol?,
domainOperationsService: DomainOperationsService,
ddkSessionCommunicator: SessionRelatedCommunicatorBetweenMainAppAndExtensions) {
required init(
initialServices: InitialServices,
networkStateService: NetworkStateInteractor,
driveCoreAlertListener: DriveCoreAlertListener,
loginBuilder: LoginManagerBuilder,
postLoginServicesBuilder: PostLoginServicesBuilder,
logContentLoader: LogContentLoader,
launchOnBoot: any LaunchOnBootServiceProtocol,
appUpdateService: AppUpdateServiceProtocol?,
domainOperationsService: DomainOperationsService,
ddkSessionCommunicator: SessionRelatedCommunicatorBetweenMainAppAndExtensions,
promoCampaignInteractor: PromoCampaignInteractorProtocol
) {
#if DEBUG && !canImport(XCTest)
// Make sure this is only instantiated once
#if DEBUG
Self.counter += 1
assert(Self.counter == 1)
// Make sure this is only instantiated once only if we're not running tests
if ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] == nil {
assert(Self.counter == 1)
}
#endif
self.initialServices = initialServices
@@ -228,9 +240,9 @@ class AppCoordinator: NSObject, ObservableObject {
self.ddkSessionCommunicator = ddkSessionCommunicator
self.appState = ApplicationState()
self.subscriptionService = SubscriptionService(apiService: initialServices.authenticator.apiService)
self.observationCenter = UserDefaultsObservationCenter(userDefaults: Constants.appGroup.userDefaults)
self.performanceMetricsReporter = DBPerformanceMetricsReporter()
self.promoCampaignInteractor = promoCampaignInteractor
super.init()
@@ -647,6 +659,10 @@ class AppCoordinator: NSObject, ObservableObject {
sessionVault: postLoginServices.tower.sessionVault
)
applicationEventObserver?.startGeneralSettingsMonitoring(
settingsService: postLoginServices.tower.generalSettings
)
let hasPlan = initialServices.sessionVault.userInfo?.hasAnySubscription
DriveIntegrityErrorMonitor.configure(with: Constants.appGroup, forUserWithPlan: hasPlan)
} catch {
@@ -669,10 +685,13 @@ class AppCoordinator: NSObject, ObservableObject {
appState.setAccountInfo(self.initialServices.sessionVault.getAccountInfo())
self.applicationEventObserver = ApplicationEventObserver(state: appState,
logoutStateService: initialServices,
networkStateService: networkStateService,
appUpdateService: appUpdateService)
self.applicationEventObserver = ApplicationEventObserver(
state: appState,
logoutStateService: initialServices,
networkStateService: networkStateService,
appUpdateService: appUpdateService,
promoCampaignInteractor: promoCampaignInteractor
)
self.menuBarCoordinator = await MenuBarCoordinator(
state: appState,
@@ -1446,6 +1465,12 @@ extension AppCoordinator: UserActionsDelegate {
}
return itemIdentifiers
}
// MARK: - Promotional actions
func dismissPromoBanner() {
promoCampaignInteractor.dismissCampaign()
}
}
// MARK: -
@@ -0,0 +1,9 @@
{
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"provides-namespace" : true
}
}
@@ -0,0 +1,16 @@
{
"images" : [
{
"filename" : "ic-cross-big.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true,
"template-rendering-intent" : "template"
}
}
@@ -0,0 +1,16 @@
{
"images" : [
{
"filename" : "ic-drive-plus.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true,
"template-rendering-intent" : "template"
}
}
@@ -0,0 +1,16 @@
{
"images" : [
{
"filename" : "receipt-percent.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true,
"template-rendering-intent" : "template"
}
}
@@ -0,0 +1,16 @@
{
"images" : [
{
"filename" : "status-promo.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true,
"template-rendering-intent" : "template"
}
}
@@ -0,0 +1,64 @@
// Copyright (c) 2025 Proton AG
//
// This file is part of Proton Drive.
//
// Proton Drive 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 Drive 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 Drive. If not, see https://www.gnu.org/licenses/.
import Foundation
import PDCore
final class PromoCampaignDateResource: DateResource {
#if HAS_QA_FEATURES
@SettingsStorage(QASettingsConstants.overrideDateForPromoCampaign) var overrideDateForPromoCampaign: String?
private let dateFormatter: DateFormatter = {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm"
dateFormatter.timeZone = .init(identifier: "CET")
return dateFormatter
}()
#endif
private let underlyingDateResource: DateResource
init(dateResource: DateResource) {
self.underlyingDateResource = dateResource
}
convenience init() {
self.init(dateResource: PlatformCurrentDateResource())
}
func getDate() -> Date {
#if HAS_QA_FEATURES
guard let overrideDateForPromoCampaign else {
return underlyingDateResource.getDate()
}
guard let parsedDate = dateFormatter.date(from: overrideDateForPromoCampaign) else {
Log.trace("Possible misconfiguration: have a string for promo date override, but it's not parseable as a date.")
return underlyingDateResource.getDate()
}
return parsedDate
#else
return underlyingDateResource.getDate()
#endif
}
func getPastDate() -> Date {
underlyingDateResource.getPastDate()
}
}
@@ -0,0 +1,152 @@
// Copyright (c) 2025 Proton AG
//
// This file is part of Proton Drive.
//
// Proton Drive 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 Drive 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 Drive. If not, see https://www.gnu.org/licenses/.
import Foundation
import ProtonCoreUIFoundations
import SwiftUI
import PDCore
import Combine
struct PromoCampaignConfiguration {
enum BannerIcon {
case drivePlus
case discount
var imageName: String {
switch self {
case .drivePlus:
"Promo/ic-drive-plus"
case .discount:
"Promo/ic-promo-discount"
}
}
}
enum TimeRange {
/// Campaign should only be active while start < date < end
case limitedTime(start: Date, end: Date)
/// Campaign should be active after the given date
case indefinite(after: Date)
/// Campaign should always be active (useful for testing!)
case always
}
fileprivate static let activeCampaigns: [PromoCampaignConfiguration] = [
PromoCampaignConfiguration(
timeRange: .limitedTime(
start: Date(timeIntervalSinceReferenceDate: 783860400), // 2025-11-03 12:00 CET
end: Date(timeIntervalSinceReferenceDate: 785156400) // 2025-11-18 12:00 CET
),
backgroundColor: Color(hex: "#D8FF00"),
tintColor: Color(hex: "#291C5D"),
icon: .discount,
text: "Black Friday: 50% off"
),
PromoCampaignConfiguration(
timeRange: .limitedTime(
start: Date(timeIntervalSinceReferenceDate: 785156400), // 2025-11-18 12:00 CET
end: Date(timeIntervalSinceReferenceDate: 786452400) // 2025-12-03 12:00 CET
),
backgroundColor: Color(hex: "#D8FF00"),
tintColor: Color(hex: "#291C5D"),
icon: .discount,
text: "Black Friday: 80% off"
),
PromoCampaignConfiguration(
timeRange: .indefinite(
after: Date(timeIntervalSinceReferenceDate: 786452400) // 2025-12-03 12:00 CET
),
backgroundColor: ColorProvider.Primary,
tintColor: ColorProvider.White,
icon: .drivePlus,
text: "Upgrade to Drive Plus"
)
]
let timeRange: TimeRange
let backgroundColor: Color
let tintColor: Color
let icon: BannerIcon
let text: String
}
protocol PromoCampaignInteractorProtocol {
var activeCampaign: AnyPublisher<PromoCampaignConfiguration?, Never> { get }
func dismissCampaign()
}
final class PromoCampaignInteractor: PromoCampaignInteractorProtocol {
var activeCampaign: AnyPublisher<PromoCampaignConfiguration?, Never> {
currentlyActiveCampaign.eraseToAnyPublisher()
}
@SettingsStorage(UserDefaults.PromoCampaign.hasDismissedBanner.rawValue) private var hasDismissedBanner: Bool?
private var currentlyActiveCampaign = CurrentValueSubject<PromoCampaignConfiguration?, Never>(nil)
private let dateResource: DateResource
static let shared = PromoCampaignInteractor()
init(
dateResource: DateResource
) {
self.dateResource = dateResource
_hasDismissedBanner.configure(with: Constants.appGroup)
refreshCampaign()
}
private convenience init() {
self.init(dateResource: PromoCampaignDateResource())
}
func refreshCampaign(resetBannerDismissal: Bool = false) {
if resetBannerDismissal {
hasDismissedBanner = false
}
guard (hasDismissedBanner ?? false) == false else {
currentlyActiveCampaign.send(.none)
return
}
let activeCampaign = getActiveCampaign()
currentlyActiveCampaign.send(activeCampaign)
}
func dismissCampaign() {
hasDismissedBanner = true
currentlyActiveCampaign.send(.none)
}
private func getActiveCampaign() -> PromoCampaignConfiguration? {
PromoCampaignConfiguration.activeCampaigns.first { campaign in
let currentDate = dateResource.getDate()
switch campaign.timeRange {
case let .limitedTime(start, end):
return start <= currentDate && currentDate < end
case let .indefinite(start):
return start <= currentDate
case .always:
return true
}
}
}
}
@@ -0,0 +1,24 @@
// Copyright (c) 2025 Proton AG
//
// This file is part of Proton Drive.
//
// Proton Drive 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 Drive 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 Drive. If not, see https://www.gnu.org/licenses/.
import Foundation
extension UserDefaults {
enum PromoCampaign: String {
case hasDismissedBanner
}
}
@@ -385,6 +385,33 @@ struct QASettingsView: View {
.padding(.bottom, 10)
.padding(.top, 20)
}
GroupBox {
VStack(alignment: .leading, spacing: 16) {
Text("To clear override date, remove text from field and submit")
Text("The date you input will be parsed in CET time zone")
TextField("yyyy-MM-dd HH:mm", text: $vm.overrideDateForPromoCampaign)
.onSubmit { vm.refreshPromoCampaign() }
.font(.system(size: 11))
.fixedSize(horizontal: false, vertical: true)
if let activeCampaign = vm.activeCampaign {
HStack {
Text("Active campaign:")
Spacer()
Text(activeCampaign.text)
}
} else {
Text("No active campaign for \(vm.overrideDateForPromoCampaign.isEmpty ? "current" : "override") date")
}
}
.frame(maxWidth: .infinity)
.padding(16)
} label: {
Text("Promotional campaigns")
.font(.headline)
.padding(.bottom, 10)
.padding(.top, 20)
}
}
.frame(width: 350)
.padding(20)
@@ -33,6 +33,7 @@ struct QASettingsConstants {
static let disconnectDomainOnSignOut = "disconnectDomainOnSignOut"
static let driveDDKEnabledInQASettings = "driveDDKEnabledInQASettings"
static let globalProgressStatusMenuEnabled = "globalProgressStatusMenuEnabled"
static let overrideDateForPromoCampaign = "overrideDateForPromoCampaign"
}
protocol EventLoopManager: AnyObject {
@@ -69,7 +70,7 @@ class QASettingsViewModel: ObservableObject {
@SettingsStorage(QASettingsConstants.shouldUpdateEvenOnTestFlight) var shouldUpdateEvenOnTestFlightStorage: Bool?
@SettingsStorage(QASettingsConstants.updateChannel) var updateChannelStorage: String?
#endif
@Published var shouldFetchEvents: Bool = true {
didSet {
guard let eventLoopManager else { return }
@@ -78,22 +79,22 @@ class QASettingsViewModel: ObservableObject {
}
}
}
@Published var shouldObfuscateDumps: Bool = false {
didSet { shouldObfuscateDumpsStorage = shouldObfuscateDumps }
}
@Published var enablePostMigrationCleanup: Bool = false {
didSet { requiresPostMigrationCleanup = enablePostMigrationCleanup }
}
@SettingsStorage(QASettingsConstants.shouldObfuscateDumpsStorage) var shouldObfuscateDumpsStorage: Bool?
@SettingsStorage("requiresPostMigrationStep") private var requiresPostMigrationCleanup: Bool?
enum FeatureFlagOptions: String, CaseIterable {
case useFF
case enabled
case disabled
var toBool: Bool? {
switch self {
case .useFF: return nil
@@ -101,7 +102,7 @@ class QASettingsViewModel: ObservableObject {
case .disabled: return false
}
}
init(bool: Bool?) {
switch bool {
case nil: self = .useFF
@@ -130,11 +131,19 @@ class QASettingsViewModel: ObservableObject {
}
@SettingsStorage(QASettingsConstants.driveDDKEnabledInQASettings) var driveDDKEnabledStorage: Bool?
@Published var overrideDateForPromoCampaign: String = "" {
didSet { overrideDateForPromoCampaignStorage = overrideDateForPromoCampaign }
}
@SettingsStorage(QASettingsConstants.overrideDateForPromoCampaign) var overrideDateForPromoCampaignStorage: String?
@Published var activeCampaign: PromoCampaignConfiguration?
let parentSessionUID: String
let childSessionUID: String
let userID: String
let clearCredentials: () -> Void
private var cancellables: Set<AnyCancellable> = []
private let dumper: Dumper?
private let eventLoopManager: EventLoopManager?
private let featureFlags: PDCore.FeatureFlagsRepository?
@@ -143,6 +152,7 @@ class QASettingsViewModel: ObservableObject {
private let metadataStorage: StorageManager?
private let eventsStorage: EventStorageManager?
private let jailDependencies: (PMAPIService, Client)?
private let promoCampaignInteractor: PromoCampaignInteractor
let applicationEventObserver: ApplicationEventObserver
let userActions: UserActions
@@ -158,7 +168,8 @@ class QASettingsViewModel: ObservableObject {
userActions: UserActions,
metadataStorage: StorageManager?,
eventsStorage: EventStorageManager?,
jailDependencies: (PMAPIService, Client)?
jailDependencies: (PMAPIService, Client)?,
promoCampaignInteractor: PromoCampaignInteractor
) {
let suite = Constants.appGroup
self._requiresPostMigrationCleanup.configure(with: suite)
@@ -186,12 +197,17 @@ class QASettingsViewModel: ObservableObject {
self.metadataStorage = metadataStorage
self.eventsStorage = eventsStorage
self.jailDependencies = jailDependencies
self.promoCampaignInteractor = promoCampaignInteractor
self.shouldObfuscateDumps = shouldObfuscateDumpsStorage ?? false
self.enablePostMigrationCleanup = requiresPostMigrationCleanup ?? false
self.disconnectDomainOnSignOut = FeatureFlagOptions(bool: disconnectDomainOnSignOutStorage).rawValue
self.driveDDKEnabled = FeatureFlagOptions(bool: driveDDKEnabledStorage).rawValue
self.promoCampaignInteractor.activeCampaign.sink { activeCampaign in
self.activeCampaign = activeCampaign
}.store(in: &cancellables)
#if HAS_BUILTIN_UPDATER
self.shouldUpdateEvenOnDebugBuild = shouldUpdateEvenOnDebugBuildStorage ?? false
self.shouldUpdateEvenOnTestFlight = shouldUpdateEvenOnTestFlightStorage ?? false
@@ -475,6 +491,14 @@ class QASettingsViewModel: ObservableObject {
dumperError = $0.localizedDescription
}
}
func setOverrideDateForPromoCampaign(dateString: String) {
self.overrideDateForPromoCampaign = dateString
}
func refreshPromoCampaign() {
self.promoCampaignInteractor.refreshCampaign(resetBannerDismissal: true)
}
}
extension Notification.Name {
@@ -77,18 +77,21 @@ final class QASettingsWindowCoordinator: NSObject, NSWindowDelegate {
}
private func configureWindow() {
let vm = QASettingsViewModel(signoutManager: signoutManager,
sessionStore: sessionStore,
mainKeyProvider: mainKeyProvider,
appUpdateService: appUpdateService,
eventLoopManager: eventLoopManager,
featureFlags: featureFlags,
dumperDependencies: dumperDependencies,
applicationEventObserver: applicationEventObserver,
userActions: userActions,
metadataStorage: metadataStorage,
eventsStorage: eventsStorage,
jailDependencies: jailDependencies)
let vm = QASettingsViewModel(
signoutManager: signoutManager,
sessionStore: sessionStore,
mainKeyProvider: mainKeyProvider,
appUpdateService: appUpdateService,
eventLoopManager: eventLoopManager,
featureFlags: featureFlags,
dumperDependencies: dumperDependencies,
applicationEventObserver: applicationEventObserver,
userActions: userActions,
metadataStorage: metadataStorage,
eventsStorage: eventsStorage,
jailDependencies: jailDependencies,
promoCampaignInteractor: PromoCampaignInteractor.shared
)
let view = QASettingsView(vm: vm)
@@ -62,7 +62,9 @@ struct QAStateDebuggingView_Previews: PreviewProvider {
state: ApplicationState.mockWithErrorItems,
logoutStateService: nil,
networkStateService: nil,
appUpdateService: nil)
appUpdateService: nil,
promoCampaignInteractor: nil
)
QAStateDebuggingView(
observer: observer,
@@ -37,22 +37,25 @@ final class DBPerformanceMetricsReporter: PerformanceMetricsReporter {
private let repository: PeformanceMeasurementRepository
private let uploadResource: UploadSpeedMetricResource
private let downloadResource: DownloadSpeedMetricResource
private let dateResource: DateResource
private let reportAggregator: PerformanceMetricsReportAggregating
private let shouldCleanOnStartup: Bool
init(
repository: PeformanceMeasurementRepository,
uploadResource: UploadSpeedMetricResource,
downloadResource: DownloadSpeedMetricResource,
dateResource: DateResource,
inactivityTimer: PausableTimerResource,
reportingCycleTimer: PausableTimerResource
reportingCycleTimer: PausableTimerResource,
reportAggregator: PerformanceMetricsReportAggregating,
shouldCleanOnStartup: Bool
) {
self.repository = repository
self.uploadResource = uploadResource
self.downloadResource = downloadResource
self.dateResource = dateResource
self.inactivityTimeout = inactivityTimer
self.reportingCycle = reportingCycleTimer
self.reportAggregator = reportAggregator
self.shouldCleanOnStartup = shouldCleanOnStartup
}
convenience init() {
@@ -60,13 +63,14 @@ final class DBPerformanceMetricsReporter: PerformanceMetricsReporter {
repository: DBPerformanceMeasurementRepository(),
uploadResource: ObservabilityUploadSpeedMetricResource(),
downloadResource: ObservabilityDownloadSpeedMetricResource(),
dateResource: PlatformCurrentDateResource(),
inactivityTimer: CommonRunLoopPausableTimerResource(
duration: Constants.inactivityTimeout
),
reportingCycleTimer: CommonRunLoopPausableTimerResource(
duration: Constants.reportingCycleLength
)
),
reportAggregator: PerformanceMetricsReportAggregator(),
shouldCleanOnStartup: Self.shouldClearOldMeasurementsByDefault()
)
}
@@ -75,12 +79,9 @@ final class DBPerformanceMetricsReporter: PerformanceMetricsReporter {
func startReporting() {
Log.trace()
#if !HAS_QA_FEATURES
if self.shouldCleanOnStartup {
repository.deleteAllMeasurements()
#endif
self.reportingCycle.restart()
self.inactivityTimeout.restart()
}
repository.unreportedMeasurementPublisher.sink { [weak self] events in
self?.restartTimersIfNeeded()
@@ -105,6 +106,7 @@ final class DBPerformanceMetricsReporter: PerformanceMetricsReporter {
reportIfNeeded()
cancellables.forEach { $0.cancel() }
cancellables = []
}
}
@@ -138,11 +140,9 @@ private extension DBPerformanceMetricsReporter {
PerformanceOperationType.download: await repository.fetchUnreportedMeasurements(for: .download)
]
let hasMeasurements = measurements.values.contains { !$0.isEmpty }
guard hasMeasurements else { return }
let report = reportAggregator.buildReport(from: measurements)
// prepare report: segmented by opreration then by pipeline for ease of reporting
let report = measurements.mapValues { makePerPipelineReport(measurements: $0) }
guard !report.isEmpty else { return }
report.forEach { operation, perPipelineReport in
switch operation {
@@ -172,52 +172,12 @@ private extension DBPerformanceMetricsReporter {
}
}
func makePerPipelineReport(measurements: [PerformanceMeasurementEvent]) -> [DriveObservabilityPipeline: Int] {
Log.trace()
// Ideally we'll only ever see one pipeline, but we handle the case where more than one.
let groupedByPipeline = Dictionary(grouping: measurements, by: \.pipeline)
return groupedByPipeline.mapValues {
calculateSpeed(measurements: $0)
}
}
func calculateSpeed(measurements: [PerformanceMeasurementEvent]) -> Int {
Log.trace()
let measurementTimeRange = getMeasurementTimeRange(from: measurements)
let measuredBytes = measurements.map(\.progressInBytes).reduce(0) { $0 + $1 }
let speedInKib = Double(measuredBytes) / (Double(1024) * measurementTimeRange)
return Int(round(speedInKib))
}
func getMeasurementTimeRange(from measurements: [PerformanceMeasurementEvent]) -> TimeInterval {
// We expect the measurements to already be sorted by timestamp.
// This is done via sortDescriptor in the repository.
if measurements.count >= 2 {
let measurementTimes = measurements.map(\.timestamp)
return measurementTimes[0] - measurementTimes[measurements.count - 1]
} else if measurements.count == 1 {
// It's unexpected that we'll reach this sceneario - the collector should always
// write at least a pair of events to the database: a start event with 0 progress,
// progress updates (these are optional though) and an end event with the remaining
// progress when the file upload completes.
// If we only have a single measurement, we use the distance to current time
// as its duration; the worst case scenario here (a single very small file), we'll divide
// its size by ~timeout.
Log.warning("Measurement issue: attempting to calculate time range for single measurement", domain: .metrics)
let distanceToCurrentTime = measurements[0].timestamp.distance(to: dateResource.getDate().timeIntervalSinceReferenceDate)
return distanceToCurrentTime == .zero ? 1 : distanceToCurrentTime
} else {
// We've guaranteed earlier in the call chain that we'd have
// at least one element in the measurements list.
Log.error("Measuring error: attempted to calculate time range for empty set of measurements", domain: .metrics)
return 1
}
// MARK: - Static helpers
private static func shouldClearOldMeasurementsByDefault() -> Bool {
#if HAS_QA_FEATURES
return false
#else
return true
#endif
}
}
@@ -0,0 +1,105 @@
// Copyright (c) 2025 Proton AG
//
// This file is part of Proton Drive.
//
// Proton Drive 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 Drive 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 Drive. If not, see https://www.gnu.org/licenses/.
import Foundation
import PDCore
protocol PerformanceMetricsReportAggregating {
func buildReport(
from measurements: [PerformanceOperationType: [PerformanceMeasurementEvent]]
) -> [PerformanceOperationType: [DriveObservabilityPipeline: Int]]
}
final class PerformanceMetricsReportAggregator: PerformanceMetricsReportAggregating {
typealias PerformanceReport = [PerformanceOperationType: [DriveObservabilityPipeline: Int]]
private let dateResource: DateResource
init(dateResource: DateResource) {
self.dateResource = dateResource
}
convenience init() {
self.init(
dateResource: PlatformCurrentDateResource()
)
}
func buildReport(
from measurements: [PerformanceOperationType: [PerformanceMeasurementEvent]]
) -> [PerformanceOperationType: [DriveObservabilityPipeline: Int]] {
let hasMeasurements = measurements.values.contains { !$0.isEmpty }
guard hasMeasurements else { return [:] }
// prepare report: segmented by opreration then by pipeline for ease of reporting
return measurements.mapValues { makePerPipelineReport(measurements: $0) }
}
}
private extension PerformanceMetricsReportAggregator {
func makePerPipelineReport(measurements: [PerformanceMeasurementEvent]) -> [DriveObservabilityPipeline: Int] {
Log.trace()
// Ideally we'll only ever see one pipeline, but we handle the case where more than one exists.
let groupedByPipeline = Dictionary(grouping: measurements, by: \.pipeline)
return groupedByPipeline.mapValues {
calculateSpeed(measurements: $0)
}
}
func calculateSpeed(measurements: [PerformanceMeasurementEvent]) -> Int {
Log.trace()
let measurementTimeRange = getMeasurementTimeRange(from: measurements)
let measuredBytes = measurements.map(\.progressInBytes).reduce(0) { $0 + $1 }
let speedInKib = Double(measuredBytes) / (Double(1024) * measurementTimeRange)
return Int(round(speedInKib))
}
func getMeasurementTimeRange(from measurements: [PerformanceMeasurementEvent]) -> TimeInterval {
// We expect the measurements to already be sorted by timestamp.
// This is done via sortDescriptor in the repository.
if measurements.count >= 2 {
let measurementTimes = measurements.map(\.timestamp)
let measuredDifference = measurementTimes[0] - measurementTimes[measurements.count - 1]
// We have a floor of 1 millisecond for the time range.
return measuredDifference <= 0.001 ? 0.001 : measuredDifference
} else if measurements.count == 1 {
// It's unexpected that we'll reach this sceneario - the collector should always
// write at least a pair of events to the database: a start event with 0 progress,
// progress updates (these are optional though) and an end event with the remaining
// progress when the file upload completes.
// If we only have a single measurement, we use the distance to current time
// as its duration; the worst case scenario here (a single very small file), we'll divide
// its size by ~timeout.
Log.warning("Measurement issue: attempting to calculate time range for single measurement", domain: .metrics)
let distanceToCurrentTime = measurements[0].timestamp.distance(to: dateResource.getDate().timeIntervalSinceReferenceDate)
return distanceToCurrentTime == .zero ? 1 : distanceToCurrentTime
} else {
// We've guaranteed earlier in the call chain that we'd have
// at least one element in the measurements list.
Log.error("Measuring error: attempted to calculate time range for empty set of measurements", domain: .metrics)
return 1
}
}
}
@@ -71,6 +71,12 @@ class ApplicationEventObserver: ObservableObject {
/// Fires every `ElapsedTimeService.timeInterval` seconds, only the dropdown Menu or Status Window are opened.
private var elapsedTimeService: ElapsedTimeService?
/// Fires whenever there's a change to active promo campaigns for the user.
private var promoCampaignInteractor: PromoCampaignInteractorProtocol?
/// Responsible for fetching user config
private var generalSettingsService: GeneralSettings?
private let resyncUpdateSubject = PassthroughSubject<Int, Never>()
/// Always-on
@@ -78,14 +84,18 @@ class ApplicationEventObserver: ObservableObject {
/// Only while user is logged in
private var userCancellables = Set<AnyCancellable>()
init(state: ApplicationState,
logoutStateService: LoggedInStateReporter?,
networkStateService: NetworkStateInteractor?,
appUpdateService: AppUpdateServiceProtocol?) {
init(
state: ApplicationState,
logoutStateService: LoggedInStateReporter?,
networkStateService: NetworkStateInteractor?,
appUpdateService: AppUpdateServiceProtocol?,
promoCampaignInteractor: PromoCampaignInteractorProtocol?
) {
self.state = state
self.logoutStateService = logoutStateService
self.networkStateService = networkStateService
self.appUpdateService = appUpdateService
self.promoCampaignInteractor = promoCampaignInteractor
self.deleteAlerter = DeleteAlerter()
@@ -118,6 +128,20 @@ class ApplicationEventObserver: ObservableObject {
self.subscribetoUserInfo()
}
public func startGeneralSettingsMonitoring(settingsService: GeneralSettings) {
Log.trace()
generalSettingsService = settingsService
generalSettingsService?.fetchUserSettings()
generalSettingsService?.userSettings
.receive(on: DispatchQueue.main)
.sink { [weak self] userSettings in
self?.state.setUserSettings(userSettings)
}
.store(in: &userCancellables)
}
/// - Parameters:
/// - all: stop all subscriptions, not just user-specific ones.
public func stopMonitoring(dueToSignOut: Bool) {
@@ -310,6 +334,38 @@ class ApplicationEventObserver: ObservableObject {
}
}
.store(in: &globalCancellables)
guard let promoCampaignInteractor else { return }
Publishers.CombineLatest3(
promoCampaignInteractor.activeCampaign,
state.$userInfo,
state.$userSettings
)
.receive(on: DispatchQueue.main)
.sink { [weak self] campaign, userInfo, userSettings in
guard let userInfo, let userSettings else {
Log.trace("Promo campaign filtered out because user info or settings aren't available yet")
self?.state.setVisibleCampaign(.none)
return
}
// In-app notifications are defined as bit 15 of userSettings.news
let userHasInAppNotificationsEnabled = ((userSettings.news >> 14) & 1) == 1 ? true : false
// We don't display campaigns to users who are
// * Paying customers
// * Delinquent users
// * Users who disabled in-app notifications
if userInfo.isDelinquent || userInfo.isPaid || !userHasInAppNotificationsEnabled {
Log.trace("Promo campaign filtered out because user is not in the target audience")
self?.state.setVisibleCampaign(.none)
return
}
self?.state.setVisibleCampaign(campaign)
}
.store(in: &userCancellables)
}
// MARK: - Update availability (appUpdateService)
@@ -20,6 +20,7 @@ import PDCore
import AppKit
import SwiftUI
import PDLocalization
import ProtonCoreUIFoundations
/// Encapsulates the state of the entire app, driving the UI by publishing updates.
class ApplicationState: ObservableObject {
@@ -39,14 +40,14 @@ class ApplicationState: ObservableObject {
}
}
}
enum FullResyncState: CustomStringConvertible, Equatable {
case idle
case inProgress(Int)
case enumerating
case completed(hasFileProviderResponded: Bool?)
case errored(String)
var isHappening: Bool {
switch self {
case .idle, .completed: false
@@ -57,9 +58,9 @@ class ApplicationState: ObservableObject {
/// Displayed in SyncStateView
var description: String {
switch self {
case .idle:
case .idle:
"Idle"
case .inProgress:
case .inProgress:
"Full resync in progress: downloading data..."
case .enumerating:
"Full resync in progress: refreshing directories..."
@@ -85,10 +86,13 @@ class ApplicationState: ObservableObject {
#endif
init() {
#if DEBUG && !canImport(XCTest)
#if DEBUG
Self.counter += 1
// Make sure this is only instantiated once.
assert(Self.counter == 1)
// Make sure this is only instantiated once only if we're not running tests
if ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] == nil {
assert(Self.counter == 1)
}
#endif
$items
@@ -103,11 +107,13 @@ class ApplicationState: ObservableObject {
@Published private(set) var accountInfo: AccountInfo?
@Published private(set) var userInfo: UserInfo?
@Published private(set) var userSettings: UserSettings?
@Published private(set) var canGetMoreStorage = true
@Published private(set) var isOffline = false
@Published private(set) var isUpdateAvailable = false
/// Percentage of launch sequence that has been completed
@Published private(set) var launchCompletion = 0
@Published private(set) var visibleCampaign: PromoCampaignConfiguration?
// MARK: Sync state
@@ -145,7 +151,7 @@ class ApplicationState: ObservableObject {
@Published var deleteCount = 0
@Published var globalSyncStateDescription: String?
// private var fullResyncStateDescription: String = "Full resync"
// private var fullResyncStateDescription: String = "Full resync"
// MARK: Computed
@@ -253,6 +259,14 @@ class ApplicationState: ObservableObject {
}
}
func setUserSettings(_ settings: UserSettings?) {
self.userSettings = settings
}
func setVisibleCampaign(_ campaign: PromoCampaignConfiguration?) {
self.visibleCampaign = campaign
}
deinit {
Log.trace()
}
@@ -36,6 +36,7 @@ struct ItemListView: View {
var body: some View {
ScrollView {
VStack(spacing: 0) {
PromoCampaignBanner(state: state, actions: actions)
TimelineView(.periodic(from: Date.now, by: 0.04)) { context in
ForEach(Array(state.throttledItems.enumerated()), id: \.element) { index, item in
ItemRowView(
@@ -0,0 +1,53 @@
// Copyright (c) 2025 Proton AG
//
// This file is part of Proton Drive.
//
// Proton Drive 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 Drive 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 Drive. If not, see https://www.gnu.org/licenses/.
import SwiftUI
struct PromoCampaignBanner: View {
@ObservedObject private var state: ApplicationState
private var actions: UserActions
init(state: ApplicationState, actions: UserActions) {
self.state = state
self.actions = actions
}
var body: some View {
state.visibleCampaign.map { campaign in
HStack {
Spacer()
.frame(width: 16)
Label(campaign.text, image: campaign.icon.imageName)
.font(.system(size: 14, weight: .medium))
.foregroundStyle(campaign.tintColor)
Spacer()
Button(
action: { self.actions.promo.dismissPromoBanner() },
label: { Image("Promo/ic-cross").tint(campaign.tintColor) }
)
.buttonStyle(.borderless)
.tint(campaign.tintColor)
Spacer()
.frame(width: 16)
}
.contentShape(Rectangle())
.onTapGesture { self.actions.promo.goToPromoPageOnWeb(email: state.accountInfo?.email) }
.frame(minHeight: 36, maxHeight: 36)
.background(campaign.backgroundColor)
}
}
}
@@ -103,19 +103,23 @@ final class MenuBarCoordinator: NSObject, ObservableObject, NSMenuDelegate {
private func statusItemImageName(_ status: ApplicationSyncStatus) -> String {
switch status {
case .signedOut:
"status-signed-out"
return "status-signed-out"
case .paused:
"status-paused"
return "status-paused"
case .offline:
"status-offline"
return "status-offline"
case .syncing, .enumerating, .launching, .fullResyncInProgress:
"status-syncing"
return "status-syncing"
case .errored:
"status-error"
return "status-error"
case .updateAvailable:
"status-update-available"
return "status-update-available"
case .synced, .fullResyncCompleted:
"status-synced"
if state.visibleCampaign != nil && !state.items.isEmpty {
return "status-promo"
} else {
return "status-synced"
}
}
}
@@ -34,6 +34,9 @@ import PDCore
func refreshUserInfo()
func signInUsingTestCredentials(login: String, password: String)
// Promo
func dismissPromoBanner()
// Sync
func pauseSyncing()
func resumeSyncing()
@@ -76,6 +79,7 @@ class UserActions {
private weak var delegate: UserActionsDelegate?
lazy var app = ApplicationActions(delegate: delegate)
lazy var promo = PromotionalActions(delegate: delegate)
lazy var account = AccountActions(delegate: delegate)
lazy var sync = SyncActions(delegate: delegate)
lazy var resync = ResyncActions(delegate: delegate)
@@ -175,6 +179,23 @@ class UserActions {
}
}
class PromotionalActions {
private weak var delegate: UserActionsDelegate?
init(delegate: UserActionsDelegate?) {
self.delegate = delegate
}
func dismissPromoBanner() {
delegate?.dismissPromoBanner()
}
func goToPromoPageOnWeb(email: String?) {
// reuse LinkActions to go to drive dashboard
LinkActions().getMoreStorage(email: email)
}
}
class SyncActions {
private weak var delegate: UserActionsDelegate?
@@ -32,8 +32,11 @@ class DeleteAlerter {
init() {
Self.counter += 1
// Make sure this is only instantiated once.
assert(Self.counter == 1)
// Make sure this is only instantiated once only if we're not running tests
if ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] == nil {
assert(Self.counter == 1)
}
}
#endif
@@ -22,12 +22,9 @@ import PDFileProviderOperations
import ProtonCoreUtilities
final class DBPerformanceMeasurementCollector: ProgressPerformanceCollector {
public let hasOperations: AnyPublisher<Bool, Never>
private let operationType: PerformanceOperationType
private var progresses: Atomic<[Progress: UUID]> = Atomic([:])
private var cancellables: [UUID: AnyCancellable] = [:]
private let progressCountSubject = CurrentValueSubject<Int, Never>(0)
private let repository: PeformanceMeasurementRepository
private let dateResource: DateResource
@@ -38,14 +35,8 @@ final class DBPerformanceMeasurementCollector: ProgressPerformanceCollector {
dateResource: DateResource
) {
self.operationType = operationType
self.repository = repository
self.dateResource = dateResource
self.hasOperations = progressCountSubject
.map { $0 > 0 }
.removeDuplicates()
.eraseToAnyPublisher()
}
convenience init(operationType: PerformanceOperationType) {
@@ -62,7 +53,6 @@ final class DBPerformanceMeasurementCollector: ProgressPerformanceCollector {
let taskID = UUID()
progresses.mutate { $0.updateValue(taskID, forKey: progress) }
progressCountSubject.send(progresses.fetch(\.count))
// Upload progress events are optional: these will only be reliably sent for
// large files.
@@ -117,7 +107,6 @@ final class DBPerformanceMeasurementCollector: ProgressPerformanceCollector {
)
cancellables[taskID]?.cancel()
dict[progress] = nil
return
}
+6
View File
@@ -1,4 +1,10 @@
<div>
<h1>2.10.0</h1>
<p>
- Fixes a rare crash encountered when uploading or downloading many files<br>
</p>
<h1>2.9.0</h1>
<p>