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.
This commit is contained in:
65
Severed/Logic/RulePolicy.swift
Normal file
65
Severed/Logic/RulePolicy.swift
Normal file
@@ -0,0 +1,65 @@
|
||||
//
|
||||
// RulePolicy.swift
|
||||
// Severed
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Gates every mutation of a rule. This is where Hard Mode is enforced:
|
||||
/// while a hard-mode rule is actively blocking, nothing about it can be
|
||||
/// weakened until the window ends.
|
||||
enum RulePolicy {
|
||||
/// True while the rule is actively blocking with Hard Mode on.
|
||||
static func isHardLocked(
|
||||
_ rule: BlockingRule, at now: Date = .now, calendar: Calendar = .current
|
||||
) -> Bool {
|
||||
rule.hardMode && rule.status(at: now, calendar: calendar).isActive
|
||||
}
|
||||
|
||||
static func canEdit(
|
||||
_ rule: BlockingRule, at now: Date = .now, calendar: Calendar = .current
|
||||
) -> Bool {
|
||||
!isHardLocked(rule, at: now, calendar: calendar)
|
||||
}
|
||||
|
||||
static func canDisable(
|
||||
_ rule: BlockingRule, at now: Date = .now, calendar: Calendar = .current
|
||||
) -> Bool {
|
||||
!isHardLocked(rule, at: now, calendar: calendar)
|
||||
}
|
||||
|
||||
static func canDelete(
|
||||
_ rule: BlockingRule, at now: Date = .now, calendar: Calendar = .current
|
||||
) -> Bool {
|
||||
!isHardLocked(rule, at: now, calendar: calendar)
|
||||
}
|
||||
|
||||
/// Whether the user may lift the current block early ("Unblock").
|
||||
/// Requires an active window and Hard Mode off.
|
||||
static func canUnblock(
|
||||
_ rule: BlockingRule, at now: Date = .now, calendar: Calendar = .current
|
||||
) -> Bool {
|
||||
rule.status(at: now, calendar: calendar).isActive && !rule.hardMode
|
||||
}
|
||||
|
||||
/// Hard Mode can always be turned on, but never off while the rule is
|
||||
/// actively blocking — that is the whole point of a hard block.
|
||||
static func canTurnOffHardMode(
|
||||
_ rule: BlockingRule, at now: Date = .now, calendar: Calendar = .current
|
||||
) -> Bool {
|
||||
!isHardLocked(rule, at: now, calendar: calendar)
|
||||
}
|
||||
|
||||
/// Pauses the rule's current window. Returns false (and changes nothing)
|
||||
/// when unblocking is not allowed.
|
||||
@discardableResult
|
||||
static func unblock(
|
||||
_ rule: BlockingRule, at now: Date = .now, calendar: Calendar = .current
|
||||
) -> Bool {
|
||||
guard canUnblock(rule, at: now, calendar: calendar),
|
||||
let window = rule.schedule.activeWindow(containing: now, calendar: calendar)
|
||||
else { return false }
|
||||
rule.pausedUntil = window.end
|
||||
return true
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user