From c33c79016993054830bb9da832c3f9ac4cf0940b Mon Sep 17 00:00:00 2001 From: ykisana Date: Mon, 5 Oct 2026 18:55:45 -0600 Subject: [PATCH] feat: shield page --- Normal.xcodeproj/project.pbxproj | 173 +++++++++++++++++- NormalShield/Info.plist | 13 ++ NormalShield/NormalShield.entitlements | 12 ++ .../ShieldConfigurationExtension.swift | 71 +++++++ 4 files changed, 259 insertions(+), 10 deletions(-) create mode 100644 NormalShield/Info.plist create mode 100644 NormalShield/NormalShield.entitlements create mode 100644 NormalShield/ShieldConfigurationExtension.swift diff --git a/Normal.xcodeproj/project.pbxproj b/Normal.xcodeproj/project.pbxproj index 81e0910..f3ad6f1 100644 --- a/Normal.xcodeproj/project.pbxproj +++ b/Normal.xcodeproj/project.pbxproj @@ -10,6 +10,7 @@ 2F4718682F5569C90013F94D /* DeviceActivity.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2F4718672F5569C90013F94D /* DeviceActivity.framework */; }; 2F4718702F5569C90013F94D /* NormalMonitor.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 2F4718652F5569C90013F94D /* NormalMonitor.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 2FDA0000000000000000A002 /* NormalWidget.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 2FDA0000000000000000A001 /* NormalWidget.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 2FDA0000000000000000B002 /* NormalShield.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 2FDA0000000000000000B001 /* NormalShield.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -41,6 +42,13 @@ remoteGlobalIDString = 2FDA0000000000000000A003; remoteInfo = NormalWidget; }; + 2FDA0000000000000000B007 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 2F58EFE02EC9C5DE00A38180 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 2FDA0000000000000000B003; + remoteInfo = NormalShield; + }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -52,6 +60,7 @@ files = ( 2F4718702F5569C90013F94D /* NormalMonitor.appex in Embed Foundation Extensions */, 2FDA0000000000000000A002 /* NormalWidget.appex in Embed Foundation Extensions */, + 2FDA0000000000000000B002 /* NormalShield.appex in Embed Foundation Extensions */, ); name = "Embed Foundation Extensions"; runOnlyForDeploymentPostprocessing = 0; @@ -65,6 +74,7 @@ 2F58EFF52EC9C5DF00A38180 /* NormalTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = NormalTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 2F58EFFF2EC9C5DF00A38180 /* NormalUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = NormalUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 2FDA0000000000000000A001 /* NormalWidget.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = NormalWidget.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + 2FDA0000000000000000B001 /* NormalShield.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = NormalShield.appex; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ @@ -150,6 +160,25 @@ ); target = 2FDA0000000000000000A003 /* NormalWidget */; }; + 2FDA0000000000000000B005 /* Exceptions for "NormalShield" folder in "NormalShield" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Info.plist, + ); + target = 2FDA0000000000000000B003 /* NormalShield */; + }; + 2FDA0000000000000000B006 /* Exceptions for "Shared" folder in "NormalShield" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + DataExtension.swift, + SharedConstants.swift, + SharedDTO.swift, + SharedStore.swift, + SharedStoreProviding.swift, + UsageDTO.swift, + ); + target = 2FDA0000000000000000B003 /* NormalShield */; + }; /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ @@ -180,6 +209,7 @@ exceptions = ( 2F4718A12F556F340013F94D /* Exceptions for "Shared" folder in "NormalMonitor" target */, 2FDA0000000000000000A006 /* Exceptions for "Shared" folder in "NormalWidget" target */, + 2FDA0000000000000000B006 /* Exceptions for "Shared" folder in "NormalShield" target */, ); path = Shared; sourceTree = ""; @@ -201,6 +231,14 @@ path = NormalWidget; sourceTree = ""; }; + 2FDA0000000000000000B004 /* NormalShield */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + 2FDA0000000000000000B005 /* Exceptions for "NormalShield" folder in "NormalShield" target */, + ); + path = NormalShield; + sourceTree = ""; + }; /* End PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFrameworksBuildPhase section */ @@ -240,6 +278,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 2FDA0000000000000000B010 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -258,6 +303,7 @@ 2F58EFEA2EC9C5DE00A38180 /* Normal */, 2F4718692F5569C90013F94D /* NormalMonitor */, 2FDA0000000000000000A004 /* NormalWidget */, + 2FDA0000000000000000B004 /* NormalShield */, 2F4718662F5569C90013F94D /* Frameworks */, 2F58EFE92EC9C5DE00A38180 /* Products */, 2F4301E62ECBBAA7001FBF91 /* NormalTests */, @@ -273,6 +319,7 @@ 2F58EFFF2EC9C5DF00A38180 /* NormalUITests.xctest */, 2F4718652F5569C90013F94D /* NormalMonitor.appex */, 2FDA0000000000000000A001 /* NormalWidget.appex */, + 2FDA0000000000000000B001 /* NormalShield.appex */, ); name = Products; sourceTree = ""; @@ -316,6 +363,7 @@ dependencies = ( 2F47186F2F5569C90013F94D /* PBXTargetDependency */, 2FDA0000000000000000A008 /* PBXTargetDependency */, + 2FDA0000000000000000B008 /* PBXTargetDependency */, ); fileSystemSynchronizedGroups = ( 2F4301E92ECBBAA7001FBF91 /* NormalUITests */, @@ -394,6 +442,28 @@ productReference = 2FDA0000000000000000A001 /* NormalWidget.appex */; productType = "com.apple.product-type.app-extension"; }; + 2FDA0000000000000000B003 /* NormalShield */ = { + isa = PBXNativeTarget; + buildConfigurationList = 2FDA0000000000000000B014 /* Build configuration list for PBXNativeTarget "NormalShield" */; + buildPhases = ( + 2FDA0000000000000000B009 /* Sources */, + 2FDA0000000000000000B010 /* Frameworks */, + 2FDA0000000000000000B011 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + 2FDA0000000000000000B004 /* NormalShield */, + ); + name = NormalShield; + packageProductDependencies = ( + ); + productName = NormalShield; + productReference = 2FDA0000000000000000B001 /* NormalShield.appex */; + productType = "com.apple.product-type.app-extension"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -439,6 +509,7 @@ 2F58EFFE2EC9C5DF00A38180 /* NormalUITests */, 2F4718642F5569C90013F94D /* NormalMonitor */, 2FDA0000000000000000A003 /* NormalWidget */, + 2FDA0000000000000000B003 /* NormalShield */, ); }; /* End PBXProject section */ @@ -479,6 +550,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 2FDA0000000000000000B011 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -517,6 +595,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 2FDA0000000000000000B009 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ @@ -540,6 +625,11 @@ target = 2FDA0000000000000000A003 /* NormalWidget */; targetProxy = 2FDA0000000000000000A007 /* PBXContainerItemProxy */; }; + 2FDA0000000000000000B008 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 2FDA0000000000000000B003 /* NormalShield */; + targetProxy = 2FDA0000000000000000B007 /* PBXContainerItemProxy */; + }; /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ @@ -561,7 +651,6 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.4; PRODUCT_BUNDLE_IDENTIFIER = org.normalengineering.Normal.NormalMonitor; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -593,7 +682,6 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.4; PRODUCT_BUNDLE_IDENTIFIER = org.normalengineering.Normal.NormalMonitor; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -662,6 +750,7 @@ GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 18.0; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.5; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -722,6 +811,7 @@ GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 18.0; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.5; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; @@ -757,7 +847,6 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.4; PRODUCT_BUNDLE_IDENTIFIER = org.normalengineering.Normal; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -796,7 +885,6 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.4; PRODUCT_BUNDLE_IDENTIFIER = org.normalengineering.Normal; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -818,7 +906,6 @@ CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 18.0; - MARKETING_VERSION = 1.4; PRODUCT_BUNDLE_IDENTIFIER = org.normalengineering.NormalTests; PRODUCT_NAME = "$(TARGET_NAME)"; STRING_CATALOG_GENERATE_SYMBOLS = NO; @@ -840,7 +927,6 @@ CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 18.0; - MARKETING_VERSION = 1.4; PRODUCT_BUNDLE_IDENTIFIER = org.normalengineering.NormalTests; PRODUCT_NAME = "$(TARGET_NAME)"; STRING_CATALOG_GENERATE_SYMBOLS = NO; @@ -860,7 +946,6 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.4; PRODUCT_BUNDLE_IDENTIFIER = org.normalengineering.NormalUITests; PRODUCT_NAME = "$(TARGET_NAME)"; STRING_CATALOG_GENERATE_SYMBOLS = NO; @@ -879,7 +964,6 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.4; PRODUCT_BUNDLE_IDENTIFIER = org.normalengineering.NormalUITests; PRODUCT_NAME = "$(TARGET_NAME)"; STRING_CATALOG_GENERATE_SYMBOLS = NO; @@ -911,7 +995,6 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.4; PRODUCT_BUNDLE_IDENTIFIER = org.normalengineering.Normal.NormalWidget; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -944,7 +1027,6 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.4; PRODUCT_BUNDLE_IDENTIFIER = org.normalengineering.Normal.NormalWidget; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -958,6 +1040,68 @@ }; name = Release; }; + 2FDA0000000000000000B012 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_ENTITLEMENTS = NormalShield/NormalShield.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 8FNVWC4DBU; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = NormalShield/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = NormalShield; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = org.normalengineering.Normal.NormalShield; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SKIP_INSTALL = YES; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 2FDA0000000000000000B013 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_ENTITLEMENTS = NormalShield/NormalShield.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 8FNVWC4DBU; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = NormalShield/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = NormalShield; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 18.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = org.normalengineering.Normal.NormalShield; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SKIP_INSTALL = YES; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -1015,6 +1159,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 2FDA0000000000000000B014 /* Build configuration list for PBXNativeTarget "NormalShield" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 2FDA0000000000000000B012 /* Debug */, + 2FDA0000000000000000B013 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ }; rootObject = 2F58EFE02EC9C5DE00A38180 /* Project object */; diff --git a/NormalShield/Info.plist b/NormalShield/Info.plist new file mode 100644 index 0000000..c89920e --- /dev/null +++ b/NormalShield/Info.plist @@ -0,0 +1,13 @@ + + + + + NSExtension + + NSExtensionPointIdentifier + com.apple.ManagedSettingsUI.shield-configuration-service + NSExtensionPrincipalClass + $(PRODUCT_MODULE_NAME).ShieldConfigurationExtension + + + diff --git a/NormalShield/NormalShield.entitlements b/NormalShield/NormalShield.entitlements new file mode 100644 index 0000000..4f43a82 --- /dev/null +++ b/NormalShield/NormalShield.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.developer.family-controls + + com.apple.security.application-groups + + group.com.normalengineering.block + + + diff --git a/NormalShield/ShieldConfigurationExtension.swift b/NormalShield/ShieldConfigurationExtension.swift new file mode 100644 index 0000000..2d256f5 --- /dev/null +++ b/NormalShield/ShieldConfigurationExtension.swift @@ -0,0 +1,71 @@ +import FamilyControls +import Foundation +import ManagedSettings +import ManagedSettingsUI +import UIKit + +final class ShieldConfigurationExtension: ShieldConfigurationDataSource { + private let sharedStore = SharedStore() + + override func configuration(shielding application: Application) -> ShieldConfiguration { + let limited = reachedLimitSelection() + return makeConfiguration( + subject: "app", + isDailyLimit: application.token.map(limited.applicationTokens.contains) ?? false + ) + } + + override func configuration( + shielding application: Application, + in category: ActivityCategory + ) -> ShieldConfiguration { + let limited = reachedLimitSelection() + return makeConfiguration( + subject: "app", + isDailyLimit: (application.token.map(limited.applicationTokens.contains) ?? false) + || (category.token.map(limited.categoryTokens.contains) ?? false) + ) + } + + override func configuration(shielding webDomain: WebDomain) -> ShieldConfiguration { + let limited = reachedLimitSelection() + return makeConfiguration( + subject: "website", + isDailyLimit: webDomain.token.map(limited.webDomainTokens.contains) ?? false + ) + } + + override func configuration( + shielding webDomain: WebDomain, + in category: ActivityCategory + ) -> ShieldConfiguration { + let limited = reachedLimitSelection() + return makeConfiguration( + subject: "website", + isDailyLimit: (webDomain.token.map(limited.webDomainTokens.contains) ?? false) + || (category.token.map(limited.categoryTokens.contains) ?? false) + ) + } + + /// Union of every spent Max Daily Limit — the same set the Monitor applies to the `.dailyLimits` store. + private func reachedLimitSelection() -> FamilyActivitySelection { + sharedStore.reachedUsageLimits() + .compactMap { try? FamilyActivitySelection.fromData($0.selectionData) } + .reduce(FamilyActivitySelection()) { $0.union($1) } + } + + private func makeConfiguration(subject: String, isDailyLimit: Bool) -> ShieldConfiguration { + let subtitle = isDailyLimit + ? "You've reached your max daily limit." + : "If you need to use this \(subject), unblock with a key in Normal." + + return ShieldConfiguration( + backgroundBlurStyle: .dark, + backgroundColor: .black, + title: ShieldConfiguration.Label(text: "Blocked by Normal", color: .white), + subtitle: ShieldConfiguration.Label(text: subtitle, color: .lightGray), + primaryButtonLabel: ShieldConfiguration.Label(text: "Close", color: .black), + primaryButtonBackgroundColor: .white + ) + } +}