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

13 KiB

KeyUI Components

The KeyUI components provide the user interface for the Keys for All system, including the main panel, activation views, and visual indicators.

Main Components

KeysForAllPanel

struct KeysForAllPanel: View {
    @StateObject private var keyManager = KeyManager.shared
    @State private var showActivation = false
    @State private var showPurchase = false
    @State private var selectedTab = 0
    
    var body: some View {
        VStack(spacing: 0) {
            // Header
            KeysHeaderView(currentLevel: keyManager.currentLicenseLevel)
            
            // Tab Selection
            Picker("View", selection: $selectedTab) {
                Text("Status").tag(0)
                Text("Purchase").tag(1)
                Text("Share").tag(2)
            }
            .pickerStyle(SegmentedPickerStyle())
            .padding()
            
            // Content
            ScrollView {
                switch selectedTab {
                case 0:
                    LicenseStatusView()
                case 1:
                    PurchaseOptionsView()
                case 2:
                    KeySharingView()
                default:
                    EmptyView()
                }
            }
        }
        .navigationTitle("Keys for All")
        .toolbar {
            ToolbarItem(placement: .navigationBarTrailing) {
                Button("Have a Key?") {
                    showActivation = true
                }
            }
        }
        .sheet(isPresented: $showActivation) {
            KeyActivationView()
        }
    }
}

KeysHeaderView

struct KeysHeaderView: View {
    let currentLevel: LicenseLevel
    
    var body: some View {
        HStack {
            VStack(alignment: .leading, spacing: 4) {
                Text("Current License")
                    .font(.caption)
                    .foregroundColor(.secondary)
                
                HStack {
                    Text(currentLevel.displayName)
                        .font(.title2)
                        .fontWeight(.semibold)
                    
                    ForEach(0..<currentLevel.rawValue, id: \.self) { _ in
                        Image(systemName: "key.fill")
                            .foregroundColor(.accentColor)
                    }
                }
            }
            
            Spacer()
            
            // Visual indicator
            ZStack {
                Circle()
                    .fill(currentLevel.color.opacity(0.2))
                    .frame(width: 60, height: 60)
                
                Image(systemName: currentLevel.icon)
                    .font(.title)
                    .foregroundColor(currentLevel.color)
            }
        }
        .padding()
        .background(Color(.secondarySystemBackground))
        .cornerRadius(12)
        .padding()
    }
}

LicenseStatusView

struct LicenseStatusView: View {
    @ObservedObject private var featureGating = FeatureGating.shared
    
    var body: some View {
        VStack(spacing: 16) {
            // Current features
            GroupBox {
                VStack(alignment: .leading, spacing: 12) {
                    Label("Your Features", systemImage: "checkmark.circle.fill")
                        .font(.headline)
                        .foregroundColor(.green)
                    
                    Divider()
                    
                    ForEach(unlockedFeatures, id: \.self) { feature in
                        HStack {
                            Image(systemName: "checkmark")
                                .foregroundColor(.green)
                            Text(feature.displayName)
                            Spacer()
                        }
                        .font(.subheadline)
                    }
                }
                .padding(.vertical, 8)
            }
            
            // Locked features
            if !lockedFeatures.isEmpty {
                GroupBox {
                    VStack(alignment: .leading, spacing: 12) {
                        Label("Available Upgrades", systemImage: "lock.fill")
                            .font(.headline)
                            .foregroundColor(.orange)
                        
                        Divider()
                        
                        ForEach(lockedFeatures, id: \.self) { feature in
                            LockedFeatureRow(feature: feature)
                        }
                    }
                    .padding(.vertical, 8)
                }
            }
        }
        .padding()
    }
    
    private var unlockedFeatures: [FeatureGating.Feature] {
        FeatureGating.Feature.allCases.filter { 
            featureGating.isAvailable($0) 
        }
    }
    
    private var lockedFeatures: [FeatureGating.Feature] {
        FeatureGating.Feature.allCases.filter { 
            !featureGating.isAvailable($0) 
        }
    }
}

KeyActivationView

struct KeyActivationView: View {
    @Environment(\.dismiss) private var dismiss
    @StateObject private var keyManager = KeyManager.shared
    @State private var keyInput = ""
    @State private var isValidating = false
    @State private var error: KeyError?
    @State private var showSuccess = false
    
    var body: some View {
        NavigationView {
            VStack(spacing: 24) {
                // Instructions
                VStack(spacing: 8) {
                    Image(systemName: "key.fill")
                        .font(.system(size: 48))
                        .foregroundColor(.accentColor)
                    
                    Text("Enter Your License Key")
                        .font(.title2)
                        .fontWeight(.semibold)
                    
                    Text("Keys look like: VUUW-XXXX-XXXX-XXXX-L1")
                        .font(.caption)
                        .foregroundColor(.secondary)
                }
                .padding(.top)
                
                // Key input field
                VStack(alignment: .leading, spacing: 8) {
                    TextField("License Key", text: $keyInput)
                        .textFieldStyle(RoundedBorderTextFieldStyle())
                        .autocapitalization(.allCharacters)
                        .disableAutocorrection(true)
                        .onChange(of: keyInput) { newValue in
                            // Auto-format with dashes
                            keyInput = formatKey(newValue)
                        }
                    
                    if let error = error {
                        Label(error.localizedDescription, systemImage: "exclamationmark.circle.fill")
                            .font(.caption)
                            .foregroundColor(.red)
                    }
                }
                .padding(.horizontal)
                
                // Activation button
                Button(action: activateKey) {
                    if isValidating {
                        ProgressView()
                            .progressViewStyle(CircularProgressViewStyle())
                            .scaleEffect(0.8)
                    } else {
                        Text("Activate")
                    }
                }
                .buttonStyle(PrimaryButtonStyle())
                .disabled(keyInput.count < 19 || isValidating)
                .padding(.horizontal)
                
                Spacer()
                
                // Help text
                VStack(spacing: 4) {
                    Text("Don't have a key?")
                        .font(.caption)
                    
                    Button("Get one in the Purchase tab") {
                        dismiss()
                    }
                    .font(.caption)
                }
                .padding(.bottom)
            }
            .navigationTitle("Activate Key")
            .navigationBarTitleDisplayMode(.inline)
            .toolbar {
                ToolbarItem(placement: .navigationBarLeading) {
                    Button("Cancel") {
                        dismiss()
                    }
                }
            }
        }
        .alert("Success!", isPresented: $showSuccess) {
            Button("OK") {
                dismiss()
            }
        } message: {
            Text("Your license has been activated successfully!")
        }
    }
    
    private func activateKey() {
        isValidating = true
        error = nil
        
        Task {
            let result = await keyManager.activate(key: keyInput)
            
            await MainActor.run {
                isValidating = false
                
                switch result {
                case .success:
                    showSuccess = true
                    HapticFeedback.success()
                    
                case .failure(let keyError):
                    error = keyError
                    HapticFeedback.error()
                }
            }
        }
    }
    
    private func formatKey(_ input: String) -> String {
        // Remove all non-alphanumeric characters
        let cleaned = input.uppercased().filter { $0.isLetter || $0.isNumber }
        
        // Add dashes at appropriate positions
        var formatted = ""
        for (index, char) in cleaned.enumerated() {
            if index > 0 && index % 4 == 0 && index < 16 {
                formatted += "-"
            }
            formatted.append(char)
        }
        
        // Limit length
        return String(formatted.prefix(19))
    }
}

KeyBadge

struct KeyBadge: View {
    let keysRequired: Int
    let isCompact: Bool
    
    init(keysRequired: Int, compact: Bool = false) {
        self.keysRequired = keysRequired
        self.isCompact = compact
    }
    
    var body: some View {
        if keysRequired > 0 {
            HStack(spacing: isCompact ? 2 : 4) {
                ForEach(0..<keysRequired, id: \.self) { _ in
                    Image(systemName: "key.fill")
                        .font(isCompact ? .caption2 : .caption)
                        .foregroundColor(.orange)
                }
                
                if !isCompact {
                    Text("Required")
                        .font(.caption2)
                        .foregroundColor(.secondary)
                }
            }
            .padding(.horizontal, isCompact ? 4 : 8)
            .padding(.vertical, isCompact ? 2 : 4)
            .background(Color.orange.opacity(0.1))
            .cornerRadius(isCompact ? 4 : 6)
        }
    }
}

LockedFeatureRow

struct LockedFeatureRow: View {
    let feature: FeatureGating.Feature
    @ObservedObject private var featureGating = FeatureGating.shared
    @State private var showDemo = false
    
    var body: some View {
        HStack {
            VStack(alignment: .leading, spacing: 4) {
                Text(feature.displayName)
                    .font(.subheadline)
                
                KeyBadge(keysRequired: featureGating.keysNeeded(for: feature))
            }
            
            Spacer()
            
            if featureGating.canDemo(feature) {
                Button("Demo") {
                    showDemo = true
                }
                .font(.caption)
                .buttonStyle(.bordered)
            }
        }
        .alert("Try \(feature.displayName)", isPresented: $showDemo) {
            Button("Start 30s Demo") {
                featureGating.temporarilyUnlock(feature, for: 30)
            }
            Button("Cancel", role: .cancel) {}
        } message: {
            Text("Experience this feature free for 30 seconds")
        }
    }
}

Custom Button Styles

struct PrimaryButtonStyle: ButtonStyle {
    @Environment(\.isEnabled) private var isEnabled
    
    func makeBody(configuration: Configuration) -> some View {
        configuration.label
            .frame(maxWidth: .infinity)
            .padding()
            .background(isEnabled ? Color.accentColor : Color.gray)
            .foregroundColor(.white)
            .cornerRadius(10)
            .scaleEffect(configuration.isPressed ? 0.95 : 1)
            .animation(.easeInOut(duration: 0.1), value: configuration.isPressed)
    }
}

Design Principles

  1. Clear Visual Hierarchy

    • Current status prominently displayed
    • Clear separation between unlocked/locked
    • Visual indicators (keys, locks, checkmarks)
  2. Progressive Disclosure

    • Show relevant information based on license level
    • Hide complex features until needed
    • Clear upgrade paths
  3. Feedback and Validation

    • Real-time key formatting
    • Clear error messages
    • Success animations
    • Haptic feedback
  4. Accessibility

    • VoiceOver labels
    • Dynamic type support
    • Color-blind friendly indicators
    • Clear contrast ratios