Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Assets/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.1.1</string>
<string>1.1.2</string>
<key>CFBundleVersion</key>
<string>14</string>
<string>15</string>
<key>LSMinimumSystemVersion</key>
<string>14.0</string>
<key>LSUIElement</key>
Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ All notable changes to this project are documented here.

## [Unreleased]

## [1.1.2] - 2026-09-18

### Fixed

- Quiet checks keep running while an update is on offer and after a failed update, so a newer version shows up without restarting Encaje. The card only changes when a newer version is actually found, a failed update keeps its Retry, and nothing is ever installed without "Install now".
- Pressing Update while a slow check is still running no longer ends in a failed update: the download waits for that check to finish and still stops at "Ready to install".

## [1.1.1] - 2026-09-18

### Changed
Expand Down
39 changes: 35 additions & 4 deletions Sources/EncajeApp/UpdateManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ final class UpdateManager: ObservableObject {
}

static let autoCheckDefaultsKey = "autoUpdateCheckEnabled"
static let installNowCheckRetryLimit = 40
static let installNowCheckRetryLimit = 300
static let installNowCheckRetryDelay: TimeInterval = 0.25
static let backgroundCheckInterval: TimeInterval = 30 * 60
static let backgroundCheckThrottle: TimeInterval = 5 * 60

Expand Down Expand Up @@ -131,6 +132,23 @@ final class UpdateManager: ObservableObject {

var backgroundDiscoveryArmed: Bool { backgroundCheckTimer != nil }

var phaseAllowsQuietCheck: Bool {
guard !installRequested, !installNowRequested, !resumeCheckPending,
!manualCheckPending, pendingInstallReply == nil
else { return false }
switch phase {
case .idle, .available, .failed:
return true
case .downloading, .readyToInstall, .installing:
return false
}
}

private var sessionIsUserDriven: Bool {
installRequested || installNowRequested || resumeCheckPending
|| (manualCheckPending && !manualCheckWaiting)
}

func startBackgroundDiscovery() {
guard backgroundCheckTimer == nil else { return }
let interval = backgroundCheckIntervalProvider()
Expand All @@ -157,7 +175,9 @@ final class UpdateManager: ObservableObject {
}

func requestBackgroundCheck() {
guard autoCheckEnabled, phase == .idle, !isSessionInProgress(self) else { return }
guard autoCheckEnabled, phaseAllowsQuietCheck, hasLiveUpdater(self),
!isSessionInProgress(self)
else { return }
let now = monotonicClock()
if let lastBackgroundCheck, now - lastBackgroundCheck < Self.backgroundCheckThrottle { return }
lastBackgroundCheck = now
Expand Down Expand Up @@ -296,7 +316,8 @@ final class UpdateManager: ObservableObject {
return
}
Task { [weak self] in
try? await Task.sleep(nanoseconds: 250_000_000)
try? await Task.sleep(
nanoseconds: UInt64(Self.installNowCheckRetryDelay * 1_000_000_000))
guard let self, !Task.isCancelled else { return }
self.startInstallNowCheck(attempt: attempt + 1)
}
Expand All @@ -308,6 +329,11 @@ final class UpdateManager: ObservableObject {
informationOnly: Bool,
stage: SPUUserUpdateStage
) -> SPUUserUpdateChoice {
if !sessionIsUserDriven, phase != .idle, let pendingVersion,
!Self.isNewerVersion(version, than: pendingVersion)
{
return .dismiss
}
resumeCheckPending = false
pendingVersion = version
pendingIsInformationOnly = informationOnly
Expand Down Expand Up @@ -418,8 +444,13 @@ final class UpdateManager: ObservableObject {
}
}

private static func isNewerVersion(_ version: String, than current: String) -> Bool {
SUStandardVersionComparator.default.compareVersion(version, toVersion: current)
== .orderedDescending
}

private func finishManualCheck(status: ManualCheckStatus) {
guard manualCheckPending else { return }
guard manualCheckPending, !manualCheckWaiting else { return }
manualCheckPending = false
manualCheckStatus = status
guard status != .idle else { return }
Expand Down
3 changes: 2 additions & 1 deletion Tests/EncajeAppTests/PermissionLifecycleTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import XCTest

final class PermissionLifecycleTests: XCTestCase {
@MainActor func testDismissPreservesPendingSetupAcrossCoordinatorInstances() {
let name = "Encaje.Tests.\(UUID().uuidString)"
let name = "Encaje.PermissionLifecycleTests"
let defaults = UserDefaults(suiteName: name)!
defaults.removePersistentDomain(forName: name)
defer { defaults.removePersistentDomain(forName: name) }
defaults.set(true, forKey: "permissionSetupPending")
defaults.set(true, forKey: "welcomeComplete")
Expand Down
Loading
Loading