Skip to content

feat: guided permissions onboarding for Accessibility and Screen Recording - #84

Open
iOSDevSK wants to merge 1 commit into
KartikLabhshetwar:mainfrom
iOSDevSK:feat/permissions-onboarding
Open

feat: guided permissions onboarding for Accessibility and Screen Recording#84
iOSDevSK wants to merge 1 commit into
KartikLabhshetwar:mainfrom
iOSDevSK:feat/permissions-onboarding

Conversation

@iOSDevSK

Copy link
Copy Markdown

What

Replaces the silent accessibility polling on first launch with a welcome window that walks the user through both required permissions — no manual hunting through System Settings.

How it works

  • Fires the native system prompts (AXIsProcessTrustedWithOptions / CGRequestScreenCaptureAccess), which auto-add the app to the Privacy & Security lists, and deep-links to the exact Settings pane (x-apple.systempreferences:...Privacy_Accessibility / Privacy_ScreenCapture)
  • Polls TCC state and shows live checkmarks; keyboard shortcuts activate the moment Accessibility lands — no restart needed
  • CGPreflightScreenCaptureAccess caches its answer for the process lifetime, so a Screen Recording grant made while the app runs never turns true — the window probes ScreenCaptureKit directly to detect the grant and restarts the app automatically when required
  • Stale TCC entries left by a previous build (whose code signature no longer matches) silently break permissions, and the system prompt won't fire again because the app is "already in the list" — they're reset via tccutil reset so the fresh prompt + auto-add works
  • Window closes itself once both permissions are granted

Scope

Single file (BetterShotDelegate.swift) + CHANGELOG.md under Unreleased. No version bump.

Testing

  • make release builds clean
  • Manually verified from a clean TCC state (tccutil reset): both prompts fire, Settings opens on the right pane, checkmarks appear within seconds of flipping the toggles, shortcuts work without restart after Accessibility, and the app auto-restarts after Screen Recording

Note: on the newest Xcode toolchains main itself doesn't compile — see #80 for minimal Swift 6 concurrency fixes. This PR is independent of it.

🤖 Generated with Claude Code

…rding

Replaces the silent accessibility polling with a welcome window that
walks the user through both required permissions:

- Fires the native system prompts, which auto-add the app to the
  Privacy & Security lists, and deep-links to the exact System
  Settings pane - no manual hunting
- Polls TCC state and shows live checkmarks; shortcuts activate the
  moment Accessibility lands, no restart needed
- Probes ScreenCaptureKit directly to detect a Screen Recording grant
  (CGPreflightScreenCaptureAccess caches its answer for the process
  lifetime) and restarts the app automatically when required
- Stale entries left by a previous build (whose code signature no
  longer matches) are reset via tccutil, so the fresh prompt works
  instead of silently failing

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 14, 2026

Copy link
Copy Markdown

Someone is attempting to deploy a commit to the knox projects Team on Vercel.

A member of the Team first needs to authorize it.

}
}

private func runTCCReset(service: String, bundleID: String) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

tccutil reset is run on the main actor here, and waitUntilExit() can block the UI (and potentially the onboarding window) longer than needed.

Suggested change
private func runTCCReset(service: String, bundleID: String) {
private func runTCCReset(service: String, bundleID: String) {
Task.detached(priority: .utility) {
let process = Process()
process.executableURL = URL(fileURLWithPath: "/usr/bin/tccutil")
process.arguments = ["reset", service, bundleID]
do {
try process.run()
process.waitUntilExit()
} catch { }
}
}

NSApp.activate(ignoringOtherApps: true)
window?.makeKeyAndOrderFront(nil)

startPolling()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Minor UX: if Accessibility is already granted when this window opens, shortcuts won’t get registered until the first poll tick (up to 1s). You can register immediately before starting the timer.

Suggested change
startPolling()
if accessibilityGranted && !ShortcutService.shared.isRegistered {
ShortcutService.shared.registerAll()
}
startPolling()

}
}

static func restartApp() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Small consistency thing: elsewhere you use executableURL, but restartApp() uses launchPath (deprecated). Suggest switching to executableURL.

Suggested change
static func restartApp() {
static func restartApp() {
let task = Process()
task.executableURL = URL(fileURLWithPath: "/bin/sh")
task.arguments = ["-c", "sleep 0.5; open \"\(Bundle.main.bundlePath)\""]
try? task.run()
NSApp.terminate(nil)
}

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