From d4e01064943a52afbad8af23b4e2d36fad11f284 Mon Sep 17 00:00:00 2001 From: Robert Patchett Date: Wed, 29 Oct 2025 10:05:22 +0100 Subject: [PATCH] 2.10.1 --- .../Domain/ExternalFeatureFlag.swift | 1 + .../ExternalFeatureFlagsRepository.swift | 1 + .../ExternalFeatureFlagsStore.swift | 2 + .../FeatureAvailabilityFlag.swift | 1 + PDCore/PDCore/User/LocalSettings.swift | 7 +++ .../project.pbxproj | 58 +++++++++---------- .../ProtonDriveMac/AppCoordinator.swift | 12 ++-- .../PromoCampaignInteractor.swift | 42 ++++++++++---- .../UserDefaults+PromoCampaigns.swift | 1 + .../QA Settings/QASettingsView.swift | 30 +++++++++- .../QA Settings/QASettingsViewModel.swift | 17 +++++- .../ApplicationEventObserver.swift | 56 ++++++++++++++---- ProtonDrive-macOS/ReleaseNotes.html | 6 ++ 13 files changed, 173 insertions(+), 61 deletions(-) diff --git a/PDClient/PDClient/FeatureFlags/Domain/ExternalFeatureFlag.swift b/PDClient/PDClient/FeatureFlags/Domain/ExternalFeatureFlag.swift index 85236fa..fbba865 100644 --- a/PDClient/PDClient/FeatureFlags/Domain/ExternalFeatureFlag.swift +++ b/PDClient/PDClient/FeatureFlags/Domain/ExternalFeatureFlag.swift @@ -30,6 +30,7 @@ public enum ExternalFeatureFlag: String, CaseIterable, Codable { case driveDDKDisabled = "DriveDDKDisabled" case driveMacSyncRecoveryDisabled = "DriveMacSyncRecoveryDisabled" case driveMacKeepDownloadedDisabled = "DriveMacKeepDownloadedDisabled" + case driveMacPromoBannerDisabled = "DriveMacPromoBannerDisabled" // Sharing case driveSharingMigration = "DriveSharingMigration" diff --git a/PDCore/PDCore/FeatureFlags/ExternalFeatureFlagsRepository.swift b/PDCore/PDCore/FeatureFlags/ExternalFeatureFlagsRepository.swift index 341b8af..b369926 100644 --- a/PDCore/PDCore/FeatureFlags/ExternalFeatureFlagsRepository.swift +++ b/PDCore/PDCore/FeatureFlags/ExternalFeatureFlagsRepository.swift @@ -150,6 +150,7 @@ class ExternalFeatureFlagsRepository: FeatureFlagsRepository { case .driveDDKDisabled: return .driveDDKDisabled case .driveMacSyncRecoveryDisabled: return .driveMacSyncRecoveryDisabled case .driveMacKeepDownloadedDisabled: return .driveMacKeepDownloadedDisabled + case .driveMacPromoBannerDisabled: return .driveMacPromoBannerDisabled // Sharing case .driveSharingMigration: return .driveSharingMigration case .driveSharingInvitations: return .driveSharingInvitations diff --git a/PDCore/PDCore/FeatureFlags/ExternalFeatureFlagsStore.swift b/PDCore/PDCore/FeatureFlags/ExternalFeatureFlagsStore.swift index 120d0d9..ba21cb4 100644 --- a/PDCore/PDCore/FeatureFlags/ExternalFeatureFlagsStore.swift +++ b/PDCore/PDCore/FeatureFlags/ExternalFeatureFlagsStore.swift @@ -39,6 +39,7 @@ extension LocalSettings: ExternalFeatureFlagsStore { case .driveDDKDisabled: driveDDKDisabled = value case .driveMacSyncRecoveryDisabled: driveMacSyncRecoveryDisabled = value case .driveMacKeepDownloadedDisabled: driveMacKeepDownloadedDisabled = value + case .driveMacPromoBannerDisabled: driveMacPromoBannerDisabled = value // Sharing case .driveSharingMigration: driveSharingMigration = value case .driveSharingInvitations: driveSharingInvitations = value @@ -103,6 +104,7 @@ extension LocalSettings: ExternalFeatureFlagsStore { case .driveDDKDisabled: return driveDDKDisabled case .driveMacSyncRecoveryDisabled: return driveMacSyncRecoveryDisabled case .driveMacKeepDownloadedDisabled: return driveMacKeepDownloadedDisabled + case .driveMacPromoBannerDisabled: return driveMacPromoBannerDisabled // Sharing case .driveSharingMigration: return driveSharingMigration case .driveSharingInvitations: return driveSharingInvitations diff --git a/PDCore/PDCore/FeatureFlags/FeatureAvailabilityFlag.swift b/PDCore/PDCore/FeatureFlags/FeatureAvailabilityFlag.swift index c7fcd91..095f063 100644 --- a/PDCore/PDCore/FeatureFlags/FeatureAvailabilityFlag.swift +++ b/PDCore/PDCore/FeatureFlags/FeatureAvailabilityFlag.swift @@ -33,6 +33,7 @@ public enum FeatureAvailabilityFlag: CaseIterable { case driveDDKDisabled case driveMacSyncRecoveryDisabled case driveMacKeepDownloadedDisabled + case driveMacPromoBannerDisabled // Sharing case driveSharingMigration diff --git a/PDCore/PDCore/User/LocalSettings.swift b/PDCore/PDCore/User/LocalSettings.swift index 7d57950..dbafd15 100644 --- a/PDCore/PDCore/User/LocalSettings.swift +++ b/PDCore/PDCore/User/LocalSettings.swift @@ -61,6 +61,7 @@ public class LocalSettings: NSObject { @SettingsStorage("DriveDDKDisabled") public var driveDDKDisabledValue: Bool? @SettingsStorage("DriveMacSyncRecoveryDisabled") public var driveMacSyncRecoveryDisabledValue: Bool? @SettingsStorage("DriveMacKeepDownloadedDisabled") public var driveMacKeepDownloadedDisabledValue: Bool? + @SettingsStorage("DriveMacPromoBannerDisabled") public var driveMacPromoBannerDisabledValue: Bool? @SettingsStorage("DriveAlbumsDisabled") public var driveAlbumsDisabledValue: Bool? @SettingsStorage("DriveCopyDisabled") public var driveCopyDisabledValue: Bool? @SettingsStorage("photoVolumeMigrationLastShownDate") public var photoVolumeMigrationLastShownDate: Date? @@ -184,6 +185,7 @@ public class LocalSettings: NSObject { self._driveDDKDisabledValue.configure(with: suite) self._driveMacSyncRecoveryDisabledValue.configure(with: suite) self._driveMacKeepDownloadedDisabledValue.configure(with: suite) + self._driveMacPromoBannerDisabledValue.configure(with: suite) self._didFetchFeatureFlags.configure(with: suite) self._promotedNewFeaturesValue.configure(with: suite) self._driveAlbumsDisabledValue.configure(with: suite) @@ -663,6 +665,11 @@ public class LocalSettings: NSObject { set { driveMacKeepDownloadedDisabledValue = newValue } } + public var driveMacPromoBannerDisabled: Bool { + get { driveMacPromoBannerDisabledValue ?? false } + set { driveMacPromoBannerDisabledValue = newValue } + } + public var ratingIOSDrive: Bool { get { ratingIOSDriveValue ?? false } set { ratingIOSDriveValue = newValue } diff --git a/ProtonDrive-macOS/ProtonDrive-macOS.xcodeproj/project.pbxproj b/ProtonDrive-macOS/ProtonDrive-macOS.xcodeproj/project.pbxproj index e230eeb..903535c 100644 --- a/ProtonDrive-macOS/ProtonDrive-macOS.xcodeproj/project.pbxproj +++ b/ProtonDrive-macOS/ProtonDrive-macOS.xcodeproj/project.pbxproj @@ -1152,14 +1152,14 @@ ); mainGroup = AB71531724274ED900543720; packageReferences = ( - D83C419C2C53A233002EF29C /* XCRemoteSwiftPackageReference "Sparkle.git" */, + D83C419C2C53A233002EF29C /* XCRemoteSwiftPackageReference "Sparkle" */, D8AB30D62C6217B5006A5F7C /* XCRemoteSwiftPackageReference "OHHTTPStubs" */, - D8AB30D92C621957006A5F7C /* XCRemoteSwiftPackageReference "TrustKit.git" */, - D8AB30E32C621ABF006A5F7C /* XCRemoteSwiftPackageReference "apple-fusion.git" */, + D8AB30D92C621957006A5F7C /* XCRemoteSwiftPackageReference "TrustKit" */, + D8AB30E32C621ABF006A5F7C /* XCRemoteSwiftPackageReference "apple-fusion" */, 3E9137BE2CC77C0400651BC1 /* XCRemoteSwiftPackageReference "SwiftLintPlugins" */, - 661DC23D2CB94C3C00DECBDE /* XCRemoteSwiftPackageReference "CryptoSwift.git" */, - 66E09DFC2E7DA3C30082A1B0 /* XCRemoteSwiftPackageReference "Yams.git" */, - 66E09E202E7DAD9F0082A1B0 /* XCRemoteSwiftPackageReference "protoncore_ios.git" */, + 661DC23D2CB94C3C00DECBDE /* XCRemoteSwiftPackageReference "CryptoSwift" */, + 66E09DFC2E7DA3C30082A1B0 /* XCRemoteSwiftPackageReference "Yams" */, + 66E09E202E7DAD9F0082A1B0 /* XCRemoteSwiftPackageReference "protoncore_ios" */, ); productRefGroup = AB71532124274ED900543720 /* Products */; projectDirPath = ""; @@ -1825,7 +1825,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 2.10.0; + MARKETING_VERSION = 2.10.1; OTHER_CODE_SIGN_FLAGS = ""; PRODUCT_BUNDLE_IDENTIFIER = ch.protonmail.drive; PRODUCT_MODULE_NAME = ProtonDriveMac; @@ -1912,7 +1912,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 2.10.0; + MARKETING_VERSION = 2.10.1; OTHER_CODE_SIGN_FLAGS = ""; PRODUCT_BUNDLE_IDENTIFIER = ch.protonmail.drive; PRODUCT_MODULE_NAME = ProtonDriveMac; @@ -1983,7 +1983,7 @@ "@executable_path/../../../../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 2.10.0; + MARKETING_VERSION = 2.10.1; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; OTHER_CODE_SIGN_FLAGS = ""; @@ -2686,7 +2686,7 @@ version = 0.57.0; }; }; - 661DC23D2CB94C3C00DECBDE /* XCRemoteSwiftPackageReference "CryptoSwift.git" */ = { + 661DC23D2CB94C3C00DECBDE /* XCRemoteSwiftPackageReference "CryptoSwift" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/krzyzanowskim/CryptoSwift.git"; requirement = { @@ -2694,7 +2694,7 @@ minimumVersion = 1.8.3; }; }; - 66E09DFC2E7DA3C30082A1B0 /* XCRemoteSwiftPackageReference "Yams.git" */ = { + 66E09DFC2E7DA3C30082A1B0 /* XCRemoteSwiftPackageReference "Yams" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/jpsim/Yams.git"; requirement = { @@ -2702,7 +2702,7 @@ minimumVersion = 5.0.0; }; }; - 66E09E202E7DAD9F0082A1B0 /* XCRemoteSwiftPackageReference "protoncore_ios.git" */ = { + 66E09E202E7DAD9F0082A1B0 /* XCRemoteSwiftPackageReference "protoncore_ios" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/ProtonMail/protoncore_ios.git"; requirement = { @@ -2710,7 +2710,7 @@ version = 33.2.0; }; }; - D83C419C2C53A233002EF29C /* XCRemoteSwiftPackageReference "Sparkle.git" */ = { + D83C419C2C53A233002EF29C /* XCRemoteSwiftPackageReference "Sparkle" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/sparkle-project/Sparkle.git"; requirement = { @@ -2727,7 +2727,7 @@ minimumVersion = 0.0.0; }; }; - D8AB30D92C621957006A5F7C /* XCRemoteSwiftPackageReference "TrustKit.git" */ = { + D8AB30D92C621957006A5F7C /* XCRemoteSwiftPackageReference "TrustKit" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/ProtonMail/TrustKit.git"; requirement = { @@ -2736,7 +2736,7 @@ minimumVersion = 0.0.0; }; }; - D8AB30E32C621ABF006A5F7C /* XCRemoteSwiftPackageReference "apple-fusion.git" */ = { + D8AB30E32C621ABF006A5F7C /* XCRemoteSwiftPackageReference "apple-fusion" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/ProtonMail/apple-fusion.git"; requirement = { @@ -2766,7 +2766,7 @@ }; 661DC23E2CB94C3C00DECBDE /* CryptoSwift */ = { isa = XCSwiftPackageProductDependency; - package = 661DC23D2CB94C3C00DECBDE /* XCRemoteSwiftPackageReference "CryptoSwift.git" */; + package = 661DC23D2CB94C3C00DECBDE /* XCRemoteSwiftPackageReference "CryptoSwift" */; productName = CryptoSwift; }; 667B32052C69F4E500D15C95 /* PDCore */ = { @@ -2787,47 +2787,47 @@ }; 66E09DFD2E7DA3C30082A1B0 /* Yams */ = { isa = XCSwiftPackageProductDependency; - package = 66E09DFC2E7DA3C30082A1B0 /* XCRemoteSwiftPackageReference "Yams.git" */; + package = 66E09DFC2E7DA3C30082A1B0 /* XCRemoteSwiftPackageReference "Yams" */; productName = Yams; }; 66E09DFF2E7DA42D0082A1B0 /* Yams */ = { isa = XCSwiftPackageProductDependency; - package = 66E09DFC2E7DA3C30082A1B0 /* XCRemoteSwiftPackageReference "Yams.git" */; + package = 66E09DFC2E7DA3C30082A1B0 /* XCRemoteSwiftPackageReference "Yams" */; productName = Yams; }; 66E09E012E7DA4490082A1B0 /* Yams */ = { isa = XCSwiftPackageProductDependency; - package = 66E09DFC2E7DA3C30082A1B0 /* XCRemoteSwiftPackageReference "Yams.git" */; + package = 66E09DFC2E7DA3C30082A1B0 /* XCRemoteSwiftPackageReference "Yams" */; productName = Yams; }; 66E09E282E7DB0A40082A1B0 /* ProtonCoreCryptoMultiversionPatchedGoImplementation */ = { isa = XCSwiftPackageProductDependency; - package = 66E09E202E7DAD9F0082A1B0 /* XCRemoteSwiftPackageReference "protoncore_ios.git" */; + package = 66E09E202E7DAD9F0082A1B0 /* XCRemoteSwiftPackageReference "protoncore_ios" */; productName = ProtonCoreCryptoMultiversionPatchedGoImplementation; }; 66E09E2A2E7DB0CA0082A1B0 /* ProtonCoreCryptoMultiversionPatchedGoImplementation */ = { isa = XCSwiftPackageProductDependency; - package = 66E09E202E7DAD9F0082A1B0 /* XCRemoteSwiftPackageReference "protoncore_ios.git" */; + package = 66E09E202E7DAD9F0082A1B0 /* XCRemoteSwiftPackageReference "protoncore_ios" */; productName = ProtonCoreCryptoMultiversionPatchedGoImplementation; }; 66E09E2C2E7DB0D00082A1B0 /* ProtonCoreCryptoMultiversionPatchedGoImplementation */ = { isa = XCSwiftPackageProductDependency; - package = 66E09E202E7DAD9F0082A1B0 /* XCRemoteSwiftPackageReference "protoncore_ios.git" */; + package = 66E09E202E7DAD9F0082A1B0 /* XCRemoteSwiftPackageReference "protoncore_ios" */; productName = ProtonCoreCryptoMultiversionPatchedGoImplementation; }; 66E09E2E2E7DB2F60082A1B0 /* ProtonCoreQuarkCommands */ = { isa = XCSwiftPackageProductDependency; - package = 66E09E202E7DAD9F0082A1B0 /* XCRemoteSwiftPackageReference "protoncore_ios.git" */; + package = 66E09E202E7DAD9F0082A1B0 /* XCRemoteSwiftPackageReference "protoncore_ios" */; productName = ProtonCoreQuarkCommands; }; 66E09E302E7DB2FC0082A1B0 /* ProtonCoreQuarkCommands */ = { isa = XCSwiftPackageProductDependency; - package = 66E09E202E7DAD9F0082A1B0 /* XCRemoteSwiftPackageReference "protoncore_ios.git" */; + package = 66E09E202E7DAD9F0082A1B0 /* XCRemoteSwiftPackageReference "protoncore_ios" */; productName = ProtonCoreQuarkCommands; }; 66E09E322E7DB3030082A1B0 /* ProtonCoreQuarkCommands */ = { isa = XCSwiftPackageProductDependency; - package = 66E09E202E7DAD9F0082A1B0 /* XCRemoteSwiftPackageReference "protoncore_ios.git" */; + package = 66E09E202E7DAD9F0082A1B0 /* XCRemoteSwiftPackageReference "protoncore_ios" */; productName = ProtonCoreQuarkCommands; }; 726691C02E9FCACD00796513 /* PDCoreTestingToolkit */ = { @@ -2852,7 +2852,7 @@ }; D83C419D2C53A233002EF29C /* Sparkle */ = { isa = XCSwiftPackageProductDependency; - package = D83C419C2C53A233002EF29C /* XCRemoteSwiftPackageReference "Sparkle.git" */; + package = D83C419C2C53A233002EF29C /* XCRemoteSwiftPackageReference "Sparkle" */; productName = Sparkle; }; D85AB8362C539D5600FFDC10 /* PDFileProvider */ = { @@ -2913,17 +2913,17 @@ }; D8AB30DA2C621957006A5F7C /* TrustKit */ = { isa = XCSwiftPackageProductDependency; - package = D8AB30D92C621957006A5F7C /* XCRemoteSwiftPackageReference "TrustKit.git" */; + package = D8AB30D92C621957006A5F7C /* XCRemoteSwiftPackageReference "TrustKit" */; productName = TrustKit; }; D8AB30E12C621A2F006A5F7C /* TrustKit */ = { isa = XCSwiftPackageProductDependency; - package = D8AB30D92C621957006A5F7C /* XCRemoteSwiftPackageReference "TrustKit.git" */; + package = D8AB30D92C621957006A5F7C /* XCRemoteSwiftPackageReference "TrustKit" */; productName = TrustKit; }; D8AB30E42C621ABF006A5F7C /* fusion */ = { isa = XCSwiftPackageProductDependency; - package = D8AB30E32C621ABF006A5F7C /* XCRemoteSwiftPackageReference "apple-fusion.git" */; + package = D8AB30E32C621ABF006A5F7C /* XCRemoteSwiftPackageReference "apple-fusion" */; productName = fusion; }; D8AB30E62C621B7F006A5F7C /* OHHTTPStubs */ = { diff --git a/ProtonDrive-macOS/ProtonDriveMac/AppCoordinator.swift b/ProtonDrive-macOS/ProtonDriveMac/AppCoordinator.swift index 66625c7..b5629b4 100644 --- a/ProtonDrive-macOS/ProtonDriveMac/AppCoordinator.swift +++ b/ProtonDrive-macOS/ProtonDriveMac/AppCoordinator.swift @@ -652,15 +652,13 @@ class AppCoordinator: NSObject, ObservableObject { state: appState, domainOperationsService: domainOperationsService ) - - await applicationEventObserver?.startSyncMonitoring( + + await applicationEventObserver?.configurePostLoginServices( syncObserver: syncObserver, globalProgressObserver: globalProgressObserver, - sessionVault: postLoginServices.tower.sessionVault - ) - - applicationEventObserver?.startGeneralSettingsMonitoring( - settingsService: postLoginServices.tower.generalSettings + sessionVault: postLoginServices.tower.sessionVault, + settingsService: postLoginServices.tower.generalSettings, + featureFlagsRepository: postLoginServices.tower.featureFlags ) let hasPlan = initialServices.sessionVault.userInfo?.hasAnySubscription diff --git a/ProtonDrive-macOS/ProtonDriveMac/PromoCampaigns/PromoCampaignInteractor.swift b/ProtonDrive-macOS/ProtonDriveMac/PromoCampaigns/PromoCampaignInteractor.swift index b5b5132..c9d9bcc 100644 --- a/ProtonDrive-macOS/ProtonDriveMac/PromoCampaigns/PromoCampaignInteractor.swift +++ b/ProtonDrive-macOS/ProtonDriveMac/PromoCampaigns/PromoCampaignInteractor.swift @@ -47,6 +47,7 @@ struct PromoCampaignConfiguration { fileprivate static let activeCampaigns: [PromoCampaignConfiguration] = [ PromoCampaignConfiguration( + campaignId: "bf-25-stage-1", timeRange: .limitedTime( start: Date(timeIntervalSinceReferenceDate: 783860400), // 2025-11-03 12:00 CET end: Date(timeIntervalSinceReferenceDate: 785156400) // 2025-11-18 12:00 CET @@ -54,9 +55,11 @@ struct PromoCampaignConfiguration { backgroundColor: Color(hex: "#D8FF00"), tintColor: Color(hex: "#291C5D"), icon: .discount, - text: "Black Friday: 50% off" + text: "Black Friday: 50% off", + resetsPreviousDismissal: false ), PromoCampaignConfiguration( + campaignId: "bf-25-stage-2", timeRange: .limitedTime( start: Date(timeIntervalSinceReferenceDate: 785156400), // 2025-11-18 12:00 CET end: Date(timeIntervalSinceReferenceDate: 786452400) // 2025-12-03 12:00 CET @@ -64,24 +67,29 @@ struct PromoCampaignConfiguration { backgroundColor: Color(hex: "#D8FF00"), tintColor: Color(hex: "#291C5D"), icon: .discount, - text: "Black Friday: 80% off" + text: "Black Friday: 80% off", + resetsPreviousDismissal: true ), PromoCampaignConfiguration( + campaignId: "upgrade-drive-plus", 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" + text: "Upgrade to Drive Plus", + resetsPreviousDismissal: false ) ] + let campaignId: String let timeRange: TimeRange let backgroundColor: Color let tintColor: Color let icon: BannerIcon let text: String + let resetsPreviousDismissal: Bool } protocol PromoCampaignInteractorProtocol { @@ -96,18 +104,19 @@ final class PromoCampaignInteractor: PromoCampaignInteractorProtocol { } @SettingsStorage(UserDefaults.PromoCampaign.hasDismissedBanner.rawValue) private var hasDismissedBanner: Bool? + @SettingsStorage(UserDefaults.PromoCampaign.lastSeenCampaignId.rawValue) private var lastSeenCampaign: String? + private var currentlyActiveCampaign = CurrentValueSubject(nil) private let dateResource: DateResource static let shared = PromoCampaignInteractor() - init( - dateResource: DateResource - ) { + init(dateResource: DateResource) { self.dateResource = dateResource _hasDismissedBanner.configure(with: Constants.appGroup) + _lastSeenCampaign.configure(with: Constants.appGroup) refreshCampaign() } @@ -116,17 +125,18 @@ final class PromoCampaignInteractor: PromoCampaignInteractorProtocol { self.init(dateResource: PromoCampaignDateResource()) } - func refreshCampaign(resetBannerDismissal: Bool = false) { - if resetBannerDismissal { + func refreshCampaign(forceResetBannerDismissal: Bool = false) { + let activeCampaign = getActiveCampaign() + + if forceResetBannerDismissal || shouldResetBannerDismissal(for: activeCampaign) { hasDismissedBanner = false } - guard (hasDismissedBanner ?? false) == false else { - currentlyActiveCampaign.send(.none) - return + if hasDismissedBanner == true { + return currentlyActiveCampaign.send(.none) } - let activeCampaign = getActiveCampaign() + lastSeenCampaign = activeCampaign?.campaignId currentlyActiveCampaign.send(activeCampaign) } @@ -149,4 +159,12 @@ final class PromoCampaignInteractor: PromoCampaignInteractorProtocol { } } } + + private func shouldResetBannerDismissal(for activeCampaign: PromoCampaignConfiguration?) -> Bool { + guard let activeCampaign, let lastSeenCampaign, hasDismissedBanner == true else { + return false + } + + return activeCampaign.resetsPreviousDismissal && activeCampaign.campaignId != lastSeenCampaign + } } diff --git a/ProtonDrive-macOS/ProtonDriveMac/PromoCampaigns/UserDefaults+PromoCampaigns.swift b/ProtonDrive-macOS/ProtonDriveMac/PromoCampaigns/UserDefaults+PromoCampaigns.swift index 343236c..033eb6b 100644 --- a/ProtonDrive-macOS/ProtonDriveMac/PromoCampaigns/UserDefaults+PromoCampaigns.swift +++ b/ProtonDrive-macOS/ProtonDriveMac/PromoCampaigns/UserDefaults+PromoCampaigns.swift @@ -20,5 +20,6 @@ import Foundation extension UserDefaults { enum PromoCampaign: String { case hasDismissedBanner + case lastSeenCampaignId } } diff --git a/ProtonDrive-macOS/ProtonDriveMac/QA Settings/QASettingsView.swift b/ProtonDrive-macOS/ProtonDriveMac/QA Settings/QASettingsView.swift index 6061f0a..5175850 100644 --- a/ProtonDrive-macOS/ProtonDriveMac/QA Settings/QASettingsView.swift +++ b/ProtonDrive-macOS/ProtonDriveMac/QA Settings/QASettingsView.swift @@ -198,9 +198,35 @@ struct QASettingsView: View { exit(0) } - Text("Unleash FFs — DriveDDKDisabled: \(vm.driveDDKDisabledFeatureFlagValue ? "true" : "false"), DriveDDKIntelEnabled: \(vm.driveDDKIntelEnabledFeatureFlagValue ? "true" : "false") (used on Intel)") + Text( + [ + "Unleash FFs — DriveDDKDisabled: \(vm.driveDDKDisabledFeatureFlagValue ? "true" : "false"),", + "DriveDDKIntelEnabled: \(vm.driveDDKIntelEnabledFeatureFlagValue ? "true" : "false") (used on Intel),", + ].joined(separator: "\n") + ) } + VStack(alignment: .leading, spacing: 2) { + Text("BF'25:") + .fontWeight(.bold) + Picker("", selection: $vm.driveMacPromoBannerDisabled) { + ForEach(QASettingsViewModel.FeatureFlagOptions.allCases.map(\.rawValue), id: \.self) { + Text($0) + } + } + .pickerStyle(SegmentedPickerStyle()) + .onChange(of: vm.driveMacPromoBannerDisabled) { _ in + exit(0) + } + Text("Remember, this is a killswitch: enabled means banner should be disabled.") + Text( + [ + "Unleash FFs — DriveMacPromoBannerDisabled: \(vm.driveDDKDisabledFeatureFlagValue ? "true" : "false"),", + "QA Setting - DriveMacPromoBannerDisabled: \(vm.driveMacPromoBannerDisabledStorage ?? false)" + ].joined(separator: "\n") + ) + } + VStack(alignment: .leading, spacing: 2) { Text("Disconnect domain:") .fontWeight(.bold) @@ -210,7 +236,7 @@ struct QASettingsView: View { } } .pickerStyle(SegmentedPickerStyle()) - + Text("Backend feature flag value: \(vm.domainReconnectionFeatureFlagValue ? "true" : "false")") } } diff --git a/ProtonDrive-macOS/ProtonDriveMac/QA Settings/QASettingsViewModel.swift b/ProtonDrive-macOS/ProtonDriveMac/QA Settings/QASettingsViewModel.swift index 1b1b965..a99d1a0 100644 --- a/ProtonDrive-macOS/ProtonDriveMac/QA Settings/QASettingsViewModel.swift +++ b/ProtonDrive-macOS/ProtonDriveMac/QA Settings/QASettingsViewModel.swift @@ -34,6 +34,7 @@ struct QASettingsConstants { static let driveDDKEnabledInQASettings = "driveDDKEnabledInQASettings" static let globalProgressStatusMenuEnabled = "globalProgressStatusMenuEnabled" static let overrideDateForPromoCampaign = "overrideDateForPromoCampaign" + static let driveMacPromoBannerDisabled = "driveMacPromoBannerDisabled" } protocol EventLoopManager: AnyObject { @@ -126,11 +127,23 @@ class QASettingsViewModel: ObservableObject { var driveDDKDisabledFeatureFlagValue: Bool { featureFlags?.isEnabled(flag: .driveDDKDisabled) ?? false } + @Published var driveDDKEnabled: String = FeatureFlagOptions.useFF.rawValue { didSet { driveDDKEnabledStorage = FeatureFlagOptions(rawValue: driveDDKEnabled)?.toBool } } + @SettingsStorage(QASettingsConstants.driveDDKEnabledInQASettings) var driveDDKEnabledStorage: Bool? + var driveMacPromoBannerDisabledFeatureFlagValue: Bool { + featureFlags?.isEnabled(flag: .driveMacPromoBannerDisabled) ?? false + } + + @Published var driveMacPromoBannerDisabled: String = FeatureFlagOptions.useFF.rawValue { + didSet { driveMacPromoBannerDisabledStorage = FeatureFlagOptions(rawValue: driveMacPromoBannerDisabled)?.toBool } + } + + @SettingsStorage(QASettingsConstants.driveMacPromoBannerDisabled) var driveMacPromoBannerDisabledStorage: Bool? + @Published var overrideDateForPromoCampaign: String = "" { didSet { overrideDateForPromoCampaignStorage = overrideDateForPromoCampaign } } @@ -175,6 +188,7 @@ class QASettingsViewModel: ObservableObject { self._requiresPostMigrationCleanup.configure(with: suite) self._disconnectDomainOnSignOutStorage.configure(with: suite) self._driveDDKEnabledStorage.configure(with: suite) + self._driveMacPromoBannerDisabledStorage.configure(with: suite) self.dumper = dumperDependencies.map(Dumper.init) self.environment = Constants.appGroup.userDefaults.string(forKey: Constants.SettingsBundleKeys.host.rawValue) ?? "" @@ -203,6 +217,7 @@ class QASettingsViewModel: ObservableObject { self.enablePostMigrationCleanup = requiresPostMigrationCleanup ?? false self.disconnectDomainOnSignOut = FeatureFlagOptions(bool: disconnectDomainOnSignOutStorage).rawValue self.driveDDKEnabled = FeatureFlagOptions(bool: driveDDKEnabledStorage).rawValue + self.driveMacPromoBannerDisabled = FeatureFlagOptions(bool: driveMacPromoBannerDisabledStorage).rawValue self.promoCampaignInteractor.activeCampaign.sink { activeCampaign in self.activeCampaign = activeCampaign @@ -497,7 +512,7 @@ class QASettingsViewModel: ObservableObject { } func refreshPromoCampaign() { - self.promoCampaignInteractor.refreshCampaign(resetBannerDismissal: true) + self.promoCampaignInteractor.refreshCampaign(forceResetBannerDismissal: true) } } diff --git a/ProtonDrive-macOS/ProtonDriveMac/StatusWindow/ApplicationEventObserver.swift b/ProtonDrive-macOS/ProtonDriveMac/StatusWindow/ApplicationEventObserver.swift index 137a130..a0df8b2 100644 --- a/ProtonDrive-macOS/ProtonDriveMac/StatusWindow/ApplicationEventObserver.swift +++ b/ProtonDrive-macOS/ProtonDriveMac/StatusWindow/ApplicationEventObserver.swift @@ -40,6 +40,7 @@ class ApplicationEventObserver: ObservableObject { #if HAS_QA_FEATURES @Published private(set) var state: ApplicationState @Published var syncItemHistory = [SyncHistoryItem]() + @SettingsStorage(QASettingsConstants.driveMacPromoBannerDisabled) var hasPromoBannerDisabledInQASettings: Bool? /// Counts how many times the application state is updated, to enable detecting when it happens too much. static var updateCounter = 0 @@ -71,6 +72,9 @@ 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 feature flags for the user + private var featureFlagsRepository: FeatureFlagsRepository? + /// Fires whenever there's a change to active promo campaigns for the user. private var promoCampaignInteractor: PromoCampaignInteractorProtocol? @@ -99,6 +103,10 @@ class ApplicationEventObserver: ObservableObject { self.deleteAlerter = DeleteAlerter() + #if HAS_QA_FEATURES + self._hasPromoBannerDisabledInQASettings.configure(with: Constants.appGroup) + #endif + setUpObservers() } @@ -109,9 +117,19 @@ class ApplicationEventObserver: ObservableObject { // MARK: - Public - public func startSyncMonitoring(syncObserver: SyncDBObserver, - globalProgressObserver: GlobalProgressObserver?, - sessionVault: SessionVault?) async { + /// Some Drive services are only available after user is logged in, + /// such as the session vault, general settings and feature flags. + /// + /// This function provides a convenient place to configure observation + /// of these services. It's expected that any service with long running + /// observations are cancelled in `stopMonitoring` as needed. + public func configurePostLoginServices( + syncObserver: SyncDBObserver, + globalProgressObserver: GlobalProgressObserver?, + sessionVault: SessionVault?, + settingsService: GeneralSettings?, + featureFlagsRepository: FeatureFlagsRepository? + ) async { Log.trace() self.syncObserver = syncObserver @@ -126,10 +144,6 @@ class ApplicationEventObserver: ObservableObject { self.subscribetoLogin() self.subscribetoUserInfo() - } - - public func startGeneralSettingsMonitoring(settingsService: GeneralSettings) { - Log.trace() generalSettingsService = settingsService generalSettingsService?.fetchUserSettings() @@ -140,6 +154,8 @@ class ApplicationEventObserver: ObservableObject { self?.state.setUserSettings(userSettings) } .store(in: &userCancellables) + + self.featureFlagsRepository = featureFlagsRepository } /// - Parameters: @@ -151,8 +167,12 @@ class ApplicationEventObserver: ObservableObject { self.globalProgressObserver = nil self.elapsedTimeService = nil self.sessionVault = nil + self.generalSettingsService = nil + self.featureFlagsRepository = nil self.userCancellables.removeAll() + didReceiveLogoutState(isSignedIn: false) + if !dueToSignOut { self.globalCancellables.removeAll() } @@ -344,9 +364,25 @@ class ApplicationEventObserver: ObservableObject { ) .receive(on: DispatchQueue.main) .sink { [weak self] campaign, userInfo, userSettings in + guard let self, let featureFlagsRepository else { + return + } + + guard !featureFlagsRepository.isEnabled(flag: .driveMacPromoBannerDisabled) else { + Log.trace("Promo campaign filtered out because killswitch is active") + return self.state.setVisibleCampaign(nil) + } + + #if HAS_QA_FEATURES + if hasPromoBannerDisabledInQASettings == true { + Log.trace("Promo campaign filtered out because it's disabled in QA settings") + return self.state.setVisibleCampaign(nil) + } + #endif + 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) + self.state.setVisibleCampaign(.none) return } @@ -359,11 +395,11 @@ class ApplicationEventObserver: ObservableObject { // * 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) + self.state.setVisibleCampaign(.none) return } - self?.state.setVisibleCampaign(campaign) + self.state.setVisibleCampaign(campaign) } .store(in: &userCancellables) } diff --git a/ProtonDrive-macOS/ReleaseNotes.html b/ProtonDrive-macOS/ReleaseNotes.html index f5e3c5c..df67c33 100644 --- a/ProtonDrive-macOS/ReleaseNotes.html +++ b/ProtonDrive-macOS/ReleaseNotes.html @@ -1,4 +1,10 @@
+

2.10.1

+ +

+ - Fixes a rare crash encountered when uploading or downloading many files
+

+

2.10.0