Skip to content

Commit

Permalink
Merge branch 'main' into sam/vpn-error-description-fix
Browse files Browse the repository at this point in the history
* main:
  Upgrade to Xcode 16 and macOS 15 (#1075)
  add text zoom feature flag (#1057)
  Add support for local overrides for feature flags (#1074)
  Speculative password import prompt crash fix (#1072)
  • Loading branch information
samsymons committed Nov 18, 2024
2 parents a0de881 + 07af4d6 commit 607342d
Show file tree
Hide file tree
Showing 27 changed files with 742 additions and 106 deletions.
15 changes: 9 additions & 6 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:

name: Run unit tests (macOS)

runs-on: macos-14
runs-on: macos-15
timeout-minutes: 30

outputs:
Expand Down Expand Up @@ -67,12 +67,13 @@ jobs:
run: set -o pipefail && swift test | tee -a build-log.txt | xcbeautify --report junit --report-path . --junit-report-filename tests.xml

- name: Publish Unit Tests Report
uses: mikepenz/action-junit-report@v3
uses: mikepenz/action-junit-report@v4
if: always()
with:
check_name: Test Report (macOS)
report_paths: tests.xml
require_tests: true
check_retries: true

- name: Update Asana with failed unit tests
if: always() # always run even if the previous step fails
Expand Down Expand Up @@ -108,7 +109,7 @@ jobs:

name: Run unit tests (iOS)

runs-on: macos-14
runs-on: macos-15
timeout-minutes: 30

steps:
Expand Down Expand Up @@ -143,7 +144,7 @@ jobs:
run: |
while xcodebuild -resolvePackageDependencies \
-scheme BrowserServicesKit-Package \
-destination 'platform=iOS Simulator,name=iPhone 15,OS=17' \
-destination 'platform=iOS Simulator,name=iPhone 16,OS=18.1' \
-derivedDataPath DerivedData \
2>&1 | grep Error; do :; done
Expand All @@ -156,16 +157,18 @@ jobs:
run: |
set -o pipefail && xcodebuild test \
-scheme BrowserServicesKit \
-destination 'platform=iOS Simulator,name=iPhone 15,OS=17' \
-destination 'platform=iOS Simulator,name=iPhone 16,OS=18.1' \
-derivedDataPath DerivedData \
-skipPackagePluginValidation \
-skipMacroValidation \
-test-iterations 3 \
-retry-tests-on-failure \
CODE_SIGNING_ALLOWED=NO \
| tee -a ios-build-log.txt \
| xcbeautify --report junit --report-path . --junit-report-filename ios-unittests.xml
- name: Publish Unit Tests Report
uses: mikepenz/action-junit-report@v3
uses: mikepenz/action-junit-report@v4
if: always()
with:
check_name: Test Report (iOS)
Expand Down
2 changes: 1 addition & 1 deletion .xcode-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
15.4
16.1
4 changes: 2 additions & 2 deletions Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/duckduckgo/GRDB.swift.git",
"state" : {
"revision" : "4225b85c9a0c50544e413a1ea1e502c802b44b35",
"version" : "2.4.0"
"revision" : "5b2f6a81099d26ae0f9e38788f51490cd6a4b202",
"version" : "2.4.2"
}
},
{
Expand Down
10 changes: 3 additions & 7 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ let package = Package(
],
dependencies: [
.package(url: "https://github.com/duckduckgo/duckduckgo-autofill.git", exact: "15.1.0"),
.package(url: "https://github.com/duckduckgo/GRDB.swift.git", exact: "2.4.0"),
.package(url: "https://github.com/duckduckgo/GRDB.swift.git", exact: "2.4.2"),
.package(url: "https://github.com/duckduckgo/TrackerRadarKit", exact: "3.0.0"),
.package(url: "https://github.com/duckduckgo/sync_crypto", exact: "0.3.0"),
.package(url: "https://github.com/gumob/PunycodeSwift.git", exact: "3.0.0"),
Expand Down Expand Up @@ -411,10 +411,6 @@ let package = Package(
dependencies: [
"Common"
],
resources: [
.copy("hashPrefixes.json"),
.copy("filterSet.json")
],
swiftSettings: [
.define("DEBUG", .when(configuration: .debug))
]
Expand Down Expand Up @@ -655,8 +651,8 @@ let package = Package(
"PixelKit"
],
resources: [
.copy("hashPrefixes.json"),
.copy("filterSet.json")
.copy("Resources/hashPrefixes.json"),
.copy("Resources/filterSet.json")
]
),
.testTarget(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -748,7 +748,6 @@ extension AutofillUserScript {
if !credentials.isEmpty {
self?.passwordImportDelegate?.autofillUserScriptDidFinishImportWithImportedCredentialForCurrentDomain()
}
replyHandler(nil)
})
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
//
// FeatureFlagLocalOverrides.swift
//
// Copyright © 2024 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 Combine
import Foundation
import Persistence

/// This protocol defines persistence layer for feature flag overrides.
public protocol FeatureFlagLocalOverridesPersisting {
/// Return value for the flag override.
///
/// If there's no override, this function should return `nil`.
///
func value<Flag: FeatureFlagDescribing>(for flag: Flag) -> Bool?

/// Set new override for the feature flag.
///
/// Flag can be overridden to `true` or `false`. Setting `nil` clears the override.
///
func set<Flag: FeatureFlagDescribing>(_ value: Bool?, for flag: Flag)
}

public struct FeatureFlagLocalOverridesUserDefaultsPersistor: FeatureFlagLocalOverridesPersisting {

public let keyValueStore: KeyValueStoring

public init(keyValueStore: KeyValueStoring) {
self.keyValueStore = keyValueStore
}

public func value<Flag: FeatureFlagDescribing>(for flag: Flag) -> Bool? {
let key = key(for: flag)
return keyValueStore.object(forKey: key) as? Bool
}

public func set<Flag: FeatureFlagDescribing>(_ value: Bool?, for flag: Flag) {
let key = key(for: flag)
keyValueStore.set(value, forKey: key)
}

/// This function returns the User Defaults key for a feature flag override.
///
/// It uses camel case to simplify inter-process User Defaults KVO.
///
private func key<Flag: FeatureFlagDescribing>(for flag: Flag) -> String {
return "localOverride\(flag.rawValue.capitalizedFirstLetter)"
}
}

private extension String {
var capitalizedFirstLetter: String {
return prefix(1).capitalized + dropFirst()
}
}

/// This protocol defines the callback that can be used to reacting to feature flag changes.
public protocol FeatureFlagLocalOverridesHandling {

/// This function is called whenever an effective value of a feature flag
/// changes as a result of adding or removing a local override.
///
/// It can be implemented by client apps to react to changes to feature flag
/// value in runtime, caused by adjusting its local override.
func flagDidChange<Flag: FeatureFlagDescribing>(_ featureFlag: Flag, isEnabled: Bool)
}

/// `FeatureFlagLocalOverridesHandling` implementation providing Combine publisher for flag changes.
///
/// It can be used by client apps if a more sophisticated handler isn't needed.
///
public struct FeatureFlagOverridesPublishingHandler<F: FeatureFlagDescribing>: FeatureFlagLocalOverridesHandling {

public let flagDidChangePublisher: AnyPublisher<(F, Bool), Never>
private let flagDidChangeSubject = PassthroughSubject<(F, Bool), Never>()

public init() {
flagDidChangePublisher = flagDidChangeSubject.eraseToAnyPublisher()
}

public func flagDidChange<Flag: FeatureFlagDescribing>(_ featureFlag: Flag, isEnabled: Bool) {
guard let flag = featureFlag as? F else { return }
flagDidChangeSubject.send((flag, isEnabled))
}
}

/// This protocol defines the interface for feature flag overriding mechanism.
///
/// All flag overrides APIs only have effect if flag has `supportsLocalOverriding` set to `true`.
///
public protocol FeatureFlagLocalOverriding: AnyObject {

/// Handle to the feature flagger.
///
/// It's used to query current, non-overriden state of a feature flag to
/// decide about calling `FeatureFlagLocalOverridesHandling.flagDidChange`
/// upon clearing an override.
var featureFlagger: FeatureFlagger? { get set }

/// The action handler responding to feature flag changes.
var actionHandler: FeatureFlagLocalOverridesHandling { get }

/// Returns the current override for a feature flag, or `nil` if override is not set.
func override<Flag: FeatureFlagDescribing>(for featureFlag: Flag) -> Bool?

/// Toggles override for a feature flag.
///
/// If override is not currently present, it sets the override to the opposite of the current flag value.
///
func toggleOverride<Flag: FeatureFlagDescribing>(for featureFlag: Flag)

/// Clears override for a feature flag.
///
/// Calls `FeatureFlagLocalOverridesHandling.flagDidChange` if the effective flag value
/// changes as a result of clearing the override.
///
func clearOverride<Flag: FeatureFlagDescribing>(for featureFlag: Flag)

/// Clears overrides for all feature flags.
///
/// This function calls `clearOverride(for:)` for each flag.
///
func clearAllOverrides<Flag: FeatureFlagDescribing>(for flagType: Flag.Type)
}

public final class FeatureFlagLocalOverrides: FeatureFlagLocalOverriding {

public let actionHandler: FeatureFlagLocalOverridesHandling
public weak var featureFlagger: FeatureFlagger?
private let persistor: FeatureFlagLocalOverridesPersisting

public convenience init(
keyValueStore: KeyValueStoring,
actionHandler: FeatureFlagLocalOverridesHandling
) {
self.init(
persistor: FeatureFlagLocalOverridesUserDefaultsPersistor(keyValueStore: keyValueStore),
actionHandler: actionHandler
)
}

public init(
persistor: FeatureFlagLocalOverridesPersisting,
actionHandler: FeatureFlagLocalOverridesHandling
) {
self.persistor = persistor
self.actionHandler = actionHandler
}

public func override<Flag: FeatureFlagDescribing>(for featureFlag: Flag) -> Bool? {
guard featureFlag.supportsLocalOverriding else {
return nil
}
return persistor.value(for: featureFlag)
}

public func toggleOverride<Flag: FeatureFlagDescribing>(for featureFlag: Flag) {
guard featureFlag.supportsLocalOverriding else {
return
}
let currentValue = persistor.value(for: featureFlag) ?? currentValue(for: featureFlag) ?? false
let newValue = !currentValue
persistor.set(newValue, for: featureFlag)
actionHandler.flagDidChange(featureFlag, isEnabled: newValue)
}

public func clearOverride<Flag: FeatureFlagDescribing>(for featureFlag: Flag) {
guard let override = override(for: featureFlag) else {
return
}
persistor.set(nil, for: featureFlag)
if let defaultValue = currentValue(for: featureFlag), defaultValue != override {
actionHandler.flagDidChange(featureFlag, isEnabled: defaultValue)
}
}

public func clearAllOverrides<Flag: FeatureFlagDescribing>(for flagType: Flag.Type) {
flagType.allCases.forEach { flag in
clearOverride(for: flag)
}
}

private func currentValue<Flag: FeatureFlagDescribing>(for featureFlag: Flag) -> Bool? {
featureFlagger?.isFeatureOn(for: featureFlag, allowOverride: true)
}
}
Loading

0 comments on commit 607342d

Please sign in to comment.