diff --git a/Sources/AppDelegate.swift b/Sources/AppDelegate.swift index 34aa3bc6..abe88d39 100644 --- a/Sources/AppDelegate.swift +++ b/Sources/AppDelegate.swift @@ -5910,6 +5910,11 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent updateController.checkForUpdates() } + func checkForUpdatesInCustomUI() { + updateViewModel.overrideState = nil + updateController.checkForUpdatesInCustomUI() + } + func openWelcomeWorkspace() { guard let context = preferredMainWindowContextForWorkspaceCreation(event: nil, debugSource: "welcome") else { return diff --git a/Sources/Update/UpdateController.swift b/Sources/Update/UpdateController.swift index 3ac1394a..c5fd4ad0 100644 --- a/Sources/Update/UpdateController.swift +++ b/Sources/Update/UpdateController.swift @@ -208,6 +208,11 @@ class UpdateController { checkForUpdatesWhenReady(retries: readyRetryCount) } + /// Check for updates using the custom popover-based UI. + func checkForUpdatesInCustomUI() { + checkForUpdatesWhenReady(retries: readyRetryCount) + } + private func performCheckForUpdates() { startUpdaterIfNeeded() ensureSparkleInstallationCache() diff --git a/Sources/Update/UpdatePill.swift b/Sources/Update/UpdatePill.swift index 42abf9f8..25f163b2 100644 --- a/Sources/Update/UpdatePill.swift +++ b/Sources/Update/UpdatePill.swift @@ -28,8 +28,12 @@ struct UpdatePill: View { private var pillButton: some View { Button(action: { if model.showsDetectedBackgroundUpdate { - showPopover = false - AppDelegate.shared?.checkForUpdates(nil) + if showPopover { + showPopover = false + } else { + showPopover = true + AppDelegate.shared?.checkForUpdatesInCustomUI() + } return } if case .notFound(let notFound) = model.state { diff --git a/Sources/Update/UpdatePopoverView.swift b/Sources/Update/UpdatePopoverView.swift index 2361775d..301c3ae1 100644 --- a/Sources/Update/UpdatePopoverView.swift +++ b/Sources/Update/UpdatePopoverView.swift @@ -11,7 +11,12 @@ struct UpdatePopoverView: View { VStack(alignment: .leading, spacing: 0) { switch model.effectiveState { case .idle: - EmptyView() + if let detectedVersion = model.detectedUpdateVersion, + model.showsDetectedBackgroundUpdate { + DetectedBackgroundUpdateView(version: detectedVersion) + } else { + EmptyView() + } case .permissionRequest(let request): PermissionRequestView(request: request, dismiss: dismiss) @@ -42,6 +47,35 @@ struct UpdatePopoverView: View { } } +fileprivate struct DetectedBackgroundUpdateView: View { + let version: String + + var body: some View { + VStack(alignment: .leading, spacing: 16) { + VStack(alignment: .leading, spacing: 8) { + Text(String(localized: "update.popover.updateAvailable", defaultValue: "Update Available")) + .font(.system(size: 13, weight: .semibold)) + + HStack(spacing: 6) { + Text(String(localized: "update.popover.version", defaultValue: "Version:")) + .foregroundColor(.secondary) + .frame(width: 60, alignment: .trailing) + Text(version) + } + .font(.system(size: 11)) + } + + HStack(spacing: 10) { + ProgressView() + .controlSize(.small) + Text(String(localized: "update.popover.checking", defaultValue: "Checking for updates…")) + .font(.system(size: 13)) + } + } + .padding(16) + } +} + fileprivate struct PermissionRequestView: View { let request: UpdateState.PermissionRequest let dismiss: DismissAction diff --git a/cmuxUITests/UpdatePillUITests.swift b/cmuxUITests/UpdatePillUITests.swift index d76c40ba..8493a0de 100644 --- a/cmuxUITests/UpdatePillUITests.swift +++ b/cmuxUITests/UpdatePillUITests.swift @@ -59,6 +59,32 @@ final class UpdatePillUITests: XCTestCase { attachScreenshot(name: "background-detected-update-available") } + func testDetectedBackgroundUpdateFirstClickOpensPopover() { + let systemSettings = XCUIApplication(bundleIdentifier: "com.apple.systempreferences") + systemSettings.terminate() + + let app = XCUIApplication() + app.launchEnvironment["CMUX_UI_TEST_MODE"] = "1" + app.launchEnvironment["CMUX_UI_TEST_DETECTED_UPDATE_VERSION"] = "9.9.9" + app.launchEnvironment["CMUX_UI_TEST_FEED_URL"] = "https://cmux.test/appcast.xml" + app.launchEnvironment["CMUX_UI_TEST_FEED_MODE"] = "available" + app.launchEnvironment["CMUX_UI_TEST_UPDATE_VERSION"] = "9.9.9" + app.launchEnvironment["CMUX_UI_TEST_AUTO_ALLOW_PERMISSION"] = "1" + launchAndActivate(app) + + let pill = pillButton(app: app, expectedLabel: "Update Available: 9.9.9") + XCTAssertTrue(pill.waitForExistence(timeout: 6.0)) + assertVisibleSize(pill) + + pill.click() + + XCTAssertTrue( + app.staticTexts["Update Available"].waitForExistence(timeout: 8.0), + "Expected the first click on a background-detected update pill to open the popover" + ) + XCTAssertTrue(app.buttons["Install and Relaunch"].waitForExistence(timeout: 2.0)) + } + func testUpdatePillShowsForNoUpdateThenDismisses() { let systemSettings = XCUIApplication(bundleIdentifier: "com.apple.systempreferences") systemSettings.terminate()