keys-for-all/docs/components/FeatureGating.md

308 lines
10 KiB
Markdown
Raw Permalink Normal View History

2025-07-22 18:27:21 -07:00
# 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<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