diff --git a/examples/demo/App.xcodeproj/project.pbxproj b/examples/demo/App.xcodeproj/project.pbxproj index c2eb9fe65..ab0de05ff 100644 --- a/examples/demo/App.xcodeproj/project.pbxproj +++ b/examples/demo/App.xcodeproj/project.pbxproj @@ -30,6 +30,7 @@ 2FA8128D2A921DDF02210D8A /* RemoveMultiDialog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A654457BF0A55B54220E669 /* RemoveMultiDialog.swift */; }; 2FCC417641D480849E99588B /* SmsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = E194A3F19072CB17A8F1A12E /* SmsSection.swift */; }; 3038C8C43A465DFED77AA533 /* OneSignalNotificationServiceExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = B24E059F4DF6ABED55BA3183 /* OneSignalNotificationServiceExtension.appex */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 38E86618D2F63D37CA764656 /* SecondaryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6902EC187E8FAC8926412762 /* SecondaryView.swift */; }; 3927A4BF207695E98A57E445 /* TagsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECAC7EF0B67920F9FEC4F129 /* TagsSection.swift */; }; 39D2C94F79A62BFF9DE5DBA9 /* OutcomeDialog.swift in Sources */ = {isa = PBXBuildFile; fileRef = D54B9DAAEDBE67B73893C522 /* OutcomeDialog.swift */; }; 3C899E2494DE29756F5451BE /* OneSignalNotifications.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 283B132CEE12D26D1FA1AADF /* OneSignalNotifications.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; @@ -331,6 +332,7 @@ 5984B93007C6B85AFE09045A /* KeyValueRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyValueRow.swift; sourceTree = ""; }; 5C0EE23773C63B6EC3FB563A /* LocationSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationSection.swift; sourceTree = ""; }; 5E0F5CBE80CF861238E1A9AA /* OneSignalService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OneSignalService.swift; sourceTree = ""; }; + 6902EC187E8FAC8926412762 /* SecondaryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecondaryView.swift; sourceTree = ""; }; 6B00BC406653BC6B08ECCE26 /* Secrets.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Secrets.plist; sourceTree = ""; }; 6DD14C3CEEDFB9557E589B45 /* OneSignalWidgetBundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OneSignalWidgetBundle.swift; sourceTree = ""; }; 76989E05CECAD7B8B3C424A7 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -358,7 +360,7 @@ E194A3F19072CB17A8F1A12E /* SmsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SmsSection.swift; sourceTree = ""; }; ECAC7EF0B67920F9FEC4F129 /* TagsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagsSection.swift; sourceTree = ""; }; F46DFACB9F304B9374F3C570 /* ToastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastView.swift; sourceTree = ""; }; - "TEMP_E243B27C-2274-4671-9C94-8B86EB8D4EFA" /* Build.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Build.xcconfig; sourceTree = ""; }; + "TEMP_28AA92EA-E2C8-4969-80AB-657A0AF14B2F" /* Build.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Build.xcconfig; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -559,15 +561,16 @@ B9C3E998662065E7D921A5CA /* Components */, 53EC9A9E14C3568A020C0977 /* Sections */, 35F726E64F9B6817F917227C /* ContentView.swift */, + 6902EC187E8FAC8926412762 /* SecondaryView.swift */, B3E7F504B0421F2B6247E2F5 /* Theme.swift */, ); path = Views; sourceTree = ""; }; - "TEMP_9E448277-C21D-48E5-98E2-992293BCF68A" /* demo */ = { + "TEMP_D8085C8F-3CA5-457D-9FB3-7C8E9DA8B66A" /* demo */ = { isa = PBXGroup; children = ( - "TEMP_E243B27C-2274-4671-9C94-8B86EB8D4EFA" /* Build.xcconfig */, + "TEMP_28AA92EA-E2C8-4969-80AB-657A0AF14B2F" /* Build.xcconfig */, ); path = demo; sourceTree = ""; @@ -820,6 +823,7 @@ 20792A9930A201E187AA0ABF /* PreferencesService.swift in Sources */, EA93E372AA3E66073487B89C /* PushSection.swift in Sources */, 2FA8128D2A921DDF02210D8A /* RemoveMultiDialog.swift in Sources */, + 38E86618D2F63D37CA764656 /* SecondaryView.swift in Sources */, 28D491D31B5C07E4D4F48A7D /* SecretsConfig.swift in Sources */, 7B94F48C31E0BEA4B8CB20E2 /* SectionCard.swift in Sources */, 27C72DF35BE082E3E1093F75 /* SendIamSection.swift in Sources */, @@ -936,7 +940,7 @@ /* Begin XCBuildConfiguration section */ 0D2EF3911CA89837C30DB0D1 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = "TEMP_E243B27C-2274-4671-9C94-8B86EB8D4EFA" /* Build.xcconfig */; + baseConfigurationReference = "TEMP_28AA92EA-E2C8-4969-80AB-657A0AF14B2F" /* Build.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; @@ -954,7 +958,7 @@ }; 4A0C935808978B5A7673E412 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = "TEMP_E243B27C-2274-4671-9C94-8B86EB8D4EFA" /* Build.xcconfig */; + baseConfigurationReference = "TEMP_28AA92EA-E2C8-4969-80AB-657A0AF14B2F" /* Build.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = App.entitlements; @@ -1034,7 +1038,7 @@ }; D0E56A85F1C385808720F94B /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = "TEMP_E243B27C-2274-4671-9C94-8B86EB8D4EFA" /* Build.xcconfig */; + baseConfigurationReference = "TEMP_28AA92EA-E2C8-4969-80AB-657A0AF14B2F" /* Build.xcconfig */; buildSettings = { CODE_SIGN_ENTITLEMENTS = OneSignalNotificationServiceExtension/OneSignalNotificationServiceExtension.entitlements; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; @@ -1056,7 +1060,7 @@ }; EB1CC3A930E09FEBECF9195D /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = "TEMP_E243B27C-2274-4671-9C94-8B86EB8D4EFA" /* Build.xcconfig */; + baseConfigurationReference = "TEMP_28AA92EA-E2C8-4969-80AB-657A0AF14B2F" /* Build.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = App.entitlements; @@ -1079,7 +1083,7 @@ }; F305A3E63851EE49DA2D190E /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = "TEMP_E243B27C-2274-4671-9C94-8B86EB8D4EFA" /* Build.xcconfig */; + baseConfigurationReference = "TEMP_28AA92EA-E2C8-4969-80AB-657A0AF14B2F" /* Build.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; @@ -1102,7 +1106,7 @@ }; F5FD25168D9B32A08A468069 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = "TEMP_E243B27C-2274-4671-9C94-8B86EB8D4EFA" /* Build.xcconfig */; + baseConfigurationReference = "TEMP_28AA92EA-E2C8-4969-80AB-657A0AF14B2F" /* Build.xcconfig */; buildSettings = { CODE_SIGN_ENTITLEMENTS = OneSignalNotificationServiceExtension/OneSignalNotificationServiceExtension.entitlements; DEVELOPMENT_TEAM = 99SW8E36CT; diff --git a/examples/demo/App/Views/ContentView.swift b/examples/demo/App/Views/ContentView.swift index cdc99be60..9e6578488 100644 --- a/examples/demo/App/Views/ContentView.swift +++ b/examples/demo/App/Views/ContentView.swift @@ -31,6 +31,7 @@ import SwiftUI struct ContentView: View { @EnvironmentObject var viewModel: OneSignalViewModel @EnvironmentObject var toast: ToastPresenter + @State private var showSecondary = false var body: some View { NavigationStack { @@ -51,6 +52,12 @@ struct ContentView: View { CustomEventsSection() LocationSection() LiveActivitySection() + ActionButton( + "NEXT SCREEN", + accessibilityID: "next_screen_button" + ) { + showSecondary = true + } } .padding(.horizontal, OS.Spacing.pagePadding) .padding(.top, OS.Spacing.pagePadding) @@ -73,6 +80,9 @@ struct ContentView: View { .toolbarBackground(.visible, for: .navigationBar) .toolbarColorScheme(.dark, for: .navigationBar) .toolbar { toolbarContent } + .navigationDestination(isPresented: $showSecondary) { + SecondaryView() + } } .osCenteredDialog( isPresented: Binding( diff --git a/examples/demo/App/Views/SecondaryView.swift b/examples/demo/App/Views/SecondaryView.swift new file mode 100644 index 000000000..dc18d9770 --- /dev/null +++ b/examples/demo/App/Views/SecondaryView.swift @@ -0,0 +1,82 @@ +/** + * Modified MIT License + * + * Copyright 2024 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 OneSignalOSCore +import SwiftUI + +/// Isolated screen for the test-crash action, matching Android's SecondaryActivity. +struct SecondaryView: View { + var body: some View { + VStack(spacing: 32) { + Spacer() + + Text("Secondary Screen") + .font(.system(size: 28)) + .foregroundColor(OS.Color.bodyText) + .accessibilityIdentifier("secondary_screen_title") + + ActionButton( + "CRASH", + style: .outline, + accessibilityID: "crash_button" + ) { + triggerCrash() + } + + Spacer() + } + .padding(.horizontal, OS.Spacing.pagePadding) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(OS.Color.lightBackground.ignoresSafeArea()) + .navigationTitle("Secondary Screen") + .navigationBarTitleDisplayMode(.inline) + .toolbarBackground(OS.Color.primary, for: .navigationBar) + .toolbarBackground(.visible, for: .navigationBar) + .toolbarColorScheme(.dark, for: .navigationBar) + } + + /// NSException reaches the SDK uncaught-exception handler. Swift `fatalError` is a POSIX + /// signal, which that handler does not intercept. The marker is required because this + /// exception is raised from app code, so no OneSignal frame appears on the stack. + private func triggerCrash() { + let formatter = DateFormatter() + formatter.dateFormat = "MMM dd, yyyy HH:mm:ss" + let timestamp = formatter.string(from: Date()) + NSException( + name: NSExceptionName("RuntimeException"), + reason: "Test crash from OneSignal Demo App - \(timestamp)", + userInfo: [OSCrashTestMarker.userInfoKey: true] + ).raise() + } +} + +#Preview { + NavigationStack { + SecondaryView() + } +} diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLogCrashHandler.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLogCrashHandler.swift index a310fe9b3..5269748fa 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLogCrashHandler.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Logging/OSLogCrashHandler.swift @@ -35,6 +35,15 @@ private func osLogUncaughtExceptionHandler(_ exception: NSException) { OSLogCrashHandler.handleActive(exception) } +/// Opts an exception into crash reporting that module attribution would otherwise reject. +/// +/// Demo and integration apps raise their test crash from their own code, so no OneSignal +/// frame appears on the stack and `isOneSignalAtFault` correctly returns false. Setting +/// this key to `true` in `NSException.userInfo` is the only supported way to bypass that. +public enum OSCrashTestMarker { + public static let userInfoKey = "com.onesignal.crash.test" +} + final class OSCrashLogger: ILogger { func error(message: String) { NSLog("[OneSignal crash] ERROR: %@", message) @@ -131,7 +140,7 @@ final class OSLogCrashHandler: ILogCrashHandler { } func handle(exception: NSException, stackSymbols: [String]) { - guard Self.isOneSignalAtFault(stackSymbols) else { + guard Self.isMarkedTestCrash(exception) || Self.isOneSignalAtFault(stackSymbols) else { previousExceptionHandler?(exception) return } @@ -199,6 +208,10 @@ final class OSLogCrashHandler: ILogCrashHandler { } } + static func isMarkedTestCrash(_ exception: NSException) -> Bool { + exception.userInfo?[OSCrashTestMarker.userInfoKey] as? Bool == true + } + static func isOneSignalAtFault(_ stackSymbols: [String]) -> Bool { for frame in stackSymbols { guard let module = moduleName(from: frame) else { diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCoreTests/OSLogCrashHandlerTests.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCoreTests/OSLogCrashHandlerTests.swift index d68c704b5..315d0d569 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalOSCoreTests/OSLogCrashHandlerTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCoreTests/OSLogCrashHandlerTests.swift @@ -76,6 +76,41 @@ final class OSLogCrashHandlerTests: XCTestCase { XCTAssertTrue(try FileManager.default.contentsOfDirectory(atPath: temporaryDirectory.path).isEmpty) } + func testPersistsMarkedTestCrashWithoutOneSignalModule() throws { + let handler = makeCrashHandler() + + handler.handle( + exception: NSException( + name: NSExceptionName("RuntimeException"), + reason: nil, + userInfo: [OSCrashTestMarker.userInfoKey: true] + ), + stackSymbols: ["0 ExampleApp 0x000000 SecondaryView.triggerCrash + 1"] + ) + + XCTAssertEqual( + try FileManager.default.contentsOfDirectory(atPath: temporaryDirectory.path) + .filter { $0.hasSuffix(".otlp") } + .count, + 1 + ) + } + + func testIgnoresUnmarkedHostCrashSharingTheMarkerKey() throws { + let handler = makeCrashHandler() + + handler.handle( + exception: NSException( + name: NSExceptionName("RuntimeException"), + reason: nil, + userInfo: [OSCrashTestMarker.userInfoKey: false] + ), + stackSymbols: ["0 ExampleApp 0x000000 SecondaryView.triggerCrash + 1"] + ) + + XCTAssertTrue(try FileManager.default.contentsOfDirectory(atPath: temporaryDirectory.path).isEmpty) + } + func testIgnoresOneSignalSubstringOutsideModuleField() { XCTAssertFalse( OSLogCrashHandler.isOneSignalAtFault(