keys-ios-integration/Sources/KeysForAll/UI/KeysForAllView.swift
2025-07-22 01:42:57 -07:00

416 lines
No EOL
15 KiB
Swift

//
// KeysForAllView.swift
// KeysForAll
//
// Main UI for Keys for All licensing
//
import SwiftUI
/// Main view for Keys for All licensing
public struct KeysForAllView<Provider: FeatureProvider, PurchaseHandler: ObservableObject>: View {
@ObservedObject private var manager: KeysForAllManager<Provider>
private let colorProvider: ColorProvider
private let purchaseHandler: PurchaseHandler
@State private var showingShareSheet = false
@State private var showingDonateSheet = false
@State private var showingActivateAlert = false
@State private var licenseKey = ""
public init(
manager: KeysForAllManager<Provider>,
colorProvider: ColorProvider,
purchaseHandler: PurchaseHandler
) {
self.manager = manager
self.colorProvider = colorProvider
self.purchaseHandler = purchaseHandler
}
public var body: some View {
ScrollView {
VStack(spacing: 24) {
// License Status
licenseStatusSection
// Purchase Options
if manager.currentLevel != .level2 {
purchaseOptionsSection
}
// Unlocked Features
unlockedFeaturesSection
// Distribution (if user has extra keys)
if manager.keyCount > 1 {
distributionSection
}
}
.padding()
}
.navigationTitle("Keys for All")
.navigationBarTitleDisplayMode(.large)
.sheet(isPresented: $showingShareSheet) {
ShareKeySheet(keysToShare: manager.keyCount - 1, colorProvider: colorProvider)
}
.sheet(isPresented: $showingDonateSheet) {
DonateKeySheet(colorProvider: colorProvider)
}
.alert("Activate License Key", isPresented: $showingActivateAlert) {
TextField("Enter key", text: $licenseKey)
.textInputAutocapitalization(.characters)
Button("Cancel", role: .cancel) {
licenseKey = ""
}
Button("Activate") {
Task {
do {
if try await KeyValidator.validate(licenseKey) {
manager.addKeys(1)
}
} catch {
// In production, show error alert
print("Key validation error: \(error)")
}
licenseKey = ""
}
}
} message: {
Text("Enter your license key to unlock premium features")
}
}
// MARK: - Sections
private var licenseStatusSection: some View {
VStack(alignment: .leading, spacing: 16) {
Label("License Status", systemImage: "key.fill")
.font(.headline)
VStack(alignment: .leading, spacing: 12) {
HStack {
VStack(alignment: .leading, spacing: 4) {
Text("Current Status")
.font(.caption)
.foregroundStyle(.secondary)
Text(manager.currentLevel.displayName)
.font(.headline)
.foregroundColor(manager.currentLevel == .free ? .secondary : colorProvider.primaryColor)
}
Spacer()
if manager.keyCount > 0 {
VStack(alignment: .trailing, spacing: 4) {
Text("Keys Owned")
.font(.caption)
.foregroundStyle(.secondary)
Text("\(manager.keyCount)")
.font(.title2.weight(.bold))
.foregroundStyle(colorProvider.primaryColor)
}
}
}
.padding()
.background(Color(UIColor.secondarySystemGroupedBackground))
.cornerRadius(12)
// Activate key button
Button {
showingActivateAlert = true
} label: {
HStack {
Image(systemName: "key.horizontal.fill")
Text("Activate Key")
Spacer()
Image(systemName: "chevron.right")
.foregroundStyle(.secondary)
}
}
.buttonStyle(.plain)
.padding()
.background(Color(UIColor.secondarySystemGroupedBackground))
.cornerRadius(12)
}
}
}
private var purchaseOptionsSection: some View {
VStack(alignment: .leading, spacing: 16) {
Label("Purchase Options", systemImage: "cart.fill")
.font(.headline)
VStack(spacing: 12) {
ForEach(PurchaseOption.all) { option in
PurchaseOptionRow(
option: option,
colorProvider: colorProvider,
action: {
// TODO: Handle purchase through purchaseHandler
print("Purchase: \(option.title)")
}
)
}
}
}
}
private var unlockedFeaturesSection: some View {
VStack(alignment: .leading, spacing: 16) {
Label("Features", systemImage: "star.fill")
.font(.headline)
// Group features by category
ForEach(FeatureCategory.allCases, id: \.self) { category in
let features = manager.features(in: category)
if !features.isEmpty {
VStack(alignment: .leading, spacing: 8) {
HStack {
Image(systemName: category.iconName)
.foregroundColor(colorProvider.primaryColor)
Text(category.rawValue)
.font(.subheadline.weight(.semibold))
}
VStack(spacing: 0) {
ForEach(Array(features), id: \.self) { feature in
FeatureRow(
feature: feature,
isUnlocked: manager.isFeatureAvailable(feature),
featureProvider: manager.featureProvider,
colorProvider: colorProvider
)
if feature != features.last {
Divider()
.padding(.leading, 44)
}
}
}
.background(Color(UIColor.secondarySystemGroupedBackground))
.cornerRadius(12)
}
}
}
}
}
private var distributionSection: some View {
VStack(alignment: .leading, spacing: 16) {
Label("Distribution", systemImage: "square.and.arrow.up.fill")
.font(.headline)
HStack {
Label("Keys Remaining", systemImage: "key.fill")
Spacer()
Text("\(manager.keyCount - 1)")
.font(.body.weight(.medium))
}
.padding()
.background(Color(UIColor.secondarySystemGroupedBackground))
.cornerRadius(12)
HStack(spacing: 12) {
Button {
showingShareSheet = true
} label: {
Label("Share Key", systemImage: "square.and.arrow.up")
.frame(maxWidth: .infinity)
}
.buttonStyle(.bordered)
Button {
showingDonateSheet = true
} label: {
Label("Donate Key", systemImage: "gift.fill")
.frame(maxWidth: .infinity)
}
.buttonStyle(.bordered)
.tint(colorProvider.primaryColor)
}
}
}
}
// MARK: - Supporting Views
struct PurchaseOptionRow: View {
let option: PurchaseOption
let colorProvider: ColorProvider
let action: () -> Void
var body: some View {
Button(action: action) {
HStack {
Image(systemName: "person.fill")
.font(.title2)
.foregroundStyle(colorProvider.primaryColor)
.frame(width: 40)
VStack(alignment: .leading, spacing: 2) {
HStack(spacing: 8) {
Text(option.title)
.font(.body.weight(.medium))
.foregroundStyle(.primary)
if let savings = option.savings {
Text(savings)
.font(.caption2)
.foregroundStyle(.white)
.padding(.horizontal, 6)
.padding(.vertical, 2)
.background(colorProvider.successColor)
.cornerRadius(4)
}
}
Text(option.subtitle)
.font(.caption)
.foregroundStyle(.secondary)
}
Spacer()
Text("$\(option.price)")
.font(.body.weight(.semibold))
.foregroundStyle(colorProvider.primaryColor)
}
.padding()
.background(Color(UIColor.secondarySystemGroupedBackground))
.cornerRadius(12)
}
.buttonStyle(.plain)
}
}
struct FeatureRow<Provider: FeatureProvider>: View {
let feature: Provider.Feature
let isUnlocked: Bool
let featureProvider: Provider
let colorProvider: ColorProvider
var body: some View {
HStack {
Image(systemName: isUnlocked ? "checkmark.circle.fill" : "lock.circle.fill")
.foregroundStyle(isUnlocked ? colorProvider.successColor : .secondary)
.font(.title3)
VStack(alignment: .leading, spacing: 2) {
Text(featureProvider.displayName(for: feature))
.font(.footnote.weight(.medium))
.foregroundStyle(isUnlocked ? .primary : .secondary)
if let description = featureProvider.description(for: feature) {
Text(description)
.font(.caption2)
.foregroundStyle(.secondary)
}
}
Spacer()
if !isUnlocked {
Text(featureProvider.requiredLevel(for: feature).shortName)
.font(.caption2)
.foregroundStyle(.secondary)
.padding(.horizontal, 8)
.padding(.vertical, 2)
.background(Color(UIColor.tertiarySystemGroupedBackground))
.cornerRadius(4)
}
}
.padding(.vertical, 8)
.padding(.horizontal, 12)
}
}
// MARK: - Sheet Views
struct ShareKeySheet: View {
@Environment(\.dismiss) private var dismiss
let keysToShare: Int
let colorProvider: ColorProvider
@State private var generatedKey = KeyValidator.generateKey()
var body: some View {
NavigationView {
VStack(spacing: 20) {
Text("Share License Key")
.font(.title2.weight(.bold))
Text("Generated key for sharing:")
.font(.subheadline)
.foregroundStyle(.secondary)
Text(generatedKey)
.font(.system(.title3, design: .monospaced))
.padding()
.background(Color.secondary.opacity(0.1))
.cornerRadius(8)
ShareLink(item: "License Key: \(generatedKey)") {
Label("Share Key", systemImage: "square.and.arrow.up")
}
.buttonStyle(.borderedProminent)
.tint(colorProvider.primaryColor)
Text("Keys remaining: \(keysToShare)")
.font(.caption)
.foregroundStyle(.secondary)
Spacer()
}
.padding()
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Done") { dismiss() }
}
}
}
}
}
struct DonateKeySheet: View {
@Environment(\.dismiss) private var dismiss
let colorProvider: ColorProvider
@State private var numberOfKeys = 1
@State private var message = ""
var body: some View {
NavigationView {
Form {
Section {
Stepper("Keys to donate: \(numberOfKeys)", value: $numberOfKeys, in: 1...10)
TextField("Message (optional)", text: $message, axis: .vertical)
.lineLimit(3...5)
} header: {
Text("Donation Details")
}
Section {
Button {
// TODO: Process donation
dismiss()
} label: {
HStack {
Spacer()
Label("Donate Keys", systemImage: "gift.fill")
.foregroundStyle(.white)
Spacer()
}
}
.listRowBackground(colorProvider.primaryColor)
}
}
.navigationTitle("Donate Keys")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Cancel") { dismiss() }
}
}
}
}
}