diff --git a/SonarDesk.xcodeproj/project.pbxproj b/SonarDesk.xcodeproj/project.pbxproj index 6e47af1..6da5e25 100644 --- a/SonarDesk.xcodeproj/project.pbxproj +++ b/SonarDesk.xcodeproj/project.pbxproj @@ -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; diff --git a/SonarDesk.xcodeproj/project.pbxproj.backup b/SonarDesk.xcodeproj/project.pbxproj.backup new file mode 100644 index 0000000..6e47af1 --- /dev/null +++ b/SonarDesk.xcodeproj/project.pbxproj.backup @@ -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 = ""; + }; +/* 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 = ""; + }; + A4 /* Products */ = { + isa = PBXGroup; + children = ( + A7 /* SonarDesk.app */, + ); + name = Products; + sourceTree = ""; + }; +/* 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 */; +} diff --git a/SonarDesk/Stores/LicenseStore.swift b/SonarDesk/Stores/LicenseStore.swift index 33361c0..f5cf1f4 100644 --- a/SonarDesk/Stores/LicenseStore.swift +++ b/SonarDesk/Stores/LicenseStore.swift @@ -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 diff --git a/SonarDesk/Views/Settings/LicenseSettingsView.swift b/SonarDesk/Views/Settings/LicenseSettingsView.swift index 70f17d8..996f1e9 100644 --- a/SonarDesk/Views/Settings/LicenseSettingsView.swift +++ b/SonarDesk/Views/Settings/LicenseSettingsView.swift @@ -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 + } + } } }