new git ingnore

This commit is contained in:
Mark Gregory
2026-10-01 14:47:13 +01:00
parent 6313d784fe
commit f9c697619a
4 changed files with 655 additions and 32 deletions
+2 -2
View File
@@ -314,7 +314,7 @@
PRODUCT_BUNDLE_IDENTIFIER = net.absdev.SonarDesk;
PRODUCT_NAME = "$(TARGET_NAME)";
SONARDESK_ENVIRONMENT = Develop;
SONARDESK_LICENSE_BASE_URL = "https://sonardesk.vm";
SONARDESK_LICENSE_BASE_URL = "https://sonardesk.absdev.net";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_STRICT_CONCURRENCY = targeted;
SWIFT_VERSION = 5.0;
@@ -345,7 +345,7 @@
PRODUCT_BUNDLE_IDENTIFIER = net.absdev.SonarDesk;
PRODUCT_NAME = "$(TARGET_NAME)";
SONARDESK_ENVIRONMENT = Develop;
SONARDESK_LICENSE_BASE_URL = "https://sonardesk.vm";
SONARDESK_LICENSE_BASE_URL = "https://sonardesk.absdev.net";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_STRICT_CONCURRENCY = targeted;
SWIFT_VERSION = 5.0;
+449
View File
@@ -0,0 +1,449 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 77;
objects = {
/* Begin PBXFileReference section */
A7 /* SonarDesk.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SonarDesk.app; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedRootGroup section */
A6 /* SonarDesk */ = {
isa = PBXFileSystemSynchronizedRootGroup;
path = SonarDesk;
sourceTree = "<group>";
};
/* End PBXFileSystemSynchronizedRootGroup section */
/* Begin PBXFrameworksBuildPhase section */
A10 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
A3 = {
isa = PBXGroup;
children = (
A6 /* SonarDesk */,
A4 /* Products */,
);
sourceTree = "<group>";
};
A4 /* Products */ = {
isa = PBXGroup;
children = (
A7 /* SonarDesk.app */,
);
name = Products;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
A5 /* SonarDesk */ = {
isa = PBXNativeTarget;
buildConfigurationList = A8 /* Build configuration list for PBXNativeTarget "SonarDesk" */;
buildPhases = (
A9 /* Sources */,
A10 /* Frameworks */,
A11 /* Resources */,
);
buildRules = (
);
dependencies = (
);
fileSystemSynchronizedGroups = (
A6 /* SonarDesk */,
);
name = SonarDesk;
packageProductDependencies = (
);
productName = SonarDesk;
productReference = A7 /* SonarDesk.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
A1 /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = 1;
LastSwiftUpdateCheck = 2600;
LastUpgradeCheck = 2700;
};
buildConfigurationList = A2 /* Build configuration list for PBXProject "SonarDesk" */;
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = A3;
preferredProjectObjectVersion = 77;
productRefGroup = A4 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
A5 /* SonarDesk */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
A11 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
A9 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin XCBuildConfiguration section */
A19 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ENABLE_MODULES = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 39R6U3437D;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 14.0;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = macosx;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_VERSION = 5.0;
};
name = Debug;
};
A12 /* Develop */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ENABLE_MODULES = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 39R6U3437D;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 14.0;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = macosx;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_VERSION = 5.0;
};
name = Develop;
};
A16 /* Stage */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ENABLE_MODULES = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 39R6U3437D;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 14.0;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = macosx;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_VERSION = 5.0;
};
name = Stage;
};
A13 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ENABLE_MODULES = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 39R6U3437D;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 14.0;
SDKROOT = macosx;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_VERSION = 5.0;
};
name = Release;
};
A20 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CLANG_ENABLE_OBJC_WEAK = YES;
CODE_SIGN_ENTITLEMENTS = SonarDesk/SonarDesk.entitlements;
CODE_SIGN_INJECT_BASE_ENTITLEMENTS = YES;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 6;
DEAD_CODE_STRIPPING = YES;
ENABLE_APP_SANDBOX = YES;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
GENERATE_INFOPLIST_FILE = NO;
INFOPLIST_FILE = Configuration/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = SonarDesk;
INFOPLIST_KEY_CFBundleIconName = AppIcon;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools";
MARKETING_VERSION = 2.2.0;
PRODUCT_BUNDLE_IDENTIFIER = net.absdev.SonarDesk;
PRODUCT_NAME = "$(TARGET_NAME)";
SONARDESK_ENVIRONMENT = Develop;
SONARDESK_LICENSE_BASE_URL = "https://sonardesk.vm";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_STRICT_CONCURRENCY = targeted;
SWIFT_VERSION = 5.0;
};
name = Debug;
};
A14 /* Develop */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CLANG_ENABLE_OBJC_WEAK = YES;
CODE_SIGN_ENTITLEMENTS = SonarDesk/SonarDesk.entitlements;
CODE_SIGN_INJECT_BASE_ENTITLEMENTS = YES;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 6;
DEAD_CODE_STRIPPING = YES;
ENABLE_APP_SANDBOX = YES;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
GENERATE_INFOPLIST_FILE = NO;
INFOPLIST_FILE = Configuration/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = SonarDesk;
INFOPLIST_KEY_CFBundleIconName = AppIcon;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools";
MARKETING_VERSION = 2.2.0;
PRODUCT_BUNDLE_IDENTIFIER = net.absdev.SonarDesk;
PRODUCT_NAME = "$(TARGET_NAME)";
SONARDESK_ENVIRONMENT = Develop;
SONARDESK_LICENSE_BASE_URL = "https://sonardesk.vm";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_STRICT_CONCURRENCY = targeted;
SWIFT_VERSION = 5.0;
};
name = Develop;
};
A18 /* Stage */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CLANG_ENABLE_OBJC_WEAK = YES;
CODE_SIGN_ENTITLEMENTS = SonarDesk/SonarDesk.entitlements;
CODE_SIGN_INJECT_BASE_ENTITLEMENTS = YES;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 6;
DEAD_CODE_STRIPPING = YES;
ENABLE_APP_SANDBOX = YES;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
GENERATE_INFOPLIST_FILE = NO;
INFOPLIST_FILE = Configuration/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = SonarDesk;
INFOPLIST_KEY_CFBundleIconName = AppIcon;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools";
MARKETING_VERSION = 2.2.0;
PRODUCT_BUNDLE_IDENTIFIER = net.absdev.SonarDesk;
PRODUCT_NAME = "$(TARGET_NAME)";
SONARDESK_ENVIRONMENT = Stage;
SONARDESK_LICENSE_BASE_URL = "https://sonardesk.stage.absdev.net";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_STRICT_CONCURRENCY = targeted;
SWIFT_VERSION = 5.0;
};
name = Stage;
};
A15 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CLANG_ENABLE_OBJC_WEAK = YES;
CODE_SIGN_ENTITLEMENTS = SonarDesk/SonarDesk.entitlements;
CODE_SIGN_INJECT_BASE_ENTITLEMENTS = NO;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 6;
DEAD_CODE_STRIPPING = YES;
ENABLE_APP_SANDBOX = YES;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES;
GENERATE_INFOPLIST_FILE = NO;
INFOPLIST_FILE = Configuration/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = SonarDesk;
INFOPLIST_KEY_CFBundleIconName = AppIcon;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools";
MARKETING_VERSION = 2.2.0;
PRODUCT_BUNDLE_IDENTIFIER = net.absdev.SonarDesk;
PRODUCT_NAME = "$(TARGET_NAME)";
SONARDESK_ENVIRONMENT = Production;
SONARDESK_LICENSE_BASE_URL = "https://sonardesk.absdev.net";
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OPTIMIZATION_LEVEL = "-O";
SWIFT_STRICT_CONCURRENCY = targeted;
SWIFT_VERSION = 5.0;
VALIDATE_PRODUCT = YES;
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
A2 /* Build configuration list for PBXProject "SonarDesk" */ = {
isa = XCConfigurationList;
buildConfigurations = (
A19 /* Debug */,
A12 /* Develop */,
A16 /* Stage */,
A13 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
A8 /* Build configuration list for PBXNativeTarget "SonarDesk" */ = {
isa = XCConfigurationList;
buildConfigurations = (
A20 /* Debug */,
A14 /* Develop */,
A18 /* Stage */,
A15 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = A1 /* Project object */;
}
+35
View File
@@ -33,6 +33,41 @@ final class LicenseStore: ObservableObject {
}
}
func replaceLicense(with key: String) async -> Bool {
let clean = key.trimmingCharacters(in: .whitespacesAndNewlines).uppercased()
guard !clean.isEmpty else {
errorMessage = "Enter your new SonarDesk licence key."
return false
}
let previousToken = KeychainService.licenseToken()
isWorking = true
errorMessage = nil
defer {
isWorking = false
hasChecked = true
}
do {
// Validate the replacement before touching the currently working licence.
let (result, newToken) = try await LicenseService.shared.activate(key: clean)
try KeychainService.saveLicenseToken(newToken)
status = result
// Once the replacement is safely stored, release the old activation.
// Failure here must not undo a successfully installed replacement licence.
if let previousToken, previousToken != newToken {
try? await LicenseService.shared.deactivate(token: previousToken)
}
return true
} catch {
errorMessage = error.localizedDescription
return false
}
}
func refresh() async {
guard let token = KeychainService.licenseToken() else {
status = nil
@@ -3,54 +3,193 @@ import SwiftUI
struct LicenseSettingsView: View {
@EnvironmentObject private var license: LicenseStore
@State private var key = ""
@State private var isChangingLicense = false
@State private var showingDeactivateConfirmation = false
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 20) {
Text("Licence").font(.title2.bold())
Text("Activate this Mac with the SonarDesk licence key sent to you after purchase.")
Text("Licence")
.font(.title2.bold())
Text("Manage the SonarDesk licence used to activate this Mac.")
.foregroundStyle(.secondary)
GroupBox {
VStack(alignment: .leading, spacing: 14) {
if license.isLicensed, let status = license.status {
Label("SonarDesk is activated", systemImage: "checkmark.seal.fill")
.font(.headline).foregroundStyle(.green)
LabeledContent("Edition", value: status.edition ?? "SonarDesk")
if let expires = status.expiresAt {
LabeledContent("Valid until", value: expires.formatted(date: .abbreviated, time: .omitted))
}
LabeledContent("Server", value: LicenseEnvironment.displayName)
HStack {
Button("Check Licence") { Task { await license.refresh() } }
Spacer()
Button("Deactivate This Mac", role: .destructive) { Task { await license.deactivate() } }
}
activatedContent(status)
} else {
SecureField("XXXXX-XXXXX-XXXXX-XXXXX", text: $key)
.textFieldStyle(.roundedBorder)
.onSubmit { Task { await license.activate(key) } }
HStack {
Text("Server: \(LicenseEnvironment.displayName)")
.font(.caption).foregroundStyle(.secondary)
Spacer()
Button("Activate") { Task { await license.activate(key) } }
.buttonStyle(.borderedProminent)
.disabled(key.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || license.isWorking)
}
activationForm
}
if license.isWorking { ProgressView().controlSize(.small) }
if license.isWorking {
ProgressView()
.controlSize(.small)
}
if let error = license.errorMessage {
Text(error).font(.callout).foregroundStyle(.red).textSelection(.enabled)
Text(error)
.font(.callout)
.foregroundStyle(.red)
.textSelection(.enabled)
}
}.padding(8)
}
.padding(8)
}
Text("Your signed activation token is stored only in this Mac's Keychain. Your licence key is used to activate the Mac and is not retained by SonarDesk.")
.font(.caption).foregroundStyle(.secondary)
Text("SonarDesk stores the signed activation token in this Mac's Keychain. Licence keys are used only for activation and are not retained by the app.")
.font(.caption)
.foregroundStyle(.secondary)
}
.frame(maxWidth: 620, alignment: .leading)
.padding(32)
}
.confirmationDialog(
"Deactivate SonarDesk on this Mac?",
isPresented: $showingDeactivateConfirmation,
titleVisibility: .visible
) {
Button("Deactivate This Mac", role: .destructive) {
Task {
await license.deactivate()
if !license.isLicensed {
key = ""
isChangingLicense = false
}
}
}
Button("Cancel", role: .cancel) { }
} message: {
Text("This removes the activation from this Mac. You will need a valid licence key to activate SonarDesk again.")
}
}
@ViewBuilder
private func activatedContent(_ status: LicenseStatus) -> some View {
Label("SonarDesk is activated", systemImage: "checkmark.seal.fill")
.font(.headline)
.foregroundStyle(.green)
LabeledContent("Edition", value: status.edition ?? "SonarDesk")
if let expires = status.expiresAt {
LabeledContent(
"Valid until",
value: expires.formatted(date: .abbreviated, time: .omitted)
)
}
LabeledContent("Server", value: LicenseEnvironment.displayName)
if isChangingLicense {
Divider()
VStack(alignment: .leading, spacing: 10) {
Text("Change Licence")
.font(.headline)
Text("Enter the replacement licence key. Your current activation remains in place unless the new licence is successfully validated.")
.font(.callout)
.foregroundStyle(.secondary)
SecureField("XXXXX-XXXXX-XXXXX-XXXXX", text: $key)
.textFieldStyle(.roundedBorder)
.disabled(license.isWorking)
.onSubmit { replaceLicense() }
HStack {
Button("Cancel") {
key = ""
isChangingLicense = false
license.errorMessage = nil
}
.disabled(license.isWorking)
Spacer()
Button("Activate New Licence") {
replaceLicense()
}
.buttonStyle(.borderedProminent)
.disabled(cleanKey.isEmpty || license.isWorking)
}
}
} else {
HStack {
Button("Check Licence") {
Task { await license.refresh() }
}
.disabled(license.isWorking)
Button("Change Licence…") {
key = ""
license.errorMessage = nil
isChangingLicense = true
}
.disabled(license.isWorking)
Spacer()
Button("Deactivate This Mac", role: .destructive) {
showingDeactivateConfirmation = true
}
.disabled(license.isWorking)
}
}
}
private var activationForm: some View {
VStack(alignment: .leading, spacing: 14) {
Text("Activate SonarDesk")
.font(.headline)
Text("Enter the licence key emailed to you after purchase.")
.font(.callout)
.foregroundStyle(.secondary)
SecureField("XXXXX-XXXXX-XXXXX-XXXXX", text: $key)
.textFieldStyle(.roundedBorder)
.disabled(license.isWorking)
.onSubmit { activateLicense() }
HStack {
Text("Server: \(LicenseEnvironment.displayName)")
.font(.caption)
.foregroundStyle(.secondary)
Spacer()
Button("Activate") {
activateLicense()
}
.buttonStyle(.borderedProminent)
.disabled(cleanKey.isEmpty || license.isWorking)
}
}
}
private var cleanKey: String {
key.trimmingCharacters(in: .whitespacesAndNewlines)
}
private func activateLicense() {
guard !cleanKey.isEmpty else { return }
Task {
await license.activate(key)
if license.isLicensed {
key = ""
}
}
}
private func replaceLicense() {
guard !cleanKey.isEmpty else { return }
Task {
if await license.replaceLicense(with: key) {
key = ""
isChangingLicense = false
}
}
}
}