Skip to content

feat: onboarding screen for macOS permission setup - #54

Merged
quiet-node merged 7 commits into
mainfrom
worktree-kind-swinging-engelbart
Apr 7, 2026
Merged

feat: onboarding screen for macOS permission setup#54
quiet-node merged 7 commits into
mainfrom
worktree-kind-swinging-engelbart

Conversation

@quiet-node

Copy link
Copy Markdown
Owner

Summary

  • Adds a two-step onboarding screen shown at first launch when Accessibility or Screen Recording permissions are missing
  • Step 1 polls for Accessibility grant after directing the user to System Settings (no restart needed)
  • Step 2 registers Thuki in TCC via CGRequestScreenCaptureAccess, polls for the TCC grant via CGWindowListCopyWindowInfo, then prompts Quit & Reopen since macOS requires a process restart for Screen Recording to take effect
  • Onboarding window uses NSPanel with shadow and a drag region so it can be repositioned over System Settings
  • Full 100% test coverage (438 frontend tests, Rust unit tests for needs_onboarding and all permission predicates)

What's new

Backend (src-tauri/src/permissions.rs):

  • needs_onboarding(accessibility, screen_recording) -> bool — pure predicate, fully unit tested
  • check_accessibility_permission / open_accessibility_settings
  • check_screen_recording_permission / open_screen_recording_settings / request_screen_recording_access
  • check_screen_recording_tcc_granted — polls via CGWindowListCopyWindowInfo (works pre-restart, unlike CGPreflightScreenCaptureAccess)
  • quit_and_relaunch — uses app_handle.restart() for proper bundle relaunch

Frontend (src/view/OnboardingView.tsx):

  • Sequential two-step permission flow with animated StepCard components
  • mountedRef + in-flight guards prevent stale state updates after unmount
  • Warm Ambient dark theme with orange radial glow, transparent window for rounded corners

Test plan

  • Launch app without Accessibility permission — step 1 is active, step 2 dimmed
  • Grant Accessibility in System Settings — step 1 turns green, step 2 activates automatically
  • Click "Open Screen Recording Settings" — System Settings opens, polling spinner appears
  • Toggle Screen Recording on — "Quit & Reopen Thuki" button appears
  • Click "Quit & Reopen" — app restarts and lands on main overlay (both permissions granted)
  • Launch app with both permissions already granted — onboarding is skipped entirely

🤖 Generated with Claude Code

quiet-node and others added 7 commits April 6, 2026 16:57
Shows a Warm Ambient onboarding screen (dark base, orange radial glow,
rounded panel) on first launch whenever Accessibility or Screen Recording
permissions have not been granted.

Permission flow:
- Step 1: Accessibility - opens System Settings, polls every 500ms until
  granted, advances automatically (no restart needed)
- Step 2: Screen Recording - calls CGRequestScreenCaptureAccess to register
  Thuki in TCC, then opens the Screen Recording settings pane directly,
  polls CGWindowListCopyWindowInfo to detect the grant pre-restart, shows
  Quit & Reopen only after grant is confirmed

Key implementation details:
- activator.rs: suppress native Accessibility popup at startup (prompt=false)
- permissions.rs: new module with needs_onboarding pure logic + 7 Tauri
  commands (check/open/request for both permissions, tcc poll, quit+relaunch)
- lib.rs: show_onboarding_window resizes/centers/shows panel at normal level
  (0) with native shadow re-enabled for tight window + professional shadow;
  notify_frontend_ready gates on permission check before showing overlay
- OnboardingView.tsx: sequential two-step UI with polling state machines,
  drag region on logo/title area, transparent outer container for native
  rounded corners, CTAButton with brightness-only hover effect
- 100% test coverage maintained (frontend + backend)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
- Replace placeholder SVG icon with actual Thuki bear logo (128x128.png)
- Add KeyChip component for macOS-style key visualization (⌃ + ⌃)
- Remove "Two quick steps" subtitle and Step 1/Step 2 badges
- Show Granted badge only when accessibility is confirmed
- Update Accessibility description to show double-tap hotkey with key chips
- Update Screen Recording description to mention /screen and full screen capture
- Increase title bottom margin for better spacing without subtitle

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
…nt updates

- Add axInFlightRef and screenInFlightRef to prevent concurrent invoke calls
  when a poll tick fires before the previous one resolves
- Add mountedRef to skip state updates from in-flight promises that resolve
  after the component unmounts
- Fix typo: "response" -> "respond" in Accessibility description
- Remove dead 'orange' and 'muted' Badge color variants (Step badges removed)
- Add tests for unmount-during-polling cleanup on both permission flows

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
mountedRef was initialized once with useRef(true) but only ever set to
false in the effect cleanup — never reset back to true. On any unmount
followed by remount (conditional render, React StrictMode double-invoke),
the ref stays false permanently and all polling state updates are silently
dropped, leaving the component frozen.

Fix: reset mountedRef.current = true at the top of the effect body so
each mount starts with a clean guard regardless of prior lifecycle cycles.

Also replace the weak not.toThrow() assertions in the unmount tests with
console.error spies. The previous assertions would have passed even without
the fix because React state-update-after-unmount warnings are console.error
calls, not thrown exceptions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
- Fix StepCard done={false} → done={screenGranted} so step 2 turns green
  once screen recording is granted in TCC
- Remove + separator between ⌃⌃ key chips (double-tap, not a chord)
- Guard handleOpenScreenRecording against post-unmount state updates by
  checking mountedRef before setScreenRecordingStatus and setInterval
- Add CTAButton hover/mouseLeave branch tests to maintain 100% coverage

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
- Bump vite from 8.0.3 to 8.0.5 to resolve two high-severity CVEs
  (GHSA-v2wj-q39q-566r, GHSA-p9ff-h696-f583)
- Add 6 tests that exercise the defensive early-return guards:
    - mountedRef check in initial accessibility check (line 188)
    - axInFlightRef guard that blocks concurrent poll ticks (line 204)
    - mountedRef check inside ax poll callback (line 208)
    - mountedRef check before screen-recording poll setup (line 225)
    - screenInFlightRef guard that blocks concurrent TCC checks (line 228)
    - mountedRef check inside screen-recording poll callback (line 234)
  Each test uses a deferred promise to keep the relevant invoke call
  in-flight long enough to trigger the guard

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
vitest resolves its own copy of vite at 8.0.3 (in the vulnerable
8.0.0-8.0.4 range). Adding vite to the overrides map forces every
transitive resolution to 8.0.5, clearing all high-severity CVEs
reported by bun audit.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
@quiet-node
quiet-node merged commit d42ae2a into main Apr 7, 2026
3 checks passed
@quiet-node
quiet-node deleted the worktree-kind-swinging-engelbart branch April 7, 2026 03:12
quiet-node added a commit that referenced this pull request Apr 10, 2026
* feat: add premium onboarding screen for macOS permission setup

Shows a Warm Ambient onboarding screen (dark base, orange radial glow,
rounded panel) on first launch whenever Accessibility or Screen Recording
permissions have not been granted.

Permission flow:
- Step 1: Accessibility - opens System Settings, polls every 500ms until
  granted, advances automatically (no restart needed)
- Step 2: Screen Recording - calls CGRequestScreenCaptureAccess to register
  Thuki in TCC, then opens the Screen Recording settings pane directly,
  polls CGWindowListCopyWindowInfo to detect the grant pre-restart, shows
  Quit & Reopen only after grant is confirmed

Key implementation details:
- activator.rs: suppress native Accessibility popup at startup (prompt=false)
- permissions.rs: new module with needs_onboarding pure logic + 7 Tauri
  commands (check/open/request for both permissions, tcc poll, quit+relaunch)
- lib.rs: show_onboarding_window resizes/centers/shows panel at normal level
  (0) with native shadow re-enabled for tight window + professional shadow;
  notify_frontend_ready gates on permission check before showing overlay
- OnboardingView.tsx: sequential two-step UI with polling state machines,
  drag region on logo/title area, transparent outer container for native
  rounded corners, CTAButton with brightness-only hover effect
- 100% test coverage maintained (frontend + backend)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>

* feat: polish onboarding UI with real logo and key chips

- Replace placeholder SVG icon with actual Thuki bear logo (128x128.png)
- Add KeyChip component for macOS-style key visualization (⌃ + ⌃)
- Remove "Two quick steps" subtitle and Step 1/Step 2 badges
- Show Granted badge only when accessibility is confirmed
- Update Accessibility description to show double-tap hotkey with key chips
- Update Screen Recording description to mention /screen and full screen capture
- Increase title bottom margin for better spacing without subtitle

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>

* fix: guard polling intervals against in-flight overlap and post-unmount updates

- Add axInFlightRef and screenInFlightRef to prevent concurrent invoke calls
  when a poll tick fires before the previous one resolves
- Add mountedRef to skip state updates from in-flight promises that resolve
  after the component unmounts
- Fix typo: "response" -> "respond" in Accessibility description
- Remove dead 'orange' and 'muted' Badge color variants (Step badges removed)
- Add tests for unmount-during-polling cleanup on both permission flows

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>

* fix: reset mountedRef on remount and strengthen unmount tests

mountedRef was initialized once with useRef(true) but only ever set to
false in the effect cleanup — never reset back to true. On any unmount
followed by remount (conditional render, React StrictMode double-invoke),
the ref stays false permanently and all polling state updates are silently
dropped, leaving the component frozen.

Fix: reset mountedRef.current = true at the top of the effect body so
each mount starts with a clean guard regardless of prior lifecycle cycles.

Also replace the weak not.toThrow() assertions in the unmount tests with
console.error spies. The previous assertions would have passed even without
the fix because React state-update-after-unmount warnings are console.error
calls, not thrown exceptions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>

* fix(onboarding): address all code review issues

- Fix StepCard done={false} → done={screenGranted} so step 2 turns green
  once screen recording is granted in TCC
- Remove + separator between ⌃⌃ key chips (double-tap, not a chord)
- Guard handleOpenScreenRecording against post-unmount state updates by
  checking mountedRef before setScreenRecordingStatus and setInterval
- Add CTAButton hover/mouseLeave branch tests to maintain 100% coverage

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>

* fix: update vite to 8.0.5 and achieve 100% branch coverage

- Bump vite from 8.0.3 to 8.0.5 to resolve two high-severity CVEs
  (GHSA-v2wj-q39q-566r, GHSA-p9ff-h696-f583)
- Add 6 tests that exercise the defensive early-return guards:
    - mountedRef check in initial accessibility check (line 188)
    - axInFlightRef guard that blocks concurrent poll ticks (line 204)
    - mountedRef check inside ax poll callback (line 208)
    - mountedRef check before screen-recording poll setup (line 225)
    - screenInFlightRef guard that blocks concurrent TCC checks (line 228)
    - mountedRef check inside screen-recording poll callback (line 234)
  Each test uses a deferred promise to keep the relevant invoke call
  in-flight long enough to trigger the guard

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>

* fix: override vite to >=8.0.5 for all transitive dependents

vitest resolves its own copy of vite at 8.0.3 (in the vulnerable
8.0.0-8.0.4 range). Adding vite to the overrides map forces every
transitive resolution to 8.0.5, clearing all high-severity CVEs
reported by bun audit.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>

---------

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
quiet-node added a commit that referenced this pull request Apr 10, 2026
* feat: add premium onboarding screen for macOS permission setup

Shows a Warm Ambient onboarding screen (dark base, orange radial glow,
rounded panel) on first launch whenever Accessibility or Screen Recording
permissions have not been granted.

Permission flow:
- Step 1: Accessibility - opens System Settings, polls every 500ms until
  granted, advances automatically (no restart needed)
- Step 2: Screen Recording - calls CGRequestScreenCaptureAccess to register
  Thuki in TCC, then opens the Screen Recording settings pane directly,
  polls CGWindowListCopyWindowInfo to detect the grant pre-restart, shows
  Quit & Reopen only after grant is confirmed

Key implementation details:
- activator.rs: suppress native Accessibility popup at startup (prompt=false)
- permissions.rs: new module with needs_onboarding pure logic + 7 Tauri
  commands (check/open/request for both permissions, tcc poll, quit+relaunch)
- lib.rs: show_onboarding_window resizes/centers/shows panel at normal level
  (0) with native shadow re-enabled for tight window + professional shadow;
  notify_frontend_ready gates on permission check before showing overlay
- OnboardingView.tsx: sequential two-step UI with polling state machines,
  drag region on logo/title area, transparent outer container for native
  rounded corners, CTAButton with brightness-only hover effect
- 100% test coverage maintained (frontend + backend)

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>

* feat: polish onboarding UI with real logo and key chips

- Replace placeholder SVG icon with actual Thuki bear logo (128x128.png)
- Add KeyChip component for macOS-style key visualization (⌃ + ⌃)
- Remove "Two quick steps" subtitle and Step 1/Step 2 badges
- Show Granted badge only when accessibility is confirmed
- Update Accessibility description to show double-tap hotkey with key chips
- Update Screen Recording description to mention /screen and full screen capture
- Increase title bottom margin for better spacing without subtitle

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>

* fix: guard polling intervals against in-flight overlap and post-unmount updates

- Add axInFlightRef and screenInFlightRef to prevent concurrent invoke calls
  when a poll tick fires before the previous one resolves
- Add mountedRef to skip state updates from in-flight promises that resolve
  after the component unmounts
- Fix typo: "response" -> "respond" in Accessibility description
- Remove dead 'orange' and 'muted' Badge color variants (Step badges removed)
- Add tests for unmount-during-polling cleanup on both permission flows

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>

* fix: reset mountedRef on remount and strengthen unmount tests

mountedRef was initialized once with useRef(true) but only ever set to
false in the effect cleanup — never reset back to true. On any unmount
followed by remount (conditional render, React StrictMode double-invoke),
the ref stays false permanently and all polling state updates are silently
dropped, leaving the component frozen.

Fix: reset mountedRef.current = true at the top of the effect body so
each mount starts with a clean guard regardless of prior lifecycle cycles.

Also replace the weak not.toThrow() assertions in the unmount tests with
console.error spies. The previous assertions would have passed even without
the fix because React state-update-after-unmount warnings are console.error
calls, not thrown exceptions.

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>

* fix(onboarding): address all code review issues

- Fix StepCard done={false} → done={screenGranted} so step 2 turns green
  once screen recording is granted in TCC
- Remove + separator between ⌃⌃ key chips (double-tap, not a chord)
- Guard handleOpenScreenRecording against post-unmount state updates by
  checking mountedRef before setScreenRecordingStatus and setInterval
- Add CTAButton hover/mouseLeave branch tests to maintain 100% coverage

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>

* fix: update vite to 8.0.5 and achieve 100% branch coverage

- Bump vite from 8.0.3 to 8.0.5 to resolve two high-severity CVEs
  (GHSA-v2wj-q39q-566r, GHSA-p9ff-h696-f583)
- Add 6 tests that exercise the defensive early-return guards:
    - mountedRef check in initial accessibility check (line 188)
    - axInFlightRef guard that blocks concurrent poll ticks (line 204)
    - mountedRef check inside ax poll callback (line 208)
    - mountedRef check before screen-recording poll setup (line 225)
    - screenInFlightRef guard that blocks concurrent TCC checks (line 228)
    - mountedRef check inside screen-recording poll callback (line 234)
  Each test uses a deferred promise to keep the relevant invoke call
  in-flight long enough to trigger the guard

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>

* fix: override vite to >=8.0.5 for all transitive dependents

vitest resolves its own copy of vite at 8.0.3 (in the vulnerable
8.0.0-8.0.4 range). Adding vite to the overrides map forces every
transitive resolution to 8.0.5, clearing all high-severity CVEs
reported by bun audit.

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>

---------

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
quiet-node added a commit that referenced this pull request Apr 11, 2026
* feat: add premium onboarding screen for macOS permission setup

Shows a Warm Ambient onboarding screen (dark base, orange radial glow,
rounded panel) on first launch whenever Accessibility or Screen Recording
permissions have not been granted.

Permission flow:
- Step 1: Accessibility - opens System Settings, polls every 500ms until
  granted, advances automatically (no restart needed)
- Step 2: Screen Recording - calls CGRequestScreenCaptureAccess to register
  Thuki in TCC, then opens the Screen Recording settings pane directly,
  polls CGWindowListCopyWindowInfo to detect the grant pre-restart, shows
  Quit & Reopen only after grant is confirmed

Key implementation details:
- activator.rs: suppress native Accessibility popup at startup (prompt=false)
- permissions.rs: new module with needs_onboarding pure logic + 7 Tauri
  commands (check/open/request for both permissions, tcc poll, quit+relaunch)
- lib.rs: show_onboarding_window resizes/centers/shows panel at normal level
  (0) with native shadow re-enabled for tight window + professional shadow;
  notify_frontend_ready gates on permission check before showing overlay
- OnboardingView.tsx: sequential two-step UI with polling state machines,
  drag region on logo/title area, transparent outer container for native
  rounded corners, CTAButton with brightness-only hover effect
- 100% test coverage maintained (frontend + backend)

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>

* feat: polish onboarding UI with real logo and key chips

- Replace placeholder SVG icon with actual Thuki bear logo (128x128.png)
- Add KeyChip component for macOS-style key visualization (⌃ + ⌃)
- Remove "Two quick steps" subtitle and Step 1/Step 2 badges
- Show Granted badge only when accessibility is confirmed
- Update Accessibility description to show double-tap hotkey with key chips
- Update Screen Recording description to mention /screen and full screen capture
- Increase title bottom margin for better spacing without subtitle

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>

* fix: guard polling intervals against in-flight overlap and post-unmount updates

- Add axInFlightRef and screenInFlightRef to prevent concurrent invoke calls
  when a poll tick fires before the previous one resolves
- Add mountedRef to skip state updates from in-flight promises that resolve
  after the component unmounts
- Fix typo: "response" -> "respond" in Accessibility description
- Remove dead 'orange' and 'muted' Badge color variants (Step badges removed)
- Add tests for unmount-during-polling cleanup on both permission flows

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>

* fix: reset mountedRef on remount and strengthen unmount tests

mountedRef was initialized once with useRef(true) but only ever set to
false in the effect cleanup — never reset back to true. On any unmount
followed by remount (conditional render, React StrictMode double-invoke),
the ref stays false permanently and all polling state updates are silently
dropped, leaving the component frozen.

Fix: reset mountedRef.current = true at the top of the effect body so
each mount starts with a clean guard regardless of prior lifecycle cycles.

Also replace the weak not.toThrow() assertions in the unmount tests with
console.error spies. The previous assertions would have passed even without
the fix because React state-update-after-unmount warnings are console.error
calls, not thrown exceptions.

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>

* fix(onboarding): address all code review issues

- Fix StepCard done={false} → done={screenGranted} so step 2 turns green
  once screen recording is granted in TCC
- Remove + separator between ⌃⌃ key chips (double-tap, not a chord)
- Guard handleOpenScreenRecording against post-unmount state updates by
  checking mountedRef before setScreenRecordingStatus and setInterval
- Add CTAButton hover/mouseLeave branch tests to maintain 100% coverage

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>

* fix: update vite to 8.0.5 and achieve 100% branch coverage

- Bump vite from 8.0.3 to 8.0.5 to resolve two high-severity CVEs
  (GHSA-v2wj-q39q-566r, GHSA-p9ff-h696-f583)
- Add 6 tests that exercise the defensive early-return guards:
    - mountedRef check in initial accessibility check (line 188)
    - axInFlightRef guard that blocks concurrent poll ticks (line 204)
    - mountedRef check inside ax poll callback (line 208)
    - mountedRef check before screen-recording poll setup (line 225)
    - screenInFlightRef guard that blocks concurrent TCC checks (line 228)
    - mountedRef check inside screen-recording poll callback (line 234)
  Each test uses a deferred promise to keep the relevant invoke call
  in-flight long enough to trigger the guard

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>

* fix: override vite to >=8.0.5 for all transitive dependents

vitest resolves its own copy of vite at 8.0.3 (in the vulnerable
8.0.0-8.0.4 range). Adding vite to the overrides map forces every
transitive resolution to 8.0.5, clearing all high-severity CVEs
reported by bun audit.

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>

---------

Signed-off-by: Logan Nguyen <lg.131.dev@gmail.com>
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