Skip to content

fix: enforce uninstall protection in background and lock its toggle during hard sessions#5

Merged
brendan-ch merged 2 commits into
mainfrom
fix/uninstall-protection-background-and-lock
Jun 15, 2026
Merged

fix: enforce uninstall protection in background and lock its toggle during hard sessions#5
brendan-ch merged 2 commits into
mainfrom
fix/uninstall-protection-background-and-lock

Conversation

@brendan-ch

Copy link
Copy Markdown
Owner

Summary

Fixes two defects in Uninstall Protection (Settings → "Uninstall Protection", which denies device app removal while a Hard Mode rule is actively blocking, so a hard block can't be escaped by uninstalling OpenAppLock):

  1. Background gap (confirmed). App-removal denial was only recomputed on the foreground path (RuleEnforcer.refresh), so a Hard Mode window that started while the app was closed left the device removable (an escape hatch defeating the feature), and one that ended left protection stuck on until the next foreground. This was documented as a known v1 limitation in the spec.
  2. Toggle editable mid-block. The Settings toggle had no gating and could be turned off while a Hard Mode rule was actively blocking.

What changed

  • Shared keyAppGroup.uninstallProtectionKey so the Screen Time extensions can read the opt-in; AppSettingsStore points at it.
  • Snapshot policy — new UninstallProtectionPolicy (in Shared/) mirrors RulePolicy's active/hard-locked semantics exactly (including the scheduled-today check for limit rules). A parity unit test asserts it never drifts from RulePolicy.
  • Background enforcer — new UninstallProtectionEnforcer.reconcile() recomputes denial from the snapshots + opt-in. Called from the DeviceActivity monitor (interval start/end, usage threshold) and the ShieldAction extension (after a granted open), so denial tracks hard blocks even while the app is closed. ShieldApplying is left unchanged.
  • Toggle lock — while any Hard Mode rule is actively blocking, the Settings switch is replaced by a red lock.fill (mirroring the Home "Currently Blocking" treatment) with an explanatory notice, gated by RulePolicy.canToggleUninstallProtection. The binding setter is also guarded as defense in depth.
  • SpecRULES_FEATURE_SPEC.md §6 / §6.1 updated to document both paths and the locked toggle.

Test plan

  • Unit tests: snapshot-policy scenarios + RulePolicy parity, background enforcer reconcile, canToggleUninstallProtection.
  • UI test: SettingsUITests.testUninstallProtectionLockedDuringHardSession (switch hidden + lock/notice shown under the hard-mode-active scenario); existing flip test still passes.
  • Full suite green (218/218) via Xcode MCP.
  • Manual UI validation in the simulator (locked vs. unlocked Settings states confirmed visually).
  • On-device / TestFlight: the actual background denyAppRemoval transition when a hard window starts/ends with the app closed is only observable on a real device (the simulator uses mock shields and delivers no DeviceActivity callbacks).

🤖 Generated with Claude Code

brendan-ch and others added 2 commits June 14, 2026 18:27
…uring hard sessions

Two defects in Uninstall Protection:

1. Background gap — app-removal denial was only recomputed on the foreground
   path (RuleEnforcer.refresh), so a Hard Mode window that started or ended
   while the app was closed left denyAppRemoval out of sync (an escape hatch
   on start; stuck-on after end).
2. The Settings toggle could be turned off mid-block, defeating the feature.

Changes:
- Add AppGroup.uninstallProtectionKey so the extensions can read the opt-in;
  point AppSettingsStore at it.
- Add snapshot-based UninstallProtectionPolicy (mirrors RulePolicy's
  active/hard-locked semantics, including scheduled-today for limit rules;
  a parity unit test guards against drift) and UninstallProtectionEnforcer.
  Call reconcile() from the DeviceActivity monitor (interval start/end, usage
  threshold) and ShieldAction (after a granted open) so denial tracks hard
  blocks even while the app is closed.
- Lock the Settings toggle while any Hard Mode rule is actively blocking: the
  switch is replaced by a red lock (mirrors Home's "Currently Blocking" rows)
  via RulePolicy.canToggleUninstallProtection, with an explanatory notice;
  guard the binding setter as defense in depth.
- Update RULES_FEATURE_SPEC §6/§6.1; add unit + UI tests (218 passing).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-and-lock

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@brendan-ch brendan-ch merged commit 6aa319e into main Jun 15, 2026
1 check passed
@brendan-ch brendan-ch deleted the fix/uninstall-protection-background-and-lock branch June 15, 2026 00:49
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