diff --git a/AEPCore.podspec b/AEPCore.podspec index 23c90fb34..1fce3855c 100644 --- a/AEPCore.podspec +++ b/AEPCore.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "AEPCore" - s.version = "5.10.0" + s.version = "5.11.0" s.summary = "Core library for Adobe Experience Platform Mobile SDK. Written and maintained by Adobe." s.description = <<-DESC @@ -19,7 +19,7 @@ Pod::Spec.new do |s| s.pod_target_xcconfig = { 'BUILD_LIBRARY_FOR_DISTRIBUTION' => 'YES' } s.dependency 'AEPRulesEngine', '>= 5.0.0', '< 6.0.0' - s.dependency 'AEPServices', '>= 5.10.0', '< 6.0.0' + s.dependency 'AEPServices', '>= 5.11.0', '< 6.0.0' s.source_files = 'AEPCore/Sources/**/*.swift' s.resource_bundles = { 'AEPCore_Resources' => ['AEPCore/Sources/PrivacyInfo.xcprivacy'] } diff --git a/AEPCore.xcodeproj/project.pbxproj b/AEPCore.xcodeproj/project.pbxproj index 25d3661f2..d7c92312e 100644 --- a/AEPCore.xcodeproj/project.pbxproj +++ b/AEPCore.xcodeproj/project.pbxproj @@ -163,6 +163,8 @@ 3F0397DC24BE5FF30019F095 /* UserDefaultsNamedCollection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F0397BD24BE5FF30019F095 /* UserDefaultsNamedCollection.swift */; }; 3F0397DD24BE5FF30019F095 /* ApplicationSystemInfoService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F0397BE24BE5FF30019F095 /* ApplicationSystemInfoService.swift */; }; 3F0397DE24BE5FF30019F095 /* SystemInfoService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F0397BF24BE5FF30019F095 /* SystemInfoService.swift */; }; + B8A2C00424BE5FF30019F13 /* NetworkPathMonitorProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8A2C03024BE5FF30019F13 /* NetworkPathMonitorProvider.swift */; }; + B8A2C00724BE5FF30019F16 /* NetworkingIsAvailableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8A2C06024BE5FF30019F16 /* NetworkingIsAvailableTests.swift */; }; 3F0397DF24BE5FF30019F095 /* URLOpening.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F0397C024BE5FF30019F095 /* URLOpening.swift */; }; 3F0397E024BE5FF30019F095 /* URLService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F0397C124BE5FF30019F095 /* URLService.swift */; }; 3F0397F224BE60910019F095 /* HitProcessing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F0397E324BE60910019F095 /* HitProcessing.swift */; }; @@ -962,6 +964,8 @@ 3F0397BD24BE5FF30019F095 /* UserDefaultsNamedCollection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserDefaultsNamedCollection.swift; sourceTree = ""; }; 3F0397BE24BE5FF30019F095 /* ApplicationSystemInfoService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ApplicationSystemInfoService.swift; sourceTree = ""; }; 3F0397BF24BE5FF30019F095 /* SystemInfoService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SystemInfoService.swift; sourceTree = ""; }; + B8A2C03024BE5FF30019F13 /* NetworkPathMonitorProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkPathMonitorProvider.swift; sourceTree = ""; }; + B8A2C06024BE5FF30019F16 /* NetworkingIsAvailableTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkingIsAvailableTests.swift; sourceTree = ""; }; 3F0397C024BE5FF30019F095 /* URLOpening.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLOpening.swift; sourceTree = ""; }; 3F0397C124BE5FF30019F095 /* URLService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLService.swift; sourceTree = ""; }; 3F0397E324BE60910019F095 /* HitProcessing.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HitProcessing.swift; sourceTree = ""; }; @@ -1741,6 +1745,7 @@ 3F0397BE24BE5FF30019F095 /* ApplicationSystemInfoService.swift */, 3F0397A524BE5FF30019F095 /* ServiceProvider.swift */, 3F0397BF24BE5FF30019F095 /* SystemInfoService.swift */, + B8A2C03024BE5FF30019F13 /* NetworkPathMonitorProvider.swift */, 3F0397C024BE5FF30019F095 /* URLOpening.swift */, 3F0397C124BE5FF30019F095 /* URLService.swift */, 78B36A7725CA0D7700D6D25F /* UIUtils.swift */, @@ -1986,6 +1991,7 @@ 3F03980724BE61520019F095 /* FileSystemNamedCollectionTest.swift */, 78AA4EB92502DF2200205AE9 /* ZipArchiveTest.swift */, 78F2E2D327FF7B900073CE00 /* ServiceProviderTests.swift */, + B8A2C06024BE5FF30019F16 /* NetworkingIsAvailableTests.swift */, 75550A3E2A9930A500747BB7 /* NamedCollectionDataStoreFunctionalTests.swift */, ); path = services; @@ -3807,6 +3813,7 @@ 3F0397D524BE5FF30019F095 /* DataQueue.swift in Sources */, BB00E26D24D9BF6C00C578C1 /* URLUtility.swift in Sources */, 3F0397DE24BE5FF30019F095 /* SystemInfoService.swift in Sources */, + B8A2C00424BE5FF30019F13 /* NetworkPathMonitorProvider.swift in Sources */, 3F0397F624BE60910019F095 /* FileUnzipper.swift in Sources */, 3F0397D424BE5FF30019F095 /* SQLiteWrapper.swift in Sources */, 3F0397D024BE5FF30019F095 /* HttpMethod.swift in Sources */, @@ -3849,6 +3856,7 @@ 3F03981824BE61520019F095 /* DataQueueServiceTests.swift in Sources */, 3F03981B24BE61520019F095 /* FileSystemNamedCollectionTest.swift in Sources */, 78F2E2D427FF7B900073CE00 /* ServiceProviderTests.swift in Sources */, + B8A2C00724BE5FF30019F16 /* NetworkingIsAvailableTests.swift in Sources */, 24B860492CB5B0A00072868A /* HttpConnectionTests.swift in Sources */, 2EF54C292BF6C87500685C07 /* URLError+RecoverableTests.swift in Sources */, 786C004825B8F43E00F26D34 /* DefaultHeadersFormatterTests.swift in Sources */, @@ -4386,7 +4394,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 5.10.0; + MARKETING_VERSION = 5.11.0; PRODUCT_BUNDLE_IDENTIFIER = com.adobe.aep.core; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; @@ -4420,7 +4428,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 5.10.0; + MARKETING_VERSION = 5.11.0; PRODUCT_BUNDLE_IDENTIFIER = com.adobe.aep.core; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; @@ -4512,7 +4520,7 @@ "$(inherited)", /usr/lib/swift, ); - MARKETING_VERSION = 5.10.0; + MARKETING_VERSION = 5.11.0; PRODUCT_BUNDLE_IDENTIFIER = com.adobe.aep.signal; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; @@ -4552,7 +4560,7 @@ "$(inherited)", /usr/lib/swift, ); - MARKETING_VERSION = 5.10.0; + MARKETING_VERSION = 5.11.0; PRODUCT_BUNDLE_IDENTIFIER = com.adobe.aep.signal; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; @@ -4627,7 +4635,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 5.10.0; + MARKETING_VERSION = 5.11.0; PRODUCT_BUNDLE_IDENTIFIER = come.adobe.aep.services; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; @@ -4658,7 +4666,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 5.10.0; + MARKETING_VERSION = 5.11.0; PRODUCT_BUNDLE_IDENTIFIER = come.adobe.aep.services; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; @@ -4802,7 +4810,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 5.10.0; + MARKETING_VERSION = 5.11.0; PRODUCT_BUNDLE_IDENTIFIER = com.adobe.aep.AEPServicesMocks; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; @@ -4833,7 +4841,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 5.10.0; + MARKETING_VERSION = 5.11.0; PRODUCT_BUNDLE_IDENTIFIER = com.adobe.aep.AEPServicesMocks; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; @@ -4864,7 +4872,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 5.10.0; + MARKETING_VERSION = 5.11.0; PRODUCT_BUNDLE_IDENTIFIER = com.adobe.aep.lifecycle; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; @@ -4896,7 +4904,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 5.10.0; + MARKETING_VERSION = 5.11.0; PRODUCT_BUNDLE_IDENTIFIER = com.adobe.aep.lifecycle; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; @@ -4974,7 +4982,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 5.10.0; + MARKETING_VERSION = 5.11.0; PRODUCT_BUNDLE_IDENTIFIER = com.adobe.aep.identity; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; @@ -5006,7 +5014,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 5.10.0; + MARKETING_VERSION = 5.11.0; PRODUCT_BUNDLE_IDENTIFIER = com.adobe.aep.identity; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; @@ -5085,7 +5093,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 5.10.0; + MARKETING_VERSION = 5.11.0; PRODUCT_BUNDLE_IDENTIFIER = com.adobe.aep.AEPCoreMocks; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; @@ -5118,7 +5126,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 5.10.0; + MARKETING_VERSION = 5.11.0; PRODUCT_BUNDLE_IDENTIFIER = com.adobe.aep.AEPCoreMocks; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; diff --git a/AEPCore/Sources/configuration/ConfigurationConstants.swift b/AEPCore/Sources/configuration/ConfigurationConstants.swift index 5cbb3c7f0..3de30f7c3 100644 --- a/AEPCore/Sources/configuration/ConfigurationConstants.swift +++ b/AEPCore/Sources/configuration/ConfigurationConstants.swift @@ -15,7 +15,7 @@ import Foundation struct ConfigurationConstants { static let EXTENSION_NAME = "com.adobe.module.configuration" static let FRIENDLY_NAME = "Configuration" - static let EXTENSION_VERSION = "5.10.0" + static let EXTENSION_VERSION = "5.11.0" static let DATA_STORE_NAME = EXTENSION_NAME diff --git a/AEPCore/Sources/eventhub/EventHubConstants.swift b/AEPCore/Sources/eventhub/EventHubConstants.swift index 0b127ecd7..e239feffe 100644 --- a/AEPCore/Sources/eventhub/EventHubConstants.swift +++ b/AEPCore/Sources/eventhub/EventHubConstants.swift @@ -17,7 +17,7 @@ enum EventHubConstants { static let XDM_STATE_CHANGE = "Shared state change (XDM)" static let NAME = "com.adobe.module.eventhub" static let FRIENDLY_NAME = "EventHub" - static let VERSION_NUMBER = "5.10.0" + static let VERSION_NUMBER = "5.11.0" enum EventDataKeys { static let VERSION = "version" diff --git a/AEPCore/Tests/MobileCoreTests.swift b/AEPCore/Tests/MobileCoreTests.swift index 7f9a24362..857889155 100644 --- a/AEPCore/Tests/MobileCoreTests.swift +++ b/AEPCore/Tests/MobileCoreTests.swift @@ -208,7 +208,7 @@ class MobileCoreTests: XCTestCase { "friendlyName" : "mockExtension" }, "com.adobe.module.configuration" : { - "version" : "5.10.0", + "version" : "5.11.0", "friendlyName" : "Configuration" }, diff --git a/AEPIdentity.podspec b/AEPIdentity.podspec index e83c823c4..247d5a3e0 100644 --- a/AEPIdentity.podspec +++ b/AEPIdentity.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "AEPIdentity" - s.version = "5.10.0" + s.version = "5.11.0" s.summary = "Identity extension for Adobe Experience Platform Mobile SDK. Written and maintained by Adobe." s.description = <<-DESC @@ -20,7 +20,7 @@ Pod::Spec.new do |s| s.pod_target_xcconfig = { 'BUILD_LIBRARY_FOR_DISTRIBUTION' => 'YES' } - s.dependency 'AEPCore', '>= 5.10.0', '< 6.0.0' + s.dependency 'AEPCore', '>= 5.11.0', '< 6.0.0' end diff --git a/AEPIdentity/Sources/IdentityConstants.swift b/AEPIdentity/Sources/IdentityConstants.swift index 173aaec63..c038b4b30 100644 --- a/AEPIdentity/Sources/IdentityConstants.swift +++ b/AEPIdentity/Sources/IdentityConstants.swift @@ -14,7 +14,7 @@ import Foundation enum IdentityConstants { static let EXTENSION_NAME = "com.adobe.module.identity" static let FRIENDLY_NAME = "Identity" - static let EXTENSION_VERSION = "5.10.0" + static let EXTENSION_VERSION = "5.11.0" static let DATASTORE_NAME = EXTENSION_NAME diff --git a/AEPLifecycle.podspec b/AEPLifecycle.podspec index 45736cfa8..83b259c91 100644 --- a/AEPLifecycle.podspec +++ b/AEPLifecycle.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "AEPLifecycle" - s.version = "5.10.0" + s.version = "5.11.0" s.summary = "Lifecycle extension for Adobe Experience Platform Mobile SDK. Written and maintained by Adobe." s.description = <<-DESC @@ -20,7 +20,7 @@ Pod::Spec.new do |s| s.pod_target_xcconfig = { 'BUILD_LIBRARY_FOR_DISTRIBUTION' => 'YES' } - s.dependency 'AEPCore', '>= 5.10.0', '< 6.0.0' + s.dependency 'AEPCore', '>= 5.11.0', '< 6.0.0' end diff --git a/AEPLifecycle/Sources/LifecycleConstants.swift b/AEPLifecycle/Sources/LifecycleConstants.swift index 8fb9fbfbc..2575edd0b 100644 --- a/AEPLifecycle/Sources/LifecycleConstants.swift +++ b/AEPLifecycle/Sources/LifecycleConstants.swift @@ -16,7 +16,7 @@ import Foundation enum LifecycleConstants { static let EXTENSION_NAME = "com.adobe.module.lifecycle" static let FRIENDLY_NAME = "Lifecycle" - static let EXTENSION_VERSION = "5.10.0" + static let EXTENSION_VERSION = "5.11.0" static let DATA_STORE_NAME = LifecycleConstants.EXTENSION_NAME diff --git a/AEPServices.podspec b/AEPServices.podspec index 766dc020e..e71f3627a 100644 --- a/AEPServices.podspec +++ b/AEPServices.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "AEPServices" - s.version = "5.10.0" + s.version = "5.11.0" s.summary = "Servcies library for Adobe Experience Platform Mobile SDK. Written and maintained by Adobe." s.description = <<-DESC diff --git a/AEPServices/Sources/NetworkPathMonitorProvider.swift b/AEPServices/Sources/NetworkPathMonitorProvider.swift new file mode 100644 index 000000000..0e2471e28 --- /dev/null +++ b/AEPServices/Sources/NetworkPathMonitorProvider.swift @@ -0,0 +1,35 @@ +/* + Copyright 2026 Adobe. All rights reserved. + This file is licensed to you under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. You may obtain a copy + of the License at http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software distributed under + the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + OF ANY KIND, either express or implied. See the License for the specific language + governing permissions and limitations under the License. + */ + +import Foundation +import Network + +/// Default device path check backed by `NWPathMonitor`, used by `Networking`'s default +/// `isInternetAvailable()` implementation. A single shared, long-lived monitor is used (rather than +/// creating a new one per call) so `currentPath` stays continuously up to date. +final class NetworkPathMonitorProvider { + static let shared = NetworkPathMonitorProvider() + + private let monitor = NWPathMonitor() + private let queue = DispatchQueue(label: "com.adobe.networkPathMonitor.queue") + + private init() { + monitor.start(queue: queue) + } + + func isPathAvailable() -> Bool { + return queue.sync { + monitor.currentPath.status == .satisfied + } + } + +} diff --git a/AEPServices/Sources/network/Networking.swift b/AEPServices/Sources/network/Networking.swift index c2cb55271..3838c4a3e 100644 --- a/AEPServices/Sources/network/Networking.swift +++ b/AEPServices/Sources/network/Networking.swift @@ -19,4 +19,16 @@ public protocol Networking { /// - completionHandler:Optional completion handler which is called once the `HttpConnection` is available; it can be called from an `HttpConnectionPerformer` if `NetworkServiceOverrider` is enabled. /// In case of a network error, timeout or an unexpected error, the `HttpConnection` is nil func connectAsync(networkRequest: NetworkRequest, completionHandler: ((HttpConnection) -> Void)?) + + /// Returns whether network-bound SDK work should proceed right now. The default implementation checks + /// device-level path status only (`NWPathMonitor`). Override this in a custom `Networking` conformer + /// (see: overriding `NetworkService`, `ServiceProvider.shared.networkService`) to implement custom logic — + /// for example, pinging your own backend's health endpoint instead of relying on device connectivity alone. + func isInternetAvailable() -> Bool +} + +public extension Networking { + func isInternetAvailable() -> Bool { + return NetworkPathMonitorProvider.shared.isPathAvailable() + } } diff --git a/AEPServices/Tests/services/NetworkingIsAvailableTests.swift b/AEPServices/Tests/services/NetworkingIsAvailableTests.swift new file mode 100644 index 000000000..8dbcc8602 --- /dev/null +++ b/AEPServices/Tests/services/NetworkingIsAvailableTests.swift @@ -0,0 +1,107 @@ +/* + Copyright 2026 Adobe. All rights reserved. + This file is licensed to you under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. You may obtain a copy + of the License at http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software distributed under + the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + OF ANY KIND, either express or implied. See the License for the specific language + governing permissions and limitations under the License. + */ + +@testable import AEPServices +import XCTest + +/// Tests for `Networking.isInternetAvailable()`. +/// +/// All deterministic coverage uses mock `Networking` conformers — no test seams live in +/// production code. Custom conformers exercise the override path; the default-implementation +/// tests verify the protocol extension is reachable and behaves correctly end-to-end. +class NetworkingIsAvailableTests: XCTestCase { + + override func tearDown() { + ServiceProvider.shared.reset() + super.tearDown() + } + + // MARK: - Test fixtures + + /// Minimal conformer that does NOT override `isInternetAvailable()` — receives the protocol + /// extension default for free. Source-compatible with all pre-existing `Networking` conformers + /// across AEP repos. + private class MinimalNetworkingConformer: Networking { + func connectAsync(networkRequest: NetworkRequest, completionHandler: ((HttpConnection) -> Void)?) {} + } + + /// Conformer with custom availability logic — the documented override pattern for + /// customers who want their own health-check instead of device connectivity. + private class CustomNetworking: Networking { + var overrideValue: Bool + private(set) var connectAsyncCallCount = 0 + + init(_ value: Bool) { overrideValue = value } + + func connectAsync(networkRequest: NetworkRequest, completionHandler: ((HttpConnection) -> Void)?) { + connectAsyncCallCount += 1 + } + + func isInternetAvailable() -> Bool { return overrideValue } + } + + // MARK: - Default implementation (live NWPathMonitor) + + func testDefaultImpl_returnsValidBool() { + // Verifies the protocol extension is wired up and returns without crashing. + // Result depends on the test host's network state (intentional). + let result = MinimalNetworkingConformer().isInternetAvailable() + XCTAssertTrue(result == true || result == false) + } + + func testDefaultImpl_repeatedReads_noCrashOrLeak() { + let conformer = MinimalNetworkingConformer() + for _ in 0..<1_000 { _ = conformer.isInternetAvailable() } + } + + func testDefaultImpl_concurrentReads_noDataRaceOrDeadlock() { + let conformer = MinimalNetworkingConformer() + let group = DispatchGroup() + let q = DispatchQueue(label: "com.adobe.test.concurrent", attributes: .concurrent) + for _ in 0..<50 { + group.enter() + q.async { _ = conformer.isInternetAvailable(); group.leave() } + } + XCTAssertEqual(group.wait(timeout: .now() + 5), .success, + "Concurrent reads timed out — possible deadlock.") + } + + // MARK: - Custom override: isInternetAvailable + + func testCustomOverride_returnsTrue_whenOverrideIsTrue() { + XCTAssertTrue(CustomNetworking(true).isInternetAvailable()) + } + + func testCustomOverride_returnsFalse_whenOverrideIsFalse() { + XCTAssertFalse(CustomNetworking(false).isInternetAvailable()) + } + + func testCustomOverride_isHonored_viaServiceProvider() { + let custom = CustomNetworking(false) + ServiceProvider.shared.networkService = custom + XCTAssertFalse(ServiceProvider.shared.networkService.isInternetAvailable()) + + custom.overrideValue = true + XCTAssertTrue(ServiceProvider.shared.networkService.isInternetAvailable()) + } + + // MARK: - Transport behaviour is unchanged + + func testConnectAsync_isCallableIndependentlyOfIsNetworkAvailable() { + let custom = CustomNetworking(false) + ServiceProvider.shared.networkService = custom + let request = NetworkRequest(url: URL(string: "https://example.com")!) + ServiceProvider.shared.networkService.connectAsync(networkRequest: request, completionHandler: nil) + XCTAssertEqual(custom.connectAsyncCallCount, 1, + "connectAsync must be dispatched regardless of isInternetAvailable() result.") + } +} diff --git a/AEPServices/Tests/services/ServiceProviderTests.swift b/AEPServices/Tests/services/ServiceProviderTests.swift index b5a03fb3d..d25cd3787 100644 --- a/AEPServices/Tests/services/ServiceProviderTests.swift +++ b/AEPServices/Tests/services/ServiceProviderTests.swift @@ -85,7 +85,7 @@ class ServiceProviderTests: XCTestCase { ServiceProvider.shared.reset() XCTAssertTrue(ServiceProvider.shared.loggingService is LoggingService) } - + #if os(iOS) func testOverridingAppOnlyServices() { let mockUIService = MockUIService() diff --git a/AEPSignal.podspec b/AEPSignal.podspec index 9499296fe..719f1b7a7 100644 --- a/AEPSignal.podspec +++ b/AEPSignal.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "AEPSignal" - s.version = "5.10.0" + s.version = "5.11.0" s.summary = "Signal extension for Adobe Experience Platform Mobile SDK. Written and maintained by Adobe." s.description = <<-DESC @@ -20,7 +20,7 @@ Pod::Spec.new do |s| s.pod_target_xcconfig = { 'BUILD_LIBRARY_FOR_DISTRIBUTION' => 'YES' } - s.dependency 'AEPCore', '>= 5.10.0', '< 6.0.0' + s.dependency 'AEPCore', '>= 5.11.0', '< 6.0.0' end diff --git a/AEPSignal/Sources/SignalConstants.swift b/AEPSignal/Sources/SignalConstants.swift index 98dc96110..b9aa6bde9 100644 --- a/AEPSignal/Sources/SignalConstants.swift +++ b/AEPSignal/Sources/SignalConstants.swift @@ -15,7 +15,7 @@ import Foundation enum SignalConstants { static let EXTENSION_NAME = "com.adobe.module.signal" static let FRIENDLY_NAME = "Signal" - static let EXTENSION_VERSION = "5.10.0" + static let EXTENSION_VERSION = "5.11.0" static let DATASTORE_NAME = EXTENSION_NAME static let LOG_PREFIX = FRIENDLY_NAME diff --git a/API/AEPServices-ios.json b/API/AEPServices-ios.json index 2cb59cebd..26f5caff9 100644 --- a/API/AEPServices-ios.json +++ b/API/AEPServices-ios.json @@ -32,6 +32,13 @@ "declKind": "Import", "moduleName": "AEPServices" }, + { + "kind": "Import", + "name": "Network", + "printedName": "Network", + "declKind": "Import", + "moduleName": "AEPServices" + }, { "kind": "Import", "name": "SQLite3", @@ -7653,6 +7660,49 @@ "protocolReq": true, "reqNewWitnessTableEntry": true, "funcSelfKind": "NonMutating" + }, + { + "kind": "Function", + "name": "isInternetAvailable", + "printedName": "isInternetAvailable()", + "children": [ + { + "kind": "TypeNominal", + "name": "Bool", + "printedName": "Swift.Bool", + "usr": "s:Sb" + } + ], + "declKind": "Func", + "usr": "s:11AEPServices10NetworkingP19isInternetAvailableSbyF", + "mangledName": "$s11AEPServices10NetworkingP19isInternetAvailableSbyF", + "moduleName": "AEPServices", + "genericSig": "<τ_0_0 where τ_0_0 : AEPServices.Networking>", + "sugared_genericSig": "", + "protocolReq": true, + "reqNewWitnessTableEntry": true, + "funcSelfKind": "NonMutating" + }, + { + "kind": "Function", + "name": "isInternetAvailable", + "printedName": "isInternetAvailable()", + "children": [ + { + "kind": "TypeNominal", + "name": "Bool", + "printedName": "Swift.Bool", + "usr": "s:Sb" + } + ], + "declKind": "Func", + "usr": "s:11AEPServices10NetworkingPAAE19isInternetAvailableSbyF", + "mangledName": "$s11AEPServices10NetworkingPAAE19isInternetAvailableSbyF", + "moduleName": "AEPServices", + "genericSig": "<τ_0_0 where τ_0_0 : AEPServices.Networking>", + "sugared_genericSig": "", + "isFromExtension": true, + "funcSelfKind": "NonMutating" } ], "declKind": "Protocol", diff --git a/API/AEPServices-tvos.json b/API/AEPServices-tvos.json index d44e9650d..aa4e19b35 100644 --- a/API/AEPServices-tvos.json +++ b/API/AEPServices-tvos.json @@ -25,6 +25,13 @@ "declKind": "Import", "moduleName": "AEPServices" }, + { + "kind": "Import", + "name": "Network", + "printedName": "Network", + "declKind": "Import", + "moduleName": "AEPServices" + }, { "kind": "Import", "name": "SQLite3", @@ -7451,6 +7458,49 @@ "protocolReq": true, "reqNewWitnessTableEntry": true, "funcSelfKind": "NonMutating" + }, + { + "kind": "Function", + "name": "isInternetAvailable", + "printedName": "isInternetAvailable()", + "children": [ + { + "kind": "TypeNominal", + "name": "Bool", + "printedName": "Swift.Bool", + "usr": "s:Sb" + } + ], + "declKind": "Func", + "usr": "s:11AEPServices10NetworkingP19isInternetAvailableSbyF", + "mangledName": "$s11AEPServices10NetworkingP19isInternetAvailableSbyF", + "moduleName": "AEPServices", + "genericSig": "<τ_0_0 where τ_0_0 : AEPServices.Networking>", + "sugared_genericSig": "", + "protocolReq": true, + "reqNewWitnessTableEntry": true, + "funcSelfKind": "NonMutating" + }, + { + "kind": "Function", + "name": "isInternetAvailable", + "printedName": "isInternetAvailable()", + "children": [ + { + "kind": "TypeNominal", + "name": "Bool", + "printedName": "Swift.Bool", + "usr": "s:Sb" + } + ], + "declKind": "Func", + "usr": "s:11AEPServices10NetworkingPAAE19isInternetAvailableSbyF", + "mangledName": "$s11AEPServices10NetworkingPAAE19isInternetAvailableSbyF", + "moduleName": "AEPServices", + "genericSig": "<τ_0_0 where τ_0_0 : AEPServices.Networking>", + "sugared_genericSig": "", + "isFromExtension": true, + "funcSelfKind": "NonMutating" } ], "declKind": "Protocol", diff --git a/TestApps/AEPCoreTestApp.xcodeproj/project.pbxproj b/TestApps/AEPCoreTestApp.xcodeproj/project.pbxproj index a477fa5b3..03e2abda1 100644 --- a/TestApps/AEPCoreTestApp.xcodeproj/project.pbxproj +++ b/TestApps/AEPCoreTestApp.xcodeproj/project.pbxproj @@ -102,6 +102,8 @@ 7A08C6747E305D5BF9CE2A3D /* Pods_TestApp_Objc.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B889355E99E7CE85BE01DA34 /* Pods_TestApp_Objc.framework */; }; 9F65FBBE14E3EB518B8FA743 /* Pods_TestApp_Swift.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 148E88836D30DB0A4542D6F1 /* Pods_TestApp_Swift.framework */; }; AC0BC7342C66C34A0034105C /* UIServicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC0BC7312C66ADB70034105C /* UIServicesView.swift */; }; + 63D2236E7BE63ECCE605679E /* NetworkAvailabilityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74DF4FC844A98DFE4A3FC256 /* NetworkAvailabilityView.swift */; }; + C50E6C71BF4F7B2D9C39627F /* NetworkAvailabilityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74DF4FC844A98DFE4A3FC256 /* NetworkAvailabilityView.swift */; }; ACC05DC22C6AC15A005101D7 /* ADBMobileConfig_custom.json in Resources */ = {isa = PBXBuildFile; fileRef = ACC05DC02C6AC15A005101D7 /* ADBMobileConfig_custom.json */; }; ACC05DC32C6AC15A005101D7 /* ADBMobileConfig_custom.json in Resources */ = {isa = PBXBuildFile; fileRef = ACC05DC02C6AC15A005101D7 /* ADBMobileConfig_custom.json */; }; ACC05DC52C6AC997005101D7 /* ADBMobileConfig.json in Resources */ = {isa = PBXBuildFile; fileRef = ACC05DC42C6AC997005101D7 /* ADBMobileConfig.json */; }; @@ -354,6 +356,7 @@ 8DD57656B2661DC8D3B4EB5D /* Pods-TestAppExtension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TestAppExtension.release.xcconfig"; path = "Target Support Files/Pods-TestAppExtension/Pods-TestAppExtension.release.xcconfig"; sourceTree = ""; }; 9E695D276A2D5CA7797EF41F /* Pods-PerformanceApp.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-PerformanceApp.debug.xcconfig"; path = "Target Support Files/Pods-PerformanceApp/Pods-PerformanceApp.debug.xcconfig"; sourceTree = ""; }; AC0BC7312C66ADB70034105C /* UIServicesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIServicesView.swift; sourceTree = ""; }; + 74DF4FC844A98DFE4A3FC256 /* NetworkAvailabilityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkAvailabilityView.swift; sourceTree = ""; }; AC0BC7352C66C8D60034105C /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = System/Library/Frameworks/WebKit.framework; sourceTree = SDKROOT; }; ACC05DC02C6AC15A005101D7 /* ADBMobileConfig_custom.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = ADBMobileConfig_custom.json; sourceTree = ""; }; ACC05DC42C6AC997005101D7 /* ADBMobileConfig.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = ADBMobileConfig.json; sourceTree = ""; }; @@ -556,6 +559,7 @@ 780888A626D56F3800F5A665 /* LifecycleView.swift */, ACC05DCA2C6ACB39005101D7 /* SignalView.swift */, AC0BC7312C66ADB70034105C /* UIServicesView.swift */, + 74DF4FC844A98DFE4A3FC256 /* NetworkAvailabilityView.swift */, 78A69819289C70B700F67FEC /* ButtonStyle.swift */, 780888B126D56F3800F5A665 /* Assets.xcassets */, 780888A926D56F3800F5A665 /* Base.lproj */, @@ -1367,6 +1371,7 @@ 780888B526D56F3800F5A665 /* LifecycleView.swift in Sources */, ACC05DCB2C6ACB39005101D7 /* SignalView.swift in Sources */, AC0BC7342C66C34A0034105C /* UIServicesView.swift in Sources */, + 63D2236E7BE63ECCE605679E /* NetworkAvailabilityView.swift in Sources */, 780888B626D56F3800F5A665 /* MenuView.swift in Sources */, 780888B926D56F3800F5A665 /* SceneDelegate.swift in Sources */, 78A6981A289C70B700F67FEC /* ButtonStyle.swift in Sources */, @@ -1393,6 +1398,7 @@ 78A26DD3284A9D6B004A91B9 /* CoreView.swift in Sources */, 78A26DD5284A9D71004A91B9 /* LifecycleView.swift in Sources */, ACC05DCC2C6ACB39005101D7 /* SignalView.swift in Sources */, + C50E6C71BF4F7B2D9C39627F /* NetworkAvailabilityView.swift in Sources */, 78A26DD2284A9D69004A91B9 /* MenuView.swift in Sources */, 78A26DD1284A9D66004A91B9 /* SceneDelegate.swift in Sources */, 7816BBCC289DB32600FCBCDF /* ButtonStyle.swift in Sources */, diff --git a/TestApps/TestApp_Swift/MenuView.swift b/TestApps/TestApp_Swift/MenuView.swift index b9a3fcf57..994d558f4 100644 --- a/TestApps/TestApp_Swift/MenuView.swift +++ b/TestApps/TestApp_Swift/MenuView.swift @@ -31,6 +31,10 @@ struct MenuView: View { NavigationLink(destination: SignalView().navigationBarTitle("Signal")) { Text("Signal") } + + NavigationLink(destination: NetworkAvailabilityView().navigationBarTitle("Network Availability")) { + Text("Network Availability") + } #if os(iOS) NavigationLink(destination: UIServicesView().navigationBarTitle("UIServices")) { Text("UIServices") diff --git a/TestApps/TestApp_Swift/NetworkAvailabilityView.swift b/TestApps/TestApp_Swift/NetworkAvailabilityView.swift new file mode 100644 index 000000000..61d4cfaa8 --- /dev/null +++ b/TestApps/TestApp_Swift/NetworkAvailabilityView.swift @@ -0,0 +1,97 @@ +/* + Copyright 2026 Adobe. All rights reserved. + This file is licensed to you under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. You may obtain a copy + of the License at http://www.apache.org/licenses/LICENSE-2.0 + Unless required by applicable law or agreed to in writing, software distributed under + the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + OF ANY KIND, either express or implied. See the License for the specific language + governing permissions and limitations under the License. + */ + +import SwiftUI +import AEPServices + +private let timeFormatter: DateFormatter = { + let f = DateFormatter() + f.dateFormat = "HH:mm:ss" + return f +}() + +struct NetworkAvailabilityView: View { + @State private var isAvailable: Bool? = nil + @State private var lastChecked: Date? = nil + + var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: 20) { + statusSection + checkSection + } + .padding() + } + .onAppear { runCheck() } + } + + // MARK: - Sections + + private var statusSection: some View { + VStack(alignment: .leading, spacing: 6) { + Text("Current Status").font(.headline) + HStack(spacing: 10) { + Circle() + .fill(statusColor) + .frame(width: 14, height: 14) + Text(statusLabel) + .font(.body) + .fontWeight(.semibold) + .foregroundColor(statusColor) + Spacer() + if let date = lastChecked { + Text("at \(timeFormatter.string(from: date))") + .font(.caption) + .foregroundColor(.secondary) + } + } + .padding(12) + .background(Color(.systemGray6)) + .cornerRadius(10) + } + } + + private var checkSection: some View { + VStack(alignment: .leading, spacing: 8) { + Text("Check Now").font(.headline) + Text("Reads the current network path from NWPathMonitor synchronously.") + .font(.caption) + .foregroundColor(.secondary) + Button("Check isInternetAvailable()") { + runCheck() + } + .buttonStyle(CustomButtonStyle()) + } + } + + // MARK: - Helpers + + private func runCheck() { + isAvailable = ServiceProvider.shared.networkService.isInternetAvailable() + lastChecked = Date() + } + + private var statusColor: Color { + guard let available = isAvailable else { return .gray } + return available ? .green : .red + } + + private var statusLabel: String { + guard let available = isAvailable else { return "Unknown" } + return available ? "Available" : "Not Available" + } +} + +struct NetworkAvailabilityView_Previews: PreviewProvider { + static var previews: some View { + NetworkAvailabilityView() + } +}