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

424 lines
No EOL
13 KiB
Markdown

# 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
```swift
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
```swift
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
```swift
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
```swift
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
```swift
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
```swift
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
```swift
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