RuleEnforcer.refresh only shielded a limit rule once its budget was spent, so it tore down the background's proactive open-limit "turnstile" shield (the shield is how opens are counted) whenever the app was foregrounded, and a freshly created open-limit rule did nothing until the next midnight reset. refresh now applies the same proactive gate as LimitEnforcement.handleDayStart: an enabled, scheduled-today, un-paused open-limit rule is shielded even before its budget is spent, so it takes effect immediately. A new app-group OpenSessionStore records a granted "Open" session's expiry so neither enforcement path re-shields (and cuts short) a sanctioned ~15-minute session. Overlapping rules already enforce strictly: each rule shields its own ManagedSettingsStore and Screen Time unions them, so an app is blocked if any covering rule blocks it. Document that model in the spec (§4.8) and lock it in with tests, including a time limit blocking during an open-limit's granted session and a daily opens reset.
73 lines
2.4 KiB
Swift
73 lines
2.4 KiB
Swift
//
|
|
// OpenAppLockApp.swift
|
|
// OpenAppLock
|
|
//
|
|
// Created by Brendan Chen on 2025.08.09.
|
|
//
|
|
|
|
import SwiftData
|
|
import SwiftUI
|
|
|
|
@main
|
|
struct OpenAppLockApp: App {
|
|
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, AppList.self])
|
|
let modelConfiguration = ModelConfiguration(
|
|
schema: schema, isStoredInMemoryOnly: config.isUITesting
|
|
)
|
|
do {
|
|
container = try ModelContainer(for: schema, configurations: [modelConfiguration])
|
|
} catch {
|
|
fatalError("Could not create ModelContainer: \(error)")
|
|
}
|
|
|
|
let usageLedger: UsageReading = config.isUITesting ? MockUsageLedger() : UsageLedger()
|
|
|
|
if let scenario = config.seedScenario {
|
|
SampleRules.seed(
|
|
scenario, into: container.mainContext, usage: usageLedger as? MockUsageLedger
|
|
)
|
|
}
|
|
AppListMigration.run(in: 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()
|
|
let scheduler =
|
|
config.isUITesting
|
|
? nil : RuleScheduler(monitor: DeviceActivityCenterMonitor())
|
|
let openSessions: OpenSessionReading =
|
|
config.isUITesting ? MockOpenSessionStore() : OpenSessionStore()
|
|
_enforcer = State(
|
|
initialValue: RuleEnforcer(
|
|
shields: shields, usage: usageLedger, scheduler: scheduler,
|
|
openSessions: openSessions))
|
|
}
|
|
|
|
var body: some Scene {
|
|
WindowGroup {
|
|
RootView()
|
|
.environment(authorization)
|
|
.environment(enforcer)
|
|
}
|
|
.modelContainer(container)
|
|
}
|
|
}
|