refactor: rename app from Severed to OpenAppLock

Full rename ahead of App Store submission: project, targets, bundle
identifiers (dev.bchen.Severed -> dev.bchen.OpenAppLock), entitlements
file, app entry point, user-facing onboarding strings, test helpers,
and docs. Verified: all 62 unit tests pass.
This commit is contained in:
2026-06-12 18:05:06 -04:00
parent af919c1585
commit e91d8344a9
42 changed files with 145 additions and 145 deletions

View File

@@ -1,6 +1,6 @@
# Severed — Agent Guide # OpenAppLock — Agent Guide
Severed is an iOS Screen Time app: recurring **rules** that block selected OpenAppLock is an iOS Screen Time app: recurring **rules** that block selected
apps (Schedule windows, Time Limits, Open Limits), with a **Hard Mode** that apps (Schedule windows, Time Limits, Open Limits), with a **Hard Mode** that
makes an active block impossible to lift, edit, or delete until it ends. The makes an active block impossible to lift, edit, or delete until it ends. The
feature set is a clone of Opal's "Rules"; the presentation is bare native iOS feature set is a clone of Opal's "Rules"; the presentation is bare native iOS
@@ -9,7 +9,7 @@ feature set is a clone of Opal's "Rules"; the presentation is bare native iOS
## Repo layout ## Repo layout
``` ```
Severed/ App target (iOS 26, SwiftUI + SwiftData) OpenAppLock/ App target (iOS 26, SwiftUI + SwiftData)
Models/ BlockingRule (@Model), RuleDraft, RuleKind, Models/ BlockingRule (@Model), RuleDraft, RuleKind,
Weekday, RulePreset Weekday, RulePreset
Logic/ Pure, heavily unit-tested: Logic/ Pure, heavily unit-tested:
@@ -22,9 +22,9 @@ Severed/ App target (iOS 26, SwiftUI + SwiftData)
RuleEnforcer (active rules → shields), RuleEnforcer (active rules → shields),
LaunchConfiguration + SampleRules (UI-test harness) LaunchConfiguration + SampleRules (UI-test harness)
Views/ Native SwiftUI screens (see docs spec §6) Views/ Native SwiftUI screens (see docs spec §6)
SeveredTests/ Swift Testing unit suites (@MainActor — the app OpenAppLockTests/ Swift Testing unit suites (@MainActor — the app
target defaults to MainActor isolation) target defaults to MainActor isolation)
SeveredUITests/ XCUITest flows (see harness below) OpenAppLockUITests/ XCUITest flows (see harness below)
docs/RULES_FEATURE_SPEC.md Feature spec derived from the Opal reference docs/RULES_FEATURE_SPEC.md Feature spec derived from the Opal reference
recording; §6 maps it to the native presentation. recording; §6 maps it to the native presentation.
Review/update this BEFORE behavior changes. Review/update this BEFORE behavior changes.
@@ -50,14 +50,14 @@ docs/RULES_FEATURE_SPEC.md Feature spec derived from the Opal reference
## Build & test ## Build & test
- Open `Severed.xcodeproj` in Xcode; build/test through the **Xcode MCP** - Open `OpenAppLock.xcodeproj` in Xcode; build/test through the **Xcode MCP**
tools (`BuildProject`, `RunAllTests`, `RunSomeTests` — get the tab id from tools (`BuildProject`, `RunAllTests`, `RunSomeTests` — get the tab id from
`XcodeListWindows`). Make sure the scheme destination is an iOS `XcodeListWindows`). Make sure the scheme destination is an iOS
**simulator**; a physical-device destination makes test runs hang or get **simulator**; a physical-device destination makes test runs hang or get
cancelled. cancelled.
- The project uses Xcode file-system-synchronized groups: adding/removing - The project uses Xcode file-system-synchronized groups: adding/removing
`.swift` files on disk is enough, no pbxproj editing. `.swift` files on disk is enough, no pbxproj editing.
- Family Controls entitlement is configured (`Severed/Severed.entitlements`). - Family Controls entitlement is configured (`OpenAppLock/OpenAppLock.entitlements`).
FamilyControls/ManagedSettings compile and run on the simulator, but real FamilyControls/ManagedSettings compile and run on the simulator, but real
blocking behavior is only observable on a device. blocking behavior is only observable on a device.
@@ -72,7 +72,7 @@ docs/RULES_FEATURE_SPEC.md Feature spec derived from the Opal reference
## UI-test harness ## UI-test harness
`SeveredApp` reads launch arguments (parsed by `LaunchConfiguration`): `OpenAppLockApp` reads launch arguments (parsed by `LaunchConfiguration`):
| Argument | Effect | | Argument | Effect |
|---|---| |---|---|
@@ -81,7 +81,7 @@ docs/RULES_FEATURE_SPEC.md Feature spec derived from the Opal reference
| `-seed-scenario=standard` | Active soft rule "Work Time" + upcoming "Sleep" | | `-seed-scenario=standard` | Active soft rule "Work Time" + upcoming "Sleep" |
| `-seed-scenario=hard-mode-active` | Active Hard Mode rule "Locked In" + upcoming "Sleep" | | `-seed-scenario=hard-mode-active` | Active Hard Mode rule "Locked In" + upcoming "Sleep" |
Use `XCUIApplication.launchSevered(...)` (UITestSupport.swift), which also Use `XCUIApplication.launchOpenAppLock(...)` (UITestSupport.swift), which also
provides `app.element(_:)` for identifier lookup across element types and provides `app.element(_:)` for identifier lookup across element types and
`waitToAppear()`. `waitToAppear()`.

View File

@@ -1,4 +1,4 @@
# Severed # OpenAppLock
All project context for agents lives in [AGENTS.md](AGENTS.md) — read that All project context for agents lives in [AGENTS.md](AGENTS.md) — read that
first. Feature behavior is specified in first. Feature behavior is specified in

View File

@@ -12,37 +12,37 @@
containerPortal = 20A7EDC62E47B7CD0097608D /* Project object */; containerPortal = 20A7EDC62E47B7CD0097608D /* Project object */;
proxyType = 1; proxyType = 1;
remoteGlobalIDString = 20A7EDCD2E47B7CD0097608D; remoteGlobalIDString = 20A7EDCD2E47B7CD0097608D;
remoteInfo = Severed; remoteInfo = OpenAppLock;
}; };
20A7EDE82E47B7CF0097608D /* PBXContainerItemProxy */ = { 20A7EDE82E47B7CF0097608D /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy; isa = PBXContainerItemProxy;
containerPortal = 20A7EDC62E47B7CD0097608D /* Project object */; containerPortal = 20A7EDC62E47B7CD0097608D /* Project object */;
proxyType = 1; proxyType = 1;
remoteGlobalIDString = 20A7EDCD2E47B7CD0097608D; remoteGlobalIDString = 20A7EDCD2E47B7CD0097608D;
remoteInfo = Severed; remoteInfo = OpenAppLock;
}; };
/* End PBXContainerItemProxy section */ /* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
20A7EDCE2E47B7CD0097608D /* Severed.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Severed.app; sourceTree = BUILT_PRODUCTS_DIR; }; 20A7EDCE2E47B7CD0097608D /* OpenAppLock.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = OpenAppLock.app; sourceTree = BUILT_PRODUCTS_DIR; };
20A7EDDD2E47B7CF0097608D /* SeveredTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SeveredTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 20A7EDDD2E47B7CF0097608D /* OpenAppLockTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = OpenAppLockTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
20A7EDE72E47B7CF0097608D /* SeveredUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SeveredUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 20A7EDE72E47B7CF0097608D /* OpenAppLockUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = OpenAppLockUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFileSystemSynchronizedRootGroup section */
20A7EDD02E47B7CD0097608D /* Severed */ = { 20A7EDD02E47B7CD0097608D /* OpenAppLock */ = {
isa = PBXFileSystemSynchronizedRootGroup; isa = PBXFileSystemSynchronizedRootGroup;
path = Severed; path = OpenAppLock;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
20A7EDE02E47B7CF0097608D /* SeveredTests */ = { 20A7EDE02E47B7CF0097608D /* OpenAppLockTests */ = {
isa = PBXFileSystemSynchronizedRootGroup; isa = PBXFileSystemSynchronizedRootGroup;
path = SeveredTests; path = OpenAppLockTests;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
20A7EDEA2E47B7CF0097608D /* SeveredUITests */ = { 20A7EDEA2E47B7CF0097608D /* OpenAppLockUITests */ = {
isa = PBXFileSystemSynchronizedRootGroup; isa = PBXFileSystemSynchronizedRootGroup;
path = SeveredUITests; path = OpenAppLockUITests;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
/* End PBXFileSystemSynchronizedRootGroup section */ /* End PBXFileSystemSynchronizedRootGroup section */
@@ -75,9 +75,9 @@
20A7EDC52E47B7CD0097608D = { 20A7EDC52E47B7CD0097608D = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
20A7EDD02E47B7CD0097608D /* Severed */, 20A7EDD02E47B7CD0097608D /* OpenAppLock */,
20A7EDE02E47B7CF0097608D /* SeveredTests */, 20A7EDE02E47B7CF0097608D /* OpenAppLockTests */,
20A7EDEA2E47B7CF0097608D /* SeveredUITests */, 20A7EDEA2E47B7CF0097608D /* OpenAppLockUITests */,
20A7EDCF2E47B7CD0097608D /* Products */, 20A7EDCF2E47B7CD0097608D /* Products */,
); );
sourceTree = "<group>"; sourceTree = "<group>";
@@ -85,9 +85,9 @@
20A7EDCF2E47B7CD0097608D /* Products */ = { 20A7EDCF2E47B7CD0097608D /* Products */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
20A7EDCE2E47B7CD0097608D /* Severed.app */, 20A7EDCE2E47B7CD0097608D /* OpenAppLock.app */,
20A7EDDD2E47B7CF0097608D /* SeveredTests.xctest */, 20A7EDDD2E47B7CF0097608D /* OpenAppLockTests.xctest */,
20A7EDE72E47B7CF0097608D /* SeveredUITests.xctest */, 20A7EDE72E47B7CF0097608D /* OpenAppLockUITests.xctest */,
); );
name = Products; name = Products;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -95,9 +95,9 @@
/* End PBXGroup section */ /* End PBXGroup section */
/* Begin PBXNativeTarget section */ /* Begin PBXNativeTarget section */
20A7EDCD2E47B7CD0097608D /* Severed */ = { 20A7EDCD2E47B7CD0097608D /* OpenAppLock */ = {
isa = PBXNativeTarget; isa = PBXNativeTarget;
buildConfigurationList = 20A7EDF12E47B7CF0097608D /* Build configuration list for PBXNativeTarget "Severed" */; buildConfigurationList = 20A7EDF12E47B7CF0097608D /* Build configuration list for PBXNativeTarget "OpenAppLock" */;
buildPhases = ( buildPhases = (
20A7EDCA2E47B7CD0097608D /* Sources */, 20A7EDCA2E47B7CD0097608D /* Sources */,
20A7EDCB2E47B7CD0097608D /* Frameworks */, 20A7EDCB2E47B7CD0097608D /* Frameworks */,
@@ -108,18 +108,18 @@
dependencies = ( dependencies = (
); );
fileSystemSynchronizedGroups = ( fileSystemSynchronizedGroups = (
20A7EDD02E47B7CD0097608D /* Severed */, 20A7EDD02E47B7CD0097608D /* OpenAppLock */,
); );
name = Severed; name = OpenAppLock;
packageProductDependencies = ( packageProductDependencies = (
); );
productName = Severed; productName = OpenAppLock;
productReference = 20A7EDCE2E47B7CD0097608D /* Severed.app */; productReference = 20A7EDCE2E47B7CD0097608D /* OpenAppLock.app */;
productType = "com.apple.product-type.application"; productType = "com.apple.product-type.application";
}; };
20A7EDDC2E47B7CF0097608D /* SeveredTests */ = { 20A7EDDC2E47B7CF0097608D /* OpenAppLockTests */ = {
isa = PBXNativeTarget; isa = PBXNativeTarget;
buildConfigurationList = 20A7EDF42E47B7CF0097608D /* Build configuration list for PBXNativeTarget "SeveredTests" */; buildConfigurationList = 20A7EDF42E47B7CF0097608D /* Build configuration list for PBXNativeTarget "OpenAppLockTests" */;
buildPhases = ( buildPhases = (
20A7EDD92E47B7CF0097608D /* Sources */, 20A7EDD92E47B7CF0097608D /* Sources */,
20A7EDDA2E47B7CF0097608D /* Frameworks */, 20A7EDDA2E47B7CF0097608D /* Frameworks */,
@@ -131,18 +131,18 @@
20A7EDDF2E47B7CF0097608D /* PBXTargetDependency */, 20A7EDDF2E47B7CF0097608D /* PBXTargetDependency */,
); );
fileSystemSynchronizedGroups = ( fileSystemSynchronizedGroups = (
20A7EDE02E47B7CF0097608D /* SeveredTests */, 20A7EDE02E47B7CF0097608D /* OpenAppLockTests */,
); );
name = SeveredTests; name = OpenAppLockTests;
packageProductDependencies = ( packageProductDependencies = (
); );
productName = SeveredTests; productName = OpenAppLockTests;
productReference = 20A7EDDD2E47B7CF0097608D /* SeveredTests.xctest */; productReference = 20A7EDDD2E47B7CF0097608D /* OpenAppLockTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test"; productType = "com.apple.product-type.bundle.unit-test";
}; };
20A7EDE62E47B7CF0097608D /* SeveredUITests */ = { 20A7EDE62E47B7CF0097608D /* OpenAppLockUITests */ = {
isa = PBXNativeTarget; isa = PBXNativeTarget;
buildConfigurationList = 20A7EDF72E47B7CF0097608D /* Build configuration list for PBXNativeTarget "SeveredUITests" */; buildConfigurationList = 20A7EDF72E47B7CF0097608D /* Build configuration list for PBXNativeTarget "OpenAppLockUITests" */;
buildPhases = ( buildPhases = (
20A7EDE32E47B7CF0097608D /* Sources */, 20A7EDE32E47B7CF0097608D /* Sources */,
20A7EDE42E47B7CF0097608D /* Frameworks */, 20A7EDE42E47B7CF0097608D /* Frameworks */,
@@ -154,13 +154,13 @@
20A7EDE92E47B7CF0097608D /* PBXTargetDependency */, 20A7EDE92E47B7CF0097608D /* PBXTargetDependency */,
); );
fileSystemSynchronizedGroups = ( fileSystemSynchronizedGroups = (
20A7EDEA2E47B7CF0097608D /* SeveredUITests */, 20A7EDEA2E47B7CF0097608D /* OpenAppLockUITests */,
); );
name = SeveredUITests; name = OpenAppLockUITests;
packageProductDependencies = ( packageProductDependencies = (
); );
productName = SeveredUITests; productName = OpenAppLockUITests;
productReference = 20A7EDE72E47B7CF0097608D /* SeveredUITests.xctest */; productReference = 20A7EDE72E47B7CF0097608D /* OpenAppLockUITests.xctest */;
productType = "com.apple.product-type.bundle.ui-testing"; productType = "com.apple.product-type.bundle.ui-testing";
}; };
/* End PBXNativeTarget section */ /* End PBXNativeTarget section */
@@ -186,7 +186,7 @@
}; };
}; };
}; };
buildConfigurationList = 20A7EDC92E47B7CD0097608D /* Build configuration list for PBXProject "Severed" */; buildConfigurationList = 20A7EDC92E47B7CD0097608D /* Build configuration list for PBXProject "OpenAppLock" */;
developmentRegion = en; developmentRegion = en;
hasScannedForEncodings = 0; hasScannedForEncodings = 0;
knownRegions = ( knownRegions = (
@@ -200,9 +200,9 @@
projectDirPath = ""; projectDirPath = "";
projectRoot = ""; projectRoot = "";
targets = ( targets = (
20A7EDCD2E47B7CD0097608D /* Severed */, 20A7EDCD2E47B7CD0097608D /* OpenAppLock */,
20A7EDDC2E47B7CF0097608D /* SeveredTests */, 20A7EDDC2E47B7CF0097608D /* OpenAppLockTests */,
20A7EDE62E47B7CF0097608D /* SeveredUITests */, 20A7EDE62E47B7CF0097608D /* OpenAppLockUITests */,
); );
}; };
/* End PBXProject section */ /* End PBXProject section */
@@ -258,12 +258,12 @@
/* Begin PBXTargetDependency section */ /* Begin PBXTargetDependency section */
20A7EDDF2E47B7CF0097608D /* PBXTargetDependency */ = { 20A7EDDF2E47B7CF0097608D /* PBXTargetDependency */ = {
isa = PBXTargetDependency; isa = PBXTargetDependency;
target = 20A7EDCD2E47B7CD0097608D /* Severed */; target = 20A7EDCD2E47B7CD0097608D /* OpenAppLock */;
targetProxy = 20A7EDDE2E47B7CF0097608D /* PBXContainerItemProxy */; targetProxy = 20A7EDDE2E47B7CF0097608D /* PBXContainerItemProxy */;
}; };
20A7EDE92E47B7CF0097608D /* PBXTargetDependency */ = { 20A7EDE92E47B7CF0097608D /* PBXTargetDependency */ = {
isa = PBXTargetDependency; isa = PBXTargetDependency;
target = 20A7EDCD2E47B7CD0097608D /* Severed */; target = 20A7EDCD2E47B7CD0097608D /* OpenAppLock */;
targetProxy = 20A7EDE82E47B7CF0097608D /* PBXContainerItemProxy */; targetProxy = 20A7EDE82E47B7CF0097608D /* PBXContainerItemProxy */;
}; };
/* End PBXTargetDependency section */ /* End PBXTargetDependency section */
@@ -390,7 +390,7 @@
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = Severed/Severed.entitlements; CODE_SIGN_ENTITLEMENTS = OpenAppLock/OpenAppLock.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 4A9XHUS87Q; DEVELOPMENT_TEAM = 4A9XHUS87Q;
@@ -414,7 +414,7 @@
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 15.6; MACOSX_DEPLOYMENT_TARGET = 15.6;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = dev.bchen.Severed; PRODUCT_BUNDLE_IDENTIFIER = dev.bchen.OpenAppLock;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
REGISTER_APP_GROUPS = YES; REGISTER_APP_GROUPS = YES;
SDKROOT = auto; SDKROOT = auto;
@@ -433,7 +433,7 @@
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = Severed/Severed.entitlements; CODE_SIGN_ENTITLEMENTS = OpenAppLock/OpenAppLock.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 4A9XHUS87Q; DEVELOPMENT_TEAM = 4A9XHUS87Q;
@@ -457,7 +457,7 @@
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 15.6; MACOSX_DEPLOYMENT_TARGET = 15.6;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = dev.bchen.Severed; PRODUCT_BUNDLE_IDENTIFIER = dev.bchen.OpenAppLock;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
REGISTER_APP_GROUPS = YES; REGISTER_APP_GROUPS = YES;
SDKROOT = auto; SDKROOT = auto;
@@ -482,7 +482,7 @@
IPHONEOS_DEPLOYMENT_TARGET = 26.0; IPHONEOS_DEPLOYMENT_TARGET = 26.0;
MACOSX_DEPLOYMENT_TARGET = 15.6; MACOSX_DEPLOYMENT_TARGET = 15.6;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = dev.bchen.SeveredTests; PRODUCT_BUNDLE_IDENTIFIER = dev.bchen.OpenAppLockTests;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = auto; SDKROOT = auto;
STRING_CATALOG_GENERATE_SYMBOLS = NO; STRING_CATALOG_GENERATE_SYMBOLS = NO;
@@ -491,7 +491,7 @@
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2"; TARGETED_DEVICE_FAMILY = "1,2";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Severed.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Severed"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/OpenAppLock.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/OpenAppLock";
XROS_DEPLOYMENT_TARGET = 26.0; XROS_DEPLOYMENT_TARGET = 26.0;
}; };
name = Debug; name = Debug;
@@ -507,7 +507,7 @@
IPHONEOS_DEPLOYMENT_TARGET = 26.0; IPHONEOS_DEPLOYMENT_TARGET = 26.0;
MACOSX_DEPLOYMENT_TARGET = 15.6; MACOSX_DEPLOYMENT_TARGET = 15.6;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = dev.bchen.SeveredTests; PRODUCT_BUNDLE_IDENTIFIER = dev.bchen.OpenAppLockTests;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = auto; SDKROOT = auto;
STRING_CATALOG_GENERATE_SYMBOLS = NO; STRING_CATALOG_GENERATE_SYMBOLS = NO;
@@ -516,7 +516,7 @@
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2"; TARGETED_DEVICE_FAMILY = "1,2";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Severed.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Severed"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/OpenAppLock.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/OpenAppLock";
XROS_DEPLOYMENT_TARGET = 26.0; XROS_DEPLOYMENT_TARGET = 26.0;
}; };
name = Release; name = Release;
@@ -531,7 +531,7 @@
IPHONEOS_DEPLOYMENT_TARGET = 26.0; IPHONEOS_DEPLOYMENT_TARGET = 26.0;
MACOSX_DEPLOYMENT_TARGET = 15.6; MACOSX_DEPLOYMENT_TARGET = 15.6;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = dev.bchen.SeveredUITests; PRODUCT_BUNDLE_IDENTIFIER = dev.bchen.OpenAppLockUITests;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = auto; SDKROOT = auto;
STRING_CATALOG_GENERATE_SYMBOLS = NO; STRING_CATALOG_GENERATE_SYMBOLS = NO;
@@ -540,7 +540,7 @@
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2"; TARGETED_DEVICE_FAMILY = "1,2";
TEST_TARGET_NAME = Severed; TEST_TARGET_NAME = OpenAppLock;
XROS_DEPLOYMENT_TARGET = 26.0; XROS_DEPLOYMENT_TARGET = 26.0;
}; };
name = Debug; name = Debug;
@@ -555,7 +555,7 @@
IPHONEOS_DEPLOYMENT_TARGET = 26.0; IPHONEOS_DEPLOYMENT_TARGET = 26.0;
MACOSX_DEPLOYMENT_TARGET = 15.6; MACOSX_DEPLOYMENT_TARGET = 15.6;
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = dev.bchen.SeveredUITests; PRODUCT_BUNDLE_IDENTIFIER = dev.bchen.OpenAppLockUITests;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = auto; SDKROOT = auto;
STRING_CATALOG_GENERATE_SYMBOLS = NO; STRING_CATALOG_GENERATE_SYMBOLS = NO;
@@ -564,7 +564,7 @@
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2"; TARGETED_DEVICE_FAMILY = "1,2";
TEST_TARGET_NAME = Severed; TEST_TARGET_NAME = OpenAppLock;
XROS_DEPLOYMENT_TARGET = 26.0; XROS_DEPLOYMENT_TARGET = 26.0;
}; };
name = Release; name = Release;
@@ -572,7 +572,7 @@
/* End XCBuildConfiguration section */ /* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */ /* Begin XCConfigurationList section */
20A7EDC92E47B7CD0097608D /* Build configuration list for PBXProject "Severed" */ = { 20A7EDC92E47B7CD0097608D /* Build configuration list for PBXProject "OpenAppLock" */ = {
isa = XCConfigurationList; isa = XCConfigurationList;
buildConfigurations = ( buildConfigurations = (
20A7EDEF2E47B7CF0097608D /* Debug */, 20A7EDEF2E47B7CF0097608D /* Debug */,
@@ -581,7 +581,7 @@
defaultConfigurationIsVisible = 0; defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release; defaultConfigurationName = Release;
}; };
20A7EDF12E47B7CF0097608D /* Build configuration list for PBXNativeTarget "Severed" */ = { 20A7EDF12E47B7CF0097608D /* Build configuration list for PBXNativeTarget "OpenAppLock" */ = {
isa = XCConfigurationList; isa = XCConfigurationList;
buildConfigurations = ( buildConfigurations = (
20A7EDF22E47B7CF0097608D /* Debug */, 20A7EDF22E47B7CF0097608D /* Debug */,
@@ -590,7 +590,7 @@
defaultConfigurationIsVisible = 0; defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release; defaultConfigurationName = Release;
}; };
20A7EDF42E47B7CF0097608D /* Build configuration list for PBXNativeTarget "SeveredTests" */ = { 20A7EDF42E47B7CF0097608D /* Build configuration list for PBXNativeTarget "OpenAppLockTests" */ = {
isa = XCConfigurationList; isa = XCConfigurationList;
buildConfigurations = ( buildConfigurations = (
20A7EDF52E47B7CF0097608D /* Debug */, 20A7EDF52E47B7CF0097608D /* Debug */,
@@ -599,7 +599,7 @@
defaultConfigurationIsVisible = 0; defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release; defaultConfigurationName = Release;
}; };
20A7EDF72E47B7CF0097608D /* Build configuration list for PBXNativeTarget "SeveredUITests" */ = { 20A7EDF72E47B7CF0097608D /* Build configuration list for PBXNativeTarget "OpenAppLockUITests" */ = {
isa = XCConfigurationList; isa = XCConfigurationList;
buildConfigurations = ( buildConfigurations = (
20A7EDF82E47B7CF0097608D /* Debug */, 20A7EDF82E47B7CF0097608D /* Debug */,

View File

@@ -1,6 +1,6 @@
// //
// RulePolicy.swift // RulePolicy.swift
// Severed // OpenAppLock
// //
import Foundation import Foundation

View File

@@ -1,6 +1,6 @@
// //
// RuleSchedule.swift // RuleSchedule.swift
// Severed // OpenAppLock
// //
import Foundation import Foundation

View File

@@ -1,6 +1,6 @@
// //
// RuleStatus.swift // RuleStatus.swift
// Severed // OpenAppLock
// //
import Foundation import Foundation

View File

@@ -1,6 +1,6 @@
// //
// BlockingRule.swift // BlockingRule.swift
// Severed // OpenAppLock
// //
import Foundation import Foundation

View File

@@ -1,6 +1,6 @@
// //
// RuleDraft.swift // RuleDraft.swift
// Severed // OpenAppLock
// //
import Foundation import Foundation

View File

@@ -1,6 +1,6 @@
// //
// RuleKind.swift // RuleKind.swift
// Severed // OpenAppLock
// //
import Foundation import Foundation

View File

@@ -1,6 +1,6 @@
// //
// RulePreset.swift // RulePreset.swift
// Severed // OpenAppLock
// //
import SwiftUI import SwiftUI

View File

@@ -1,6 +1,6 @@
// //
// Weekday.swift // Weekday.swift
// Severed // OpenAppLock
// //
import Foundation import Foundation

View File

@@ -1,6 +1,6 @@
// //
// SeveredApp.swift // OpenAppLockApp.swift
// Severed // OpenAppLock
// //
// Created by Brendan Chen on 2025.08.09. // Created by Brendan Chen on 2025.08.09.
// //
@@ -9,7 +9,7 @@ import SwiftData
import SwiftUI import SwiftUI
@main @main
struct SeveredApp: App { struct OpenAppLockApp: App {
private let container: ModelContainer private let container: ModelContainer
@State private var authorization: ScreenTimeAuthorization @State private var authorization: ScreenTimeAuthorization
@State private var enforcer: RuleEnforcer @State private var enforcer: RuleEnforcer

View File

@@ -1,6 +1,6 @@
// //
// LaunchConfiguration.swift // LaunchConfiguration.swift
// Severed // OpenAppLock
// //
import Foundation import Foundation

View File

@@ -1,6 +1,6 @@
// //
// RuleEnforcer.swift // RuleEnforcer.swift
// Severed // OpenAppLock
// //
import Foundation import Foundation

View File

@@ -1,6 +1,6 @@
// //
// SampleRules.swift // SampleRules.swift
// Severed // OpenAppLock
// //
import Foundation import Foundation

View File

@@ -1,6 +1,6 @@
// //
// ScreenTimeAuthorization.swift // ScreenTimeAuthorization.swift
// Severed // OpenAppLock
// //
import FamilyControls import FamilyControls

View File

@@ -1,6 +1,6 @@
// //
// ShieldController.swift // ShieldController.swift
// Severed // OpenAppLock
// //
import FamilyControls import FamilyControls

View File

@@ -1,6 +1,6 @@
// //
// AppsHomeView.swift // AppsHomeView.swift
// Severed // OpenAppLock
// //
import SwiftData import SwiftData

View File

@@ -1,6 +1,6 @@
// //
// DayOfWeekPicker.swift // DayOfWeekPicker.swift
// Severed // OpenAppLock
// //
import SwiftUI import SwiftUI

View File

@@ -1,6 +1,6 @@
// //
// OnboardingView.swift // OnboardingView.swift
// Severed // OpenAppLock
// //
import SwiftUI import SwiftUI
@@ -39,7 +39,7 @@ struct OnboardingView: View {
Image(systemName: "scissors") Image(systemName: "scissors")
.font(.system(size: 56)) .font(.system(size: 56))
.foregroundStyle(.tint) .foregroundStyle(.tint)
Text("Severed") Text("OpenAppLock")
.font(.largeTitle.bold()) .font(.largeTitle.bold())
Text("Block your most distracting apps with rules that keep you honest — on a schedule, with no way out when you choose Hard Mode.") Text("Block your most distracting apps with rules that keep you honest — on a schedule, with no way out when you choose Hard Mode.")
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
@@ -56,13 +56,13 @@ struct OnboardingView: View {
.font(.title.bold()) .font(.title.bold())
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
VStack(alignment: .leading, spacing: 14) { VStack(alignment: .leading, spacing: 14) {
bullet("shield.fill", "Severed uses Apple's Screen Time framework to block the apps you choose.") bullet("shield.fill", "OpenAppLock uses Apple's Screen Time framework to block the apps you choose.")
bullet("hand.raised.fill", "Your app activity stays on this device and is never collected.") bullet("hand.raised.fill", "Your app activity stays on this device and is never collected.")
bullet("gearshape.fill", "You can change this anytime in Settings.") bullet("gearshape.fill", "You can change this anytime in Settings.")
} }
if authorization.status == .denied || authorization.lastRequestFailed { if authorization.status == .denied || authorization.lastRequestFailed {
VStack(spacing: 10) { VStack(spacing: 10) {
Text("Screen Time access was declined. Severed can't block apps without it.") Text("Screen Time access was declined. OpenAppLock can't block apps without it.")
.font(.footnote) .font(.footnote)
.foregroundStyle(.red) .foregroundStyle(.red)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)

View File

@@ -1,6 +1,6 @@
// //
// RootView.swift // RootView.swift
// Severed // OpenAppLock
// //
import SwiftUI import SwiftUI

View File

@@ -1,6 +1,6 @@
// //
// AppSelectionSheet.swift // AppSelectionSheet.swift
// Severed // OpenAppLock
// //
import FamilyControls import FamilyControls

View File

@@ -1,6 +1,6 @@
// //
// NewRuleSheet.swift // NewRuleSheet.swift
// Severed // OpenAppLock
// //
import SwiftData import SwiftData

View File

@@ -1,6 +1,6 @@
// //
// RuleDetailSheet.swift // RuleDetailSheet.swift
// Severed // OpenAppLock
// //
import SwiftData import SwiftData

View File

@@ -1,6 +1,6 @@
// //
// RuleEditorView.swift // RuleEditorView.swift
// Severed // OpenAppLock
// //
import SwiftUI import SwiftUI

View File

@@ -1,19 +1,19 @@
// //
// LaunchSupportTests.swift // LaunchSupportTests.swift
// SeveredTests // OpenAppLockTests
// //
import Foundation import Foundation
import Testing import Testing
@testable import Severed @testable import OpenAppLock
@MainActor @MainActor
@Suite("Launch configuration parsing") @Suite("Launch configuration parsing")
struct LaunchConfigurationTests { struct LaunchConfigurationTests {
@Test("Defaults are production settings") @Test("Defaults are production settings")
func defaults() { func defaults() {
let config = LaunchConfiguration.parse(arguments: ["SeveredApp"]) let config = LaunchConfiguration.parse(arguments: ["OpenAppLockApp"])
#expect(!config.isUITesting) #expect(!config.isUITesting)
#expect(config.onboardingCompleted == nil) #expect(config.onboardingCompleted == nil)
#expect(config.seedScenario == nil) #expect(config.seedScenario == nil)
@@ -22,7 +22,7 @@ struct LaunchConfigurationTests {
@Test("UI testing flags are recognized") @Test("UI testing flags are recognized")
func uiTestingFlags() { func uiTestingFlags() {
let config = LaunchConfiguration.parse(arguments: [ let config = LaunchConfiguration.parse(arguments: [
"SeveredApp", "-ui-testing", "-onboarding-completed", "-seed-scenario=standard", "OpenAppLockApp", "-ui-testing", "-onboarding-completed", "-seed-scenario=standard",
]) ])
#expect(config.isUITesting) #expect(config.isUITesting)
#expect(config.onboardingCompleted == true) #expect(config.onboardingCompleted == true)
@@ -31,13 +31,13 @@ struct LaunchConfigurationTests {
@Test("Onboarding can be forced on") @Test("Onboarding can be forced on")
func onboardingRequired() { func onboardingRequired() {
let config = LaunchConfiguration.parse(arguments: ["SeveredApp", "-onboarding-required"]) let config = LaunchConfiguration.parse(arguments: ["OpenAppLockApp", "-onboarding-required"])
#expect(config.onboardingCompleted == false) #expect(config.onboardingCompleted == false)
} }
@Test("Unknown seed scenarios are ignored") @Test("Unknown seed scenarios are ignored")
func unknownScenario() { func unknownScenario() {
let config = LaunchConfiguration.parse(arguments: ["SeveredApp", "-seed-scenario=nope"]) let config = LaunchConfiguration.parse(arguments: ["OpenAppLockApp", "-seed-scenario=nope"])
#expect(config.seedScenario == nil) #expect(config.seedScenario == nil)
} }
} }

View File

@@ -1,12 +1,12 @@
// //
// RuleEnforcerTests.swift // RuleEnforcerTests.swift
// SeveredTests // OpenAppLockTests
// //
import Foundation import Foundation
import Testing import Testing
@testable import Severed @testable import OpenAppLock
@MainActor @MainActor
@Suite("Rule enforcement → shields") @Suite("Rule enforcement → shields")

View File

@@ -1,13 +1,13 @@
// //
// RuleModelTests.swift // RuleModelTests.swift
// SeveredTests // OpenAppLockTests
// //
import Foundation import Foundation
import SwiftData import SwiftData
import Testing import Testing
@testable import Severed @testable import OpenAppLock
@MainActor @MainActor
@Suite("BlockingRule model & persistence") @Suite("BlockingRule model & persistence")

View File

@@ -1,12 +1,12 @@
// //
// RulePolicyTests.swift // RulePolicyTests.swift
// SeveredTests // OpenAppLockTests
// //
import Foundation import Foundation
import Testing import Testing
@testable import Severed @testable import OpenAppLock
@MainActor @MainActor
@Suite("Hard Mode policy") @Suite("Hard Mode policy")

View File

@@ -1,12 +1,12 @@
// //
// RuleScheduleTests.swift // RuleScheduleTests.swift
// SeveredTests // OpenAppLockTests
// //
import Foundation import Foundation
import Testing import Testing
@testable import Severed @testable import OpenAppLock
@MainActor @MainActor
@Suite("RuleSchedule window math") @Suite("RuleSchedule window math")

View File

@@ -1,12 +1,12 @@
// //
// RuleStatusTests.swift // RuleStatusTests.swift
// SeveredTests // OpenAppLockTests
// //
import Foundation import Foundation
import Testing import Testing
@testable import Severed @testable import OpenAppLock
@MainActor @MainActor
@Suite("Rule status derivation and labels") @Suite("Rule status derivation and labels")

View File

@@ -1,11 +1,11 @@
// //
// TestSupport.swift // TestSupport.swift
// SeveredTests // OpenAppLockTests
// //
import Foundation import Foundation
@testable import Severed @testable import OpenAppLock
/// Fixed UTC gregorian calendar so schedule math is deterministic regardless /// Fixed UTC gregorian calendar so schedule math is deterministic regardless
/// of the machine running the tests. /// of the machine running the tests.

View File

@@ -1,6 +1,6 @@
// //
// OnboardingUITests.swift // OnboardingUITests.swift
// SeveredUITests // OpenAppLockUITests
// //
import XCTest import XCTest
@@ -11,10 +11,10 @@ final class OnboardingUITests: XCTestCase {
} }
func testOnboardingWalksThroughPermissionToHome() throws { func testOnboardingWalksThroughPermissionToHome() throws {
let app = XCUIApplication.launchSevered(onboardingCompleted: false) let app = XCUIApplication.launchOpenAppLock(onboardingCompleted: false)
// Welcome step. // Welcome step.
app.staticTexts["Severed"].waitToAppear() app.staticTexts["OpenAppLock"].waitToAppear()
app.buttons["onboardingContinueButton"].waitToAppear().tap() app.buttons["onboardingContinueButton"].waitToAppear().tap()
// Permission step: granting (mocked) lands on the Apps home screen. // Permission step: granting (mocked) lands on the Apps home screen.
@@ -24,7 +24,7 @@ final class OnboardingUITests: XCTestCase {
} }
func testCompletedOnboardingIsSkipped() throws { func testCompletedOnboardingIsSkipped() throws {
let app = XCUIApplication.launchSevered(onboardingCompleted: true) let app = XCUIApplication.launchOpenAppLock(onboardingCompleted: true)
app.buttons["newRuleButton"].waitToAppear() app.buttons["newRuleButton"].waitToAppear()
XCTAssertFalse(app.buttons["onboardingContinueButton"].exists) XCTAssertFalse(app.buttons["onboardingContinueButton"].exists)
} }

View File

@@ -1,6 +1,6 @@
// //
// RuleCreationUITests.swift // RuleCreationUITests.swift
// SeveredUITests // OpenAppLockUITests
// //
import XCTest import XCTest
@@ -11,7 +11,7 @@ final class RuleCreationUITests: XCTestCase {
} }
func testCreateScheduleRuleFromTypeCard() throws { func testCreateScheduleRuleFromTypeCard() throws {
let app = XCUIApplication.launchSevered() let app = XCUIApplication.launchOpenAppLock()
app.element("emptyRulesCard").waitToAppear() app.element("emptyRulesCard").waitToAppear()
app.buttons["newRuleButton"].tap() app.buttons["newRuleButton"].tap()
@@ -32,7 +32,7 @@ final class RuleCreationUITests: XCTestCase {
} }
func testCreateRuleFromPreset() throws { func testCreateRuleFromPreset() throws {
let app = XCUIApplication.launchSevered() let app = XCUIApplication.launchOpenAppLock()
app.buttons["newRuleButton"].waitToAppear().tap() app.buttons["newRuleButton"].waitToAppear().tap()
app.buttons["preset-work-time"].waitToAppear().tap() app.buttons["preset-work-time"].waitToAppear().tap()
@@ -43,7 +43,7 @@ final class RuleCreationUITests: XCTestCase {
} }
func testRenameRuleInEditor() throws { func testRenameRuleInEditor() throws {
let app = XCUIApplication.launchSevered() let app = XCUIApplication.launchOpenAppLock()
app.buttons["newRuleButton"].waitToAppear().tap() app.buttons["newRuleButton"].waitToAppear().tap()
app.buttons["ruleKind-schedule"].waitToAppear().tap() app.buttons["ruleKind-schedule"].waitToAppear().tap()
@@ -62,7 +62,7 @@ final class RuleCreationUITests: XCTestCase {
} }
func testDayTogglesFillRowAndHaveLargeTapTargets() throws { func testDayTogglesFillRowAndHaveLargeTapTargets() throws {
let app = XCUIApplication.launchSevered() let app = XCUIApplication.launchOpenAppLock()
app.buttons["newRuleButton"].waitToAppear().tap() app.buttons["newRuleButton"].waitToAppear().tap()
app.buttons["ruleKind-schedule"].waitToAppear().tap() app.buttons["ruleKind-schedule"].waitToAppear().tap()
@@ -80,7 +80,7 @@ final class RuleCreationUITests: XCTestCase {
} }
func testDayTogglesUpdateSummary() throws { func testDayTogglesUpdateSummary() throws {
let app = XCUIApplication.launchSevered() let app = XCUIApplication.launchOpenAppLock()
app.buttons["newRuleButton"].waitToAppear().tap() app.buttons["newRuleButton"].waitToAppear().tap()
app.buttons["ruleKind-schedule"].waitToAppear().tap() app.buttons["ruleKind-schedule"].waitToAppear().tap()
@@ -92,7 +92,7 @@ final class RuleCreationUITests: XCTestCase {
} }
func testCreateTimeLimitRule() throws { func testCreateTimeLimitRule() throws {
let app = XCUIApplication.launchSevered() let app = XCUIApplication.launchOpenAppLock()
app.buttons["newRuleButton"].waitToAppear().tap() app.buttons["newRuleButton"].waitToAppear().tap()
app.buttons["ruleKind-timeLimit"].waitToAppear().tap() app.buttons["ruleKind-timeLimit"].waitToAppear().tap()
@@ -108,7 +108,7 @@ final class RuleCreationUITests: XCTestCase {
} }
func testAdultContentToggleFlowsToDetail() throws { func testAdultContentToggleFlowsToDetail() throws {
let app = XCUIApplication.launchSevered() let app = XCUIApplication.launchOpenAppLock()
app.buttons["newRuleButton"].waitToAppear().tap() app.buttons["newRuleButton"].waitToAppear().tap()
app.buttons["ruleKind-schedule"].waitToAppear().tap() app.buttons["ruleKind-schedule"].waitToAppear().tap()
@@ -125,14 +125,14 @@ final class RuleCreationUITests: XCTestCase {
} }
func testAdultContentDefaultsToAllowed() throws { func testAdultContentDefaultsToAllowed() throws {
let app = XCUIApplication.launchSevered(seedScenario: "standard") let app = XCUIApplication.launchOpenAppLock(seedScenario: "standard")
app.buttons["ruleCard-Work Time"].waitToAppear().tap() app.buttons["ruleCard-Work Time"].waitToAppear().tap()
let row = app.element("detailRow-Adult websites").waitToAppear() let row = app.element("detailRow-Adult websites").waitToAppear()
XCTAssertTrue(row.label.contains("Allowed"), "Got: \(row.label)") XCTAssertTrue(row.label.contains("Allowed"), "Got: \(row.label)")
} }
func testEditorSupportsNativeSwipeBack() throws { func testEditorSupportsNativeSwipeBack() throws {
let app = XCUIApplication.launchSevered() let app = XCUIApplication.launchOpenAppLock()
app.buttons["newRuleButton"].waitToAppear().tap() app.buttons["newRuleButton"].waitToAppear().tap()
app.buttons["ruleKind-schedule"].waitToAppear().tap() app.buttons["ruleKind-schedule"].waitToAppear().tap()
app.staticTexts["ruleEditorTitle"].waitToAppear() app.staticTexts["ruleEditorTitle"].waitToAppear()
@@ -147,7 +147,7 @@ final class RuleCreationUITests: XCTestCase {
} }
func testNewRuleSheetShowsTypesAndPresets() throws { func testNewRuleSheetShowsTypesAndPresets() throws {
let app = XCUIApplication.launchSevered() let app = XCUIApplication.launchOpenAppLock()
app.buttons["newRuleButton"].waitToAppear().tap() app.buttons["newRuleButton"].waitToAppear().tap()
app.buttons["ruleKind-schedule"].waitToAppear() app.buttons["ruleKind-schedule"].waitToAppear()

View File

@@ -1,6 +1,6 @@
// //
// RuleManagementUITests.swift // RuleManagementUITests.swift
// SeveredUITests // OpenAppLockUITests
// //
import XCTest import XCTest
@@ -13,7 +13,7 @@ final class RuleManagementUITests: XCTestCase {
} }
func testDetailShowsLiveStatusAndFacts() throws { func testDetailShowsLiveStatusAndFacts() throws {
let app = XCUIApplication.launchSevered(seedScenario: "standard") let app = XCUIApplication.launchOpenAppLock(seedScenario: "standard")
app.buttons["ruleCard-Work Time"].waitToAppear().tap() app.buttons["ruleCard-Work Time"].waitToAppear().tap()
@@ -29,7 +29,7 @@ final class RuleManagementUITests: XCTestCase {
} }
func testEditRuleTogglesHardModeOn() throws { func testEditRuleTogglesHardModeOn() throws {
let app = XCUIApplication.launchSevered(seedScenario: "standard") let app = XCUIApplication.launchOpenAppLock(seedScenario: "standard")
app.buttons["ruleCard-Sleep"].waitToAppear().tap() app.buttons["ruleCard-Sleep"].waitToAppear().tap()
app.buttons["editRuleButton"].waitToAppear().tap() app.buttons["editRuleButton"].waitToAppear().tap()
@@ -43,7 +43,7 @@ final class RuleManagementUITests: XCTestCase {
} }
func testDisableRule() throws { func testDisableRule() throws {
let app = XCUIApplication.launchSevered(seedScenario: "standard") let app = XCUIApplication.launchOpenAppLock(seedScenario: "standard")
app.buttons["ruleCard-Sleep"].waitToAppear().tap() app.buttons["ruleCard-Sleep"].waitToAppear().tap()
app.buttons["editRuleButton"].waitToAppear().tap() app.buttons["editRuleButton"].waitToAppear().tap()
@@ -64,7 +64,7 @@ final class RuleManagementUITests: XCTestCase {
} }
func testDeleteRuleRemovesCard() throws { func testDeleteRuleRemovesCard() throws {
let app = XCUIApplication.launchSevered(seedScenario: "standard") let app = XCUIApplication.launchOpenAppLock(seedScenario: "standard")
app.buttons["ruleCard-Sleep"].waitToAppear().tap() app.buttons["ruleCard-Sleep"].waitToAppear().tap()
app.buttons["editRuleButton"].waitToAppear().tap() app.buttons["editRuleButton"].waitToAppear().tap()
@@ -82,7 +82,7 @@ final class RuleManagementUITests: XCTestCase {
} }
func testUnblockActiveSoftRule() throws { func testUnblockActiveSoftRule() throws {
let app = XCUIApplication.launchSevered(seedScenario: "standard") let app = XCUIApplication.launchOpenAppLock(seedScenario: "standard")
// The active rule surfaces in Blocked Apps; unblocking pauses it. // The active rule surfaces in Blocked Apps; unblocking pauses it.
app.buttons["blockedTile-Work Time"].waitToAppear().tap() app.buttons["blockedTile-Work Time"].waitToAppear().tap()
@@ -100,7 +100,7 @@ final class HardModeUITests: XCTestCase {
} }
func testHardLockedRuleCannotBeEdited() throws { func testHardLockedRuleCannotBeEdited() throws {
let app = XCUIApplication.launchSevered(seedScenario: "hard-mode-active") let app = XCUIApplication.launchOpenAppLock(seedScenario: "hard-mode-active")
app.buttons["ruleCard-Locked In"].waitToAppear().tap() app.buttons["ruleCard-Locked In"].waitToAppear().tap()
@@ -111,7 +111,7 @@ final class HardModeUITests: XCTestCase {
} }
func testHardLockedRuleCannotBeUnblocked() throws { func testHardLockedRuleCannotBeUnblocked() throws {
let app = XCUIApplication.launchSevered(seedScenario: "hard-mode-active") let app = XCUIApplication.launchOpenAppLock(seedScenario: "hard-mode-active")
app.buttons["blockedTile-Locked In"].waitToAppear().tap() app.buttons["blockedTile-Locked In"].waitToAppear().tap()
@@ -126,7 +126,7 @@ final class HardModeUITests: XCTestCase {
} }
func testSoftRuleUnblockOfferedButHardRuleRefused() throws { func testSoftRuleUnblockOfferedButHardRuleRefused() throws {
let app = XCUIApplication.launchSevered(seedScenario: "standard") let app = XCUIApplication.launchOpenAppLock(seedScenario: "standard")
app.buttons["blockedTile-Work Time"].waitToAppear().tap() app.buttons["blockedTile-Work Time"].waitToAppear().tap()
// Soft rule: the confirmation dialog appears instead of the refusal alert. // Soft rule: the confirmation dialog appears instead of the refusal alert.

View File

@@ -1,6 +1,6 @@
// //
// UITestSupport.swift // UITestSupport.swift
// SeveredUITests // OpenAppLockUITests
// //
import XCTest import XCTest
@@ -8,7 +8,7 @@ import XCTest
extension XCUIApplication { extension XCUIApplication {
/// Launches the app in UI-testing mode: in-memory storage, mocked Screen /// Launches the app in UI-testing mode: in-memory storage, mocked Screen
/// Time authorization, and no shield side effects. /// Time authorization, and no shield side effects.
static func launchSevered( static func launchOpenAppLock(
onboardingCompleted: Bool = true, onboardingCompleted: Bool = true,
seedScenario: String? = nil seedScenario: String? = nil
) -> XCUIApplication { ) -> XCUIApplication {

View File

@@ -1,9 +1,9 @@
# Severed — "Rules" Feature Spec (derived from Opal screen recording) # OpenAppLock — "Rules" Feature Spec (derived from Opal screen recording)
Source: `ScreenRecording_06-12-2026 11-26-19_1.MP4` (iPhone, 4:29). The rules feature Source: `ScreenRecording_06-12-2026 11-26-19_1.MP4` (iPhone, 4:29). The rules feature
appears roughly between 1:003:00 of the recording, on Opal's "My Apps" tab. appears roughly between 1:003:00 of the recording, on Opal's "My Apps" tab.
This spec describes what was observed, then maps it onto an implementation plan This spec describes what was observed, then maps it onto an implementation plan
for Severed. for OpenAppLock.
--- ---
@@ -126,7 +126,7 @@ Presented from "+ New". Full-height sheet, scrollable.
and a circular "+" button bottom-right. Tapping anywhere opens the and a circular "+" button bottom-right. Tapping anywhere opens the
Schedule editor pre-filled with the preset's name/times/days. Schedule editor pre-filled with the preset's name/times/days.
> **Navigation (Severed):** picking a rule type or preset **pushes** the > **Navigation (OpenAppLock):** picking a rule type or preset **pushes** the
> editor inside the sheet via native SwiftUI navigation (`NavigationStack` + > editor inside the sheet via native SwiftUI navigation (`NavigationStack` +
> `navigationDestination(item:)`), so the system push animation and > `navigationDestination(item:)`), so the system push animation and
> edge-swipe-back work; the editor keeps its custom header chrome. > edge-swipe-back work; the editor keeps its custom header chrome.
@@ -150,7 +150,7 @@ Sections (each an inset rounded group with a small icon + caption header):
- Row: `Selected Apps``N Apps ` — pushes the App Picker. - Row: `Selected Apps``N Apps ` — pushes the App Picker.
4. **Hard Mode** `⚡PRO` badge — subtitle "No unblocks allowed"; trailing 4. **Hard Mode** `⚡PRO` badge — subtitle "No unblocks allowed"; trailing
toggle. toggle.
5. **Block Adult Content** *(Severed addition — not in the Opal video)* 5. **Block Adult Content** *(OpenAppLock addition — not in the Opal video)*
subtitle "Filter adult websites while this rule is active"; trailing subtitle "Filter adult websites while this rule is active"; trailing
toggle. Maps to Screen Time's web-content filter toggle. Maps to Screen Time's web-content filter
(`ManagedSettingsStore.webContent.blockedByFilter = .auto(...)`), applied (`ManagedSettingsStore.webContent.blockedByFilter = .auto(...)`), applied
@@ -208,7 +208,7 @@ Full-height sheet:
> Implementation note: Opal ships its own app categorization. On iOS, third > Implementation note: Opal ships its own app categorization. On iOS, third
> parties cannot enumerate installed apps; the system-sanctioned route is > parties cannot enumerate installed apps; the system-sanctioned route is
> `FamilyActivityPicker` (FamilyControls), which provides its own > `FamilyActivityPicker` (FamilyControls), which provides its own
> category/app/website UI and returns opaque tokens. **v1 of Severed should > category/app/website UI and returns opaque tokens. **v1 of OpenAppLock should
> embed `FamilyActivityPicker`** instead of cloning Opal's custom picker, and > embed `FamilyActivityPicker`** instead of cloning Opal's custom picker, and
> keep the `Block`/`Allow Only` segmented control as our own wrapper state. > keep the `Block`/`Allow Only` segmented control as our own wrapper state.
@@ -241,7 +241,7 @@ Full-height sheet:
--- ---
## 5. Implementation plan for Severed ## 5. Implementation plan for OpenAppLock
### 5.1 Frameworks & capabilities ### 5.1 Frameworks & capabilities
@@ -334,7 +334,7 @@ enum SelectionMode: String, Codable { case block, allowOnly }
## 6. Native UI re-skin (current presentation) ## 6. Native UI re-skin (current presentation)
Severed has since replaced the Opal-style custom presentation with the bare OpenAppLock has since replaced the Opal-style custom presentation with the bare
iOS design language, keeping the backend (models, logic, services), the iOS design language, keeping the backend (models, logic, services), the
flows, and the accessibility identifiers intact. Sections 15 remain as the flows, and the accessibility identifiers intact. Sections 15 remain as the
reference for *what* the feature does; presentation now maps as follows: reference for *what* the feature does; presentation now maps as follows: