Skip to content

Fix desktop onboarding reset scoping#5604

Merged
kodjima33 merged 1 commit intomainfrom
fix/reset-onboarding-current-build
Mar 13, 2026
Merged

Fix desktop onboarding reset scoping#5604
kodjima33 merged 1 commit intomainfrom
fix/reset-onboarding-current-build

Conversation

@kodjima33
Copy link
Collaborator

Summary

  • reset onboarding through a live in-process notification before restart
  • clear current-build onboarding app storage state in desktop home and onboarding views
  • clarify settings copy that reset is scoped to the current app build only

Testing

  • xcrun swift build -c debug --package-path Desktop (currently fails in repo due existing macOS deployment target mismatch: omi-lib 10.13 vs AudioKit 11.0)

@kodjima33 kodjima33 merged commit 43bc936 into main Mar 13, 2026
2 checks passed
@kodjima33 kodjima33 deleted the fix/reset-onboarding-current-build branch March 13, 2026 20:48
@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 13, 2026

Greptile Summary

This PR fixes a race condition in the desktop onboarding reset flow where @AppStorage-backed properties could hold stale in-memory values (e.g. hasCompletedOnboarding = true, onboardingStep = 3) after UserDefaults keys were cleared, potentially writing those stale values back before app termination. The fix posts a resetOnboardingRequested notification so that live SwiftUI views explicitly zero out their @AppStorage state before the restart fires.

Key changes:

  • AppState.resetOnboardingAndRestart() now dispatches a resetOnboardingRequested notification (async on main thread) and sleeps 150 ms on a background thread before calling restartApp(), giving the main thread time to process the notification.
  • DesktopHomeView registers two new @AppStorage bindings (onboardingStep, onboardingJustCompleted) and resets them to 0/false in the notification handler alongside the existing hasCompletedOnboarding reset.
  • OnboardingView also listens for the notification and resets currentStep = 0 — this guard is active when the user triggers reset while mid-onboarding (e.g. via the menu bar in State 2).
  • Settings copy clarifies that the reset is scoped to the current app build only.

Issues found:

  • The comment above the DispatchQueue.main.async block says the notification fires "before touching raw UserDefaults," but since both call-sites (SettingsPage button and OmiApp's @MainActor resetOnboarding()) run on the main thread, DispatchQueue.main.async actually schedules the notification for the next run-loop iteration — i.e., after the synchronous removeObject loop. The comment is misleading about execution order, though the code is functionally correct.
  • Thread.sleep(forTimeInterval: 0.15) blocks a GCD thread-pool worker for 150 ms; DispatchQueue.main.asyncAfter would be a cleaner alternative.
  • The @AppStorage writes in DesktopHomeView's notification handler re-create the two keys that were just removed (onboardingStep = 0, onboardingJustCompleted = false). This is harmless since 0/false are the correct reset values, but it means the keys end up present (at their defaults) rather than absent.

Confidence Score: 4/5

  • Safe to merge; all identified issues are style/documentation concerns with no functional regressions.
  • The core logic is sound: UserDefaults keys are removed, @AppStorage properties are reset via notification, and a 150 ms buffer gives the main thread time to process before restart. The two flagged issues (misleading comment about execution order, Thread.sleep on GCD pool) are minor code-quality concerns that don't affect correctness. The @AppStorage re-write of cleared keys is harmless since 0/false matches the intended reset state.
  • desktop/Desktop/Sources/AppState.swift — comment/ordering issue and Thread.sleep pattern worth cleaning up before the function grows more callers.

Important Files Changed

Filename Overview
desktop/Desktop/Sources/AppState.swift Adds notification dispatch + 150 ms sleep before restart; the inline comment claims notification fires "before touching raw UserDefaults" but DispatchQueue.main.async actually schedules it after the synchronous removal when called from the main thread, making the comment misleading about execution order.
desktop/Desktop/Sources/MainWindow/DesktopHomeView.swift Adds @AppStorage bindings for onboardingStep and onboardingJustCompleted and resets them via onReceive; the writes happen after UserDefaults keys are already removed, re-creating them with value 0 — harmless but worth noting.
desktop/Desktop/Sources/OnboardingView.swift Adds an onReceive handler that resets currentStep to 0; correctly scoped to mid-onboarding reset (State 2), since the view is not mounted when the user triggers reset from Settings.
desktop/Desktop/Sources/MainWindow/Pages/SettingsPage.swift UI copy-only change to clarify reset is scoped to the current app build; no logic changes.

Sequence Diagram

sequenceDiagram
    participant User
    participant SettingsPage
    participant AppState
    participant Main as Main Thread
    participant BG as BG Thread
    participant DesktopHomeView
    participant UserDefaults

    User->>SettingsPage: Tap Reset and Restart
    SettingsPage->>AppState: resetOnboardingAndRestart()
    AppState->>Main: schedule post(resetOnboardingRequested) async
    AppState->>UserDefaults: removeObject x11 keys + synchronize
    AppState->>BG: schedule sleep(0.15) then restartApp()

    Note over Main: Next run-loop iteration
    Main->>DesktopHomeView: onReceive(resetOnboardingRequested)
    DesktopHomeView->>AppState: hasCompletedOnboarding = false
    DesktopHomeView->>UserDefaults: onboardingStep = 0 via AppStorage
    DesktopHomeView->>UserDefaults: onboardingJustCompleted = false via AppStorage
    DesktopHomeView->>AppState: stopTranscription()

    Note over BG: After 150 ms
    BG->>AppState: restartApp()
    AppState->>Main: NSApplication.terminate(nil)
Loading

Last reviewed commit: b717ab7

Comment on lines +2469 to +2473
// Update live @AppStorage state in the current app instance before touching
// raw UserDefaults so SwiftUI doesn't write stale onboarding values back.
DispatchQueue.main.async {
NotificationCenter.default.post(name: .resetOnboardingRequested, object: nil)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Misleading comment — notification fires after UserDefaults clearing on the main thread

The inline comment says "before touching raw UserDefaults", but both call sites (SettingsPage.swift button action and OmiApp.swift's @MainActor resetOnboarding()) invoke this function on the main thread. Because DispatchQueue.main.async simply enqueues a block for the next run-loop iteration, the synchronous UserDefaults.standard.removeObject loop below runs first. The notification is therefore dispatched after the keys are already cleared, not before.

This doesn't cause a functional bug in practice (clearing + writing 0/false produces the same final state), but the comment misrepresents the ordering and could mislead future readers about the intent of the guard.

Consider either:

  1. Posting the notification synchronously (before the removal loop) to match the comment's intent:
// Reset live @AppStorage before clearing raw UserDefaults
NotificationCenter.default.post(name: .resetOnboardingRequested, object: nil)

// Clear onboarding-related UserDefaults keys
for key in onboardingKeys { ... }
  1. Or, keeping the current order and correcting the comment to reflect what actually happens:
// Clear raw UserDefaults first, then reset live @AppStorage via notification
// so any subsequent @AppStorage write-backs use 0/false (the desired reset state).
DispatchQueue.main.async {
    NotificationCenter.default.post(name: .resetOnboardingRequested, object: nil)
}

Comment on lines 2511 to +2512
DispatchQueue.global(qos: .utility).async { [self] in
Thread.sleep(forTimeInterval: 0.15)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thread.sleep blocks a GCD thread pool worker

Thread.sleep(forTimeInterval: 0.15) ties up a thread in the shared GCD utility thread pool for 150 ms. While this runs just before termination and is unlikely to cause starvation in practice, it is an unconventional pattern when DispatchQueue.main.asyncAfter (or a Task { try await Task.sleep(...) }) would express the delay without blocking a pool thread.

DispatchQueue.main.asyncAfter(deadline: .now() + 0.15) { [self] in
    self.restartApp()
}

This also removes the need for DispatchQueue.global(qos: .utility).async wrapping.

Comment on lines +206 to +212
.onReceive(NotificationCenter.default.publisher(for: .resetOnboardingRequested)) { _ in
log("DesktopHomeView: resetOnboardingRequested — clearing live onboarding state for current app")
appState.hasCompletedOnboarding = false
onboardingStep = 0
onboardingJustCompleted = false
appState.stopTranscription()
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@AppStorage writes recreate the UserDefaults keys that were just removed

resetOnboardingAndRestart() removes "onboardingStep" and "onboardingJustCompleted" from UserDefaults synchronously. On the very next run-loop iteration, this handler fires and the @AppStorage setter assignments immediately write those same keys back into UserDefaults with their default values (0 / false).

The net result is functionally identical to having removed them — both paths produce the default value on next read. However, it is worth noting that after the notification fires the keys will be present in UserDefaults (value 0/false) rather than absent. If the intent is strictly "keys absent after reset," the two explicit @AppStorage writes here are unnecessary since @AppStorage returns the declared default automatically when a key is missing.

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