Skip to content

Commit

Permalink
Add Sync feature flags (#607)
Browse files Browse the repository at this point in the history
Task/Issue URL: https://app.asana.com/0/0/1206046777189407/f

Description:
This change add support for Sync feature in Privacy Configuration, together with
4 subfeatures defining availability of various parts of Sync experience.
DDGSyncing gets a read-only feature flag variable as well as a publisher.
PrivacyConfigurationManager is now a Sync dependency, and DDGSync takes
care internally of listening to Privacy Config changes and updating feature flags
as needed.
Feature flag responsible for actual data syncing is handled internally in DDGSync
by cancelling all pending sync operations and disabling adding new operations.
Other feature flags should be handled by client apps.
  • Loading branch information
ayoy authored Dec 20, 2023
1 parent 308abf4 commit d671acc
Show file tree
Hide file tree
Showing 11 changed files with 244 additions and 11 deletions.
4 changes: 2 additions & 2 deletions Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-argument-parser",
"state" : {
"revision" : "8f4d2753f0e4778c76d5f05ad16c74f707390531",
"version" : "1.2.3"
"revision" : "c8ed701b513cf5177118a175d85fbbbcd707ab41",
"version" : "1.3.0"
}
},
{
Expand Down
1 change: 1 addition & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ let package = Package(
.target(
name: "DDGSync",
dependencies: [
"BrowserServicesKit",
"Common",
.product(name: "DDGSyncCrypto", package: "sync_crypto"),
"Networking"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ public enum PrivacyFeature: String {
case newTabContinueSetUp
case networkProtection
case dbp
case sync
}

/// An abstraction to be implemented by any "subfeature" of a given `PrivacyConfiguration` feature.
Expand Down Expand Up @@ -82,3 +83,14 @@ public enum DBPSubfeature: String, Equatable, PrivacySubfeature {
case waitlist
case waitlistBetaActive
}

public enum SyncSubfeature: String, PrivacySubfeature {
public var parent: PrivacyFeature {
.sync
}

case level0ShowSync
case level1AllowDataSyncing
case level2AllowSetupFlows
case level3AllowCreateAccount
}
34 changes: 31 additions & 3 deletions Sources/DDGSync/DDGSync.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,21 @@
// limitations under the License.
//

import Foundation
import BrowserServicesKit
import Combine
import DDGSyncCrypto
import Common
import DDGSyncCrypto
import Foundation

public class DDGSync: DDGSyncing {

public static let bundle = Bundle.module

@Published public private(set) var featureFlags: SyncFeatureFlags = .all
public var featureFlagsPublisher: AnyPublisher<SyncFeatureFlags, Never> {
$featureFlags.eraseToAnyPublisher()
}

enum Constants {
public static let syncEnabledKey = "com.duckduckgo.sync.enabled"
}
Expand Down Expand Up @@ -55,9 +61,15 @@ public class DDGSync: DDGSyncing {
/// This is the constructor intended for use by app clients.
public convenience init(dataProvidersSource: DataProvidersSource,
errorEvents: EventMapping<SyncError>,
privacyConfigurationManager: PrivacyConfigurationManaging,
log: @escaping @autoclosure () -> OSLog = .disabled,
environment: ServerEnvironment = .production) {
let dependencies = ProductionDependencies(serverEnvironment: environment, errorEvents: errorEvents, log: log())
let dependencies = ProductionDependencies(
serverEnvironment: environment,
privacyConfigurationManager: privacyConfigurationManager,
errorEvents: errorEvents,
log: log()
)
self.init(dataProvidersSource: dataProvidersSource, dependencies: dependencies)
}

Expand Down Expand Up @@ -189,6 +201,16 @@ public class DDGSync: DDGSyncing {
init(dataProvidersSource: DataProvidersSource, dependencies: SyncDependencies) {
self.dataProvidersSource = dataProvidersSource
self.dependencies = dependencies

featureFlagsCancellable = self.dependencies.privacyConfigurationManager.updatesPublisher
.compactMap { [weak self] in
self?.dependencies.privacyConfigurationManager.privacyConfig
}
.prepend(dependencies.privacyConfigurationManager.privacyConfig)
.map(SyncFeatureFlags.init)
.removeDuplicates()
.receive(on: DispatchQueue.main)
.assign(to: \.featureFlags, onWeaklyHeld: self)
}

public func initializeIfNeeded() {
Expand Down Expand Up @@ -229,6 +251,7 @@ public class DDGSync: DDGSyncing {
dependencies.scheduler.isEnabled = false
startSyncCancellable?.cancel()
syncQueueCancellable?.cancel()
isDataSyncingFeatureFlagEnabledCancellable?.cancel()
try syncQueue?.dataProviders.forEach { try $0.deregisterFeature() }
syncQueue = nil
authState = .inactive
Expand Down Expand Up @@ -291,6 +314,9 @@ public class DDGSync: DDGSyncing {
self?.syncQueue?.resumeQueue()
}

isDataSyncingFeatureFlagEnabledCancellable = featureFlagsPublisher.prepend(featureFlags).map { $0.contains(.dataSyncing) }
.assign(to: \.isDataSyncingFeatureFlagEnabled, onWeaklyHeld: syncQueue)

dependencies.scheduler.isEnabled = true
self.syncQueue = syncQueue
}
Expand All @@ -317,6 +343,8 @@ public class DDGSync: DDGSyncing {
private var startSyncCancellable: AnyCancellable?
private var cancelSyncCancellable: AnyCancellable?
private var resumeSyncCancellable: AnyCancellable?
private var featureFlagsCancellable: AnyCancellable?
private var isDataSyncingFeatureFlagEnabledCancellable: AnyCancellable?

private var syncQueue: SyncQueue?
private var syncQueueCancellable: AnyCancellable?
Expand Down
15 changes: 13 additions & 2 deletions Sources/DDGSync/DDGSyncing.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@
// limitations under the License.
//

import Foundation
import DDGSyncCrypto
import BrowserServicesKit
import Combine
import DDGSyncCrypto
import Foundation

public enum SyncAuthState: String, Sendable, Codable {
/// Sync engine is not initialized.
Expand Down Expand Up @@ -48,6 +49,16 @@ public protocol DDGSyncing: DDGSyncingDebuggingSupport {

var dataProvidersSource: DataProvidersSource? { get set }

/**
Describes current availability of sync features.
*/
var featureFlags: SyncFeatureFlags { get }

/**
Emits changes to current availability of sync features
*/
var featureFlagsPublisher: AnyPublisher<SyncFeatureFlags, Never> { get }

/**
Describes current state of sync account.

Expand Down
87 changes: 87 additions & 0 deletions Sources/DDGSync/SyncFeatureFlags.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
//
// SyncFeatureFlags.swift
//
// Copyright © 2023 DuckDuckGo. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

import BrowserServicesKit
import Foundation

/**
* This enum describes available Sync features.
*/
public struct SyncFeatureFlags: OptionSet {
public let rawValue: Int

public init(rawValue: Int) {
self.rawValue = rawValue
}

// MARK: - Individual Flags

/// Sync UI is visible
public static let userInterface = SyncFeatureFlags(rawValue: 1 << 0)

/// Data syncing is available
public static let dataSyncing = SyncFeatureFlags(rawValue: 1 << 1)

/// Logging in to existing accounts is available (connect flows + account recovery)
public static let accountLogin = SyncFeatureFlags(rawValue: 1 << 2)

/// Creating new accounts is available
public static let accountCreation = SyncFeatureFlags(rawValue: 1 << 4)

// MARK: - Helper Flags

public static let connectFlows = SyncFeatureFlags.accountLogin
public static let accountRecovery = SyncFeatureFlags.accountLogin

// MARK: - Support levels

/// Used when all feature flags are disabled
public static let unavailable: SyncFeatureFlags = []

/// Level 0 feature flag as defined in Privacy Configuration
public static let level0ShowSync: SyncFeatureFlags = [.userInterface]
/// Level 1 feature flag as defined in Privacy Configuration
public static let level1AllowDataSyncing: SyncFeatureFlags = [.userInterface, .dataSyncing]
/// Level 2 feature flag as defined in Privacy Configuration
public static let level2AllowSetupFlows: SyncFeatureFlags = [.userInterface, .dataSyncing, .accountLogin]
/// Level 3 feature flag as defined in Privacy Configuration
public static let level3AllowCreateAccount: SyncFeatureFlags = [.userInterface, .dataSyncing, .accountLogin, .accountCreation]

/// Alias for the state when all features are available
public static let all: SyncFeatureFlags = .level3AllowCreateAccount

// MARK: -

init(privacyConfig: PrivacyConfiguration) {
guard privacyConfig.isEnabled(featureKey: .sync) else {
self = .unavailable
return
}
if !privacyConfig.isSubfeatureEnabled(SyncSubfeature.level0ShowSync) {
self = .unavailable
} else if !privacyConfig.isSubfeatureEnabled(SyncSubfeature.level1AllowDataSyncing) {
self = .level0ShowSync
} else if !privacyConfig.isSubfeatureEnabled(SyncSubfeature.level2AllowSetupFlows) {
self = .level1AllowDataSyncing
} else if !privacyConfig.isSubfeatureEnabled(SyncSubfeature.level3AllowCreateAccount) {
self = .level2AllowSetupFlows
} else {
self = .level3AllowCreateAccount
}
}
}
15 changes: 12 additions & 3 deletions Sources/DDGSync/internal/ProductionDependencies.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@
// limitations under the License.
//

import Foundation
import BrowserServicesKit
import Common
import Foundation
import Persistence

struct ProductionDependencies: SyncDependencies {
Expand All @@ -30,19 +31,25 @@ struct ProductionDependencies: SyncDependencies {
let secureStore: SecureStoring
let crypter: CryptingInternal
let scheduler: SchedulingInternal
let privacyConfigurationManager: PrivacyConfigurationManaging
let errorEvents: EventMapping<SyncError>

var log: OSLog {
getLog()
}
private let getLog: () -> OSLog

init(serverEnvironment: ServerEnvironment, errorEvents: EventMapping<SyncError>, log: @escaping @autoclosure () -> OSLog = .disabled) {

init(
serverEnvironment: ServerEnvironment,
privacyConfigurationManager: PrivacyConfigurationManaging,
errorEvents: EventMapping<SyncError>,
log: @escaping @autoclosure () -> OSLog = .disabled
) {
self.init(fileStorageUrl: FileManager.default.applicationSupportDirectoryForComponent(named: "Sync"),
serverEnvironment: serverEnvironment,
keyValueStore: UserDefaults(),
secureStore: SecureStorage(),
privacyConfigurationManager: privacyConfigurationManager,
errorEvents: errorEvents,
log: log())
}
Expand All @@ -52,13 +59,15 @@ struct ProductionDependencies: SyncDependencies {
serverEnvironment: ServerEnvironment,
keyValueStore: KeyValueStoring,
secureStore: SecureStoring,
privacyConfigurationManager: PrivacyConfigurationManaging,
errorEvents: EventMapping<SyncError>,
log: @escaping @autoclosure () -> OSLog = .disabled
) {
self.fileStorageUrl = fileStorageUrl
self.endpoints = Endpoints(serverEnvironment: serverEnvironment)
self.keyValueStore = keyValueStore
self.secureStore = secureStore
self.privacyConfigurationManager = privacyConfigurationManager
self.errorEvents = errorEvents
self.getLog = log

Expand Down
4 changes: 3 additions & 1 deletion Sources/DDGSync/internal/SyncDependencies.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@
// limitations under the License.
//

import Foundation
import BrowserServicesKit
import Combine
import Common
import Foundation
import Persistence

protocol SyncDependenciesDebuggingSupport {
Expand All @@ -34,6 +35,7 @@ protocol SyncDependencies: SyncDependenciesDebuggingSupport {
var secureStore: SecureStoring { get }
var crypter: CryptingInternal { get }
var scheduler: SchedulingInternal { get }
var privacyConfigurationManager: PrivacyConfigurationManaging { get }
var errorEvents: EventMapping<SyncError> { get }
var log: OSLog { get }

Expand Down
15 changes: 15 additions & 0 deletions Sources/DDGSync/internal/SyncQueue.swift
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,22 @@ final class SyncQueue {
}
}

var isDataSyncingFeatureFlagEnabled: Bool = true {
didSet {
if isDataSyncingFeatureFlagEnabled {
os_log(.debug, log: self.log, "Sync Feature has been enabled")
} else {
os_log(.debug, log: self.log, "Sync Feature has been disabled, cancelling all operations")
operationQueue.cancelAllOperations()
}
}
}

func startSync() {
guard isDataSyncingFeatureFlagEnabled else {
os_log(.debug, log: self.log, "Sync Feature is temporarily disabled, not starting sync")
return
}
let operation = makeSyncOperation()
operationQueue.addOperation(operation)
}
Expand Down
Loading

0 comments on commit d671acc

Please sign in to comment.