diff --git a/OneSignal-KMP-SDK b/OneSignal-KMP-SDK index 5c9f5a582..d417111af 160000 --- a/OneSignal-KMP-SDK +++ b/OneSignal-KMP-SDK @@ -1 +1 @@ -Subproject commit 5c9f5a582543ef1b01db5bb33b4607ded294fad3 +Subproject commit d417111afa4e81ab80e49931944230e6a0ad47df diff --git a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj index ff45d89b3..10f512fa6 100644 --- a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj +++ b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj @@ -69,6 +69,12 @@ 3C14E39F2AFAE39B006ED053 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 3C14E39E2AFAE39B006ED053 /* PrivacyInfo.xcprivacy */; }; 3C14E3A12AFAE461006ED053 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 3C14E3A02AFAE461006ED053 /* PrivacyInfo.xcprivacy */; }; 3C14E3A42AFAE54C006ED053 /* OneSignalSwiftInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEC08AFF2947D4E900C81DA3 /* OneSignalSwiftInterface.swift */; }; + 3C14E3B02FAE54C006ED053 /* IOSLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C14E3A92FAE54C006ED053 /* IOSLogger.swift */; }; + 3C14E3B12FAE54C006ED053 /* OneSignalLogHttpSender.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C14E3AA2FAE54C006ED053 /* OneSignalLogHttpSender.swift */; }; + 3C14E3B22FAE54C006ED053 /* FileLogStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C14E3AB2FAE54C006ED053 /* FileLogStore.swift */; }; + 3C14E3B32FAE54C006ED053 /* OSLoggerPlatformProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C14E3AC2FAE54C006ED053 /* OSLoggerPlatformProvider.swift */; }; + 3C14E3B42FAE54C006ED053 /* KotlinByteArray+Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C14E3AD2FAE54C006ED053 /* KotlinByteArray+Data.swift */; }; + 3C14E3B52FAE54C006ED053 /* OSLoggerAdaptersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C14E3AE2FAE54C006ED053 /* OSLoggerAdaptersTests.swift */; }; 3C19C6322E919F0C00D6731E /* OSRequestLiveActivityClicked.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C19C6312E919F0C00D6731E /* OSRequestLiveActivityClicked.swift */; }; 3C23A21B2FCE0A52001D32E3 /* OneSignalIdentifiersFallbackTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C23A21A2FCE0A52001D32E3 /* OneSignalIdentifiersFallbackTests.swift */; }; 3C23A21D2FCE0A83001D32E3 /* OSModelStoreRefreshTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C23A21C2FCE0A83001D32E3 /* OSModelStoreRefreshTests.swift */; }; @@ -1777,6 +1783,12 @@ DEBAAEB62A4381AE00BF2C1C /* OSInAppMessageMigrationController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OSInAppMessageMigrationController.h; sourceTree = ""; }; DEBAAEB72A4381AE00BF2C1C /* OSInAppMessageMigrationController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OSInAppMessageMigrationController.m; sourceTree = ""; }; DEC08AFF2947D4E900C81DA3 /* OneSignalSwiftInterface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OneSignalSwiftInterface.swift; sourceTree = ""; }; + 3C14E3A92FAE54C006ED053 /* IOSLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IOSLogger.swift; sourceTree = ""; }; + 3C14E3AA2FAE54C006ED053 /* OneSignalLogHttpSender.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OneSignalLogHttpSender.swift; sourceTree = ""; }; + 3C14E3AB2FAE54C006ED053 /* FileLogStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileLogStore.swift; sourceTree = ""; }; + 3C14E3AC2FAE54C006ED053 /* OSLoggerPlatformProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSLoggerPlatformProvider.swift; sourceTree = ""; }; + 3C14E3AD2FAE54C006ED053 /* KotlinByteArray+Data.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "KotlinByteArray+Data.swift"; sourceTree = ""; }; + 3C14E3AE2FAE54C006ED053 /* OSLoggerAdaptersTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSLoggerAdaptersTests.swift; sourceTree = ""; }; DEF5CCF12539321A0003E9CC /* UnitTestApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = UnitTestApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; DEF5CCF32539321A0003E9CC /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; DEF5CCF42539321A0003E9CC /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; @@ -2242,6 +2254,7 @@ isa = PBXGroup; children = ( 5BC1DE652C90BC9F00CA8807 /* Consistency */, + 3C14E3AF2FAE54C006ED053 /* Logging */, 3C115163289A259500565C41 /* OneSignalOSCore.h */, 3C115188289ADEA300565C41 /* OSModelStore.swift */, 3C5C6FFB2FCB8DED00102E2C /* OneSignalIdentifiers.swift */, @@ -2264,6 +2277,18 @@ path = Source; sourceTree = ""; }; + 3C14E3AF2FAE54C006ED053 /* Logging */ = { + isa = PBXGroup; + children = ( + 3C14E3AD2FAE54C006ED053 /* KotlinByteArray+Data.swift */, + 3C14E3AB2FAE54C006ED053 /* FileLogStore.swift */, + 3C14E3AA2FAE54C006ED053 /* OneSignalLogHttpSender.swift */, + 3C14E3A92FAE54C006ED053 /* IOSLogger.swift */, + 3C14E3AC2FAE54C006ED053 /* OSLoggerPlatformProvider.swift */, + ); + path = Logging; + sourceTree = ""; + }; 3C70222A2ECF126B001768C6 /* OneSignalInAppMessagesMocks */ = { isa = PBXGroup; children = ( @@ -2531,6 +2556,7 @@ children = ( 5BC1DE672C90C23E00CA8807 /* OSConsistencyManagerTests.swift */, 3C427AC8301BB28A0059B8B7 /* OSOperationRepoFlushTests.swift */, + 3C14E3AE2FAE54C006ED053 /* OSLoggerAdaptersTests.swift */, 3C23A21A2FCE0A52001D32E3 /* OneSignalIdentifiersFallbackTests.swift */, 3C23A21E2FCE0AA1001D32E3 /* OSResilientStorageTests.swift */, 3C23A21C2FCE0A83001D32E3 /* OSModelStoreRefreshTests.swift */, @@ -4391,9 +4417,13 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 3C14E3B42FAE54C006ED053 /* KotlinByteArray+Data.swift in Sources */, DEFB3E652BB7346D00E65DAD /* OSLiveActivities.swift in Sources */, + 3C14E3B22FAE54C006ED053 /* FileLogStore.swift in Sources */, 5BC1DE602C90B83900CA8807 /* OSConsistencyKeyEnum.swift in Sources */, + 3C14E3B12FAE54C006ED053 /* OneSignalLogHttpSender.swift in Sources */, 3C4F9E4428A4466C009F453A /* OSOperationRepo.swift in Sources */, + 3C14E3B02FAE54C006ED053 /* IOSLogger.swift in Sources */, 3C11518B289ADEEB00565C41 /* OSEventProducer.swift in Sources */, 3C115165289A259500565C41 /* OneSignalOSCore.docc in Sources */, 5BC1DE5E2C90B80E00CA8807 /* OSCondition.swift in Sources */, @@ -4414,6 +4444,7 @@ DEFB3E672BB735B500E65DAD /* OSStubLiveActivities.swift in Sources */, 5BC1DE622C90B85A00CA8807 /* OSIamFetchOffsetKey.swift in Sources */, 3C11518D289AF5E800565C41 /* OSModelChangedHandler.swift in Sources */, + 3C14E3B32FAE54C006ED053 /* OSLoggerPlatformProvider.swift in Sources */, 3C8E6DF928A6D89E0031E48A /* OSOperationExecutor.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -4552,6 +4583,7 @@ 3C23A21F2FCE0AA1001D32E3 /* OSResilientStorageTests.swift in Sources */, 3C23A21D2FCE0A83001D32E3 /* OSModelStoreRefreshTests.swift in Sources */, 3C427AC9301BB28A0059B8B7 /* OSOperationRepoFlushTests.swift in Sources */, + 3C14E3B52FAE54C006ED053 /* OSLoggerAdaptersTests.swift in Sources */, 3C23A21B2FCE0A52001D32E3 /* OneSignalIdentifiersFallbackTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -5378,6 +5410,14 @@ buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; BUILD_LIBRARY_FOR_DISTRIBUTION = YES; + "FRAMEWORK_SEARCH_PATHS[sdk=iphoneos*]" = ( + "$(inherited)", + "$(SRCROOT)/../../OneSignal-KMP-SDK/kmp/build/XCFrameworks/release/OneSignalKMP.xcframework/ios-arm64", + ); + "FRAMEWORK_SEARCH_PATHS[sdk=iphonesimulator*]" = ( + "$(inherited)", + "$(SRCROOT)/../../OneSignal-KMP-SDK/kmp/build/XCFrameworks/release/OneSignalKMP.xcframework/ios-arm64_x86_64-simulator", + ); CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; @@ -5438,6 +5478,14 @@ buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; BUILD_LIBRARY_FOR_DISTRIBUTION = YES; + "FRAMEWORK_SEARCH_PATHS[sdk=iphoneos*]" = ( + "$(inherited)", + "$(SRCROOT)/../../OneSignal-KMP-SDK/kmp/build/XCFrameworks/release/OneSignalKMP.xcframework/ios-arm64", + ); + "FRAMEWORK_SEARCH_PATHS[sdk=iphonesimulator*]" = ( + "$(inherited)", + "$(SRCROOT)/../../OneSignal-KMP-SDK/kmp/build/XCFrameworks/release/OneSignalKMP.xcframework/ios-arm64_x86_64-simulator", + ); CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; @@ -6928,6 +6976,14 @@ buildSettings = { ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; BUNDLE_LOADER = "$(TEST_HOST)"; + "FRAMEWORK_SEARCH_PATHS[sdk=iphoneos*]" = ( + "$(inherited)", + "$(SRCROOT)/../../OneSignal-KMP-SDK/kmp/build/XCFrameworks/release/OneSignalKMP.xcframework/ios-arm64", + ); + "FRAMEWORK_SEARCH_PATHS[sdk=iphonesimulator*]" = ( + "$(inherited)", + "$(SRCROOT)/../../OneSignal-KMP-SDK/kmp/build/XCFrameworks/release/OneSignalKMP.xcframework/ios-arm64_x86_64-simulator", + ); CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; @@ -6974,6 +7030,14 @@ buildSettings = { ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; BUNDLE_LOADER = "$(TEST_HOST)"; + "FRAMEWORK_SEARCH_PATHS[sdk=iphoneos*]" = ( + "$(inherited)", + "$(SRCROOT)/../../OneSignal-KMP-SDK/kmp/build/XCFrameworks/release/OneSignalKMP.xcframework/ios-arm64", + ); + "FRAMEWORK_SEARCH_PATHS[sdk=iphonesimulator*]" = ( + "$(inherited)", + "$(SRCROOT)/../../OneSignal-KMP-SDK/kmp/build/XCFrameworks/release/OneSignalKMP.xcframework/ios-arm64_x86_64-simulator", + ); CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; @@ -7028,6 +7092,14 @@ buildSettings = { ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; BUNDLE_LOADER = "$(TEST_HOST)"; + "FRAMEWORK_SEARCH_PATHS[sdk=iphoneos*]" = ( + "$(inherited)", + "$(SRCROOT)/../../OneSignal-KMP-SDK/kmp/build/XCFrameworks/release/OneSignalKMP.xcframework/ios-arm64", + ); + "FRAMEWORK_SEARCH_PATHS[sdk=iphonesimulator*]" = ( + "$(inherited)", + "$(SRCROOT)/../../OneSignal-KMP-SDK/kmp/build/XCFrameworks/release/OneSignalKMP.xcframework/ios-arm64_x86_64-simulator", + ); CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; @@ -7119,6 +7191,14 @@ CA2951B72167F4120064227A /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + "FRAMEWORK_SEARCH_PATHS[sdk=iphoneos*]" = ( + "$(inherited)", + "$(SRCROOT)/../../OneSignal-KMP-SDK/kmp/build/XCFrameworks/release/OneSignalKMP.xcframework/ios-arm64", + ); + "FRAMEWORK_SEARCH_PATHS[sdk=iphonesimulator*]" = ( + "$(inherited)", + "$(SRCROOT)/../../OneSignal-KMP-SDK/kmp/build/XCFrameworks/release/OneSignalKMP.xcframework/ios-arm64_x86_64-simulator", + ); CLANG_ANALYZER_GCD_PERFORMANCE = YES; CLANG_ANALYZER_SECURITY_FLOATLOOPCOUNTER = YES; CLANG_ENABLE_CODE_COVERAGE = NO; @@ -7145,6 +7225,14 @@ CA2951B82167F4120064227A /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + "FRAMEWORK_SEARCH_PATHS[sdk=iphoneos*]" = ( + "$(inherited)", + "$(SRCROOT)/../../OneSignal-KMP-SDK/kmp/build/XCFrameworks/release/OneSignalKMP.xcframework/ios-arm64", + ); + "FRAMEWORK_SEARCH_PATHS[sdk=iphonesimulator*]" = ( + "$(inherited)", + "$(SRCROOT)/../../OneSignal-KMP-SDK/kmp/build/XCFrameworks/release/OneSignalKMP.xcframework/ios-arm64_x86_64-simulator", + ); BUILD_LIBRARY_FOR_DISTRIBUTION = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ENABLE_CODE_COVERAGE = NO; @@ -7190,6 +7278,14 @@ CA2951B92167F4120064227A /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + "FRAMEWORK_SEARCH_PATHS[sdk=iphoneos*]" = ( + "$(inherited)", + "$(SRCROOT)/../../OneSignal-KMP-SDK/kmp/build/XCFrameworks/release/OneSignalKMP.xcframework/ios-arm64", + ); + "FRAMEWORK_SEARCH_PATHS[sdk=iphonesimulator*]" = ( + "$(inherited)", + "$(SRCROOT)/../../OneSignal-KMP-SDK/kmp/build/XCFrameworks/release/OneSignalKMP.xcframework/ios-arm64_x86_64-simulator", + ); BUNDLE_LOADER = "$(TEST_HOST)"; CLANG_ANALYZER_NONNULL = YES; CLANG_ENABLE_MODULES = YES; @@ -7299,6 +7395,14 @@ CA2951C32167FB950064227A /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + "FRAMEWORK_SEARCH_PATHS[sdk=iphoneos*]" = ( + "$(inherited)", + "$(SRCROOT)/../../OneSignal-KMP-SDK/kmp/build/XCFrameworks/release/OneSignalKMP.xcframework/ios-arm64", + ); + "FRAMEWORK_SEARCH_PATHS[sdk=iphonesimulator*]" = ( + "$(inherited)", + "$(SRCROOT)/../../OneSignal-KMP-SDK/kmp/build/XCFrameworks/release/OneSignalKMP.xcframework/ios-arm64_x86_64-simulator", + ); CLANG_ANALYZER_GCD_PERFORMANCE = YES; CLANG_ANALYZER_SECURITY_FLOATLOOPCOUNTER = YES; CLANG_ENABLE_CODE_COVERAGE = NO; @@ -7325,6 +7429,14 @@ CA2951C42167FB950064227A /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + "FRAMEWORK_SEARCH_PATHS[sdk=iphoneos*]" = ( + "$(inherited)", + "$(SRCROOT)/../../OneSignal-KMP-SDK/kmp/build/XCFrameworks/release/OneSignalKMP.xcframework/ios-arm64", + ); + "FRAMEWORK_SEARCH_PATHS[sdk=iphonesimulator*]" = ( + "$(inherited)", + "$(SRCROOT)/../../OneSignal-KMP-SDK/kmp/build/XCFrameworks/release/OneSignalKMP.xcframework/ios-arm64_x86_64-simulator", + ); BUILD_LIBRARY_FOR_DISTRIBUTION = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ENABLE_CODE_COVERAGE = YES; @@ -7372,6 +7484,14 @@ CA2951C52167FB950064227A /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + "FRAMEWORK_SEARCH_PATHS[sdk=iphoneos*]" = ( + "$(inherited)", + "$(SRCROOT)/../../OneSignal-KMP-SDK/kmp/build/XCFrameworks/release/OneSignalKMP.xcframework/ios-arm64", + ); + "FRAMEWORK_SEARCH_PATHS[sdk=iphonesimulator*]" = ( + "$(inherited)", + "$(SRCROOT)/../../OneSignal-KMP-SDK/kmp/build/XCFrameworks/release/OneSignalKMP.xcframework/ios-arm64_x86_64-simulator", + ); BUNDLE_LOADER = "$(TEST_HOST)"; CLANG_ANALYZER_NONNULL = YES; CLANG_ENABLE_MODULES = YES; @@ -7481,6 +7601,14 @@ DE3D8F3928C15839008C2BBF /* Test */ = { isa = XCBuildConfiguration; buildSettings = { + "FRAMEWORK_SEARCH_PATHS[sdk=iphoneos*]" = ( + "$(inherited)", + "$(SRCROOT)/../../OneSignal-KMP-SDK/kmp/build/XCFrameworks/release/OneSignalKMP.xcframework/ios-arm64", + ); + "FRAMEWORK_SEARCH_PATHS[sdk=iphonesimulator*]" = ( + "$(inherited)", + "$(SRCROOT)/../../OneSignal-KMP-SDK/kmp/build/XCFrameworks/release/OneSignalKMP.xcframework/ios-arm64_x86_64-simulator", + ); CLANG_ANALYZER_GCD_PERFORMANCE = YES; CLANG_ANALYZER_SECURITY_FLOATLOOPCOUNTER = YES; CLANG_ENABLE_CODE_COVERAGE = NO; @@ -7507,6 +7635,14 @@ DE3D8F3A28C15839008C2BBF /* Test */ = { isa = XCBuildConfiguration; buildSettings = { + "FRAMEWORK_SEARCH_PATHS[sdk=iphoneos*]" = ( + "$(inherited)", + "$(SRCROOT)/../../OneSignal-KMP-SDK/kmp/build/XCFrameworks/release/OneSignalKMP.xcframework/ios-arm64", + ); + "FRAMEWORK_SEARCH_PATHS[sdk=iphonesimulator*]" = ( + "$(inherited)", + "$(SRCROOT)/../../OneSignal-KMP-SDK/kmp/build/XCFrameworks/release/OneSignalKMP.xcframework/ios-arm64_x86_64-simulator", + ); BUILD_LIBRARY_FOR_DISTRIBUTION = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ENABLE_CODE_COVERAGE = YES; @@ -7554,6 +7690,14 @@ DE3D8F3B28C15839008C2BBF /* Test */ = { isa = XCBuildConfiguration; buildSettings = { + "FRAMEWORK_SEARCH_PATHS[sdk=iphoneos*]" = ( + "$(inherited)", + "$(SRCROOT)/../../OneSignal-KMP-SDK/kmp/build/XCFrameworks/release/OneSignalKMP.xcframework/ios-arm64", + ); + "FRAMEWORK_SEARCH_PATHS[sdk=iphonesimulator*]" = ( + "$(inherited)", + "$(SRCROOT)/../../OneSignal-KMP-SDK/kmp/build/XCFrameworks/release/OneSignalKMP.xcframework/ios-arm64_x86_64-simulator", + ); BUNDLE_LOADER = "$(TEST_HOST)"; CLANG_ANALYZER_NONNULL = YES; CLANG_ENABLE_MODULES = YES; @@ -9061,6 +9205,14 @@ buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; BUILD_LIBRARY_FOR_DISTRIBUTION = YES; + "FRAMEWORK_SEARCH_PATHS[sdk=iphoneos*]" = ( + "$(inherited)", + "$(SRCROOT)/../../OneSignal-KMP-SDK/kmp/build/XCFrameworks/release/OneSignalKMP.xcframework/ios-arm64", + ); + "FRAMEWORK_SEARCH_PATHS[sdk=iphonesimulator*]" = ( + "$(inherited)", + "$(SRCROOT)/../../OneSignal-KMP-SDK/kmp/build/XCFrameworks/release/OneSignalKMP.xcframework/ios-arm64_x86_64-simulator", + ); CLANG_ENABLE_MODULES = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/FileLogStore.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/FileLogStore.swift new file mode 100644 index 000000000..85bf04266 --- /dev/null +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/FileLogStore.swift @@ -0,0 +1,240 @@ +/* + Modified MIT License + + Copyright 2026 OneSignal + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + 1. The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + 2. All copies of substantial portions of the Software may only be used in connection + with services provided by OneSignal. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + */ + +// Kotlin/Native does not produce a Mac Catalyst framework slice. +#if !targetEnvironment(macCatalyst) + +import Darwin +import Foundation +import OneSignalCore +@_implementationOnly import OneSignalKMP + +/// Persists encoded crash logs so they can be uploaded after the app restarts. +/// +/// Writes are synchronous and durable because fatal handlers may terminate the +/// process immediately after `save` returns. Directory scans and cleanup run on +/// a utility queue to keep disk I/O off the caller. +final class FileLogStore: ILogFileStore { + /// Complete records use `.otlp`; interrupted durable writes leave + /// `.otlp.tmp` files that are safe to reap after the minimum-age gate. + private static let ownedFileSuffix = ".otlp" + private static let temporaryFileSuffix = ".otlp.tmp" + private static let queueLabel = "com.onesignal.logger.file-store" + + private let rootURL: URL + private let fileManager: FileManager + private let ioQueue = DispatchQueue(label: queueLabel, qos: .utility) + + init(rootPath: String, fileManager: FileManager = .default) { + self.rootURL = URL(fileURLWithPath: rootPath, isDirectory: true) + self.fileManager = fileManager + try? createRootDirectory() + } + + func save(bytes: KotlinByteArray) -> Bool { + guard bytes.size > 0 else { + return false + } + do { + try createRootDirectory() + let timestamp = Int64(Date().timeIntervalSince1970 * 1_000) + let id = "\(timestamp)-\(UUID().uuidString)\(Self.ownedFileSuffix)" + try writeDurably(bytes.data, to: rootURL.appendingPathComponent(id)) + return true + } catch { + return false + } + } + + func listReadable( + minAgeMillis: Int64, + completionHandler: @escaping ([StoredLogFile]?, Error?) -> Void + ) { + ioQueue.async { + do { + let entries = try self.readableEntries(minAgeMillis: minAgeMillis) + completionHandler(entries, nil) + } catch { + OneSignalLog.onesignalLog( + .LL_WARN, + message: "FileLogStore listReadable failed: \(error.localizedDescription)" + ) + completionHandler([], nil) + } + } + } + + func delete(id: String, completionHandler: @escaping (Error?) -> Void) { + ioQueue.async { + guard self.isSafeEntryId(id) else { + completionHandler(nil) + return + } + + do { + let url = self.rootURL.appendingPathComponent(id) + if self.fileManager.fileExists(atPath: url.path) { + try self.fileManager.removeItem(at: url) + } + } catch { + OneSignalLog.onesignalLog( + .LL_WARN, + message: "FileLogStore delete failed: \(error.localizedDescription)" + ) + } + completionHandler(nil) + } + } + + func deleteUnrecognizedEntries( + minAgeMillis: Int64, + completionHandler: @escaping (KotlinInt?, Error?) -> Void + ) { + ioQueue.async { + var deleted = 0 + do { + for url in try self.fileURLs() + where url.lastPathComponent.hasSuffix(Self.temporaryFileSuffix) { + guard try self.isOldEnough(url, minAgeMillis: minAgeMillis) else { + continue + } + try self.fileManager.removeItem(at: url) + deleted += 1 + } + } catch { + OneSignalLog.onesignalLog( + .LL_WARN, + message: "FileLogStore cleanup failed: \(error.localizedDescription)" + ) + } + completionHandler(KotlinInt(int: Int32(deleted)), nil) + } + } + + private func readableEntries(minAgeMillis: Int64) throws -> [StoredLogFile] { + try fileURLs() + .filter { $0.lastPathComponent.hasSuffix(Self.ownedFileSuffix) } + .filter { try isOldEnough($0, minAgeMillis: minAgeMillis) } + .compactMap { url in + guard let data = try? Data(contentsOf: url) else { + return nil + } + return StoredLogFile(id: url.lastPathComponent, bytes: data.kotlinByteArray) + } + } + + private func fileURLs() throws -> [URL] { + guard fileManager.fileExists(atPath: rootURL.path) else { + return [] + } + return try fileManager.contentsOfDirectory( + at: rootURL, + includingPropertiesForKeys: [.contentModificationDateKey, .isRegularFileKey], + options: [.skipsHiddenFiles] + ).filter { + (try? $0.resourceValues(forKeys: [.isRegularFileKey]).isRegularFile) == true + } + } + + private func isOldEnough(_ url: URL, minAgeMillis: Int64) throws -> Bool { + let values = try url.resourceValues(forKeys: [.contentModificationDateKey]) + guard let modifiedAt = values.contentModificationDate else { + return false + } + return Date().timeIntervalSince(modifiedAt) * 1_000 >= Double(max(0, minAgeMillis)) + } + + private func isSafeEntryId(_ id: String) -> Bool { + !id.isEmpty && URL(fileURLWithPath: id).lastPathComponent == id + } + + private func createRootDirectory() throws { + try fileManager.createDirectory(at: rootURL, withIntermediateDirectories: true) + } + + private func writeDurably(_ data: Data, to targetURL: URL) throws { + let temporaryURL = targetURL.appendingPathExtension("tmp") + let descriptor = open(temporaryURL.path, O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR) + guard descriptor >= 0 else { + throw POSIXError(POSIXErrorCode(rawValue: errno) ?? .EIO) + } + var isClosed = false + defer { + if !isClosed { + close(descriptor) + } + } + do { + try fileManager.setAttributes( + [.protectionKey: FileProtectionType.completeUntilFirstUserAuthentication], + ofItemAtPath: temporaryURL.path + ) + try data.withUnsafeBytes { rawBuffer in + guard var pointer = rawBuffer.baseAddress else { + return + } + var remaining = rawBuffer.count + while remaining > 0 { + let count = Darwin.write(descriptor, pointer, remaining) + if count < 0 && errno == EINTR { + continue + } + guard count > 0 else { + throw POSIXError(POSIXErrorCode(rawValue: errno) ?? .EIO) + } + pointer = pointer.advanced(by: count) + remaining -= count + } + } + guard fsync(descriptor) == 0 else { + throw POSIXError(POSIXErrorCode(rawValue: errno) ?? .EIO) + } + guard close(descriptor) == 0 else { + throw POSIXError(POSIXErrorCode(rawValue: errno) ?? .EIO) + } + isClosed = true + try fileManager.moveItem(at: temporaryURL, to: targetURL) + try syncDirectory() + } catch { + try? fileManager.removeItem(at: temporaryURL) + throw error + } + } + + private func syncDirectory() throws { + let descriptor = open(rootURL.path, O_RDONLY) + guard descriptor >= 0 else { + throw POSIXError(POSIXErrorCode(rawValue: errno) ?? .EIO) + } + defer { close(descriptor) } + guard fsync(descriptor) == 0 else { + throw POSIXError(POSIXErrorCode(rawValue: errno) ?? .EIO) + } + } +} + +#endif diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/IOSLogger.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/IOSLogger.swift new file mode 100644 index 000000000..682cf1360 --- /dev/null +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/IOSLogger.swift @@ -0,0 +1,55 @@ +/* + Modified MIT License + + Copyright 2026 OneSignal + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + 1. The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + 2. All copies of substantial portions of the Software may only be used in connection + with services provided by OneSignal. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + */ + +// Kotlin/Native does not produce a Mac Catalyst framework slice. +#if !targetEnvironment(macCatalyst) + +import Foundation +import OneSignalCore +@_implementationOnly import OneSignalKMP + +/// Routes diagnostic messages from the shared logger through the iOS SDK's +/// existing logging pipeline. +final class IOSLogger: ILogger { + func error(message: String) { + OneSignalLog.onesignalLog(.LL_ERROR, message: message) + } + + func warn(message: String) { + OneSignalLog.onesignalLog(.LL_WARN, message: message) + } + + func info(message: String) { + OneSignalLog.onesignalLog(.LL_INFO, message: message) + } + + func debug(message: String) { + OneSignalLog.onesignalLog(.LL_DEBUG, message: message) + } +} + +#endif diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/KotlinByteArray+Data.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/KotlinByteArray+Data.swift new file mode 100644 index 000000000..31b38b0bd --- /dev/null +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/KotlinByteArray+Data.swift @@ -0,0 +1,46 @@ +/* + Modified MIT License + + Copyright 2026 OneSignal + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + 1. The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + 2. All copies of substantial portions of the Software may only be used in connection + with services provided by OneSignal. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + */ + +// Kotlin/Native does not produce a Mac Catalyst framework slice. +#if !targetEnvironment(macCatalyst) + +import Foundation +@_implementationOnly import OneSignalKMP + +extension KotlinByteArray { + var data: Data { + AppleByteArrayInterop.shared.toNSData(bytes: self) + } +} + +extension Data { + var kotlinByteArray: KotlinByteArray { + AppleByteArrayInterop.shared.toByteArray(data: self) + } +} + +#endif diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLoggerPlatformProvider.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLoggerPlatformProvider.swift new file mode 100644 index 000000000..c8ae20ee2 --- /dev/null +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLoggerPlatformProvider.swift @@ -0,0 +1,245 @@ +/* + Modified MIT License + + Copyright 2026 OneSignal + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + 1. The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + 2. All copies of substantial portions of the Software may only be used in connection + with services provided by OneSignal. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + */ + +// Kotlin/Native does not produce a Mac Catalyst framework slice. +#if !targetEnvironment(macCatalyst) + +import Darwin +import Foundation +import OneSignalCore +@_implementationOnly import OneSignalKMP +import UIKit + +/// Supplies iOS SDK state and platform metadata to the shared logger. +/// +/// Mutable SDK state is injected by the composition layer because +/// `OneSignalUser` depends on `OneSignalOSCore`; importing it here would create +/// a circular module dependency. Providers must be safe to invoke from arbitrary +/// threads, including synchronously from a crash handler. Construct this adapter +/// on the main thread because its static device metadata comes from `UIDevice`. +final class OSLoggerPlatformProvider: ILoggerPlatformProvider { + typealias IdentifierProvider = () -> String? + typealias InstallIdProvider = () -> String + typealias AppStateProvider = () -> String + typealias FeatureFlagsProvider = () -> [String] + typealias LogLevelProvider = () -> String? + typealias BoolProvider = () -> Bool + + private enum Constants { + static let sdkBase = "ios" + static let deviceManufacturer = "Apple" + static let unknown = "unknown" + static let osBuildName = "kern.osversion" + static let disabledLogLevel = "NONE" + static let crashDirectoryComponents = ["onesignal", "logger", "crashes"] + + /// Prevents reading a report while a terminating process may still be + /// completing its durable write. + static let minimumFileAgeMillis: Int64 = 5_000 + } + + private static let processStartedAtUptime = + processStartUptime() ?? ProcessInfo.processInfo.systemUptime + + private let installIdProvider: InstallIdProvider + private let onesignalIdProvider: IdentifierProvider + private let pushSubscriptionIdProvider: IdentifierProvider + private let appStateProvider: AppStateProvider + private let featureFlagsProvider: FeatureFlagsProvider + private let remoteLogLevelProvider: LogLevelProvider + private let exporterLoggingEnabledProvider: BoolProvider + + init( + installIdProvider: @escaping InstallIdProvider, + onesignalIdProvider: @escaping IdentifierProvider, + pushSubscriptionIdProvider: @escaping IdentifierProvider, + appStateProvider: @escaping AppStateProvider, + featureFlagsProvider: @escaping FeatureFlagsProvider, + remoteLogLevelProvider: @escaping LogLevelProvider, + exporterLoggingEnabledProvider: @escaping BoolProvider + ) { + self.installIdProvider = installIdProvider + self.onesignalIdProvider = onesignalIdProvider + self.pushSubscriptionIdProvider = pushSubscriptionIdProvider + self.appStateProvider = appStateProvider + self.featureFlagsProvider = featureFlagsProvider + self.remoteLogLevelProvider = remoteLogLevelProvider + self.exporterLoggingEnabledProvider = exporterLoggingEnabledProvider + } + + /// The completion must run inline because crash reporting blocks the + /// crashing thread until this value is returned. + func getInstallId(completionHandler: @escaping (String?, Error?) -> Void) { + completionHandler(installIdProvider(), nil) + } + + let sdkBase = Constants.sdkBase + let sdkBaseVersion = ONESIGNAL_VERSION + let appPackageId = Bundle.main.bundleIdentifier ?? Constants.unknown + let appVersion = + Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String + ?? Constants.unknown + let deviceManufacturer = Constants.deviceManufacturer + let deviceModel = OSDeviceUtils.getDeviceVariant() ?? Constants.unknown + let osName = UIDevice.current.systemName + let osVersion = UIDevice.current.systemVersion + let osBuildId = OSLoggerPlatformProvider.systemValue(named: Constants.osBuildName) + let sdkWrapper = OneSignalWrapper.sdkType + let sdkWrapperVersion = OneSignalWrapper.sdkVersion + var enabledFeatureFlags: [String] { + featureFlagsProvider() + } + + var appId: String? { + OneSignalIdentifiers.currentAppId + } + + var onesignalId: String? { + onesignalIdProvider() + } + + var pushSubscriptionId: String? { + pushSubscriptionIdProvider() + } + + var appState: String { + appStateProvider() + } + + var processUptime: Int64 { + Self.processUptimeMillis( + systemUptime: ProcessInfo.processInfo.systemUptime, + processStartUptime: Self.processStartedAtUptime + ) + } + + var currentThreadName: String { + if let name = Thread.current.name, !name.isEmpty { + return name + } + if Thread.isMainThread { + return "main" + } + var name = [CChar](repeating: 0, count: 64) + guard pthread_getname_np(pthread_self(), &name, name.count) == 0 else { + return Constants.unknown + } + let threadName = String(cString: name) + return threadName.isEmpty ? Constants.unknown : threadName + } + + let crashStoragePath: String = { + let caches = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first + ?? FileManager.default.temporaryDirectory + return Constants.crashDirectoryComponents.reduce(caches) { + $0.appendingPathComponent($1, isDirectory: true) + }.path + }() + + let minFileAgeForReadMillis = Constants.minimumFileAgeMillis + + var isRemoteLoggingEnabled: Bool { + guard let level = remoteLogLevel else { + return false + } + return level != Constants.disabledLogLevel + } + + var remoteLogLevel: String? { + remoteLogLevelProvider()?.uppercased() + } + + var isExporterLoggingEnabled: Bool { + exporterLoggingEnabledProvider() + } + + var appIdForHeaders: String { + appId ?? "" + } + + let apiBaseUrl = OS_API_SERVER_URL.trimmingCharacters(in: CharacterSet(charactersIn: "/")) + + private static func systemValue(named name: String) -> String { + var size = 0 + guard sysctlbyname(name, nil, &size, nil, 0) == 0, size > 0 else { + return Constants.unknown + } + var value = [CChar](repeating: 0, count: size) + guard sysctlbyname(name, &value, &size, nil, 0) == 0 else { + return Constants.unknown + } + return String(cString: value) + } + + static func processUptimeMillis( + systemUptime: TimeInterval, + processStartUptime: TimeInterval + ) -> Int64 { + Int64(max(0, systemUptime - processStartUptime) * 1_000) + } + + private static func processStartUptime() -> TimeInterval? { + var processInfo = kinfo_proc() + var processInfoSize = MemoryLayout.stride + var processName = [CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid()] + guard sysctl( + &processName, + u_int(processName.count), + &processInfo, + &processInfoSize, + nil, + 0 + ) == 0 else { + return nil + } + + var bootTime = timeval() + var bootTimeSize = MemoryLayout.stride + var bootName = [CTL_KERN, KERN_BOOTTIME] + guard sysctl( + &bootName, + u_int(bootName.count), + &bootTime, + &bootTimeSize, + nil, + 0 + ) == 0 else { + return nil + } + + let processStart = timeInterval(processInfo.kp_proc.p_starttime) + let systemBoot = timeInterval(bootTime) + return max(0, processStart - systemBoot) + } + + private static func timeInterval(_ value: timeval) -> TimeInterval { + TimeInterval(value.tv_sec) + TimeInterval(value.tv_usec) / 1_000_000 + } + +} + +#endif diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OneSignalLogHttpSender.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OneSignalLogHttpSender.swift new file mode 100644 index 000000000..1d3bc5814 --- /dev/null +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OneSignalLogHttpSender.swift @@ -0,0 +1,162 @@ +/* + Modified MIT License + + Copyright 2026 OneSignal + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + 1. The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + 2. All copies of substantial portions of the Software may only be used in connection + with services provided by OneSignal. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + */ + +// Kotlin/Native does not produce a Mac Catalyst framework slice. +#if !targetEnvironment(macCatalyst) + +import Foundation +@_implementationOnly import OneSignalKMP + +/// Sends the KMP logger's encoded OTLP requests using the native URL loading system. +final class OneSignalLogHttpSender: ILogHttpSender { + private static let requestTimeout: TimeInterval = 10 + private static let transportFailureStatusCode: Int32 = -1 + private static let maximumDiagnosticBodyLength = 500 + private static let defaultSession: URLSession = { + let configuration = URLSessionConfiguration.default + configuration.timeoutIntervalForRequest = requestTimeout + configuration.timeoutIntervalForResource = requestTimeout + return URLSession(configuration: configuration) + }() + + typealias RequestSender = ( + URLRequest, + @escaping (Data?, URLResponse?, Error?) -> Void + ) -> Void + + init( + session: URLSession = OneSignalLogHttpSender.defaultSession, + logger: ILogger = IOSLogger(), + isDiagnosticsEnabled: @escaping () -> Bool = { false } + ) { + self.requestSender = { request, completion in + session.dataTask(with: request, completionHandler: completion).resume() + } + self.logger = logger + self.isDiagnosticsEnabled = isDiagnosticsEnabled + } + + init( + requestSender: @escaping RequestSender, + logger: ILogger = IOSLogger(), + isDiagnosticsEnabled: @escaping () -> Bool = { false } + ) { + self.requestSender = requestSender + self.logger = logger + self.isDiagnosticsEnabled = isDiagnosticsEnabled + } + + private let requestSender: RequestSender + private let logger: ILogger + private let isDiagnosticsEnabled: () -> Bool + + func send( + request: LogHttpRequest, + completionHandler: @escaping (LogHttpResponse?, Error?) -> Void + ) { + guard let url = URL(string: request.url) else { + completionHandler( + LogHttpResponse( + success: false, + statusCode: Self.transportFailureStatusCode, + message: "Invalid log request URL" + ), + nil + ) + return + } + + var urlRequest = URLRequest(url: url) + urlRequest.httpMethod = "POST" + urlRequest.httpBody = request.body.data + urlRequest.setValue(request.contentType, forHTTPHeaderField: "Content-Type") + request.headers.forEach { urlRequest.setValue($0.value, forHTTPHeaderField: $0.key) } + + requestSender(urlRequest) { data, response, error in + if let error = error { + if self.isDiagnosticsEnabled() { + self.logger.warn( + message: "OneSignalLogHttpSender: POST \(request.url) failed: \(error.localizedDescription)" + ) + } + completionHandler( + LogHttpResponse( + success: false, + statusCode: Self.transportFailureStatusCode, + message: error.localizedDescription + ), + nil + ) + return + } + + guard let response = response as? HTTPURLResponse else { + completionHandler( + LogHttpResponse( + success: false, + statusCode: Self.transportFailureStatusCode, + message: "Missing HTTP response" + ), + nil + ) + return + } + + let success = (200...299).contains(response.statusCode) + let responseBody = data.flatMap { String(data: $0, encoding: .utf8) } + if self.isDiagnosticsEnabled() { + if success { + self.logger.debug( + message: "OneSignalLogHttpSender: POST \(request.url) -> \(response.statusCode) OK " + + "(\(request.body.size)B)" + ) + } else { + self.logger.warn( + message: "OneSignalLogHttpSender: POST \(request.url) -> \(response.statusCode) " + + "(ct=\(request.contentType), \(request.body.size)B) " + + "body=\(responseBody.map(Self.truncatedDiagnosticBody) ?? "nil")" + ) + } + } + + completionHandler( + LogHttpResponse( + success: success, + statusCode: Int32(response.statusCode), + message: success ? nil : responseBody + ), + nil + ) + } + } + + private static func truncatedDiagnosticBody(_ body: String) -> String { + String(body.prefix(maximumDiagnosticBodyLength)) + } +} + +#endif diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCoreTests/OSLoggerAdaptersTests.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCoreTests/OSLoggerAdaptersTests.swift new file mode 100644 index 000000000..9c856d1e0 --- /dev/null +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCoreTests/OSLoggerAdaptersTests.swift @@ -0,0 +1,297 @@ +/* + Modified MIT License + + Copyright 2026 OneSignal + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + 1. The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + 2. All copies of substantial portions of the Software may only be used in connection + with services provided by OneSignal. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + */ + +import Foundation +import OneSignalCore +import OneSignalKMP +@testable import OneSignalOSCore +import XCTest + +final class OSLoggerAdaptersTests: XCTestCase { + private var temporaryDirectory: URL! + + override func setUpWithError() throws { + temporaryDirectory = FileManager.default.temporaryDirectory + .appendingPathComponent(UUID().uuidString, isDirectory: true) + try FileManager.default.createDirectory(at: temporaryDirectory, withIntermediateDirectories: true) + } + + override func tearDownWithError() throws { + try? FileManager.default.removeItem(at: temporaryDirectory) + } + + func testFileStoreSynchronouslySavesAndListsPayload() throws { + let store = FileLogStore(rootPath: temporaryDirectory.path) + let payload = makeKotlinBytes([1, 2, 3, 255]) + + XCTAssertTrue(store.save(bytes: payload)) + let files = try FileManager.default.contentsOfDirectory(atPath: temporaryDirectory.path) + XCTAssertEqual(files.count, 1) + XCTAssertTrue(files[0].hasSuffix(".otlp")) + + let listed = expectation(description: "lists saved payload") + store.listReadable(minAgeMillis: 0) { records, error in + XCTAssertNil(error) + XCTAssertEqual(records?.count, 1) + XCTAssertEqual(records?.first?.id, files[0]) + XCTAssertEqual(records?.first?.bytes.bytes, [1, 2, 3, 255]) + listed.fulfill() + } + wait(for: [listed], timeout: 2) + } + + func testFileStoreDeletesOnlyInterruptedTemporaryWrites() throws { + let store = FileLogStore(rootPath: temporaryDirectory.path) + XCTAssertTrue(store.save(bytes: makeKotlinBytes([1]))) + + let temporaryURL = temporaryDirectory.appendingPathComponent("interrupted.otlp.tmp") + try Data([2]).write(to: temporaryURL) + let foreignURL = temporaryDirectory.appendingPathComponent("unowned") + try Data([2]).write(to: foreignURL) + + let cleaned = expectation(description: "cleans interrupted write") + store.deleteUnrecognizedEntries(minAgeMillis: 0) { count, error in + XCTAssertNil(error) + XCTAssertEqual(count?.int32Value, 1) + XCTAssertFalse(FileManager.default.fileExists(atPath: temporaryURL.path)) + XCTAssertTrue(FileManager.default.fileExists(atPath: foreignURL.path)) + cleaned.fulfill() + } + wait(for: [cleaned], timeout: 2) + + XCTAssertEqual( + try FileManager.default.contentsOfDirectory(atPath: temporaryDirectory.path) + .filter { $0.hasSuffix(".otlp") } + .count, + 1 + ) + } + + func testFileStoreRejectsEmptyPayload() { + let store = FileLogStore(rootPath: temporaryDirectory.path) + + XCTAssertFalse(store.save(bytes: makeKotlinBytes([]))) + } + + func testHttpSenderPostsEncodedBytesAndPassesHeaders() { + let sent = expectation(description: "sends payload") + let sender = OneSignalLogHttpSender { request, completion in + XCTAssertEqual(request.httpMethod, "POST") + XCTAssertEqual(request.value(forHTTPHeaderField: "Content-Type"), "application/custom") + XCTAssertEqual(request.value(forHTTPHeaderField: "SDK-Version"), "onesignal/ios/test") + XCTAssertEqual(request.httpBody, Data([4, 5, 6])) + let response = HTTPURLResponse( + url: request.url!, + statusCode: 202, + httpVersion: nil, + headerFields: nil + )! + completion(nil, response, nil) + } + + let request = LogHttpRequest( + url: "https://example.com/sdk/log", + headers: [ + "Content-Type": "application/custom", + "SDK-Version": "onesignal/ios/test" + ], + contentType: "application/x-protobuf", + body: makeKotlinBytes([4, 5, 6]) + ) + sender.send(request: request) { response, error in + XCTAssertNil(error) + XCTAssertEqual(response?.statusCode, 202) + XCTAssertTrue(response?.success == true) + sent.fulfill() + } + + wait(for: [sent], timeout: 2) + } + + func testHttpSenderReturnsAndLogsFailureBodyWhenDiagnosticsEnabled() { + let logger = TestLogger() + let sender = OneSignalLogHttpSender( + requestSender: { request, completion in + let response = HTTPURLResponse( + url: request.url!, + statusCode: 403, + httpVersion: nil, + headerFields: nil + )! + completion(Data("denied".utf8), response, nil) + }, + logger: logger, + isDiagnosticsEnabled: { true } + ) + let request = LogHttpRequest( + url: "https://example.com/sdk/log", + headers: [:], + contentType: "application/x-protobuf", + body: makeKotlinBytes([1]) + ) + let sent = expectation(description: "returns failed response") + + sender.send(request: request) { response, error in + XCTAssertNil(error) + XCTAssertEqual(response?.statusCode, 403) + XCTAssertEqual(response?.message, "denied") + XCTAssertEqual(logger.warnings.count, 1) + sent.fulfill() + } + + wait(for: [sent], timeout: 2) + } + + func testLoggerDelegatesToOneSignalLog() { + let listener = LoggerAdapterListener() + OneSignalLog.debug().__add(listener) + defer { OneSignalLog.debug().__remove(listener) } + + let logger = IOSLogger() + logger.error(message: "error") + logger.warn(message: "warn") + logger.info(message: "info") + logger.debug(message: "debug") + + XCTAssertEqual(listener.levels, [.LL_ERROR, .LL_WARN, .LL_INFO, .LL_DEBUG]) + } + + func testKmpPipelineInvokesSwiftAdapters() throws { + let listener = LoggerAdapterListener() + OneSignalLog.debug().__add(listener) + defer { OneSignalLog.debug().__remove(listener) } + let store = FileLogStore(rootPath: temporaryDirectory.path) + let logger = IOSLogger() + let telemetry = LoggerFactory.shared.createCrashLocalTelemetry( + platformProvider: makePlatformProvider(), + fileStore: store + ) + let reporter = LoggerFactory.shared.createCrashReporter( + crashTelemetry: telemetry, + logger: logger + ) + let crash = CrashData( + threadName: "test", + exceptionType: "TestError", + exceptionMessage: "test message", + stacktrace: "test stack" + ) + + _ = try reporter.saveNonFatal(crash: crash) + + XCTAssertEqual(listener.levels, [.LL_INFO, .LL_INFO]) + XCTAssertEqual( + try FileManager.default.contentsOfDirectory(atPath: temporaryDirectory.path) + .filter { $0.hasSuffix(".otlp") } + .count, + 1 + ) + } + + func testPlatformProviderReturnsInjectedIdentifiersAndPlatformMetadata() { + let provider = makePlatformProvider() + var firstInstallId: String? + var secondInstallId: String? + + provider.getInstallId { value, _ in firstInstallId = value } + provider.getInstallId { value, _ in secondInstallId = value } + + XCTAssertEqual(firstInstallId, "install-id") + XCTAssertEqual(firstInstallId, secondInstallId) + XCTAssertEqual(provider.onesignalId, "onesignal-id") + XCTAssertEqual(provider.pushSubscriptionId, "subscription-id") + XCTAssertEqual(provider.appState, "foreground") + XCTAssertEqual(provider.enabledFeatureFlags, ["feature"]) + XCTAssertEqual(provider.remoteLogLevel, "WARN") + XCTAssertTrue(provider.isExporterLoggingEnabled) + XCTAssertEqual(provider.sdkBase, "ios") + XCTAssertFalse(provider.appPackageId.isEmpty) + XCTAssertFalse(provider.osVersion.isEmpty) + XCTAssertGreaterThanOrEqual(provider.processUptime, 0) + } + + func testProcessUptimeUsesProcessStartAndClampsClockMismatch() { + XCTAssertEqual( + OSLoggerPlatformProvider.processUptimeMillis( + systemUptime: 100, + processStartUptime: 95 + ), + 5_000 + ) + XCTAssertEqual( + OSLoggerPlatformProvider.processUptimeMillis( + systemUptime: 95, + processStartUptime: 100 + ), + 0 + ) + } + + private func makePlatformProvider() -> OSLoggerPlatformProvider { + OSLoggerPlatformProvider( + installIdProvider: { "install-id" }, + onesignalIdProvider: { "onesignal-id" }, + pushSubscriptionIdProvider: { "subscription-id" }, + appStateProvider: { "foreground" }, + featureFlagsProvider: { ["feature"] }, + remoteLogLevelProvider: { "warn" }, + exporterLoggingEnabledProvider: { true } + ) + } + + private func makeKotlinBytes(_ bytes: [UInt8]) -> KotlinByteArray { + AppleByteArrayInterop.shared.toByteArray(data: Data(bytes)) + } +} + +private final class LoggerAdapterListener: NSObject, OSLogListener { + var levels: [ONE_S_LOG_LEVEL] = [] + + func onLogEvent(_ event: OneSignalLogEvent) { + levels.append(event.level) + } +} + +private final class TestLogger: NSObject, ILogger { + var warnings: [String] = [] + + func error(message: String) {} + + func warn(message: String) { + warnings.append(message) + } + + func info(message: String) {} + + func debug(message: String) {} +} + +private extension KotlinByteArray { + var bytes: [UInt8] { + Array(AppleByteArrayInterop.shared.toNSData(bytes: self)) + } +}