feat: clone Opal's rules feature with hard block enforcement

Rebuild the app around recurring screen-time blocking rules modeled on
Opal's My Apps tab:

- Onboarding with FamilyControls Screen Time authorization
- Apps home with Blocked Apps tiles and live rule cards
- New Rule sheet: Schedule/Time Limit/Open Limit types + preset gallery
- Rule editors: time windows (incl. overnight), day picker, app selection
  via FamilyActivityPicker (Block/Allow Only), rename, Hold to Commit
- Hard Mode: active hard rules cannot be edited, disabled, deleted, or
  unblocked until their window ends; soft rules pause until next window
- Shield enforcement through per-rule ManagedSettingsStore + RuleEnforcer
- 73 unit tests (Swift Testing) + 16 UI tests (XCUITest) with launch-arg
  harness for in-memory storage, mocked authorization, seeded scenarios
- docs/RULES_FEATURE_SPEC.md: spec derived from the reference recording

Known gap: background window transitions and time/open-limit thresholds
need a DeviceActivityMonitor extension; shields currently sync while the
app is running.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-06-12 12:35:52 -04:00
parent 0c03e35ea9
commit e6c87baeba
45 changed files with 3749 additions and 206 deletions

View File

@@ -5,28 +5,57 @@
// Created by Brendan Chen on 2025.08.09.
//
import SwiftUI
import SwiftData
import SwiftUI
@main
struct SeveredApp: App {
var sharedModelContainer: ModelContainer = {
let schema = Schema([
Item.self,
])
let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false)
private let container: ModelContainer
@State private var authorization: ScreenTimeAuthorization
@State private var enforcer: RuleEnforcer
init() {
let config = LaunchConfiguration.current
if let onboardingCompleted = config.onboardingCompleted {
UserDefaults.standard.set(onboardingCompleted, forKey: "hasCompletedOnboarding")
}
let schema = Schema([BlockingRule.self])
let modelConfiguration = ModelConfiguration(
schema: schema, isStoredInMemoryOnly: config.isUITesting
)
do {
return try ModelContainer(for: schema, configurations: [modelConfiguration])
container = try ModelContainer(for: schema, configurations: [modelConfiguration])
} catch {
fatalError("Could not create ModelContainer: \(error)")
}
}()
if let scenario = config.seedScenario {
SampleRules.seed(scenario, into: container.mainContext)
}
let authProvider: AuthorizationProviding =
config.isUITesting
? MockAuthorizationProvider(
status: config.onboardingCompleted == false ? .notDetermined : .approved
)
: FamilyControlsAuthorizationProvider()
_authorization = State(initialValue: ScreenTimeAuthorization(provider: authProvider))
let shields: ShieldApplying =
config.isUITesting ? MockShieldController() : ManagedSettingsShieldController()
_enforcer = State(initialValue: RuleEnforcer(shields: shields))
}
var body: some Scene {
WindowGroup {
ContentView()
RootView()
.environment(authorization)
.environment(enforcer)
.preferredColorScheme(.dark)
.tint(Theme.accent)
}
.modelContainer(sharedModelContainer)
.modelContainer(container)
}
}