Skip to content

feat(settings): add user-controllable update settings - #351

Closed
M3NT1 wants to merge 3 commits into
JerryZLiu:mainfrom
M3NT1:feature/auto-update-toggle
Closed

M3NT1 wants to merge 3 commits into
JerryZLiu:mainfrom
M3NT1:feature/auto-update-toggle

Conversation

@M3NT1

@M3NT1 M3NT1 commented Aug 13, 2026

Copy link
Copy Markdown

Why

Dayflow currently ships with SilentUserDriver configured so Sparkle
silently auto-installs every update — no prompt, no opt-out, no
changelog. Combined with SUAutomaticallyUpdate = true in Info.plist,
the user has zero control over when the app upgrades itself.

This PR adds a user-controllable update preference, following the
pattern of the other toggles in the Settings "App preferences"
section: a SettingsToggle bound to an @Published mirror of a
SPUUpdater property, written back through an explicit setter
(the only path the UI uses — see the Sparkle docs note below).

What changes

3 commits, all on feature/auto-update-toggle (branched from main @ 8944d27):

1. feat(updater): expose Sparkle update controls via UpdaterManager

  • Adds @Published private(set) var automaticallyChecksForUpdates
    (mirror of SPUUpdater.automaticallyChecksForUpdates)
  • Adds @Published private(set) var automaticallyDownloadsUpdates
  • Adds @Published private(set) var canCheckForUpdates
  • Adds @Published var pendingReleaseNotesURL: URL? — captured from
    SUAppcastItem.releaseNotesURL in didFindValidUpdate
  • All four are populated by KVO observers in setupObservers(),
    called after updater.start() so the .initial flag pulls the
    user's saved NSUserDefaults preference from the previous session
  • setAutomaticallyChecksForUpdates(_:) and
    setAutomaticallyDownloadsUpdates(_:) are the only path the UI
    uses to mutate Sparkle settings. They write through SPUUpdater,
    which is what persists the change — the Sparkle docs are explicit
    that you should only set these properties in response to a user
    setting change
  • openReleaseNotes() opens the cached URL via NSWorkspace.shared
    so the "View changelog" link works even when the Sparkle UI was
    dismissed

2. fix(updater): respect user update preference in SilentUserDriver

  • Adds a public var automaticallyChecksForUpdates: Bool = true
    to SilentUserDriver, mirrored from SPUUpdater
  • showUpdateFound now branches on this flag:
    • true.install (existing silent-install behavior)
    • false.dismiss (keeps the update cached, surfaces the
      release-notes URL via Settings, doesn't push the install)
  • The KVO observer in UpdaterManager.setupObservers() is updated
    to write the new value to SilentUserDriver.automaticallyChecksForUpdates
    on every change, so both Sparkle-internal updates (e.g. the
    2nd-launch permission prompt) and user toggle changes take effect
    on the very next update check

3. feat(settings): add update controls to Other tab

  • New "Updates" section in SettingsOtherTabView, between
    "App preferences" and "Output language"
  • Toggle: "Automatically check for updates" — when off, Dayflow
    only checks when the user clicks "Check now"
  • Sub-toggle: "Automatically download and install updates" —
    disabled when the auto-check toggle is off, matching the
    Sparkle reference UI from the official docs
  • "Check now" button: calls updaterManager.checkForUpdates(showUI: true),
    which routes through the SPUStandardUpdaterController (not
    SilentUserDriver) so the user always gets the standard
    release-notes UI even when auto-update is off. Replaced with a
    ProgressView while the check is in flight
  • Status subtitle: shows updaterManager.statusText (e.g.
    "Latest version" / "Update available: v2.2.0" / "Checking…")
  • Conditional "Latest release notes" row: only rendered when
    pendingReleaseNotesURL is non-nil. Subtitle is the URL host
    so the user sees where the link points. "View changelog" button
    calls updaterManager.openReleaseNotes()

Test plan

  • Builds clean on Dayflow v2.1.0 with Xcode 26.6 (arm64)
  • Toggle state persists across app relaunch (via Sparkle's
    NSUserDefaults storage)
  • When auto-check is on, updates still install silently
    (no regression)
  • When auto-check is off, "Check now" surfaces the standard
    Sparkle update UI with release notes
  • "View changelog" opens the URL in the default browser

Notes

  • The Info.plist keys (SUEnableAutomaticChecks, SUAutomaticallyUpdate,
    SUScheduledCheckInterval) are unchanged. The user preference is
    applied at runtime via the SPUUpdater setters, which is the
    pattern the Sparkle docs recommend
  • I considered replacing SilentUserDriver with
    SPUStandardUserDriver outright, but kept the silent driver so
    users who never visit Settings keep the existing behavior
    (and so the diff is smaller / easier to review)
  • The pendingReleaseNotesURL is cleared in updaterDidNotFindUpdate
    and the no-update branch of didAbortWithError so it never points
    at a stale URL after a "no updates" check

M3NT1 added 3 commits August 13, 2026 14:34
Add @published mirrors of SPUUpdater settings so the Settings UI can bind
to them via SwiftUI:

  - automaticallyChecksForUpdates (read-only @published; backed by KVO)
  - automaticallyDownloadsUpdates (read-only @published; backed by KVO)
  - canCheckForUpdates             (read-only @published; backed by KVO)
  - pendingReleaseNotesURL         (read/write @published; captured from
                                    SUAppcastItem.releaseNotesURL)

The mirrors are populated by KVO observers in setupObservers(), which is
called after updater.start() so the .initial flag pulls the user's
saved NSUserDefaults preference from the previous session. This means
the Settings toggle starts in the right state on every launch without
us having to read the value ourselves at init.

Two new setters, setAutomaticallyChecksForUpdates() and
setAutomaticallyDownloadsUpdates(), are the only path the UI uses to
change Sparkle settings. They write through SPUUpdater, which is what
persists the change to NSUserDefaults — the Sparkle docs require this
("Only set this property if the user wants to change the default via a
user settings option"). Writing through the @published mirror would
silently drop the change on next launch.

openReleaseNotes() opens the cached URL via NSWorkspace.shared so the
"View changelog" link in Settings works even when the Sparkle UI was
dismissed (i.e. when the user has auto-update disabled).

pendingReleaseNotesURL is captured in didFindValidUpdate and cleared in
updaterDidNotFindUpdate and the no-update branch of didAbortWithError,
so it never points at a stale release-notes URL after a successful
"no updates" check.

import AppKit added for NSWorkspace.
SilentUserDriver currently responds .install to every showUpdateFound
callback, meaning Dayflow installs every Sparkle update without any
user interaction. With the new Settings toggle, we want the driver to
honor the user's preference:

  - if automaticallyChecksForUpdates == true: reply .install
    (the existing silent-install behavior — used when the user has
    opted into auto-update)
  - if automaticallyChecksForUpdates == false: reply .dismiss
    (keep the update cached, surface the release-notes URL via the
    "View changelog" link in Settings, but don't push the install)

The automaticallyChecksForUpdates property is mirrored from
SPUUpdater.automaticallyChecksForUpdates. The mirror is kept in sync
in two places: the KVO observer in UpdaterManager.setupObservers()
(picks up Sparkle-internal changes, e.g. the 2nd-launch permission
prompt) and the user-facing setters in UpdaterManager
(picks up user-initiated changes immediately).

The KVO observer change in UpdaterManager is a behavioral change for
SilentUserDriver, so it's bundled into this commit rather than the
first one — both changes are required for the toggle to actually
take effect.
Wires UpdaterManager into SettingsOtherTabView as a new "Updates"
section. Follows the same pattern as the existing toggles in the
"App preferences" section: SettingsRow + SettingsToggle bound via a
custom Binding(get:set:) that delegates to UpdaterManager.

Layout (top to bottom):

  - Toggle: "Automatically check for updates"
    Binds to updaterManager.automaticallyChecksForUpdates via
    setAutomaticallyChecksForUpdates(). When off, Dayflow only checks
    for updates when the user clicks "Check now".

  - Sub-toggle: "Automatically download and install updates"
    Binds to updaterManager.automaticallyDownloadsUpdates. Disabled
    when the auto-check toggle is off (mirroring the Sparkle reference
    UI in the official docs).

  - Button: "Check now"
    Calls updaterManager.checkForUpdates(showUI: true), which routes
    through the SPUStandardUpdaterController (not SilentUserDriver) so
    the user always gets the standard release-notes UI even when
    auto-update is off. Disabled while isChecking or while
    canCheckForUpdates is false. Replaced with a ProgressView while
    isChecking.

  - Status subtitle
    Shows updaterManager.statusText — empty unless a check is
    in progress ("Checking…"), just completed ("Latest version"
    or "Update available: vX.Y.Z"), or errored ("Update needs
    authorization" / "Update check failed").

  - Conditional row: "Latest release notes"
    Only rendered when pendingReleaseNotesURL is non-nil, i.e. an
    update has been found and its feed entry included a notes URL.
    Subtitle is the URL host so the user can see where the link
    points before clicking. Tapping "View changelog" calls
    updaterManager.openReleaseNotes(), which opens the URL in the
    user's default browser via NSWorkspace.

Tested: builds clean on Dayflow v2.1.0 (Xcode 26.6, arm64).
@M3NT1

M3NT1 commented Aug 13, 2026

Copy link
Copy Markdown
Author

Closing in favor of #347 — the auto-update toggle commits were cherry-picked into the fix branch so the new feature ships alongside the other v2.1.0 fixes in a single PR. Same 3 commits (cherry-pick of e8a284d, 4697ade, e0f783c), same diff in UpdaterManager.swift, SilentUserDriver.swift, and SettingsOtherTabView.swift.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant