[2026-04-28] Add apple-browsers .cursor rules (43 files) + executor v2 context
This commit is contained in:
@@ -0,0 +1,378 @@
|
||||
---
|
||||
source: ~/DuckDuckGo/apple-browsers.git/main/.cursor/rules/feature-flags.mdc
|
||||
confidence: 0.9
|
||||
namespace: work
|
||||
last_synced: 2026-04-28
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Feature Flag Patterns
|
||||
|
||||
## Type-Safe Feature Flags
|
||||
Use enum-based feature flags with protocols for type safety:
|
||||
|
||||
```swift
|
||||
// ✅ CORRECT - Type-safe feature flags
|
||||
protocol FeatureFlag: RawRepresentable where RawValue == String {
|
||||
var defaultValue: Bool { get }
|
||||
var description: String { get }
|
||||
}
|
||||
|
||||
enum UIFeatureFlag: String, FeatureFlag {
|
||||
case newTabPageRedesign = "new_tab_page_redesign"
|
||||
case advancedPrivacySettings = "advanced_privacy_settings"
|
||||
case voiceSearch = "voice_search"
|
||||
case experimentalUI = "experimental_ui"
|
||||
|
||||
var defaultValue: Bool {
|
||||
switch self {
|
||||
case .newTabPageRedesign: return false
|
||||
case .advancedPrivacySettings: return true
|
||||
case .voiceSearch: return false
|
||||
case .experimentalUI: return false
|
||||
}
|
||||
}
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .newTabPageRedesign:
|
||||
return "Enable redesigned new tab page"
|
||||
case .advancedPrivacySettings:
|
||||
return "Show advanced privacy settings"
|
||||
case .voiceSearch:
|
||||
return "Enable voice search functionality"
|
||||
case .experimentalUI:
|
||||
return "Enable experimental UI components"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum NetworkFeatureFlag: String, FeatureFlag {
|
||||
case networkProtectionV2 = "network_protection_v2"
|
||||
case enhancedBlocking = "enhanced_blocking"
|
||||
|
||||
var defaultValue: Bool {
|
||||
switch self {
|
||||
case .networkProtectionV2: return false
|
||||
case .enhancedBlocking: return true
|
||||
}
|
||||
}
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .networkProtectionV2:
|
||||
return "Enable Network Protection V2"
|
||||
case .enhancedBlocking:
|
||||
return "Enhanced content blocking"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## FeatureFlagger Protocol Implementation
|
||||
Extend the existing FeatureFlagger with type-safe methods:
|
||||
|
||||
```swift
|
||||
// ✅ CORRECT - Type-safe FeatureFlagger extension
|
||||
extension FeatureFlagger {
|
||||
func isEnabled<Flag: FeatureFlag>(_ flag: Flag) -> Bool {
|
||||
return isFeatureOn(flag.rawValue) ?? flag.defaultValue
|
||||
}
|
||||
|
||||
func setEnabled<Flag: FeatureFlag>(_ enabled: Bool, for flag: Flag) {
|
||||
setFeatureOn(flag.rawValue, enabled: enabled)
|
||||
}
|
||||
}
|
||||
|
||||
// Usage in code
|
||||
final class FeatureViewModel: ObservableObject {
|
||||
private let featureFlagger: FeatureFlagger
|
||||
|
||||
init(featureFlagger: FeatureFlagger) {
|
||||
self.featureFlagger = featureFlagger
|
||||
}
|
||||
|
||||
func loadContent() {
|
||||
if featureFlagger.isEnabled(UIFeatureFlag.newTabPageRedesign) {
|
||||
loadNewDesign()
|
||||
} else {
|
||||
loadLegacyDesign()
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Feature Flag ViewModifier
|
||||
Create SwiftUI modifiers for conditional UI:
|
||||
|
||||
```swift
|
||||
// ✅ ADVANCED - SwiftUI feature flag modifier
|
||||
struct FeatureFlagModifier<Flag: FeatureFlag>: ViewModifier {
|
||||
let flag: Flag
|
||||
let featureFlagger: FeatureFlagger
|
||||
let fallback: () -> AnyView
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
if featureFlagger.isEnabled(flag) {
|
||||
content
|
||||
} else {
|
||||
fallback()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
func featureFlag<Flag: FeatureFlag>(
|
||||
_ flag: Flag,
|
||||
featureFlagger: FeatureFlagger,
|
||||
@ViewBuilder fallback: @escaping () -> some View = { EmptyView() }
|
||||
) -> some View {
|
||||
modifier(FeatureFlagModifier(
|
||||
flag: flag,
|
||||
featureFlagger: featureFlagger,
|
||||
fallback: { AnyView(fallback()) }
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
struct ContentView: View {
|
||||
@Environment(\.dependencies) var dependencies
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
NewFeatureView()
|
||||
.featureFlag(UIFeatureFlag.experimentalUI,
|
||||
featureFlagger: dependencies.featureFlagger) {
|
||||
LegacyFeatureView()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Feature Flag Property Wrapper
|
||||
Create a property wrapper for reactive feature flags:
|
||||
|
||||
```swift
|
||||
// ✅ ADVANCED - Reactive feature flag property wrapper
|
||||
@propertyWrapper
|
||||
struct FeatureFlagState<Flag: FeatureFlag>: DynamicProperty {
|
||||
@ObservedObject private var flagger: ObservableFeatureFlagger
|
||||
private let flag: Flag
|
||||
|
||||
var wrappedValue: Bool {
|
||||
get { flagger.isEnabled(flag) }
|
||||
nonmutating set { flagger.setEnabled(newValue, for: flag) }
|
||||
}
|
||||
|
||||
var projectedValue: Binding<Bool> {
|
||||
Binding(
|
||||
get: { wrappedValue },
|
||||
set: { wrappedValue = $0 }
|
||||
)
|
||||
}
|
||||
|
||||
init(_ flag: Flag) {
|
||||
self.flag = flag
|
||||
self._flagger = ObservedObject(wrappedValue: ObservableFeatureFlagger.shared)
|
||||
}
|
||||
}
|
||||
|
||||
// Usage in SwiftUI views
|
||||
struct SettingsView: View {
|
||||
@FeatureFlagState(UIFeatureFlag.voiceSearch) var voiceSearchEnabled
|
||||
@FeatureFlagState(UIFeatureFlag.experimentalUI) var experimentalUIEnabled
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
Toggle("Voice Search", isOn: $voiceSearchEnabled)
|
||||
Toggle("Experimental UI", isOn: $experimentalUIEnabled)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## A/B Testing Integration
|
||||
Integrate feature flags with A/B testing:
|
||||
|
||||
```swift
|
||||
// ✅ ADVANCED - A/B testing with feature flags
|
||||
enum ABTestVariant: String, CaseIterable {
|
||||
case control = "control"
|
||||
case variantA = "variant_a"
|
||||
case variantB = "variant_b"
|
||||
|
||||
var displayName: String {
|
||||
switch self {
|
||||
case .control: return "Control Group"
|
||||
case .variantA: return "Variant A"
|
||||
case .variantB: return "Variant B"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protocol ABTestFeatureFlag: FeatureFlag {
|
||||
var variants: [ABTestVariant] { get }
|
||||
var currentVariant: ABTestVariant { get }
|
||||
}
|
||||
|
||||
enum ExperimentalFeatureFlag: String, ABTestFeatureFlag {
|
||||
case newOnboardingFlow = "new_onboarding_flow"
|
||||
case redesignedSearch = "redesigned_search"
|
||||
|
||||
var defaultValue: Bool { true }
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .newOnboardingFlow: return "New onboarding flow experiment"
|
||||
case .redesignedSearch: return "Redesigned search interface experiment"
|
||||
}
|
||||
}
|
||||
|
||||
var variants: [ABTestVariant] {
|
||||
[.control, .variantA, .variantB]
|
||||
}
|
||||
|
||||
var currentVariant: ABTestVariant {
|
||||
// Get variant from A/B testing service
|
||||
return ABTestingService.shared.getVariant(for: self.rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
// Usage with variants
|
||||
func configureOnboarding() {
|
||||
let experiment = ExperimentalFeatureFlag.newOnboardingFlow
|
||||
|
||||
switch experiment.currentVariant {
|
||||
case .control:
|
||||
showLegacyOnboarding()
|
||||
case .variantA:
|
||||
showNewOnboardingVariantA()
|
||||
case .variantB:
|
||||
showNewOnboardingVariantB()
|
||||
}
|
||||
|
||||
// Track experiment exposure
|
||||
PixelFiring.fire(.experimentExposure(experiment.rawValue, experiment.currentVariant.rawValue))
|
||||
}
|
||||
```
|
||||
|
||||
## Remote Feature Flags
|
||||
Integrate with remote configuration:
|
||||
|
||||
```swift
|
||||
// ✅ ADVANCED - Remote feature flag management
|
||||
protocol RemoteFeatureFlag: FeatureFlag {
|
||||
var remoteKey: String { get }
|
||||
var localOverrideKey: String? { get }
|
||||
}
|
||||
|
||||
extension RemoteFeatureFlag {
|
||||
var remoteKey: String { rawValue }
|
||||
var localOverrideKey: String? { "local_override_\(rawValue)" }
|
||||
}
|
||||
|
||||
enum RemoteUIFeatureFlag: String, RemoteFeatureFlag {
|
||||
case serverDrivenUI = "server_driven_ui"
|
||||
case dynamicThemes = "dynamic_themes"
|
||||
|
||||
var defaultValue: Bool { false }
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .serverDrivenUI: return "Server-driven UI configuration"
|
||||
case .dynamicThemes: return "Dynamic theme system"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class RemoteFeatureFlagger: FeatureFlagger {
|
||||
private let remoteConfig: RemoteConfigProtocol
|
||||
private let localDefaults: UserDefaults
|
||||
|
||||
init(remoteConfig: RemoteConfigProtocol, localDefaults: UserDefaults) {
|
||||
self.remoteConfig = remoteConfig
|
||||
self.localDefaults = localDefaults
|
||||
}
|
||||
|
||||
func isEnabled<Flag: RemoteFeatureFlag>(_ flag: Flag) -> Bool {
|
||||
// Check local override first
|
||||
if let overrideKey = flag.localOverrideKey,
|
||||
let localOverride = localDefaults.object(forKey: overrideKey) as? Bool {
|
||||
return localOverride
|
||||
}
|
||||
|
||||
// Check remote config
|
||||
if let remoteValue = remoteConfig.boolValue(for: flag.remoteKey) {
|
||||
return remoteValue
|
||||
}
|
||||
|
||||
// Fall back to default
|
||||
return flag.defaultValue
|
||||
}
|
||||
|
||||
func setLocalOverride<Flag: RemoteFeatureFlag>(_ enabled: Bool?, for flag: Flag) {
|
||||
guard let overrideKey = flag.localOverrideKey else { return }
|
||||
|
||||
if let enabled = enabled {
|
||||
localDefaults.set(enabled, forKey: overrideKey)
|
||||
} else {
|
||||
localDefaults.removeObject(forKey: overrideKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Debug Feature Flag Interface
|
||||
Create debug interface for testing:
|
||||
|
||||
```swift
|
||||
// ✅ DEBUG - Feature flag debug interface
|
||||
#if DEBUG
|
||||
struct FeatureFlagDebugView: View {
|
||||
@StateObject private var debugFlags = DebugFeatureFlags()
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
List {
|
||||
Section("UI Features") {
|
||||
ForEach(UIFeatureFlag.allCases, id: \.rawValue) { flag in
|
||||
FeatureFlagRow(flag: flag, debugFlags: debugFlags)
|
||||
}
|
||||
}
|
||||
|
||||
Section("Network Features") {
|
||||
ForEach(NetworkFeatureFlag.allCases, id: \.rawValue) { flag in
|
||||
FeatureFlagRow(flag: flag, debugFlags: debugFlags)
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Feature Flags")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct FeatureFlagRow<Flag: FeatureFlag>: View where Flag: CaseIterable {
|
||||
let flag: Flag
|
||||
@ObservedObject var debugFlags: DebugFeatureFlags
|
||||
|
||||
var body: some View {
|
||||
Toggle(isOn: Binding(
|
||||
get: { debugFlags.isEnabled(flag) },
|
||||
set: { debugFlags.setEnabled($0, for: flag) }
|
||||
)) {
|
||||
VStack(alignment: .leading) {
|
||||
Text(flag.rawValue)
|
||||
.font(.headline)
|
||||
Text(flag.description)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
```
|
||||
|
||||
See `analytics-patterns.md` for pixel firing patterns and `configuration-management.md` for advanced configuration management.
|
||||
Reference in New Issue
Block a user