Files
2026-09-09 08:23:43 +01:00

134 lines
5.9 KiB
Swift

import SwiftUI
struct LicenseView: View {
@EnvironmentObject private var licensing: LicensingManager
@State private var licenceKey = ""
@State private var signedToken = ""
@State private var errorMessage: String?
@State private var showingTokenEntry = false
var body: some View {
Form {
Section("Licence Status") {
HStack(spacing: 12) {
Image(systemName: statusIcon)
.font(.title2)
.foregroundStyle(licensing.canUseApp ? Color.green : Color.orange)
VStack(alignment: .leading, spacing: 3) {
Text(licensing.statusTitle).font(.headline)
Text(licensing.statusDetail).font(.caption).foregroundStyle(.secondary)
}
Spacer()
}
LabeledContent("Installation ID", value: licensing.installationID)
.textSelection(.enabled)
Text("The installation ID is a random UUID stored in macOS Keychain. Rsync Studio does not use your Mac serial number or other hardware identifiers.")
.font(.caption)
.foregroundStyle(.secondary)
}
if case .licensed(let payload) = licensing.state {
Section("Current Licence") {
LabeledContent("Edition", value: payload.edition)
LabeledContent("Licence ID", value: payload.licenseID)
LabeledContent("Licensed to", value: payload.displayOwner)
if let expiresAt = payload.expiresAt {
LabeledContent("Expires", value: expiresAt.formatted(date: .long, time: .omitted))
} else {
LabeledContent("Term", value: "Perpetual")
}
Button("Deactivate This Mac…", role: .destructive) {
Task { await deactivate() }
}
.disabled(licensing.isWorking)
}
} else {
Section("Activate Rsync Studio") {
TextField("Licence key", text: $licenceKey, prompt: Text("XXXX-XXXX-XXXX-XXXX"))
.textContentType(.password)
HStack {
Button {
Task { await activate() }
} label: {
if licensing.isWorking { ProgressView().controlSize(.small) }
else { Label("Activate", systemImage: "checkmark.seal") }
}
.buttonStyle(.borderedProminent)
.disabled(licenceKey.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || licensing.isWorking)
Button(showingTokenEntry ? "Hide Signed Token" : "Install Signed Token…") {
showingTokenEntry.toggle()
}
}
Text("Online activation will be enabled when the licensing service URL is configured. Signed offline licence tokens can be installed without contacting a server.")
.font(.caption)
.foregroundStyle(.secondary)
}
if showingTokenEntry {
Section("Signed Offline Licence") {
TextEditor(text: $signedToken)
.font(.system(.caption, design: .monospaced))
.frame(minHeight: 86)
Button("Install Signed Licence") { installToken() }
.disabled(signedToken.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
}
}
if case .invalid = licensing.state {
Section("Recovery") {
Button("Remove Invalid Local Licence") { licensing.removeInvalidLocalLicence() }
Text("This removes only the stored local licence token. Trial dates and the installation identity are not reset.")
.font(.caption)
.foregroundStyle(.secondary)
}
}
}
Section("Privacy") {
Text("Licence activation sends only the licence key, random installation ID, app version and platform to the licensing service. Sync paths, filenames, SSH hosts, credentials, job history and file contents are never included.")
.font(.caption)
.foregroundStyle(.secondary)
}
}
.formStyle(.grouped)
.alert("Rsync Studio Licence", isPresented: Binding(
get: { errorMessage != nil || licensing.lastMessage != nil },
set: { if !$0 { errorMessage = nil; licensing.lastMessage = nil } }
)) {
Button("OK", role: .cancel) { errorMessage = nil; licensing.lastMessage = nil }
} message: {
Text(errorMessage ?? licensing.lastMessage ?? "")
}
}
private var statusIcon: String {
switch licensing.state {
case .licensed: "checkmark.seal.fill"
case .trial: "clock.badge.checkmark"
case .expired: "clock.badge.exclamationmark"
case .invalid: "exclamationmark.shield.fill"
}
}
private func activate() async {
do {
try await licensing.activate(licenseKey: licenceKey)
licenceKey = ""
} catch { errorMessage = error.localizedDescription }
}
private func deactivate() async {
do { try await licensing.deactivate() }
catch { errorMessage = error.localizedDescription }
}
private func installToken() {
do {
try licensing.installSignedToken(signedToken)
signedToken = ""
showingTokenEntry = false
} catch { errorMessage = error.localizedDescription }
}
}