From 7b3eca058c73bbf2f47e0bce644f39320da8530f Mon Sep 17 00:00:00 2001 From: Fadi George Date: Thu, 13 Aug 2026 22:16:00 -0700 Subject: [PATCH 1/7] chore(demo): add Mac Catalyst support --- examples/demo/App.xcodeproj/project.pbxproj | 22 ++++++---- .../App/Services/LiveActivityController.swift | 6 +++ .../OneSignalWidgetBundle.swift | 4 ++ .../OneSignalWidgetLiveActivity.swift | 42 ++++++++++++++++++- examples/demo/project.yml | 2 + 5 files changed, 66 insertions(+), 10 deletions(-) diff --git a/examples/demo/App.xcodeproj/project.pbxproj b/examples/demo/App.xcodeproj/project.pbxproj index 7de7b5a01..ae17cd23e 100644 --- a/examples/demo/App.xcodeproj/project.pbxproj +++ b/examples/demo/App.xcodeproj/project.pbxproj @@ -364,7 +364,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_ABAB8D6F-E57E-4D74-98B6-E29157EB8D71" /* Build.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Build.xcconfig; sourceTree = ""; }; + "TEMP_DEC528CA-1E47-4B0F-AAAF-071417BC60C5" /* Build.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Build.xcconfig; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -582,10 +582,10 @@ path = Support; sourceTree = ""; }; - "TEMP_50803CDB-250B-41C8-93E2-9DF8EB2C4802" /* demo */ = { + "TEMP_17E43425-D1F7-4A88-B0A0-C0368CB0E171" /* demo */ = { isa = PBXGroup; children = ( - "TEMP_ABAB8D6F-E57E-4D74-98B6-E29157EB8D71" /* Build.xcconfig */, + "TEMP_DEC528CA-1E47-4B0F-AAAF-071417BC60C5" /* Build.xcconfig */, ); path = demo; sourceTree = ""; @@ -956,7 +956,7 @@ /* Begin XCBuildConfiguration section */ 0D2EF3911CA89837C30DB0D1 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = "TEMP_ABAB8D6F-E57E-4D74-98B6-E29157EB8D71" /* Build.xcconfig */; + baseConfigurationReference = "TEMP_DEC528CA-1E47-4B0F-AAAF-071417BC60C5" /* Build.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; @@ -974,7 +974,7 @@ }; 4A0C935808978B5A7673E412 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = "TEMP_ABAB8D6F-E57E-4D74-98B6-E29157EB8D71" /* Build.xcconfig */; + baseConfigurationReference = "TEMP_DEC528CA-1E47-4B0F-AAAF-071417BC60C5" /* Build.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = App.entitlements; @@ -1029,6 +1029,7 @@ COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIER = YES; DEVELOPMENT_TEAM = ""; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -1047,6 +1048,7 @@ MTL_FAST_MATH = YES; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; + SUPPORTS_MACCATALYST = YES; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; SWIFT_VERSION = 5.9; @@ -1055,7 +1057,7 @@ }; D0E56A85F1C385808720F94B /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = "TEMP_ABAB8D6F-E57E-4D74-98B6-E29157EB8D71" /* Build.xcconfig */; + baseConfigurationReference = "TEMP_DEC528CA-1E47-4B0F-AAAF-071417BC60C5" /* Build.xcconfig */; buildSettings = { CODE_SIGN_ENTITLEMENTS = OneSignalNotificationServiceExtension/OneSignalNotificationServiceExtension.entitlements; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; @@ -1077,7 +1079,7 @@ }; EB1CC3A930E09FEBECF9195D /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = "TEMP_ABAB8D6F-E57E-4D74-98B6-E29157EB8D71" /* Build.xcconfig */; + baseConfigurationReference = "TEMP_DEC528CA-1E47-4B0F-AAAF-071417BC60C5" /* Build.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = App.entitlements; @@ -1101,7 +1103,7 @@ }; F305A3E63851EE49DA2D190E /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = "TEMP_ABAB8D6F-E57E-4D74-98B6-E29157EB8D71" /* Build.xcconfig */; + baseConfigurationReference = "TEMP_DEC528CA-1E47-4B0F-AAAF-071417BC60C5" /* Build.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; @@ -1124,7 +1126,7 @@ }; F5FD25168D9B32A08A468069 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = "TEMP_ABAB8D6F-E57E-4D74-98B6-E29157EB8D71" /* Build.xcconfig */; + baseConfigurationReference = "TEMP_DEC528CA-1E47-4B0F-AAAF-071417BC60C5" /* Build.xcconfig */; buildSettings = { CODE_SIGN_ENTITLEMENTS = OneSignalNotificationServiceExtension/OneSignalNotificationServiceExtension.entitlements; DEVELOPMENT_TEAM = 99SW8E36CT; @@ -1176,6 +1178,7 @@ COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = dwarf; + DERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIER = YES; DEVELOPMENT_TEAM = ""; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -1201,6 +1204,7 @@ ONLY_ACTIVE_ARCH = YES; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; + SUPPORTS_MACCATALYST = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.9; diff --git a/examples/demo/App/Services/LiveActivityController.swift b/examples/demo/App/Services/LiveActivityController.swift index 8d00a542d..94c6cee21 100644 --- a/examples/demo/App/Services/LiveActivityController.swift +++ b/examples/demo/App/Services/LiveActivityController.swift @@ -27,7 +27,9 @@ import Foundation import OneSignalFramework +#if !targetEnvironment(macCatalyst) import OneSignalLiveActivities +#endif /// Order tracking phases used by the Live Activity demo enum LiveActivityStatus: String, CaseIterable, Identifiable { @@ -76,7 +78,9 @@ enum LiveActivityController { @available(iOS 16.1, *) static func setup() { + #if !targetEnvironment(macCatalyst) OneSignal.LiveActivities.setupDefault() + #endif } @available(iOS 16.1, *) @@ -85,6 +89,7 @@ enum LiveActivityController { orderNumber: String, status: LiveActivityStatus ) { + #if !targetEnvironment(macCatalyst) let attributes: [String: Any] = [ "orderNumber": orderNumber ] @@ -98,6 +103,7 @@ enum LiveActivityController { attributes: attributes, content: content ) + #endif } static func update(appId: String, activityId: String, status: LiveActivityStatus) async -> Bool { diff --git a/examples/demo/OneSignalWidget/OneSignalWidgetBundle.swift b/examples/demo/OneSignalWidget/OneSignalWidgetBundle.swift index 063a36e0c..4db76db04 100644 --- a/examples/demo/OneSignalWidget/OneSignalWidgetBundle.swift +++ b/examples/demo/OneSignalWidget/OneSignalWidgetBundle.swift @@ -31,8 +31,12 @@ import SwiftUI @main struct OneSignalWidgetBundle: WidgetBundle { var body: some Widget { + #if targetEnvironment(macCatalyst) + OneSignalWidgetPlaceholder() + #else if #available(iOS 16.2, *) { OneSignalWidgetLiveActivity() } + #endif } } diff --git a/examples/demo/OneSignalWidget/OneSignalWidgetLiveActivity.swift b/examples/demo/OneSignalWidget/OneSignalWidgetLiveActivity.swift index cf4a029d3..181af0ea4 100644 --- a/examples/demo/OneSignalWidget/OneSignalWidgetLiveActivity.swift +++ b/examples/demo/OneSignalWidget/OneSignalWidgetLiveActivity.swift @@ -25,9 +25,48 @@ * THE SOFTWARE. */ -import ActivityKit import WidgetKit import SwiftUI + +#if targetEnvironment(macCatalyst) +private struct CatalystPlaceholderEntry: TimelineEntry { + let date: Date +} + +private struct CatalystPlaceholderProvider: TimelineProvider { + func placeholder(in context: Context) -> CatalystPlaceholderEntry { + CatalystPlaceholderEntry(date: Date()) + } + + func getSnapshot( + in context: Context, + completion: @escaping (CatalystPlaceholderEntry) -> Void + ) { + completion(CatalystPlaceholderEntry(date: Date())) + } + + func getTimeline( + in context: Context, + completion: @escaping (Timeline) -> Void + ) { + completion(Timeline(entries: [CatalystPlaceholderEntry(date: Date())], policy: .never)) + } +} + +struct OneSignalWidgetPlaceholder: Widget { + var body: some WidgetConfiguration { + StaticConfiguration( + kind: "com.onesignal.example.catalyst-placeholder", + provider: CatalystPlaceholderProvider() + ) { _ in + Text("OneSignal") + } + .configurationDisplayName("OneSignal") + .description("Live Activities are unavailable on Mac Catalyst.") + } +} +#else +import ActivityKit import OneSignalLiveActivities /// Live Activity widget that renders the order tracking flow used by the demo. @@ -173,3 +212,4 @@ struct DeliveryProgressBar: View { .frame(height: 6) } } +#endif diff --git a/examples/demo/project.yml b/examples/demo/project.yml index 16736ca48..22f5b3842 100644 --- a/examples/demo/project.yml +++ b/examples/demo/project.yml @@ -18,6 +18,8 @@ settings: CODE_SIGN_STYLE: Automatic DEVELOPMENT_TEAM: "" GENERATE_INFOPLIST_FILE: NO + SUPPORTS_MACCATALYST: YES + DERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIER: YES projectReferences: OneSignalSDK: path: ../../iOS_SDK/OneSignalSDK/OneSignal.xcodeproj From 1acf3b254fd006fe7f610c0c43ac66b1ba580a61 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Thu, 13 Aug 2026 22:48:26 -0700 Subject: [PATCH 2/7] fix(storage): create directory before atomic write --- .../Source/OSResilientStorage.swift | 28 +++++++++++++++---- .../OSResilientStorageTests.swift | 12 ++++++++ 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSResilientStorage.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSResilientStorage.swift index 21f3b07ae..c9dc9f260 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSResilientStorage.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSResilientStorage.swift @@ -59,27 +59,45 @@ public final class OSResilientStorage: NSObject { /// the NSE can read the same file. Falls back to the app's private /// Application Support directory when no App Group is entitled. private static func fileURL() -> URL? { - let fm = FileManager.default + let fileManager = FileManager.default let groupName = OneSignalUserDefaults.appGroupName() - if let container = fm.containerURL(forSecurityApplicationGroupIdentifier: groupName) { - return container.appendingPathComponent(fileName) + if let container = fileManager.containerURL(forSecurityApplicationGroupIdentifier: groupName) { + do { + return try preparedFileURL(in: container, fileManager: fileManager) + } catch { + OneSignalLog.onesignalLog( + .LL_WARN, + message: "OSResilientStorage could not prepare the App Group container: \(error)" + ) + } } do { - let support = try fm.url( + let support = try fileManager.url( for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true ) - return support.appendingPathComponent(fileName) + return try preparedFileURL(in: support, fileManager: fileManager) } catch { OneSignalLog.onesignalLog(.LL_ERROR, message: "OSResilientStorage could not resolve a container URL: \(error)") return nil } } + static func preparedFileURL( + in directory: URL, + fileManager: FileManager = .default + ) throws -> URL { + try fileManager.createDirectory( + at: directory, + withIntermediateDirectories: true + ) + return directory.appendingPathComponent(fileName) + } + /// Reads the cache file. Caller is responsible for queue-serialization. /// Returns an empty dict if the file is missing or unreadable. private static func loadUnsafe() -> [String: String] { diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCoreTests/OSResilientStorageTests.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCoreTests/OSResilientStorageTests.swift index af6f527d6..cf7ebade7 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalOSCoreTests/OSResilientStorageTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCoreTests/OSResilientStorageTests.swift @@ -64,6 +64,18 @@ final class OSResilientStorageTests: XCTestCase { XCTAssertNil(OSResilientStorage.string(forKey: "never_written_\(UUID().uuidString)")) } + func testPreparedFileURL_createsMissingContainerForAtomicWrites() throws { + let root = FileManager.default.temporaryDirectory + .appendingPathComponent(UUID().uuidString, isDirectory: true) + let container = root.appendingPathComponent("group.example", isDirectory: true) + defer { try? FileManager.default.removeItem(at: root) } + + let fileURL = try OSResilientStorage.preparedFileURL(in: container) + try Data("value".utf8).write(to: fileURL, options: .atomic) + + XCTAssertTrue(FileManager.default.fileExists(atPath: fileURL.path)) + } + func testSetWithNil_removesKey() { OSResilientStorage.setString("alpha", forKey: keyA) XCTAssertEqual(OSResilientStorage.string(forKey: keyA), "alpha") From 1e4e6a5a548be484f7872b2fe19754a5a42063f0 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Thu, 13 Aug 2026 22:50:25 -0700 Subject: [PATCH 3/7] fix(live-activities): stub out on Mac Catalyst --- iOS_SDK/OneSignalSDK/Source/OneSignal.m | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/iOS_SDK/OneSignalSDK/Source/OneSignal.m b/iOS_SDK/OneSignalSDK/Source/OneSignal.m index 0b3313bf8..0a1bd38ec 100755 --- a/iOS_SDK/OneSignalSDK/Source/OneSignal.m +++ b/iOS_SDK/OneSignalSDK/Source/OneSignal.m @@ -26,6 +26,7 @@ */ #import +#import #import "OneSignalFramework.h" #import #import "OneSignalInternal.h" @@ -206,6 +207,9 @@ + (void)logout { } + (Class)LiveActivities { +#if TARGET_OS_MACCATALYST + return [OSStubLiveActivities liveActivities]; +#else let oneSignalLiveActivities = NSClassFromString(ONE_SIGNAL_LIVE_ACTIVITIES_CLASS_NAME); if (oneSignalLiveActivities != nil && [oneSignalLiveActivities respondsToSelector:@selector(liveActivities)]) { return [oneSignalLiveActivities performSelector:@selector(liveActivities)]; @@ -213,6 +217,7 @@ + (void)logout { [OneSignalLog onesignalLog:ONE_S_LL_ERROR message:@"oneSignalLiveActivities not found. In order to use OneSignal's LiveActivities features the OneSignalLiveActivities module must be added."]; return [OSStubLiveActivities liveActivities]; } +#endif } + (Class)Location { @@ -440,12 +445,16 @@ + (void)startUserManager { } + (void)startLiveActivitiesManager { +#if TARGET_OS_MACCATALYST + return; +#else let oneSignalLiveActivities = NSClassFromString(ONE_SIGNAL_LIVE_ACTIVITIES_CLASS_NAME); if (oneSignalLiveActivities != nil && [oneSignalLiveActivities respondsToSelector:@selector(start)]) { [oneSignalLiveActivities performSelector:@selector(start)]; } else { [OneSignalLog onesignalLog:ONE_S_LL_ERROR message:@"oneSignalLiveActivities not found. In order to use OneSignal's LiveActivities features the OneSignalLiveActivities module must be added."]; } +#endif } + (void)delayInitializationForPrivacyConsent { From ddc633b46b6bd721f6736af3498c8cc5a82fc632 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Tue, 18 Aug 2026 17:03:40 -0700 Subject: [PATCH 4/7] fix(catalyst): preserve demo platform semantics Pin shared storage to the entitled App Group and prevent unsupported Live Activities behavior from masking Catalyst validation failures. Co-authored-by: Cursor --- examples/demo/App/App.swift | 2 + examples/demo/App/Info.plist | 2 + .../App/Services/LiveActivityController.swift | 16 +++++++- examples/demo/App/Views/ContentView.swift | 2 + .../Info.plist | 2 + .../Source/OSResilientStorage.swift | 39 ++++++++++++++----- .../Source/OSStubLiveActivities.swift | 18 ++++++--- .../OSResilientStorageTests.swift | 20 ++++++++++ 8 files changed, 85 insertions(+), 16 deletions(-) diff --git a/examples/demo/App/App.swift b/examples/demo/App/App.swift index 58de08ea3..8111e8756 100644 --- a/examples/demo/App/App.swift +++ b/examples/demo/App/App.swift @@ -27,7 +27,9 @@ import SwiftUI import OneSignalFramework +#if !targetEnvironment(macCatalyst) import OneSignalLiveActivities +#endif @main struct App: SwiftUI.App { diff --git a/examples/demo/App/Info.plist b/examples/demo/App/Info.plist index 21a2421ab..5138a7d3f 100644 --- a/examples/demo/App/Info.plist +++ b/examples/demo/App/Info.plist @@ -26,6 +26,8 @@ This app uses your location to personalize notifications and content. NSLocationAlwaysAndWhenInUseUsageDescription This app uses your location to personalize notifications and content even when the app is in the background. + OneSignal_app_groups_key + group.com.onesignal.example.onesignal NSSupportsLiveActivities NSSupportsLiveActivitiesFrequentUpdates diff --git a/examples/demo/App/Services/LiveActivityController.swift b/examples/demo/App/Services/LiveActivityController.swift index 94c6cee21..5d0ba8797 100644 --- a/examples/demo/App/Services/LiveActivityController.swift +++ b/examples/demo/App/Services/LiveActivityController.swift @@ -107,6 +107,9 @@ enum LiveActivityController { } static func update(appId: String, activityId: String, status: LiveActivityStatus) async -> Bool { + #if targetEnvironment(macCatalyst) + return false + #else let payload: [String: Any] = [ "event": "update", "name": "Live Activity Update", @@ -120,9 +123,13 @@ enum LiveActivityController { ] ] return await postLiveActivity(appId: appId, activityId: activityId, payload: payload) + #endif } static func end(appId: String, activityId: String) async -> Bool { + #if targetEnvironment(macCatalyst) + return false + #else let payload: [String: Any] = [ "event": "end", "name": "End Live Activity", @@ -133,9 +140,16 @@ enum LiveActivityController { ] ] return await postLiveActivity(appId: appId, activityId: activityId, payload: payload) + #endif } - static var hasApiKey: Bool { SecretsConfig.hasApiKey } + static var hasApiKey: Bool { + #if targetEnvironment(macCatalyst) + false + #else + SecretsConfig.hasApiKey + #endif + } private static func postLiveActivity(appId: String, activityId: String, payload: [String: Any]) async -> Bool { guard let key = SecretsConfig.apiKey else { return false } diff --git a/examples/demo/App/Views/ContentView.swift b/examples/demo/App/Views/ContentView.swift index 9e6578488..fbaf70fd8 100644 --- a/examples/demo/App/Views/ContentView.swift +++ b/examples/demo/App/Views/ContentView.swift @@ -51,7 +51,9 @@ struct ContentView: View { TriggersSection() CustomEventsSection() LocationSection() + #if !targetEnvironment(macCatalyst) LiveActivitySection() + #endif ActionButton( "NEXT SCREEN", accessibilityID: "next_screen_button" diff --git a/examples/demo/OneSignalNotificationServiceExtension/Info.plist b/examples/demo/OneSignalNotificationServiceExtension/Info.plist index ca3cd8f43..19587da71 100644 --- a/examples/demo/OneSignalNotificationServiceExtension/Info.plist +++ b/examples/demo/OneSignalNotificationServiceExtension/Info.plist @@ -20,6 +20,8 @@ 1.0 CFBundleVersion 1 + OneSignal_app_groups_key + group.com.onesignal.example.onesignal NSExtension NSExtensionPointIdentifier diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSResilientStorage.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSResilientStorage.swift index c9dc9f260..2b16a64e4 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSResilientStorage.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSResilientStorage.swift @@ -62,25 +62,44 @@ public final class OSResilientStorage: NSObject { let fileManager = FileManager.default let groupName = OneSignalUserDefaults.appGroupName() - if let container = fileManager.containerURL(forSecurityApplicationGroupIdentifier: groupName) { + let groupContainer = fileManager.containerURL( + forSecurityApplicationGroupIdentifier: groupName + ) + return resolveFileURL( + groupContainer: groupContainer, + applicationSupportDirectory: { + try fileManager.url( + for: .applicationSupportDirectory, + in: .userDomainMask, + appropriateFor: nil, + create: true + ) + }, + prepare: { + try preparedFileURL(in: $0, fileManager: fileManager) + } + ) + } + + static func resolveFileURL( + groupContainer: URL?, + applicationSupportDirectory: () throws -> URL, + prepare: (URL) throws -> URL + ) -> URL? { + if let groupContainer { do { - return try preparedFileURL(in: container, fileManager: fileManager) + return try prepare(groupContainer) } catch { OneSignalLog.onesignalLog( - .LL_WARN, + .LL_ERROR, message: "OSResilientStorage could not prepare the App Group container: \(error)" ) + return nil } } do { - let support = try fileManager.url( - for: .applicationSupportDirectory, - in: .userDomainMask, - appropriateFor: nil, - create: true - ) - return try preparedFileURL(in: support, fileManager: fileManager) + return try prepare(applicationSupportDirectory()) } catch { OneSignalLog.onesignalLog(.LL_ERROR, message: "OSResilientStorage could not resolve a container URL: \(error)") return nil diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSStubLiveActivities.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSStubLiveActivities.swift index 761875502..0639a8b04 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSStubLiveActivities.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSStubLiveActivities.swift @@ -16,23 +16,31 @@ public class OSStubLiveActivities: NSObject, OSLiveActivities { } public static func enter(_ activityId: String, withToken: String) { - OneSignalLog.onesignalLog(.LL_ERROR, message: "OneSignalLiveActivities not found. In order to use OneSignal's LiveActivities features the OneSignalLiveActivities module must be added.") + logUnavailable() } public static func enter(_ activityId: String, withToken: String, withSuccess: OSResultSuccessBlock?, withFailure: OSFailureBlock?) { - OneSignalLog.onesignalLog(.LL_ERROR, message: "OneSignalLiveActivities not found. In order to use OneSignal's LiveActivities features the OneSignalLiveActivities module must be added.") + logUnavailable() } public static func exit(_ activityId: String) { - OneSignalLog.onesignalLog(.LL_ERROR, message: "OneSignalLiveActivities not found. In order to use OneSignal's LiveActivities features the OneSignalLiveActivities module must be added.") + logUnavailable() } public static func exit(_ activityId: String, withSuccess: OSResultSuccessBlock?, withFailure: OSFailureBlock?) { - OneSignalLog.onesignalLog(.LL_ERROR, message: "OneSignalLiveActivities not found. In order to use OneSignal's LiveActivities features the OneSignalLiveActivities module must be added.") + logUnavailable() } public static func trackClickAndReturnOriginal(_ url: URL) -> URL? { - OneSignalLog.onesignalLog(.LL_ERROR, message: "OneSignalLiveActivities not found. In order to use OneSignal's LiveActivities features the OneSignalLiveActivities module must be added.") + logUnavailable() return url } + + private static func logUnavailable() { + #if !targetEnvironment(macCatalyst) + let message = "OneSignalLiveActivities not found. In order to use OneSignal's " + + "LiveActivities features the OneSignalLiveActivities module must be added." + OneSignalLog.onesignalLog(.LL_ERROR, message: message) + #endif + } } diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCoreTests/OSResilientStorageTests.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCoreTests/OSResilientStorageTests.swift index cf7ebade7..6e4dcb69b 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalOSCoreTests/OSResilientStorageTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCoreTests/OSResilientStorageTests.swift @@ -31,6 +31,10 @@ import XCTest final class OSResilientStorageTests: XCTestCase { + private enum TestError: Error { + case preparationFailed + } + /// Test keys. We avoid the real key constants so collisions with anything written by /// other tests / fixtures during the same simulator session can't bleed into assertions. private let keyA = "test_key_a" @@ -76,6 +80,22 @@ final class OSResilientStorageTests: XCTestCase { XCTAssertTrue(FileManager.default.fileExists(atPath: fileURL.path)) } + func testResolveFileURL_doesNotFallBackWhenGroupPreparationFails() { + var applicationSupportWasRequested = false + + let fileURL = OSResilientStorage.resolveFileURL( + groupContainer: URL(fileURLWithPath: "/group"), + applicationSupportDirectory: { + applicationSupportWasRequested = true + return URL(fileURLWithPath: "/application-support") + }, + prepare: { _ in throw TestError.preparationFailed } + ) + + XCTAssertNil(fileURL) + XCTAssertFalse(applicationSupportWasRequested) + } + func testSetWithNil_removesKey() { OSResilientStorage.setString("alpha", forKey: keyA) XCTAssertEqual(OSResilientStorage.string(forKey: keyA), "alpha") From e7ba088dba8ff01da09729bf98db9de912a1f781 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Tue, 18 Aug 2026 17:43:50 -0700 Subject: [PATCH 5/7] style(toggle): add grey background to toggle to show for mac catalyst --- examples/demo/App/Views/Components/ToggleRow.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/demo/App/Views/Components/ToggleRow.swift b/examples/demo/App/Views/Components/ToggleRow.swift index 59d1ceeba..3454247c0 100644 --- a/examples/demo/App/Views/Components/ToggleRow.swift +++ b/examples/demo/App/Views/Components/ToggleRow.swift @@ -66,6 +66,7 @@ struct ToggleRow: View { Toggle("", isOn: isOn) .labelsHidden() .tint(OS.Color.primary) + .background(OS.Color.grey500.opacity(0.35), in: Capsule()) .disabled(isDisabled) .accessibilityIdentifier(accessibilityID) } From 2d7060bb5dab8fefd4d296efda89e924c21663e7 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Tue, 18 Aug 2026 19:11:45 -0700 Subject: [PATCH 6/7] fix(storage): no-op on Mac Catalyst --- .../OneSignalOSCore/Source/OSResilientStorage.swift | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSResilientStorage.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSResilientStorage.swift index 2b16a64e4..28dce3a38 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSResilientStorage.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSResilientStorage.swift @@ -33,6 +33,7 @@ import OneSignalCore /// /// Stored in the App Group container, so it's shared across any targets (main app, NSE, etc.) /// configured with the same App Group entitlement. Opaque identifiers only: no PII or credentials. +/// Accessors are no-ops on Mac Catalyst, where iOS data protection and prewarming do not apply. @objc(OSResilientStorage) public final class OSResilientStorage: NSObject { @@ -50,6 +51,14 @@ public final class OSResilientStorage: NSObject { // MARK: - Internal + private static var isSupported: Bool { + #if targetEnvironment(macCatalyst) + return false + #else + return true + #endif + } + private static let fileName = "onesignal_identity.json" /// Serial queue used to serialize all file reads/writes. @@ -158,6 +167,7 @@ public final class OSResilientStorage: NSObject { /// Returns the full current contents of the cache. Empty dict if absent. @objc public static func snapshot() -> [String: String] { + guard isSupported else { return [:] } return queue.sync { loadUnsafe() } } @@ -170,6 +180,7 @@ public final class OSResilientStorage: NSObject { /// Atomically updates a single value. Passing nil or an empty string removes the key. @objc public static func setString(_ value: String?, forKey key: String) { + guard isSupported else { return } queue.async { var current = loadUnsafe() if let value = value, !value.isEmpty { @@ -184,7 +195,7 @@ public final class OSResilientStorage: NSObject { /// Atomically updates multiple values, preserving keys not in `values`. /// An empty-string value removes the corresponding key. @objc public static func setStrings(_ values: [String: String]) { - guard !values.isEmpty else { return } + guard isSupported, !values.isEmpty else { return } queue.async { var current = loadUnsafe() for (key, value) in values { From 2eb5bf02d342ce0dbb2cbb296833b29f174aadbc Mon Sep 17 00:00:00 2001 From: Fadi George Date: Wed, 19 Aug 2026 09:59:28 -0700 Subject: [PATCH 7/7] fix(catalyst): simplify unsupported platform handling Keep Catalyst behavior explicit while avoiding unnecessary resilient-storage and Live Activities branches. Co-authored-by: Cursor --- examples/demo/App/App.swift | 5 +- .../App/Services/LiveActivityController.swift | 26 ++------- .../App/ViewModels/OneSignalViewModel.swift | 2 + .../Views/Sections/LiveActivitySection.swift | 4 ++ examples/demo/README.md | 2 +- .../Source/OSResilientStorage.swift | 55 +++---------------- .../Source/OSStubLiveActivities.swift | 2 - .../OSResilientStorageTests.swift | 32 ----------- 8 files changed, 23 insertions(+), 105 deletions(-) diff --git a/examples/demo/App/App.swift b/examples/demo/App/App.swift index 8111e8756..db1d37689 100644 --- a/examples/demo/App/App.swift +++ b/examples/demo/App/App.swift @@ -27,9 +27,6 @@ import SwiftUI import OneSignalFramework -#if !targetEnvironment(macCatalyst) -import OneSignalLiveActivities -#endif @main struct App: SwiftUI.App { @@ -64,9 +61,11 @@ class AppDelegate: NSObject, UIApplicationDelegate { setupInAppMessageListeners() // Set up Live Activities (iOS 16.1+) + #if !targetEnvironment(macCatalyst) if #available(iOS 16.1, *) { LiveActivityController.setup() } + #endif return true } diff --git a/examples/demo/App/Services/LiveActivityController.swift b/examples/demo/App/Services/LiveActivityController.swift index 5d0ba8797..5d9f3c045 100644 --- a/examples/demo/App/Services/LiveActivityController.swift +++ b/examples/demo/App/Services/LiveActivityController.swift @@ -27,9 +27,10 @@ import Foundation import OneSignalFramework -#if !targetEnvironment(macCatalyst) +#if targetEnvironment(macCatalyst) +// Live Activities are unavailable on Mac Catalyst. +#else import OneSignalLiveActivities -#endif /// Order tracking phases used by the Live Activity demo enum LiveActivityStatus: String, CaseIterable, Identifiable { @@ -78,9 +79,7 @@ enum LiveActivityController { @available(iOS 16.1, *) static func setup() { - #if !targetEnvironment(macCatalyst) OneSignal.LiveActivities.setupDefault() - #endif } @available(iOS 16.1, *) @@ -89,7 +88,6 @@ enum LiveActivityController { orderNumber: String, status: LiveActivityStatus ) { - #if !targetEnvironment(macCatalyst) let attributes: [String: Any] = [ "orderNumber": orderNumber ] @@ -103,13 +101,9 @@ enum LiveActivityController { attributes: attributes, content: content ) - #endif } static func update(appId: String, activityId: String, status: LiveActivityStatus) async -> Bool { - #if targetEnvironment(macCatalyst) - return false - #else let payload: [String: Any] = [ "event": "update", "name": "Live Activity Update", @@ -123,13 +117,9 @@ enum LiveActivityController { ] ] return await postLiveActivity(appId: appId, activityId: activityId, payload: payload) - #endif } static func end(appId: String, activityId: String) async -> Bool { - #if targetEnvironment(macCatalyst) - return false - #else let payload: [String: Any] = [ "event": "end", "name": "End Live Activity", @@ -140,16 +130,9 @@ enum LiveActivityController { ] ] return await postLiveActivity(appId: appId, activityId: activityId, payload: payload) - #endif } - static var hasApiKey: Bool { - #if targetEnvironment(macCatalyst) - false - #else - SecretsConfig.hasApiKey - #endif - } + static var hasApiKey: Bool { SecretsConfig.hasApiKey } private static func postLiveActivity(appId: String, activityId: String, payload: [String: Any]) async -> Bool { guard let key = SecretsConfig.apiKey else { return false } @@ -171,3 +154,4 @@ enum LiveActivityController { } } } +#endif diff --git a/examples/demo/App/ViewModels/OneSignalViewModel.swift b/examples/demo/App/ViewModels/OneSignalViewModel.swift index dfa23553a..e158be52d 100644 --- a/examples/demo/App/ViewModels/OneSignalViewModel.swift +++ b/examples/demo/App/ViewModels/OneSignalViewModel.swift @@ -389,6 +389,7 @@ final class OneSignalViewModel: ObservableObject { // MARK: - Live Activities + #if !targetEnvironment(macCatalyst) func startLiveActivity(activityId: String, orderNumber: String, status: LiveActivityStatus) { let trimmedId = activityId.trimmingCharacters(in: .whitespacesAndNewlines) guard !trimmedId.isEmpty else { return } @@ -423,6 +424,7 @@ final class OneSignalViewModel: ObservableObject { ) } } + #endif // MARK: - Tooltips diff --git a/examples/demo/App/Views/Sections/LiveActivitySection.swift b/examples/demo/App/Views/Sections/LiveActivitySection.swift index 136c6564d..9086da6a2 100644 --- a/examples/demo/App/Views/Sections/LiveActivitySection.swift +++ b/examples/demo/App/Views/Sections/LiveActivitySection.swift @@ -27,6 +27,9 @@ import SwiftUI +#if targetEnvironment(macCatalyst) +// Live Activities are unavailable on Mac Catalyst. +#else /// Live Activities (iOS 16.1+) section with activity ID + order # inputs and status cycler. /// Mirrors the Capacitor demo's LiveActivitySection. struct LiveActivitySection: View { @@ -142,3 +145,4 @@ struct LiveActivitySection: View { } } } +#endif diff --git a/examples/demo/README.md b/examples/demo/README.md index 42e5d5513..292202b5a 100644 --- a/examples/demo/README.md +++ b/examples/demo/README.md @@ -92,7 +92,7 @@ xcodegen generate # rewrites App.xcodeproj ### 3. Capabilities & App Group -The shipped `App.entitlements` and `OneSignalNotificationServiceExtension/OneSignalNotificationServiceExtension.entitlements` use `group.com.onesignal.example.onesignal`. If you need a different group (for example to install on a real device under your own team), change the value in both files to the same string. The other capabilities (Push Notifications, Remote notifications background mode, `NSSupportsLiveActivities`) are already declared in the entitlements / `App/Info.plist`. +The shipped `App.entitlements` and `OneSignalNotificationServiceExtension/OneSignalNotificationServiceExtension.entitlements` use `group.com.onesignal.example.onesignal`. Both targets also set `OneSignal_app_groups_key` to this value because Catalyst derives a `maccatalyst.*` bundle identifier that would otherwise produce a different default group name. If you need a different group (for example to install on a real device under your own team), change the value in both entitlements and both Info.plists to the same string. The other capabilities (Push Notifications, Remote notifications background mode, `NSSupportsLiveActivities`) are already declared in the entitlements / `App/Info.plist`. ### 4. Configure your OneSignal credentials diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSResilientStorage.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSResilientStorage.swift index 28dce3a38..312cd8d65 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSResilientStorage.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSResilientStorage.swift @@ -71,61 +71,24 @@ public final class OSResilientStorage: NSObject { let fileManager = FileManager.default let groupName = OneSignalUserDefaults.appGroupName() - let groupContainer = fileManager.containerURL( - forSecurityApplicationGroupIdentifier: groupName - ) - return resolveFileURL( - groupContainer: groupContainer, - applicationSupportDirectory: { - try fileManager.url( - for: .applicationSupportDirectory, - in: .userDomainMask, - appropriateFor: nil, - create: true - ) - }, - prepare: { - try preparedFileURL(in: $0, fileManager: fileManager) - } - ) - } - - static func resolveFileURL( - groupContainer: URL?, - applicationSupportDirectory: () throws -> URL, - prepare: (URL) throws -> URL - ) -> URL? { - if let groupContainer { - do { - return try prepare(groupContainer) - } catch { - OneSignalLog.onesignalLog( - .LL_ERROR, - message: "OSResilientStorage could not prepare the App Group container: \(error)" - ) - return nil - } + if let container = fileManager.containerURL(forSecurityApplicationGroupIdentifier: groupName) { + return container.appendingPathComponent(fileName) } do { - return try prepare(applicationSupportDirectory()) + let support = try fileManager.url( + for: .applicationSupportDirectory, + in: .userDomainMask, + appropriateFor: nil, + create: true + ) + return support.appendingPathComponent(fileName) } catch { OneSignalLog.onesignalLog(.LL_ERROR, message: "OSResilientStorage could not resolve a container URL: \(error)") return nil } } - static func preparedFileURL( - in directory: URL, - fileManager: FileManager = .default - ) throws -> URL { - try fileManager.createDirectory( - at: directory, - withIntermediateDirectories: true - ) - return directory.appendingPathComponent(fileName) - } - /// Reads the cache file. Caller is responsible for queue-serialization. /// Returns an empty dict if the file is missing or unreadable. private static func loadUnsafe() -> [String: String] { diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSStubLiveActivities.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSStubLiveActivities.swift index 0639a8b04..6360b564f 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSStubLiveActivities.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSStubLiveActivities.swift @@ -37,10 +37,8 @@ public class OSStubLiveActivities: NSObject, OSLiveActivities { } private static func logUnavailable() { - #if !targetEnvironment(macCatalyst) let message = "OneSignalLiveActivities not found. In order to use OneSignal's " + "LiveActivities features the OneSignalLiveActivities module must be added." OneSignalLog.onesignalLog(.LL_ERROR, message: message) - #endif } } diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCoreTests/OSResilientStorageTests.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCoreTests/OSResilientStorageTests.swift index 6e4dcb69b..af6f527d6 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalOSCoreTests/OSResilientStorageTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCoreTests/OSResilientStorageTests.swift @@ -31,10 +31,6 @@ import XCTest final class OSResilientStorageTests: XCTestCase { - private enum TestError: Error { - case preparationFailed - } - /// Test keys. We avoid the real key constants so collisions with anything written by /// other tests / fixtures during the same simulator session can't bleed into assertions. private let keyA = "test_key_a" @@ -68,34 +64,6 @@ final class OSResilientStorageTests: XCTestCase { XCTAssertNil(OSResilientStorage.string(forKey: "never_written_\(UUID().uuidString)")) } - func testPreparedFileURL_createsMissingContainerForAtomicWrites() throws { - let root = FileManager.default.temporaryDirectory - .appendingPathComponent(UUID().uuidString, isDirectory: true) - let container = root.appendingPathComponent("group.example", isDirectory: true) - defer { try? FileManager.default.removeItem(at: root) } - - let fileURL = try OSResilientStorage.preparedFileURL(in: container) - try Data("value".utf8).write(to: fileURL, options: .atomic) - - XCTAssertTrue(FileManager.default.fileExists(atPath: fileURL.path)) - } - - func testResolveFileURL_doesNotFallBackWhenGroupPreparationFails() { - var applicationSupportWasRequested = false - - let fileURL = OSResilientStorage.resolveFileURL( - groupContainer: URL(fileURLWithPath: "/group"), - applicationSupportDirectory: { - applicationSupportWasRequested = true - return URL(fileURLWithPath: "/application-support") - }, - prepare: { _ in throw TestError.preparationFailed } - ) - - XCTAssertNil(fileURL) - XCTAssertFalse(applicationSupportWasRequested) - } - func testSetWithNil_removesKey() { OSResilientStorage.setString("alpha", forKey: keyA) XCTAssertEqual(OSResilientStorage.string(forKey: keyA), "alpha")