Add accessibility onboarding flow - #9
Conversation
Replace the old accessibility check that force-quit the app with a guided onboarding window. When permissions are missing, a SwiftUI window walks users through enabling accessibility in System Settings. Polls for permission grant and automatically proceeds to normal operation once trusted.
Greptile SummaryThis PR replaces the old hard Key changes and findings:
Confidence Score: 3/5
Important Files Changed
Sequence DiagramsequenceDiagram
participant App as applicationDidFinishLaunching
participant Del as EMRAppDelegate
participant Bridge as AccessibilityOnboardingBridge
participant Timer as Poll Timer (1s)
participant Sys as System Settings
App->>Del: applicationDidFinishLaunching
Del->>Del: AXIsProcessTrusted()?
alt Permission granted
Del->>Del: setupEventTapAndObservers()
else Permission missing
Del->>Bridge: alloc/init (creates NSWindow)
Del->>Bridge: setOnPermissionGranted: block
Del->>Bridge: showWindow()
Del->>Bridge: startPolling()
Bridge->>Timer: scheduledTimer(1s, repeats:true)
loop Every 1 second
Timer->>Bridge: AXIsProcessTrusted()?
Bridge-->>Timer: false → keep polling
end
Note over Bridge,Sys: User clicks "Open System Settings"
Bridge->>Sys: NSWorkspace.open(settingsURL)
Note over Sys: User enables Zooom3 in Accessibility
Timer->>Bridge: AXIsProcessTrusted() → true
Bridge->>Bridge: stopPolling()
Bridge->>Del: onPermissionGranted() [dispatch_async main]
Del->>Bridge: closeWindow() → stopPolling + orderOut
Del->>Del: onboardingBridge = nil
Del->>Del: setupEventTapAndObservers()
end
Note over Del: Permission later revoked
Del->>Del: myCGEventCallback: kCGEventTapDisabledByTimeout/UserInput
Del->>Del: dispatch_async → handlePermissionRevoked()
Del->>Del: removeObserver (workspace), teardown event tap
Del->>Bridge: showAccessibilityOnboarding() → new bridge
Prompt To Fix All With AIThis is a comment left during a code review.
Path: zooom3/EMRAppDelegate.m
Line: 562-574
Comment:
**Orphaned onboarding window when `showAccessibilityOnboarding` is called while already onboarding**
`showAccessibilityOnboarding` unconditionally replaces `onboardingBridge` without first dismissing the existing window. When `handlePermissionRevoked` is called a second time while onboarding is already active (e.g., multiple `kCGEventTapDisabledByTimeout` events queuing up on the main thread before the first `dispatch_async` is processed), ARC releases the old bridge and its `deinit` only calls `stopPolling()` — there is no `window.orderOut(nil)`. The old window remains visible while the new bridge immediately calls `showWindow()`, stacking a second onboarding window on top of the first.
Add an early return guard at the top of `showAccessibilityOnboarding`:
```objc
- (void)showAccessibilityOnboarding {
if (onboardingBridge != nil) {
// Already onboarding — just bring the existing window to front
if (@available(macOS 13.0, *)) {
[onboardingBridge showWindow];
}
return;
}
if (@available(macOS 13.0, *)) {
onboardingBridge = [[AccessibilityOnboardingBridge alloc] init];
__weak typeof(self) weakSelf = self;
[onboardingBridge setOnPermissionGranted:^{
dispatch_async(dispatch_get_main_queue(), ^{
[weakSelf accessibilityPermissionGranted];
});
}];
[onboardingBridge showWindow];
[onboardingBridge startPolling];
}
}
```
How can I resolve this? If you propose a fix, please make it concise.
---
This is a comment left during a code review.
Path: zooom3/AccessibilityOnboardingView.swift
Line: 21-24
Comment:
**`foregroundColor(_:)` is deprecated in favour of `foregroundStyle(_:)`**
Both uses of `.foregroundColor(.secondary)` (lines 22 and 49) trigger a deprecation warning on macOS 14+. The replacement is `.foregroundStyle(.secondary)`, which also supports multi-layer styles available since macOS 13 — matching the deployment target.
```suggestion
.foregroundStyle(.secondary)
```
How can I resolve this? If you propose a fix, please make it concise.
---
This is a comment left during a code review.
Path: zooom3/AccessibilityOnboardingView.swift
Line: 47-50
Comment:
**Second `foregroundColor(.secondary)` also deprecated**
Same deprecation as above — the `"Waiting for permission..."` label also uses `.foregroundColor(.secondary)`:
```suggestion
.foregroundStyle(.secondary)
```
How can I resolve this? If you propose a fix, please make it concise.Last reviewed commit: "Fix duplicate worksp..." |
When permissions were revoked while the app was running, the event tap callback would enter a tight loop re-enabling the tap that the system kept disabling. Now checks AXIsProcessTrusted() before re-enabling and gracefully tears down the tap, showing the onboarding flow to re-grant permission. Includes tests for the revocation handling path.
…init, fix deprecation
…ant cycle handlePermissionRevoked now removes workspace session observers before they get re-registered by setupEventTapAndObservers on permission re-grant.
| - (void)showAccessibilityOnboarding { | ||
| if (@available(macOS 13.0, *)) { | ||
| onboardingBridge = [[AccessibilityOnboardingBridge alloc] init]; | ||
| __weak typeof(self) weakSelf = self; | ||
| [onboardingBridge setOnPermissionGranted:^{ | ||
| dispatch_async(dispatch_get_main_queue(), ^{ | ||
| [weakSelf accessibilityPermissionGranted]; | ||
| }); | ||
| }]; | ||
| [onboardingBridge showWindow]; | ||
| [onboardingBridge startPolling]; | ||
| } | ||
| } |
There was a problem hiding this comment.
Orphaned onboarding window when
showAccessibilityOnboarding is called while already onboarding
showAccessibilityOnboarding unconditionally replaces onboardingBridge without first dismissing the existing window. When handlePermissionRevoked is called a second time while onboarding is already active (e.g., multiple kCGEventTapDisabledByTimeout events queuing up on the main thread before the first dispatch_async is processed), ARC releases the old bridge and its deinit only calls stopPolling() — there is no window.orderOut(nil). The old window remains visible while the new bridge immediately calls showWindow(), stacking a second onboarding window on top of the first.
Add an early return guard at the top of showAccessibilityOnboarding:
- (void)showAccessibilityOnboarding {
if (onboardingBridge != nil) {
// Already onboarding — just bring the existing window to front
if (@available(macOS 13.0, *)) {
[onboardingBridge showWindow];
}
return;
}
if (@available(macOS 13.0, *)) {
onboardingBridge = [[AccessibilityOnboardingBridge alloc] init];
__weak typeof(self) weakSelf = self;
[onboardingBridge setOnPermissionGranted:^{
dispatch_async(dispatch_get_main_queue(), ^{
[weakSelf accessibilityPermissionGranted];
});
}];
[onboardingBridge showWindow];
[onboardingBridge startPolling];
}
}Prompt To Fix With AI
This is a comment left during a code review.
Path: zooom3/EMRAppDelegate.m
Line: 562-574
Comment:
**Orphaned onboarding window when `showAccessibilityOnboarding` is called while already onboarding**
`showAccessibilityOnboarding` unconditionally replaces `onboardingBridge` without first dismissing the existing window. When `handlePermissionRevoked` is called a second time while onboarding is already active (e.g., multiple `kCGEventTapDisabledByTimeout` events queuing up on the main thread before the first `dispatch_async` is processed), ARC releases the old bridge and its `deinit` only calls `stopPolling()` — there is no `window.orderOut(nil)`. The old window remains visible while the new bridge immediately calls `showWindow()`, stacking a second onboarding window on top of the first.
Add an early return guard at the top of `showAccessibilityOnboarding`:
```objc
- (void)showAccessibilityOnboarding {
if (onboardingBridge != nil) {
// Already onboarding — just bring the existing window to front
if (@available(macOS 13.0, *)) {
[onboardingBridge showWindow];
}
return;
}
if (@available(macOS 13.0, *)) {
onboardingBridge = [[AccessibilityOnboardingBridge alloc] init];
__weak typeof(self) weakSelf = self;
[onboardingBridge setOnPermissionGranted:^{
dispatch_async(dispatch_get_main_queue(), ^{
[weakSelf accessibilityPermissionGranted];
});
}];
[onboardingBridge showWindow];
[onboardingBridge startPolling];
}
}
```
How can I resolve this? If you propose a fix, please make it concise.
Summary
AccessibilityOnboardingViewwalks users through enabling accessibility permissions in System Settings, with a direct deep link and screenshotTest plan
AccessibilityOnboardingTests— verify all tests pass🤖 Generated with Claude Code