Commit d3a27d0680
Unsigned
Layout: unified · split
DomainDig.xcodeproj/project.pbxproj +173
| @@ -14,8 +14,33 @@ | |||
| 14 | 8BCA3CC02F9C8D57004B742C /* LocalAPIModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BCA3CBC2F9C8D57004B742C /* LocalAPIModels.swift */; }; | 14 | 8BCA3CC02F9C8D57004B742C /* LocalAPIModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BCA3CBC2F9C8D57004B742C /* LocalAPIModels.swift */; }; |
| 15 | 8BCA3CC12F9C8D57004B742C /* LocalAPIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BCA3CBD2F9C8D57004B742C /* LocalAPIService.swift */; }; | 15 | 8BCA3CC12F9C8D57004B742C /* LocalAPIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BCA3CBD2F9C8D57004B742C /* LocalAPIService.swift */; }; |
| 16 | 8BF9DA872F9B13FB00EF41D5 /* DomainDataPortabilityService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BF9DA842F9B13FB00EF41D5 /* DomainDataPortabilityService.swift */; }; | 16 | 8BF9DA872F9B13FB00EF41D5 /* DomainDataPortabilityService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BF9DA842F9B13FB00EF41D5 /* DomainDataPortabilityService.swift */; }; |
| 17 | 8BDA00010000000000000002 /* DomainDigWidgetExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 8BDA00010000000000000001 /* DomainDigWidgetExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; | ||
| 17 | /* End PBXBuildFile section */ | 18 | /* End PBXBuildFile section */ |
| 18 | 19 | ||
| 20 | /* Begin PBXContainerItemProxy section */ | ||
| 21 | 8BDA00010000000000000003 /* PBXContainerItemProxy */ = { | ||
| 22 | isa = PBXContainerItemProxy; | ||
| 23 | containerPortal = 8B7800612F6090E300933221 /* Project object */; | ||
| 24 | proxyType = 1; | ||
| 25 | remoteGlobalIDString = 8BDA00010000000000000006; | ||
| 26 | remoteInfo = DomainDigWidgetExtension; | ||
| 27 | }; | ||
| 28 | /* End PBXContainerItemProxy section */ | ||
| 29 | |||
| 30 | /* Begin PBXCopyFilesBuildPhase section */ | ||
| 31 | 8BDA00010000000000000005 /* Embed Foundation Extensions */ = { | ||
| 32 | isa = PBXCopyFilesBuildPhase; | ||
| 33 | buildActionMask = 2147483647; | ||
| 34 | dstPath = ""; | ||
| 35 | dstSubfolderSpec = 13; | ||
| 36 | files = ( | ||
| 37 | 8BDA00010000000000000002 /* DomainDigWidgetExtension.appex in Embed Foundation Extensions */, | ||
| 38 | ); | ||
| 39 | name = "Embed Foundation Extensions"; | ||
| 40 | runOnlyForDeploymentPostprocessing = 0; | ||
| 41 | }; | ||
| 42 | /* End PBXCopyFilesBuildPhase section */ | ||
| 43 | |||
| 19 | /* Begin PBXFileReference section */ | 44 | /* Begin PBXFileReference section */ |
| 20 | 8B7800692F6090E300933221 /* DomainDig.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DomainDig.app; sourceTree = BUILT_PRODUCTS_DIR; }; | 45 | 8B7800692F6090E300933221 /* DomainDig.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DomainDig.app; sourceTree = BUILT_PRODUCTS_DIR; }; |
| 21 | 8BBFEF032F9874AE00E8E144 /* DomainInspectionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainInspectionService.swift; sourceTree = "<group>"; }; | 46 | 8BBFEF032F9874AE00E8E144 /* DomainInspectionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainInspectionService.swift; sourceTree = "<group>"; }; |
| @@ -25,6 +50,7 @@ | |||
| 25 | 8BCA3CBC2F9C8D57004B742C /* LocalAPIModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalAPIModels.swift; sourceTree = "<group>"; }; | 50 | 8BCA3CBC2F9C8D57004B742C /* LocalAPIModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalAPIModels.swift; sourceTree = "<group>"; }; |
| 26 | 8BCA3CBD2F9C8D57004B742C /* LocalAPIService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalAPIService.swift; sourceTree = "<group>"; }; | 51 | 8BCA3CBD2F9C8D57004B742C /* LocalAPIService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalAPIService.swift; sourceTree = "<group>"; }; |
| 27 | 8BF9DA842F9B13FB00EF41D5 /* DomainDataPortabilityService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainDataPortabilityService.swift; sourceTree = "<group>"; }; | 52 | 8BF9DA842F9B13FB00EF41D5 /* DomainDataPortabilityService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainDataPortabilityService.swift; sourceTree = "<group>"; }; |
| 53 | 8BDA00010000000000000001 /* DomainDigWidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = DomainDigWidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; | ||
| 28 | /* End PBXFileReference section */ | 54 | /* End PBXFileReference section */ |
| 29 | 55 | ||
| 30 | /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ | 56 | /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ |
| @@ -37,6 +63,13 @@ | |||
| 37 | ); | 63 | ); |
| 38 | target = 8B7800682F6090E300933221 /* DomainDig */; | 64 | target = 8B7800682F6090E300933221 /* DomainDig */; |
| 39 | }; | 65 | }; |
| 66 | 8BDA0001000000000000000F /* Exceptions for "DomainDigWidget" folder in "DomainDigWidgetExtension" target */ = { | ||
| 67 | isa = PBXFileSystemSynchronizedBuildFileExceptionSet; | ||
| 68 | membershipExceptions = ( | ||
| 69 | Info.plist, | ||
| 70 | ); | ||
| 71 | target = 8BDA00010000000000000006 /* DomainDigWidgetExtension */; | ||
| 72 | }; | ||
| 40 | /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ | 73 | /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ |
| 41 | 74 | ||
| 42 | /* Begin PBXFileSystemSynchronizedRootGroup section */ | 75 | /* Begin PBXFileSystemSynchronizedRootGroup section */ |
| @@ -48,6 +81,19 @@ | |||
| 48 | path = DomainDig; | 81 | path = DomainDig; |
| 49 | sourceTree = "<group>"; | 82 | sourceTree = "<group>"; |
| 50 | }; | 83 | }; |
| 84 | 8BDA0001000000000000000D /* Shared */ = { | ||
| 85 | isa = PBXFileSystemSynchronizedRootGroup; | ||
| 86 | path = Shared; | ||
| 87 | sourceTree = "<group>"; | ||
| 88 | }; | ||
| 89 | 8BDA0001000000000000000E /* DomainDigWidget */ = { | ||
| 90 | isa = PBXFileSystemSynchronizedRootGroup; | ||
| 91 | exceptions = ( | ||
| 92 | 8BDA0001000000000000000F /* Exceptions for "DomainDigWidget" folder in "DomainDigWidgetExtension" target */, | ||
| 93 | ); | ||
| 94 | path = DomainDigWidget; | ||
| 95 | sourceTree = "<group>"; | ||
| 96 | }; | ||
| 51 | /* End PBXFileSystemSynchronizedRootGroup section */ | 97 | /* End PBXFileSystemSynchronizedRootGroup section */ |
| 52 | 98 | ||
| 53 | /* Begin PBXFrameworksBuildPhase section */ | 99 | /* Begin PBXFrameworksBuildPhase section */ |
| @@ -58,6 +104,13 @@ | |||
| 58 | ); | 104 | ); |
| 59 | runOnlyForDeploymentPostprocessing = 0; | 105 | runOnlyForDeploymentPostprocessing = 0; |
| 60 | }; | 106 | }; |
| 107 | 8BDA00010000000000000008 /* Frameworks */ = { | ||
| 108 | isa = PBXFrameworksBuildPhase; | ||
| 109 | buildActionMask = 2147483647; | ||
| 110 | files = ( | ||
| 111 | ); | ||
| 112 | runOnlyForDeploymentPostprocessing = 0; | ||
| 113 | }; | ||
| 61 | /* End PBXFrameworksBuildPhase section */ | 114 | /* End PBXFrameworksBuildPhase section */ |
| 62 | 115 | ||
| 63 | /* Begin PBXGroup section */ | 116 | /* Begin PBXGroup section */ |
| @@ -65,6 +118,8 @@ | |||
| 65 | isa = PBXGroup; | 118 | isa = PBXGroup; |
| 66 | children = ( | 119 | children = ( |
| 67 | 8B78006B2F6090E300933221 /* DomainDig */, | 120 | 8B78006B2F6090E300933221 /* DomainDig */, |
| 121 | 8BDA0001000000000000000D /* Shared */, | ||
| 122 | 8BDA0001000000000000000E /* DomainDigWidget */, | ||
| 68 | 8B78006A2F6090E300933221 /* Products */, | 123 | 8B78006A2F6090E300933221 /* Products */, |
| 69 | 8BBFEF062F9874AE00E8E144 /* LookupSnapshot.swift */, | 124 | 8BBFEF062F9874AE00E8E144 /* LookupSnapshot.swift */, |
| 70 | 8BBFEF042F9874AE00E8E144 /* DomainReportBuilder.swift */, | 125 | 8BBFEF042F9874AE00E8E144 /* DomainReportBuilder.swift */, |
| @@ -80,6 +135,7 @@ | |||
| 80 | isa = PBXGroup; | 135 | isa = PBXGroup; |
| 81 | children = ( | 136 | children = ( |
| 82 | 8B7800692F6090E300933221 /* DomainDig.app */, | 137 | 8B7800692F6090E300933221 /* DomainDig.app */, |
| 138 | 8BDA00010000000000000001 /* DomainDigWidgetExtension.appex */, | ||
| 83 | ); | 139 | ); |
| 84 | name = Products; | 140 | name = Products; |
| 85 | sourceTree = "<group>"; | 141 | sourceTree = "<group>"; |
| @@ -94,13 +150,16 @@ | |||
| 94 | 8B7800652F6090E300933221 /* Sources */, | 150 | 8B7800652F6090E300933221 /* Sources */, |
| 95 | 8B7800662F6090E300933221 /* Frameworks */, | 151 | 8B7800662F6090E300933221 /* Frameworks */, |
| 96 | 8B7800672F6090E300933221 /* Resources */, | 152 | 8B7800672F6090E300933221 /* Resources */, |
| 153 | 8BDA00010000000000000005 /* Embed Foundation Extensions */, | ||
| 97 | ); | 154 | ); |
| 98 | buildRules = ( | 155 | buildRules = ( |
| 99 | ); | 156 | ); |
| 100 | dependencies = ( | 157 | dependencies = ( |
| 158 | 8BDA00010000000000000004 /* PBXTargetDependency */, | ||
| 101 | ); | 159 | ); |
| 102 | fileSystemSynchronizedGroups = ( | 160 | fileSystemSynchronizedGroups = ( |
| 103 | 8B78006B2F6090E300933221 /* DomainDig */, | 161 | 8B78006B2F6090E300933221 /* DomainDig */, |
| 162 | 8BDA0001000000000000000D /* Shared */, | ||
| 104 | ); | 163 | ); |
| 105 | name = DomainDig; | 164 | name = DomainDig; |
| 106 | packageProductDependencies = ( | 165 | packageProductDependencies = ( |
| @@ -109,8 +168,39 @@ | |||
| 109 | productReference = 8B7800692F6090E300933221 /* DomainDig.app */; | 168 | productReference = 8B7800692F6090E300933221 /* DomainDig.app */; |
| 110 | productType = "com.apple.product-type.application"; | 169 | productType = "com.apple.product-type.application"; |
| 111 | }; | 170 | }; |
| 171 | 8BDA00010000000000000006 /* DomainDigWidgetExtension */ = { | ||
| 172 | isa = PBXNativeTarget; | ||
| 173 | buildConfigurationList = 8BDA0001000000000000000A /* Build configuration list for PBXNativeTarget "DomainDigWidgetExtension" */; | ||
| 174 | buildPhases = ( | ||
| 175 | 8BDA00010000000000000007 /* Sources */, | ||
| 176 | 8BDA00010000000000000008 /* Frameworks */, | ||
| 177 | 8BDA00010000000000000009 /* Resources */, | ||
| 178 | ); | ||
| 179 | buildRules = ( | ||
| 180 | ); | ||
| 181 | dependencies = ( | ||
| 182 | ); | ||
| 183 | fileSystemSynchronizedGroups = ( | ||
| 184 | 8BDA0001000000000000000D /* Shared */, | ||
| 185 | 8BDA0001000000000000000E /* DomainDigWidget */, | ||
| 186 | ); | ||
| 187 | name = DomainDigWidgetExtension; | ||
| 188 | packageProductDependencies = ( | ||
| 189 | ); | ||
| 190 | productName = DomainDigWidgetExtension; | ||
| 191 | productReference = 8BDA00010000000000000001 /* DomainDigWidgetExtension.appex */; | ||
| 192 | productType = "com.apple.product-type.app-extension"; | ||
| 193 | }; | ||
| 112 | /* End PBXNativeTarget section */ | 194 | /* End PBXNativeTarget section */ |
| 113 | 195 | ||
| 196 | /* Begin PBXTargetDependency section */ | ||
| 197 | 8BDA00010000000000000004 /* PBXTargetDependency */ = { | ||
| 198 | isa = PBXTargetDependency; | ||
| 199 | target = 8BDA00010000000000000006 /* DomainDigWidgetExtension */; | ||
| 200 | targetProxy = 8BDA00010000000000000003 /* PBXContainerItemProxy */; | ||
| 201 | }; | ||
| 202 | /* End PBXTargetDependency section */ | ||
| 203 | |||
| 114 | /* Begin PBXProject section */ | 204 | /* Begin PBXProject section */ |
| 115 | 8B7800612F6090E300933221 /* Project object */ = { | 205 | 8B7800612F6090E300933221 /* Project object */ = { |
| 116 | isa = PBXProject; | 206 | isa = PBXProject; |
| @@ -127,6 +217,9 @@ | |||
| 127 | }; | 217 | }; |
| 128 | }; | 218 | }; |
| 129 | }; | 219 | }; |
| 220 | 8BDA00010000000000000006 = { | ||
| 221 | CreatedOnToolsVersion = 26.3; | ||
| 222 | }; | ||
| 130 | }; | 223 | }; |
| 131 | }; | 224 | }; |
| 132 | buildConfigurationList = 8B7800642F6090E300933221 /* Build configuration list for PBXProject "DomainDig" */; | 225 | buildConfigurationList = 8B7800642F6090E300933221 /* Build configuration list for PBXProject "DomainDig" */; |
| @@ -144,6 +237,7 @@ | |||
| 144 | projectRoot = ""; | 237 | projectRoot = ""; |
| 145 | targets = ( | 238 | targets = ( |
| 146 | 8B7800682F6090E300933221 /* DomainDig */, | 239 | 8B7800682F6090E300933221 /* DomainDig */, |
| 240 | 8BDA00010000000000000006 /* DomainDigWidgetExtension */, | ||
| 147 | ); | 241 | ); |
| 148 | }; | 242 | }; |
| 149 | /* End PBXProject section */ | 243 | /* End PBXProject section */ |
| @@ -156,6 +250,13 @@ | |||
| 156 | ); | 250 | ); |
| 157 | runOnlyForDeploymentPostprocessing = 0; | 251 | runOnlyForDeploymentPostprocessing = 0; |
| 158 | }; | 252 | }; |
| 253 | 8BDA00010000000000000009 /* Resources */ = { | ||
| 254 | isa = PBXResourcesBuildPhase; | ||
| 255 | buildActionMask = 2147483647; | ||
| 256 | files = ( | ||
| 257 | ); | ||
| 258 | runOnlyForDeploymentPostprocessing = 0; | ||
| 259 | }; | ||
| 159 | /* End PBXResourcesBuildPhase section */ | 260 | /* End PBXResourcesBuildPhase section */ |
| 160 | 261 | ||
| 161 | /* Begin PBXSourcesBuildPhase section */ | 262 | /* Begin PBXSourcesBuildPhase section */ |
| @@ -173,6 +274,13 @@ | |||
| 173 | ); | 274 | ); |
| 174 | runOnlyForDeploymentPostprocessing = 0; | 275 | runOnlyForDeploymentPostprocessing = 0; |
| 175 | }; | 276 | }; |
| 277 | 8BDA00010000000000000007 /* Sources */ = { | ||
| 278 | isa = PBXSourcesBuildPhase; | ||
| 279 | buildActionMask = 2147483647; | ||
| 280 | files = ( | ||
| 281 | ); | ||
| 282 | runOnlyForDeploymentPostprocessing = 0; | ||
| 283 | }; | ||
| 176 | /* End PBXSourcesBuildPhase section */ | 284 | /* End PBXSourcesBuildPhase section */ |
| 177 | 285 | ||
| 178 | /* Begin XCBuildConfiguration section */ | 286 | /* Begin XCBuildConfiguration section */ |
| @@ -373,6 +481,62 @@ | |||
| 373 | }; | 481 | }; |
| 374 | name = Release; | 482 | name = Release; |
| 375 | }; | 483 | }; |
| 484 | 8BDA0001000000000000000B /* Debug */ = { | ||
| 485 | isa = XCBuildConfiguration; | ||
| 486 | buildSettings = { | ||
| 487 | APPLICATION_EXTENSION_API_ONLY = YES; | ||
| 488 | CODE_SIGN_ENTITLEMENTS = DomainDigWidget/DomainDigWidget.entitlements; | ||
| 489 | CODE_SIGN_STYLE = Automatic; | ||
| 490 | CURRENT_PROJECT_VERSION = 37; | ||
| 491 | DEVELOPMENT_TEAM = ZCNAX3VL9D; | ||
| 492 | GENERATE_INFOPLIST_FILE = NO; | ||
| 493 | INFOPLIST_FILE = DomainDigWidget/Info.plist; | ||
| 494 | IPHONEOS_DEPLOYMENT_TARGET = 17.6; | ||
| 495 | LD_RUNPATH_SEARCH_PATHS = ( | ||
| 496 | "$(inherited)", | ||
| 497 | "@executable_path/Frameworks", | ||
| 498 | "@executable_path/../../Frameworks", | ||
| 499 | ); | ||
| 500 | MARKETING_VERSION = 4.5.0; | ||
| 501 | PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.DomainDig.DomainDigWidget; | ||
| 502 | PRODUCT_NAME = "$(TARGET_NAME)"; | ||
| 503 | SKIP_INSTALL = YES; | ||
| 504 | SWIFT_APPROACHABLE_CONCURRENCY = YES; | ||
| 505 | SWIFT_EMIT_LOC_STRINGS = YES; | ||
| 506 | SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; | ||
| 507 | SWIFT_VERSION = 5.0; | ||
| 508 | TARGETED_DEVICE_FAMILY = "1,2"; | ||
| 509 | }; | ||
| 510 | name = Debug; | ||
| 511 | }; | ||
| 512 | 8BDA0001000000000000000C /* Release */ = { | ||
| 513 | isa = XCBuildConfiguration; | ||
| 514 | buildSettings = { | ||
| 515 | APPLICATION_EXTENSION_API_ONLY = YES; | ||
| 516 | CODE_SIGN_ENTITLEMENTS = DomainDigWidget/DomainDigWidget.entitlements; | ||
| 517 | CODE_SIGN_STYLE = Automatic; | ||
| 518 | CURRENT_PROJECT_VERSION = 37; | ||
| 519 | DEVELOPMENT_TEAM = ZCNAX3VL9D; | ||
| 520 | GENERATE_INFOPLIST_FILE = NO; | ||
| 521 | INFOPLIST_FILE = DomainDigWidget/Info.plist; | ||
| 522 | IPHONEOS_DEPLOYMENT_TARGET = 17.6; | ||
| 523 | LD_RUNPATH_SEARCH_PATHS = ( | ||
| 524 | "$(inherited)", | ||
| 525 | "@executable_path/Frameworks", | ||
| 526 | "@executable_path/../../Frameworks", | ||
| 527 | ); | ||
| 528 | MARKETING_VERSION = 4.5.0; | ||
| 529 | PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.DomainDig.DomainDigWidget; | ||
| 530 | PRODUCT_NAME = "$(TARGET_NAME)"; | ||
| 531 | SKIP_INSTALL = YES; | ||
| 532 | SWIFT_APPROACHABLE_CONCURRENCY = YES; | ||
| 533 | SWIFT_EMIT_LOC_STRINGS = YES; | ||
| 534 | SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; | ||
| 535 | SWIFT_VERSION = 5.0; | ||
| 536 | TARGETED_DEVICE_FAMILY = "1,2"; | ||
| 537 | }; | ||
| 538 | name = Release; | ||
| 539 | }; | ||
| 376 | /* End XCBuildConfiguration section */ | 540 | /* End XCBuildConfiguration section */ |
| 377 | 541 | ||
| 378 | /* Begin XCConfigurationList section */ | 542 | /* Begin XCConfigurationList section */ |
| @@ -394,6 +558,15 @@ | |||
| 394 | defaultConfigurationIsVisible = 0; | 558 | defaultConfigurationIsVisible = 0; |
| 395 | defaultConfigurationName = Release; | 559 | defaultConfigurationName = Release; |
| 396 | }; | 560 | }; |
| 561 | 8BDA0001000000000000000A /* Build configuration list for PBXNativeTarget "DomainDigWidgetExtension" */ = { | ||
| 562 | isa = XCConfigurationList; | ||
| 563 | buildConfigurations = ( | ||
| 564 | 8BDA0001000000000000000B /* Debug */, | ||
| 565 | 8BDA0001000000000000000C /* Release */, | ||
| 566 | ); | ||
| 567 | defaultConfigurationIsVisible = 0; | ||
| 568 | defaultConfigurationName = Release; | ||
| 569 | }; | ||
| 397 | /* End XCConfigurationList section */ | 570 | /* End XCConfigurationList section */ |
| 398 | }; | 571 | }; |
| 399 | rootObject = 8B7800612F6090E300933221 /* Project object */; | 572 | rootObject = 8B7800612F6090E300933221 /* Project object */; |
DomainDig.xcodeproj/xcuserdata/cmc.xcuserdatad/xcschemes/xcschememanagement.plist +5
| @@ -9,6 +9,11 @@ | |||
| 9 | <key>orderHint</key> | 9 | <key>orderHint</key> |
| 10 | <integer>0</integer> | 10 | <integer>0</integer> |
| 11 | </dict> | 11 | </dict> |
| 12 | <key>DomainDigWidgetExtension.xcscheme_^#shared#^_</key> | ||
| 13 | <dict> | ||
| 14 | <key>orderHint</key> | ||
| 15 | <integer>1</integer> | ||
| 16 | </dict> | ||
| 12 | </dict> | 17 | </dict> |
| 13 | <key>SuppressBuildableAutocreation</key> | 18 | <key>SuppressBuildableAutocreation</key> |
| 14 | <dict> | 19 | <dict> |
DomainDig/DomainDig.entitlements +4
| @@ -10,5 +10,9 @@ | |||
| 10 | <array> | 10 | <array> |
| 11 | <string>CloudKit</string> | 11 | <string>CloudKit</string> |
| 12 | </array> | 12 | </array> |
| 13 | <key>com.apple.security.application-groups</key> | ||
| 14 | <array> | ||
| 15 | <string>group.net.cleberg.DomainDig</string> | ||
| 16 | </array> | ||
| 13 | </dict> | 17 | </dict> |
| 14 | </plist> | 18 | </plist> |
DomainDig/DomainDigApp.swift +2
| @@ -39,6 +39,7 @@ struct DomainDigApp: App { | |||
| 39 | cloudSyncService.scheduleSyncIfNeeded(trigger: .launch) | 39 | cloudSyncService.scheduleSyncIfNeeded(trigger: .launch) |
| 40 | viewModel.monitoringStatusMessage = DomainMonitoringScheduler.shared.syncSchedule() | 40 | viewModel.monitoringStatusMessage = DomainMonitoringScheduler.shared.syncSchedule() |
| 41 | IntegrationService.shared.processQueueNow() | 41 | IntegrationService.shared.processQueueNow() |
| 42 | viewModel.refreshWidgetData() | ||
| 42 | } | 43 | } |
| 43 | .onReceive(NotificationCenter.default.publisher(for: .cloudSyncDidApplyChanges)) { _ in | 44 | .onReceive(NotificationCenter.default.publisher(for: .cloudSyncDidApplyChanges)) { _ in |
| 44 | viewModel.refreshPersistedData() | 45 | viewModel.refreshPersistedData() |
| @@ -57,6 +58,7 @@ struct DomainDigApp: App { | |||
| 57 | cloudSyncService.scheduleSyncIfNeeded(trigger: .launch) | 58 | cloudSyncService.scheduleSyncIfNeeded(trigger: .launch) |
| 58 | viewModel.monitoringStatusMessage = DomainMonitoringScheduler.shared.syncSchedule() | 59 | viewModel.monitoringStatusMessage = DomainMonitoringScheduler.shared.syncSchedule() |
| 59 | IntegrationService.shared.processQueueNow() | 60 | IntegrationService.shared.processQueueNow() |
| 61 | viewModel.refreshWidgetData() | ||
| 60 | } | 62 | } |
| 61 | } | 63 | } |
| 62 | } | 64 | } |
DomainDig/DomainDigIntents.swift +27 −51
| @@ -124,6 +124,24 @@ struct AddToWatchlistIntent: AppIntent { | |||
| 124 | } | 124 | } |
| 125 | } | 125 | } |
| 126 | 126 | ||
| 127 | /// App Intent that opens DomainDig and re-inspects every tracked domain. It runs | ||
| 128 | /// through the existing view-model batch path (`refreshAllTrackedDomains`), which | ||
| 129 | /// enforces the batch feature gate and surfaces the paywall when needed. | ||
| 130 | struct RunSweepIntent: AppIntent { | ||
| 131 | static var title: LocalizedStringResource = "Run Watchlist Sweep" | ||
| 132 | static var description = IntentDescription( | ||
| 133 | "Open DomainDig and re-inspect every domain on your watchlist." | ||
| 134 | ) | ||
| 135 | |||
| 136 | static var openAppWhenRun = true | ||
| 137 | |||
| 138 | @MainActor | ||
| 139 | func perform() async throws -> some IntentResult { | ||
| 140 | DomainDigIntentRouter.shared.pendingAction = .sweep | ||
| 141 | return .result() | ||
| 142 | } | ||
| 143 | } | ||
| 144 | |||
| 127 | /// In-process hand-off from an `openAppWhenRun` intent to the running SwiftUI | 145 | /// In-process hand-off from an `openAppWhenRun` intent to the running SwiftUI |
| 128 | /// layer. `RootTabView` observes `pendingAction` and performs it. | 146 | /// layer. `RootTabView` observes `pendingAction` and performs it. |
| 129 | @MainActor | 147 | @MainActor |
| @@ -134,57 +152,6 @@ final class DomainDigIntentRouter { | |||
| 134 | private init() {} | 152 | private init() {} |
| 135 | } | 153 | } |
| 136 | 154 | ||
| 137 | /// Shared builder/parser for the `domaindig://` URL scheme, used by both the | ||
| 138 | /// intents (to open the app) and the app (to route incoming links). | ||
| 139 | enum DomainDigDeepLink { | ||
| 140 | static let scheme = "domaindig" | ||
| 141 | |||
| 142 | enum Action: Equatable { | ||
| 143 | case inspect(String) | ||
| 144 | case watch(String) | ||
| 145 | |||
| 146 | var host: String { | ||
| 147 | switch self { | ||
| 148 | case .inspect: return "inspect" | ||
| 149 | case .watch: return "watch" | ||
| 150 | } | ||
| 151 | } | ||
| 152 | |||
| 153 | var domain: String { | ||
| 154 | switch self { | ||
| 155 | case let .inspect(domain), let .watch(domain): return domain | ||
| 156 | } | ||
| 157 | } | ||
| 158 | } | ||
| 159 | |||
| 160 | static func url(for action: Action) -> URL { | ||
| 161 | var components = URLComponents() | ||
| 162 | components.scheme = scheme | ||
| 163 | components.host = action.host | ||
| 164 | components.queryItems = [URLQueryItem(name: "domain", value: action.domain)] | ||
| 165 | // The scheme and host are fixed and the domain is percent-encoded by | ||
| 166 | // URLComponents, so this is always a valid URL. | ||
| 167 | return components.url! | ||
| 168 | } | ||
| 169 | |||
| 170 | static func action(from url: URL) -> Action? { | ||
| 171 | guard url.scheme == scheme else { return nil } | ||
| 172 | |||
| 173 | let domain = URLComponents(url: url, resolvingAgainstBaseURL: false)? | ||
| 174 | .queryItems? | ||
| 175 | .first { $0.name == "domain" }? | ||
| 176 | .value? | ||
| 177 | .trimmingCharacters(in: .whitespacesAndNewlines) ?? "" | ||
| 178 | guard !domain.isEmpty else { return nil } | ||
| 179 | |||
| 180 | switch url.host() { | ||
| 181 | case "inspect": return .inspect(domain) | ||
| 182 | case "watch": return .watch(domain) | ||
| 183 | default: return nil | ||
| 184 | } | ||
| 185 | } | ||
| 186 | } | ||
| 187 | |||
| 188 | /// Exposes DomainDig intents to Spotlight and Siri with invocation phrases. | 155 | /// Exposes DomainDig intents to Spotlight and Siri with invocation phrases. |
| 189 | struct DomainDigShortcuts: AppShortcutsProvider { | 156 | struct DomainDigShortcuts: AppShortcutsProvider { |
| 190 | static var appShortcuts: [AppShortcut] { | 157 | static var appShortcuts: [AppShortcut] { |
| @@ -206,5 +173,14 @@ struct DomainDigShortcuts: AppShortcutsProvider { | |||
| 206 | shortTitle: "Add to Watchlist", | 173 | shortTitle: "Add to Watchlist", |
| 207 | systemImageName: "plus.circle" | 174 | systemImageName: "plus.circle" |
| 208 | ) | 175 | ) |
| 176 | AppShortcut( | ||
| 177 | intent: RunSweepIntent(), | ||
| 178 | phrases: [ | ||
| 179 | "Run a sweep with \(.applicationName)", | ||
| 180 | "Sweep my \(.applicationName) watchlist" | ||
| 181 | ], | ||
| 182 | shortTitle: "Run Sweep", | ||
| 183 | systemImageName: "arrow.trianglehead.2.clockwise" | ||
| 184 | ) | ||
| 209 | } | 185 | } |
| 210 | } | 186 | } |
DomainDig/DomainViewModel+Widget.swift added +60
| @@ -0,0 +1,60 @@ | |||
| 1 | import Foundation | ||
| 2 | import WidgetKit | ||
| 3 | |||
| 4 | extension DomainViewModel { | ||
| 5 | /// Publishes the current portfolio state to the App Group container so the | ||
| 6 | /// widget can render it, then asks WidgetKit to refresh its timelines. | ||
| 7 | func refreshWidgetData() { | ||
| 8 | let data = portfolioDashboardData | ||
| 9 | let snapshot = data.snapshot | ||
| 10 | |||
| 11 | let ordered = data.domainStates.sorted { lhs, rhs in | ||
| 12 | if lhs.trackedDomain.isPinned != rhs.trackedDomain.isPinned { | ||
| 13 | return lhs.trackedDomain.isPinned | ||
| 14 | } | ||
| 15 | return lhs.health.widgetSeverityRank > rhs.health.widgetSeverityRank | ||
| 16 | } | ||
| 17 | |||
| 18 | let domains = ordered.prefix(6).map { state in | ||
| 19 | DomainDigWidgetDomain( | ||
| 20 | domain: state.trackedDomain.domain, | ||
| 21 | isPinned: state.trackedDomain.isPinned, | ||
| 22 | status: state.health.widgetStatus, | ||
| 23 | certDaysRemaining: state.certificateDaysRemaining, | ||
| 24 | lastChange: state.lastChangeDate | ||
| 25 | ) | ||
| 26 | } | ||
| 27 | |||
| 28 | DomainDigWidgetStore.write( | ||
| 29 | DomainDigWidgetData( | ||
| 30 | generatedAt: Date(), | ||
| 31 | totalDomains: snapshot.totalDomains, | ||
| 32 | healthyCount: snapshot.healthyCount, | ||
| 33 | warningCount: snapshot.warningCount, | ||
| 34 | criticalCount: snapshot.criticalCount, | ||
| 35 | expiringSoonCount: snapshot.expiringSoonCount, | ||
| 36 | unreachableCount: snapshot.unreachableCount, | ||
| 37 | domains: Array(domains) | ||
| 38 | ) | ||
| 39 | ) | ||
| 40 | WidgetCenter.shared.reloadAllTimelines() | ||
| 41 | } | ||
| 42 | } | ||
| 43 | |||
| 44 | private extension DomainHealth { | ||
| 45 | var widgetStatus: DomainDigWidgetStatus { | ||
| 46 | switch self { | ||
| 47 | case .healthy: return .healthy | ||
| 48 | case .warning: return .warning | ||
| 49 | case .critical: return .critical | ||
| 50 | } | ||
| 51 | } | ||
| 52 | |||
| 53 | var widgetSeverityRank: Int { | ||
| 54 | switch self { | ||
| 55 | case .healthy: return 0 | ||
| 56 | case .warning: return 1 | ||
| 57 | case .critical: return 2 | ||
| 58 | } | ||
| 59 | } | ||
| 60 | } | ||
DomainDig/DomainViewModel.swift +1
| @@ -2582,6 +2582,7 @@ final class DomainViewModel { | |||
| 2582 | DomainDataPortabilityService.saveTrackedDomains(trackedDomains) | 2582 | DomainDataPortabilityService.saveTrackedDomains(trackedDomains) |
| 2583 | CloudSyncService.shared.scheduleSyncIfNeeded() | 2583 | CloudSyncService.shared.scheduleSyncIfNeeded() |
| 2584 | refreshDataLifecycleSummary() | 2584 | refreshDataLifecycleSummary() |
| 2585 | refreshWidgetData() | ||
| 2585 | } | 2586 | } |
| 2586 | 2587 | ||
| 2587 | private func beginBulkPersistenceDeferral() { | 2588 | private func beginBulkPersistenceDeferral() { |
DomainDig/RootTabView.swift +19
| @@ -12,6 +12,7 @@ struct RootTabView: View { | |||
| 12 | @Bindable var viewModel: DomainViewModel | 12 | @Bindable var viewModel: DomainViewModel |
| 13 | @State private var purchaseService = PurchaseService.shared | 13 | @State private var purchaseService = PurchaseService.shared |
| 14 | @State private var intentRouter = DomainDigIntentRouter.shared | 14 | @State private var intentRouter = DomainDigIntentRouter.shared |
| 15 | @State private var detailDomain: TrackedDomain? | ||
| 15 | @State private var selectedTab: RootTab = FeatureAccessService.currentTier == .free ? .inspect : .dashboard | 16 | @State private var selectedTab: RootTab = FeatureAccessService.currentTier == .free ? .inspect : .dashboard |
| 16 | 17 | ||
| 17 | var body: some View { | 18 | var body: some View { |
| @@ -83,6 +84,11 @@ struct RootTabView: View { | |||
| 83 | selectedTab = .dashboard | 84 | selectedTab = .dashboard |
| 84 | } | 85 | } |
| 85 | } | 86 | } |
| 87 | .sheet(item: $detailDomain) { trackedDomain in | ||
| 88 | NavigationStack { | ||
| 89 | TrackedDomainDetailView(viewModel: viewModel, trackedDomain: trackedDomain) | ||
| 90 | } | ||
| 91 | } | ||
| 86 | .onOpenURL { url in | 92 | .onOpenURL { url in |
| 87 | guard let action = DomainDigDeepLink.action(from: url) else { return } | 93 | guard let action = DomainDigDeepLink.action(from: url) else { return } |
| 88 | perform(action) | 94 | perform(action) |
| @@ -113,6 +119,19 @@ struct RootTabView: View { | |||
| 113 | if viewModel.trackDomain(domain: domain, availabilityStatus: nil) { | 119 | if viewModel.trackDomain(domain: domain, availabilityStatus: nil) { |
| 114 | selectedTab = .dashboard | 120 | selectedTab = .dashboard |
| 115 | } | 121 | } |
| 122 | case let .detail(domain): | ||
| 123 | // Open the tracked domain's detail (e.g. from a widget tap). If it is | ||
| 124 | // no longer tracked, fall back to inspecting it. | ||
| 125 | if let tracked = viewModel.trackedDomains.first( | ||
| 126 | where: { $0.domain.caseInsensitiveCompare(domain) == .orderedSame } | ||
| 127 | ) { | ||
| 128 | detailDomain = tracked | ||
| 129 | } else { | ||
| 130 | perform(.inspect(domain)) | ||
| 131 | } | ||
| 132 | case .sweep: | ||
| 133 | selectedTab = .dashboard | ||
| 134 | viewModel.refreshAllTrackedDomains() | ||
| 116 | } | 135 | } |
| 117 | } | 136 | } |
| 118 | } | 137 | } |
DomainDigWidget/DomainDigPortfolioWidget.swift added +189
| @@ -0,0 +1,189 @@ | |||
| 1 | import SwiftUI | ||
| 2 | import WidgetKit | ||
| 3 | |||
| 4 | struct DomainDigEntry: TimelineEntry { | ||
| 5 | let date: Date | ||
| 6 | let data: DomainDigWidgetData | ||
| 7 | } | ||
| 8 | |||
| 9 | struct DomainDigProvider: TimelineProvider { | ||
| 10 | func placeholder(in context: Context) -> DomainDigEntry { | ||
| 11 | DomainDigEntry(date: Date(), data: .placeholder) | ||
| 12 | } | ||
| 13 | |||
| 14 | func getSnapshot(in context: Context, completion: @escaping (DomainDigEntry) -> Void) { | ||
| 15 | let data = context.isPreview ? .placeholder : (DomainDigWidgetStore.read() ?? .placeholder) | ||
| 16 | completion(DomainDigEntry(date: Date(), data: data)) | ||
| 17 | } | ||
| 18 | |||
| 19 | func getTimeline(in context: Context, completion: @escaping (Timeline<DomainDigEntry>) -> Void) { | ||
| 20 | let data = DomainDigWidgetStore.read() ?? .empty | ||
| 21 | let entry = DomainDigEntry(date: Date(), data: data) | ||
| 22 | // The app reloads timelines on foreground and on watchlist changes; this | ||
| 23 | // periodic refresh is a backstop so cert countdowns stay roughly current. | ||
| 24 | let next = Calendar.current.date(byAdding: .hour, value: 6, to: Date()) | ||
| 25 | ?? Date().addingTimeInterval(6 * 3600) | ||
| 26 | completion(Timeline(entries: [entry], policy: .after(next))) | ||
| 27 | } | ||
| 28 | } | ||
| 29 | |||
| 30 | struct DomainDigPortfolioWidget: Widget { | ||
| 31 | let kind = "DomainDigPortfolioWidget" | ||
| 32 | |||
| 33 | var body: some WidgetConfiguration { | ||
| 34 | StaticConfiguration(kind: kind, provider: DomainDigProvider()) { entry in | ||
| 35 | DomainDigWidgetView(data: entry.data) | ||
| 36 | .containerBackground(.fill.tertiary, for: .widget) | ||
| 37 | } | ||
| 38 | .configurationDisplayName("Domain Portfolio") | ||
| 39 | .description("Health and certificate status for your tracked domains.") | ||
| 40 | .supportedFamilies([.systemSmall, .systemMedium, .systemLarge]) | ||
| 41 | } | ||
| 42 | } | ||
| 43 | |||
| 44 | struct DomainDigWidgetView: View { | ||
| 45 | @Environment(\.widgetFamily) private var family | ||
| 46 | let data: DomainDigWidgetData | ||
| 47 | |||
| 48 | var body: some View { | ||
| 49 | if data.totalDomains == 0 { | ||
| 50 | emptyState | ||
| 51 | } else { | ||
| 52 | switch family { | ||
| 53 | case .systemSmall: | ||
| 54 | smallView | ||
| 55 | default: | ||
| 56 | mediumOrLargeView | ||
| 57 | } | ||
| 58 | } | ||
| 59 | } | ||
| 60 | |||
| 61 | private var emptyState: some View { | ||
| 62 | VStack(spacing: 6) { | ||
| 63 | Image(systemName: "magnifyingglass") | ||
| 64 | .font(.title2) | ||
| 65 | .foregroundStyle(.secondary) | ||
| 66 | Text("No tracked domains") | ||
| 67 | .font(.caption) | ||
| 68 | .foregroundStyle(.secondary) | ||
| 69 | .multilineTextAlignment(.center) | ||
| 70 | } | ||
| 71 | } | ||
| 72 | |||
| 73 | // MARK: Small | ||
| 74 | |||
| 75 | private var smallView: some View { | ||
| 76 | VStack(alignment: .leading, spacing: 8) { | ||
| 77 | HStack(spacing: 4) { | ||
| 78 | Image(systemName: "shield.lefthalf.filled") | ||
| 79 | Text("DomainDig") | ||
| 80 | .fontWeight(.semibold) | ||
| 81 | Spacer() | ||
| 82 | } | ||
| 83 | .font(.caption2) | ||
| 84 | .foregroundStyle(.secondary) | ||
| 85 | |||
| 86 | Text("\(data.totalDomains)") | ||
| 87 | .font(.system(size: 34, weight: .bold, design: .rounded)) | ||
| 88 | Text("tracked") | ||
| 89 | .font(.caption2) | ||
| 90 | .foregroundStyle(.secondary) | ||
| 91 | |||
| 92 | Spacer(minLength: 0) | ||
| 93 | |||
| 94 | HStack(spacing: 10) { | ||
| 95 | countPill(data.healthyCount, .green) | ||
| 96 | countPill(data.warningCount, .orange) | ||
| 97 | countPill(data.criticalCount, .red) | ||
| 98 | } | ||
| 99 | } | ||
| 100 | } | ||
| 101 | |||
| 102 | private func countPill(_ value: Int, _ color: Color) -> some View { | ||
| 103 | HStack(spacing: 3) { | ||
| 104 | Circle().fill(color).frame(width: 7, height: 7) | ||
| 105 | Text("\(value)").font(.caption).fontWeight(.medium) | ||
| 106 | } | ||
| 107 | } | ||
| 108 | |||
| 109 | // MARK: Medium / Large | ||
| 110 | |||
| 111 | private var mediumOrLargeView: some View { | ||
| 112 | VStack(alignment: .leading, spacing: 10) { | ||
| 113 | HStack { | ||
| 114 | Label("Domain Portfolio", systemImage: "shield.lefthalf.filled") | ||
| 115 | .font(.caption) | ||
| 116 | .fontWeight(.semibold) | ||
| 117 | .foregroundStyle(.secondary) | ||
| 118 | Spacer() | ||
| 119 | Text("\(data.totalDomains) tracked") | ||
| 120 | .font(.caption2) | ||
| 121 | .foregroundStyle(.secondary) | ||
| 122 | } | ||
| 123 | |||
| 124 | HStack(spacing: 12) { | ||
| 125 | summaryStat(data.healthyCount, "Healthy", .green) | ||
| 126 | summaryStat(data.warningCount, "Warning", .orange) | ||
| 127 | summaryStat(data.criticalCount, "Critical", .red) | ||
| 128 | summaryStat(data.expiringSoonCount, "Expiring", .yellow) | ||
| 129 | } | ||
| 130 | |||
| 131 | Divider() | ||
| 132 | |||
| 133 | VStack(spacing: 6) { | ||
| 134 | ForEach(data.domains.prefix(family == .systemLarge ? 6 : 3)) { domain in | ||
| 135 | Link(destination: DomainDigDeepLink.url(for: .detail(domain.domain))) { | ||
| 136 | domainRow(domain) | ||
| 137 | } | ||
| 138 | } | ||
| 139 | } | ||
| 140 | Spacer(minLength: 0) | ||
| 141 | } | ||
| 142 | } | ||
| 143 | |||
| 144 | private func summaryStat(_ value: Int, _ label: String, _ color: Color) -> some View { | ||
| 145 | VStack(alignment: .leading, spacing: 1) { | ||
| 146 | Text("\(value)") | ||
| 147 | .font(.headline) | ||
| 148 | .foregroundStyle(color) | ||
| 149 | Text(label) | ||
| 150 | .font(.system(size: 9)) | ||
| 151 | .foregroundStyle(.secondary) | ||
| 152 | } | ||
| 153 | .frame(maxWidth: .infinity, alignment: .leading) | ||
| 154 | } | ||
| 155 | |||
| 156 | private func domainRow(_ domain: DomainDigWidgetDomain) -> some View { | ||
| 157 | HStack(spacing: 6) { | ||
| 158 | Circle() | ||
| 159 | .fill(color(for: domain.status)) | ||
| 160 | .frame(width: 8, height: 8) | ||
| 161 | if domain.isPinned { | ||
| 162 | Image(systemName: "pin.fill") | ||
| 163 | .font(.system(size: 8)) | ||
| 164 | .foregroundStyle(.secondary) | ||
| 165 | } | ||
| 166 | Text(domain.domain) | ||
| 167 | .font(.caption) | ||
| 168 | .lineLimit(1) | ||
| 169 | Spacer(minLength: 4) | ||
| 170 | Text(certLabel(for: domain)) | ||
| 171 | .font(.caption2) | ||
| 172 | .foregroundStyle(.secondary) | ||
| 173 | } | ||
| 174 | } | ||
| 175 | |||
| 176 | private func certLabel(for domain: DomainDigWidgetDomain) -> String { | ||
| 177 | guard let days = domain.certDaysRemaining else { return "—" } | ||
| 178 | if days < 0 { return "expired" } | ||
| 179 | return "\(days)d" | ||
| 180 | } | ||
| 181 | |||
| 182 | private func color(for status: DomainDigWidgetStatus) -> Color { | ||
| 183 | switch status { | ||
| 184 | case .healthy: return .green | ||
| 185 | case .warning: return .orange | ||
| 186 | case .critical: return .red | ||
| 187 | } | ||
| 188 | } | ||
| 189 | } | ||
DomainDigWidget/DomainDigWidget.entitlements added +10
| @@ -0,0 +1,10 @@ | |||
| 1 | <?xml version="1.0" encoding="UTF-8"?> | ||
| 2 | <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | ||
| 3 | <plist version="1.0"> | ||
| 4 | <dict> | ||
| 5 | <key>com.apple.security.application-groups</key> | ||
| 6 | <array> | ||
| 7 | <string>group.net.cleberg.DomainDig</string> | ||
| 8 | </array> | ||
| 9 | </dict> | ||
| 10 | </plist> | ||
DomainDigWidget/DomainDigWidgetBundle.swift added +9
| @@ -0,0 +1,9 @@ | |||
| 1 | import SwiftUI | ||
| 2 | import WidgetKit | ||
| 3 | |||
| 4 | @main | ||
| 5 | struct DomainDigWidgetBundle: WidgetBundle { | ||
| 6 | var body: some Widget { | ||
| 7 | DomainDigPortfolioWidget() | ||
| 8 | } | ||
| 9 | } | ||
DomainDigWidget/Info.plist added +29
| @@ -0,0 +1,29 @@ | |||
| 1 | <?xml version="1.0" encoding="UTF-8"?> | ||
| 2 | <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | ||
| 3 | <plist version="1.0"> | ||
| 4 | <dict> | ||
| 5 | <key>CFBundleDevelopmentRegion</key> | ||
| 6 | <string>$(DEVELOPMENT_LANGUAGE)</string> | ||
| 7 | <key>CFBundleDisplayName</key> | ||
| 8 | <string>DomainDig</string> | ||
| 9 | <key>CFBundleExecutable</key> | ||
| 10 | <string>$(EXECUTABLE_NAME)</string> | ||
| 11 | <key>CFBundleIdentifier</key> | ||
| 12 | <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string> | ||
| 13 | <key>CFBundleInfoDictionaryVersion</key> | ||
| 14 | <string>6.0</string> | ||
| 15 | <key>CFBundleName</key> | ||
| 16 | <string>$(PRODUCT_NAME)</string> | ||
| 17 | <key>CFBundlePackageType</key> | ||
| 18 | <string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string> | ||
| 19 | <key>CFBundleShortVersionString</key> | ||
| 20 | <string>$(MARKETING_VERSION)</string> | ||
| 21 | <key>CFBundleVersion</key> | ||
| 22 | <string>$(CURRENT_PROJECT_VERSION)</string> | ||
| 23 | <key>NSExtension</key> | ||
| 24 | <dict> | ||
| 25 | <key>NSExtensionPointIdentifier</key> | ||
| 26 | <string>com.apple.widgetkit-extension</string> | ||
| 27 | </dict> | ||
| 28 | </dict> | ||
| 29 | </plist> | ||
Shared/DomainDigDeepLink.swift added +71
| @@ -0,0 +1,71 @@ | |||
| 1 | import Foundation | ||
| 2 | |||
| 3 | /// Shared builder/parser for the `domaindig://` URL scheme, used by the intents | ||
| 4 | /// and app (to open/route) and by the widget (to deep-link into a domain). | ||
| 5 | /// | ||
| 6 | /// Lives in `Shared/` so it compiles into both the app and the widget target. | ||
| 7 | /// It relies only on Foundation and no actor isolation, so it is safe in the | ||
| 8 | /// widget extension (`APPLICATION_EXTENSION_API_ONLY`). | ||
| 9 | enum DomainDigDeepLink { | ||
| 10 | static let scheme = "domaindig" | ||
| 11 | |||
| 12 | enum Action: Equatable { | ||
| 13 | case inspect(String) | ||
| 14 | case watch(String) | ||
| 15 | case detail(String) | ||
| 16 | case sweep | ||
| 17 | |||
| 18 | var host: String { | ||
| 19 | switch self { | ||
| 20 | case .inspect: return "inspect" | ||
| 21 | case .watch: return "watch" | ||
| 22 | case .detail: return "domain" | ||
| 23 | case .sweep: return "sweep" | ||
| 24 | } | ||
| 25 | } | ||
| 26 | |||
| 27 | /// The domain the action targets, if any. `.sweep` has no domain. | ||
| 28 | var domain: String? { | ||
| 29 | switch self { | ||
| 30 | case let .inspect(domain), let .watch(domain), let .detail(domain): | ||
| 31 | return domain | ||
| 32 | case .sweep: | ||
| 33 | return nil | ||
| 34 | } | ||
| 35 | } | ||
| 36 | } | ||
| 37 | |||
| 38 | static func url(for action: Action) -> URL { | ||
| 39 | var components = URLComponents() | ||
| 40 | components.scheme = scheme | ||
| 41 | components.host = action.host | ||
| 42 | if let domain = action.domain { | ||
| 43 | components.queryItems = [URLQueryItem(name: "domain", value: domain)] | ||
| 44 | } | ||
| 45 | // The scheme and host are fixed and any domain is percent-encoded by | ||
| 46 | // URLComponents, so this is always a valid URL. | ||
| 47 | return components.url! | ||
| 48 | } | ||
| 49 | |||
| 50 | static func action(from url: URL) -> Action? { | ||
| 51 | guard url.scheme == scheme else { return nil } | ||
| 52 | |||
| 53 | if url.host() == "sweep" { | ||
| 54 | return .sweep | ||
| 55 | } | ||
| 56 | |||
| 57 | let domain = URLComponents(url: url, resolvingAgainstBaseURL: false)? | ||
| 58 | .queryItems? | ||
| 59 | .first { $0.name == "domain" }? | ||
| 60 | .value? | ||
| 61 | .trimmingCharacters(in: .whitespacesAndNewlines) ?? "" | ||
| 62 | guard !domain.isEmpty else { return nil } | ||
| 63 | |||
| 64 | switch url.host() { | ||
| 65 | case "inspect": return .inspect(domain) | ||
| 66 | case "watch": return .watch(domain) | ||
| 67 | case "domain": return .detail(domain) | ||
| 68 | default: return nil | ||
| 69 | } | ||
| 70 | } | ||
| 71 | } | ||
Shared/DomainDigWidgetData.swift added +82
| @@ -0,0 +1,82 @@ | |||
| 1 | import Foundation | ||
| 2 | |||
| 3 | /// App Group + snapshot shared between the app (writer) and the widget (reader). | ||
| 4 | /// | ||
| 5 | /// Deliberately self-contained: it references no app-target types (TrackedDomain, | ||
| 6 | /// PortfolioSnapshot, DomainHealth, …) so it compiles unchanged into the widget | ||
| 7 | /// extension. The app maps its richer types into these DTOs. | ||
| 8 | enum DomainDigWidgetStore { | ||
| 9 | static let appGroupID = "group.net.cleberg.DomainDig" | ||
| 10 | private static let key = "widgetData" | ||
| 11 | |||
| 12 | private static var defaults: UserDefaults? { | ||
| 13 | UserDefaults(suiteName: appGroupID) | ||
| 14 | } | ||
| 15 | |||
| 16 | static func write(_ data: DomainDigWidgetData) { | ||
| 17 | guard let defaults, let encoded = try? JSONEncoder().encode(data) else { return } | ||
| 18 | defaults.set(encoded, forKey: key) | ||
| 19 | } | ||
| 20 | |||
| 21 | static func read() -> DomainDigWidgetData? { | ||
| 22 | guard let defaults, | ||
| 23 | let encoded = defaults.data(forKey: key), | ||
| 24 | let data = try? JSONDecoder().decode(DomainDigWidgetData.self, from: encoded) | ||
| 25 | else { return nil } | ||
| 26 | return data | ||
| 27 | } | ||
| 28 | } | ||
| 29 | |||
| 30 | struct DomainDigWidgetData: Codable, Sendable { | ||
| 31 | var generatedAt: Date | ||
| 32 | var totalDomains: Int | ||
| 33 | var healthyCount: Int | ||
| 34 | var warningCount: Int | ||
| 35 | var criticalCount: Int | ||
| 36 | var expiringSoonCount: Int | ||
| 37 | var unreachableCount: Int | ||
| 38 | var domains: [DomainDigWidgetDomain] | ||
| 39 | |||
| 40 | static let placeholder = DomainDigWidgetData( | ||
| 41 | generatedAt: .distantPast, | ||
| 42 | totalDomains: 3, | ||
| 43 | healthyCount: 2, | ||
| 44 | warningCount: 1, | ||
| 45 | criticalCount: 0, | ||
| 46 | expiringSoonCount: 1, | ||
| 47 | unreachableCount: 0, | ||
| 48 | domains: [ | ||
| 49 | DomainDigWidgetDomain(domain: "example.com", isPinned: true, status: .healthy, certDaysRemaining: 240, lastChange: nil), | ||
| 50 | DomainDigWidgetDomain(domain: "cleberg.net", isPinned: false, status: .warning, certDaysRemaining: 21, lastChange: nil), | ||
| 51 | DomainDigWidgetDomain(domain: "example.org", isPinned: false, status: .healthy, certDaysRemaining: 88, lastChange: nil) | ||
| 52 | ] | ||
| 53 | ) | ||
| 54 | |||
| 55 | /// Empty state used before the app has written any data. | ||
| 56 | static let empty = DomainDigWidgetData( | ||
| 57 | generatedAt: .distantPast, | ||
| 58 | totalDomains: 0, | ||
| 59 | healthyCount: 0, | ||
| 60 | warningCount: 0, | ||
| 61 | criticalCount: 0, | ||
| 62 | expiringSoonCount: 0, | ||
| 63 | unreachableCount: 0, | ||
| 64 | domains: [] | ||
| 65 | ) | ||
| 66 | } | ||
| 67 | |||
| 68 | struct DomainDigWidgetDomain: Codable, Sendable, Identifiable { | ||
| 69 | var domain: String | ||
| 70 | var isPinned: Bool | ||
| 71 | var status: DomainDigWidgetStatus | ||
| 72 | var certDaysRemaining: Int? | ||
| 73 | var lastChange: Date? | ||
| 74 | |||
| 75 | var id: String { domain } | ||
| 76 | } | ||
| 77 | |||
| 78 | enum DomainDigWidgetStatus: String, Codable, Sendable { | ||
| 79 | case healthy | ||
| 80 | case warning | ||
| 81 | case critical | ||
| 82 | } | ||