Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 13 additions & 9 deletions examples/demo/App.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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, ); }; };
Expand Down Expand Up @@ -331,6 +332,7 @@
5984B93007C6B85AFE09045A /* KeyValueRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyValueRow.swift; sourceTree = "<group>"; };
5C0EE23773C63B6EC3FB563A /* LocationSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationSection.swift; sourceTree = "<group>"; };
5E0F5CBE80CF861238E1A9AA /* OneSignalService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OneSignalService.swift; sourceTree = "<group>"; };
6902EC187E8FAC8926412762 /* SecondaryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecondaryView.swift; sourceTree = "<group>"; };
6B00BC406653BC6B08ECCE26 /* Secrets.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Secrets.plist; sourceTree = "<group>"; };
6DD14C3CEEDFB9557E589B45 /* OneSignalWidgetBundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OneSignalWidgetBundle.swift; sourceTree = "<group>"; };
76989E05CECAD7B8B3C424A7 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
Expand Down Expand Up @@ -358,7 +360,7 @@
E194A3F19072CB17A8F1A12E /* SmsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SmsSection.swift; sourceTree = "<group>"; };
ECAC7EF0B67920F9FEC4F129 /* TagsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagsSection.swift; sourceTree = "<group>"; };
F46DFACB9F304B9374F3C570 /* ToastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastView.swift; sourceTree = "<group>"; };
"TEMP_E243B27C-2274-4671-9C94-8B86EB8D4EFA" /* Build.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Build.xcconfig; sourceTree = "<group>"; };
"TEMP_28AA92EA-E2C8-4969-80AB-657A0AF14B2F" /* Build.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Build.xcconfig; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -559,15 +561,16 @@
B9C3E998662065E7D921A5CA /* Components */,
53EC9A9E14C3568A020C0977 /* Sections */,
35F726E64F9B6817F917227C /* ContentView.swift */,
6902EC187E8FAC8926412762 /* SecondaryView.swift */,
B3E7F504B0421F2B6247E2F5 /* Theme.swift */,
);
path = Views;
sourceTree = "<group>";
};
"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 = "<group>";
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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";
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down
10 changes: 10 additions & 0 deletions examples/demo/App/Views/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
Expand All @@ -73,6 +80,9 @@ struct ContentView: View {
.toolbarBackground(.visible, for: .navigationBar)
.toolbarColorScheme(.dark, for: .navigationBar)
.toolbar { toolbarContent }
.navigationDestination(isPresented: $showSecondary) {
SecondaryView()
}
}
.osCenteredDialog(
isPresented: Binding(
Expand Down
82 changes: 82 additions & 0 deletions examples/demo/App/Views/SecondaryView.swift
Original file line number Diff line number Diff line change
@@ -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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider — only demo file importing OneSignalOSCore (C/D).

Other app sources use OneSignalFramework / feature modules. This import exists solely for OSCrashTestMarker.userInfoKey and teaches sub-framework coupling.

If the raise helper / private marker path lands, this import should disappear.

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()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Act on / judo — host-raised crash forces the bypass (C).

Raising from app code guarantees no OneSignal frame, which is why the marker exists. An OSCore helper that raises would put OneSignalOSCore on the stack (oneSignalModules already includes it) and keep handle attribution-only — no public key, no unlabeled host records.

Also: DateFormatter without en_US_POSIX makes the reason string locale-dependent; with unlabeled payloads that string is the only filter handle.

}
}

#Preview {
NavigationStack {
SecondaryView()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Act on — undeclared public API (A/B/C/D).

OneSignalOSCore ships to integrators (SPM + xcframework). This public enum is new supported surface whose only in-repo consumer is the demo, and the PR checklist leaves Public API changes unchecked.

Commit 7f205d3a already gated the same path with private static let testCrashExceptionName and no public symbol; a0f0e53f traded that for Android-parity RuntimeException + this key.

Prefer an OSCore-owned raise helper (throwing frame = OneSignalOSCore → existing isOneSignalAtFault), or keep the marker internal/@_spi and document if a public constant is truly required.


final class OSCrashLogger: ILogger {
func error(message: String) {
NSLog("[OneSignal crash] ERROR: %@", message)
Expand Down Expand Up @@ -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 {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Act on — demo concern on the shared fatal path (A/C/D).

This || punches a hole in the host-crash firewall from #1713 for any process that can set the documented key. Complexity belongs with the demo raise site, not every uncaught exception.

Code judo: OSCrashTest.raise(...) (or equivalent) inside OneSignalOSCore so attribution already accepts the frame — then delete isMarkedTestCrash entirely.

previousExceptionHandler?(exception)
return
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Act on — marker is gate-only; uploaded records are unlabeled (A/C/D).

After the guard admits a marked crash, the marker is discarded. CrashData gets exception.name (RuntimeException), caller reason, and a pure host stack — indistinguishable from a real SDK-attributed fault except brittle string matching.

The name-based predecessor self-labeled via exceptionType == "com.onesignal.crash.test". If a bypass must remain, stamp a deterministic type/prefix here when the marker branch fires, and assert it in tests.

Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider — thin verification vs claimed contract (B/D).

Covers Swift true.otlp count only. Missing: NSNumber/@YES bridging, previous-handler still forwarded after marked capture, payload exceptionType/message assertions, and any raise→uncaught-handler path (tests call handle directly). reason: nil also diverges from the demo’s always-set reason.

}

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(
Expand Down
Loading