This commit is contained in:
Robert Patchett
2025-10-29 10:05:22 +01:00
parent 9b7ddc5396
commit d4e0106494
13 changed files with 173 additions and 61 deletions
@@ -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"
@@ -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
@@ -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
@@ -33,6 +33,7 @@ public enum FeatureAvailabilityFlag: CaseIterable {
case driveDDKDisabled
case driveMacSyncRecoveryDisabled
case driveMacKeepDownloadedDisabled
case driveMacPromoBannerDisabled
// Sharing
case driveSharingMigration
+7
View File
@@ -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 }
@@ -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 */ = {
@@ -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
@@ -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<PromoCampaignConfiguration?, Never>(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
}
}
@@ -20,5 +20,6 @@ import Foundation
extension UserDefaults {
enum PromoCampaign: String {
case hasDismissedBanner
case lastSeenCampaignId
}
}
@@ -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")")
}
}
@@ -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)
}
}
@@ -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)
}
+6
View File
@@ -1,4 +1,10 @@
<div>
<h1>2.10.1</h1>
<p>
- Fixes a rare crash encountered when uploading or downloading many files<br>
</p>
<h1>2.10.0</h1>
<p>