13 KiB
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
-
Clear Visual Hierarchy
- Current status prominently displayed
- Clear separation between unlocked/locked
- Visual indicators (keys, locks, checkmarks)
-
Progressive Disclosure
- Show relevant information based on license level
- Hide complex features until needed
- Clear upgrade paths
-
Feedback and Validation
- Real-time key formatting
- Clear error messages
- Success animations
- Haptic feedback
-
Accessibility
- VoiceOver labels
- Dynamic type support
- Color-blind friendly indicators
- Clear contrast ratios