416 lines
No EOL
15 KiB
Swift
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() }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} |