Files

146 lines
6.4 KiB
Swift
Executable File

import SwiftUI
import AppKit
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()
}
}
Button("Buy Rsync Studio…") { openPurchasePage() }
Text("Buy Personal or Professional online, then enter the licence key delivered to your email. Signed offline licence tokens can also be installed without contacting the activation 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 }
}
private func openPurchasePage() {
guard let raw = Bundle.main.object(forInfoDictionaryKey: "RsyncStudioPurchaseURL") as? String,
let url = URL(string: raw),
url.scheme?.lowercased() == "https" else {
errorMessage = "The Rsync Studio purchase page is not configured in this build."
return
}
NSWorkspace.shared.open(url)
}
}