Skip to content

fix: stop the permission-required screen flashing at launch when access is granted - #54

Merged
brendan-ch merged 2 commits into
mainfrom
feat/fix-permission-screen-flicker
Jul 21, 2026
Merged

fix: stop the permission-required screen flashing at launch when access is granted#54
brendan-ch merged 2 commits into
mainfrom
feat/fix-permission-screen-flicker

Conversation

@brendan-ch

@brendan-ch brendan-ch commented Jul 21, 2026

Copy link
Copy Markdown
Owner

Problem

When launching the app with Screen Time access already granted, the ScreenTimeAccessRequiredView flashes briefly before landing on MainView.

Root cause

FamilyControls loads authorization asynchronously and reports a stale .notDetermined for the first moments after a cold launch — even for a user who granted access (Apple Developer Forums 747915). RootView captured that stale value once at init (ScreenTimeAuthorization.init) and only corrected it when the scene became active, so RootDestination.resolve mapped the transient non-.approved status to .screenTimeAccessRequired for a frame or two before flipping to .main — the visible flicker.

Onboarding only completes on .approved, so a post-onboarding .notDetermined at launch is almost always this stale value settling to .approved.

Fix

Treat .notDetermined as .main. Only .denied routes to .screenTimeAccessRequired:

  • RootDestination.resolve returns .main for every post-onboarding status except .denied. The overwhelmingly common launch (access already granted, momentarily reported as a stale .notDetermined) shows the real app immediately, with no flash of the access-required screen. A genuinely revoked user may see .main briefly at launch before the status settles to .denied and flips them to the access-required screen — a rare case where a momentary flicker is acceptable.
  • ScreenTimeAuthorization.resolveAtLaunch() stays: it re-reads while the status is .notDetermined (bounded ~500ms) so a revoked user's .denied surfaces promptly rather than waiting for the next foreground.
  • Removes the now-unneeded resolving machinery: the .resolvingAuthorization case, LaunchResolvingView, and ScreenTimeAuthorization.hasResolvedStatus.

Test plan

  • RootDestinationTests — non-.denied post-onboarding statuses (.approved, .notDetermined) route to .main; .denied routes to the access-required screen; onboarding-incomplete always routes to onboarding.
  • ScreenTimeAuthorizationTestsresolveAtLaunch settles past a stale launch-time .notDetermined to a revoked user's real .denied; gives up gracefully when the status never settles.
  • Full unit-test target green: 353 tests in 61 suites passed.
  • On-device manual check (FamilyControls async behavior doesn't reproduce on Simulator): cold-launch with access granted → main flow immediately, no access-required flash; with access revoked → main flow briefly, then access-required.

🤖 Generated with Claude Code

brendan-ch and others added 2 commits July 21, 2026 01:43
…s granted

FamilyControls loads authorization asynchronously and reports a stale
`.notDetermined` for the first moments after a cold launch, even for a user
who granted Screen Time access. `RootView` read that stale value once at init
and only corrected it on the scene becoming active, so it rendered
`ScreenTimeAccessRequiredView` for a frame or two before flipping to
`MainView` — the visible flicker.

Distinguish "not yet resolved" from "resolved and not approved":

- `ScreenTimeAuthorization` gains `hasResolvedStatus` and `resolveAtLaunch()`,
  which re-reads the status while it stays `.notDetermined`, giving the
  framework a brief bounded window to settle before giving up. A definitive
  `.approved`/`.denied` resolves immediately (no wait, no splash).
- `RootDestination` routes a not-yet-approved, not-yet-resolved status to a
  new `.resolvingAuthorization` case instead of the access-required screen.
- `RootView` shows a neutral `LaunchResolvingView` (the app logo) while
  resolving and calls `resolveAtLaunch()` in `.task` at launch.

`.approved` is still trusted immediately — stale reads under-report as
`.notDetermined`, they never falsely approve — so granted access goes
straight to `MainView` with no flash.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…mined

Treat .notDetermined as .main instead of holding on a resolving screen, so
the common launch (access granted, reported as a stale .notDetermined while
FamilyControls loads) shows the real app with no flicker. Only .denied now
routes to the access-required screen; a genuinely revoked user may see .main
briefly before the status settles to .denied — an accepted rare-case flicker.

Removes the now-unneeded resolving machinery: the .resolvingAuthorization
case, LaunchResolvingView, and ScreenTimeAuthorization.hasResolvedStatus.
resolveAtLaunch() stays to settle a revoked user's .denied promptly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@brendan-ch
brendan-ch marked this pull request as ready for review July 21, 2026 20:10
@brendan-ch
brendan-ch merged commit 9705cb3 into main Jul 21, 2026
2 checks passed
@brendan-ch
brendan-ch deleted the feat/fix-permission-screen-flicker branch July 21, 2026 20:12
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