# FeatureGating Component The FeatureGating component determines which features are available based on the user's license level and manages temporary feature unlocks for demo purposes. ## Pseudocode ```swift class FeatureGating: ObservableObject { // Current state @Published private(set) var availableFeatures: Set = [] private var currentLevel: LicenseLevel = .free private var temporaryUnlocks: [Feature: Date] = [:] // Timer for demo expirations private var demoTimer: Timer? // MARK: - Feature Definitions enum Feature: String, CaseIterable { // Free features (always available) case basicVisualization = "basic_visualization" case singleMonitor = "single_monitor" case standardHaptics = "standard_haptics" // Level 1 features (1 key required) case monitorReordering = "monitor_reordering" case breathingMonitor = "breathing_monitor" case articulationMonitor = "articulation_monitor" case toneMonitor = "tone_monitor" case rhythmMonitor = "rhythm_monitor" case resonanceMonitor = "resonance_monitor" case voiceQualityMonitor = "voice_quality_monitor" case brightnessMonitor = "brightness_monitor" case vocalFryMonitor = "vocal_fry_monitor" case tremorMonitor = "tremor_monitor" case strainMonitor = "strain_monitor" case diplophoniaMonitor = "diplophonia_monitor" case hideMonetizationUI = "hide_monetization_ui" // Level 2 features (2 keys required) case circleTrailVisualization = "circle_trail_tvr_monitors" case multiMonitorView = "multi_monitor_view" case advancedCustomization = "advanced_customization" var requiredLevel: LicenseLevel { switch self { case .basicVisualization, .singleMonitor, .standardHaptics: return .free case .monitorReordering, .breathingMonitor, .articulationMonitor, .toneMonitor, .rhythmMonitor, .resonanceMonitor, .voiceQualityMonitor, .brightnessMonitor, .vocalFryMonitor, .tremorMonitor, .strainMonitor, .diplophoniaMonitor, .hideMonetizationUI: return .level1 case .circleTrailVisualization, .multiMonitorView, .advancedCustomization: return .level2 } } var displayName: String { switch self { case .basicVisualization: return "Basic Visualization" case .singleMonitor: return "Single Voice Monitor" case .standardHaptics: return "Standard Haptic Feedback" case .monitorReordering: return "Reorder Monitors" case .breathingMonitor: return "Breathing Monitor" case .articulationMonitor: return "Articulation Monitor" case .toneMonitor: return "Tone Monitor" case .rhythmMonitor: return "Rhythm Monitor" case .resonanceMonitor: return "Resonance Monitor" case .voiceQualityMonitor: return "Voice Quality Monitor" case .brightnessMonitor: return "Brightness Monitor" case .vocalFryMonitor: return "Vocal Fry Monitor" case .tremorMonitor: return "Tremor Monitor" case .strainMonitor: return "Strain Monitor" case .diplophoniaMonitor: return "Diplophonia Monitor" case .hideMonetizationUI: return "Hide Monetization UI" case .circleTrailVisualization: return "Circle Trail + Monitors" case .multiMonitorView: return "Multi-Monitor View" case .advancedCustomization: return "Advanced Customization" } } } // MARK: - License Level Updates func updateAvailableFeatures(for level: LicenseLevel) { currentLevel = level // Clear previous features availableFeatures.removeAll() // Add all features up to current level for feature in Feature.allCases { if feature.requiredLevel.rawValue <= level.rawValue { availableFeatures.insert(feature) } } // Re-apply any active demo unlocks applyTemporaryUnlocks() } // MARK: - Feature Availability Checks func isAvailable(_ feature: Feature) -> Bool { // Check if permanently unlocked if availableFeatures.contains(feature) { return true } // Check if temporarily unlocked if let expiryDate = temporaryUnlocks[feature] { return Date() < expiryDate } return false } func keysNeeded(for feature: Feature) -> Int { let currentKeys = currentLevel.rawValue let requiredKeys = feature.requiredLevel.rawValue return max(0, requiredKeys - currentKeys) } func canDemo(_ feature: Feature) -> Bool { // Can only demo features above current level return feature.requiredLevel.rawValue > currentLevel.rawValue } // MARK: - Demo Mode func temporarilyUnlock(_ feature: Feature, for duration: TimeInterval) { guard canDemo(feature) else { return } let expiryDate = Date().addingTimeInterval(duration) temporaryUnlocks[feature] = expiryDate // Add to available features temporarily availableFeatures.insert(feature) // Start or update timer startDemoTimer() // Notify observers objectWillChange.send() } private func startDemoTimer() { demoTimer?.invalidate() demoTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in self.checkDemoExpirations() } } private func checkDemoExpirations() { let now = Date() var hasExpired = false for (feature, expiryDate) in temporaryUnlocks { if now >= expiryDate { // Remove expired demo temporaryUnlocks.removeValue(forKey: feature) // Remove from available if not permanently unlocked if feature.requiredLevel.rawValue > currentLevel.rawValue { availableFeatures.remove(feature) } hasExpired = true } } if hasExpired { objectWillChange.send() } // Stop timer if no more demos if temporaryUnlocks.isEmpty { demoTimer?.invalidate() demoTimer = nil } } private func applyTemporaryUnlocks() { let now = Date() for (feature, expiryDate) in temporaryUnlocks { if now < expiryDate { availableFeatures.insert(feature) } } } // MARK: - UI Helpers func badgeText(for feature: Feature) -> String? { if isAvailable(feature) { return nil // No badge needed } let keysNeeded = keysNeeded(for: feature) return String(repeating: "🔑", count: keysNeeded) } func shouldShowMonetizationUI() -> Bool { // Hide if user has Level 1 or higher return !isAvailable(.hideMonetizationUI) } func getDemoTimeRemaining(for feature: Feature) -> TimeInterval? { guard let expiryDate = temporaryUnlocks[feature] else { return nil } let remaining = expiryDate.timeIntervalSince(Date()) return remaining > 0 ? remaining : nil } } // MARK: - SwiftUI View Modifier struct FeatureGated: ViewModifier { let feature: FeatureGating.Feature @ObservedObject var gating = FeatureGating.shared @State private var showUpgradePrompt = false func body(content: Content) -> some View { if gating.isAvailable(feature) { content } else { ZStack { content .blur(radius: 3) .disabled(true) VStack(spacing: 12) { Image(systemName: "lock.fill") .font(.largeTitle) .foregroundColor(.secondary) Text(gating.badgeText(for: feature) ?? "") .font(.title2) Text(feature.displayName) .font(.headline) Text("Requires \(feature.requiredLevel.displayName)") .font(.caption) .foregroundColor(.secondary) HStack(spacing: 16) { if gating.canDemo(feature) { Button("Try Demo") { gating.temporarilyUnlock(feature, for: 30) } .buttonStyle(.bordered) } Button("Unlock") { showUpgradePrompt = true } .buttonStyle(.borderedProminent) } } .padding() .background(.regularMaterial) .cornerRadius(12) } .sheet(isPresented: $showUpgradePrompt) { KeysForAllUpgradeView(targetFeature: feature) } } } } extension View { func gated(by feature: FeatureGating.Feature) -> some View { modifier(FeatureGated(feature: feature)) } } ``` ## Feature Management 1. **Feature Definition** - Centralized feature list - Clear level requirements - Display names for UI 2. **Availability Logic** - Permanent unlocks via license - Temporary unlocks for demos - Dynamic badge generation 3. **Demo System** - 30-second demos for locked features - Automatic expiration - Timer-based cleanup 4. **UI Integration** - SwiftUI view modifier - Automatic lock overlays - Upgrade prompts