From 1a7d026eddcae8c98c49159bb67726ae22f0e02f Mon Sep 17 00:00:00 2001 From: Mark Gregory Date: Wed, 30 Sep 2026 10:27:14 +0100 Subject: [PATCH] updated deployer code --- Configuration/Info.plist | 2 + SonarDesk.xcodeproj/project.pbxproj | 94 +++++++++++++++++-- SonarDesk/App/SonarDeskApp.swift | 25 +++-- SonarDesk/Services/KeychainService.swift | 30 ++++++ .../Views/Projects/ProjectDetailView.swift | 27 +++++- SonarDesk/Views/Settings/SettingsView.swift | 4 + 6 files changed, 166 insertions(+), 16 deletions(-) diff --git a/Configuration/Info.plist b/Configuration/Info.plist index c799e1e..56635a4 100644 --- a/Configuration/Info.plist +++ b/Configuration/Info.plist @@ -11,6 +11,8 @@ CFBundleShortVersionString$(MARKETING_VERSION) CFBundleVersion$(CURRENT_PROJECT_VERSION) LSApplicationCategoryTypepublic.app-category.developer-tools + SonarDeskEnvironment$(SONARDESK_ENVIRONMENT) + SonarDeskLicenseBaseURL$(SONARDESK_LICENSE_BASE_URL) NSUbiquitousContainers iCloud.net.absdev.SonarDesk diff --git a/SonarDesk.xcodeproj/project.pbxproj b/SonarDesk.xcodeproj/project.pbxproj index 9061a2f..58365ed 100644 --- a/SonarDesk.xcodeproj/project.pbxproj +++ b/SonarDesk.xcodeproj/project.pbxproj @@ -119,7 +119,7 @@ /* End PBXSourcesBuildPhase section */ /* Begin XCBuildConfiguration section */ - A12 /* Debug */ = { + A12 /* Develop */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; @@ -160,8 +160,52 @@ STRING_CATALOG_GENERATE_SYMBOLS = YES; SWIFT_VERSION = 5.0; }; - name = Debug; + 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 = { @@ -203,7 +247,7 @@ }; name = Release; }; - A14 /* Debug */ = { + A14 /* Develop */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; @@ -226,12 +270,46 @@ 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; + 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 = { @@ -255,6 +333,8 @@ MARKETING_VERSION = 2.2.0; PRODUCT_BUNDLE_IDENTIFIER = net.absdev.SonarDesk; PRODUCT_NAME = "$(TARGET_NAME)"; + SONARDESK_ENVIRONMENT = Production; + SONARDESK_LICENSE_BASE_URL = "https://sonardsk.absdev.net"; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OPTIMIZATION_LEVEL = "-O"; @@ -270,7 +350,8 @@ A2 /* Build configuration list for PBXProject "SonarDesk" */ = { isa = XCConfigurationList; buildConfigurations = ( - A12 /* Debug */, + A12 /* Develop */, + A16 /* Stage */, A13 /* Release */, ); defaultConfigurationIsVisible = 0; @@ -279,7 +360,8 @@ A8 /* Build configuration list for PBXNativeTarget "SonarDesk" */ = { isa = XCConfigurationList; buildConfigurations = ( - A14 /* Debug */, + A14 /* Develop */, + A18 /* Stage */, A15 /* Release */, ); defaultConfigurationIsVisible = 0; diff --git a/SonarDesk/App/SonarDeskApp.swift b/SonarDesk/App/SonarDeskApp.swift index ab5c026..8f672c5 100644 --- a/SonarDesk/App/SonarDeskApp.swift +++ b/SonarDesk/App/SonarDeskApp.swift @@ -13,6 +13,7 @@ struct SonarDeskApp: App { @NSApplicationDelegateAdaptor(SonarDeskAppDelegate.self) private var appDelegate @StateObject private var appState = AppState() @StateObject private var reportStore = ReportStore() + @StateObject private var licenseStore = LicenseStore() @State private var launchPhase: LaunchPhase = .splash private enum LaunchPhase { @@ -28,10 +29,17 @@ struct SonarDeskApp: App { LaunchView() .task { await startApplication() } case .application: - RootView() - .environmentObject(appState) - .environmentObject(reportStore) - .transition(.opacity) + Group { + if licenseStore.isLicensed { + RootView() + .environmentObject(appState) + .environmentObject(reportStore) + } else { + LicenseRequiredView() + } + } + .environmentObject(licenseStore) + .transition(.opacity) } } .frame(minWidth: 1040, minHeight: 680) @@ -51,7 +59,8 @@ struct SonarDeskApp: App { SettingsView() .environmentObject(appState) .environmentObject(reportStore) - .frame(width: 620, height: 430) + .environmentObject(licenseStore) + .frame(width: 720, height: 560) } } @@ -60,11 +69,13 @@ struct SonarDeskApp: App { // Keep the branded launch experience visible long enough to be perceptible, // while using the time to perform the first SonarQube refresh. async let minimumSplash: Void = Task.sleep(for: .seconds(1.6)) + async let licenceRefresh: Void = licenseStore.refresh() - if appState.projects.isEmpty, appState.selectedServer != nil { + _ = await licenceRefresh + + if licenseStore.isLicensed, appState.projects.isEmpty, appState.selectedServer != nil { await appState.refresh() } - _ = try? await minimumSplash withAnimation(.easeInOut(duration: 0.32)) { launchPhase = .application diff --git a/SonarDesk/Services/KeychainService.swift b/SonarDesk/Services/KeychainService.swift index c9df283..a9b2e09 100644 --- a/SonarDesk/Services/KeychainService.swift +++ b/SonarDesk/Services/KeychainService.swift @@ -79,3 +79,33 @@ enum KeychainError: LocalizedError { } } } + +extension KeychainService { + private static let licenseTokenAccount = "sonardesk.license-token" + + static func saveLicenseToken(_ token: String) throws { + let clean = token.trimmingCharacters(in: .whitespacesAndNewlines) + guard !clean.isEmpty else { throw KeychainError.emptyToken } + let data = Data(clean.utf8) + SecItemDelete(query(account: licenseTokenAccount, synchronizable: kSecAttrSynchronizableAny) as CFDictionary) + var add = query(account: licenseTokenAccount, synchronizable: kCFBooleanFalse) + add[kSecValueData as String] = data + add[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly + let status = SecItemAdd(add as CFDictionary, nil) + guard status == errSecSuccess else { throw KeychainError.status(status) } + } + + static func licenseToken() -> String? { + var lookup = query(account: licenseTokenAccount, synchronizable: kSecAttrSynchronizableAny) + lookup[kSecReturnData as String] = true + lookup[kSecMatchLimit as String] = kSecMatchLimitOne + var item: CFTypeRef? + guard SecItemCopyMatching(lookup as CFDictionary, &item) == errSecSuccess, + let data = item as? Data else { return nil } + return String(data: data, encoding: .utf8) + } + + static func deleteLicenseToken() { + SecItemDelete(query(account: licenseTokenAccount, synchronizable: kSecAttrSynchronizableAny) as CFDictionary) + } +} diff --git a/SonarDesk/Views/Projects/ProjectDetailView.swift b/SonarDesk/Views/Projects/ProjectDetailView.swift index 8a3b586..e5510d6 100644 --- a/SonarDesk/Views/Projects/ProjectDetailView.swift +++ b/SonarDesk/Views/Projects/ProjectDetailView.swift @@ -9,6 +9,7 @@ struct ProjectDetailView: View { @State private var credentialError: String? @State private var generatingCredentials = false @State private var exportingAI = false + @State private var exportingPDF = false @State private var exportError: String? private var item: ProjectSnapshot? { app.projects.first { $0.project.key == projectKey } } @@ -18,9 +19,15 @@ struct ProjectDetailView: View { HStack { VStack(alignment: .leading, spacing: 4) { Text(item.project.name).font(.system(size: 30, weight: .bold)); Text(item.project.key).foregroundStyle(.secondary) } Spacer() - Button { Task { await exportAIReport(item) } } label: { - if exportingAI { ProgressView().controlSize(.small) } else { Label("Export for AI Analysis…", systemImage: "brain.head.profile") } - }.buttonStyle(.bordered).disabled(exportingAI) + Menu { + Button { Task { await exportPDFReport(item) } } label: { Label("PDF Report…", systemImage: "doc.richtext") } + Button { Task { await exportAIReport(item) } } label: { Label("AI Analysis Export…", systemImage: "brain.head.profile") } + } label: { + if exportingPDF || exportingAI { ProgressView().controlSize(.small) } else { Label("Export Report…", systemImage: "square.and.arrow.up") } + } + .menuStyle(.borderlessButton) + .fixedSize() + .disabled(exportingPDF || exportingAI) GateBadge(status: item.qualityGate) }.padding(.horizontal, 26).padding(.top, 22).padding(.bottom, 14) @@ -172,6 +179,20 @@ struct ProjectDetailView: View { catch { credentialError = error.localizedDescription } } + @MainActor private func exportPDFReport(_ item: ProjectSnapshot) async { + exportingPDF = true; exportError = nil + defer { exportingPDF = false } + do { + let issues = try await app.allIssues(projectKey: item.project.key) + try ProjectPDFReportExporter.export( + project: item, + issues: issues, + serverURL: app.selectedServer?.baseURL ?? "Unknown", + serverVersion: app.serverVersion + ) + } catch { exportError = error.localizedDescription } + } + @MainActor private func exportAIReport(_ item: ProjectSnapshot) async { exportingAI = true; exportError = nil defer { exportingAI = false } diff --git a/SonarDesk/Views/Settings/SettingsView.swift b/SonarDesk/Views/Settings/SettingsView.swift index 5e81d4a..bf7a42d 100644 --- a/SonarDesk/Views/Settings/SettingsView.swift +++ b/SonarDesk/Views/Settings/SettingsView.swift @@ -3,6 +3,7 @@ import SwiftUI struct SettingsView: View { @EnvironmentObject private var app: AppState @EnvironmentObject private var reportStore: ReportStore + @EnvironmentObject private var license: LicenseStore @State private var editingServer: SonarServer? @State private var showingAddServer = false @@ -10,6 +11,9 @@ struct SettingsView: View { TabView { serversPage .tabItem { Label("Servers", systemImage: "server.rack") } + LicenseSettingsView() + .environmentObject(license) + .tabItem { Label("Licence", systemImage: "key.fill") } reportsPage .tabItem { Label("Reports", systemImage: "doc.text") } aboutPage