keys-for-all/docs/components/FeatureGating.md
2025-07-22 18:27:21 -07:00

10 KiB

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

class FeatureGating: ObservableObject {
    // Current state
    @Published private(set) var availableFeatures: Set<Feature> = []
    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