diff --git a/.gitignore b/.gitignore
index b3367d14e..0e7dffa5d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -18,4 +18,7 @@ DerivedData
.idea/
iOS_SDK/Carthage/Build
/temp/
-.build/
\ No newline at end of file
+.build/
+
+examples/demo/App/Secrets.plist
+examples/demo/Local.xcconfig
\ No newline at end of file
diff --git a/GettingStarted.md b/GettingStarted.md
index ed68b0727..2fd265c7e 100644
--- a/GettingStarted.md
+++ b/GettingStarted.md
@@ -1,10 +1,11 @@
-# Getting Started with the Red App
+# Getting Started
-The **Red App** is a SwiftUI sample application that exercises every major feature of the OneSignal iOS SDK. Use it to validate SDK behavior, test integrations, and explore the API surface on a simulator or physical device.
+This repo ships two ways to exercise the OneSignal iOS SDK:
-
-
-
+| App | Location | Purpose |
+|-----|----------|---------|
+| **OneSignalDevApp** | `iOS_SDK/OneSignalDevApp/` | Internal dev/test app wired into `OneSignalSDK.xcworkspace`. Builds against **local SDK source**, so any changes you make to the SDK are picked up immediately. Use this when modifying the SDK. |
+| **examples/demo** | `examples/demo/` | Customer-facing SwiftUI demo that mirrors the OneSignal Capacitor / Cordova / RN demos (same section layout, accessibility identifiers, sdk-shared tooltip content). Builds against the published SwiftPM SDK. Use this as a reference integration. |
## Prerequisites
@@ -15,86 +16,42 @@ The **Red App** is a SwiftUI sample application that exercises every major featu
| Swift | 5.9+ |
| iOS target | 16.0+ |
-## Running the App
+## Running OneSignalDevApp (SDK contributors)
-### Option A — Open via the workspace (recommended)
+This is the recommended path when you're working on the SDK itself.
-
-
-
-
-1. Open `iOS_SDK/OneSignalSDK.xcworkspace` in Xcode.
-2. In the scheme selector (top-left toolbar), choose **OneSignalSwiftUIExample**.
-3. Pick a simulator (e.g. iPhone 17 Pro) or a connected device.
+1. Open `iOS_SDK/OneSignalSDK.xcworkspace` in Xcode (the workspace, not any individual `.xcodeproj`).
+2. Select the **OneSignalDevApp** scheme.
+3. Pick a simulator or a connected device.
4. Press **Cmd + R** to build and run.
-Your Xcode toolbar should look like this above — scheme set to **OneSignalSwiftUIExample**, a simulator or physical device chosen, and the app running.
-
-The workspace contains multiple schemes. Make sure **OneSignalSwiftUIExample** is selected.
-
-Once the app is running, SDK debug logs stream to the Xcode console — useful for verifying network calls, subscription state, and in-app message events:
-
-
-
-
-
-### Option B — Open from the terminal
-
-```bash
-open iOS_SDK/OneSignalSDK.xcworkspace
-```
-
-Then follow steps 2–4 from Option A.
-
-### Option C — Open only the example project
-
-> Use this if you only need the sample app and don't plan to modify the SDK source.
-
-```bash
-open iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample.xcodeproj
-```
+SDK debug logs stream to the Xcode console — useful for verifying network calls, subscription state, and in-app message events.
-Select the **OneSignalSwiftUIExample** scheme, pick a destination, and run.
+> Push notification delivery requires a **physical device** with a valid APNs configuration. The simulator supports permission prompts and token generation but won't receive remote pushes.
-> **Note:** Opening the `.xcworkspace` (Options A/B) is preferred because it links the sample app against the SDK source, so any local SDK changes are picked up automatically.
+## Running examples/demo (reference integration)
-## Using Your Own App ID
+The `examples/demo/` app demonstrates the recommended integration shape for app developers, including a Notification Service Extension target and a Live Activities Widget Extension target.
-The default App ID (`77e32082-ea27-...c72e141824ef`) is a shared test key. To use your own:
+See [`examples/demo/README.md`](examples/demo/README.md) for full setup steps. In short:
-**Changing the App ID requires uninstalling and reinstalling the app for it to take effect.**
+1. Create the Xcode project at `examples/demo/App.xcodeproj` (the source files and extension folders are checked in but `project.pbxproj` is not).
+2. Add the OneSignal SwiftPM dependency (`https://github.com/OneSignal/OneSignal-iOS-SDK`, 5.0.0+) and attach the right products to each of the three targets (App / NSE / Widget).
+3. Configure capabilities (Push Notifications, App Groups, Background Modes → Remote notifications) and run.
-1. Open `iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Services/OneSignalService.swift`.
-2. Replace the `defaultAppId` value with your OneSignal App ID (available at [onesignal.com](https://onesignal.com)).
-3. Then uninstall the app from the device/simulator and run it again.
+## Using your own App ID
-## Features
+Both apps default to a shared OneSignal App ID. To switch to your own:
-The Red App is organized into scrollable sections, each mapping to a OneSignal SDK capability:
+- **OneSignalDevApp** — open `iOS_SDK/OneSignalDevApp/OneSignalDevApp/AppDelegate.m` and replace the App ID passed to `OneSignal.initialize`.
+- **examples/demo** — edit `examples/demo/App/Services/OneSignalService.swift` and replace `defaultAppId`, or override at runtime via `UserDefaults` (key `OneSignalAppId`).
-| Section | What It Does |
-|---------|-------------|
-| **Logs** | Collapsible live SDK log viewer with a configurable entry limit and clear button. |
-| **App Info** | Displays the current App ID and a consent-required toggle that gates SDK data processing. |
-| **User** | Shows login status (Anonymous / Identified) and External ID. Login and logout buttons to switch between user states. |
-| **Push** | Displays the Push Subscription ID, an enable/disable toggle, and permission status. |
-| **Send Push Notification** | Quick-fire buttons (Simple, Custom) to send test push notifications to the current device. |
-| **In-App Messaging** | Pause/resume in-app messages. |
-| **Send In-App Message** | Trigger a test in-app message. |
-| **Aliases** | Add and remove key-value aliases for the current user. |
-| **Email** | Add and remove email subscriptions. |
-| **SMS** | Add and remove SMS subscriptions. |
-| **Tags** | Manage user tags used for audience segmentation. |
-| **Outcome Events** | Fire unique, regular, or valued outcome events for analytics. |
-| **Triggers** | Set and remove in-app message triggers. |
-| **Track Event** | Send custom user events with optional properties. |
-| **Location** | Toggle location sharing and request location permissions. |
-| **Live Activities** | Start and manage iOS Live Activities via OneSignal. |
+Changing the App ID requires uninstalling and reinstalling the app for it to take effect.
## Troubleshooting
| Problem | Fix |
|---------|-----|
-| Build fails with missing framework | Make sure you opened the **`.xcworkspace`**, not the `.xcodeproj`. |
-| Push notifications don't arrive on simulator | Push delivery requires a **physical device** with a valid APNs configuration. The simulator supports permission prompts and token generation but won't receive remote pushes. |
-| "Consent Required" blocks SDK calls | Toggle **Consent Required** off, or call the consent API to grant consent. |
+| Build fails with missing framework | Open the workspace (`OneSignalSDK.xcworkspace`), not an individual `.xcodeproj`. |
+| Push notifications don't arrive on simulator | Push delivery requires a physical device with APNs configured. |
+| "Consent Required" blocks SDK calls | Toggle **Consent Required** off, or grant consent via the SDK's consent API. |
diff --git a/OneSignalSwiftUIExample/OneSignalSwiftUIExample.entitlements b/OneSignalSwiftUIExample/OneSignalSwiftUIExample.entitlements
deleted file mode 100644
index 903def2af..000000000
--- a/OneSignalSwiftUIExample/OneSignalSwiftUIExample.entitlements
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
- aps-environment
- development
-
-
diff --git a/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Models/AppModels.swift b/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Models/AppModels.swift
deleted file mode 100644
index 85a9bfc37..000000000
--- a/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Models/AppModels.swift
+++ /dev/null
@@ -1,145 +0,0 @@
-/**
- * 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
-
-// MARK: - Key-Value Item
-
-/// A generic key-value pair used for aliases, tags, and triggers
-struct KeyValueItem: Identifiable, Equatable {
- let id = UUID()
- let key: String
- let value: String
-}
-
-// MARK: - Notification Type
-
-/// Types of test push notifications that can be sent
-enum NotificationType: String, CaseIterable, Identifiable {
- case general = "General"
- case greetings = "Greetings"
- case promotions = "Promotions"
- case breakingNews = "Breaking News"
- case abandonedCart = "Abandoned Cart"
- case newPost = "New Post"
- case reEngagement = "Re-Engagement"
- case rating = "Rating"
-
- var id: String { rawValue }
-
- var iconName: String {
- switch self {
- case .general: return "bell.fill"
- case .greetings: return "hand.wave.fill"
- case .promotions: return "tag.fill"
- case .breakingNews: return "newspaper.fill"
- case .abandonedCart: return "cart.fill"
- case .newPost: return "photo.fill"
- case .reEngagement: return "hand.tap.fill"
- case .rating: return "star.fill"
- }
- }
-}
-
-// MARK: - In-App Message Type
-
-/// Types of in-app messages that can be displayed
-enum InAppMessageType: String, CaseIterable, Identifiable {
- case topBanner = "Top Banner"
- case bottomBanner = "Bottom Banner"
- case centerModal = "Center Modal"
- case fullScreen = "Full Screen"
-
- var id: String { rawValue }
-
- var iconName: String {
- switch self {
- case .topBanner: return "rectangle.topthird.inset.filled"
- case .bottomBanner: return "rectangle.bottomthird.inset.filled"
- case .centerModal: return "rectangle.center.inset.filled"
- case .fullScreen: return "rectangle.inset.filled"
- }
- }
-}
-
-// MARK: - Add Item Type
-
-/// Types of items that can be added via the add sheet
-enum AddItemType {
- case alias
- case email
- case sms
- case tag
- case trigger
- case externalUserId
-
- var title: String {
- switch self {
- case .alias: return "Add Alias"
- case .email: return "Add Email"
- case .sms: return "Add SMS"
- case .tag: return "Add Tag"
- case .trigger: return "Add Trigger"
- case .externalUserId: return "Login User"
- }
- }
-
- var requiresKeyValue: Bool {
- switch self {
- case .alias, .tag, .trigger: return true
- case .email, .sms, .externalUserId: return false
- }
- }
-
- var keyPlaceholder: String {
- switch self {
- case .alias: return "Alias Label"
- case .tag: return "Tag Key"
- case .trigger: return "Trigger Key"
- default: return "Key"
- }
- }
-
- var valuePlaceholder: String {
- switch self {
- case .alias: return "Alias ID"
- case .email: return "email@example.com"
- case .sms: return "+1234567890"
- case .tag: return "Tag Value"
- case .trigger: return "Trigger Value"
- case .externalUserId: return "External User ID"
- }
- }
-
- var keyboardType: UIKeyboardType {
- switch self {
- case .email: return .emailAddress
- case .sms: return .phonePad
- default: return .default
- }
- }
-}
diff --git a/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Services/OneSignalService.swift b/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Services/OneSignalService.swift
deleted file mode 100644
index 5684a2a34..000000000
--- a/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Services/OneSignalService.swift
+++ /dev/null
@@ -1,236 +0,0 @@
-/**
- * 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 OneSignalFramework
-import OneSignalInAppMessages
-import OneSignalLocation
-
-/// Service layer that wraps OneSignal SDK calls
-final class OneSignalService {
-
- // MARK: - Singleton
-
- static let shared = OneSignalService()
-
- private init() {}
-
- // MARK: - App ID
-
- private let appIdKey = "OneSignalAppId"
- private let defaultAppId = "77e32082-ea27-42e3-a898-c72e141824ef"
-
- var appId: String {
- get {
- UserDefaults.standard.string(forKey: appIdKey) ?? defaultAppId
- }
- set {
- UserDefaults.standard.set(newValue, forKey: appIdKey)
- }
- }
-
- // MARK: - Initialization
-
- func initialize(launchOptions: [UIApplication.LaunchOptionsKey: Any]?) {
- OneSignal.Debug.setLogLevel(.LL_VERBOSE)
- OneSignal.initialize(appId, withLaunchOptions: launchOptions)
- }
-
- // MARK: - Consent
-
- var consentRequired: Bool {
- get { OneSignal.privacyConsentRequired }
- set { OneSignal.setConsentRequired(newValue) }
- }
-
- var consentGiven: Bool {
- get { OneSignal.privacyConsentGiven }
- set { OneSignal.setConsentGiven(newValue) }
- }
-
- func revokeConsent() {
- OneSignal.setConsentGiven(false)
- }
-
- // MARK: - User Management
-
- func login(externalId: String) {
- OneSignal.login(externalId)
- }
-
- func logout() {
- OneSignal.logout()
- }
-
- // MARK: - Aliases
-
- func addAlias(label: String, id: String) {
- OneSignal.User.addAlias(label, id: id)
- }
-
- func removeAlias(_ label: String) {
- OneSignal.User.removeAlias(label)
- }
-
- // MARK: - Push Subscription
-
- var pushSubscriptionId: String? {
- OneSignal.User.pushSubscription.id
- }
-
- var isPushEnabled: Bool {
- OneSignal.User.pushSubscription.optedIn
- }
-
- func optInPush() {
- OneSignal.User.pushSubscription.optIn()
- }
-
- func optOutPush() {
- OneSignal.User.pushSubscription.optOut()
- }
-
- func requestPushPermission(completion: @escaping (Bool) -> Void) {
- OneSignal.Notifications.requestPermission({ accepted in
- completion(accepted)
- }, fallbackToSettings: true)
- }
-
- // MARK: - Email
-
- func addEmail(_ email: String) {
- OneSignal.User.addEmail(email)
- }
-
- func removeEmail(_ email: String) {
- OneSignal.User.removeEmail(email)
- }
-
- // MARK: - SMS
-
- func addSms(_ number: String) {
- OneSignal.User.addSms(number)
- }
-
- func removeSms(_ number: String) {
- OneSignal.User.removeSms(number)
- }
-
- // MARK: - Tags
-
- func addTag(key: String, value: String) {
- OneSignal.User.addTag(key: key, value: value)
- }
-
- func removeTag(_ key: String) {
- OneSignal.User.removeTag(key)
- }
-
- func getTags() -> [String: String] {
- OneSignal.User.getTags()
- }
-
- // MARK: - Outcomes
-
- func sendOutcome(_ name: String) {
- OneSignal.Session.addOutcome(name)
- }
-
- func sendOutcome(_ name: String, value: NSNumber) {
- OneSignal.Session.addOutcome(name, value: value)
- }
-
- func sendUniqueOutcome(_ name: String) {
- OneSignal.Session.addUniqueOutcome(name)
- }
-
- // MARK: - In-App Messages
-
- var isInAppMessagesPaused: Bool {
- get { OneSignal.InAppMessages.paused }
- set { OneSignal.InAppMessages.paused = newValue }
- }
-
- func addTrigger(key: String, value: String) {
- OneSignal.InAppMessages.addTrigger(key, withValue: value)
- }
-
- func removeTrigger(_ key: String) {
- OneSignal.InAppMessages.removeTrigger(key)
- }
-
- // MARK: - Location
-
- var isLocationShared: Bool {
- get { OneSignal.Location.isShared }
- set { OneSignal.Location.isShared = newValue }
- }
-
- func requestLocationPermission() {
- OneSignal.Location.requestPermission()
- }
-
- // MARK: - Notifications
-
- func clearAllNotifications() {
- OneSignal.Notifications.clearAll()
- }
-
- var hasNotificationPermission: Bool {
- OneSignal.Notifications.permission
- }
-
- // MARK: - Observers
-
- func addPushSubscriptionObserver(_ observer: OSPushSubscriptionObserver) {
- OneSignal.User.pushSubscription.addObserver(observer)
- }
-
- func addUserObserver(_ observer: OSUserStateObserver) {
- OneSignal.User.addObserver(observer)
- }
-
- func addPermissionObserver(_ observer: OSNotificationPermissionObserver) {
- OneSignal.Notifications.addPermissionObserver(observer)
- }
-
- func addNotificationClickListener(_ listener: OSNotificationClickListener) {
- OneSignal.Notifications.addClickListener(listener)
- }
-
- func addNotificationLifecycleListener(_ listener: OSNotificationLifecycleListener) {
- OneSignal.Notifications.addForegroundLifecycleListener(listener)
- }
-
- func addInAppMessageClickListener(_ listener: OSInAppMessageClickListener) {
- OneSignal.InAppMessages.addClickListener(listener)
- }
-
- func addInAppMessageLifecycleListener(_ listener: OSInAppMessageLifecycleListener) {
- OneSignal.InAppMessages.addLifecycleListener(listener)
- }
-}
diff --git a/OneSignalSwiftUIExample/OneSignalSwiftUIExample/ViewModels/OneSignalViewModel.swift b/OneSignalSwiftUIExample/OneSignalSwiftUIExample/ViewModels/OneSignalViewModel.swift
deleted file mode 100644
index 7e8e8dae9..000000000
--- a/OneSignalSwiftUIExample/OneSignalSwiftUIExample/ViewModels/OneSignalViewModel.swift
+++ /dev/null
@@ -1,350 +0,0 @@
-/**
- * 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 Combine
-import OneSignalFramework
-import OneSignalInAppMessages
-import OneSignalLocation
-
-/// Main ViewModel managing all OneSignal SDK state and interactions
-@MainActor
-final class OneSignalViewModel: ObservableObject {
-
- // MARK: - Published Properties
-
- // App Info
- @Published var appId: String
-
- // User
- @Published var externalUserId: String?
- @Published var aliases: [KeyValueItem] = []
-
- // Push Subscription
- @Published var pushSubscriptionId: String?
- @Published var isPushEnabled: Bool = false
-
- // Email & SMS
- @Published var emails: [String] = []
- @Published var smsNumbers: [String] = []
-
- // Tags
- @Published var tags: [KeyValueItem] = []
-
- // In-App Messaging
- @Published var isInAppMessagesPaused: Bool = true
- @Published var triggers: [KeyValueItem] = []
-
- // Location
- @Published var isLocationShared: Bool = false
-
- // UI State
- @Published var showingAddSheet: Bool = false
- @Published var addItemType: AddItemType = .email
- @Published var toastMessage: String?
-
- // MARK: - Private Properties
-
- private let service: OneSignalService
- private var observers = Observers()
-
- // MARK: - Initialization
-
- init(service: OneSignalService = .shared) {
- self.service = service
- self.appId = service.appId
-
- // Initial state sync
- refreshState()
-
- // Set up observers
- setupObservers()
- }
-
- // MARK: - State Management
-
- func refreshState() {
- pushSubscriptionId = service.pushSubscriptionId
- isPushEnabled = service.isPushEnabled
- isInAppMessagesPaused = service.isInAppMessagesPaused
- isLocationShared = service.isLocationShared
-
- // Sync tags from SDK
- let sdkTags = service.getTags()
- tags = sdkTags.map { KeyValueItem(key: $0.key, value: $0.value) }
- }
-
- // MARK: - Consent
-
- func revokeConsent() {
- service.revokeConsent()
- showToast("Consent revoked")
- }
-
- // MARK: - User Management
-
- func login(externalId: String) {
- service.login(externalId: externalId)
- externalUserId = externalId
- showToast("Logged in as \(externalId)")
- }
-
- func logout() {
- service.logout()
- externalUserId = nil
- aliases.removeAll()
- emails.removeAll()
- smsNumbers.removeAll()
- tags.removeAll()
- triggers.removeAll()
- showToast("Logged out")
- }
-
- // MARK: - Aliases
-
- func addAlias(label: String, id: String) {
- service.addAlias(label: label, id: id)
- aliases.append(KeyValueItem(key: label, value: id))
- showToast("Alias added")
- }
-
- func removeAlias(_ item: KeyValueItem) {
- service.removeAlias(item.key)
- aliases.removeAll { $0.id == item.id }
- showToast("Alias removed")
- }
-
- // MARK: - Push Subscription
-
- func togglePushEnabled() {
- if isPushEnabled {
- service.optOutPush()
- isPushEnabled = false
- showToast("Push disabled")
- } else {
- service.optInPush()
- isPushEnabled = true
- showToast("Push enabled")
- }
- }
-
- func requestPushPermission() {
- service.requestPushPermission { [weak self] accepted in
- Task { @MainActor in
- self?.isPushEnabled = accepted
- self?.showToast(accepted ? "Push permission granted" : "Push permission denied")
- }
- }
- }
-
- // MARK: - Email
-
- func addEmail(_ email: String) {
- service.addEmail(email)
- emails.append(email)
- showToast("Email added")
- }
-
- func removeEmail(_ email: String) {
- service.removeEmail(email)
- emails.removeAll { $0 == email }
- showToast("Email removed")
- }
-
- // MARK: - SMS
-
- func addSms(_ number: String) {
- service.addSms(number)
- smsNumbers.append(number)
- showToast("SMS added")
- }
-
- func removeSms(_ number: String) {
- service.removeSms(number)
- smsNumbers.removeAll { $0 == number }
- showToast("SMS removed")
- }
-
- // MARK: - Tags
-
- func addTag(key: String, value: String) {
- service.addTag(key: key, value: value)
- // Remove existing tag with same key if present
- tags.removeAll { $0.key == key }
- tags.append(KeyValueItem(key: key, value: value))
- showToast("Tag added")
- }
-
- func removeTag(_ item: KeyValueItem) {
- service.removeTag(item.key)
- tags.removeAll { $0.id == item.id }
- showToast("Tag removed")
- }
-
- // MARK: - Outcomes
-
- func sendOutcome(_ name: String) {
- service.sendOutcome(name)
- showToast("Outcome '\(name)' sent")
- }
-
- func sendOutcome(_ name: String, value: Double) {
- service.sendOutcome(name, value: NSNumber(value: value))
- showToast("Outcome '\(name)' with value \(value) sent")
- }
-
- func sendUniqueOutcome(_ name: String) {
- service.sendUniqueOutcome(name)
- showToast("Unique outcome '\(name)' sent")
- }
-
- // MARK: - In-App Messaging
-
- func toggleInAppMessagesPaused() {
- isInAppMessagesPaused.toggle()
- service.isInAppMessagesPaused = isInAppMessagesPaused
- showToast(isInAppMessagesPaused ? "In-app messages paused" : "In-app messages resumed")
- }
-
- func addTrigger(key: String, value: String) {
- service.addTrigger(key: key, value: value)
- // Remove existing trigger with same key if present
- triggers.removeAll { $0.key == key }
- triggers.append(KeyValueItem(key: key, value: value))
- showToast("Trigger added")
- }
-
- func removeTrigger(_ item: KeyValueItem) {
- service.removeTrigger(item.key)
- triggers.removeAll { $0.id == item.id }
- showToast("Trigger removed")
- }
-
- // MARK: - Location
-
- func toggleLocationShared() {
- isLocationShared.toggle()
- service.isLocationShared = isLocationShared
- showToast(isLocationShared ? "Location sharing enabled" : "Location sharing disabled")
- }
-
- func promptLocation() {
- service.requestLocationPermission()
- showToast("Location permission requested")
- }
-
- // MARK: - Notifications
-
- func clearAllNotifications() {
- service.clearAllNotifications()
- showToast("All notifications cleared")
- }
-
- func sendTestNotification(_ type: NotificationType) {
- // In a real app, this would trigger a notification via your backend
- // For demo purposes, we just show a toast
- showToast("Test '\(type.rawValue)' notification triggered")
- }
-
- func sendTestInAppMessage(_ type: InAppMessageType) {
- // In a real app, this would trigger an IAM via your backend
- // For demo purposes, we just show a toast
- showToast("Test '\(type.rawValue)' in-app message triggered")
- }
-
- // MARK: - Add Sheet
-
- func showAddSheet(for type: AddItemType) {
- addItemType = type
- showingAddSheet = true
- }
-
- func handleAddItem(key: String, value: String) {
- switch addItemType {
- case .alias:
- addAlias(label: key, id: value)
- case .email:
- addEmail(value)
- case .sms:
- addSms(value)
- case .tag:
- addTag(key: key, value: value)
- case .trigger:
- addTrigger(key: key, value: value)
- case .externalUserId:
- login(externalId: value)
- }
- showingAddSheet = false
- }
-
- // MARK: - Toast
-
- private func showToast(_ message: String) {
- toastMessage = message
-
- // Auto-dismiss after 2 seconds
- Task {
- try? await Task.sleep(nanoseconds: 2_000_000_000)
- toastMessage = nil
- }
- }
-
- // MARK: - Observers
-
- private func setupObservers() {
- observers.viewModel = self
- service.addPushSubscriptionObserver(observers)
- service.addUserObserver(observers)
- service.addPermissionObserver(observers)
- }
-}
-
-// MARK: - Observer Classes
-
-private class Observers: NSObject, OSPushSubscriptionObserver, OSUserStateObserver, OSNotificationPermissionObserver {
- weak var viewModel: OneSignalViewModel?
-
- func onPushSubscriptionDidChange(state: OSPushSubscriptionChangedState) {
- Task { @MainActor in
- viewModel?.pushSubscriptionId = state.current.id
- viewModel?.isPushEnabled = state.current.optedIn
- }
- }
-
- func onUserStateDidChange(state: OSUserChangedState) {
- Task { @MainActor in
- // User state changed - could refresh aliases, etc.
- print("User state changed: \(state.jsonRepresentation())")
- }
- }
-
- func onNotificationPermissionDidChange(_ permission: Bool) {
- Task { @MainActor in
- viewModel?.isPushEnabled = permission && (viewModel?.isPushEnabled ?? false)
- }
- }
-}
diff --git a/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Components/AddItemSheet.swift b/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Components/AddItemSheet.swift
deleted file mode 100644
index ed1c621d6..000000000
--- a/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Components/AddItemSheet.swift
+++ /dev/null
@@ -1,126 +0,0 @@
-/**
- * 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 SwiftUI
-
-/// A reusable sheet for adding items with one or two text fields
-struct AddItemSheet: View {
- let itemType: AddItemType
- let onAdd: (String, String) -> Void
- let onCancel: () -> Void
-
- @State private var keyText: String = ""
- @State private var valueText: String = ""
- @FocusState private var focusedField: Field?
-
- private enum Field {
- case key, value
- }
-
- var body: some View {
- NavigationStack {
- Form {
- if itemType.requiresKeyValue {
- Section {
- TextField(itemType.keyPlaceholder, text: $keyText)
- .focused($focusedField, equals: .key)
- .textInputAutocapitalization(.never)
- .autocorrectionDisabled()
-
- TextField(itemType.valuePlaceholder, text: $valueText)
- .focused($focusedField, equals: .value)
- .textInputAutocapitalization(.never)
- .autocorrectionDisabled()
- .keyboardType(itemType.keyboardType)
- }
- } else {
- Section {
- TextField(itemType.valuePlaceholder, text: $valueText)
- .focused($focusedField, equals: .value)
- .textInputAutocapitalization(.never)
- .autocorrectionDisabled()
- .keyboardType(itemType.keyboardType)
- }
- }
- }
- .navigationTitle(itemType.title)
- .navigationBarTitleDisplayMode(.inline)
- .toolbar {
- ToolbarItem(placement: .cancellationAction) {
- Button("Cancel") {
- onCancel()
- }
- }
-
- ToolbarItem(placement: .confirmationAction) {
- Button(itemType == .externalUserId ? "Login" : "Add") {
- onAdd(keyText, valueText)
- }
- .disabled(!isValid)
- }
- }
- .onAppear {
- focusedField = itemType.requiresKeyValue ? .key : .value
- }
- }
- .presentationDetents([.medium])
- .presentationDragIndicator(.visible)
- }
-
- private var isValid: Bool {
- if itemType.requiresKeyValue {
- return !keyText.trimmingCharacters(in: .whitespaces).isEmpty &&
- !valueText.trimmingCharacters(in: .whitespaces).isEmpty
- } else {
- return !valueText.trimmingCharacters(in: .whitespaces).isEmpty
- }
- }
-}
-
-#Preview("Add Alias") {
- AddItemSheet(
- itemType: .alias,
- onAdd: { key, value in print("Add: \(key) = \(value)") },
- onCancel: { print("Cancel") }
- )
-}
-
-#Preview("Add Email") {
- AddItemSheet(
- itemType: .email,
- onAdd: { _, value in print("Add: \(value)") },
- onCancel: { print("Cancel") }
- )
-}
-
-#Preview("Login User") {
- AddItemSheet(
- itemType: .externalUserId,
- onAdd: { _, value in print("Login: \(value)") },
- onCancel: { print("Cancel") }
- )
-}
diff --git a/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Components/KeyValueRow.swift b/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Components/KeyValueRow.swift
deleted file mode 100644
index af50dc8b2..000000000
--- a/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Components/KeyValueRow.swift
+++ /dev/null
@@ -1,158 +0,0 @@
-/**
- * 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 SwiftUI
-
-/// A row displaying a key-value pair with optional delete action
-struct KeyValueRow: View {
- let item: KeyValueItem
- let onDelete: (() -> Void)?
-
- init(item: KeyValueItem, onDelete: (() -> Void)? = nil) {
- self.item = item
- self.onDelete = onDelete
- }
-
- var body: some View {
- HStack {
- VStack(alignment: .leading, spacing: 2) {
- Text(item.key)
- .font(.subheadline)
- .foregroundColor(.secondary)
- Text(item.value)
- .font(.body)
- }
-
- Spacer()
-
- if let onDelete = onDelete {
- Button(action: onDelete) {
- Image(systemName: "trash")
- .foregroundColor(.red)
- }
- .buttonStyle(.borderless)
- }
- }
- .contentShape(Rectangle())
- }
-}
-
-/// A row displaying a single value with optional delete action
-struct SingleValueRow: View {
- let value: String
- let onDelete: (() -> Void)?
-
- init(value: String, onDelete: (() -> Void)? = nil) {
- self.value = value
- self.onDelete = onDelete
- }
-
- var body: some View {
- HStack {
- Text(value)
- .font(.body)
-
- Spacer()
-
- if let onDelete = onDelete {
- Button(action: onDelete) {
- Image(systemName: "trash")
- .foregroundColor(.red)
- }
- .buttonStyle(.borderless)
- }
- }
- .contentShape(Rectangle())
- }
-}
-
-/// A row displaying a label and value in a horizontal layout
-struct InfoRow: View {
- let label: String
- let value: String
- let isMonospaced: Bool
-
- init(label: String, value: String, isMonospaced: Bool = false) {
- self.label = label
- self.value = value
- self.isMonospaced = isMonospaced
- }
-
- var body: some View {
- HStack {
- Text(label)
- .foregroundColor(.secondary)
- Spacer()
- Text(value)
- .font(isMonospaced ? .system(.body, design: .monospaced) : .body)
- .foregroundColor(.primary)
- .lineLimit(1)
- .truncationMode(.middle)
- }
- }
-}
-
-/// A placeholder row for empty lists
-struct EmptyListRow: View {
- let message: String
-
- var body: some View {
- Text(message)
- .foregroundColor(.secondary)
- .font(.subheadline)
- .frame(maxWidth: .infinity, alignment: .center)
- .padding(.vertical, 8)
- }
-}
-
-#Preview {
- List {
- Section("Key-Value Items") {
- KeyValueRow(
- item: KeyValueItem(key: "external_id", value: "user_123"),
- onDelete: {}
- )
- KeyValueRow(
- item: KeyValueItem(key: "subscription_tier", value: "premium")
- )
- }
-
- Section("Single Values") {
- SingleValueRow(value: "user@example.com", onDelete: {})
- SingleValueRow(value: "+1234567890")
- }
-
- Section("Info Rows") {
- InfoRow(label: "App ID", value: "77e32082-ea27-42e3-a898-c72e141824ef", isMonospaced: true)
- InfoRow(label: "Status", value: "Active")
- }
-
- Section("Empty") {
- EmptyListRow(message: "No items added")
- }
- }
-}
diff --git a/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Components/NotificationGrid.swift b/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Components/NotificationGrid.swift
deleted file mode 100644
index 9a54202eb..000000000
--- a/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Components/NotificationGrid.swift
+++ /dev/null
@@ -1,137 +0,0 @@
-/**
- * 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 SwiftUI
-
-/// A grid of notification type buttons
-struct NotificationTypeGrid: View {
- let onSelect: (NotificationType) -> Void
-
- private let columns = [
- GridItem(.flexible(), spacing: 12),
- GridItem(.flexible(), spacing: 12)
- ]
-
- var body: some View {
- LazyVGrid(columns: columns, spacing: 12) {
- ForEach(NotificationType.allCases) { type in
- NotificationTypeButton(type: type) {
- onSelect(type)
- }
- }
- }
- .padding(.vertical, 8)
- }
-}
-
-/// A grid of in-app message type buttons
-struct InAppMessageTypeGrid: View {
- let onSelect: (InAppMessageType) -> Void
-
- private let columns = [
- GridItem(.flexible(), spacing: 12),
- GridItem(.flexible(), spacing: 12)
- ]
-
- var body: some View {
- LazyVGrid(columns: columns, spacing: 12) {
- ForEach(InAppMessageType.allCases) { type in
- InAppMessageTypeButton(type: type) {
- onSelect(type)
- }
- }
- }
- .padding(.vertical, 8)
- }
-}
-
-/// A button for a notification type with icon and label
-struct NotificationTypeButton: View {
- let type: NotificationType
- let action: () -> Void
-
- var body: some View {
- Button(action: action) {
- VStack(spacing: 8) {
- Image(systemName: type.iconName)
- .font(.title2)
- Text(type.rawValue)
- .font(.caption)
- .multilineTextAlignment(.center)
- }
- .frame(maxWidth: .infinity)
- .padding(.vertical, 16)
- .background(Color.accentColor)
- .foregroundColor(.white)
- .cornerRadius(12)
- }
- .buttonStyle(.plain)
- }
-}
-
-/// A button for an in-app message type with icon and label
-struct InAppMessageTypeButton: View {
- let type: InAppMessageType
- let action: () -> Void
-
- var body: some View {
- Button(action: action) {
- VStack(spacing: 8) {
- Image(systemName: type.iconName)
- .font(.title2)
- Text(type.rawValue)
- .font(.caption)
- .multilineTextAlignment(.center)
- }
- .frame(maxWidth: .infinity)
- .padding(.vertical, 16)
- .background(Color.accentColor)
- .foregroundColor(.white)
- .cornerRadius(12)
- }
- .buttonStyle(.plain)
- }
-}
-
-#Preview {
- ScrollView {
- VStack(alignment: .leading, spacing: 20) {
- Text("Send Push Notification")
- .font(.headline)
- NotificationTypeGrid(onSelect: { type in
- print("Selected: \(type.rawValue)")
- })
-
- Text("Send In-App Message")
- .font(.headline)
- InAppMessageTypeGrid(onSelect: { type in
- print("Selected: \(type.rawValue)")
- })
- }
- .padding()
- }
-}
diff --git a/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Sections/MessagingSection.swift b/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Sections/MessagingSection.swift
deleted file mode 100644
index ef21501a0..000000000
--- a/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Sections/MessagingSection.swift
+++ /dev/null
@@ -1,179 +0,0 @@
-/**
- * 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 SwiftUI
-
-/// Section for outcomes, in-app messaging, and triggers
-struct MessagingSection: View {
- @EnvironmentObject var viewModel: OneSignalViewModel
- @State private var showingOutcomeSheet = false
- @State private var outcomeName = ""
- @State private var outcomeValue = ""
-
- var body: some View {
- // Outcome Events Section
- Section {
- Button {
- showingOutcomeSheet = true
- } label: {
- HStack {
- Spacer()
- Text("Send Outcome")
- .fontWeight(.medium)
- Spacer()
- }
- }
- } header: {
- Text("Outcome Events")
- }
-
- // In-App Messaging Section
- Section {
- Toggle(isOn: Binding(
- get: { viewModel.isInAppMessagesPaused },
- set: { _ in viewModel.toggleInAppMessagesPaused() }
- )) {
- VStack(alignment: .leading, spacing: 2) {
- Text("Pause In-App Messages")
- Text("Toggle in-app messages")
- .font(.caption)
- .foregroundColor(.secondary)
- }
- }
- } header: {
- Text("In-App Messaging")
- }
-
- // Triggers Section
- Section {
- if viewModel.triggers.isEmpty {
- EmptyListRow(message: "No Triggers Added")
- } else {
- ForEach(viewModel.triggers) { trigger in
- KeyValueRow(item: trigger) {
- viewModel.removeTrigger(trigger)
- }
- }
- }
-
- Button {
- viewModel.showAddSheet(for: .trigger)
- } label: {
- HStack {
- Spacer()
- Label("Add Trigger", systemImage: "plus")
- .fontWeight(.medium)
- Spacer()
- }
- }
- } header: {
- Text("Triggers")
- }
- .sheet(isPresented: $showingOutcomeSheet) {
- OutcomeSheet(
- onSend: { name, value in
- if let value = value {
- viewModel.sendOutcome(name, value: value)
- } else {
- viewModel.sendOutcome(name)
- }
- showingOutcomeSheet = false
- },
- onCancel: {
- showingOutcomeSheet = false
- }
- )
- }
- }
-}
-
-/// Sheet for sending outcomes
-struct OutcomeSheet: View {
- let onSend: (String, Double?) -> Void
- let onCancel: () -> Void
-
- @State private var outcomeName = ""
- @State private var outcomeValue = ""
- @State private var includeValue = false
- @FocusState private var focusedField: Field?
-
- private enum Field {
- case name, value
- }
-
- var body: some View {
- NavigationStack {
- Form {
- Section {
- TextField("Outcome Name", text: $outcomeName)
- .focused($focusedField, equals: .name)
- .textInputAutocapitalization(.never)
- .autocorrectionDisabled()
- }
-
- Section {
- Toggle("Include Value", isOn: $includeValue)
-
- if includeValue {
- TextField("Value", text: $outcomeValue)
- .focused($focusedField, equals: .value)
- .keyboardType(.decimalPad)
- }
- }
- }
- .navigationTitle("Send Outcome")
- .navigationBarTitleDisplayMode(.inline)
- .toolbar {
- ToolbarItem(placement: .cancellationAction) {
- Button("Cancel") {
- onCancel()
- }
- }
-
- ToolbarItem(placement: .confirmationAction) {
- Button("Send") {
- let value = includeValue ? Double(outcomeValue) : nil
- onSend(outcomeName, value)
- }
- .disabled(outcomeName.trimmingCharacters(in: .whitespaces).isEmpty)
- }
- }
- .onAppear {
- focusedField = .name
- }
- }
- .presentationDetents([.medium])
- .presentationDragIndicator(.visible)
- }
-}
-
-#Preview {
- List {
- MessagingSection()
- }
- .environmentObject(OneSignalViewModel())
-}
diff --git a/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Sections/SubscriptionSection.swift b/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Sections/SubscriptionSection.swift
deleted file mode 100644
index 24ea67eca..000000000
--- a/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Sections/SubscriptionSection.swift
+++ /dev/null
@@ -1,116 +0,0 @@
-/**
- * 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 SwiftUI
-
-/// Section for push subscription, email, and SMS management
-struct SubscriptionSection: View {
- @EnvironmentObject var viewModel: OneSignalViewModel
-
- var body: some View {
- // Push Section
- Section {
- // Push ID
- VStack(alignment: .leading, spacing: 4) {
- Text("Push ID")
- .font(.caption)
- .foregroundColor(.secondary)
- Text(viewModel.pushSubscriptionId ?? "Not available")
- .font(.system(.footnote, design: .monospaced))
- .textSelection(.enabled)
- }
- .padding(.vertical, 4)
-
- // Enabled Toggle
- Toggle("Enabled", isOn: Binding(
- get: { viewModel.isPushEnabled },
- set: { _ in viewModel.togglePushEnabled() }
- ))
- } header: {
- Text("Push")
- }
-
- // Emails Section
- Section {
- if viewModel.emails.isEmpty {
- EmptyListRow(message: "No Emails Added")
- } else {
- ForEach(viewModel.emails, id: \.self) { email in
- SingleValueRow(value: email) {
- viewModel.removeEmail(email)
- }
- }
- }
-
- Button {
- viewModel.showAddSheet(for: .email)
- } label: {
- HStack {
- Spacer()
- Label("Add Email", systemImage: "plus")
- .fontWeight(.medium)
- Spacer()
- }
- }
- } header: {
- Text("Emails")
- }
-
- // SMS Section
- Section {
- if viewModel.smsNumbers.isEmpty {
- EmptyListRow(message: "No SMSs Added")
- } else {
- ForEach(viewModel.smsNumbers, id: \.self) { sms in
- SingleValueRow(value: sms) {
- viewModel.removeSms(sms)
- }
- }
- }
-
- Button {
- viewModel.showAddSheet(for: .sms)
- } label: {
- HStack {
- Spacer()
- Label("Add SMS", systemImage: "plus")
- .fontWeight(.medium)
- Spacer()
- }
- }
- } header: {
- Text("SMSs")
- }
- }
-}
-
-#Preview {
- List {
- SubscriptionSection()
- }
- .environmentObject(OneSignalViewModel())
-}
diff --git a/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Sections/UserSection.swift b/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Sections/UserSection.swift
deleted file mode 100644
index 77b8c130b..000000000
--- a/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Sections/UserSection.swift
+++ /dev/null
@@ -1,103 +0,0 @@
-/**
- * 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 SwiftUI
-
-/// Section for user login/logout and alias management
-struct UserSection: View {
- @EnvironmentObject var viewModel: OneSignalViewModel
-
- var body: some View {
- // Login/Logout Section
- Section {
- // Login Button
- Button {
- viewModel.showAddSheet(for: .externalUserId)
- } label: {
- HStack {
- Spacer()
- Text("Login User")
- .fontWeight(.medium)
- Spacer()
- }
- }
-
- // Logout Button
- Button(role: .destructive) {
- viewModel.logout()
- } label: {
- HStack {
- Spacer()
- Text("Logout User")
- .fontWeight(.medium)
- Spacer()
- }
- }
- .disabled(viewModel.externalUserId == nil)
-
- // Current User Info
- if let userId = viewModel.externalUserId {
- InfoRow(label: "External User ID", value: userId)
- }
- } header: {
- Text("User")
- }
-
- // Aliases Section
- Section {
- if viewModel.aliases.isEmpty {
- EmptyListRow(message: "No Aliases Added")
- } else {
- ForEach(viewModel.aliases) { alias in
- KeyValueRow(item: alias) {
- viewModel.removeAlias(alias)
- }
- }
- }
-
- Button {
- viewModel.showAddSheet(for: .alias)
- } label: {
- HStack {
- Spacer()
- Label("Add Alias", systemImage: "plus")
- .fontWeight(.medium)
- Spacer()
- }
- }
- } header: {
- Text("Aliases")
- }
- }
-}
-
-#Preview {
- List {
- UserSection()
- }
- .environmentObject(OneSignalViewModel())
-}
diff --git a/OneSignalSwiftUIExample/README.md b/OneSignalSwiftUIExample/README.md
deleted file mode 100644
index f20899df8..000000000
--- a/OneSignalSwiftUIExample/README.md
+++ /dev/null
@@ -1,136 +0,0 @@
-# OneSignal SwiftUI Example App
-
-A modern SwiftUI example app demonstrating the OneSignal iOS SDK features using MVVM architecture.
-
-## Features
-
-This example app demonstrates all major OneSignal SDK capabilities:
-
-- **User Management**: Login/logout with external user ID
-- **Aliases**: Add and remove user aliases
-- **Push Subscriptions**: Enable/disable push notifications, view push ID
-- **Email & SMS**: Add and remove email and SMS subscriptions
-- **Tags**: Manage user tags for segmentation
-- **Outcomes**: Track outcome events with optional values
-- **In-App Messaging**: Pause/resume IAM, manage triggers
-- **Location**: Toggle location sharing, request permissions
-- **Test Notifications**: Grid of notification types for testing
-
-## Architecture
-
-The app follows the **MVVM (Model-View-ViewModel)** pattern:
-
-```
-OneSignalSwiftUIExample/
-├── App/
-│ └── OneSignalSwiftUIExampleApp.swift # App entry point, SDK init
-├── Views/
-│ ├── ContentView.swift # Main view
-│ ├── Sections/ # Feature sections
-│ │ ├── AppInfoSection.swift
-│ │ ├── UserSection.swift
-│ │ ├── SubscriptionSection.swift
-│ │ ├── TagsSection.swift
-│ │ ├── MessagingSection.swift
-│ │ ├── LocationSection.swift
-│ │ └── NotificationSection.swift
-│ └── Components/ # Reusable UI components
-│ ├── KeyValueRow.swift
-│ ├── AddItemSheet.swift
-│ ├── NotificationGrid.swift
-│ └── ToastView.swift
-├── ViewModels/
-│ └── OneSignalViewModel.swift # Main ViewModel
-├── Models/
-│ └── AppModels.swift # Data models
-├── Services/
-│ └── OneSignalService.swift # SDK wrapper
-└── Assets.xcassets/ # App assets
-```
-
-## Setup Instructions
-
-### 1. Create Xcode Project
-
-1. Open Xcode and create a new project
-2. Select **iOS** → **App**
-3. Configure the project:
- - Product Name: `OneSignalSwiftUIExample`
- - Team: Your development team
- - Organization Identifier: `com.onesignal`
- - Interface: **SwiftUI**
- - Language: **Swift**
- - Storage: None
-4. Save the project in `iOS_SDK/OneSignalSwiftUIExample/`
-
-### 2. Add Source Files
-
-1. Delete the auto-generated `ContentView.swift` and `OneSignalSwiftUIExampleApp.swift`
-2. Drag all the folders from `OneSignalSwiftUIExample/` into your Xcode project:
- - `App/`
- - `Views/`
- - `ViewModels/`
- - `Models/`
- - `Services/`
- - `Assets.xcassets/`
-3. Make sure "Copy items if needed" is **unchecked** and "Create groups" is selected
-
-### 3. Add OneSignal SDK Dependencies
-
-#### Option A: Swift Package Manager (Recommended)
-
-1. In Xcode, go to **File** → **Add Package Dependencies...**
-2. Enter the OneSignal SDK repository URL: `https://github.com/OneSignal/OneSignal-iOS-SDK`
-3. Select version **5.0.0** or later
-4. Add the following packages to your main target:
- - `OneSignalFramework`
- - `OneSignalInAppMessages`
- - `OneSignalLocation`
-
-#### Option B: Local Development
-
-If you're developing the SDK locally:
-
-1. Drag the parent `OneSignal-iOS-SDK` folder into your project
-2. Or add local package dependency pointing to the repo root
-
-### 4. Configure Capabilities
-
-1. Select your project in the navigator
-2. Select your app target
-3. Go to **Signing & Capabilities**
-4. Add the following capabilities:
- - **Push Notifications**
- - **Background Modes** → Check "Remote notifications"
-
-### 5. Configure Info.plist
-
-The included `Info.plist` already has the required keys:
-- `NSLocationWhenInUseUsageDescription`
-- `NSLocationAlwaysAndWhenInUseUsageDescription`
-- `UIBackgroundModes` with `remote-notification`
-
-### 6. Update App ID (Optional)
-
-The default OneSignal App ID is configured in `OneSignalService.swift`. To use your own:
-
-1. Open `Services/OneSignalService.swift`
-2. Change the `defaultAppId` value to your OneSignal App ID
-
-## Running the App
-
-1. Select a simulator or device
-2. Build and run (⌘R)
-3. Grant notification permissions when prompted
-4. Explore the various OneSignal features
-
-## Requirements
-
-- iOS 15.0+
-- Xcode 15.0+
-- Swift 5.9+
-- OneSignal iOS SDK 5.0+
-
-## License
-
-Modified MIT License - See LICENSE file for details.
diff --git a/docs/assets/red-app-screenshot.png b/docs/assets/red-app-screenshot.png
deleted file mode 100644
index c0c61439e..000000000
Binary files a/docs/assets/red-app-screenshot.png and /dev/null differ
diff --git a/docs/assets/xcode-console-output.png b/docs/assets/xcode-console-output.png
deleted file mode 100644
index 2c154c292..000000000
Binary files a/docs/assets/xcode-console-output.png and /dev/null differ
diff --git a/docs/assets/xcode-scheme-selector.png b/docs/assets/xcode-scheme-selector.png
deleted file mode 100644
index e3ecb2608..000000000
Binary files a/docs/assets/xcode-scheme-selector.png and /dev/null differ
diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample.entitlements b/examples/demo/App.entitlements
similarity index 100%
rename from iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample.entitlements
rename to examples/demo/App.entitlements
diff --git a/examples/demo/App.xcodeproj/project.pbxproj b/examples/demo/App.xcodeproj/project.pbxproj
new file mode 100644
index 000000000..73adfdf6e
--- /dev/null
+++ b/examples/demo/App.xcodeproj/project.pbxproj
@@ -0,0 +1,1222 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 77;
+ objects = {
+
+/* Begin PBXBuildFile section */
+ 00B9E4C45782A6AD1CD3FE48 /* OneSignalExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B2F82BD207897EB78739182A /* OneSignalExtension.framework */; };
+ 08C5E83ABC14EC2FC88276B9 /* OneSignalLiveActivities.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F5EBC48D807036E572D97F0E /* OneSignalLiveActivities.framework */; };
+ 0BC1978B56970258E20C24C4 /* SendPushSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B81D7E1A7EB9BB4466C768F /* SendPushSection.swift */; };
+ 0C4F40193331204CC2E05743 /* OneSignalNotifications.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 283B132CEE12D26D1FA1AADF /* OneSignalNotifications.framework */; };
+ 0E6E9EEBF2500A4E70022960 /* UserFetchService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FD8258E807E6672642A32E6 /* UserFetchService.swift */; };
+ 0E7D0439A19C8BF291A8BEB1 /* EmailsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1892E9B40F0E8FB23DD64206 /* EmailsSection.swift */; };
+ 12597AC14E1783CC87D6E147 /* OneSignalService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E0F5CBE80CF861238E1A9AA /* OneSignalService.swift */; };
+ 14228D0C997F9168643F8154 /* OutcomesSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4078B5F096680AFA83D1CB85 /* OutcomesSection.swift */; };
+ 1653658B1F4EE21203BBAA5D /* TriggersSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1155DE423DAA8B5396948C9B /* TriggersSection.swift */; };
+ 16B0C854BC2498BEEDC4EEED /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3E7F504B0421F2B6247E2F5 /* Theme.swift */; };
+ 1A247F505A707756873AA9FA /* AddItemDialog.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8D84B9C2B0EFE086A8E48CD /* AddItemDialog.swift */; };
+ 1F417CAD2528616703CAA54D /* ToastPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F6CBCD47A3EA4209A6DDB03 /* ToastPresenter.swift */; };
+ 1FF9F70882A0CF6A73416DDF /* OneSignalWidgetLiveActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0864BD4A6F62539B2809338F /* OneSignalWidgetLiveActivity.swift */; };
+ 2015D767360C96D01A2FF3D9 /* OneSignalCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 2C1B9BE42E81492B9DB61343 /* OneSignalCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+ 20792A9930A201E187AA0ABF /* PreferencesService.swift in Sources */ = {isa = PBXBuildFile; fileRef = C76153AD2F6C6EB8F77138B9 /* PreferencesService.swift */; };
+ 232A52A5D5719F863641166C /* OneSignalExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B2F82BD207897EB78739182A /* OneSignalExtension.framework */; };
+ 25C963476DF0B01BAABE24D9 /* TooltipService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 939E1F476EE48B9833A3311C /* TooltipService.swift */; };
+ 27C72DF35BE082E3E1093F75 /* SendIamSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = C60D09DC8C809877FB4BF465 /* SendIamSection.swift */; };
+ 2859C7827BE28C7EE4AFDAB4 /* CustomNotificationDialog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D959B1636916DAEE5FE6278 /* CustomNotificationDialog.swift */; };
+ 28D491D31B5C07E4D4F48A7D /* SecretsConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4EBF32FC1DBF3C34188E08C /* SecretsConfig.swift */; };
+ 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, ); }; };
+ 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, ); }; };
+ 3FB3C8C0634A765EE81D042E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 76989E05CECAD7B8B3C424A7 /* Assets.xcassets */; };
+ 4414A3304BAEB384B8D16D09 /* OneSignalExtension.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = B2F82BD207897EB78739182A /* OneSignalExtension.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+ 4C18E3D284BB28BD846162F3 /* NotificationSender.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38138523A8A81A60A77800CA /* NotificationSender.swift */; };
+ 56DF63011CB625810F81075D /* OneSignalWidgetBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DD14C3CEEDFB9557E589B45 /* OneSignalWidgetBundle.swift */; };
+ 5737CABFA55E019B2732B90D /* OneSignalViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F1FA3F3D16A857DB6ED045F /* OneSignalViewModel.swift */; };
+ 5B959D44AB09CB821C00AFBF /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35F726E64F9B6817F917227C /* ContentView.swift */; };
+ 5F4B7EC8437D1A8D80DF7674 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = A20B46F63592FC67B655BEB8 /* NotificationService.swift */; };
+ 638B81D9DA5FD8636BB038B0 /* OneSignalUser.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EF49509A218369322ECFA3B9 /* OneSignalUser.framework */; };
+ 674995A7A55C13341317E19B /* OSDialog.swift in Sources */ = {isa = PBXBuildFile; fileRef = D261D46C404E325CBA87A9E0 /* OSDialog.swift */; };
+ 68BC99D15FDCB26EB35EBB07 /* OneSignalLocation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FE9834773C437CC373607693 /* OneSignalLocation.framework */; };
+ 6E3E040FD8A750248E70E46F /* AppModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7EA9D80191548D49F09D30B3 /* AppModels.swift */; };
+ 7B02F364CA25825E50B09CDB /* OneSignalOutcomes.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EC2530FFC78AECEAF6A7E1B7 /* OneSignalOutcomes.framework */; };
+ 7B94F48C31E0BEA4B8CB20E2 /* SectionCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 497484E7C094D645338BD404 /* SectionCard.swift */; };
+ 7C904CE2F4C2A2083324BBEB /* ToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F46DFACB9F304B9374F3C570 /* ToastView.swift */; };
+ 7D2BA9022E77B00205453467 /* LiveActivityController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0542854462194E28D9E4638D /* LiveActivityController.swift */; };
+ 7EBB68E75FAE49C09C048251 /* OneSignalUser.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = EF49509A218369322ECFA3B9 /* OneSignalUser.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+ 8068AFC608E7E82F06733BE7 /* OneSignalCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2C1B9BE42E81492B9DB61343 /* OneSignalCore.framework */; };
+ 80E3E2B5438CFEBE1316FA84 /* OneSignalCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2C1B9BE42E81492B9DB61343 /* OneSignalCore.framework */; };
+ 837FCE7A095ED1D7CCFEACF3 /* InAppSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = B76FFAF18177241F4A47FE23 /* InAppSection.swift */; };
+ 8E57965CF0E9F1C341E61996 /* OneSignal-Dynamic.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 25848A5B7E93DCA744373200 /* OneSignal-Dynamic.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+ 8EA2FF24D93691FDC1661913 /* CustomEventsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B6FF0BC430F5A2B89215967 /* CustomEventsSection.swift */; };
+ 902A116B26B8ECAD8EE29C95 /* ToggleRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 911376C90AA43F41478596FE /* ToggleRow.swift */; };
+ 93104E0838915DD6194805D5 /* Secrets.plist in Resources */ = {isa = PBXBuildFile; fileRef = 6B00BC406653BC6B08ECCE26 /* Secrets.plist */; };
+ 99C141A5ED972D66B5CD255A /* OneSignalWidget.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = AB92C9C455B15E0B571A1C66 /* OneSignalWidget.appex */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+ 9A7BE456B679D7DE7CA300BC /* ListWidgets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 280B23B41935EAB89C8C6FCB /* ListWidgets.swift */; };
+ 9B0D1DD32B99602629BBCB95 /* ActionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6094499773760C3190F62C9 /* ActionButton.swift */; };
+ 9FCE157075859B954814F6B7 /* OneSignalLiveActivities.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = F5EBC48D807036E572D97F0E /* OneSignalLiveActivities.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+ A2B1975BB925DCA45327D71E /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDB04A33912347325A0155D6 /* App.swift */; };
+ A2E180ADEA246FB6530E5C8C /* TooltipDialog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38067B9DDA25E12809D9A245 /* TooltipDialog.swift */; };
+ A7A5153D68A4967A88B2B433 /* AliasesSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 225DEBDFE699D266D5BDE7ED /* AliasesSection.swift */; };
+ A8498B9A2AA7DA8CB0856CAC /* TrackEventDialog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 432444EA41C495988DFAB422 /* TrackEventDialog.swift */; };
+ AB963AA6733C32EFB84DCBC0 /* vine_boom.wav in Resources */ = {isa = PBXBuildFile; fileRef = 1EE449E8308FCB038408D7CF /* vine_boom.wav */; };
+ AD514855D19B2C249269C7D2 /* OneSignalOutcomes.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EC2530FFC78AECEAF6A7E1B7 /* OneSignalOutcomes.framework */; };
+ AF8A33E8CFAF1A8DB1907130 /* OneSignalOSCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1636BCBAB444EAC7BB34CEDE /* OneSignalOSCore.framework */; };
+ B3AE701075398C6A369DBBE0 /* LocationSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C0EE23773C63B6EC3FB563A /* LocationSection.swift */; };
+ C31F603E294D69E30CE65EA3 /* OneSignalInAppMessages.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 919F0D129D0FA271E24F0BBD /* OneSignalInAppMessages.framework */; };
+ C6E9E2C6716059856A1AC571 /* OneSignalInAppMessages.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 919F0D129D0FA271E24F0BBD /* OneSignalInAppMessages.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+ CAB90AEC123A57B401881DBC /* OneSignalOSCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 1636BCBAB444EAC7BB34CEDE /* OneSignalOSCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+ CE2C54D552E42C2538FB95C4 /* UserSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E2383E8AA63BF04ADA2CF0D /* UserSection.swift */; };
+ D154E0266C8AB372F928D42A /* OneSignalLocation.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = FE9834773C437CC373607693 /* OneSignalLocation.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+ D65AC6EC014557264E7A3697 /* OneSignal-Dynamic.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 25848A5B7E93DCA744373200 /* OneSignal-Dynamic.framework */; };
+ DBDA727D6317A3CCC73A1699 /* LiveActivitySection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27856D76807C31B23CD10CFD /* LiveActivitySection.swift */; };
+ E3725231A3FD5F5A88BAA758 /* MultiPairInputDialog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4072202943CA64D2CBC38CB5 /* MultiPairInputDialog.swift */; };
+ E5671E3A84219D49E45F8DCF /* KeyValueRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5984B93007C6B85AFE09045A /* KeyValueRow.swift */; };
+ E72F68A7DB51347AB8B10FA7 /* OneSignalLiveActivities.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F5EBC48D807036E572D97F0E /* OneSignalLiveActivities.framework */; };
+ EA93E372AA3E66073487B89C /* PushSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = A42B5EDA3264E3D00A5CB265 /* PushSection.swift */; };
+ F1DBBD7F18CB70C5DF2FFC32 /* AppSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7EB548D4688766660864F581 /* AppSection.swift */; };
+ FB0472C73D007EABFD01CCA8 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C360043C7B20683C2E3FA3B6 /* Assets.xcassets */; };
+ FFD2A54AE34AEADBBAE5B7E3 /* OneSignalOutcomes.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = EC2530FFC78AECEAF6A7E1B7 /* OneSignalOutcomes.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+/* End PBXBuildFile section */
+
+/* Begin PBXContainerItemProxy section */
+ 005EC764DD2429AF6136BB9A /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 291D83300C20BA3831824AFD /* OneSignalSDK */;
+ proxyType = 1;
+ remoteGlobalIDString = DEBAAE272A4211D900BF2C1C;
+ remoteInfo = OneSignalInAppMessages;
+ };
+ 06313C80EB92E867CF4AE55C /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 291D83300C20BA3831824AFD /* OneSignalSDK */;
+ proxyType = 2;
+ remoteGlobalIDString = DE7D17F927026BA3002D3A5D;
+ remoteInfo = OneSignalExtension;
+ };
+ 13BDB99578D2E19A9A750DEF /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 291D83300C20BA3831824AFD /* OneSignalSDK */;
+ proxyType = 1;
+ remoteGlobalIDString = DE69E19A282ED8060090BB3D;
+ remoteInfo = OneSignalUser;
+ };
+ 21265D9F7D2B1FBDDD3E716F /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 291D83300C20BA3831824AFD /* OneSignalSDK */;
+ proxyType = 1;
+ remoteGlobalIDString = DE7D17F827026BA3002D3A5D;
+ remoteInfo = OneSignalExtension;
+ };
+ 25CC6D5CA1E9DA9142715E8A /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 291D83300C20BA3831824AFD /* OneSignalSDK */;
+ proxyType = 2;
+ remoteGlobalIDString = 3E2400381D4FFC31008BDE70;
+ remoteInfo = OneSignalFramework;
+ };
+ 5C38CF82FCDF8C89EA732187 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 291D83300C20BA3831824AFD /* OneSignalSDK */;
+ proxyType = 1;
+ remoteGlobalIDString = 3E2400371D4FFC31008BDE70;
+ remoteInfo = OneSignalFramework;
+ };
+ 5C4A42186FB7137C13DD3C0D /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 291D83300C20BA3831824AFD /* OneSignalSDK */;
+ proxyType = 1;
+ remoteGlobalIDString = DEBAADF82A420A3700BF2C1C;
+ remoteInfo = OneSignalLocation;
+ };
+ 642A3E9AC0D14239CE4A128E /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 6B590BF25178DC7D824D09CE /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = 97373B3889FBDD1762E98B03;
+ remoteInfo = OneSignalWidget;
+ };
+ 6701C77339EC73AE4FEC7E32 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 291D83300C20BA3831824AFD /* OneSignalSDK */;
+ proxyType = 1;
+ remoteGlobalIDString = DE7D187F27037F43002D3A5D;
+ remoteInfo = OneSignalOutcomes;
+ };
+ 67BDC3D5140D6CC33418AA1D /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 291D83300C20BA3831824AFD /* OneSignalSDK */;
+ proxyType = 1;
+ remoteGlobalIDString = DE7D187F27037F43002D3A5D;
+ remoteInfo = OneSignalOutcomes;
+ };
+ 6DE922175F2D1304874645CD /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 291D83300C20BA3831824AFD /* OneSignalSDK */;
+ proxyType = 1;
+ remoteGlobalIDString = 3C115160289A259500565C41;
+ remoteInfo = OneSignalOSCore;
+ };
+ 8475704534409C59DAE8B29A /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 291D83300C20BA3831824AFD /* OneSignalSDK */;
+ proxyType = 1;
+ remoteGlobalIDString = DE7D17F827026BA3002D3A5D;
+ remoteInfo = OneSignalExtension;
+ };
+ 852E98FC9C8818AE6B636551 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 291D83300C20BA3831824AFD /* OneSignalSDK */;
+ proxyType = 2;
+ remoteGlobalIDString = DEF784292912DEB600A1F3A5;
+ remoteInfo = OneSignalNotifications;
+ };
+ 8B6EE756CF7175F38D5B5B4B /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 291D83300C20BA3831824AFD /* OneSignalSDK */;
+ proxyType = 2;
+ remoteGlobalIDString = 3C115161289A259500565C41;
+ remoteInfo = OneSignalOSCore;
+ };
+ 8E39421BCBE5C21C1A9CB242 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 291D83300C20BA3831824AFD /* OneSignalSDK */;
+ proxyType = 2;
+ remoteGlobalIDString = 475F471E2B8E398D00EC05B3;
+ remoteInfo = OneSignalLiveActivities;
+ };
+ 996095BEB3FD8CE51B7F1AD7 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 291D83300C20BA3831824AFD /* OneSignalSDK */;
+ proxyType = 2;
+ remoteGlobalIDString = DEBAADF92A420A3700BF2C1C;
+ remoteInfo = OneSignalLocation;
+ };
+ 9CA6E55F92E787A8186EEBA1 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 291D83300C20BA3831824AFD /* OneSignalSDK */;
+ proxyType = 1;
+ remoteGlobalIDString = DE7D17E527026B95002D3A5D;
+ remoteInfo = OneSignalCore;
+ };
+ A668D6C94AC1F182AF01EAFF /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 6B590BF25178DC7D824D09CE /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = 61033D7807F09753830EDBC1;
+ remoteInfo = OneSignalNotificationServiceExtension;
+ };
+ B627D4859D5F7B2B7F452917 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 291D83300C20BA3831824AFD /* OneSignalSDK */;
+ proxyType = 1;
+ remoteGlobalIDString = DE7D17E527026B95002D3A5D;
+ remoteInfo = OneSignalCore;
+ };
+ C43833A058207841248D7506 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 291D83300C20BA3831824AFD /* OneSignalSDK */;
+ proxyType = 2;
+ remoteGlobalIDString = DEBAAE282A4211D900BF2C1C;
+ remoteInfo = OneSignalInAppMessages;
+ };
+ D1E066F0069BD9B244E196E4 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 291D83300C20BA3831824AFD /* OneSignalSDK */;
+ proxyType = 1;
+ remoteGlobalIDString = DEF784282912DEB600A1F3A5;
+ remoteInfo = OneSignalNotifications;
+ };
+ D378765DB7FB67E222BB7FF1 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 291D83300C20BA3831824AFD /* OneSignalSDK */;
+ proxyType = 1;
+ remoteGlobalIDString = 475F471D2B8E398D00EC05B3;
+ remoteInfo = OneSignalLiveActivities;
+ };
+ D399497AA4F221AD92C859AE /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 291D83300C20BA3831824AFD /* OneSignalSDK */;
+ proxyType = 2;
+ remoteGlobalIDString = DE7D17E627026B95002D3A5D;
+ remoteInfo = OneSignalCore;
+ };
+ D47A29D1FE2C196E0715923B /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 291D83300C20BA3831824AFD /* OneSignalSDK */;
+ proxyType = 2;
+ remoteGlobalIDString = DE69E19B282ED8060090BB3D;
+ remoteInfo = OneSignalUser;
+ };
+ E36EAB44E3DD77FE80F2A724 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 291D83300C20BA3831824AFD /* OneSignalSDK */;
+ proxyType = 2;
+ remoteGlobalIDString = DE7D188027037F43002D3A5D;
+ remoteInfo = OneSignalOutcomes;
+ };
+ E48045948F131DB2BD8C1B2A /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 291D83300C20BA3831824AFD /* OneSignalSDK */;
+ proxyType = 1;
+ remoteGlobalIDString = 475F471D2B8E398D00EC05B3;
+ remoteInfo = OneSignalLiveActivities;
+ };
+/* End PBXContainerItemProxy section */
+
+/* Begin PBXCopyFilesBuildPhase section */
+ 7C6B2DB89D01A1E9B8F2A565 /* Embed Foundation Extensions */ = {
+ isa = PBXCopyFilesBuildPhase;
+ buildActionMask = 2147483647;
+ dstPath = "";
+ dstSubfolderSpec = 13;
+ files = (
+ 3038C8C43A465DFED77AA533 /* OneSignalNotificationServiceExtension.appex in Embed Foundation Extensions */,
+ 99C141A5ED972D66B5CD255A /* OneSignalWidget.appex in Embed Foundation Extensions */,
+ );
+ name = "Embed Foundation Extensions";
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 8EFDE6013C818852098745E7 /* Embed Frameworks */ = {
+ isa = PBXCopyFilesBuildPhase;
+ buildActionMask = 2147483647;
+ dstPath = "";
+ dstSubfolderSpec = 10;
+ files = (
+ 2015D767360C96D01A2FF3D9 /* OneSignalCore.framework in Embed Frameworks */,
+ CAB90AEC123A57B401881DBC /* OneSignalOSCore.framework in Embed Frameworks */,
+ FFD2A54AE34AEADBBAE5B7E3 /* OneSignalOutcomes.framework in Embed Frameworks */,
+ 3C899E2494DE29756F5451BE /* OneSignalNotifications.framework in Embed Frameworks */,
+ 7EBB68E75FAE49C09C048251 /* OneSignalUser.framework in Embed Frameworks */,
+ 4414A3304BAEB384B8D16D09 /* OneSignalExtension.framework in Embed Frameworks */,
+ D154E0266C8AB372F928D42A /* OneSignalLocation.framework in Embed Frameworks */,
+ C6E9E2C6716059856A1AC571 /* OneSignalInAppMessages.framework in Embed Frameworks */,
+ 9FCE157075859B954814F6B7 /* OneSignalLiveActivities.framework in Embed Frameworks */,
+ 8E57965CF0E9F1C341E61996 /* OneSignal-Dynamic.framework in Embed Frameworks */,
+ );
+ name = "Embed Frameworks";
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXCopyFilesBuildPhase section */
+
+/* Begin PBXFileReference section */
+ 0542854462194E28D9E4638D /* LiveActivityController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivityController.swift; sourceTree = ""; };
+ 0864BD4A6F62539B2809338F /* OneSignalWidgetLiveActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OneSignalWidgetLiveActivity.swift; sourceTree = ""; };
+ 0F1FA3F3D16A857DB6ED045F /* OneSignalViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OneSignalViewModel.swift; sourceTree = ""; };
+ 1155DE423DAA8B5396948C9B /* TriggersSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TriggersSection.swift; sourceTree = ""; };
+ 1892E9B40F0E8FB23DD64206 /* EmailsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmailsSection.swift; sourceTree = ""; };
+ 1EE449E8308FCB038408D7CF /* vine_boom.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = vine_boom.wav; sourceTree = ""; };
+ 225DEBDFE699D266D5BDE7ED /* AliasesSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AliasesSection.swift; sourceTree = ""; };
+ 27856D76807C31B23CD10CFD /* LiveActivitySection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivitySection.swift; sourceTree = ""; };
+ 280B23B41935EAB89C8C6FCB /* ListWidgets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListWidgets.swift; sourceTree = ""; };
+ 291D83300C20BA3831824AFD /* OneSignalSDK */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = OneSignalSDK; path = ../../iOS_SDK/OneSignalSDK/OneSignal.xcodeproj; sourceTree = ""; };
+ 2D959B1636916DAEE5FE6278 /* CustomNotificationDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomNotificationDialog.swift; sourceTree = ""; };
+ 35F726E64F9B6817F917227C /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; };
+ 38067B9DDA25E12809D9A245 /* TooltipDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TooltipDialog.swift; sourceTree = ""; };
+ 38138523A8A81A60A77800CA /* NotificationSender.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSender.swift; sourceTree = ""; };
+ 3A654457BF0A55B54220E669 /* RemoveMultiDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoveMultiDialog.swift; sourceTree = ""; };
+ 3B81D7E1A7EB9BB4466C768F /* SendPushSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendPushSection.swift; sourceTree = ""; };
+ 3FD8258E807E6672642A32E6 /* UserFetchService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserFetchService.swift; sourceTree = ""; };
+ 4072202943CA64D2CBC38CB5 /* MultiPairInputDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiPairInputDialog.swift; sourceTree = ""; };
+ 4078B5F096680AFA83D1CB85 /* OutcomesSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutcomesSection.swift; sourceTree = ""; };
+ 432444EA41C495988DFAB422 /* TrackEventDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackEventDialog.swift; sourceTree = ""; };
+ 497484E7C094D645338BD404 /* SectionCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SectionCard.swift; sourceTree = ""; };
+ 49D88A349FD70A9153DD2C03 /* App.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = App.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ 4B6FF0BC430F5A2B89215967 /* CustomEventsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomEventsSection.swift; sourceTree = ""; };
+ 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 = ""; };
+ 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 = ""; };
+ 7EA9D80191548D49F09D30B3 /* AppModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppModels.swift; sourceTree = ""; };
+ 7EB548D4688766660864F581 /* AppSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSection.swift; sourceTree = ""; };
+ 8F6CBCD47A3EA4209A6DDB03 /* ToastPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastPresenter.swift; sourceTree = ""; };
+ 911376C90AA43F41478596FE /* ToggleRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToggleRow.swift; sourceTree = ""; };
+ 939E1F476EE48B9833A3311C /* TooltipService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TooltipService.swift; sourceTree = ""; };
+ 9E2383E8AA63BF04ADA2CF0D /* UserSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSection.swift; sourceTree = ""; };
+ A20B46F63592FC67B655BEB8 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = ""; };
+ A42B5EDA3264E3D00A5CB265 /* PushSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushSection.swift; sourceTree = ""; };
+ AB92C9C455B15E0B571A1C66 /* OneSignalWidget.appex */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = "wrapper.app-extension"; path = OneSignalWidget.appex; sourceTree = BUILT_PRODUCTS_DIR; };
+ B24E059F4DF6ABED55BA3183 /* OneSignalNotificationServiceExtension.appex */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = "wrapper.app-extension"; path = OneSignalNotificationServiceExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
+ B3E7F504B0421F2B6247E2F5 /* Theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Theme.swift; sourceTree = ""; };
+ B76FFAF18177241F4A47FE23 /* InAppSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppSection.swift; sourceTree = ""; };
+ BDB04A33912347325A0155D6 /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = ""; };
+ C360043C7B20683C2E3FA3B6 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
+ C60D09DC8C809877FB4BF465 /* SendIamSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendIamSection.swift; sourceTree = ""; };
+ C76153AD2F6C6EB8F77138B9 /* PreferencesService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesService.swift; sourceTree = ""; };
+ D261D46C404E325CBA87A9E0 /* OSDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSDialog.swift; sourceTree = ""; };
+ D4EBF32FC1DBF3C34188E08C /* SecretsConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretsConfig.swift; sourceTree = ""; };
+ D54B9DAAEDBE67B73893C522 /* OutcomeDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutcomeDialog.swift; sourceTree = ""; };
+ D6094499773760C3190F62C9 /* ActionButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionButton.swift; sourceTree = ""; };
+ D8D84B9C2B0EFE086A8E48CD /* AddItemDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddItemDialog.swift; sourceTree = ""; };
+ 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_196A01E1-FF4D-4A91-B9D6-E6100CF24C88" /* Build.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Build.xcconfig; sourceTree = ""; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ 83824651FEF6A3650CC5A580 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 80E3E2B5438CFEBE1316FA84 /* OneSignalCore.framework in Frameworks */,
+ AD514855D19B2C249269C7D2 /* OneSignalOutcomes.framework in Frameworks */,
+ 00B9E4C45782A6AD1CD3FE48 /* OneSignalExtension.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ BC0DC3536DED64E0E8C9923E /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 8068AFC608E7E82F06733BE7 /* OneSignalCore.framework in Frameworks */,
+ AF8A33E8CFAF1A8DB1907130 /* OneSignalOSCore.framework in Frameworks */,
+ 7B02F364CA25825E50B09CDB /* OneSignalOutcomes.framework in Frameworks */,
+ 0C4F40193331204CC2E05743 /* OneSignalNotifications.framework in Frameworks */,
+ 638B81D9DA5FD8636BB038B0 /* OneSignalUser.framework in Frameworks */,
+ 232A52A5D5719F863641166C /* OneSignalExtension.framework in Frameworks */,
+ 68BC99D15FDCB26EB35EBB07 /* OneSignalLocation.framework in Frameworks */,
+ C31F603E294D69E30CE65EA3 /* OneSignalInAppMessages.framework in Frameworks */,
+ E72F68A7DB51347AB8B10FA7 /* OneSignalLiveActivities.framework in Frameworks */,
+ D65AC6EC014557264E7A3697 /* OneSignal-Dynamic.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ C330DBFD3AE797742200EF8B /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 08C5E83ABC14EC2FC88276B9 /* OneSignalLiveActivities.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ 19317E8C50FA7D56330C6BDF /* ViewModels */ = {
+ isa = PBXGroup;
+ children = (
+ 0F1FA3F3D16A857DB6ED045F /* OneSignalViewModel.swift */,
+ 8F6CBCD47A3EA4209A6DDB03 /* ToastPresenter.swift */,
+ );
+ path = ViewModels;
+ sourceTree = "";
+ };
+ 4102E3E068508DD683953C7D = {
+ isa = PBXGroup;
+ children = (
+ 443E9F32F0EC65EEBF466F3B /* App */,
+ E62E617DD5FF6C0AA303F74D /* OneSignalNotificationServiceExtension */,
+ C83678AA9260FD8491BCC58F /* OneSignalWidget */,
+ E9C3DB3EC6D7655028A9C0D3 /* Products */,
+ E81A0E25363045BA30E3D600 /* Projects */,
+ );
+ sourceTree = "";
+ };
+ 443E9F32F0EC65EEBF466F3B /* App */ = {
+ isa = PBXGroup;
+ children = (
+ 4B8DD72AD7356663EB6BAE3A /* Models */,
+ 72F51C4EC858B862B629616C /* Services */,
+ 19317E8C50FA7D56330C6BDF /* ViewModels */,
+ F0D08B397106EA8498C4A8F4 /* Views */,
+ BDB04A33912347325A0155D6 /* App.swift */,
+ 76989E05CECAD7B8B3C424A7 /* Assets.xcassets */,
+ 6B00BC406653BC6B08ECCE26 /* Secrets.plist */,
+ 1EE449E8308FCB038408D7CF /* vine_boom.wav */,
+ );
+ path = App;
+ sourceTree = "";
+ };
+ 4B8DD72AD7356663EB6BAE3A /* Models */ = {
+ isa = PBXGroup;
+ children = (
+ 7EA9D80191548D49F09D30B3 /* AppModels.swift */,
+ );
+ path = Models;
+ sourceTree = "";
+ };
+ 53EC9A9E14C3568A020C0977 /* Sections */ = {
+ isa = PBXGroup;
+ children = (
+ 225DEBDFE699D266D5BDE7ED /* AliasesSection.swift */,
+ 7EB548D4688766660864F581 /* AppSection.swift */,
+ 4B6FF0BC430F5A2B89215967 /* CustomEventsSection.swift */,
+ 1892E9B40F0E8FB23DD64206 /* EmailsSection.swift */,
+ B76FFAF18177241F4A47FE23 /* InAppSection.swift */,
+ 27856D76807C31B23CD10CFD /* LiveActivitySection.swift */,
+ 5C0EE23773C63B6EC3FB563A /* LocationSection.swift */,
+ 4078B5F096680AFA83D1CB85 /* OutcomesSection.swift */,
+ A42B5EDA3264E3D00A5CB265 /* PushSection.swift */,
+ C60D09DC8C809877FB4BF465 /* SendIamSection.swift */,
+ 3B81D7E1A7EB9BB4466C768F /* SendPushSection.swift */,
+ E194A3F19072CB17A8F1A12E /* SmsSection.swift */,
+ ECAC7EF0B67920F9FEC4F129 /* TagsSection.swift */,
+ 1155DE423DAA8B5396948C9B /* TriggersSection.swift */,
+ 9E2383E8AA63BF04ADA2CF0D /* UserSection.swift */,
+ );
+ path = Sections;
+ sourceTree = "";
+ };
+ 54D70151146ED2430545164A /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 2C1B9BE42E81492B9DB61343 /* OneSignalCore.framework */,
+ 1636BCBAB444EAC7BB34CEDE /* OneSignalOSCore.framework */,
+ EC2530FFC78AECEAF6A7E1B7 /* OneSignalOutcomes.framework */,
+ 283B132CEE12D26D1FA1AADF /* OneSignalNotifications.framework */,
+ EF49509A218369322ECFA3B9 /* OneSignalUser.framework */,
+ B2F82BD207897EB78739182A /* OneSignalExtension.framework */,
+ FE9834773C437CC373607693 /* OneSignalLocation.framework */,
+ 919F0D129D0FA271E24F0BBD /* OneSignalInAppMessages.framework */,
+ F5EBC48D807036E572D97F0E /* OneSignalLiveActivities.framework */,
+ 25848A5B7E93DCA744373200 /* OneSignal-Dynamic.framework */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ 72F51C4EC858B862B629616C /* Services */ = {
+ isa = PBXGroup;
+ children = (
+ 0542854462194E28D9E4638D /* LiveActivityController.swift */,
+ 38138523A8A81A60A77800CA /* NotificationSender.swift */,
+ 5E0F5CBE80CF861238E1A9AA /* OneSignalService.swift */,
+ C76153AD2F6C6EB8F77138B9 /* PreferencesService.swift */,
+ D4EBF32FC1DBF3C34188E08C /* SecretsConfig.swift */,
+ 939E1F476EE48B9833A3311C /* TooltipService.swift */,
+ 3FD8258E807E6672642A32E6 /* UserFetchService.swift */,
+ );
+ path = Services;
+ sourceTree = "";
+ };
+ B9C3E998662065E7D921A5CA /* Components */ = {
+ isa = PBXGroup;
+ children = (
+ D6094499773760C3190F62C9 /* ActionButton.swift */,
+ D8D84B9C2B0EFE086A8E48CD /* AddItemDialog.swift */,
+ 2D959B1636916DAEE5FE6278 /* CustomNotificationDialog.swift */,
+ 5984B93007C6B85AFE09045A /* KeyValueRow.swift */,
+ 280B23B41935EAB89C8C6FCB /* ListWidgets.swift */,
+ 4072202943CA64D2CBC38CB5 /* MultiPairInputDialog.swift */,
+ D261D46C404E325CBA87A9E0 /* OSDialog.swift */,
+ D54B9DAAEDBE67B73893C522 /* OutcomeDialog.swift */,
+ 3A654457BF0A55B54220E669 /* RemoveMultiDialog.swift */,
+ 497484E7C094D645338BD404 /* SectionCard.swift */,
+ F46DFACB9F304B9374F3C570 /* ToastView.swift */,
+ 911376C90AA43F41478596FE /* ToggleRow.swift */,
+ 38067B9DDA25E12809D9A245 /* TooltipDialog.swift */,
+ 432444EA41C495988DFAB422 /* TrackEventDialog.swift */,
+ );
+ path = Components;
+ sourceTree = "";
+ };
+ C83678AA9260FD8491BCC58F /* OneSignalWidget */ = {
+ isa = PBXGroup;
+ children = (
+ C360043C7B20683C2E3FA3B6 /* Assets.xcassets */,
+ 6DD14C3CEEDFB9557E589B45 /* OneSignalWidgetBundle.swift */,
+ 0864BD4A6F62539B2809338F /* OneSignalWidgetLiveActivity.swift */,
+ );
+ path = OneSignalWidget;
+ sourceTree = "";
+ };
+ E62E617DD5FF6C0AA303F74D /* OneSignalNotificationServiceExtension */ = {
+ isa = PBXGroup;
+ children = (
+ A20B46F63592FC67B655BEB8 /* NotificationService.swift */,
+ );
+ path = OneSignalNotificationServiceExtension;
+ sourceTree = "";
+ };
+ E81A0E25363045BA30E3D600 /* Projects */ = {
+ isa = PBXGroup;
+ children = (
+ 291D83300C20BA3831824AFD /* OneSignalSDK */,
+ );
+ name = Projects;
+ sourceTree = "";
+ };
+ E9C3DB3EC6D7655028A9C0D3 /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 49D88A349FD70A9153DD2C03 /* App.app */,
+ B24E059F4DF6ABED55BA3183 /* OneSignalNotificationServiceExtension.appex */,
+ AB92C9C455B15E0B571A1C66 /* OneSignalWidget.appex */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ F0D08B397106EA8498C4A8F4 /* Views */ = {
+ isa = PBXGroup;
+ children = (
+ B9C3E998662065E7D921A5CA /* Components */,
+ 53EC9A9E14C3568A020C0977 /* Sections */,
+ 35F726E64F9B6817F917227C /* ContentView.swift */,
+ B3E7F504B0421F2B6247E2F5 /* Theme.swift */,
+ );
+ path = Views;
+ sourceTree = "";
+ };
+ "TEMP_BC60B80C-3553-4130-8508-42B5AB6C4C1B" /* demo */ = {
+ isa = PBXGroup;
+ children = (
+ "TEMP_196A01E1-FF4D-4A91-B9D6-E6100CF24C88" /* Build.xcconfig */,
+ );
+ path = demo;
+ sourceTree = "";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ 61033D7807F09753830EDBC1 /* OneSignalNotificationServiceExtension */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = B4BE2C39FCD722539813E4EC /* Build configuration list for PBXNativeTarget "OneSignalNotificationServiceExtension" */;
+ buildPhases = (
+ 5F10FF7EC3754C580B64A5B5 /* Sources */,
+ 83824651FEF6A3650CC5A580 /* Frameworks */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ D9BA8407D7B985A052FA30B8 /* PBXTargetDependency */,
+ 77C6618DD6ABFFC53745D2D4 /* PBXTargetDependency */,
+ E848F3B50C187ED7854A19EC /* PBXTargetDependency */,
+ );
+ name = OneSignalNotificationServiceExtension;
+ packageProductDependencies = (
+ );
+ productName = OneSignalNotificationServiceExtension;
+ productReference = B24E059F4DF6ABED55BA3183 /* OneSignalNotificationServiceExtension.appex */;
+ productType = "com.apple.product-type.app-extension";
+ };
+ 93E9E330FC2CE7458D9C925F /* App */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = FFC1863BD4026D2C17CBE82B /* Build configuration list for PBXNativeTarget "App" */;
+ buildPhases = (
+ B77F0AF073564580D75A2CF1 /* Sources */,
+ 37BFF6A8CB5CC7FA21AD3FA3 /* Resources */,
+ BC0DC3536DED64E0E8C9923E /* Frameworks */,
+ 7C6B2DB89D01A1E9B8F2A565 /* Embed Foundation Extensions */,
+ 8EFDE6013C818852098745E7 /* Embed Frameworks */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ 3D3214696D0E5D8871D927DC /* PBXTargetDependency */,
+ A1E826399F98FEF2C79AAD94 /* PBXTargetDependency */,
+ 39334E1C2B1194C7AA91290D /* PBXTargetDependency */,
+ E871C2D53DEBE7E6CD0D065C /* PBXTargetDependency */,
+ 1156917B5D40E6EEA429096E /* PBXTargetDependency */,
+ 4510858C7D7B77EA769F14BF /* PBXTargetDependency */,
+ D9EE246568FB5DB89C57EB72 /* PBXTargetDependency */,
+ DAE061F893FA97AAC97DC5B5 /* PBXTargetDependency */,
+ 6307D20A82A803B0C465DE8E /* PBXTargetDependency */,
+ 8438D2BC8BEECACED093384D /* PBXTargetDependency */,
+ 5CABB31140B2E97E15CB0194 /* PBXTargetDependency */,
+ 95E2CAB6F6B8EF265EC3BF95 /* PBXTargetDependency */,
+ );
+ name = App;
+ packageProductDependencies = (
+ );
+ productName = App;
+ productReference = 49D88A349FD70A9153DD2C03 /* App.app */;
+ productType = "com.apple.product-type.application";
+ };
+ 97373B3889FBDD1762E98B03 /* OneSignalWidget */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = B4E4C9786F2CB642EDE86073 /* Build configuration list for PBXNativeTarget "OneSignalWidget" */;
+ buildPhases = (
+ FAFE195EF7AA226107A30633 /* Sources */,
+ 83BDEE3A221EC042F9FBEE81 /* Resources */,
+ C330DBFD3AE797742200EF8B /* Frameworks */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ AADD02BB12D61D365C19EBE0 /* PBXTargetDependency */,
+ );
+ name = OneSignalWidget;
+ packageProductDependencies = (
+ );
+ productName = OneSignalWidget;
+ productReference = AB92C9C455B15E0B571A1C66 /* OneSignalWidget.appex */;
+ productType = "com.apple.product-type.app-extension";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ 6B590BF25178DC7D824D09CE /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ BuildIndependentTargetsInParallel = YES;
+ LastUpgradeCheck = 1430;
+ TargetAttributes = {
+ 61033D7807F09753830EDBC1 = {
+ DevelopmentTeam = "";
+ ProvisioningStyle = Automatic;
+ };
+ 93E9E330FC2CE7458D9C925F = {
+ DevelopmentTeam = "";
+ ProvisioningStyle = Automatic;
+ };
+ 97373B3889FBDD1762E98B03 = {
+ DevelopmentTeam = "";
+ ProvisioningStyle = Automatic;
+ };
+ };
+ };
+ buildConfigurationList = B3FD05C59F197F398A0B04AB /* Build configuration list for PBXProject "App" */;
+ developmentRegion = en;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ Base,
+ en,
+ );
+ mainGroup = 4102E3E068508DD683953C7D;
+ minimizedProjectReferenceProxies = 1;
+ preferredProjectObjectVersion = 77;
+ productRefGroup = E9C3DB3EC6D7655028A9C0D3 /* Products */;
+ projectDirPath = "";
+ projectReferences = (
+ {
+ ProductGroup = 54D70151146ED2430545164A /* Products */;
+ ProjectRef = 291D83300C20BA3831824AFD /* OneSignalSDK */;
+ },
+ );
+ projectRoot = "";
+ targets = (
+ 93E9E330FC2CE7458D9C925F /* App */,
+ 61033D7807F09753830EDBC1 /* OneSignalNotificationServiceExtension */,
+ 97373B3889FBDD1762E98B03 /* OneSignalWidget */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXReferenceProxy section */
+ 1636BCBAB444EAC7BB34CEDE /* OneSignalOSCore.framework */ = {
+ isa = PBXReferenceProxy;
+ fileType = wrapper.framework;
+ path = OneSignalOSCore.framework;
+ remoteRef = 8B6EE756CF7175F38D5B5B4B /* PBXContainerItemProxy */;
+ sourceTree = BUILT_PRODUCTS_DIR;
+ };
+ 25848A5B7E93DCA744373200 /* OneSignal-Dynamic.framework */ = {
+ isa = PBXReferenceProxy;
+ fileType = wrapper.framework;
+ path = "OneSignal-Dynamic.framework";
+ remoteRef = 25CC6D5CA1E9DA9142715E8A /* PBXContainerItemProxy */;
+ sourceTree = BUILT_PRODUCTS_DIR;
+ };
+ 283B132CEE12D26D1FA1AADF /* OneSignalNotifications.framework */ = {
+ isa = PBXReferenceProxy;
+ fileType = wrapper.framework;
+ path = OneSignalNotifications.framework;
+ remoteRef = 852E98FC9C8818AE6B636551 /* PBXContainerItemProxy */;
+ sourceTree = BUILT_PRODUCTS_DIR;
+ };
+ 2C1B9BE42E81492B9DB61343 /* OneSignalCore.framework */ = {
+ isa = PBXReferenceProxy;
+ fileType = wrapper.framework;
+ path = OneSignalCore.framework;
+ remoteRef = D399497AA4F221AD92C859AE /* PBXContainerItemProxy */;
+ sourceTree = BUILT_PRODUCTS_DIR;
+ };
+ 919F0D129D0FA271E24F0BBD /* OneSignalInAppMessages.framework */ = {
+ isa = PBXReferenceProxy;
+ fileType = wrapper.framework;
+ path = OneSignalInAppMessages.framework;
+ remoteRef = C43833A058207841248D7506 /* PBXContainerItemProxy */;
+ sourceTree = BUILT_PRODUCTS_DIR;
+ };
+ B2F82BD207897EB78739182A /* OneSignalExtension.framework */ = {
+ isa = PBXReferenceProxy;
+ fileType = wrapper.framework;
+ path = OneSignalExtension.framework;
+ remoteRef = 06313C80EB92E867CF4AE55C /* PBXContainerItemProxy */;
+ sourceTree = BUILT_PRODUCTS_DIR;
+ };
+ EC2530FFC78AECEAF6A7E1B7 /* OneSignalOutcomes.framework */ = {
+ isa = PBXReferenceProxy;
+ fileType = wrapper.framework;
+ path = OneSignalOutcomes.framework;
+ remoteRef = E36EAB44E3DD77FE80F2A724 /* PBXContainerItemProxy */;
+ sourceTree = BUILT_PRODUCTS_DIR;
+ };
+ EF49509A218369322ECFA3B9 /* OneSignalUser.framework */ = {
+ isa = PBXReferenceProxy;
+ fileType = wrapper.framework;
+ path = OneSignalUser.framework;
+ remoteRef = D47A29D1FE2C196E0715923B /* PBXContainerItemProxy */;
+ sourceTree = BUILT_PRODUCTS_DIR;
+ };
+ F5EBC48D807036E572D97F0E /* OneSignalLiveActivities.framework */ = {
+ isa = PBXReferenceProxy;
+ fileType = wrapper.framework;
+ path = OneSignalLiveActivities.framework;
+ remoteRef = 8E39421BCBE5C21C1A9CB242 /* PBXContainerItemProxy */;
+ sourceTree = BUILT_PRODUCTS_DIR;
+ };
+ FE9834773C437CC373607693 /* OneSignalLocation.framework */ = {
+ isa = PBXReferenceProxy;
+ fileType = wrapper.framework;
+ path = OneSignalLocation.framework;
+ remoteRef = 996095BEB3FD8CE51B7F1AD7 /* PBXContainerItemProxy */;
+ sourceTree = BUILT_PRODUCTS_DIR;
+ };
+/* End PBXReferenceProxy section */
+
+/* Begin PBXResourcesBuildPhase section */
+ 37BFF6A8CB5CC7FA21AD3FA3 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 3FB3C8C0634A765EE81D042E /* Assets.xcassets in Resources */,
+ 93104E0838915DD6194805D5 /* Secrets.plist in Resources */,
+ AB963AA6733C32EFB84DCBC0 /* vine_boom.wav in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 83BDEE3A221EC042F9FBEE81 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ FB0472C73D007EABFD01CCA8 /* Assets.xcassets in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+ 5F10FF7EC3754C580B64A5B5 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 5F4B7EC8437D1A8D80DF7674 /* NotificationService.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ B77F0AF073564580D75A2CF1 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 9B0D1DD32B99602629BBCB95 /* ActionButton.swift in Sources */,
+ 1A247F505A707756873AA9FA /* AddItemDialog.swift in Sources */,
+ A7A5153D68A4967A88B2B433 /* AliasesSection.swift in Sources */,
+ A2B1975BB925DCA45327D71E /* App.swift in Sources */,
+ 6E3E040FD8A750248E70E46F /* AppModels.swift in Sources */,
+ F1DBBD7F18CB70C5DF2FFC32 /* AppSection.swift in Sources */,
+ 5B959D44AB09CB821C00AFBF /* ContentView.swift in Sources */,
+ 8EA2FF24D93691FDC1661913 /* CustomEventsSection.swift in Sources */,
+ 2859C7827BE28C7EE4AFDAB4 /* CustomNotificationDialog.swift in Sources */,
+ 0E7D0439A19C8BF291A8BEB1 /* EmailsSection.swift in Sources */,
+ 837FCE7A095ED1D7CCFEACF3 /* InAppSection.swift in Sources */,
+ E5671E3A84219D49E45F8DCF /* KeyValueRow.swift in Sources */,
+ 9A7BE456B679D7DE7CA300BC /* ListWidgets.swift in Sources */,
+ 7D2BA9022E77B00205453467 /* LiveActivityController.swift in Sources */,
+ DBDA727D6317A3CCC73A1699 /* LiveActivitySection.swift in Sources */,
+ B3AE701075398C6A369DBBE0 /* LocationSection.swift in Sources */,
+ E3725231A3FD5F5A88BAA758 /* MultiPairInputDialog.swift in Sources */,
+ 4C18E3D284BB28BD846162F3 /* NotificationSender.swift in Sources */,
+ 674995A7A55C13341317E19B /* OSDialog.swift in Sources */,
+ 12597AC14E1783CC87D6E147 /* OneSignalService.swift in Sources */,
+ 5737CABFA55E019B2732B90D /* OneSignalViewModel.swift in Sources */,
+ 39D2C94F79A62BFF9DE5DBA9 /* OutcomeDialog.swift in Sources */,
+ 14228D0C997F9168643F8154 /* OutcomesSection.swift in Sources */,
+ 20792A9930A201E187AA0ABF /* PreferencesService.swift in Sources */,
+ EA93E372AA3E66073487B89C /* PushSection.swift in Sources */,
+ 2FA8128D2A921DDF02210D8A /* RemoveMultiDialog.swift in Sources */,
+ 28D491D31B5C07E4D4F48A7D /* SecretsConfig.swift in Sources */,
+ 7B94F48C31E0BEA4B8CB20E2 /* SectionCard.swift in Sources */,
+ 27C72DF35BE082E3E1093F75 /* SendIamSection.swift in Sources */,
+ 0BC1978B56970258E20C24C4 /* SendPushSection.swift in Sources */,
+ 2FCC417641D480849E99588B /* SmsSection.swift in Sources */,
+ 3927A4BF207695E98A57E445 /* TagsSection.swift in Sources */,
+ 16B0C854BC2498BEEDC4EEED /* Theme.swift in Sources */,
+ 1F417CAD2528616703CAA54D /* ToastPresenter.swift in Sources */,
+ 7C904CE2F4C2A2083324BBEB /* ToastView.swift in Sources */,
+ 902A116B26B8ECAD8EE29C95 /* ToggleRow.swift in Sources */,
+ A2E180ADEA246FB6530E5C8C /* TooltipDialog.swift in Sources */,
+ 25C963476DF0B01BAABE24D9 /* TooltipService.swift in Sources */,
+ A8498B9A2AA7DA8CB0856CAC /* TrackEventDialog.swift in Sources */,
+ 1653658B1F4EE21203BBAA5D /* TriggersSection.swift in Sources */,
+ 0E6E9EEBF2500A4E70022960 /* UserFetchService.swift in Sources */,
+ CE2C54D552E42C2538FB95C4 /* UserSection.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ FAFE195EF7AA226107A30633 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 56DF63011CB625810F81075D /* OneSignalWidgetBundle.swift in Sources */,
+ 1FF9F70882A0CF6A73416DDF /* OneSignalWidgetLiveActivity.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXTargetDependency section */
+ 1156917B5D40E6EEA429096E /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ name = OneSignalUser;
+ targetProxy = 13BDB99578D2E19A9A750DEF /* PBXContainerItemProxy */;
+ };
+ 39334E1C2B1194C7AA91290D /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ name = OneSignalOutcomes;
+ targetProxy = 67BDC3D5140D6CC33418AA1D /* PBXContainerItemProxy */;
+ };
+ 3D3214696D0E5D8871D927DC /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ name = OneSignalCore;
+ targetProxy = 9CA6E55F92E787A8186EEBA1 /* PBXContainerItemProxy */;
+ };
+ 4510858C7D7B77EA769F14BF /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ name = OneSignalExtension;
+ targetProxy = 8475704534409C59DAE8B29A /* PBXContainerItemProxy */;
+ };
+ 5CABB31140B2E97E15CB0194 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = 61033D7807F09753830EDBC1 /* OneSignalNotificationServiceExtension */;
+ targetProxy = A668D6C94AC1F182AF01EAFF /* PBXContainerItemProxy */;
+ };
+ 6307D20A82A803B0C465DE8E /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ name = OneSignalLiveActivities;
+ targetProxy = E48045948F131DB2BD8C1B2A /* PBXContainerItemProxy */;
+ };
+ 77C6618DD6ABFFC53745D2D4 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ name = OneSignalOutcomes;
+ targetProxy = 6701C77339EC73AE4FEC7E32 /* PBXContainerItemProxy */;
+ };
+ 8438D2BC8BEECACED093384D /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ name = OneSignalFramework;
+ targetProxy = 5C38CF82FCDF8C89EA732187 /* PBXContainerItemProxy */;
+ };
+ 95E2CAB6F6B8EF265EC3BF95 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = 97373B3889FBDD1762E98B03 /* OneSignalWidget */;
+ targetProxy = 642A3E9AC0D14239CE4A128E /* PBXContainerItemProxy */;
+ };
+ A1E826399F98FEF2C79AAD94 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ name = OneSignalOSCore;
+ targetProxy = 6DE922175F2D1304874645CD /* PBXContainerItemProxy */;
+ };
+ AADD02BB12D61D365C19EBE0 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ name = OneSignalLiveActivities;
+ targetProxy = D378765DB7FB67E222BB7FF1 /* PBXContainerItemProxy */;
+ };
+ D9BA8407D7B985A052FA30B8 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ name = OneSignalCore;
+ targetProxy = B627D4859D5F7B2B7F452917 /* PBXContainerItemProxy */;
+ };
+ D9EE246568FB5DB89C57EB72 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ name = OneSignalLocation;
+ targetProxy = 5C4A42186FB7137C13DD3C0D /* PBXContainerItemProxy */;
+ };
+ DAE061F893FA97AAC97DC5B5 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ name = OneSignalInAppMessages;
+ targetProxy = 005EC764DD2429AF6136BB9A /* PBXContainerItemProxy */;
+ };
+ E848F3B50C187ED7854A19EC /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ name = OneSignalExtension;
+ targetProxy = 21265D9F7D2B1FBDDD3E716F /* PBXContainerItemProxy */;
+ };
+ E871C2D53DEBE7E6CD0D065C /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ name = OneSignalNotifications;
+ targetProxy = D1E066F0069BD9B244E196E4 /* PBXContainerItemProxy */;
+ };
+/* End PBXTargetDependency section */
+
+/* Begin XCBuildConfiguration section */
+ 0D2EF3911CA89837C30DB0D1 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = "TEMP_196A01E1-FF4D-4A91-B9D6-E6100CF24C88" /* Build.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
+ INFOPLIST_FILE = OneSignalWidget/Info.plist;
+ IPHONEOS_DEPLOYMENT_TARGET = 16.2;
+ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
+ PRODUCT_BUNDLE_IDENTIFIER = com.onesignal.example.LA;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SDKROOT = iphoneos;
+ SKIP_INSTALL = YES;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Debug;
+ };
+ 4A0C935808978B5A7673E412 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = "TEMP_196A01E1-FF4D-4A91-B9D6-E6100CF24C88" /* Build.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CODE_SIGN_ENTITLEMENTS = App.entitlements;
+ CODE_SIGN_IDENTITY = "iPhone Developer";
+ ENABLE_USER_SCRIPT_SANDBOXING = NO;
+ INFOPLIST_FILE = App/Info.plist;
+ IPHONEOS_DEPLOYMENT_TARGET = 16.0;
+ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+ PRODUCT_BUNDLE_IDENTIFIER = com.onesignal.example;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SDKROOT = iphoneos;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Debug;
+ };
+ 5C9EF0E6AF4F9491454DE177 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ CODE_SIGN_STYLE = Automatic;
+ COPY_PHASE_STRIP = NO;
+ CURRENT_PROJECT_VERSION = 1;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ DEVELOPMENT_TEAM = "";
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu11;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ GENERATE_INFOPLIST_FILE = NO;
+ IPHONEOS_DEPLOYMENT_TARGET = 16.0;
+ MARKETING_VERSION = 1.0;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ MTL_FAST_MATH = YES;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SDKROOT = iphoneos;
+ SWIFT_COMPILATION_MODE = wholemodule;
+ SWIFT_OPTIMIZATION_LEVEL = "-O";
+ SWIFT_VERSION = 5.9;
+ };
+ name = Release;
+ };
+ D0E56A85F1C385808720F94B /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = "TEMP_196A01E1-FF4D-4A91-B9D6-E6100CF24C88" /* Build.xcconfig */;
+ buildSettings = {
+ CODE_SIGN_ENTITLEMENTS = OneSignalNotificationServiceExtension/OneSignalNotificationServiceExtension.entitlements;
+ INFOPLIST_FILE = OneSignalNotificationServiceExtension/Info.plist;
+ IPHONEOS_DEPLOYMENT_TARGET = 16.0;
+ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
+ PRODUCT_BUNDLE_IDENTIFIER = com.onesignal.example.NSE;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SDKROOT = iphoneos;
+ SKIP_INSTALL = YES;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Release;
+ };
+ EB1CC3A930E09FEBECF9195D /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = "TEMP_196A01E1-FF4D-4A91-B9D6-E6100CF24C88" /* Build.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CODE_SIGN_ENTITLEMENTS = App.entitlements;
+ CODE_SIGN_IDENTITY = "iPhone Developer";
+ ENABLE_USER_SCRIPT_SANDBOXING = NO;
+ INFOPLIST_FILE = App/Info.plist;
+ IPHONEOS_DEPLOYMENT_TARGET = 16.0;
+ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+ PRODUCT_BUNDLE_IDENTIFIER = com.onesignal.example;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SDKROOT = iphoneos;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Release;
+ };
+ F305A3E63851EE49DA2D190E /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = "TEMP_196A01E1-FF4D-4A91-B9D6-E6100CF24C88" /* Build.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
+ INFOPLIST_FILE = OneSignalWidget/Info.plist;
+ IPHONEOS_DEPLOYMENT_TARGET = 16.2;
+ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
+ PRODUCT_BUNDLE_IDENTIFIER = com.onesignal.example.LA;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SDKROOT = iphoneos;
+ SKIP_INSTALL = YES;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Release;
+ };
+ F5FD25168D9B32A08A468069 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = "TEMP_196A01E1-FF4D-4A91-B9D6-E6100CF24C88" /* Build.xcconfig */;
+ buildSettings = {
+ CODE_SIGN_ENTITLEMENTS = OneSignalNotificationServiceExtension/OneSignalNotificationServiceExtension.entitlements;
+ INFOPLIST_FILE = OneSignalNotificationServiceExtension/Info.plist;
+ IPHONEOS_DEPLOYMENT_TARGET = 16.0;
+ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
+ PRODUCT_BUNDLE_IDENTIFIER = com.onesignal.example.NSE;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SDKROOT = iphoneos;
+ SKIP_INSTALL = YES;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Debug;
+ };
+ F61063B78755D98B1B9C3697 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ CODE_SIGN_STYLE = Automatic;
+ COPY_PHASE_STRIP = NO;
+ CURRENT_PROJECT_VERSION = 1;
+ DEBUG_INFORMATION_FORMAT = dwarf;
+ DEVELOPMENT_TEAM = "";
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_TESTABILITY = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu11;
+ GCC_DYNAMIC_NO_PIC = NO;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_OPTIMIZATION_LEVEL = 0;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "$(inherited)",
+ "DEBUG=1",
+ );
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ GENERATE_INFOPLIST_FILE = NO;
+ IPHONEOS_DEPLOYMENT_TARGET = 16.0;
+ MARKETING_VERSION = 1.0;
+ MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
+ MTL_FAST_MATH = YES;
+ ONLY_ACTIVE_ARCH = YES;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SDKROOT = iphoneos;
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ SWIFT_VERSION = 5.9;
+ };
+ name = Debug;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ B3FD05C59F197F398A0B04AB /* Build configuration list for PBXProject "App" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ F61063B78755D98B1B9C3697 /* Debug */,
+ 5C9EF0E6AF4F9491454DE177 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Debug;
+ };
+ B4BE2C39FCD722539813E4EC /* Build configuration list for PBXNativeTarget "OneSignalNotificationServiceExtension" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ F5FD25168D9B32A08A468069 /* Debug */,
+ D0E56A85F1C385808720F94B /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Debug;
+ };
+ B4E4C9786F2CB642EDE86073 /* Build configuration list for PBXNativeTarget "OneSignalWidget" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 0D2EF3911CA89837C30DB0D1 /* Debug */,
+ F305A3E63851EE49DA2D190E /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Debug;
+ };
+ FFC1863BD4026D2C17CBE82B /* Build configuration list for PBXNativeTarget "App" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 4A0C935808978B5A7673E412 /* Debug */,
+ EB1CC3A930E09FEBECF9195D /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Debug;
+ };
+/* End XCConfigurationList section */
+ };
+ rootObject = 6B590BF25178DC7D824D09CE /* Project object */;
+}
diff --git a/OneSignalSwiftUIExample/OneSignalSwiftUIExample/App/OneSignalSwiftUIExampleApp.swift b/examples/demo/App/App.swift
similarity index 93%
rename from OneSignalSwiftUIExample/OneSignalSwiftUIExample/App/OneSignalSwiftUIExampleApp.swift
rename to examples/demo/App/App.swift
index 891c6a1ca..58de08ea3 100644
--- a/OneSignalSwiftUIExample/OneSignalSwiftUIExample/App/OneSignalSwiftUIExampleApp.swift
+++ b/examples/demo/App/App.swift
@@ -27,18 +27,19 @@
import SwiftUI
import OneSignalFramework
-import OneSignalInAppMessages
-import OneSignalLocation
+import OneSignalLiveActivities
@main
-struct OneSignalSwiftUIExampleApp: App {
+struct App: SwiftUI.App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
@StateObject private var viewModel = OneSignalViewModel()
+ @StateObject private var toastPresenter = ToastPresenter()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(viewModel)
+ .environmentObject(toastPresenter)
}
}
}
@@ -60,6 +61,11 @@ class AppDelegate: NSObject, UIApplicationDelegate {
// Set up in-app message listeners
setupInAppMessageListeners()
+ // Set up Live Activities (iOS 16.1+)
+ if #available(iOS 16.1, *) {
+ LiveActivityController.setup()
+ }
+
return true
}
@@ -77,9 +83,6 @@ class AppDelegate: NSObject, UIApplicationDelegate {
// In-app message click handling
OneSignal.InAppMessages.addClickListener(InAppMessageClickHandler.shared)
-
- // Start with IAM paused
- OneSignal.InAppMessages.paused = true
}
}
diff --git a/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AccentColor.colorset/Contents.json b/examples/demo/App/Assets.xcassets/AccentColor.colorset/Contents.json
similarity index 100%
rename from OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AccentColor.colorset/Contents.json
rename to examples/demo/App/Assets.xcassets/AccentColor.colorset/Contents.json
diff --git a/examples/demo/App/Assets.xcassets/AppIcon.appiconset/AppIcon.png b/examples/demo/App/Assets.xcassets/AppIcon.appiconset/AppIcon.png
new file mode 100644
index 000000000..a4d02a3bc
Binary files /dev/null and b/examples/demo/App/Assets.xcassets/AppIcon.appiconset/AppIcon.png differ
diff --git a/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/Contents.json b/examples/demo/App/Assets.xcassets/AppIcon.appiconset/Contents.json
similarity index 83%
rename from OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/Contents.json
rename to examples/demo/App/Assets.xcassets/AppIcon.appiconset/Contents.json
index 13613e3ee..cefcc878e 100644
--- a/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/Contents.json
+++ b/examples/demo/App/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -1,6 +1,7 @@
{
"images" : [
{
+ "filename" : "AppIcon.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
diff --git a/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/Contents.json b/examples/demo/App/Assets.xcassets/Contents.json
similarity index 100%
rename from OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/Contents.json
rename to examples/demo/App/Assets.xcassets/Contents.json
diff --git a/examples/demo/App/Assets.xcassets/LaunchBackground.colorset/Contents.json b/examples/demo/App/Assets.xcassets/LaunchBackground.colorset/Contents.json
new file mode 100644
index 000000000..97650a1a6
--- /dev/null
+++ b/examples/demo/App/Assets.xcassets/LaunchBackground.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "1.000",
+ "green" : "1.000",
+ "red" : "1.000"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/OneSignalLogo.imageset/Contents.json b/examples/demo/App/Assets.xcassets/onesignal_launch_icon.imageset/Contents.json
similarity index 63%
rename from iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/OneSignalLogo.imageset/Contents.json
rename to examples/demo/App/Assets.xcassets/onesignal_launch_icon.imageset/Contents.json
index cbc496abc..f6b59d0ab 100644
--- a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/OneSignalLogo.imageset/Contents.json
+++ b/examples/demo/App/Assets.xcassets/onesignal_launch_icon.imageset/Contents.json
@@ -1,15 +1,17 @@
{
"images" : [
{
- "filename" : "onesignal_rectangle.png",
+ "filename" : "onesignal_launch_icon@1x.png",
"idiom" : "universal",
"scale" : "1x"
},
{
+ "filename" : "onesignal_launch_icon@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
+ "filename" : "onesignal_launch_icon@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
@@ -17,8 +19,5 @@
"info" : {
"author" : "xcode",
"version" : 1
- },
- "properties" : {
- "template-rendering-intent" : "template"
}
}
diff --git a/examples/demo/App/Assets.xcassets/onesignal_launch_icon.imageset/onesignal_launch_icon@1x.png b/examples/demo/App/Assets.xcassets/onesignal_launch_icon.imageset/onesignal_launch_icon@1x.png
new file mode 100644
index 000000000..5898d09a6
Binary files /dev/null and b/examples/demo/App/Assets.xcassets/onesignal_launch_icon.imageset/onesignal_launch_icon@1x.png differ
diff --git a/examples/demo/App/Assets.xcassets/onesignal_launch_icon.imageset/onesignal_launch_icon@2x.png b/examples/demo/App/Assets.xcassets/onesignal_launch_icon.imageset/onesignal_launch_icon@2x.png
new file mode 100644
index 000000000..92cac76d9
Binary files /dev/null and b/examples/demo/App/Assets.xcassets/onesignal_launch_icon.imageset/onesignal_launch_icon@2x.png differ
diff --git a/examples/demo/App/Assets.xcassets/onesignal_launch_icon.imageset/onesignal_launch_icon@3x.png b/examples/demo/App/Assets.xcassets/onesignal_launch_icon.imageset/onesignal_launch_icon@3x.png
new file mode 100644
index 000000000..7f2c280df
Binary files /dev/null and b/examples/demo/App/Assets.xcassets/onesignal_launch_icon.imageset/onesignal_launch_icon@3x.png differ
diff --git a/examples/demo/App/Assets.xcassets/onesignal_logo.imageset/Contents.json b/examples/demo/App/Assets.xcassets/onesignal_logo.imageset/Contents.json
new file mode 100644
index 000000000..43a089b72
--- /dev/null
+++ b/examples/demo/App/Assets.xcassets/onesignal_logo.imageset/Contents.json
@@ -0,0 +1,16 @@
+{
+ "images" : [
+ {
+ "filename" : "onesignal_logo.pdf",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "properties" : {
+ "preserves-vector-representation" : true,
+ "template-rendering-intent" : "original"
+ }
+}
diff --git a/examples/demo/App/Assets.xcassets/onesignal_logo.imageset/onesignal_logo.pdf b/examples/demo/App/Assets.xcassets/onesignal_logo.imageset/onesignal_logo.pdf
new file mode 100644
index 000000000..30d78ec1b
Binary files /dev/null and b/examples/demo/App/Assets.xcassets/onesignal_logo.imageset/onesignal_logo.pdf differ
diff --git a/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Info.plist b/examples/demo/App/Info.plist
similarity index 83%
rename from OneSignalSwiftUIExample/OneSignalSwiftUIExample/Info.plist
rename to examples/demo/App/Info.plist
index aba8d06a4..21a2421ab 100644
--- a/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Info.plist
+++ b/examples/demo/App/Info.plist
@@ -5,7 +5,7 @@
CFBundleDevelopmentRegion
$(DEVELOPMENT_LANGUAGE)
CFBundleDisplayName
- OneSignal SwiftUI
+ OneSignal Demo
CFBundleExecutable
$(EXECUTABLE_NAME)
CFBundleIdentifier
@@ -19,13 +19,17 @@
CFBundleShortVersionString
1.0
CFBundleVersion
- 1
+ 2
LSRequiresIPhoneOS
NSLocationWhenInUseUsageDescription
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.
+ NSSupportsLiveActivities
+
+ NSSupportsLiveActivitiesFrequentUpdates
+
UIApplicationSceneManifest
UIApplicationSupportsMultipleScenes
@@ -36,10 +40,17 @@
remote-notification
UILaunchScreen
-
+
+ UIColorName
+ LaunchBackground
+ UIImageName
+ onesignal_launch_icon
+ UIImageRespectsSafeAreaInsets
+
+
UIRequiredDeviceCapabilities
- armv7
+ arm64
UISupportedInterfaceOrientations
diff --git a/examples/demo/App/Models/AppModels.swift b/examples/demo/App/Models/AppModels.swift
new file mode 100644
index 000000000..70ccd615a
--- /dev/null
+++ b/examples/demo/App/Models/AppModels.swift
@@ -0,0 +1,257 @@
+/**
+ * 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 UIKit
+
+// MARK: - Key-Value Item
+
+/// Generic key-value pair used for aliases, tags, and triggers
+struct KeyValueItem: Identifiable, Equatable {
+ let id = UUID()
+ let key: String
+ let value: String
+}
+
+// MARK: - Notification Type
+
+/// Push notification samples that can be sent from the demo
+enum NotificationType: String, CaseIterable, Identifiable {
+ case simple = "Simple"
+ case withImage = "With Image"
+ case withSound = "With Sound"
+
+ var id: String { rawValue }
+}
+
+// MARK: - In-App Message Type
+
+/// Sample in-app message layouts triggered by the iam_type trigger
+enum InAppMessageType: String, CaseIterable, Identifiable {
+ case topBanner = "Top Banner"
+ case bottomBanner = "Bottom Banner"
+ case centerModal = "Center Modal"
+ case fullScreen = "Full Screen"
+
+ var id: String { rawValue }
+
+ /// Trigger value the OneSignal IAM rules listen for
+ var triggerValue: String {
+ switch self {
+ case .topBanner: return "top_banner"
+ case .bottomBanner: return "bottom_banner"
+ case .centerModal: return "center_modal"
+ case .fullScreen: return "full_screen"
+ }
+ }
+}
+
+// MARK: - Add Item Type
+
+/// Single-input add dialog flavors
+enum AddItemType {
+ case alias
+ case email
+ case sms
+ case tag
+ case trigger
+ case externalUserId
+
+ var title: String {
+ switch self {
+ case .alias: return "Add Alias"
+ case .email: return "Add Email"
+ case .sms: return "Add SMS"
+ case .tag: return "Add Tag"
+ case .trigger: return "Add Trigger"
+ case .externalUserId: return "Login User"
+ }
+ }
+
+ var requiresKeyValue: Bool {
+ switch self {
+ case .alias, .tag, .trigger: return true
+ case .email, .sms, .externalUserId: return false
+ }
+ }
+
+ var keyPlaceholder: String {
+ switch self {
+ case .alias: return "Label"
+ case .tag, .trigger: return "Key"
+ default: return "Key"
+ }
+ }
+
+ var valuePlaceholder: String {
+ switch self {
+ case .alias: return "ID"
+ case .email: return "Email Address"
+ case .sms: return "Phone Number"
+ case .tag, .trigger: return "Value"
+ case .externalUserId: return "External User Id"
+ }
+ }
+
+ var keyboardType: UIKeyboardType {
+ switch self {
+ case .email: return .emailAddress
+ case .sms: return .phonePad
+ default: return .default
+ }
+ }
+
+ var confirmLabel: String {
+ switch self {
+ case .externalUserId: return "Login"
+ default: return "Add"
+ }
+ }
+
+ /// Stable accessibility id prefix shared with the rest of the demo
+ var accessibilityKey: String {
+ switch self {
+ case .alias: return "alias"
+ case .email: return "email"
+ case .sms: return "sms"
+ case .tag: return "tag"
+ case .trigger: return "trigger"
+ case .externalUserId: return "login_user_id"
+ }
+ }
+
+ /// Accessibility id for the first text field in two-input dialogs.
+ /// Mirrors the shared Appium spec naming (`alias_label_input`,
+ /// `tag_key_input`, `trigger_key_input`).
+ var keyInputID: String {
+ switch self {
+ case .alias: return "alias_label_input"
+ case .tag: return "tag_key_input"
+ case .trigger: return "trigger_key_input"
+ default: return "\(accessibilityKey)_key_input"
+ }
+ }
+
+ /// Accessibility id for the second / single text field.
+ /// Mirrors the shared Appium spec naming (`alias_id_input`,
+ /// `tag_value_input`, `trigger_value_input`, `email_input`,
+ /// `sms_input`, `login_user_id_input`).
+ var valueInputID: String {
+ switch self {
+ case .alias: return "alias_id_input"
+ case .tag: return "tag_value_input"
+ case .trigger: return "trigger_value_input"
+ default: return "\(accessibilityKey)_input"
+ }
+ }
+
+ /// Two-input flavors share `singlepair_*` buttons; single-input flavors
+ /// share `singleinput_*` so the Appium suite can find them by a stable id
+ /// regardless of the specific item type.
+ var confirmButtonID: String {
+ requiresKeyValue ? "singlepair_confirm_button" : "singleinput_confirm_button"
+ }
+
+ var cancelButtonID: String {
+ requiresKeyValue ? "singlepair_cancel_button" : "singleinput_cancel_button"
+ }
+}
+
+// MARK: - Multi-Add Item Type
+
+/// Multi-pair add dialog flavors (Add Multiple Aliases / Tags / Triggers)
+enum MultiAddItemType: String {
+ case aliases = "Add Multiple Aliases"
+ case tags = "Add Multiple Tags"
+ case triggers = "Add Multiple Triggers"
+
+ var keyPlaceholder: String {
+ switch self {
+ case .aliases: return "Label"
+ case .tags, .triggers: return "Key"
+ }
+ }
+
+ var valuePlaceholder: String {
+ switch self {
+ case .aliases: return "ID"
+ case .tags, .triggers: return "Value"
+ }
+ }
+}
+
+// MARK: - Remove Multi Item Type
+
+/// Multi-select remove dialog flavors
+enum RemoveMultiItemType: String {
+ case tags = "Remove Tags"
+ case triggers = "Remove Triggers"
+}
+
+// MARK: - Outcome Mode
+
+/// Variants supported by the Send Outcome dialog
+enum OutcomeMode: String, CaseIterable, Identifiable {
+ case normal = "Normal Outcome"
+ case unique = "Unique Outcome"
+ case value = "Outcome with Value"
+
+ var id: String { rawValue }
+
+ var accessibilityKey: String {
+ switch self {
+ case .normal: return "normal"
+ case .unique: return "unique"
+ case .value: return "value"
+ }
+ }
+}
+
+// MARK: - Tooltip Models
+
+/// Tooltip content fetched from sdk-shared (or bundled fallback)
+struct TooltipData {
+ let title: String
+ let description: String
+ let options: [TooltipOption]?
+}
+
+struct TooltipOption {
+ let name: String
+ let description: String
+}
+
+// MARK: - User Data
+
+/// User payload returned from the OneSignal /users API
+struct UserData {
+ let aliases: [String: String]
+ let tags: [String: String]
+ let emails: [String]
+ let smsNumbers: [String]
+ let externalId: String?
+}
diff --git a/examples/demo/App/Services/LiveActivityController.swift b/examples/demo/App/Services/LiveActivityController.swift
new file mode 100644
index 000000000..8d00a542d
--- /dev/null
+++ b/examples/demo/App/Services/LiveActivityController.swift
@@ -0,0 +1,153 @@
+/**
+ * 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 OneSignalFramework
+import OneSignalLiveActivities
+
+/// Order tracking phases used by the Live Activity demo
+enum LiveActivityStatus: String, CaseIterable, Identifiable {
+ case preparing
+ case onTheWay = "on_the_way"
+ case delivered
+
+ var id: String { rawValue }
+
+ var displayName: String {
+ switch self {
+ case .preparing: return "Preparing"
+ case .onTheWay: return "On The Way"
+ case .delivered: return "Delivered"
+ }
+ }
+
+ var message: String {
+ switch self {
+ case .preparing: return "Your order is being prepared"
+ case .onTheWay: return "Driver is heading your way"
+ case .delivered: return "Order delivered!"
+ }
+ }
+
+ var estimatedTime: String {
+ switch self {
+ case .preparing: return "15 min"
+ case .onTheWay: return "10 min"
+ case .delivered: return ""
+ }
+ }
+
+ /// Returns the next status in the preparing → on_the_way → delivered → preparing cycle
+ var next: LiveActivityStatus {
+ switch self {
+ case .preparing: return .onTheWay
+ case .onTheWay: return .delivered
+ case .delivered: return .preparing
+ }
+ }
+}
+
+/// Wraps the OneSignal Live Activities SDK and the REST API endpoints used to update / end activities
+enum LiveActivityController {
+
+ @available(iOS 16.1, *)
+ static func setup() {
+ OneSignal.LiveActivities.setupDefault()
+ }
+
+ @available(iOS 16.1, *)
+ static func start(
+ activityId: String,
+ orderNumber: String,
+ status: LiveActivityStatus
+ ) {
+ let attributes: [String: Any] = [
+ "orderNumber": orderNumber
+ ]
+ let content: [String: Any] = [
+ "status": status.rawValue,
+ "message": status.message,
+ "estimatedTime": status.estimatedTime
+ ]
+ OneSignal.LiveActivities.startDefault(
+ activityId,
+ attributes: attributes,
+ content: content
+ )
+ }
+
+ static func update(appId: String, activityId: String, status: LiveActivityStatus) async -> Bool {
+ let payload: [String: Any] = [
+ "event": "update",
+ "name": "Live Activity Update",
+ "priority": 10,
+ "event_updates": [
+ "data": [
+ "status": status.rawValue,
+ "message": status.message,
+ "estimatedTime": status.estimatedTime
+ ]
+ ]
+ ]
+ return await postLiveActivity(appId: appId, activityId: activityId, payload: payload)
+ }
+
+ static func end(appId: String, activityId: String) async -> Bool {
+ let payload: [String: Any] = [
+ "event": "end",
+ "name": "End Live Activity",
+ "priority": 10,
+ "dismissal_date": Int(Date().timeIntervalSince1970),
+ "event_updates": [
+ "message": "Ended Live Activity"
+ ]
+ ]
+ return await postLiveActivity(appId: appId, activityId: activityId, payload: payload)
+ }
+
+ 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 }
+ let urlString = "https://api.onesignal.com/apps/\(appId)/live_activities/\(activityId)/notifications"
+ guard let url = URL(string: urlString) else { return false }
+
+ var request = URLRequest(url: url)
+ request.httpMethod = "POST"
+ request.setValue("application/json", forHTTPHeaderField: "Content-Type")
+ request.setValue("Key \(key)", forHTTPHeaderField: "Authorization")
+ request.httpBody = try? JSONSerialization.data(withJSONObject: payload, options: [])
+
+ do {
+ let (_, response) = try await URLSession.shared.data(for: request)
+ guard let http = response as? HTTPURLResponse else { return false }
+ return (200..<300).contains(http.statusCode)
+ } catch {
+ return false
+ }
+ }
+}
diff --git a/examples/demo/App/Services/NotificationSender.swift b/examples/demo/App/Services/NotificationSender.swift
new file mode 100644
index 000000000..f95b25739
--- /dev/null
+++ b/examples/demo/App/Services/NotificationSender.swift
@@ -0,0 +1,187 @@
+/**
+ * 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
+
+/// Posts to the OneSignal /notifications REST endpoint to send sample push payloads
+final class NotificationSender {
+ static let shared = NotificationSender()
+ private init() {}
+
+ enum SendError: Error, LocalizedError {
+ case noSubscriptionId
+ case requestFailed(String)
+ case transient(String)
+
+ var errorDescription: String? {
+ switch self {
+ case .noSubscriptionId: return "No push subscription"
+ case .requestFailed(let msg): return msg
+ case .transient(let msg): return msg
+ }
+ }
+ }
+
+ private let endpoint = URL(string: "https://onesignal.com/api/v1/notifications")!
+ private let maxAttempts = 5
+
+ func sendNotification(
+ _ type: NotificationType,
+ appId: String,
+ subscriptionId: String,
+ completion: @escaping (Result) -> Void
+ ) {
+ var headings = "Simple Notification"
+ var contents = "This is a simple push notification"
+ var extra: [String: Any] = [:]
+
+ switch type {
+ case .simple:
+ break
+ case .withImage:
+ headings = "Image Notification"
+ contents = "This notification includes an image"
+ let url = "https://media.onesignal.com/automated_push_templates/ratings_template.png"
+ extra["big_picture"] = url
+ extra["ios_attachments"] = ["image": url]
+ case .withSound:
+ headings = "Sound Notification"
+ contents = "This notification plays a custom sound"
+ extra["ios_sound"] = "vine_boom.wav"
+ }
+
+ post(
+ appId: appId,
+ subscriptionId: subscriptionId,
+ heading: headings,
+ content: contents,
+ extra: extra,
+ attempt: 1,
+ completion: completion
+ )
+ }
+
+ func sendCustomNotification(
+ title: String,
+ body: String,
+ appId: String,
+ subscriptionId: String,
+ completion: @escaping (Result) -> Void
+ ) {
+ post(
+ appId: appId,
+ subscriptionId: subscriptionId,
+ heading: title,
+ content: body,
+ extra: [:],
+ attempt: 1,
+ completion: completion
+ )
+ }
+
+ private func post(
+ appId: String,
+ subscriptionId: String,
+ heading: String,
+ content: String,
+ extra: [String: Any],
+ attempt: Int,
+ completion: @escaping (Result) -> Void
+ ) {
+ var payload: [String: Any] = [
+ "app_id": appId,
+ "include_subscription_ids": [subscriptionId],
+ "headings": ["en": heading],
+ "contents": ["en": content]
+ ]
+ payload.merge(extra) { _, new in new }
+
+ guard let body = try? JSONSerialization.data(withJSONObject: payload, options: []) else {
+ completion(.failure(SendError.requestFailed("Could not encode payload")))
+ return
+ }
+
+ var request = URLRequest(url: endpoint)
+ request.httpMethod = "POST"
+ request.httpBody = body
+ request.setValue("application/json", forHTTPHeaderField: "Content-Type")
+ request.setValue("application/vnd.onesignal.v1+json", forHTTPHeaderField: "Accept")
+
+ URLSession.shared.dataTask(with: request) { [weak self] data, response, error in
+ guard let self = self else { return }
+ if let error = error {
+ completion(.failure(SendError.requestFailed(error.localizedDescription)))
+ return
+ }
+ guard let http = response as? HTTPURLResponse else {
+ completion(.failure(SendError.requestFailed("Unexpected response")))
+ return
+ }
+ guard (200..<300).contains(http.statusCode) else {
+ let text = data.flatMap { String(data: $0, encoding: .utf8) } ?? "HTTP \(http.statusCode)"
+ completion(.failure(SendError.requestFailed(text)))
+ return
+ }
+
+ // Treat 200 with empty id / errors / zero recipients as a transient backend race
+ // (subscription not yet indexed) and retry with exponential backoff.
+ if let data = data,
+ let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
+ self.isTransientFailure(json) {
+ if attempt < self.maxAttempts {
+ let delay = UInt64(2_000_000_000) * UInt64(1 << (attempt - 1))
+ Task {
+ try? await Task.sleep(nanoseconds: delay)
+ self.post(
+ appId: appId,
+ subscriptionId: subscriptionId,
+ heading: heading,
+ content: content,
+ extra: extra,
+ attempt: attempt + 1,
+ completion: completion
+ )
+ }
+ return
+ }
+ completion(.failure(SendError.transient(String(describing: json))))
+ return
+ }
+
+ completion(.success(()))
+ }.resume()
+ }
+
+ private func isTransientFailure(_ json: [String: Any]) -> Bool {
+ let id = json["id"] as? String ?? ""
+ if id.isEmpty { return true }
+ if let recipients = json["recipients"] as? Int, recipients == 0 { return true }
+ if let errorsDict = json["errors"] as? [String: Any], !errorsDict.isEmpty { return true }
+ if let errorsArr = json["errors"] as? [Any], !errorsArr.isEmpty { return true }
+ return false
+ }
+}
diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Services/OneSignalService.swift b/examples/demo/App/Services/OneSignalService.swift
similarity index 51%
rename from iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Services/OneSignalService.swift
rename to examples/demo/App/Services/OneSignalService.swift
index 981fe99a9..598189ea0 100644
--- a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Services/OneSignalService.swift
+++ b/examples/demo/App/Services/OneSignalService.swift
@@ -28,108 +28,99 @@
import Foundation
import OneSignalFramework
-/// Service layer that wraps OneSignal SDK calls
+/// Thin wrapper that funnels demo calls through a single OneSignal entry point.
+/// Caching for state we restore across cold launches lives in `PreferencesService`.
final class OneSignalService {
- // MARK: - Singleton
-
static let shared = OneSignalService()
- private init() {}
+ private let prefs: PreferencesService
- // MARK: - App ID
+ private init(prefs: PreferencesService = .shared) {
+ self.prefs = prefs
+ }
- private let appIdKey = "OneSignalAppId"
- private let defaultAppId = "77e32082-ea27-42e3-a898-c72e141824ef"
+ // MARK: - App ID
- var appId: String {
- get {
- UserDefaults.standard.string(forKey: appIdKey) ?? defaultAppId
- }
- set {
- UserDefaults.standard.set(newValue, forKey: appIdKey)
- }
- }
+ /// Read once at init from `Secrets.plist` (or the hard-coded fallback) so
+ /// the running session uses a stable value even if the bundle changes.
+ let appId: String = SecretsConfig.appId
// MARK: - Initialization
+ /// Mirrors the Capacitor demo's `useOneSignal` init order: feed cached
+ /// consent into the SDK BEFORE `initialize`, then restore IAM-paused,
+ /// location-shared, and a previously-logged-in external user id once the
+ /// SDK is ready. Without this, toggles flip back to defaults on every
+ /// cold launch.
func initialize(launchOptions: [UIApplication.LaunchOptionsKey: Any]?) {
OneSignal.Debug.setLogLevel(.LL_VERBOSE)
+
+ OneSignal.setConsentRequired(prefs.getConsentRequired())
+ OneSignal.setConsentGiven(prefs.getConsentGiven())
+
OneSignal.initialize(appId, withLaunchOptions: launchOptions)
- OneSignal.Notifications.requestPermission()
- }
- // MARK: - Identity
+ OneSignal.InAppMessages.paused = prefs.getIamPaused()
+ OneSignal.Location.isShared = prefs.getLocationShared()
- var onesignalId: String? {
- OneSignal.User.onesignalId
+ if let storedExternalId = prefs.getExternalUserId() {
+ OneSignal.login(storedExternalId)
+ }
}
- var externalId: String? {
- OneSignal.User.externalId
- }
+ // MARK: - Identity
- // MARK: - Consent
+ var onesignalId: String? { OneSignal.User.onesignalId }
+ var externalId: String? { OneSignal.User.externalId }
- func setConsentRequired(_ required: Bool) {
- OneSignal.setConsentRequired(required)
- }
+ // MARK: - Consent
- func setConsentGiven(_ granted: Bool) {
- OneSignal.setConsentGiven(granted)
+ /// Read-through cache. `set` writes the value to `PreferencesService` and
+ /// forwards to the SDK so the next cold launch can restore it.
+ var consentRequired: Bool {
+ get { prefs.getConsentRequired() }
+ set {
+ prefs.setConsentRequired(newValue)
+ OneSignal.setConsentRequired(newValue)
+ }
}
- func revokeConsent() {
- // Must set consent as required first, then revoke it
- OneSignal.setConsentRequired(true)
- OneSignal.setConsentGiven(false)
+ var consentGiven: Bool {
+ get { prefs.getConsentGiven() }
+ set {
+ prefs.setConsentGiven(newValue)
+ OneSignal.setConsentGiven(newValue)
+ }
}
- // MARK: - User Management
+ // MARK: - User
func login(externalId: String) {
+ prefs.setExternalUserId(externalId)
OneSignal.login(externalId)
}
func logout() {
+ prefs.setExternalUserId(nil)
OneSignal.logout()
}
// MARK: - Aliases
- func addAlias(label: String, id: String) {
- OneSignal.User.addAlias(label: label, id: id)
- }
-
- func addAliases(_ aliases: [String: String]) {
- OneSignal.User.addAliases(aliases)
- }
-
- func removeAlias(_ label: String) {
- OneSignal.User.removeAlias(label)
- }
-
- func removeAliases(_ labels: [String]) {
- OneSignal.User.removeAliases(labels)
- }
+ func addAlias(label: String, id: String) { OneSignal.User.addAlias(label: label, id: id) }
+ func addAliases(_ aliases: [String: String]) { OneSignal.User.addAliases(aliases) }
+ func removeAlias(_ label: String) { OneSignal.User.removeAlias(label) }
+ func removeAliases(_ labels: [String]) { OneSignal.User.removeAliases(labels) }
// MARK: - Push Subscription
- var pushSubscriptionId: String? {
- OneSignal.User.pushSubscription.id
- }
-
- var isPushEnabled: Bool {
- OneSignal.User.pushSubscription.optedIn
- }
-
- func optInPush() {
- OneSignal.User.pushSubscription.optIn()
- }
+ var pushSubscriptionId: String? { OneSignal.User.pushSubscription.id }
+ var isPushEnabled: Bool { OneSignal.User.pushSubscription.optedIn }
+ var hasNotificationPermission: Bool { OneSignal.Notifications.permission }
- func optOutPush() {
- OneSignal.User.pushSubscription.optOut()
- }
+ func optInPush() { OneSignal.User.pushSubscription.optIn() }
+ func optOutPush() { OneSignal.User.pushSubscription.optOut() }
func requestPushPermission(completion: @escaping (Bool) -> Void) {
OneSignal.Notifications.requestPermission({ accepted in
@@ -139,65 +130,36 @@ final class OneSignalService {
// MARK: - Email
- func addEmail(_ email: String) {
- OneSignal.User.addEmail(email)
- }
-
- func removeEmail(_ email: String) {
- OneSignal.User.removeEmail(email)
- }
+ func addEmail(_ email: String) { OneSignal.User.addEmail(email) }
+ func removeEmail(_ email: String) { OneSignal.User.removeEmail(email) }
// MARK: - SMS
- func addSms(_ number: String) {
- OneSignal.User.addSms(number)
- }
-
- func removeSms(_ number: String) {
- OneSignal.User.removeSms(number)
- }
+ func addSms(_ number: String) { OneSignal.User.addSms(number) }
+ func removeSms(_ number: String) { OneSignal.User.removeSms(number) }
// MARK: - Tags
- func addTag(key: String, value: String) {
- OneSignal.User.addTag(key: key, value: value)
- }
-
- func addTags(_ tags: [String: String]) {
- OneSignal.User.addTags(tags)
- }
-
- func removeTag(_ key: String) {
- OneSignal.User.removeTag(key)
- }
-
- func removeTags(_ keys: [String]) {
- OneSignal.User.removeTags(keys)
- }
-
- func getTags() -> [String: String] {
- OneSignal.User.getTags()
- }
+ func addTag(key: String, value: String) { OneSignal.User.addTag(key: key, value: value) }
+ func addTags(_ tags: [String: String]) { OneSignal.User.addTags(tags) }
+ func removeTag(_ key: String) { OneSignal.User.removeTag(key) }
+ func removeTags(_ keys: [String]) { OneSignal.User.removeTags(keys) }
+ func getTags() -> [String: String] { OneSignal.User.getTags() }
// MARK: - Outcomes
- func sendOutcome(_ name: String) {
- OneSignal.Session.addOutcome(name)
- }
-
- func sendOutcome(_ name: String, value: NSNumber) {
- OneSignal.Session.addOutcome(name, value)
- }
-
- func sendUniqueOutcome(_ name: String) {
- OneSignal.Session.addUniqueOutcome(name)
- }
+ func sendOutcome(_ name: String) { OneSignal.Session.addOutcome(name) }
+ func sendOutcome(_ name: String, value: NSNumber) { OneSignal.Session.addOutcome(name, value) }
+ func sendUniqueOutcome(_ name: String) { OneSignal.Session.addUniqueOutcome(name) }
// MARK: - In-App Messages
var isInAppMessagesPaused: Bool {
- get { OneSignal.InAppMessages.paused }
- set { OneSignal.InAppMessages.paused = newValue }
+ get { prefs.getIamPaused() }
+ set {
+ prefs.setIamPaused(newValue)
+ OneSignal.InAppMessages.paused = newValue
+ }
}
func addTrigger(key: String, value: String) {
@@ -217,15 +179,17 @@ final class OneSignalService {
}
func clearTriggers() {
- // Remove all triggers by clearing the list
OneSignal.InAppMessages.clearTriggers()
}
// MARK: - Location
var isLocationShared: Bool {
- get { OneSignal.Location.isShared }
- set { OneSignal.Location.isShared = newValue }
+ get { prefs.getLocationShared() }
+ set {
+ prefs.setLocationShared(newValue)
+ OneSignal.Location.isShared = newValue
+ }
}
func requestLocationPermission() {
@@ -238,8 +202,10 @@ final class OneSignalService {
OneSignal.Notifications.clearAll()
}
- var hasNotificationPermission: Bool {
- OneSignal.Notifications.permission
+ // MARK: - Custom Events
+
+ func trackEvent(name: String, properties: [String: Any]?) {
+ OneSignal.User.trackEvent(name: name, properties: properties)
}
// MARK: - Observers
diff --git a/examples/demo/App/Services/PreferencesService.swift b/examples/demo/App/Services/PreferencesService.swift
new file mode 100644
index 000000000..b055224f7
--- /dev/null
+++ b/examples/demo/App/Services/PreferencesService.swift
@@ -0,0 +1,86 @@
+/**
+ * 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
+
+/// `UserDefaults`-backed cache for state the demo restores across cold launches:
+/// consent flags, IAM paused, location shared, and the last-logged-in external
+/// user id. Mirrors the Capacitor demo's `PreferencesService` so the iOS demo
+/// re-feeds these into the SDK during initialization.
+final class PreferencesService {
+
+ static let shared = PreferencesService()
+
+ private let defaults: UserDefaults
+
+ private init(defaults: UserDefaults = .standard) {
+ self.defaults = defaults
+ }
+
+ private enum Key {
+ static let consentRequired = "onesignal.demo.consentRequired"
+ static let consentGiven = "onesignal.demo.consentGiven"
+ static let iamPaused = "onesignal.demo.iamPaused"
+ static let locationShared = "onesignal.demo.locationShared"
+ static let externalUserId = "onesignal.demo.externalUserId"
+ }
+
+ // MARK: - Consent
+
+ func getConsentRequired() -> Bool { defaults.bool(forKey: Key.consentRequired) }
+ func setConsentRequired(_ value: Bool) { defaults.set(value, forKey: Key.consentRequired) }
+
+ func getConsentGiven() -> Bool { defaults.bool(forKey: Key.consentGiven) }
+ func setConsentGiven(_ value: Bool) { defaults.set(value, forKey: Key.consentGiven) }
+
+ // MARK: - In-App Messages
+
+ func getIamPaused() -> Bool { defaults.bool(forKey: Key.iamPaused) }
+ func setIamPaused(_ value: Bool) { defaults.set(value, forKey: Key.iamPaused) }
+
+ // MARK: - Location
+
+ func getLocationShared() -> Bool { defaults.bool(forKey: Key.locationShared) }
+ func setLocationShared(_ value: Bool) { defaults.set(value, forKey: Key.locationShared) }
+
+ // MARK: - External user id
+
+ func getExternalUserId() -> String? {
+ guard let value = defaults.string(forKey: Key.externalUserId), !value.isEmpty else {
+ return nil
+ }
+ return value
+ }
+
+ func setExternalUserId(_ value: String?) {
+ if let value = value, !value.isEmpty {
+ defaults.set(value, forKey: Key.externalUserId)
+ } else {
+ defaults.removeObject(forKey: Key.externalUserId)
+ }
+ }
+}
diff --git a/examples/demo/App/Services/SecretsConfig.swift b/examples/demo/App/Services/SecretsConfig.swift
new file mode 100644
index 000000000..51bfedeb5
--- /dev/null
+++ b/examples/demo/App/Services/SecretsConfig.swift
@@ -0,0 +1,71 @@
+/**
+ * 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
+
+/// Single source of truth for the demo's OneSignal credentials. Mirrors the
+/// Capacitor demo's `.env` (`ONESIGNAL_APP_ID`, `ONESIGNAL_API_KEY`) but reads
+/// values from `Secrets.plist` bundled with the app — the iOS-idiomatic
+/// equivalent. Both keys are optional; consumers fall back to platform defaults
+/// when missing.
+enum SecretsConfig {
+
+ /// Hard-coded fallback when `ONESIGNAL_APP_ID` is missing or empty in
+ /// `Secrets.plist`. Matches the default in `sdk-shared/demo/build.md`.
+ static let defaultAppId = "77e32082-ea27-42e3-a898-c72e141824ef"
+
+ /// Resolved OneSignal App ID. Reads `ONESIGNAL_APP_ID` from `Secrets.plist`
+ /// and falls back to `defaultAppId` when missing or empty.
+ static var appId: String {
+ string(forKey: "ONESIGNAL_APP_ID") ?? defaultAppId
+ }
+
+ /// Resolved REST API key, if any. Required for Live Activity update/end.
+ static var apiKey: String? { string(forKey: "ONESIGNAL_API_KEY") }
+
+ /// Convenience used by the Live Activity section to disable update/end
+ /// buttons when no key is configured.
+ static var hasApiKey: Bool { apiKey != nil }
+
+ private static let cache: [String: Any] = {
+ guard
+ let url = Bundle.main.url(forResource: "Secrets", withExtension: "plist"),
+ let data = try? Data(contentsOf: url),
+ let plist = try? PropertyListSerialization.propertyList(
+ from: data, options: [], format: nil
+ ) as? [String: Any]
+ else {
+ return [:]
+ }
+ return plist
+ }()
+
+ private static func string(forKey key: String) -> String? {
+ guard let value = cache[key] as? String, !value.isEmpty else { return nil }
+ return value
+ }
+}
diff --git a/examples/demo/App/Services/TooltipService.swift b/examples/demo/App/Services/TooltipService.swift
new file mode 100644
index 000000000..b568e1b29
--- /dev/null
+++ b/examples/demo/App/Services/TooltipService.swift
@@ -0,0 +1,169 @@
+/**
+ * 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
+
+/// Loads tooltip content shared with the other OneSignal demo apps
+final class TooltipService {
+ static let shared = TooltipService()
+
+ private static let remoteURL = URL(
+ string: "https://raw.githubusercontent.com/OneSignal/sdk-shared/main/demo/tooltip_content.json"
+ )!
+
+ private var cache: [String: TooltipData] = [:]
+ private var loaded = false
+
+ private init() {
+ cache = TooltipService.bundledFallback()
+ }
+
+ func loadIfNeeded() {
+ guard !loaded else { return }
+ loaded = true
+
+ Task.detached { [weak self] in
+ guard let self = self else { return }
+ guard let (data, response) = try? await URLSession.shared.data(from: TooltipService.remoteURL),
+ let http = response as? HTTPURLResponse,
+ (200..<300).contains(http.statusCode),
+ let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
+ return
+ }
+ var parsed: [String: TooltipData] = [:]
+ for (key, value) in json {
+ guard let dict = value as? [String: Any],
+ let title = dict["title"] as? String,
+ let description = dict["description"] as? String else {
+ continue
+ }
+ let options: [TooltipOption]?
+ if let rawOptions = dict["options"] as? [[String: Any]] {
+ options = rawOptions.compactMap { entry -> TooltipOption? in
+ guard let name = entry["name"] as? String,
+ let optDescription = entry["description"] as? String else { return nil }
+ return TooltipOption(name: name, description: optDescription)
+ }
+ } else {
+ options = nil
+ }
+ parsed[key] = TooltipData(title: title, description: description, options: options)
+ }
+ await MainActor.run {
+ if !parsed.isEmpty {
+ self.cache = parsed
+ }
+ }
+ }
+ }
+
+ func tooltip(for key: String) -> TooltipData? {
+ cache[key]
+ }
+
+ /// Minimal fallback content (keys match the sdk-shared tooltip JSON) so info icons
+ /// still work without network. `app` and `user` are demo-only and not in sdk-shared.
+ private static func bundledFallback() -> [String: TooltipData] {
+ [
+ "app": TooltipData(
+ title: "App",
+ description: "Your OneSignal App ID and consent settings.",
+ options: nil
+ ),
+ "user": TooltipData(
+ title: "User",
+ description: "External User Id is your own identifier for the current user. Login/logout to associate the device with a user.",
+ options: nil
+ ),
+ "push": TooltipData(
+ title: "Push Subscription",
+ description: "The push subscription for this device. Enables push notifications, in-app messages, and Live Activities.",
+ options: nil
+ ),
+ "sendPushNotification": TooltipData(
+ title: "Send Push Notification",
+ description: "Test push notifications by sending them to this device via the OneSignal REST API.",
+ options: nil
+ ),
+ "inAppMessaging": TooltipData(
+ title: "In-App Messaging",
+ description: "Display targeted messages inside your app. Pause IAM display while testing.",
+ options: nil
+ ),
+ "sendInAppMessage": TooltipData(
+ title: "Send In-App Message",
+ description: "Adds an iam_type trigger that your dashboard IAM rules can listen for.",
+ options: nil
+ ),
+ "aliases": TooltipData(
+ title: "Aliases",
+ description: "Custom label/id pairs that let you reference users by your own identifiers.",
+ options: nil
+ ),
+ "emails": TooltipData(
+ title: "Email Subscriptions",
+ description: "Email addresses associated with this user.",
+ options: nil
+ ),
+ "sms": TooltipData(
+ title: "SMS Subscriptions",
+ description: "Phone numbers associated with this user.",
+ options: nil
+ ),
+ "tags": TooltipData(
+ title: "Tags",
+ description: "Key-value string pairs attached to the user for segmentation and personalization.",
+ options: nil
+ ),
+ "outcomes": TooltipData(
+ title: "Outcomes",
+ description: "Track user actions attributed to push notifications.",
+ options: nil
+ ),
+ "triggers": TooltipData(
+ title: "Triggers",
+ description: "Device-local key-value pairs that control when in-app messages display.",
+ options: nil
+ ),
+ "customEvents": TooltipData(
+ title: "Custom Events",
+ description: "Send custom events with optional properties to trigger Journeys.",
+ options: nil
+ ),
+ "location": TooltipData(
+ title: "Location",
+ description: "Share device location for location-based segmentation.",
+ options: nil
+ ),
+ "liveActivities": TooltipData(
+ title: "Live Activities",
+ description: "Display real-time updates on the iOS Lock Screen and Dynamic Island.",
+ options: nil
+ )
+ ]
+ }
+}
diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Services/UserFetchService.swift b/examples/demo/App/Services/UserFetchService.swift
similarity index 55%
rename from iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Services/UserFetchService.swift
rename to examples/demo/App/Services/UserFetchService.swift
index eee835141..836559eaa 100644
--- a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Services/UserFetchService.swift
+++ b/examples/demo/App/Services/UserFetchService.swift
@@ -27,93 +27,66 @@
import Foundation
-/// Service for fetching user data from the OneSignal REST API.
-/// No API key is required for this public endpoint.
+/// Reads the OneSignal /users API to hydrate aliases / tags / channels in the demo
final class UserFetchService {
-
static let shared = UserFetchService()
-
private init() {}
- /// Fetch user data by OneSignal ID. No auth header required.
func fetchUser(appId: String, onesignalId: String) async -> UserData? {
let urlString = "https://api.onesignal.com/apps/\(appId)/users/by/onesignal_id/\(onesignalId)"
guard let url = URL(string: urlString) else { return nil }
var request = URLRequest(url: url)
request.httpMethod = "GET"
- request.timeoutInterval = 15
do {
let (data, response) = try await URLSession.shared.data(for: request)
-
- guard let httpResponse = response as? HTTPURLResponse,
- httpResponse.statusCode == 200 else {
- print("[UserFetchService] Non-200 response")
+ guard let http = response as? HTTPURLResponse, (200..<300).contains(http.statusCode) else {
return nil
}
-
guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] else {
return nil
}
-
- return parseUserData(json)
+ return parse(json)
} catch {
- print("[UserFetchService] Fetch failed: \(error.localizedDescription)")
return nil
}
}
- // MARK: - Private
+ private func parse(_ json: [String: Any]) -> UserData {
+ let identity = json["identity"] as? [String: Any] ?? [:]
+ let properties = json["properties"] as? [String: Any] ?? [:]
+ let subscriptions = json["subscriptions"] as? [[String: Any]] ?? []
- private func parseUserData(_ json: [String: Any]) -> UserData {
- // Parse identity (aliases)
var aliases: [String: String] = [:]
- var externalId: String?
-
- if let identity = json["identity"] as? [String: Any] {
- for (key, value) in identity {
- if key == "external_id" {
- externalId = value as? String
- } else if key == "onesignal_id" {
- // Skip onesignal_id from aliases display
- continue
- } else if let strValue = value as? String {
- aliases[key] = strValue
- }
+ for (key, value) in identity {
+ guard key != "external_id", key != "onesignal_id" else { continue }
+ if let stringValue = value as? String {
+ aliases[key] = stringValue
}
}
- // Parse tags from properties
var tags: [String: String] = [:]
- if let properties = json["properties"] as? [String: Any],
- let tagsDict = properties["tags"] as? [String: Any] {
- for (key, value) in tagsDict {
- if let strValue = value as? String {
- tags[key] = strValue
- } else {
- tags[key] = "\(value)"
+ if let rawTags = properties["tags"] as? [String: Any] {
+ for (key, value) in rawTags {
+ if let stringValue = value as? String {
+ tags[key] = stringValue
}
}
}
- // Parse subscriptions for emails and SMS
var emails: [String] = []
var smsNumbers: [String] = []
-
- if let subscriptions = json["subscriptions"] as? [[String: Any]] {
- for sub in subscriptions {
- guard let type = sub["type"] as? String,
- let token = sub["token"] as? String else { continue }
-
- if type == "Email" {
- emails.append(token)
- } else if type == "SMS" {
- smsNumbers.append(token)
- }
- }
+ for sub in subscriptions {
+ let type = sub["type"] as? String ?? ""
+ let token = sub["token"] as? String ?? ""
+ guard !token.isEmpty else { continue }
+ if type == "Email" { emails.append(token) }
+ if type == "SMS" { smsNumbers.append(token) }
}
+ let externalId = identity["external_id"] as? String
+
return UserData(
aliases: aliases,
tags: tags,
diff --git a/examples/demo/App/ViewModels/OneSignalViewModel.swift b/examples/demo/App/ViewModels/OneSignalViewModel.swift
new file mode 100644
index 000000000..dfa23553a
--- /dev/null
+++ b/examples/demo/App/ViewModels/OneSignalViewModel.swift
@@ -0,0 +1,473 @@
+/**
+ * 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 Combine
+import OneSignalFramework
+
+/// ViewModel that backs every section of the demo
+@MainActor
+final class OneSignalViewModel: ObservableObject {
+
+ // MARK: - App / Consent
+
+ @Published var appId: String
+ @Published var consentRequired: Bool = false
+ @Published var consentGiven: Bool = false
+
+ // MARK: - Identity
+
+ @Published var externalUserId: String?
+ @Published var aliases: [KeyValueItem] = []
+
+ // MARK: - Push
+
+ @Published var pushSubscriptionId: String?
+ @Published var isPushEnabled: Bool = false
+ @Published var hasNotificationPermission: Bool = false
+
+ // MARK: - Channels
+
+ @Published var emails: [String] = []
+ @Published var smsNumbers: [String] = []
+
+ // MARK: - Tags / Triggers
+
+ @Published var tags: [KeyValueItem] = []
+ @Published var triggers: [KeyValueItem] = []
+
+ // MARK: - In-App / Location
+
+ @Published var isInAppMessagesPaused: Bool = false
+ @Published var isLocationShared: Bool = false
+
+ // MARK: - UI State
+
+ @Published var isLoading: Bool = false
+
+ @Published var activeTooltip: TooltipData?
+
+ // MARK: - Computed
+
+ var isLoggedIn: Bool {
+ guard let id = externalUserId else { return false }
+ return !id.isEmpty
+ }
+
+ var loginButtonTitle: String { isLoggedIn ? "SWITCH USER" : "LOGIN USER" }
+
+ // MARK: - Private
+
+ private let service: OneSignalService
+ private let prefs: PreferencesService
+ private var observers = Observers()
+
+ /// Monotonically incremented on every `fetchUserDataFromApi` call. The
+ /// value captured at entry guards the post-await write so a slow fetch
+ /// for an old `onesignalId` cannot overwrite a newer fetch's results.
+ private var requestSequence: UInt64 = 0
+
+ // MARK: - Init
+
+ init(service: OneSignalService = .shared, prefs: PreferencesService = .shared) {
+ self.service = service
+ self.prefs = prefs
+ self.appId = service.appId
+ self.consentRequired = service.consentRequired
+ self.consentGiven = service.consentGiven
+ self.externalUserId = service.externalId ?? prefs.getExternalUserId()
+ self.hasNotificationPermission = service.hasNotificationPermission
+ refreshState()
+ setupObservers()
+
+ TooltipService.shared.loadIfNeeded()
+
+ if service.onesignalId != nil {
+ Task { await fetchUserDataFromApi() }
+ }
+ }
+
+ // MARK: - State sync
+
+ func refreshState() {
+ pushSubscriptionId = service.pushSubscriptionId
+ isPushEnabled = service.isPushEnabled
+ isInAppMessagesPaused = service.isInAppMessagesPaused
+ isLocationShared = service.isLocationShared
+ hasNotificationPermission = service.hasNotificationPermission
+ externalUserId = service.externalId
+
+ let sdkTags = service.getTags()
+ tags = sdkTags.map { KeyValueItem(key: $0.key, value: $0.value) }
+ }
+
+ func fetchUserDataFromApi() async {
+ guard let onesignalId = service.onesignalId else { return }
+ requestSequence &+= 1
+ let captured = requestSequence
+ isLoading = true
+
+ let userData = await UserFetchService.shared.fetchUser(appId: appId, onesignalId: onesignalId)
+
+ // Drop the result if a newer fetch has started while this one was in flight.
+ guard captured == requestSequence else { return }
+
+ if let userData = userData {
+ aliases = userData.aliases.map { KeyValueItem(key: $0.key, value: $0.value) }
+ tags = userData.tags.map { KeyValueItem(key: $0.key, value: $0.value) }
+ emails = userData.emails
+ smsNumbers = userData.smsNumbers
+ if let extId = userData.externalId, !extId.isEmpty {
+ externalUserId = extId
+ }
+ }
+ isLoading = false
+ }
+
+ // MARK: - Consent
+
+ func setConsentRequired(_ required: Bool) {
+ consentRequired = required
+ service.consentRequired = required
+ if !required {
+ consentGiven = true
+ service.consentGiven = true
+ }
+ }
+
+ func setConsentGiven(_ granted: Bool) {
+ consentGiven = granted
+ service.consentGiven = granted
+ }
+
+ // MARK: - User
+
+ func login(externalId: String) {
+ let trimmed = externalId.trimmingCharacters(in: .whitespacesAndNewlines)
+ guard !trimmed.isEmpty else { return }
+ isLoading = true
+ service.login(externalId: trimmed)
+ externalUserId = trimmed
+ clearUserData()
+ }
+
+ func logout() {
+ service.logout()
+ externalUserId = nil
+ clearUserData()
+ }
+
+ private func clearUserData() {
+ aliases.removeAll()
+ emails.removeAll()
+ smsNumbers.removeAll()
+ tags.removeAll()
+ triggers.removeAll()
+ }
+
+ // MARK: - Aliases
+
+ func addAlias(label: String, id: String) {
+ service.addAlias(label: label, id: id)
+ aliases.removeAll { $0.key == label }
+ aliases.append(KeyValueItem(key: label, value: id))
+ }
+
+ func addAliases(_ pairs: [(String, String)]) {
+ let dict = Dictionary(pairs, uniquingKeysWith: { _, last in last })
+ service.addAliases(dict)
+ for (key, value) in pairs {
+ aliases.removeAll { $0.key == key }
+ aliases.append(KeyValueItem(key: key, value: value))
+ }
+ }
+
+ func removeAlias(_ item: KeyValueItem) {
+ service.removeAlias(item.key)
+ aliases.removeAll { $0.id == item.id }
+ }
+
+ // MARK: - Push
+
+ func setPushEnabled(_ enabled: Bool) {
+ if enabled {
+ service.optInPush()
+ isPushEnabled = true
+ } else {
+ service.optOutPush()
+ isPushEnabled = false
+ }
+ }
+
+ func promptPushPermission() {
+ service.requestPushPermission { [weak self] accepted in
+ Task { @MainActor in
+ self?.hasNotificationPermission = accepted
+ self?.isPushEnabled = accepted
+ }
+ }
+ }
+
+ // MARK: - Email
+
+ func addEmail(_ email: String) {
+ service.addEmail(email)
+ if !emails.contains(email) { emails.append(email) }
+ }
+
+ func removeEmail(_ email: String) {
+ service.removeEmail(email)
+ emails.removeAll { $0 == email }
+ }
+
+ // MARK: - SMS
+
+ func addSms(_ number: String) {
+ service.addSms(number)
+ if !smsNumbers.contains(number) { smsNumbers.append(number) }
+ }
+
+ func removeSms(_ number: String) {
+ service.removeSms(number)
+ smsNumbers.removeAll { $0 == number }
+ }
+
+ // MARK: - Tags
+
+ func addTag(key: String, value: String) {
+ service.addTag(key: key, value: value)
+ tags.removeAll { $0.key == key }
+ tags.append(KeyValueItem(key: key, value: value))
+ }
+
+ func addTags(_ pairs: [(String, String)]) {
+ let dict = Dictionary(pairs, uniquingKeysWith: { _, last in last })
+ service.addTags(dict)
+ for (key, value) in pairs {
+ tags.removeAll { $0.key == key }
+ tags.append(KeyValueItem(key: key, value: value))
+ }
+ }
+
+ func removeTag(_ item: KeyValueItem) {
+ service.removeTag(item.key)
+ tags.removeAll { $0.id == item.id }
+ }
+
+ func removeSelectedTags(_ keys: [String]) {
+ guard !keys.isEmpty else { return }
+ service.removeTags(keys)
+ tags.removeAll { keys.contains($0.key) }
+ }
+
+ // MARK: - Outcomes
+
+ func sendOutcome(_ name: String) {
+ service.sendOutcome(name)
+ print("[OneSignal] Outcome sent: \(name)")
+ }
+
+ func sendUniqueOutcome(_ name: String) {
+ service.sendUniqueOutcome(name)
+ print("[OneSignal] Unique outcome sent: \(name)")
+ }
+
+ func sendOutcome(_ name: String, value: Double) {
+ service.sendOutcome(name, value: NSNumber(value: value))
+ print("[OneSignal] Outcome sent: \(name) = \(value)")
+ }
+
+ // MARK: - In-App
+
+ func setIamPaused(_ paused: Bool) {
+ isInAppMessagesPaused = paused
+ service.isInAppMessagesPaused = paused
+ }
+
+ func sendIamTrigger(_ type: InAppMessageType) {
+ service.addTrigger(key: "iam_type", value: type.triggerValue)
+ triggers.removeAll { $0.key == "iam_type" }
+ triggers.append(KeyValueItem(key: "iam_type", value: type.triggerValue))
+ }
+
+ // MARK: - Triggers
+
+ func addTrigger(key: String, value: String) {
+ service.addTrigger(key: key, value: value)
+ triggers.removeAll { $0.key == key }
+ triggers.append(KeyValueItem(key: key, value: value))
+ }
+
+ func addTriggers(_ pairs: [(String, String)]) {
+ let dict = Dictionary(pairs, uniquingKeysWith: { _, last in last })
+ service.addTriggers(dict)
+ for (key, value) in pairs {
+ triggers.removeAll { $0.key == key }
+ triggers.append(KeyValueItem(key: key, value: value))
+ }
+ }
+
+ func removeTrigger(_ item: KeyValueItem) {
+ service.removeTrigger(item.key)
+ triggers.removeAll { $0.id == item.id }
+ }
+
+ func removeSelectedTriggers(_ keys: [String]) {
+ guard !keys.isEmpty else { return }
+ service.removeTriggers(keys)
+ triggers.removeAll { keys.contains($0.key) }
+ }
+
+ func clearTriggers() {
+ service.clearTriggers()
+ triggers.removeAll()
+ }
+
+ // MARK: - Custom Events
+
+ func trackEvent(name: String, properties: [String: Any]?) {
+ service.trackEvent(name: name, properties: properties)
+ print("[OneSignal] Event tracked: \(name)")
+ }
+
+ // MARK: - Location
+
+ func setLocationShared(_ shared: Bool) {
+ isLocationShared = shared
+ service.isLocationShared = shared
+ }
+
+ func promptLocation() {
+ service.requestLocationPermission()
+ }
+
+ func checkLocationShared() -> Bool {
+ let shared = service.isLocationShared
+ print("[OneSignal] Location shared: \(shared)")
+ return shared
+ }
+
+ // MARK: - Notifications
+
+ func clearAllNotifications() {
+ service.clearAllNotifications()
+ }
+
+ func sendNotification(_ type: NotificationType) {
+ guard let subscriptionId = service.pushSubscriptionId, !subscriptionId.isEmpty else { return }
+ NotificationSender.shared.sendNotification(type, appId: appId, subscriptionId: subscriptionId) { _ in }
+ }
+
+ func sendCustomNotification(title: String, body: String) {
+ guard let subscriptionId = service.pushSubscriptionId, !subscriptionId.isEmpty else { return }
+ NotificationSender.shared.sendCustomNotification(title: title, body: body, appId: appId, subscriptionId: subscriptionId) { _ in }
+ }
+
+ // MARK: - Live Activities
+
+ func startLiveActivity(activityId: String, orderNumber: String, status: LiveActivityStatus) {
+ let trimmedId = activityId.trimmingCharacters(in: .whitespacesAndNewlines)
+ guard !trimmedId.isEmpty else { return }
+ if #available(iOS 16.1, *) {
+ LiveActivityController.start(
+ activityId: trimmedId,
+ orderNumber: orderNumber,
+ status: status
+ )
+ }
+ }
+
+ func updateLiveActivity(activityId: String, status: LiveActivityStatus) {
+ let trimmedId = activityId.trimmingCharacters(in: .whitespacesAndNewlines)
+ guard !trimmedId.isEmpty else { return }
+ Task {
+ _ = await LiveActivityController.update(
+ appId: appId,
+ activityId: trimmedId,
+ status: status
+ )
+ }
+ }
+
+ func endLiveActivity(activityId: String) {
+ let trimmedId = activityId.trimmingCharacters(in: .whitespacesAndNewlines)
+ guard !trimmedId.isEmpty else { return }
+ Task {
+ _ = await LiveActivityController.end(
+ appId: appId,
+ activityId: trimmedId
+ )
+ }
+ }
+
+ // MARK: - Tooltips
+
+ func showTooltip(for key: String) {
+ if let tooltip = TooltipService.shared.tooltip(for: key) {
+ activeTooltip = tooltip
+ }
+ }
+
+ func dismissTooltip() {
+ activeTooltip = nil
+ }
+
+ // MARK: - Observers
+
+ private func setupObservers() {
+ observers.viewModel = self
+ service.addPushSubscriptionObserver(observers)
+ service.addUserObserver(observers)
+ service.addPermissionObserver(observers)
+ }
+}
+
+// MARK: - Observer Bridge
+
+private final class Observers: NSObject, OSPushSubscriptionObserver, OSUserStateObserver, OSNotificationPermissionObserver {
+ weak var viewModel: OneSignalViewModel?
+
+ func onPushSubscriptionDidChange(state: OSPushSubscriptionChangedState) {
+ Task { @MainActor in
+ viewModel?.pushSubscriptionId = state.current.id
+ viewModel?.isPushEnabled = state.current.optedIn
+ }
+ }
+
+ func onUserStateDidChange(state: OSUserChangedState) {
+ Task { @MainActor in
+ await viewModel?.fetchUserDataFromApi()
+ }
+ }
+
+ func onNotificationPermissionDidChange(_ permission: Bool) {
+ Task { @MainActor in
+ viewModel?.hasNotificationPermission = permission
+ viewModel?.isPushEnabled = OneSignal.User.pushSubscription.optedIn
+ }
+ }
+}
diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Components/GuidanceBanner.swift b/examples/demo/App/ViewModels/ToastPresenter.swift
similarity index 57%
rename from iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Components/GuidanceBanner.swift
rename to examples/demo/App/ViewModels/ToastPresenter.swift
index a2f73edf0..2b69f2efd 100644
--- a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Components/GuidanceBanner.swift
+++ b/examples/demo/App/ViewModels/ToastPresenter.swift
@@ -25,30 +25,32 @@
* THE SOFTWARE.
*/
-import SwiftUI
+import Foundation
+import Combine
-/// A guidance banner that instructs users to add their own App ID.
-/// Matches the Android demo's cream/yellow info banner.
-struct GuidanceBanner: View {
- var body: some View {
- VStack(alignment: .leading, spacing: 6) {
- Text("Add your own App ID, then rebuild to fully test all functionality.")
- .font(.system(size: 14))
- .foregroundColor(.primary)
+/// UI-layer toast presenter per sdk-shared/demo/build.md Prompt 7.6.
+/// Feedback messages are owned by the UI layer (injected as an
+/// `@EnvironmentObject`), never by `OneSignalViewModel`. Replace-on-show:
+/// dismisses any visible toast and resets the [toastDurationMs] timer on
+/// every call.
+@MainActor
+final class ToastPresenter: ObservableObject {
- Link("Get your keys at onesignal.com", destination: URL(string: "https://onesignal.com")!)
- .font(.system(size: 14, weight: .medium))
- .foregroundColor(.accentColor)
+ static let toastDurationMs: UInt64 = 3_000
+
+ @Published var message: String?
+
+ private var dismissTask: Task?
+
+ func show(_ message: String) {
+ dismissTask?.cancel()
+ self.message = message
+ let target = message
+ dismissTask = Task { [weak self] in
+ try? await Task.sleep(nanoseconds: ToastPresenter.toastDurationMs * 1_000_000)
+ guard !Task.isCancelled else { return }
+ guard let self else { return }
+ if self.message == target { self.message = nil }
}
- .frame(maxWidth: .infinity, alignment: .leading)
- .padding(12)
- .background(Color(red: 1.0, green: 0.98, blue: 0.90))
- .cornerRadius(12)
}
}
-
-#Preview {
- GuidanceBanner()
- .padding()
- .background(Color(.systemGroupedBackground))
-}
diff --git a/examples/demo/App/Views/Components/ActionButton.swift b/examples/demo/App/Views/Components/ActionButton.swift
new file mode 100644
index 000000000..a5a301642
--- /dev/null
+++ b/examples/demo/App/Views/Components/ActionButton.swift
@@ -0,0 +1,110 @@
+/**
+ * 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 SwiftUI
+
+/// Visual treatment of an action button. The spec defines exactly two variants:
+/// the filled primary, and the outlined ("destructive" / secondary) button.
+enum ActionButtonStyle {
+ case filled
+ case outline
+}
+
+/// Standard wide button used by sections.
+///
+/// Matches the spec: full width, 48 tall, 8 corner radius, semibold label,
+/// optional 18pt leading icon with 8pt gap before the label.
+struct ActionButton: View {
+ let title: String
+ let style: ActionButtonStyle
+ let icon: Image?
+ let isDisabled: Bool
+ let accessibilityID: String
+ let action: () -> Void
+
+ init(
+ _ title: String,
+ style: ActionButtonStyle = .filled,
+ icon: Image? = nil,
+ isDisabled: Bool = false,
+ accessibilityID: String,
+ action: @escaping () -> Void
+ ) {
+ self.title = title
+ self.style = style
+ self.icon = icon
+ self.isDisabled = isDisabled
+ self.accessibilityID = accessibilityID
+ self.action = action
+ }
+
+ var body: some View {
+ Button(action: action) {
+ HStack(spacing: 8) {
+ if let icon = icon {
+ icon
+ .font(.system(size: OS.Layout.infoIconSize, weight: .semibold))
+ }
+ Text(title)
+ .font(OS.Font.bodyMedium.weight(.semibold))
+ .lineLimit(1)
+ }
+ .frame(maxWidth: .infinity)
+ .frame(height: OS.Layout.buttonHeight)
+ .foregroundColor(foregroundColor)
+ .background(backgroundColor)
+ .clipShape(RoundedRectangle(cornerRadius: OS.Radius.button))
+ .overlay(border)
+ }
+ .buttonStyle(.plain)
+ .disabled(isDisabled)
+ .opacity(isDisabled ? 0.5 : 1)
+ .accessibilityIdentifier(accessibilityID)
+ }
+
+ private var backgroundColor: Color {
+ switch style {
+ case .filled: return OS.Color.primary
+ case .outline: return .clear
+ }
+ }
+
+ private var foregroundColor: Color {
+ switch style {
+ case .filled: return .white
+ case .outline: return OS.Color.primary
+ }
+ }
+
+ @ViewBuilder
+ private var border: some View {
+ if case .outline = style {
+ RoundedRectangle(cornerRadius: OS.Radius.button)
+ .strokeBorder(OS.Color.primary, lineWidth: 1)
+ }
+ }
+}
diff --git a/examples/demo/App/Views/Components/AddItemDialog.swift b/examples/demo/App/Views/Components/AddItemDialog.swift
new file mode 100644
index 000000000..f6b099578
--- /dev/null
+++ b/examples/demo/App/Views/Components/AddItemDialog.swift
@@ -0,0 +1,88 @@
+/**
+ * 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 SwiftUI
+
+/// Reusable centered dialog for adding items with one or two text fields.
+struct AddItemDialog: View {
+ let itemType: AddItemType
+ let onAdd: (String, String) -> Void
+ let onCancel: () -> Void
+
+ @State private var keyText: String = ""
+ @State private var valueText: String = ""
+
+ var body: some View {
+ OSDialog(
+ title: itemType.title,
+ confirmLabel: itemType.confirmLabel,
+ isConfirmEnabled: isValid,
+ confirmAccessibilityID: itemType.confirmButtonID,
+ cancelAccessibilityID: itemType.cancelButtonID,
+ onConfirm: {
+ onAdd(
+ keyText.trimmingCharacters(in: .whitespaces),
+ valueText.trimmingCharacters(in: .whitespaces)
+ )
+ },
+ onCancel: onCancel
+ ) {
+ VStack(spacing: 12) {
+ if itemType.requiresKeyValue {
+ HStack(spacing: 8) {
+ OSTextField(
+ placeholder: itemType.keyPlaceholder,
+ text: $keyText,
+ accessibilityID: itemType.keyInputID
+ )
+ OSTextField(
+ placeholder: itemType.valuePlaceholder,
+ text: $valueText,
+ keyboardType: itemType.keyboardType,
+ accessibilityID: itemType.valueInputID
+ )
+ }
+ } else {
+ OSTextField(
+ placeholder: itemType.valuePlaceholder,
+ text: $valueText,
+ keyboardType: itemType.keyboardType,
+ accessibilityID: itemType.valueInputID
+ )
+ }
+ }
+ }
+ }
+
+ private var isValid: Bool {
+ if itemType.requiresKeyValue {
+ return !keyText.trimmingCharacters(in: .whitespaces).isEmpty &&
+ !valueText.trimmingCharacters(in: .whitespaces).isEmpty
+ }
+ return !valueText.trimmingCharacters(in: .whitespaces).isEmpty
+ }
+}
diff --git a/examples/demo/App/Views/Components/CustomNotificationDialog.swift b/examples/demo/App/Views/Components/CustomNotificationDialog.swift
new file mode 100644
index 000000000..61df5e318
--- /dev/null
+++ b/examples/demo/App/Views/Components/CustomNotificationDialog.swift
@@ -0,0 +1,72 @@
+/**
+ * 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 SwiftUI
+
+/// Centered dialog for composing a custom push notification (title + body).
+struct CustomNotificationDialog: View {
+ let onSend: (String, String) -> Void
+ let onCancel: () -> Void
+
+ @State private var titleText: String = ""
+ @State private var bodyText: String = ""
+
+ var body: some View {
+ OSDialog(
+ title: "Custom Notification",
+ confirmLabel: "Send",
+ isConfirmEnabled: isValid,
+ confirmAccessibilityID: "custom_notification_send_button",
+ cancelAccessibilityID: "custom_notification_cancel_button",
+ onConfirm: {
+ onSend(
+ titleText.trimmingCharacters(in: .whitespaces),
+ bodyText.trimmingCharacters(in: .whitespaces)
+ )
+ },
+ onCancel: onCancel
+ ) {
+ VStack(spacing: 12) {
+ OSTextField(
+ placeholder: "Title",
+ text: $titleText,
+ accessibilityID: "custom_notification_title_input"
+ )
+ OSTextEditor(
+ placeholder: "Body",
+ text: $bodyText,
+ accessibilityID: "custom_notification_body_input"
+ )
+ }
+ }
+ }
+
+ private var isValid: Bool {
+ !titleText.trimmingCharacters(in: .whitespaces).isEmpty &&
+ !bodyText.trimmingCharacters(in: .whitespaces).isEmpty
+ }
+}
diff --git a/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Sections/AppInfoSection.swift b/examples/demo/App/Views/Components/KeyValueRow.swift
similarity index 56%
rename from OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Sections/AppInfoSection.swift
rename to examples/demo/App/Views/Components/KeyValueRow.swift
index 4bab9ebc2..dd4c23a16 100644
--- a/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Sections/AppInfoSection.swift
+++ b/examples/demo/App/Views/Components/KeyValueRow.swift
@@ -27,43 +27,33 @@
import SwiftUI
-/// Section displaying app information and consent management
-struct AppInfoSection: View {
- @EnvironmentObject var viewModel: OneSignalViewModel
+/// Single label/value horizontal row used inside the App / User / Push info cards.
+/// Label is 14pt, value is 12pt (monospaced when a value is an ID).
+struct InfoRow: View {
+ let label: String
+ let value: String
+ let valueAccessibilityID: String?
+ let isMonospaced: Bool
- var body: some View {
- Section {
- // App ID
- VStack(alignment: .leading, spacing: 4) {
- Text("App ID")
- .font(.caption)
- .foregroundColor(.secondary)
- Text(viewModel.appId)
- .font(.system(.footnote, design: .monospaced))
- .textSelection(.enabled)
- }
- .padding(.vertical, 4)
-
- // Revoke Consent Button
- Button(role: .destructive) {
- viewModel.revokeConsent()
- } label: {
- HStack {
- Spacer()
- Text("Revoke Consent")
- .fontWeight(.medium)
- Spacer()
- }
- }
- } header: {
- Text("App")
- }
+ init(label: String, value: String, valueAccessibilityID: String? = nil, isMonospaced: Bool = false) {
+ self.label = label
+ self.value = value
+ self.valueAccessibilityID = valueAccessibilityID
+ self.isMonospaced = isMonospaced
}
-}
-#Preview {
- List {
- AppInfoSection()
+ var body: some View {
+ HStack(alignment: .center, spacing: 12) {
+ Text(label)
+ .font(OS.Font.bodyMedium)
+ .foregroundColor(OS.Color.bodyText)
+ Spacer(minLength: 0)
+ Text(value)
+ .font(isMonospaced ? OS.Font.mono12 : OS.Font.bodySmall)
+ .foregroundColor(OS.Color.bodyText)
+ .lineLimit(1)
+ .truncationMode(.middle)
+ .accessibilityIdentifier(valueAccessibilityID ?? "")
+ }
}
- .environmentObject(OneSignalViewModel())
}
diff --git a/examples/demo/App/Views/Components/ListWidgets.swift b/examples/demo/App/Views/Components/ListWidgets.swift
new file mode 100644
index 000000000..eeb907f10
--- /dev/null
+++ b/examples/demo/App/Views/Components/ListWidgets.swift
@@ -0,0 +1,241 @@
+/**
+ * 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 SwiftUI
+
+// MARK: - Shared list-card chrome
+
+private struct ListCardEmpty: View {
+ let text: String
+ let accessibilityID: String
+
+ var body: some View {
+ Text(text)
+ .font(OS.Font.bodyMedium)
+ .foregroundColor(OS.Color.grey600)
+ .frame(maxWidth: .infinity, alignment: .center)
+ .padding(.vertical, OS.Spacing.cardPadding)
+ .accessibilityIdentifier(accessibilityID)
+ .osCard()
+ }
+}
+
+private struct ItemDivider: View {
+ var body: some View {
+ Rectangle()
+ .fill(OS.Color.divider)
+ .frame(height: OS.Layout.dividerHeight)
+ }
+}
+
+private struct DeleteButton: View {
+ let accessibilityID: String
+ let action: () -> Void
+
+ var body: some View {
+ Button(action: action) {
+ Image(systemName: "xmark")
+ .font(.system(size: OS.Layout.infoIconSize, weight: .semibold))
+ .foregroundColor(OS.Color.primary)
+ .frame(width: 28, height: 28)
+ }
+ .buttonStyle(.plain)
+ .accessibilityIdentifier(accessibilityID)
+ }
+}
+
+private struct MoreLink: View {
+ let hidden: Int
+ let onExpand: () -> Void
+ let accessibilityID: String
+
+ var body: some View {
+ Button(action: onExpand) {
+ Text("\(hidden) more")
+ .font(OS.Font.bodyMedium.weight(.medium))
+ .foregroundColor(OS.Color.primary)
+ .frame(maxWidth: .infinity)
+ .padding(.vertical, 4)
+ }
+ .buttonStyle(.plain)
+ .accessibilityIdentifier(accessibilityID)
+ }
+}
+
+// MARK: - Stacked (key-value) list
+
+/// List of paired items. Each row shows a 14pt key on top and a 12pt grey value below,
+/// with an optional close button to remove. Lists longer than `maxVisible` collapse
+/// into a "N more" link.
+struct PairList: View {
+ let items: [KeyValueItem]
+ let emptyText: String
+ let sectionKey: String
+ let onRemove: ((String) -> Void)?
+ let maxVisible: Int
+
+ @State private var expanded = false
+
+ init(
+ items: [KeyValueItem],
+ emptyText: String,
+ sectionKey: String,
+ onRemove: ((String) -> Void)? = nil,
+ maxVisible: Int = OS.Layout.listMaxVisible
+ ) {
+ self.items = items
+ self.emptyText = emptyText
+ self.sectionKey = sectionKey
+ self.onRemove = onRemove
+ self.maxVisible = maxVisible
+ }
+
+ private var visibleItems: [KeyValueItem] {
+ expanded ? items : Array(items.prefix(maxVisible))
+ }
+
+ private var hiddenCount: Int { max(0, items.count - maxVisible) }
+
+ var body: some View {
+ if items.isEmpty {
+ ListCardEmpty(text: emptyText, accessibilityID: "\(sectionKey)_empty")
+ } else {
+ VStack(spacing: 0) {
+ ForEach(visibleItems.indices, id: \.self) { index in
+ let item = visibleItems[index]
+ HStack(alignment: .center, spacing: 8) {
+ VStack(alignment: .leading, spacing: 2) {
+ Text(item.key)
+ .font(OS.Font.bodyMedium)
+ .foregroundColor(OS.Color.bodyText)
+ .accessibilityIdentifier("\(sectionKey)_pair_key_\(item.key)")
+ Text(item.value)
+ .font(OS.Font.bodySmall)
+ .foregroundColor(OS.Color.grey600)
+ .accessibilityIdentifier("\(sectionKey)_pair_value_\(item.key)")
+ }
+ Spacer(minLength: 0)
+ if let onRemove = onRemove {
+ DeleteButton(
+ accessibilityID: "\(sectionKey)_remove_\(item.key)",
+ action: { onRemove(item.key) }
+ )
+ }
+ }
+ .padding(.vertical, 4)
+ .padding(.horizontal, 4)
+
+ if index < visibleItems.count - 1 {
+ ItemDivider()
+ }
+ }
+ if !expanded && hiddenCount > 0 {
+ ItemDivider()
+ MoreLink(
+ hidden: hiddenCount,
+ onExpand: { expanded = true },
+ accessibilityID: "\(sectionKey)_more"
+ )
+ }
+ }
+ .osCard()
+ }
+ }
+}
+
+// MARK: - Unstacked (single-string) list
+
+/// List of plain string items (emails, sms numbers). Single 14pt line per row.
+struct SingleList: View {
+ let items: [String]
+ let emptyText: String
+ let sectionKey: String
+ let onRemove: ((String) -> Void)?
+ let maxVisible: Int
+
+ @State private var expanded = false
+
+ init(
+ items: [String],
+ emptyText: String,
+ sectionKey: String,
+ onRemove: ((String) -> Void)? = nil,
+ maxVisible: Int = OS.Layout.listMaxVisible
+ ) {
+ self.items = items
+ self.emptyText = emptyText
+ self.sectionKey = sectionKey
+ self.onRemove = onRemove
+ self.maxVisible = maxVisible
+ }
+
+ private var visibleItems: [String] {
+ expanded ? items : Array(items.prefix(maxVisible))
+ }
+
+ private var hiddenCount: Int { max(0, items.count - maxVisible) }
+
+ var body: some View {
+ if items.isEmpty {
+ ListCardEmpty(text: emptyText, accessibilityID: "\(sectionKey)_empty")
+ } else {
+ VStack(spacing: 0) {
+ ForEach(visibleItems.indices, id: \.self) { index in
+ let item = visibleItems[index]
+ HStack(alignment: .center, spacing: 8) {
+ Text(item)
+ .font(OS.Font.bodyMedium)
+ .foregroundColor(OS.Color.bodyText)
+ .accessibilityIdentifier("\(sectionKey)_value_\(item)")
+ Spacer(minLength: 0)
+ if let onRemove = onRemove {
+ DeleteButton(
+ accessibilityID: "\(sectionKey)_remove_\(item)",
+ action: { onRemove(item) }
+ )
+ }
+ }
+ .padding(.vertical, 4)
+ .padding(.horizontal, 4)
+
+ if index < visibleItems.count - 1 {
+ ItemDivider()
+ }
+ }
+ if !expanded && hiddenCount > 0 {
+ ItemDivider()
+ MoreLink(
+ hidden: hiddenCount,
+ onExpand: { expanded = true },
+ accessibilityID: "\(sectionKey)_more"
+ )
+ }
+ }
+ .osCard()
+ }
+ }
+}
diff --git a/examples/demo/App/Views/Components/MultiPairInputDialog.swift b/examples/demo/App/Views/Components/MultiPairInputDialog.swift
new file mode 100644
index 000000000..b785b7c3c
--- /dev/null
+++ b/examples/demo/App/Views/Components/MultiPairInputDialog.swift
@@ -0,0 +1,156 @@
+/**
+ * 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 SwiftUI
+
+/// Centered dialog that adds multiple key/value pairs at once
+/// (Add Multiple Aliases / Tags / Triggers).
+struct MultiPairInputDialog: View {
+ let type: MultiAddItemType
+ let onAdd: ([(String, String)]) -> Void
+ let onCancel: () -> Void
+
+ @State private var rows: [Row] = [Row()]
+ @State private var measuredContentHeight: CGFloat = 0
+
+ struct Row: Identifiable {
+ let id = UUID()
+ var key: String = ""
+ var value: String = ""
+ }
+
+ /// Upper bound for the rows ScrollView. Keeps the dialog from growing
+ /// past a sensible point on small devices; content scrolls beyond this.
+ private let maxRowsHeight: CGFloat = 320
+
+ var body: some View {
+ OSDialog(
+ title: type.rawValue,
+ confirmLabel: "Add All",
+ isConfirmEnabled: isValid,
+ confirmAccessibilityID: "multipair_confirm_button",
+ cancelAccessibilityID: "multipair_cancel_button",
+ onConfirm: {
+ let pairs = rows.compactMap { row -> (String, String)? in
+ let key = row.key.trimmingCharacters(in: .whitespaces)
+ let value = row.value.trimmingCharacters(in: .whitespaces)
+ guard !key.isEmpty, !value.isEmpty else { return nil }
+ return (key, value)
+ }
+ onAdd(pairs)
+ },
+ onCancel: onCancel
+ ) {
+ // Always wrap in a ScrollView so the SwiftUI view hierarchy stays
+ // stable as the keyboard appears. The earlier `ViewThatFits` swap
+ // (VStack ↔ ScrollView) tore down the focused TextField when the
+ // keyboard shrunk the safe area, dropping focus mid-typing — that
+ // broke Appium input on the second row's value field. The frame
+ // is sized to the measured rows height (clamped) so the dialog
+ // still grows/shrinks with row count instead of always claiming
+ // the maximum.
+ ScrollView {
+ rowsContent
+ .background(
+ GeometryReader { proxy in
+ Color.clear.preference(
+ key: RowsHeightPreferenceKey.self,
+ value: proxy.size.height
+ )
+ }
+ )
+ }
+ .frame(height: min(max(measuredContentHeight, 1), maxRowsHeight))
+ .onPreferenceChange(RowsHeightPreferenceKey.self) { measuredContentHeight = $0 }
+ }
+ }
+
+ private var rowsContent: some View {
+ VStack(spacing: 12) {
+ ForEach(rows.indices, id: \.self) { index in
+ VStack(spacing: 8) {
+ HStack(spacing: 8) {
+ OSTextField(
+ placeholder: type.keyPlaceholder,
+ text: $rows[index].key,
+ accessibilityID: "multipair_key_\(index)"
+ )
+ OSTextField(
+ placeholder: type.valuePlaceholder,
+ text: $rows[index].value,
+ accessibilityID: "multipair_value_\(index)"
+ )
+ if rows.count > 1 {
+ Button {
+ rows.remove(at: index)
+ } label: {
+ Image(systemName: "xmark")
+ .font(.system(size: OS.Layout.infoIconSize, weight: .semibold))
+ .foregroundColor(OS.Color.primary)
+ .frame(width: 28, height: 28)
+ }
+ .buttonStyle(.plain)
+ .accessibilityIdentifier("multipair_remove_row_\(index)")
+ }
+ }
+ if index < rows.count - 1 {
+ Rectangle()
+ .fill(OS.Color.divider)
+ .frame(height: OS.Layout.dividerHeight)
+ }
+ }
+ }
+
+ Button {
+ rows.append(Row())
+ } label: {
+ Text("+ Add")
+ .font(OS.Font.bodyMedium.weight(.bold))
+ .foregroundColor(OS.Color.primary)
+ .frame(maxWidth: .infinity, alignment: .center)
+ .padding(.vertical, 4)
+ }
+ .buttonStyle(.plain)
+ .accessibilityIdentifier("multipair_add_row_button")
+ }
+ }
+
+ private var isValid: Bool {
+ guard !rows.isEmpty else { return false }
+ return rows.allSatisfy { row in
+ !row.key.trimmingCharacters(in: .whitespaces).isEmpty &&
+ !row.value.trimmingCharacters(in: .whitespaces).isEmpty
+ }
+ }
+}
+
+private struct RowsHeightPreferenceKey: PreferenceKey {
+ static var defaultValue: CGFloat = 0
+ static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
+ value = max(value, nextValue())
+ }
+}
diff --git a/examples/demo/App/Views/Components/OSDialog.swift b/examples/demo/App/Views/Components/OSDialog.swift
new file mode 100644
index 000000000..c591dcb6a
--- /dev/null
+++ b/examples/demo/App/Views/Components/OSDialog.swift
@@ -0,0 +1,255 @@
+/**
+ * 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 SwiftUI
+
+// MARK: - Dialog container
+
+/// Standard dialog body. Wraps the supplied content in a vertical stack with
+/// 24pt outer padding, places a 24pt-weight-regular title above it, and pins
+/// an action row (Cancel / confirm) to the bottom.
+struct OSDialog: View {
+ let title: String
+ let confirmLabel: String
+ let isConfirmEnabled: Bool
+ let confirmAccessibilityID: String
+ let cancelAccessibilityID: String
+ let onConfirm: () -> Void
+ let onCancel: () -> Void
+ @ViewBuilder let content: () -> Content
+
+ init(
+ title: String,
+ confirmLabel: String = "Save",
+ isConfirmEnabled: Bool = true,
+ confirmAccessibilityID: String = "dialog_confirm_button",
+ cancelAccessibilityID: String = "dialog_cancel_button",
+ onConfirm: @escaping () -> Void,
+ onCancel: @escaping () -> Void,
+ @ViewBuilder content: @escaping () -> Content
+ ) {
+ self.title = title
+ self.confirmLabel = confirmLabel
+ self.isConfirmEnabled = isConfirmEnabled
+ self.confirmAccessibilityID = confirmAccessibilityID
+ self.cancelAccessibilityID = cancelAccessibilityID
+ self.onConfirm = onConfirm
+ self.onCancel = onCancel
+ self.content = content
+ }
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 16) {
+ Text(title)
+ .font(.system(size: 24, weight: .regular))
+ .foregroundColor(OS.Color.bodyText)
+
+ content()
+
+ HStack(spacing: 8) {
+ Spacer(minLength: 0)
+ OSDialogActionButton(
+ title: "Cancel",
+ accessibilityID: cancelAccessibilityID,
+ isEnabled: true,
+ action: onCancel
+ )
+ OSDialogActionButton(
+ title: confirmLabel,
+ accessibilityID: confirmAccessibilityID,
+ isEnabled: isConfirmEnabled,
+ action: onConfirm
+ )
+ }
+ .padding(.top, 8)
+ }
+ .padding(24)
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .background(OS.Color.cardBackground)
+ }
+}
+
+// MARK: - Dialog action button
+
+/// Text-style action button for dialog footers.
+/// Spec: 14pt, weight medium/500, color osPrimary, 12 horizontal / 8 vertical padding.
+struct OSDialogActionButton: View {
+ let title: String
+ let accessibilityID: String
+ let isEnabled: Bool
+ let action: () -> Void
+
+ var body: some View {
+ Button(action: action) {
+ Text(title)
+ .font(OS.Font.bodyMedium.weight(.medium))
+ .foregroundColor(isEnabled ? OS.Color.primary : OS.Color.grey500)
+ .padding(.horizontal, 12)
+ .padding(.vertical, 8)
+ }
+ .buttonStyle(.plain)
+ .disabled(!isEnabled)
+ .accessibilityIdentifier(accessibilityID)
+ }
+}
+
+// MARK: - Dialog text inputs
+
+/// Bordered text field used inside dialogs. Spec: 8 corner radius,
+/// 12 horizontal / 14 vertical content padding, 1px solid grey700 border,
+/// 2px solid osPrimary on focus.
+struct OSTextField: View {
+ let placeholder: String
+ @Binding var text: String
+ var keyboardType: UIKeyboardType = .default
+ var autocorrect: Bool = false
+ var capitalization: TextInputAutocapitalization = .never
+ var accessibilityID: String
+
+ @FocusState private var focused: Bool
+
+ var body: some View {
+ TextField(placeholder, text: $text)
+ .font(OS.Font.bodyMedium)
+ .foregroundColor(OS.Color.bodyText)
+ .keyboardType(keyboardType)
+ .textInputAutocapitalization(capitalization)
+ .autocorrectionDisabled(!autocorrect)
+ .padding(.horizontal, 12)
+ .padding(.vertical, 14)
+ .focused($focused)
+ .background(
+ RoundedRectangle(cornerRadius: OS.Radius.input)
+ .strokeBorder(
+ focused ? OS.Color.primary : OS.Color.grey700,
+ lineWidth: focused ? 2 : 1
+ )
+ )
+ .accessibilityIdentifier(accessibilityID)
+ }
+}
+
+/// Bordered multi-line text editor mirroring `OSTextField`'s visual.
+struct OSTextEditor: View {
+ let placeholder: String
+ @Binding var text: String
+ var minHeight: CGFloat = 90
+ var accessibilityID: String
+
+ @FocusState private var focused: Bool
+
+ var body: some View {
+ ZStack(alignment: .topLeading) {
+ if text.isEmpty {
+ Text(placeholder)
+ .font(OS.Font.bodyMedium)
+ .foregroundColor(OS.Color.grey600)
+ .padding(.horizontal, 16)
+ .padding(.vertical, 18)
+ .allowsHitTesting(false)
+ }
+
+ TextEditor(text: $text)
+ .font(OS.Font.bodyMedium)
+ .foregroundColor(OS.Color.bodyText)
+ .scrollContentBackground(.hidden)
+ .padding(.horizontal, 8)
+ .padding(.vertical, 8)
+ .focused($focused)
+ .frame(minHeight: minHeight)
+ .accessibilityIdentifier(accessibilityID)
+ }
+ .background(
+ RoundedRectangle(cornerRadius: OS.Radius.input)
+ .strokeBorder(
+ focused ? OS.Color.primary : OS.Color.grey700,
+ lineWidth: focused ? 2 : 1
+ )
+ )
+ }
+}
+
+// MARK: - Centered dialog presentation
+
+extension View {
+ /// Presents `content` as a centered modal dialog over the entire screen,
+ /// matching the styles.md "Dialogs" spec: 54% black backdrop, 16pt
+ /// horizontal / 24pt vertical insets, 28pt corner radius, white card.
+ /// Tapping the backdrop dismisses.
+ ///
+ /// Uses `.fullScreenCover` rather than `.overlay` so the backdrop and
+ /// dialog are anchored to the window — sections can attach this modifier
+ /// without the overlay being clipped to the section's frame inside
+ /// `ScrollView`. The cover's UIHostingController background is forced
+ /// clear via `ClearBackgroundView` so the dialog's own backdrop is what
+ /// the user sees (iOS 16.4+ has `presentationBackground(.clear)`; this
+ /// works on the demo's 16.0 deployment target).
+ func osCenteredDialog(
+ isPresented: Binding,
+ @ViewBuilder content: @escaping () -> DialogContent
+ ) -> some View {
+ modifier(OSCenteredDialogModifier(isPresented: isPresented, dialog: content))
+ }
+}
+
+private struct OSCenteredDialogModifier: ViewModifier {
+ @Binding var isPresented: Bool
+ @ViewBuilder var dialog: () -> DialogContent
+
+ func body(content: Content) -> some View {
+ content.fullScreenCover(isPresented: $isPresented) {
+ ZStack {
+ OS.Color.backdrop
+ .ignoresSafeArea()
+ .contentShape(Rectangle())
+ .onTapGesture { isPresented = false }
+
+ dialog()
+ .clipShape(RoundedRectangle(cornerRadius: OS.Radius.modal))
+ .padding(.horizontal, 16)
+ .padding(.vertical, 24)
+ }
+ .background(ClearBackgroundView())
+ }
+ .transaction { $0.disablesAnimations = true }
+ }
+}
+
+/// UIKit bridge that walks up to the `UIHostingController`'s view and clears
+/// its background so the SwiftUI `fullScreenCover` is see-through. Required on
+/// iOS < 16.4 where `presentationBackground(.clear)` is unavailable.
+private struct ClearBackgroundView: UIViewRepresentable {
+ func makeUIView(context: Context) -> UIView {
+ let view = UIView()
+ DispatchQueue.main.async {
+ view.superview?.superview?.backgroundColor = .clear
+ }
+ return view
+ }
+
+ func updateUIView(_ uiView: UIView, context: Context) {}
+}
diff --git a/examples/demo/App/Views/Components/OutcomeDialog.swift b/examples/demo/App/Views/Components/OutcomeDialog.swift
new file mode 100644
index 000000000..23493d36c
--- /dev/null
+++ b/examples/demo/App/Views/Components/OutcomeDialog.swift
@@ -0,0 +1,112 @@
+/**
+ * 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 SwiftUI
+
+/// Centered dialog for sending an outcome (normal, unique, or with value).
+struct OutcomeDialog: View {
+ let onSend: (String, OutcomeMode, Double?) -> Void
+ let onCancel: () -> Void
+
+ @State private var mode: OutcomeMode = .normal
+ @State private var name: String = ""
+ @State private var valueText: String = ""
+
+ var body: some View {
+ OSDialog(
+ title: "Send Outcome",
+ confirmLabel: "Send",
+ isConfirmEnabled: isValid,
+ confirmAccessibilityID: "outcome_send_button",
+ cancelAccessibilityID: "outcome_cancel_button",
+ onConfirm: {
+ let trimmed = name.trimmingCharacters(in: .whitespaces)
+ let value: Double? = mode == .value ? Double(valueText) : nil
+ onSend(trimmed, mode, value)
+ },
+ onCancel: onCancel
+ ) {
+ VStack(spacing: 14) {
+ ForEach(OutcomeMode.allCases) { option in
+ OutcomeRadioRow(
+ title: option.rawValue,
+ isSelected: mode == option,
+ accessibilityID: "outcome_type_\(option.accessibilityKey)_radio",
+ onTap: { mode = option }
+ )
+ }
+
+ OSTextField(
+ placeholder: "Outcome Name",
+ text: $name,
+ accessibilityID: "outcome_name_input"
+ )
+
+ if mode == .value {
+ OSTextField(
+ placeholder: "Outcome Value",
+ text: $valueText,
+ keyboardType: .decimalPad,
+ accessibilityID: "outcome_value_input"
+ )
+ }
+ }
+ }
+ }
+
+ private var isValid: Bool {
+ let trimmedName = name.trimmingCharacters(in: .whitespaces)
+ guard !trimmedName.isEmpty else { return false }
+ if mode == .value {
+ return Double(valueText) != nil
+ }
+ return true
+ }
+}
+
+private struct OutcomeRadioRow: View {
+ let title: String
+ let isSelected: Bool
+ let accessibilityID: String
+ let onTap: () -> Void
+
+ var body: some View {
+ Button(action: onTap) {
+ HStack(alignment: .center, spacing: 12) {
+ Image(systemName: isSelected ? "largecircle.fill.circle" : "circle")
+ .font(.system(size: 24))
+ .foregroundColor(isSelected ? OS.Color.primary : OS.Color.grey700)
+ Text(title)
+ .font(OS.Font.bodyLarge)
+ .foregroundColor(OS.Color.bodyText)
+ Spacer(minLength: 0)
+ }
+ }
+ .buttonStyle(.plain)
+ .accessibilityIdentifier(accessibilityID)
+ }
+}
diff --git a/examples/demo/App/Views/Components/RemoveMultiDialog.swift b/examples/demo/App/Views/Components/RemoveMultiDialog.swift
new file mode 100644
index 000000000..53897fae5
--- /dev/null
+++ b/examples/demo/App/Views/Components/RemoveMultiDialog.swift
@@ -0,0 +1,142 @@
+/**
+ * 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 SwiftUI
+
+/// Centered dialog that lets the user pick multiple keys to remove
+/// (Remove Tags / Remove Triggers).
+struct RemoveMultiDialog: View {
+ let type: RemoveMultiItemType
+ let items: [KeyValueItem]
+ let onRemove: ([String]) -> Void
+ let onCancel: () -> Void
+
+ @State private var selected: Set = []
+ @State private var measuredContentHeight: CGFloat = 0
+
+ /// Upper bound for the rows ScrollView. Content scrolls beyond this.
+ private let maxRowsHeight: CGFloat = 320
+
+ var body: some View {
+ OSDialog(
+ title: type.rawValue,
+ confirmLabel: selected.isEmpty ? "Remove" : "Remove (\(selected.count))",
+ isConfirmEnabled: !selected.isEmpty,
+ confirmAccessibilityID: "multiselect_confirm_button",
+ cancelAccessibilityID: "multiselect_cancel_button",
+ onConfirm: { onRemove(Array(selected)) },
+ onCancel: onCancel
+ ) {
+ if items.isEmpty {
+ Text("Nothing to remove")
+ .font(OS.Font.bodyMedium)
+ .foregroundColor(OS.Color.grey600)
+ .frame(maxWidth: .infinity, alignment: .center)
+ .padding(.vertical, OS.Spacing.cardPadding)
+ .accessibilityIdentifier("remove_multi_empty")
+ } else {
+ // Always wrap in a ScrollView (single stable view tree), and
+ // size the frame to the measured content height so the dialog
+ // shrinks for short lists and scrolls past `maxRowsHeight`.
+ ScrollView {
+ rowsContent
+ .background(
+ GeometryReader { proxy in
+ Color.clear.preference(
+ key: RowsHeightPreferenceKey.self,
+ value: proxy.size.height
+ )
+ }
+ )
+ }
+ .frame(height: min(max(measuredContentHeight, 1), maxRowsHeight))
+ .onPreferenceChange(RowsHeightPreferenceKey.self) { measuredContentHeight = $0 }
+ }
+ }
+ }
+
+ private var rowsContent: some View {
+ VStack(spacing: 0) {
+ ForEach(items.indices, id: \.self) { index in
+ let item = items[index]
+ CheckboxRow(
+ item: item,
+ isChecked: selected.contains(item.key),
+ onToggle: { isChecked in
+ if isChecked {
+ selected.insert(item.key)
+ } else {
+ selected.remove(item.key)
+ }
+ }
+ )
+ if index < items.count - 1 {
+ Rectangle()
+ .fill(OS.Color.divider)
+ .frame(height: OS.Layout.dividerHeight)
+ }
+ }
+ }
+ }
+}
+
+private struct RowsHeightPreferenceKey: PreferenceKey {
+ static var defaultValue: CGFloat = 0
+ static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
+ value = max(value, nextValue())
+ }
+}
+
+private struct CheckboxRow: View {
+ let item: KeyValueItem
+ let isChecked: Bool
+ let onToggle: (Bool) -> Void
+
+ var body: some View {
+ Button {
+ onToggle(!isChecked)
+ } label: {
+ HStack(alignment: .center, spacing: 14) {
+ Image(systemName: isChecked ? "checkmark.square.fill" : "square")
+ .font(.system(size: 24))
+ .foregroundColor(isChecked ? OS.Color.primary : OS.Color.grey700)
+ VStack(alignment: .leading, spacing: 2) {
+ Text(item.key)
+ .font(OS.Font.bodyLarge)
+ .foregroundColor(OS.Color.bodyText)
+ Text(item.value)
+ .font(OS.Font.bodySmall)
+ .foregroundColor(OS.Color.grey600)
+ }
+ Spacer(minLength: 0)
+ }
+ .padding(.vertical, 10)
+ }
+ .buttonStyle(.plain)
+ .accessibilityIdentifier("remove_checkbox_\(item.key)")
+ }
+}
diff --git a/examples/demo/App/Views/Components/SectionCard.swift b/examples/demo/App/Views/Components/SectionCard.swift
new file mode 100644
index 000000000..bc307771b
--- /dev/null
+++ b/examples/demo/App/Views/Components/SectionCard.swift
@@ -0,0 +1,133 @@
+/**
+ * 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 SwiftUI
+
+/// Section container. Renders a section header (12pt bold uppercase, osGrey700,
+/// letter spacing 0.5) above a vertical stack of children. Per the design spec
+/// children supply their own card chrome — this view only owns the header.
+struct SectionCard: View {
+ let title: String
+ let sectionKey: String
+ let onInfoTap: (() -> Void)?
+ @ViewBuilder let content: () -> Content
+
+ init(
+ title: String,
+ sectionKey: String,
+ onInfoTap: (() -> Void)? = nil,
+ @ViewBuilder content: @escaping () -> Content
+ ) {
+ self.title = title
+ self.sectionKey = sectionKey
+ self.onInfoTap = onInfoTap
+ self.content = content
+ }
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: OS.Spacing.cardGap) {
+ HStack(alignment: .center, spacing: 0) {
+ Text(title.uppercased())
+ .font(OS.Font.bodySmall.weight(.bold))
+ .tracking(0.5)
+ .foregroundColor(OS.Color.grey700)
+ Spacer(minLength: 0)
+ if let onInfoTap = onInfoTap {
+ Button(action: onInfoTap) {
+ Image(systemName: "info.circle")
+ .font(.system(size: OS.Layout.infoIconSize))
+ .foregroundColor(OS.Color.grey500)
+ .frame(width: 32, height: 32)
+ }
+ .buttonStyle(.plain)
+ .padding(.trailing, -6)
+ .accessibilityIdentifier("\(sectionKey)_info_icon")
+ }
+ }
+
+ VStack(alignment: .leading, spacing: OS.Spacing.cardGap) {
+ content()
+ }
+ }
+ // SwiftUI does not promote a bare VStack to an accessibility element,
+ // so a plain `.accessibilityIdentifier` here is invisible to XCUITest /
+ // Appium queries (e.g. `scrollToEl('user_section')` would loop until it
+ // hits the scroll cap). `.contain` makes the container queryable while
+ // keeping every child (Texts, Buttons, toggles) individually accessible.
+ .accessibilityElement(children: .contain)
+ .accessibilityIdentifier("\(sectionKey)_section")
+ }
+}
+
+/// Generic value card used at the top of sections (App ID, Push ID, Status, etc.).
+/// Renders rows with a 14pt label and a 12pt value (monospace by default for IDs).
+struct ValueCard: View {
+ struct Row {
+ let label: String
+ let value: String
+ let valueAccessibilityID: String?
+ let monospaced: Bool
+
+ init(label: String, value: String, valueAccessibilityID: String? = nil, monospaced: Bool = false) {
+ self.label = label
+ self.value = value
+ self.valueAccessibilityID = valueAccessibilityID
+ self.monospaced = monospaced
+ }
+ }
+
+ let rows: [Row]
+
+ var body: some View {
+ VStack(spacing: 0) {
+ ForEach(rows.indices, id: \.self) { index in
+ let row = rows[index]
+ HStack(alignment: .center, spacing: 12) {
+ Text(row.label)
+ .font(OS.Font.bodyMedium)
+ .foregroundColor(OS.Color.bodyText)
+ Spacer(minLength: 0)
+ Text(row.value)
+ .font(row.monospaced ? OS.Font.mono12 : OS.Font.bodySmall)
+ .foregroundColor(OS.Color.bodyText)
+ .lineLimit(1)
+ .truncationMode(.middle)
+ .accessibilityIdentifier(row.valueAccessibilityID ?? "")
+ }
+ .padding(.vertical, 4)
+
+ if index < rows.count - 1 {
+ Rectangle()
+ .fill(OS.Color.divider)
+ .frame(height: OS.Layout.dividerHeight)
+ .padding(.vertical, 4)
+ }
+ }
+ }
+ .osCard()
+ }
+}
diff --git a/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Components/ToastView.swift b/examples/demo/App/Views/Components/ToastView.swift
similarity index 97%
rename from OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Components/ToastView.swift
rename to examples/demo/App/Views/Components/ToastView.swift
index eede3ce4d..9c3845699 100644
--- a/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Components/ToastView.swift
+++ b/examples/demo/App/Views/Components/ToastView.swift
@@ -40,6 +40,7 @@ struct ToastView: View {
.background(Color.black.opacity(0.8))
.cornerRadius(8)
.shadow(radius: 4)
+ .accessibilityIdentifier("snackbar_toast")
}
}
diff --git a/examples/demo/App/Views/Components/ToggleRow.swift b/examples/demo/App/Views/Components/ToggleRow.swift
new file mode 100644
index 000000000..59d1ceeba
--- /dev/null
+++ b/examples/demo/App/Views/Components/ToggleRow.swift
@@ -0,0 +1,74 @@
+/**
+ * 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 SwiftUI
+
+/// Toggle row inside a card. Label (14) + optional supporting subtitle (12 osGrey600)
+/// on the left, native switch on the right.
+struct ToggleRow: View {
+ let label: String
+ let description: String?
+ let isOn: Binding
+ let isDisabled: Bool
+ let accessibilityID: String
+
+ init(
+ label: String,
+ description: String? = nil,
+ isOn: Binding,
+ isDisabled: Bool = false,
+ accessibilityID: String
+ ) {
+ self.label = label
+ self.description = description
+ self.isOn = isOn
+ self.isDisabled = isDisabled
+ self.accessibilityID = accessibilityID
+ }
+
+ var body: some View {
+ HStack(alignment: .center, spacing: 12) {
+ VStack(alignment: .leading, spacing: 2) {
+ Text(label)
+ .font(OS.Font.bodyMedium)
+ .foregroundColor(OS.Color.bodyText)
+ if let description = description {
+ Text(description)
+ .font(OS.Font.bodySmall)
+ .foregroundColor(OS.Color.grey600)
+ }
+ }
+ Spacer(minLength: 0)
+ Toggle("", isOn: isOn)
+ .labelsHidden()
+ .tint(OS.Color.primary)
+ .disabled(isDisabled)
+ .accessibilityIdentifier(accessibilityID)
+ }
+ .osCard()
+ }
+}
diff --git a/examples/demo/App/Views/Components/TooltipDialog.swift b/examples/demo/App/Views/Components/TooltipDialog.swift
new file mode 100644
index 000000000..f9392eb0d
--- /dev/null
+++ b/examples/demo/App/Views/Components/TooltipDialog.swift
@@ -0,0 +1,101 @@
+/**
+ * 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 SwiftUI
+
+/// Tooltip dialog body shown when the user taps a section's info icon.
+/// Single OK action — no Cancel. Presented as a centered modal via
+/// `osCenteredDialog`, so this view renders only the card chrome.
+struct TooltipDialog: View {
+ let tooltip: TooltipData
+ let onClose: () -> Void
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 16) {
+ Text(tooltip.title)
+ .font(.system(size: 24, weight: .regular))
+ .foregroundColor(OS.Color.bodyText)
+ .accessibilityIdentifier("tooltip_title")
+
+ ViewThatFits(in: .vertical) {
+ bodyContent
+ ScrollView { bodyContent }
+ }
+
+ HStack {
+ Spacer()
+ OSDialogActionButton(
+ title: "OK",
+ accessibilityID: "tooltip_ok_button",
+ isEnabled: true,
+ action: onClose
+ )
+ }
+ .padding(.top, 8)
+ }
+ .padding(24)
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .background(OS.Color.cardBackground)
+ // `.contain` mirrors `SectionCard`: without it, the outer
+ // `.accessibilityIdentifier` would propagate down and overwrite each
+ // child's identifier (`tooltip_title`, `tooltip_description`,
+ // `tooltip_ok_button`), making them unreachable to XCUITest.
+ .accessibilityElement(children: .contain)
+ .accessibilityIdentifier("tooltip_dialog")
+ }
+
+ private var bodyContent: some View {
+ VStack(alignment: .leading, spacing: 12) {
+ Text(tooltip.description)
+ .font(OS.Font.bodyMedium)
+ .foregroundColor(OS.Color.bodyText)
+ .fixedSize(horizontal: false, vertical: true)
+ .accessibilityIdentifier("tooltip_description")
+
+ if let options = tooltip.options, !options.isEmpty {
+ Rectangle()
+ .fill(OS.Color.divider)
+ .frame(height: OS.Layout.dividerHeight)
+ .padding(.vertical, 4)
+
+ VStack(alignment: .leading, spacing: 12) {
+ ForEach(options, id: \.name) { option in
+ VStack(alignment: .leading, spacing: 4) {
+ Text(option.name)
+ .font(OS.Font.bodyMedium.weight(.semibold))
+ .foregroundColor(OS.Color.bodyText)
+ Text(option.description)
+ .font(OS.Font.bodyMedium)
+ .foregroundColor(OS.Color.grey600)
+ .fixedSize(horizontal: false, vertical: true)
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/examples/demo/App/Views/Components/TrackEventDialog.swift b/examples/demo/App/Views/Components/TrackEventDialog.swift
new file mode 100644
index 000000000..37d62161e
--- /dev/null
+++ b/examples/demo/App/Views/Components/TrackEventDialog.swift
@@ -0,0 +1,103 @@
+/**
+ * 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 SwiftUI
+
+/// Centered dialog that captures an event name plus an optional
+/// JSON properties payload.
+struct TrackEventDialog: View {
+ let onTrack: (String, [String: Any]?) -> Void
+ let onCancel: () -> Void
+
+ @State private var name: String = ""
+ @State private var propertiesText: String = ""
+ @State private var error: String?
+
+ var body: some View {
+ OSDialog(
+ title: "Track Event",
+ confirmLabel: "Track",
+ isConfirmEnabled: isValid,
+ confirmAccessibilityID: "event_track_button",
+ cancelAccessibilityID: "event_cancel_button",
+ onConfirm: submit,
+ onCancel: onCancel
+ ) {
+ VStack(alignment: .leading, spacing: 12) {
+ OSTextField(
+ placeholder: "Event Name",
+ text: $name,
+ accessibilityID: "event_name_input"
+ )
+
+ Text("Properties (JSON, optional)")
+ .font(OS.Font.bodySmall)
+ .foregroundColor(OS.Color.grey600)
+
+ OSTextEditor(
+ placeholder: "{ \"key\": \"value\" }",
+ text: $propertiesText,
+ minHeight: 120,
+ accessibilityID: "event_properties_input"
+ )
+
+ if let error = error {
+ Text(error)
+ .font(OS.Font.bodySmall)
+ .foregroundColor(OS.Color.primary)
+ .accessibilityIdentifier("event_properties_error")
+ }
+ }
+ }
+ }
+
+ private var isValid: Bool {
+ !name.trimmingCharacters(in: .whitespaces).isEmpty
+ }
+
+ private func submit() {
+ let trimmedName = name.trimmingCharacters(in: .whitespaces)
+ let trimmedProps = propertiesText.trimmingCharacters(in: .whitespaces)
+
+ if trimmedProps.isEmpty {
+ onTrack(trimmedName, nil)
+ return
+ }
+
+ guard let data = trimmedProps.data(using: .utf8),
+ let json = try? JSONSerialization.jsonObject(with: data) else {
+ error = "Properties must be valid JSON"
+ return
+ }
+ guard let dict = json as? [String: Any] else {
+ error = "Properties must be a JSON object"
+ return
+ }
+ error = nil
+ onTrack(trimmedName, dict)
+ }
+}
diff --git a/examples/demo/App/Views/ContentView.swift b/examples/demo/App/Views/ContentView.swift
new file mode 100644
index 000000000..cdc99be60
--- /dev/null
+++ b/examples/demo/App/Views/ContentView.swift
@@ -0,0 +1,122 @@
+/**
+ * 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 SwiftUI
+
+/// Root view composing every section in the same order as the Capacitor demo.
+struct ContentView: View {
+ @EnvironmentObject var viewModel: OneSignalViewModel
+ @EnvironmentObject var toast: ToastPresenter
+
+ var body: some View {
+ NavigationStack {
+ ScrollView {
+ VStack(alignment: .leading, spacing: OS.Spacing.sectionGap) {
+ AppSection()
+ UserSection()
+ PushSection()
+ SendPushSection()
+ InAppSection()
+ SendIamSection()
+ AliasesSection()
+ EmailsSection()
+ SmsSection()
+ TagsSection()
+ OutcomesSection()
+ TriggersSection()
+ CustomEventsSection()
+ LocationSection()
+ LiveActivitySection()
+ }
+ .padding(.horizontal, OS.Spacing.pagePadding)
+ .padding(.top, OS.Spacing.pagePadding)
+ .padding(.bottom, OS.Spacing.sectionGap)
+ }
+ // `main_scroll_view` is anchored to the SwiftUI `ScrollView` (not
+ // the inner `VStack`) so XCUITest exposes it as
+ // `XCUIElementTypeScrollView` with the visible viewport's rect.
+ // Anchoring on the inner `VStack` reported the full content rect
+ // (multiple screens tall), causing WDIO `swipe` to compute
+ // gesture coordinates outside the viewport — iOS clipped those
+ // to the visible region and the swipe registered as a tap on
+ // whatever button sat there (e.g. `send_sound_button`). The
+ // ScrollView identifier is read by `waitForAppReady` and by
+ // Android's `scrollIntoView` `scrollableElement` param.
+ .accessibilityIdentifier("main_scroll_view")
+ .background(OS.Color.lightBackground.ignoresSafeArea())
+ .navigationBarTitleDisplayMode(.inline)
+ .toolbarBackground(OS.Color.primary, for: .navigationBar)
+ .toolbarBackground(.visible, for: .navigationBar)
+ .toolbarColorScheme(.dark, for: .navigationBar)
+ .toolbar { toolbarContent }
+ }
+ .osCenteredDialog(
+ isPresented: Binding(
+ get: { viewModel.activeTooltip != nil },
+ set: { isPresented in if !isPresented { viewModel.dismissTooltip() } }
+ )
+ ) {
+ if let tooltip = viewModel.activeTooltip {
+ TooltipDialog(tooltip: tooltip, onClose: { viewModel.dismissTooltip() })
+ }
+ }
+ .toast(message: $toast.message)
+ // Auto-prompt for notification permission on first appear, matching the
+ // Capacitor / Flutter / React Native demos (which all prompt from their
+ // home screen's mount lifecycle). This races the OneSignal iOS-params
+ // response: the standard alert shows before the SDK can register for
+ // provisional authorization (which would otherwise silently grant
+ // permission and skip the prompt entirely).
+ .task {
+ viewModel.promptPushPermission()
+ }
+ }
+
+ @ToolbarContentBuilder
+ private var toolbarContent: some ToolbarContent {
+ ToolbarItem(placement: .principal) {
+ HStack(spacing: 6) {
+ Image("onesignal_logo")
+ .renderingMode(.template)
+ .resizable()
+ .aspectRatio(contentMode: .fit)
+ .frame(height: 22)
+ .foregroundColor(.white)
+ Text("iOS")
+ .font(OS.Font.bodyMedium)
+ .foregroundColor(.white)
+ }
+ .accessibilityIdentifier("brand_title")
+ }
+ }
+}
+
+#Preview {
+ ContentView()
+ .environmentObject(OneSignalViewModel())
+ .environmentObject(ToastPresenter())
+}
diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Sections/NotificationSection.swift b/examples/demo/App/Views/Sections/AliasesSection.swift
similarity index 52%
rename from iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Sections/NotificationSection.swift
rename to examples/demo/App/Views/Sections/AliasesSection.swift
index 0d15d5785..c6772a5be 100644
--- a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Sections/NotificationSection.swift
+++ b/examples/demo/App/Views/Sections/AliasesSection.swift
@@ -27,55 +27,49 @@
import SwiftUI
-/// Section for sending test push notifications (3 full-width buttons)
-struct SendPushSection: View {
+struct AliasesSection: View {
@EnvironmentObject var viewModel: OneSignalViewModel
+ @State private var addOpen = false
+ @State private var addMultipleOpen = false
var body: some View {
- VStack(spacing: 0) {
- SectionHeader(title: "Send Push Notification", tooltipKey: "sendPushNotification")
-
- SendPushButtons(
- onSimple: {
- viewModel.sendSimpleNotification()
- },
- onWithImage: {
- viewModel.sendNotificationWithImage()
- },
- onCustom: {
- viewModel.showingCustomNotificationSheet = true
- },
- onClearAll: {
- viewModel.clearAllNotifications()
- }
+ SectionCard(
+ title: "ALIASES",
+ sectionKey: "aliases",
+ onInfoTap: { viewModel.showTooltip(for: "aliases") }
+ ) {
+ PairList(
+ items: viewModel.aliases,
+ emptyText: "No aliases added",
+ sectionKey: "aliases"
)
- }
- }
-}
-
-/// Section for sending test in-app messages (4 full-width buttons with trailing icons)
-struct SendInAppSection: View {
- @EnvironmentObject var viewModel: OneSignalViewModel
-
- var body: some View {
- VStack(spacing: 0) {
- SectionHeader(title: "Send In-App Message", tooltipKey: "sendInAppMessage")
- SendInAppButtons { type in
- viewModel.sendTestInAppMessage(type)
+ ActionButton("ADD ALIAS", accessibilityID: "add_alias_button") {
+ addOpen = true
+ }
+ ActionButton("ADD MULTIPLE ALIASES", accessibilityID: "add_multiple_aliases_button") {
+ addMultipleOpen = true
}
}
- }
-}
-
-#Preview {
- ScrollView {
- VStack {
- SendPushSection()
- SendInAppSection()
+ .osCenteredDialog(isPresented: $addOpen) {
+ AddItemDialog(
+ itemType: .alias,
+ onAdd: { key, value in
+ viewModel.addAlias(label: key, id: value)
+ addOpen = false
+ },
+ onCancel: { addOpen = false }
+ )
+ }
+ .osCenteredDialog(isPresented: $addMultipleOpen) {
+ MultiPairInputDialog(
+ type: .aliases,
+ onAdd: { pairs in
+ viewModel.addAliases(pairs)
+ addMultipleOpen = false
+ },
+ onCancel: { addMultipleOpen = false }
+ )
}
- .padding()
}
- .background(Color(.systemGroupedBackground))
- .environmentObject(OneSignalViewModel())
}
diff --git a/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Sections/LocationSection.swift b/examples/demo/App/Views/Sections/AppSection.swift
similarity index 52%
rename from OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Sections/LocationSection.swift
rename to examples/demo/App/Views/Sections/AppSection.swift
index 2d89f9077..2b772ae91 100644
--- a/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Sections/LocationSection.swift
+++ b/examples/demo/App/Views/Sections/AppSection.swift
@@ -27,43 +27,42 @@
import SwiftUI
-/// Section for location sharing and permissions
-struct LocationSection: View {
+/// App ID display + consent toggles, mirroring the Capacitor AppSection
+struct AppSection: View {
@EnvironmentObject var viewModel: OneSignalViewModel
var body: some View {
- Section {
- Toggle(isOn: Binding(
- get: { viewModel.isLocationShared },
- set: { _ in viewModel.toggleLocationShared() }
- )) {
- VStack(alignment: .leading, spacing: 2) {
- Text("Location Shared")
- Text("Location will be shared from device")
- .font(.caption)
- .foregroundColor(.secondary)
- }
- }
+ SectionCard(title: "APP", sectionKey: "app") {
+ ValueCard(rows: [
+ ValueCard.Row(
+ label: "App ID",
+ value: viewModel.appId,
+ valueAccessibilityID: "app_id_value",
+ monospaced: true
+ )
+ ])
+
+ ToggleRow(
+ label: "Consent Required",
+ description: "Require consent before SDK processes data",
+ isOn: Binding(
+ get: { viewModel.consentRequired },
+ set: { viewModel.setConsentRequired($0) }
+ ),
+ accessibilityID: "consent_required_toggle"
+ )
- Button {
- viewModel.promptLocation()
- } label: {
- HStack {
- Spacer()
- Text("Prompt Location")
- .fontWeight(.medium)
- Spacer()
- }
+ if viewModel.consentRequired {
+ ToggleRow(
+ label: "Privacy Consent",
+ description: "Consent given for data collection",
+ isOn: Binding(
+ get: { viewModel.consentGiven },
+ set: { viewModel.setConsentGiven($0) }
+ ),
+ accessibilityID: "privacy_consent_toggle"
+ )
}
- } header: {
- Text("Location")
}
}
}
-
-#Preview {
- List {
- LocationSection()
- }
- .environmentObject(OneSignalViewModel())
-}
diff --git a/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Sections/TagsSection.swift b/examples/demo/App/Views/Sections/CustomEventsSection.swift
similarity index 62%
rename from OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Sections/TagsSection.swift
rename to examples/demo/App/Views/Sections/CustomEventsSection.swift
index 586169e30..01d2e742e 100644
--- a/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Sections/TagsSection.swift
+++ b/examples/demo/App/Views/Sections/CustomEventsSection.swift
@@ -27,41 +27,30 @@
import SwiftUI
-/// Section for managing user tags
-struct TagsSection: View {
+struct CustomEventsSection: View {
@EnvironmentObject var viewModel: OneSignalViewModel
+ @EnvironmentObject var toast: ToastPresenter
+ @State private var open = false
var body: some View {
- Section {
- if viewModel.tags.isEmpty {
- EmptyListRow(message: "No Tags Added")
- } else {
- ForEach(viewModel.tags) { tag in
- KeyValueRow(item: tag) {
- viewModel.removeTag(tag)
- }
- }
+ SectionCard(
+ title: "CUSTOM EVENTS",
+ sectionKey: "custom_events",
+ onInfoTap: { viewModel.showTooltip(for: "customEvents") }
+ ) {
+ ActionButton("TRACK EVENT", accessibilityID: "track_event_button") {
+ open = true
}
-
- Button {
- viewModel.showAddSheet(for: .tag)
- } label: {
- HStack {
- Spacer()
- Label("Add Tag", systemImage: "plus")
- .fontWeight(.medium)
- Spacer()
- }
- }
- } header: {
- Text("Tags")
+ }
+ .osCenteredDialog(isPresented: $open) {
+ TrackEventDialog(
+ onTrack: { name, properties in
+ viewModel.trackEvent(name: name, properties: properties)
+ toast.show("Event tracked: \(name)")
+ open = false
+ },
+ onCancel: { open = false }
+ )
}
}
}
-
-#Preview {
- List {
- TagsSection()
- }
- .environmentObject(OneSignalViewModel())
-}
diff --git a/examples/demo/App/Views/Sections/EmailsSection.swift b/examples/demo/App/Views/Sections/EmailsSection.swift
new file mode 100644
index 000000000..ba9ef1cad
--- /dev/null
+++ b/examples/demo/App/Views/Sections/EmailsSection.swift
@@ -0,0 +1,62 @@
+/**
+ * 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 SwiftUI
+
+struct EmailsSection: View {
+ @EnvironmentObject var viewModel: OneSignalViewModel
+ @State private var addOpen = false
+
+ var body: some View {
+ SectionCard(
+ title: "EMAILS",
+ sectionKey: "emails",
+ onInfoTap: { viewModel.showTooltip(for: "emails") }
+ ) {
+ SingleList(
+ items: viewModel.emails,
+ emptyText: "No emails added",
+ sectionKey: "emails",
+ onRemove: { viewModel.removeEmail($0) }
+ )
+
+ ActionButton("ADD EMAIL", accessibilityID: "add_email_button") {
+ addOpen = true
+ }
+ }
+ .osCenteredDialog(isPresented: $addOpen) {
+ AddItemDialog(
+ itemType: .email,
+ onAdd: { _, value in
+ viewModel.addEmail(value)
+ addOpen = false
+ },
+ onCancel: { addOpen = false }
+ )
+ }
+ }
+}
diff --git a/examples/demo/App/Views/Sections/InAppSection.swift b/examples/demo/App/Views/Sections/InAppSection.swift
new file mode 100644
index 000000000..84aa00ee4
--- /dev/null
+++ b/examples/demo/App/Views/Sections/InAppSection.swift
@@ -0,0 +1,51 @@
+/**
+ * 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 SwiftUI
+
+/// In-app messaging pause toggle
+struct InAppSection: View {
+ @EnvironmentObject var viewModel: OneSignalViewModel
+
+ var body: some View {
+ SectionCard(
+ title: "IN-APP MESSAGING",
+ sectionKey: "iam",
+ onInfoTap: { viewModel.showTooltip(for: "inAppMessaging") }
+ ) {
+ ToggleRow(
+ label: "Pause In-App Messages",
+ description: "Toggle in-app message display",
+ isOn: Binding(
+ get: { viewModel.isInAppMessagesPaused },
+ set: { viewModel.setIamPaused($0) }
+ ),
+ accessibilityID: "pause_iam_toggle"
+ )
+ }
+ }
+}
diff --git a/examples/demo/App/Views/Sections/LiveActivitySection.swift b/examples/demo/App/Views/Sections/LiveActivitySection.swift
new file mode 100644
index 000000000..136c6564d
--- /dev/null
+++ b/examples/demo/App/Views/Sections/LiveActivitySection.swift
@@ -0,0 +1,144 @@
+/**
+ * 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 SwiftUI
+
+/// Live Activities (iOS 16.1+) section with activity ID + order # inputs and status cycler.
+/// Mirrors the Capacitor demo's LiveActivitySection.
+struct LiveActivitySection: View {
+ @EnvironmentObject var viewModel: OneSignalViewModel
+
+ @State private var activityId: String = "order-1"
+ @State private var orderNumber: String = "ORD-1234"
+ @State private var statusIndex: Int = 0
+
+ private let statuses: [LiveActivityStatus] = [.preparing, .onTheWay, .delivered]
+
+ var body: some View {
+ SectionCard(
+ title: "LIVE ACTIVITIES",
+ sectionKey: "live_activities",
+ onInfoTap: { viewModel.showTooltip(for: "liveActivities") }
+ ) {
+ inputCard
+
+ ActionButton(
+ "START LIVE ACTIVITY",
+ isDisabled: trimmedActivityId.isEmpty,
+ accessibilityID: "start_live_activity_button"
+ ) {
+ statusIndex = 0
+ viewModel.startLiveActivity(
+ activityId: trimmedActivityId,
+ orderNumber: orderNumber.trimmingCharacters(in: .whitespacesAndNewlines),
+ status: statuses[0]
+ )
+ }
+
+ ActionButton(
+ updateButtonTitle,
+ isDisabled: trimmedActivityId.isEmpty || !LiveActivityController.hasApiKey,
+ accessibilityID: "update_live_activity_button"
+ ) {
+ let nextIndex = (statusIndex + 1) % statuses.count
+ viewModel.updateLiveActivity(
+ activityId: trimmedActivityId,
+ status: statuses[nextIndex]
+ )
+ statusIndex = nextIndex
+ }
+
+ ActionButton(
+ "END LIVE ACTIVITY",
+ style: .outline,
+ isDisabled: trimmedActivityId.isEmpty || !LiveActivityController.hasApiKey,
+ accessibilityID: "end_live_activity_button"
+ ) {
+ viewModel.endLiveActivity(activityId: trimmedActivityId)
+ }
+
+ if !LiveActivityController.hasApiKey {
+ Text("Set ONESIGNAL_API_KEY in Secrets.plist to enable update & end")
+ .font(OS.Font.bodySmall)
+ .foregroundColor(OS.Color.grey600)
+ .frame(maxWidth: .infinity, alignment: .center)
+ .accessibilityIdentifier("live_activities_hint")
+ }
+ }
+ }
+
+ private var trimmedActivityId: String {
+ activityId.trimmingCharacters(in: .whitespacesAndNewlines)
+ }
+
+ private var nextStatus: LiveActivityStatus {
+ statuses[(statusIndex + 1) % statuses.count]
+ }
+
+ private var updateButtonTitle: String {
+ "UPDATE → \(nextStatus.displayName.uppercased())"
+ }
+
+ private var inputCard: some View {
+ VStack(spacing: 4) {
+ inlineRow(
+ label: "Activity ID",
+ placeholder: "Activity ID",
+ text: $activityId,
+ accessibilityID: "live_activity_id_input"
+ )
+ inlineRow(
+ label: "Order #",
+ placeholder: "Order #",
+ text: $orderNumber,
+ accessibilityID: "live_activity_order_number"
+ )
+ }
+ .osCard()
+ }
+
+ private func inlineRow(
+ label: String,
+ placeholder: String,
+ text: Binding,
+ accessibilityID: String
+ ) -> some View {
+ HStack(alignment: .center, spacing: 8) {
+ Text(label)
+ .font(OS.Font.bodyMedium)
+ .foregroundColor(OS.Color.grey600)
+ .frame(minWidth: OS.Layout.inlineLabelMinWidth, alignment: .leading)
+ TextField(placeholder, text: text)
+ .font(OS.Font.bodyMedium)
+ .foregroundColor(OS.Color.bodyText)
+ .multilineTextAlignment(.trailing)
+ .autocorrectionDisabled()
+ .textInputAutocapitalization(.never)
+ .accessibilityIdentifier(accessibilityID)
+ }
+ }
+}
diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Sections/LocationSection.swift b/examples/demo/App/Views/Sections/LocationSection.swift
similarity index 62%
rename from iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Sections/LocationSection.swift
rename to examples/demo/App/Views/Sections/LocationSection.swift
index 8554cdc6e..41079cea7 100644
--- a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Sections/LocationSection.swift
+++ b/examples/demo/App/Views/Sections/LocationSection.swift
@@ -27,36 +27,34 @@
import SwiftUI
-/// Section for location sharing and permissions
struct LocationSection: View {
@EnvironmentObject var viewModel: OneSignalViewModel
+ @EnvironmentObject var toast: ToastPresenter
var body: some View {
- VStack(spacing: 0) {
- SectionHeader(title: "Location", tooltipKey: "location")
+ SectionCard(
+ title: "LOCATION",
+ sectionKey: "location",
+ onInfoTap: { viewModel.showTooltip(for: "location") }
+ ) {
+ ToggleRow(
+ label: "Location Shared",
+ description: "Share device location with OneSignal",
+ isOn: Binding(
+ get: { viewModel.isLocationShared },
+ set: { viewModel.setLocationShared($0) }
+ ),
+ accessibilityID: "location_shared_toggle"
+ )
- CardContainer {
- ToggleRow(
- title: "Location Shared",
- subtitle: "Share device location with OneSignal",
- isOn: Binding(
- get: { viewModel.isLocationShared },
- set: { _ in viewModel.toggleLocationShared() }
- )
- )
+ ActionButton("PROMPT LOCATION", accessibilityID: "prompt_location_button") {
+ viewModel.promptLocation()
}
- ActionButton(title: "Prompt Location") {
- viewModel.promptLocation()
+ ActionButton("CHECK LOCATION", accessibilityID: "check_location_button") {
+ let shared = viewModel.checkLocationShared()
+ toast.show("Location shared: \(shared)")
}
- .padding(.top, 12)
}
}
}
-
-#Preview {
- LocationSection()
- .padding()
- .background(Color(.systemGroupedBackground))
- .environmentObject(OneSignalViewModel())
-}
diff --git a/examples/demo/App/Views/Sections/OutcomesSection.swift b/examples/demo/App/Views/Sections/OutcomesSection.swift
new file mode 100644
index 000000000..f4b84f259
--- /dev/null
+++ b/examples/demo/App/Views/Sections/OutcomesSection.swift
@@ -0,0 +1,67 @@
+/**
+ * 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 SwiftUI
+
+struct OutcomesSection: View {
+ @EnvironmentObject var viewModel: OneSignalViewModel
+ @EnvironmentObject var toast: ToastPresenter
+ @State private var open = false
+
+ var body: some View {
+ SectionCard(
+ title: "OUTCOME EVENTS",
+ sectionKey: "outcomes",
+ onInfoTap: { viewModel.showTooltip(for: "outcomes") }
+ ) {
+ ActionButton("SEND OUTCOME", accessibilityID: "send_outcome_button") {
+ open = true
+ }
+ }
+ .osCenteredDialog(isPresented: $open) {
+ OutcomeDialog(
+ onSend: { name, mode, value in
+ switch mode {
+ case .normal:
+ viewModel.sendOutcome(name)
+ toast.show("Outcome sent: \(name)")
+ case .unique:
+ viewModel.sendUniqueOutcome(name)
+ toast.show("Unique outcome sent: \(name)")
+ case .value:
+ if let value = value {
+ viewModel.sendOutcome(name, value: value)
+ toast.show("Outcome sent: \(name) = \(value)")
+ }
+ }
+ open = false
+ },
+ onCancel: { open = false }
+ )
+ }
+ }
+}
diff --git a/examples/demo/App/Views/Sections/PushSection.swift b/examples/demo/App/Views/Sections/PushSection.swift
new file mode 100644
index 000000000..d78dfba54
--- /dev/null
+++ b/examples/demo/App/Views/Sections/PushSection.swift
@@ -0,0 +1,97 @@
+/**
+ * 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 SwiftUI
+
+/// Push subscription ID, opt-in toggle, and prompt-for-permission CTA
+struct PushSection: View {
+ @EnvironmentObject var viewModel: OneSignalViewModel
+
+ var body: some View {
+ SectionCard(
+ title: "PUSH",
+ sectionKey: "push",
+ onInfoTap: { viewModel.showTooltip(for: "push") }
+ ) {
+ VStack(spacing: 0) {
+ pushIdRow
+ Rectangle()
+ .fill(OS.Color.divider)
+ .frame(height: OS.Layout.dividerHeight)
+ .padding(.vertical, 4)
+ pushEnabledRow
+ }
+ .osCard()
+
+ if !viewModel.hasNotificationPermission {
+ ActionButton(
+ "PROMPT PUSH",
+ accessibilityID: "prompt_push_button"
+ ) {
+ viewModel.promptPushPermission()
+ }
+ }
+ }
+ }
+
+ private var pushIdRow: some View {
+ HStack(alignment: .center, spacing: 12) {
+ Text("Push ID")
+ .font(OS.Font.bodyMedium)
+ .foregroundColor(OS.Color.bodyText)
+ Spacer(minLength: 0)
+ Text(viewModel.pushSubscriptionId ?? "—")
+ .font(OS.Font.mono12)
+ .foregroundColor(OS.Color.bodyText)
+ .lineLimit(1)
+ .truncationMode(.middle)
+ .accessibilityIdentifier("push_id_value")
+ }
+ .padding(.vertical, 4)
+ }
+
+ private var pushEnabledRow: some View {
+ HStack(alignment: .center, spacing: 12) {
+ Text("Push Enabled")
+ .font(OS.Font.bodyMedium)
+ .foregroundColor(OS.Color.bodyText)
+ Spacer(minLength: 0)
+ Toggle(
+ "",
+ isOn: Binding(
+ get: { viewModel.isPushEnabled },
+ set: { viewModel.setPushEnabled($0) }
+ )
+ )
+ .labelsHidden()
+ .tint(OS.Color.primary)
+ .disabled(!viewModel.hasNotificationPermission)
+ .accessibilityIdentifier("push_enabled_toggle")
+ }
+ .padding(.vertical, 4)
+ }
+}
diff --git a/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Sections/NotificationSection.swift b/examples/demo/App/Views/Sections/SendIamSection.swift
similarity index 67%
rename from OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Sections/NotificationSection.swift
rename to examples/demo/App/Views/Sections/SendIamSection.swift
index 92b7bcdfd..9fd851a6e 100644
--- a/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Sections/NotificationSection.swift
+++ b/examples/demo/App/Views/Sections/SendIamSection.swift
@@ -27,34 +27,24 @@
import SwiftUI
-/// Section for sending test push notifications and in-app messages
-struct NotificationSection: View {
+/// Buttons that add an `iam_type` trigger so dashboard IAM rules can fire
+struct SendIamSection: View {
@EnvironmentObject var viewModel: OneSignalViewModel
var body: some View {
- // Send Push Notification Section
- Section {
- NotificationTypeGrid { type in
- viewModel.sendTestNotification(type)
+ SectionCard(
+ title: "SEND IN-APP MESSAGE",
+ sectionKey: "send_iam",
+ onInfoTap: { viewModel.showTooltip(for: "sendInAppMessage") }
+ ) {
+ ForEach(InAppMessageType.allCases) { type in
+ ActionButton(
+ type.rawValue.uppercased(),
+ accessibilityID: "send_iam_\(type.triggerValue)_button"
+ ) {
+ viewModel.sendIamTrigger(type)
+ }
}
- } header: {
- Text("Send Push Notification")
}
-
- // Send In-App Message Section
- Section {
- InAppMessageTypeGrid { type in
- viewModel.sendTestInAppMessage(type)
- }
- } header: {
- Text("Send In-App Message")
- }
- }
-}
-
-#Preview {
- List {
- NotificationSection()
}
- .environmentObject(OneSignalViewModel())
}
diff --git a/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/ContentView.swift b/examples/demo/App/Views/Sections/SendPushSection.swift
similarity index 50%
rename from OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/ContentView.swift
rename to examples/demo/App/Views/Sections/SendPushSection.swift
index 80a7db871..329fa2c8e 100644
--- a/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/ContentView.swift
+++ b/examples/demo/App/Views/Sections/SendPushSection.swift
@@ -27,49 +27,45 @@
import SwiftUI
-/// Main content view composing all sections
-struct ContentView: View {
+/// Buttons that fire test pushes via the OneSignal REST API
+struct SendPushSection: View {
@EnvironmentObject var viewModel: OneSignalViewModel
+ @State private var customOpen = false
var body: some View {
- NavigationStack {
- List {
- AppInfoSection()
- UserSection()
- SubscriptionSection()
- TagsSection()
- MessagingSection()
- LocationSection()
- NotificationSection()
+ SectionCard(
+ title: "SEND PUSH NOTIFICATION",
+ sectionKey: "send_push",
+ onInfoTap: { viewModel.showTooltip(for: "sendPushNotification") }
+ ) {
+ ActionButton("SIMPLE", accessibilityID: "send_simple_button") {
+ viewModel.sendNotification(.simple)
}
- .listStyle(.insetGrouped)
- .navigationTitle("OneSignal")
- .toolbar {
- ToolbarItem(placement: .navigationBarTrailing) {
- Button {
- viewModel.refreshState()
- } label: {
- Image(systemName: "arrow.clockwise")
- }
- }
+ ActionButton("WITH IMAGE", accessibilityID: "send_image_button") {
+ viewModel.sendNotification(.withImage)
}
- .sheet(isPresented: $viewModel.showingAddSheet) {
- AddItemSheet(
- itemType: viewModel.addItemType,
- onAdd: { key, value in
- viewModel.handleAddItem(key: key, value: value)
- },
- onCancel: {
- viewModel.showingAddSheet = false
- }
- )
+ ActionButton("WITH SOUND", accessibilityID: "send_sound_button") {
+ viewModel.sendNotification(.withSound)
}
+ ActionButton("CUSTOM", accessibilityID: "send_custom_button") {
+ customOpen = true
+ }
+ ActionButton(
+ "CLEAR ALL",
+ style: .outline,
+ accessibilityID: "clear_all_button"
+ ) {
+ viewModel.clearAllNotifications()
+ }
+ }
+ .osCenteredDialog(isPresented: $customOpen) {
+ CustomNotificationDialog(
+ onSend: { title, body in
+ viewModel.sendCustomNotification(title: title, body: body)
+ customOpen = false
+ },
+ onCancel: { customOpen = false }
+ )
}
- .toast(message: $viewModel.toastMessage)
}
}
-
-#Preview {
- ContentView()
- .environmentObject(OneSignalViewModel())
-}
diff --git a/examples/demo/App/Views/Sections/SmsSection.swift b/examples/demo/App/Views/Sections/SmsSection.swift
new file mode 100644
index 000000000..d8deb5a1a
--- /dev/null
+++ b/examples/demo/App/Views/Sections/SmsSection.swift
@@ -0,0 +1,62 @@
+/**
+ * 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 SwiftUI
+
+struct SmsSection: View {
+ @EnvironmentObject var viewModel: OneSignalViewModel
+ @State private var addOpen = false
+
+ var body: some View {
+ SectionCard(
+ title: "SMS",
+ sectionKey: "sms",
+ onInfoTap: { viewModel.showTooltip(for: "sms") }
+ ) {
+ SingleList(
+ items: viewModel.smsNumbers,
+ emptyText: "No SMS added",
+ sectionKey: "sms",
+ onRemove: { viewModel.removeSms($0) }
+ )
+
+ ActionButton("ADD SMS", accessibilityID: "add_sms_button") {
+ addOpen = true
+ }
+ }
+ .osCenteredDialog(isPresented: $addOpen) {
+ AddItemDialog(
+ itemType: .sms,
+ onAdd: { _, value in
+ viewModel.addSms(value)
+ addOpen = false
+ },
+ onCancel: { addOpen = false }
+ )
+ }
+ }
+}
diff --git a/examples/demo/App/Views/Sections/TagsSection.swift b/examples/demo/App/Views/Sections/TagsSection.swift
new file mode 100644
index 000000000..a7e775655
--- /dev/null
+++ b/examples/demo/App/Views/Sections/TagsSection.swift
@@ -0,0 +1,101 @@
+/**
+ * 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 SwiftUI
+
+struct TagsSection: View {
+ @EnvironmentObject var viewModel: OneSignalViewModel
+ @State private var addOpen = false
+ @State private var addMultipleOpen = false
+ @State private var removeOpen = false
+
+ var body: some View {
+ SectionCard(
+ title: "TAGS",
+ sectionKey: "tags",
+ onInfoTap: { viewModel.showTooltip(for: "tags") }
+ ) {
+ PairList(
+ items: viewModel.tags,
+ emptyText: "No tags added",
+ sectionKey: "tags",
+ onRemove: { key in
+ if let item = viewModel.tags.first(where: { $0.key == key }) {
+ viewModel.removeTag(item)
+ }
+ }
+ )
+
+ ActionButton("ADD TAG", accessibilityID: "add_tag_button") {
+ addOpen = true
+ }
+ ActionButton("ADD MULTIPLE TAGS", accessibilityID: "add_multiple_tags_button") {
+ addMultipleOpen = true
+ }
+ if !viewModel.tags.isEmpty {
+ ActionButton(
+ "REMOVE TAGS",
+ style: .outline,
+ accessibilityID: "remove_tags_button"
+ ) {
+ removeOpen = true
+ }
+ }
+ }
+ .osCenteredDialog(isPresented: $addOpen) {
+ AddItemDialog(
+ itemType: .tag,
+ onAdd: { key, value in
+ viewModel.addTag(key: key, value: value)
+ addOpen = false
+ },
+ onCancel: { addOpen = false }
+ )
+ }
+ .osCenteredDialog(isPresented: $addMultipleOpen) {
+ MultiPairInputDialog(
+ type: .tags,
+ onAdd: { pairs in
+ viewModel.addTags(pairs)
+ addMultipleOpen = false
+ },
+ onCancel: { addMultipleOpen = false }
+ )
+ }
+ .osCenteredDialog(isPresented: $removeOpen) {
+ RemoveMultiDialog(
+ type: .tags,
+ items: viewModel.tags,
+ onRemove: { keys in
+ viewModel.removeSelectedTags(keys)
+ removeOpen = false
+ },
+ onCancel: { removeOpen = false }
+ )
+ }
+ }
+}
diff --git a/examples/demo/App/Views/Sections/TriggersSection.swift b/examples/demo/App/Views/Sections/TriggersSection.swift
new file mode 100644
index 000000000..0838045af
--- /dev/null
+++ b/examples/demo/App/Views/Sections/TriggersSection.swift
@@ -0,0 +1,108 @@
+/**
+ * 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 SwiftUI
+
+struct TriggersSection: View {
+ @EnvironmentObject var viewModel: OneSignalViewModel
+ @State private var addOpen = false
+ @State private var addMultipleOpen = false
+ @State private var removeOpen = false
+
+ var body: some View {
+ SectionCard(
+ title: "TRIGGERS",
+ sectionKey: "triggers",
+ onInfoTap: { viewModel.showTooltip(for: "triggers") }
+ ) {
+ PairList(
+ items: viewModel.triggers,
+ emptyText: "No triggers added",
+ sectionKey: "triggers",
+ onRemove: { key in
+ if let item = viewModel.triggers.first(where: { $0.key == key }) {
+ viewModel.removeTrigger(item)
+ }
+ }
+ )
+
+ ActionButton("ADD TRIGGER", accessibilityID: "add_trigger_button") {
+ addOpen = true
+ }
+ ActionButton("ADD MULTIPLE TRIGGERS", accessibilityID: "add_multiple_triggers_button") {
+ addMultipleOpen = true
+ }
+ if !viewModel.triggers.isEmpty {
+ ActionButton(
+ "REMOVE TRIGGERS",
+ style: .outline,
+ accessibilityID: "remove_triggers_button"
+ ) {
+ removeOpen = true
+ }
+ ActionButton(
+ "CLEAR ALL TRIGGERS",
+ style: .outline,
+ accessibilityID: "clear_triggers_button"
+ ) {
+ viewModel.clearTriggers()
+ }
+ }
+ }
+ .osCenteredDialog(isPresented: $addOpen) {
+ AddItemDialog(
+ itemType: .trigger,
+ onAdd: { key, value in
+ viewModel.addTrigger(key: key, value: value)
+ addOpen = false
+ },
+ onCancel: { addOpen = false }
+ )
+ }
+ .osCenteredDialog(isPresented: $addMultipleOpen) {
+ MultiPairInputDialog(
+ type: .triggers,
+ onAdd: { pairs in
+ viewModel.addTriggers(pairs)
+ addMultipleOpen = false
+ },
+ onCancel: { addMultipleOpen = false }
+ )
+ }
+ .osCenteredDialog(isPresented: $removeOpen) {
+ RemoveMultiDialog(
+ type: .triggers,
+ items: viewModel.triggers,
+ onRemove: { keys in
+ viewModel.removeSelectedTriggers(keys)
+ removeOpen = false
+ },
+ onCancel: { removeOpen = false }
+ )
+ }
+ }
+}
diff --git a/examples/demo/App/Views/Sections/UserSection.swift b/examples/demo/App/Views/Sections/UserSection.swift
new file mode 100644
index 000000000..0e0bb5067
--- /dev/null
+++ b/examples/demo/App/Views/Sections/UserSection.swift
@@ -0,0 +1,79 @@
+/**
+ * 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 SwiftUI
+
+/// Login/logout + status display, mirroring the Capacitor UserSection
+struct UserSection: View {
+ @EnvironmentObject var viewModel: OneSignalViewModel
+ @State private var loginOpen = false
+
+ var body: some View {
+ SectionCard(title: "USER", sectionKey: "user") {
+ ValueCard(rows: [
+ ValueCard.Row(
+ label: "Status",
+ value: viewModel.isLoggedIn ? "Logged In" : "Anonymous",
+ valueAccessibilityID: "user_status_value"
+ ),
+ ValueCard.Row(
+ label: "External ID",
+ value: viewModel.externalUserId ?? "—",
+ valueAccessibilityID: "user_external_id_value",
+ monospaced: true
+ )
+ ])
+
+ ActionButton(
+ viewModel.loginButtonTitle,
+ accessibilityID: "login_user_button"
+ ) {
+ loginOpen = true
+ }
+
+ if viewModel.isLoggedIn {
+ ActionButton(
+ "LOGOUT USER",
+ style: .outline,
+ accessibilityID: "logout_user_button"
+ ) {
+ viewModel.logout()
+ }
+ }
+ }
+ .osCenteredDialog(isPresented: $loginOpen) {
+ AddItemDialog(
+ itemType: .externalUserId,
+ onAdd: { _, value in
+ viewModel.login(externalId: value)
+ loginOpen = false
+ },
+ onCancel: { loginOpen = false }
+ )
+ }
+ }
+}
diff --git a/examples/demo/App/Views/Theme.swift b/examples/demo/App/Views/Theme.swift
new file mode 100644
index 000000000..645f5bf0b
--- /dev/null
+++ b/examples/demo/App/Views/Theme.swift
@@ -0,0 +1,119 @@
+/**
+ * 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 SwiftUI
+
+/// Design tokens shared across the demo. Mirrors the CSS variables and tables in
+/// `sdk-shared/demo/styles.md`.
+enum OS {
+
+ // MARK: Colors
+
+ enum Color {
+ static let primary = SwiftUI.Color(red: 0xE5/255, green: 0x4B/255, blue: 0x4D/255)
+ static let primaryPressed = SwiftUI.Color(red: 0xC3/255, green: 0x3F/255, blue: 0x41/255)
+ static let success = SwiftUI.Color(red: 0x34/255, green: 0xA8/255, blue: 0x53/255)
+ static let grey700 = SwiftUI.Color(red: 0x61/255, green: 0x61/255, blue: 0x61/255)
+ static let grey600 = SwiftUI.Color(red: 0x75/255, green: 0x75/255, blue: 0x75/255)
+ static let grey500 = SwiftUI.Color(red: 0x9E/255, green: 0x9E/255, blue: 0x9E/255)
+ static let lightBackground = SwiftUI.Color(red: 0xF8/255, green: 0xF9/255, blue: 0xFA/255)
+ static let cardBackground = SwiftUI.Color.white
+ static let cardBorder = SwiftUI.Color.black.opacity(0.1)
+ static let divider = SwiftUI.Color(red: 0xE8/255, green: 0xEA/255, blue: 0xED/255)
+ static let warningBackground = SwiftUI.Color(red: 0xFF/255, green: 0xF8/255, blue: 0xE1/255)
+ static let backdrop = SwiftUI.Color.black.opacity(0.54)
+ static let bodyText = SwiftUI.Color(red: 0x21/255, green: 0x21/255, blue: 0x21/255)
+ }
+
+ // MARK: Spacing
+
+ enum Spacing {
+ static let cardGap: CGFloat = 8
+ static let sectionGap: CGFloat = 24
+ static let pagePadding: CGFloat = 16
+ static let cardPadding: CGFloat = 12
+ }
+
+ // MARK: Radii
+
+ enum Radius {
+ static let card: CGFloat = 12
+ static let button: CGFloat = 8
+ static let input: CGFloat = 8
+ static let modal: CGFloat = 28
+ }
+
+ // MARK: Typography
+
+ enum Font {
+ static let bodyLarge = SwiftUI.Font.system(size: 16, weight: .regular)
+ static let bodyMedium = SwiftUI.Font.system(size: 14, weight: .regular)
+ static let bodySmall = SwiftUI.Font.system(size: 12, weight: .regular)
+ static let mono12 = SwiftUI.Font.system(size: 12, weight: .regular, design: .monospaced)
+ static let mono14 = SwiftUI.Font.system(size: 14, weight: .regular, design: .monospaced)
+ }
+
+ // MARK: Layout constants
+
+ enum Layout {
+ static let buttonHeight: CGFloat = 48
+ static let cardBorderWidth: CGFloat = 2
+ static let inputBorderWidth: CGFloat = 1
+ static let dividerHeight: CGFloat = 1
+ static let infoIconSize: CGFloat = 18
+ static let inlineLabelMinWidth: CGFloat = 80
+ static let listMaxVisible: Int = 5
+ }
+}
+
+// MARK: - Card chrome modifier
+
+/// Applies the standard demo card visual: white background, 12 corner radius,
+/// 2px border, no shadow, 12 px inner padding.
+struct CardChrome: ViewModifier {
+ var padding: CGFloat = OS.Spacing.cardPadding
+ var background: Color = OS.Color.cardBackground
+
+ func body(content: Content) -> some View {
+ content
+ .padding(padding)
+ .frame(maxWidth: .infinity, alignment: .leading)
+ .background(background)
+ .clipShape(RoundedRectangle(cornerRadius: OS.Radius.card))
+ .overlay(
+ RoundedRectangle(cornerRadius: OS.Radius.card)
+ .strokeBorder(OS.Color.cardBorder, lineWidth: OS.Layout.cardBorderWidth)
+ )
+ }
+}
+
+extension View {
+ /// Wraps the receiver in the demo's standard card chrome.
+ func osCard(padding: CGFloat = OS.Spacing.cardPadding, background: Color = OS.Color.cardBackground) -> some View {
+ modifier(CardChrome(padding: padding, background: background))
+ }
+}
diff --git a/examples/demo/App/vine_boom.wav b/examples/demo/App/vine_boom.wav
new file mode 100644
index 000000000..626bd5cc5
Binary files /dev/null and b/examples/demo/App/vine_boom.wav differ
diff --git a/examples/demo/Build.xcconfig b/examples/demo/Build.xcconfig
new file mode 100644
index 000000000..8db97b911
--- /dev/null
+++ b/examples/demo/Build.xcconfig
@@ -0,0 +1,8 @@
+// Demo build settings auto-included by every target via project.yml.
+//
+// Per-developer values (Apple DEVELOPMENT_TEAM, custom bundle ids, etc.)
+// belong in a sibling `Local.xcconfig` so they never get committed. The
+// optional include below is a no-op when the file is missing, so a fresh
+// clone builds without further setup. See `Local.xcconfig.example`.
+
+#include? "Local.xcconfig"
diff --git a/examples/demo/Local.xcconfig.example b/examples/demo/Local.xcconfig.example
new file mode 100644
index 000000000..8a403f08a
--- /dev/null
+++ b/examples/demo/Local.xcconfig.example
@@ -0,0 +1,15 @@
+// Copy this file to `Local.xcconfig` (gitignored) and uncomment any
+// per-developer overrides you need. Settings here apply to the App,
+// OneSignalNotificationServiceExtension, and OneSignalWidget targets.
+//
+// Anything you set here survives `xcodegen generate`; settings you change
+// only in Xcode's Signing & Capabilities UI are wiped on regeneration.
+
+// Apple Developer team id used by automatic signing. Required to run on a
+// physical device.
+// DEVELOPMENT_TEAM = ABCDE12345
+
+// Switch from automatic to manual signing if you have a specific
+// provisioning profile to attach. Leave on Automatic for most cases.
+// CODE_SIGN_STYLE = Manual
+// PROVISIONING_PROFILE_SPECIFIER = MyProfile
diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalNotificationServiceExtension/Info.plist b/examples/demo/OneSignalNotificationServiceExtension/Info.plist
similarity index 90%
rename from iOS_SDK/OneSignalSwiftUIExample/OneSignalNotificationServiceExtension/Info.plist
rename to examples/demo/OneSignalNotificationServiceExtension/Info.plist
index 9c03e92f1..ca3cd8f43 100644
--- a/iOS_SDK/OneSignalSwiftUIExample/OneSignalNotificationServiceExtension/Info.plist
+++ b/examples/demo/OneSignalNotificationServiceExtension/Info.plist
@@ -3,7 +3,7 @@
CFBundleDevelopmentRegion
- en
+ $(DEVELOPMENT_LANGUAGE)
CFBundleDisplayName
OneSignalNotificationServiceExtension
CFBundleExecutable
@@ -17,9 +17,9 @@
CFBundlePackageType
$(PRODUCT_BUNDLE_PACKAGE_TYPE)
CFBundleShortVersionString
- $(MARKETING_VERSION)
+ 1.0
CFBundleVersion
- $(CURRENT_PROJECT_VERSION)
+ 1
NSExtension
NSExtensionPointIdentifier
diff --git a/examples/demo/OneSignalNotificationServiceExtension/NotificationService.swift b/examples/demo/OneSignalNotificationServiceExtension/NotificationService.swift
new file mode 100644
index 000000000..df9ad5d3c
--- /dev/null
+++ b/examples/demo/OneSignalNotificationServiceExtension/NotificationService.swift
@@ -0,0 +1,71 @@
+/**
+ * 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 UserNotifications
+import OneSignalExtension
+
+/// Notification Service Extension that hands incoming pushes to OneSignal so it can
+/// download attachments, decrypt confidential pushes, and apply mutable content updates.
+/// Only runs when `mutable_content` is set on the push (which OneSignal sets automatically
+/// for any notification with attachments or action buttons).
+class NotificationService: UNNotificationServiceExtension {
+
+ var contentHandler: ((UNNotificationContent) -> Void)?
+ var receivedRequest: UNNotificationRequest!
+ var bestAttemptContent: UNMutableNotificationContent?
+
+ override func didReceive(
+ _ request: UNNotificationRequest,
+ withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void
+ ) {
+ self.receivedRequest = request
+ self.contentHandler = contentHandler
+ self.bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
+
+ if let bestAttemptContent = bestAttemptContent {
+ // Uncomment to verify the extension is firing during local debug:
+ // print("Running NotificationServiceExtension")
+ // bestAttemptContent.body = "[Modified] " + bestAttemptContent.body
+
+ OneSignalExtension.didReceiveNotificationExtensionRequest(
+ self.receivedRequest,
+ with: bestAttemptContent,
+ withContentHandler: self.contentHandler
+ )
+ }
+ }
+
+ override func serviceExtensionTimeWillExpire() {
+ if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent {
+ OneSignalExtension.serviceExtensionTimeWillExpireRequest(
+ self.receivedRequest,
+ with: self.bestAttemptContent
+ )
+ contentHandler(bestAttemptContent)
+ }
+ }
+}
diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalNotificationServiceExtension/OneSignalNotificationServiceExtension.entitlements b/examples/demo/OneSignalNotificationServiceExtension/OneSignalNotificationServiceExtension.entitlements
similarity index 100%
rename from iOS_SDK/OneSignalSwiftUIExample/OneSignalNotificationServiceExtension/OneSignalNotificationServiceExtension.entitlements
rename to examples/demo/OneSignalNotificationServiceExtension/OneSignalNotificationServiceExtension.entitlements
diff --git a/examples/demo/OneSignalWidget/Assets.xcassets/AccentColor.colorset/Contents.json b/examples/demo/OneSignalWidget/Assets.xcassets/AccentColor.colorset/Contents.json
new file mode 100644
index 000000000..0afb3cf0e
--- /dev/null
+++ b/examples/demo/OneSignalWidget/Assets.xcassets/AccentColor.colorset/Contents.json
@@ -0,0 +1,11 @@
+{
+ "colors": [
+ {
+ "idiom": "universal"
+ }
+ ],
+ "info": {
+ "author": "xcode",
+ "version": 1
+ }
+}
diff --git a/examples/demo/OneSignalWidget/Assets.xcassets/AppIcon.appiconset/Contents.json b/examples/demo/OneSignalWidget/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 000000000..b121e3bce
--- /dev/null
+++ b/examples/demo/OneSignalWidget/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,13 @@
+{
+ "images": [
+ {
+ "idiom": "universal",
+ "platform": "ios",
+ "size": "1024x1024"
+ }
+ ],
+ "info": {
+ "author": "xcode",
+ "version": 1
+ }
+}
diff --git a/examples/demo/OneSignalWidget/Assets.xcassets/Contents.json b/examples/demo/OneSignalWidget/Assets.xcassets/Contents.json
new file mode 100644
index 000000000..74d6a722c
--- /dev/null
+++ b/examples/demo/OneSignalWidget/Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info": {
+ "author": "xcode",
+ "version": 1
+ }
+}
diff --git a/examples/demo/OneSignalWidget/Assets.xcassets/WidgetBackground.colorset/Contents.json b/examples/demo/OneSignalWidget/Assets.xcassets/WidgetBackground.colorset/Contents.json
new file mode 100644
index 000000000..0afb3cf0e
--- /dev/null
+++ b/examples/demo/OneSignalWidget/Assets.xcassets/WidgetBackground.colorset/Contents.json
@@ -0,0 +1,11 @@
+{
+ "colors": [
+ {
+ "idiom": "universal"
+ }
+ ],
+ "info": {
+ "author": "xcode",
+ "version": 1
+ }
+}
diff --git a/examples/demo/OneSignalWidget/Info.plist b/examples/demo/OneSignalWidget/Info.plist
new file mode 100644
index 000000000..a75840841
--- /dev/null
+++ b/examples/demo/OneSignalWidget/Info.plist
@@ -0,0 +1,29 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleDisplayName
+ OneSignalWidget
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ $(PRODUCT_NAME)
+ CFBundlePackageType
+ $(PRODUCT_BUNDLE_PACKAGE_TYPE)
+ CFBundleShortVersionString
+ 1.0
+ CFBundleVersion
+ 1
+ NSExtension
+
+ NSExtensionPointIdentifier
+ com.apple.widgetkit-extension
+
+
+
diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Sections/TrackEventSection.swift b/examples/demo/OneSignalWidget/OneSignalWidgetBundle.swift
similarity index 70%
rename from iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Sections/TrackEventSection.swift
rename to examples/demo/OneSignalWidget/OneSignalWidgetBundle.swift
index 803342735..063a36e0c 100644
--- a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Sections/TrackEventSection.swift
+++ b/examples/demo/OneSignalWidget/OneSignalWidgetBundle.swift
@@ -25,26 +25,14 @@
* THE SOFTWARE.
*/
+import WidgetKit
import SwiftUI
-/// Section for tracking custom events
-struct TrackEventSection: View {
- @EnvironmentObject var viewModel: OneSignalViewModel
-
- var body: some View {
- VStack(spacing: 0) {
- SectionHeader(title: "Track Event", tooltipKey: "trackEvent")
-
- ActionButton(title: "Track Event") {
- viewModel.showingTrackEventSheet = true
- }
+@main
+struct OneSignalWidgetBundle: WidgetBundle {
+ var body: some Widget {
+ if #available(iOS 16.2, *) {
+ OneSignalWidgetLiveActivity()
}
}
}
-
-#Preview {
- TrackEventSection()
- .padding()
- .background(Color(.systemGroupedBackground))
- .environmentObject(OneSignalViewModel())
-}
diff --git a/examples/demo/OneSignalWidget/OneSignalWidgetLiveActivity.swift b/examples/demo/OneSignalWidget/OneSignalWidgetLiveActivity.swift
new file mode 100644
index 000000000..cf4a029d3
--- /dev/null
+++ b/examples/demo/OneSignalWidget/OneSignalWidgetLiveActivity.swift
@@ -0,0 +1,175 @@
+/**
+ * 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 ActivityKit
+import WidgetKit
+import SwiftUI
+import OneSignalLiveActivities
+
+/// Live Activity widget that renders the order tracking flow used by the demo.
+/// Uses `DefaultLiveActivityAttributes` (provided by the OneSignal SDK) so the same
+/// data shape works between `OneSignal.LiveActivities.startDefault(...)` and remote
+/// `event_updates` payloads sent via the REST API.
+@available(iOS 16.2, *)
+struct OneSignalWidgetLiveActivity: Widget {
+
+ var body: some WidgetConfiguration {
+ ActivityConfiguration(for: DefaultLiveActivityAttributes.self) { context in
+ let orderNumber = context.attributes.data["orderNumber"]?.asString() ?? "Order"
+ let status = context.state.data["status"]?.asString() ?? "preparing"
+ let message = context.state.data["message"]?.asString() ?? "Your order is being prepared"
+ let eta = context.state.data["estimatedTime"]?.asString() ?? ""
+
+ VStack(spacing: 10) {
+ HStack {
+ Text(orderNumber)
+ .font(.caption)
+ .foregroundColor(.gray)
+ Spacer()
+ if !eta.isEmpty {
+ Text(eta)
+ .font(.caption)
+ .foregroundColor(.white.opacity(0.7))
+ }
+ }
+
+ HStack(spacing: 12) {
+ Image(systemName: Self.statusIcon(for: status))
+ .font(.title2)
+ .foregroundColor(Self.statusColor(for: status))
+
+ VStack(alignment: .leading, spacing: 2) {
+ Text(Self.statusLabel(for: status))
+ .font(.headline)
+ .foregroundColor(.white)
+ Text(message)
+ .font(.subheadline)
+ .foregroundColor(.white.opacity(0.8))
+ .lineLimit(1)
+ }
+ Spacer()
+ }
+
+ DeliveryProgressBar(status: status)
+ }
+ .padding()
+ .activityBackgroundTint(Color(red: 0.11, green: 0.13, blue: 0.19))
+ .activitySystemActionForegroundColor(.white)
+
+ } dynamicIsland: { context in
+ let status = context.state.data["status"]?.asString() ?? "preparing"
+ let message = context.state.data["message"]?.asString() ?? "Preparing"
+ let eta = context.state.data["estimatedTime"]?.asString() ?? ""
+
+ return DynamicIsland {
+ DynamicIslandExpandedRegion(.leading) {
+ Image(systemName: Self.statusIcon(for: status))
+ .font(.title2)
+ .foregroundColor(Self.statusColor(for: status))
+ }
+ DynamicIslandExpandedRegion(.center) {
+ Text(Self.statusLabel(for: status))
+ .font(.headline)
+ }
+ DynamicIslandExpandedRegion(.trailing) {
+ if !eta.isEmpty {
+ Text(eta)
+ .font(.caption)
+ .foregroundColor(.secondary)
+ }
+ }
+ DynamicIslandExpandedRegion(.bottom) {
+ Text(message)
+ .font(.caption)
+ .foregroundColor(.secondary)
+ }
+ } compactLeading: {
+ Image(systemName: Self.statusIcon(for: status))
+ .foregroundColor(Self.statusColor(for: status))
+ } compactTrailing: {
+ Text(Self.statusLabel(for: status))
+ .font(.caption)
+ } minimal: {
+ Image(systemName: Self.statusIcon(for: status))
+ .foregroundColor(Self.statusColor(for: status))
+ }
+ }
+ }
+
+ // MARK: - Status helpers
+
+ private static func statusIcon(for status: String) -> String {
+ switch status {
+ case "on_the_way": return "box.truck.fill"
+ case "delivered": return "checkmark.circle.fill"
+ default: return "bag.fill"
+ }
+ }
+
+ private static func statusColor(for status: String) -> Color {
+ switch status {
+ case "on_the_way": return .blue
+ case "delivered": return .green
+ default: return .orange
+ }
+ }
+
+ private static func statusLabel(for status: String) -> String {
+ switch status {
+ case "on_the_way": return "On the Way"
+ case "delivered": return "Delivered"
+ default: return "Preparing"
+ }
+ }
+}
+
+@available(iOS 16.2, *)
+struct DeliveryProgressBar: View {
+ let status: String
+
+ private var progress: CGFloat {
+ switch status {
+ case "on_the_way": return 0.6
+ case "delivered": return 1.0
+ default: return 0.25
+ }
+ }
+
+ var body: some View {
+ GeometryReader { geo in
+ ZStack(alignment: .leading) {
+ RoundedRectangle(cornerRadius: 3)
+ .fill(Color.white.opacity(0.2))
+ .frame(height: 6)
+ RoundedRectangle(cornerRadius: 3)
+ .fill(progress >= 1.0 ? Color.green : Color.blue)
+ .frame(width: geo.size.width * progress, height: 6)
+ }
+ }
+ .frame(height: 6)
+ }
+}
diff --git a/examples/demo/README.md b/examples/demo/README.md
new file mode 100644
index 000000000..42e5d5513
--- /dev/null
+++ b/examples/demo/README.md
@@ -0,0 +1,131 @@
+# OneSignal SwiftUI Example App
+
+A SwiftUI demo app that exercises every public surface of the OneSignal iOS SDK and mirrors the layout, naming, and behavior of other OneSignal SDKS so the same end-to-end test suite (`@onesignal/sdk-shared`) can drive both apps.
+
+## Features
+
+The demo covers all major OneSignal SDK capabilities:
+
+- **App / Consent**: App ID display, `consent_required` and `privacy_consent` toggles
+- **User**: Login / logout with external user ID
+- **Push Subscription**: Push subscription ID, opt-in toggle, prompt for permission
+- **Send Push Notification**: Simple / image / sound / custom notifications via the OneSignal REST API
+- **In-App Messaging**: Pause / resume IAM display, send IAM trigger to surface dashboard messages
+- **Aliases / Emails / SMS / Tags**: Add (single or multiple), remove (single or selected)
+- **Outcomes**: Send normal / unique / value outcomes
+- **Triggers**: Add (single or multiple), remove (single or selected), clear all
+- **Custom Events**: Track event with optional JSON properties
+- **Location**: Location sharing toggle, request permission
+- **Live Activities** (iOS 16.1+): Start / update / end an activity, status cycler
+
+Section headers use ALL CAPS and an info icon (where Capacitor has one) that opens a tooltip sheet with descriptions sourced from `https://github.com/OneSignal/sdk-shared` (with a bundled fallback).
+
+Every interactive element exposes an `accessibilityIdentifier` matching the Capacitor demo's `data-testid` so the shared E2E tests can target it.
+
+## Architecture
+
+The Xcode project ships three targets, mirroring the Capacitor / Cordova / RN demos:
+
+```
+examples/demo/
+├── App.xcodeproj
+├── App.entitlements # main app: aps-environment + app group
+├── App/ # Main app target source
+│ ├── App.swift # @main + AppDelegate, SDK + Live Activity setup
+│ ├── Views/
+│ │ ├── ContentView.swift # Composes sections + sheets in Capacitor order
+│ │ ├── Sections/ # AppSection, UserSection, PushSection, ...
+│ │ └── Components/ # SectionCard, ActionButton, ToggleRow,
+│ │ # AddItemSheet, MultiPairInputSheet, RemoveMultiSheet,
+│ │ # OutcomeSheet, CustomNotificationSheet, TrackEventSheet,
+│ │ # TooltipSheet, ToastView, ListWidgets, KeyValueRow
+│ ├── ViewModels/
+│ │ └── OneSignalViewModel.swift # Single ObservableObject backing every section
+│ ├── Models/
+│ │ └── AppModels.swift # KeyValueItem, NotificationType, InAppMessageType,
+│ │ # AddItemType, MultiAddItemType, RemoveMultiItemType,
+│ │ # OutcomeMode, TooltipData, UserData
+│ ├── Services/
+│ │ ├── OneSignalService.swift # Thin wrapper over OneSignal.* APIs
+│ │ ├── NotificationSender.swift # Posts to /notifications with retry on transient failures
+│ │ ├── UserFetchService.swift # Hydrates aliases / tags / channels via /users
+│ │ ├── TooltipService.swift # Loads tooltip JSON from sdk-shared (with fallback)
+│ │ └── LiveActivityController.swift # Wraps OneSignal.LiveActivities + REST update / end
+│ ├── Assets.xcassets/
+│ └── Info.plist
+│
+├── OneSignalNotificationServiceExtension/ # NSE target — required for rich push (images, decryption, mutable content)
+│ ├── NotificationService.swift # Forwards to OneSignalExtension.didReceiveNotificationExtensionRequest
+│ ├── Info.plist # NSExtension/usernotifications.service
+│ └── OneSignalNotificationServiceExtension.entitlements # app group (must match main app)
+│
+└── OneSignalWidget/ # Widget Extension target — required to render Live Activities
+ ├── OneSignalWidgetBundle.swift # @main WidgetBundle
+ ├── OneSignalWidgetLiveActivity.swift # Lock screen + Dynamic Island UI for DefaultLiveActivityAttributes
+ ├── Info.plist # NSExtension/widgetkit-extension
+ └── Assets.xcassets/ # WidgetBackground, AccentColor, AppIcon
+```
+
+This mirrors the Capacitor demo's iOS layout (`OneSignal-Capacitor-SDK/examples/demo/ios/App/{App,OneSignalNotificationServiceExtension,OneSignalWidget}/`).
+
+## Setup Instructions
+
+The Xcode project is generated from `project.yml` with [XcodeGen](https://github.com/yonaskolb/XcodeGen) and is wired into `iOS_SDK/OneSignalSDK.xcworkspace`, so it builds against the SDK source tree directly. There are no manual Xcode setup steps.
+
+### 1. Open the workspace
+
+```bash
+open iOS_SDK/OneSignalSDK.xcworkspace
+```
+
+In the scheme picker pick **App** and run on a simulator or device. Granting notification permissions and selecting a section is enough to exercise the SDK against your local source.
+
+### 2. Regenerate the project (only when `project.yml` changes)
+
+```bash
+brew install xcodegen # one time
+cd examples/demo
+xcodegen generate # rewrites App.xcodeproj
+```
+
+`project.yml` declares three targets — `App`, `OneSignalNotificationServiceExtension`, `OneSignalWidget` — and references the framework targets in `iOS_SDK/OneSignalSDK/OneSignal.xcodeproj` so each one links and embeds the right SDK frameworks at build time.
+
+### 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`.
+
+### 4. Configure your OneSignal credentials
+
+Both the App ID and (optional) REST API key live in a single `Secrets.plist` next to `App/Info.plist`. The file is gitignored. Add it to the App target with the keys you need:
+
+```xml
+
+ ONESIGNAL_APP_ID
+ YOUR_APP_ID
+ ONESIGNAL_API_KEY
+ YOUR_REST_API_KEY
+
+```
+
+- `ONESIGNAL_APP_ID` — optional. Falls back to the placeholder in `SecretsConfig.defaultAppId` when missing.
+- `ONESIGNAL_API_KEY` — optional, only needed for Live Activity **Update** / **End**. Without it, those buttons disable themselves and the section shows a hint.
+
+> The widget renders `DefaultLiveActivityAttributes` (provided by the SDK), so the Activity ID + Order # you type into the demo flows through to the same widget regardless of whether the update came from `OneSignal.LiveActivities` locally or from the REST `/live_activities/{id}/notifications` endpoint.
+
+## Running the App
+
+1. Select a simulator or device
+2. Build and run (⌘R)
+3. Grant notification permissions when prompted
+4. Explore each section
+
+## Requirements
+
+- iOS 15.0+ (Live Activities require iOS 16.1+)
+- Xcode 15.0+
+- Swift 5.9+
+- OneSignal iOS SDK 5.0+
+
+## License
+
+Modified MIT License — see the repository LICENSE file.
diff --git a/examples/demo/build.md b/examples/demo/build.md
new file mode 100644
index 000000000..cfb7e6302
--- /dev/null
+++ b/examples/demo/build.md
@@ -0,0 +1,353 @@
+# OneSignal iOS Sample App - Build Guide
+
+This document extends the shared build guide with iOS-specific details.
+
+**Read the shared guide first:**
+https://raw.githubusercontent.com/OneSignal/sdk-shared/refs/heads/main/demo/build.md
+
+Replace `{{PLATFORM}}` with `iOS` everywhere in that guide. Everything below either overrides or supplements sections from the shared guide.
+
+---
+
+## Project Setup
+
+The demo lives at `examples/demo/` (relative to the SDK repo root) and is wired into the same Xcode workspace as the SDK source (`iOS_SDK/OneSignalSDK.xcworkspace`), so it builds against your local SDK tree directly — no tarball, CocoaPods, or SPM package reference required.
+
+`App.xcodeproj` is generated from `project.yml` with [XcodeGen](https://github.com/yonaskolb/XcodeGen):
+
+```bash
+brew install xcodegen # one time
+cd examples/demo
+xcodegen generate # regenerates App.xcodeproj from project.yml
+```
+
+`project.yml` declares three targets and links them against the SDK framework targets defined in `iOS_SDK/OneSignalSDK/OneSignal.xcodeproj` via a `projectReferences` entry:
+
+- **App** — main app target, embeds and signs every public SDK framework (`OneSignalCore`, `OneSignalOSCore`, `OneSignalOutcomes`, `OneSignalNotifications`, `OneSignalUser`, `OneSignalExtension`, `OneSignalLocation`, `OneSignalInAppMessages`, `OneSignalLiveActivities`, `OneSignalFramework`) plus the two local extensions
+- **OneSignalNotificationServiceExtension** — links (does NOT embed) `OneSignalCore`, `OneSignalOutcomes`, `OneSignalExtension`
+- **OneSignalWidget** — links (does NOT embed) `OneSignalLiveActivities`
+
+Open `iOS_SDK/OneSignalSDK.xcworkspace`, select the **App** scheme, and run. The app and both extensions build from local SDK source, so SDK edits flow through immediately.
+
+### App icons
+
+`App/Assets.xcassets/AppIcon.appiconset/` ships pre-populated with the OneSignal logo asset. The widget extension has its own `OneSignalWidget/Assets.xcassets/AppIcon.appiconset/` plus an `AccentColor` and `WidgetBackground` color set (referenced via `ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME` / `ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME` in `project.yml`). No regeneration step is needed.
+
+### Environment / secrets
+
+Per-developer build settings and OneSignal credentials are split across two layers, both wired into XcodeGen via `project.yml`:
+
+- `Build.xcconfig` -- root xcconfig referenced by every target's `configFiles: { Debug, Release }`. It only does `#include? "Local.xcconfig"`, so a fresh clone builds with no extra setup.
+- `Local.xcconfig` (gitignored) -- per-developer overrides such as `DEVELOPMENT_TEAM`, `CODE_SIGN_STYLE`, and `PROVISIONING_PROFILE_SPECIFIER`. Anything set here survives `xcodegen generate`. See `Local.xcconfig.example` for the template.
+- `App/Secrets.plist` (gitignored, `optional: true` in `project.yml`) -- `` with `ONESIGNAL_APP_ID` and `ONESIGNAL_API_KEY` strings. Parsed at runtime by `App/Services/SecretsConfig.swift`. Bundled via an explicit `buildPhase: resources` source entry because XcodeGen otherwise treats `.plist` files as Info.plist-like and skips Copy Bundle Resources.
+- `App/vine_boom.wav` (gitignored optional) -- bundled custom notification sound; XcodeGen picks it up automatically from the `App/` source path.
+
+### Build & run
+
+There is no `setup.sh` / `run-ios.sh` script -- Xcode handles everything:
+
+1. `open iOS_SDK/OneSignalSDK.xcworkspace`
+2. Pick the **App** scheme
+3. ⌘R to build and run on the selected simulator or device
+
+The only manual step is running `xcodegen generate` after editing `project.yml`.
+
+---
+
+## State Management
+
+Use a single `OneSignalViewModel` (`App/ViewModels/OneSignalViewModel.swift`) as the central state manager. There is no repository wrapper -- the view-model calls the OneSignal SDK directly through a thin `OneSignalService` singleton (`App/Services/OneSignalService.swift`).
+
+- `@MainActor final class OneSignalViewModel: ObservableObject` with `@Published` properties for reactive state (app id, push subscription id, aliases, tags, emails, SMS, triggers, consent, location, IAM paused, `isLoading`)
+- The only `@Published` UI overlay state is `activeTooltip: TooltipData?`. Action dialogs are NOT in the view-model -- each section owns its own `@State` boolean and binds `.osCenteredDialog(isPresented:)` locally
+- `init` calls `refreshState()` and the private `setupObservers()`, which registers `OSPushSubscriptionObserver`, `OSUserStateObserver`, and `OSNotificationPermissionObserver`. SwiftUI keeps the view-model alive for the app lifetime via `@StateObject` in `App.swift`, so no manual teardown is needed
+- `OneSignalService` (singleton, `App/Services/OneSignalService.swift`) funnels every SDK call through one entry point and mirrors any setters the SDK doesn't expose as getters (consent flags) into `UserDefaults`
+- `NotificationSender` (singleton, `App/Services/NotificationSender.swift`) wraps the `/notifications` REST endpoint with `URLSession` and retries with exponential backoff when the API returns 200 with empty `id` / `recipients == 0` / a non-empty `errors` payload (transient race between subscription create and notification fan-out)
+- `UserFetchService` (singleton, `App/Services/UserFetchService.swift`) hydrates aliases / tags / emails / SMS via `GET /users/by/onesignal_id/{id}` -- no auth header, public endpoint
+- `LiveActivityController` (`App/Services/LiveActivityController.swift`) wraps `OneSignal.LiveActivities.startDefault(...)` plus the authenticated REST update/end calls. Reads the API key via `SecretsConfig.apiKey`; missing/empty key disables UPDATE / END
+- `SecretsConfig` (`App/Services/SecretsConfig.swift`) reads `ONESIGNAL_APP_ID` and `ONESIGNAL_API_KEY` from a bundled `Secrets.plist` (iOS equivalent of `.env`); both keys optional, app ID falls back to a placeholder when missing
+- `TooltipService` (singleton, `App/Services/TooltipService.swift`) loads the shared tooltip JSON from `sdk-shared` on a detached task with a bundled fallback so the first render isn't blocked
+
+### SDK initialization
+
+`AppDelegate.application(_:didFinishLaunchingWithOptions:)` in `App/App.swift` is intentionally minimal:
+
+```swift
+OneSignalService.shared.initialize(launchOptions: launchOptions)
+setupNotificationListeners()
+setupInAppMessageListeners()
+if #available(iOS 16.1, *) { LiveActivityController.setup() }
+```
+
+- `OneSignalService.initialize(launchOptions:)` mirrors the Capacitor `useOneSignal` startup order so toggles persist across cold launches:
+ 1. `OneSignal.Debug.setLogLevel(.LL_VERBOSE)`
+ 2. `OneSignal.setConsentRequired(prefs.getConsentRequired())`
+ 3. `OneSignal.setConsentGiven(prefs.getConsentGiven())`
+ 4. `OneSignal.initialize(appId, withLaunchOptions:)`
+ 5. `OneSignal.InAppMessages.paused = prefs.getIamPaused()`
+ 6. `OneSignal.Location.isShared = prefs.getLocationShared()`
+ 7. If `prefs.getExternalUserId()` is non-nil: `OneSignal.login(storedExternalId)`
+- `LiveActivityController.setup()` wraps `OneSignal.LiveActivities.setupDefault()` (iOS 16.1+ guard lives in the controller, not inline)
+- The four SDK listeners (`NotificationLifecycleHandler`, `NotificationClickHandler`, `InAppMessageLifecycleHandler`, `InAppMessageClickHandler`) are registered via `OneSignal.Notifications.add*Listener(...)` / `OneSignal.InAppMessages.add*Listener(...)` from the `setupNotificationListeners` / `setupInAppMessageListeners` helpers
+
+`PreferencesService` (`App/Services/PreferencesService.swift`) is the demo's UserDefaults-backed cache, keyed under `onesignal.demo.*`. It's the single source of truth for any state the demo needs to restore on a fresh launch: consent flags, IAM-paused, location-shared, and the last-logged-in external user id. Setters on `OneSignalService` read-through and write-through this cache (in addition to forwarding to the SDK), so the view model's `@Published` props can hydrate from `service.consentRequired` / `service.consentGiven` / `service.isInAppMessagesPaused` / `service.isLocationShared` and get cached values on cold launch.
+
+Push subscription id, opt-in, notification permission, and the live `OneSignal.User.externalId` are still read directly from the SDK at runtime (they don't need preference caching).
+
+---
+
+## iOS-Specific UI Details
+
+### Notification Permission
+
+- `OneSignalViewModel` exposes `promptPushPermission()` (no `isReady` gate, no separate `promptPush()` method)
+- `ContentView` auto-prompts on first appear via an unconditional `.task { viewModel.promptPushPermission() }` modifier on the root view -- this races the OneSignal iOS-params response so the standard alert shows before the SDK can register provisional auth
+- `PushSection` renders a conditional `PROMPT PUSH` button that calls `viewModel.promptPushPermission()`. The button is hidden once `hasNotificationPermission == true`
+
+### Loading State
+
+- `isLoading` is currently dead state in the view model -- it's flipped inside `fetchUserDataFromApi()` and `login(externalId:)` but no file under `App/Views/` references it. The Aliases / Emails / SMS / Tags sections always render their static empty-state copy via `PairList` / `SingleList` regardless of fetch state
+- Stale-result protection: `fetchUserDataFromApi()` increments a `requestSequence` counter on entry, captures the value, and short-circuits after the `await` if a newer fetch has run in the meantime. Mirrors the `requestSequenceRef` pattern from the Capacitor demo so back-to-back logout / login flows don't get overwritten by a slow earlier fetch
+
+### Toast
+
+- `ToastPresenter` (`App/ViewModels/ToastPresenter.swift`) is a `@MainActor` `ObservableObject` with `@Published var message: String?` and a `show(_:)` method. It is created as a `@StateObject` in `App.swift` and injected into `ContentView` via `.environmentObject(toastPresenter)`.
+- Section views declare `@EnvironmentObject var toast: ToastPresenter` and call `toast.show(...)` from action handlers. Only Outcomes, Custom Events, and Location check trigger the toast; everything else uses `print()` only.
+- `ContentView` attaches the host `.toast(message: $toast.message)` modifier (defined in `App/Views/Components/ToastView.swift`) so a single host renders the current message regardless of which section emitted it.
+- Replace-on-show: `show(_:)` cancels the previous `dismissTask`, sets `self.message`, and starts a new `Task` that sleeps `ToastPresenter.toastDurationMs` (milliseconds) and clears `message` only if it still matches the captured target string.
+- Duration is the static constant `static let toastDurationMs: UInt64 = 3_000` (milliseconds).
+- `OneSignalViewModel` must not hold any toast state, expose `toastMessage`, or call a `showToast` method.
+
+### Dialogs
+
+- Tooltip state lives on the view model as `@Published var activeTooltip: TooltipData?`. `ContentView` owns layout only and binds the tooltip dialog via `viewModel.activeTooltip` / `viewModel.dismissTooltip()` attached with `.osCenteredDialog`. Sections call `viewModel.showTooltip(for:)` from info icons.
+- Sections declare `@State` booleans for their action dialogs (`@State private var addOpen = false`, `@State private var loginOpen = false`, ...) and attach `.osCenteredDialog(isPresented: $addOpen) { AddItemDialog(...) }` on the section view. Dialog confirm handlers call ViewModel SDK methods and (where applicable) `toast.show(...)`.
+- `OneSignalViewModel` must not hold any action dialog visibility flags or dialog input drafts.
+- `osCenteredDialog` (in `App/Views/Components/OSDialog.swift`) is implemented on top of `.fullScreenCover` with a `ClearBackgroundView` (`UIViewRepresentable`) so the dialog presents at the window level instead of being clipped to the section's frame inside `ScrollView`. The default slide-up animation is suppressed via `.transaction { $0.disablesAnimations = true }` so the dialog's own fade-in is preserved.
+- Shared dialog primitives live in `App/Views/Components/`: `AddItemDialog` (typed via `AddItemType` -- single-field and pair layouts both flow through it), `MultiPairInputDialog`, `RemoveMultiDialog`, `OutcomeDialog`, `CustomNotificationDialog`, `TrackEventDialog`, `TooltipDialog`. Sections import and compose them locally.
+
+### Accessibility (Appium)
+
+Apply test ids with SwiftUI's `.accessibilityIdentifier("…")` modifier on every interactive element and value display. The ids match the `data-testid` values used by the Capacitor / React Native / Cordova demos one-for-one so the shared Appium suite under `sdk-shared/appium/tests/` runs unchanged against the iOS build.
+
+XCUITest does NOT inherit identifiers from `Button(role:)` automatically -- set `.accessibilityIdentifier(...)` on every `Button`, `Toggle`, `TextField`, and the wrapping `VStack` of each section.
+
+- `ContentView` anchors `accessibilityIdentifier("main_scroll_view")` on the SwiftUI `ScrollView` itself (not the inner `VStack`) so XCUITest exposes it as `XCUIElementTypeScrollView` with the visible viewport's rect. The shared Appium swipe workaround on iOS depends on this anchoring -- attaching the id to the inner stack reports the full content rect (multiple screens tall) and WDIO `swipe` then computes gestures outside the viewport, which iOS clips to the visible region and registers as taps on whatever button sits there.
+- `ContentView` runs the auto push-permission prompt via `.task { viewModel.promptPushPermission() }` on mount. It races the OneSignal iOS-params response, so the standard alert can show before the SDK registers provisional auth (which would otherwise silently grant permission and skip the prompt entirely).
+
+### Branding assets
+
+`App/Assets.xcassets/` ships three branded asset folders alongside the standard `AppIcon` / `AccentColor`:
+
+- `LaunchBackground.colorset` -- referenced by `UILaunchScreen.UIColorName` in `App/Info.plist`
+- `onesignal_launch_icon.imageset` -- referenced by `UILaunchScreen.UIImageName`
+- `onesignal_logo.imageset` -- rendered as a template image in the `ContentView` toolbar's principal placement
+
+---
+
+## Xcode Project Targets
+
+### Notification Service Extension
+
+`OneSignalNotificationServiceExtension/NotificationService.swift` forwards every push to `OneSignalExtension` so rich attachments (`ios_attachments`), confidential pushes, and `mutable_content` payloads work:
+
+```swift
+override func didReceive(_ request: UNNotificationRequest,
+ withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
+ self.receivedRequest = request
+ self.contentHandler = contentHandler
+ self.bestAttemptContent = request.content.mutableCopy() as? UNMutableNotificationContent
+
+ if let bestAttemptContent = bestAttemptContent {
+ OneSignalExtension.didReceiveNotificationExtensionRequest(
+ self.receivedRequest, with: bestAttemptContent, withContentHandler: contentHandler)
+ }
+}
+
+override func serviceExtensionTimeWillExpire() {
+ if let contentHandler, let bestAttemptContent {
+ OneSignalExtension.serviceExtensionTimeWillExpireRequest(receivedRequest, with: bestAttemptContent)
+ contentHandler(bestAttemptContent)
+ }
+}
+```
+
+The NSE entitlements file (`OneSignalNotificationServiceExtension/OneSignalNotificationServiceExtension.entitlements`) **must** declare the same `com.apple.security.application-groups` value as the main app — both ship with `group.com.onesignal.example.onesignal`. If you change the group to install on a real device under your own team, change it in BOTH files to the same string.
+
+### Widget Extension (Live Activities)
+
+`OneSignalWidget/OneSignalWidgetLiveActivity.swift` renders the order tracking flow using `DefaultLiveActivityAttributes` from `OneSignalLiveActivities`. Replace the file with the shared reference implementation at `https://raw.githubusercontent.com/OneSignal/sdk-shared/main/demo/LiveActivity.swift` whenever the canonical version is updated.
+
+The widget target's deployment target is `16.2` (project-wide is `16.0`) because Dynamic Island APIs require 16.2. `NSSupportsLiveActivities = true` is declared in `App/Info.plist`.
+
+---
+
+## Platform Config
+
+### Entitlements
+
+`App.entitlements` (main app):
+
+```xml
+aps-environment
+development
+com.apple.security.application-groups
+
+ group.com.onesignal.example.onesignal
+
+```
+
+`OneSignalNotificationServiceExtension.entitlements` mirrors the same app group. Both must match or rich pushes fail silently.
+
+### Info.plist
+
+`App/Info.plist` declares:
+
+- `NSLocationWhenInUseUsageDescription` and `NSLocationAlwaysAndWhenInUseUsageDescription` -- required for the Location section's prompt
+- `NSSupportsLiveActivities = true` -- required for the Live Activity section
+- `NSSupportsLiveActivitiesFrequentUpdates = true` -- enables high-frequency push updates to running activities
+- `UIBackgroundModes` with `remote-notification` -- required for silent / background pushes
+- `UILaunchScreen` references the `LaunchBackground` color set and `onesignal_launch_icon` image set bundled in `App/Assets.xcassets/`
+
+### Custom Notification Sound
+
+The demo bundles `examples/demo/App/vine_boom.wav` (sourced from [sdk-shared/assets](https://github.com/OneSignal/sdk-shared/tree/main/assets)). XcodeGen picks it up automatically via the `sources: - path: App` block, and `NotificationSender.swift`'s WITH SOUND payload sets `ios_sound = "vine_boom.wav"` to play it.
+
+### Credentials (App ID & REST API key)
+
+The iOS demo does NOT use a `.env` file. Instead, `App/Services/SecretsConfig.swift` reads both `ONESIGNAL_APP_ID` and `ONESIGNAL_API_KEY` from a single `Secrets.plist` bundled with the App target — the iOS-idiomatic equivalent of `.env`:
+
+```xml
+
+ ONESIGNAL_APP_ID
+ YOUR_APP_ID
+ ONESIGNAL_API_KEY
+ YOUR_REST_API_KEY
+
+```
+
+- `ONESIGNAL_APP_ID` — optional. Falls back to `SecretsConfig.defaultAppId` (the placeholder defined in `sdk-shared/demo/build.md`) when missing or empty. `OneSignalService.shared.appId` is captured from `SecretsConfig.appId` once during `init`, so the value is stable for the running session.
+- `ONESIGNAL_API_KEY` — optional, only needed for Live Activity **update** / **end**. `LiveActivityController.hasApiKey` is `true` when set; otherwise the UPDATE / END buttons disable themselves and show a hint in the Live Activity section.
+
+`Secrets.plist` is gitignored.
+
+---
+
+## File Structure
+
+```
+examples/demo/
+├── App.xcodeproj # Generated by `xcodegen generate`
+├── project.yml # XcodeGen project definition
+├── App.entitlements # aps-environment + app group
+├── Build.xcconfig # Root xcconfig wired into every target;
+│ # only does `#include? "Local.xcconfig"`
+├── Local.xcconfig.example # Per-developer overrides template
+│ # (DEVELOPMENT_TEAM, CODE_SIGN_STYLE, ...)
+├── build.md # This file
+├── README.md
+├── App/ # Main app target source
+│ ├── App.swift # @main + AppDelegate; calls
+│ │ # OneSignalService.shared.initialize,
+│ │ # registers NotificationLifecycleHandler /
+│ │ # NotificationClickHandler /
+│ │ # InAppMessageLifecycleHandler /
+│ │ # InAppMessageClickHandler, runs
+│ │ # LiveActivityController.setup() on iOS 16.1+
+│ ├── Info.plist
+│ ├── Secrets.plist # gitignored optional; ONESIGNAL_APP_ID +
+│ │ # ONESIGNAL_API_KEY consumed by SecretsConfig
+│ ├── vine_boom.wav # gitignored optional; custom notification sound
+│ ├── Assets.xcassets/
+│ │ ├── AppIcon.appiconset/
+│ │ ├── AccentColor.colorset/
+│ │ ├── LaunchBackground.colorset/ # UILaunchScreen background color
+│ │ ├── onesignal_launch_icon.imageset/ # UILaunchScreen image
+│ │ └── onesignal_logo.imageset/ # Used by ContentView toolbar
+│ ├── Models/
+│ │ └── AppModels.swift # KeyValueItem, NotificationType,
+│ │ # AddItemType, MultiAddItemType,
+│ │ # RemoveMultiItemType, OutcomeMode,
+│ │ # TooltipData, UserData
+│ ├── ViewModels/
+│ │ ├── OneSignalViewModel.swift # @MainActor ObservableObject, holds
+│ │ │ # @Published activeTooltip, drives REST
+│ │ │ # fetches via UserFetchService, registers
+│ │ │ # SDK observers via private setupObservers()
+│ │ └── ToastPresenter.swift # @MainActor ObservableObject; @Published
+│ │ # message + show() with replace-on-show
+│ ├── Services/
+│ │ ├── OneSignalService.swift # Thin wrapper over OneSignal.* APIs
+│ │ ├── SecretsConfig.swift # Reads ONESIGNAL_APP_ID / ONESIGNAL_API_KEY
+│ │ │ # from Secrets.plist with defaults
+│ │ ├── NotificationSender.swift # /notifications POST + transient-retry loop
+│ │ ├── UserFetchService.swift # /users GET, parses identity + tags + subs
+│ │ ├── TooltipService.swift # Loads sdk-shared tooltip JSON (with fallback)
+│ │ └── LiveActivityController.swift # OneSignal.LiveActivities + REST update/end
+│ └── Views/
+│ ├── ContentView.swift # NavigationStack + ScrollView; layout +
+│ │ # auto push-permission `.task` + tooltip dialog
+│ │ # via viewModel.activeTooltip; sections own
+│ │ # action dialog state
+│ ├── Theme.swift # Design tokens from sdk-shared/demo/styles.md
+│ ├── Sections/
+│ │ ├── AppSection.swift
+│ │ ├── UserSection.swift
+│ │ ├── PushSection.swift
+│ │ ├── SendPushSection.swift
+│ │ ├── InAppSection.swift
+│ │ ├── SendIamSection.swift
+│ │ ├── AliasesSection.swift
+│ │ ├── EmailsSection.swift
+│ │ ├── SmsSection.swift
+│ │ ├── TagsSection.swift
+│ │ ├── OutcomesSection.swift
+│ │ ├── TriggersSection.swift
+│ │ ├── CustomEventsSection.swift
+│ │ ├── LocationSection.swift
+│ │ └── LiveActivitySection.swift
+│ └── Components/
+│ ├── SectionCard.swift
+│ ├── ActionButton.swift
+│ ├── ToggleRow.swift
+│ ├── ListWidgets.swift # PairList + SingleList; private helpers
+│ │ # ListCardEmpty, ItemDivider, DeleteButton,
+│ │ # MoreLink. No LoadingState / CollapsibleList
+│ ├── KeyValueRow.swift # Filename vs type name differ -- type is
+│ │ # `InfoRow` (currently unused in demo)
+│ ├── OSDialog.swift # osCenteredDialog modifier + ClearBackgroundView
+│ ├── AddItemDialog.swift # Single + Pair input dialogs (typed via AddItemType)
+│ ├── MultiPairInputDialog.swift # Bulk add (aliases / tags / triggers)
+│ ├── RemoveMultiDialog.swift # Bulk remove (tags / triggers)
+│ ├── OutcomeDialog.swift # Normal / Unique / With Value
+│ ├── CustomNotificationDialog.swift
+│ ├── TrackEventDialog.swift # Name + JSON properties, validates JSON
+│ ├── TooltipDialog.swift
+│ └── ToastView.swift # toast(message:) host modifier
+│
+├── OneSignalNotificationServiceExtension/ # NSE target -- rich push
+│ ├── NotificationService.swift # Forwards to OneSignalExtension
+│ ├── Info.plist # NSExtension/usernotifications.service
+│ └── OneSignalNotificationServiceExtension.entitlements # MUST match main app group
+│
+└── OneSignalWidget/ # Widget Extension target -- Live Activities
+ ├── OneSignalWidgetBundle.swift # @main WidgetBundle
+ ├── OneSignalWidgetLiveActivity.swift # Lock screen + Dynamic Island UI for
+ │ # DefaultLiveActivityAttributes
+ ├── Info.plist # NSExtension/widgetkit-extension
+ └── Assets.xcassets/ # WidgetBackground, AccentColor, AppIcon
+```
+
+---
+
+## iOS Best Practices
+
+- Re-run `xcodegen generate` after any change to `project.yml` so `App.xcodeproj` stays in sync. Commit the regenerated project file with the YAML change.
+- Always link the SDK frameworks through the workspace's `projectReferences` (not via SPM or CocoaPods inside the demo) so the demo builds against your local SDK edits without an extra publish step.
+- Keep the app group string identical in `App.entitlements` AND `OneSignalNotificationServiceExtension.entitlements` — they MUST match for confidential pushes and badge sync.
+- Embed and code-sign each SDK framework on the App target only; the NSE and Widget targets must link the frameworks they need without embedding (the App target owns them in `Frameworks/`).
+- Consent / IAM-paused / location-shared restore is NOT implemented in `App.swift` today. The view model only tracks UI toggle state in `Cached*` UserDefaults keys; the SDK side mirrors its own writes through separate `OneSignal*` UserDefaults keys via `OneSignalService`, and the two key sets are not synced. The SDK is the source of truth for everything else (push subscription id, external id, permission, tags) -- read it directly instead of caching.
+- Use `OneSignal.User.pushSubscription.optIn()` / `optOut()` rather than touching `optedIn` directly; the SDK applies side effects (token registration, server sync) inside the methods.
+- Drive `fetchUserDataFromApi` from the `OSUserStateObserver` only — never call it synchronously right after `OneSignal.login(...)`. The SDK assigns the new `onesignalId` asynchronously, so a synchronous fetch races the assignment and returns null.
+- Set `.accessibilityIdentifier(...)` on every interactive control and value display you want to drive from Appium / XCUITest. SwiftUI does not derive identifiers from button titles, and the shared E2E suite selects by identifier.
+- Bundle `Secrets.plist` with the App target for the Live Activity REST calls; without it the section disables UPDATE / END instead of failing at runtime.
diff --git a/examples/demo/project.yml b/examples/demo/project.yml
new file mode 100644
index 000000000..c79bf193f
--- /dev/null
+++ b/examples/demo/project.yml
@@ -0,0 +1,161 @@
+name: App
+options:
+ bundleIdPrefix: com.onesignal.example
+ deploymentTarget:
+ iOS: "16.0"
+ developmentLanguage: en
+ createIntermediateGroups: true
+ generateEmptyDirectories: false
+ groupSortPosition: top
+configs:
+ Debug: debug
+ Release: release
+settings:
+ base:
+ SWIFT_VERSION: "5.9"
+ CURRENT_PROJECT_VERSION: "1"
+ MARKETING_VERSION: "1.0"
+ CODE_SIGN_STYLE: Automatic
+ DEVELOPMENT_TEAM: ""
+ GENERATE_INFOPLIST_FILE: NO
+projectReferences:
+ OneSignalSDK:
+ path: ../../iOS_SDK/OneSignalSDK/OneSignal.xcodeproj
+targets:
+ # ---------------------------------------------------------------------------
+ # Main app
+ # ---------------------------------------------------------------------------
+ App:
+ type: application
+ platform: iOS
+ deploymentTarget: "16.0"
+ configFiles:
+ Debug: Build.xcconfig
+ Release: Build.xcconfig
+ sources:
+ - path: App
+ excludes:
+ - "Info.plist"
+ - "**/*.entitlements"
+ # Handled separately below so we can force buildPhase: resources.
+ # XcodeGen defaults .plist files to BuildPhase.none (assumes
+ # Info.plist-like), which leaves Secrets.plist out of Copy Bundle
+ # Resources and Bundle.main.url(...) returns nil at runtime.
+ - "Secrets.plist"
+ # Credentials read by App/Services/SecretsConfig.swift at runtime.
+ # Gitignored and auto-written by sdk-shared/appium/scripts/run-local.sh
+ # when ONESIGNAL_APP_ID / ONESIGNAL_API_KEY are set; manually populated
+ # otherwise (see README step 4). optional: true so xcodegen succeeds on
+ # a fresh clone where the file hasn't been created yet.
+ - path: App/Secrets.plist
+ buildPhase: resources
+ optional: true
+ settings:
+ base:
+ PRODUCT_NAME: "$(TARGET_NAME)"
+ PRODUCT_BUNDLE_IDENTIFIER: com.onesignal.example
+ INFOPLIST_FILE: App/Info.plist
+ CODE_SIGN_ENTITLEMENTS: App.entitlements
+ TARGETED_DEVICE_FAMILY: "1,2"
+ LD_RUNPATH_SEARCH_PATHS: "$(inherited) @executable_path/Frameworks"
+ ENABLE_USER_SCRIPT_SANDBOXING: NO
+ dependencies:
+ # SDK Swift framework targets (built from local source via the workspace)
+ - target: OneSignalSDK/OneSignalCore
+ embed: true
+ codeSign: true
+ - target: OneSignalSDK/OneSignalOSCore
+ embed: true
+ codeSign: true
+ - target: OneSignalSDK/OneSignalOutcomes
+ embed: true
+ codeSign: true
+ - target: OneSignalSDK/OneSignalNotifications
+ embed: true
+ codeSign: true
+ - target: OneSignalSDK/OneSignalUser
+ embed: true
+ codeSign: true
+ - target: OneSignalSDK/OneSignalExtension
+ embed: true
+ codeSign: true
+ - target: OneSignalSDK/OneSignalLocation
+ embed: true
+ codeSign: true
+ - target: OneSignalSDK/OneSignalInAppMessages
+ embed: true
+ codeSign: true
+ - target: OneSignalSDK/OneSignalLiveActivities
+ embed: true
+ codeSign: true
+ - target: OneSignalSDK/OneSignalFramework
+ embed: true
+ codeSign: true
+ # Local app extensions
+ - target: OneSignalNotificationServiceExtension
+ embed: true
+ codeSign: true
+ - target: OneSignalWidget
+ embed: true
+ codeSign: true
+
+ # ---------------------------------------------------------------------------
+ # Notification Service Extension (rich push)
+ # ---------------------------------------------------------------------------
+ OneSignalNotificationServiceExtension:
+ type: app-extension
+ platform: iOS
+ deploymentTarget: "16.0"
+ configFiles:
+ Debug: Build.xcconfig
+ Release: Build.xcconfig
+ sources:
+ - path: OneSignalNotificationServiceExtension
+ excludes:
+ - "Info.plist"
+ - "**/*.entitlements"
+ settings:
+ base:
+ PRODUCT_NAME: "$(TARGET_NAME)"
+ PRODUCT_BUNDLE_IDENTIFIER: com.onesignal.example.NSE
+ INFOPLIST_FILE: OneSignalNotificationServiceExtension/Info.plist
+ CODE_SIGN_ENTITLEMENTS: OneSignalNotificationServiceExtension/OneSignalNotificationServiceExtension.entitlements
+ TARGETED_DEVICE_FAMILY: "1,2"
+ LD_RUNPATH_SEARCH_PATHS: "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"
+ SKIP_INSTALL: YES
+ dependencies:
+ - target: OneSignalSDK/OneSignalCore
+ embed: false
+ - target: OneSignalSDK/OneSignalOutcomes
+ embed: false
+ - target: OneSignalSDK/OneSignalExtension
+ embed: false
+
+ # ---------------------------------------------------------------------------
+ # Widget Extension (Live Activities)
+ # ---------------------------------------------------------------------------
+ OneSignalWidget:
+ type: app-extension
+ platform: iOS
+ deploymentTarget: "16.2"
+ configFiles:
+ Debug: Build.xcconfig
+ Release: Build.xcconfig
+ sources:
+ - path: OneSignalWidget
+ excludes:
+ - "Info.plist"
+ - "**/*.entitlements"
+ settings:
+ base:
+ PRODUCT_NAME: "$(TARGET_NAME)"
+ PRODUCT_BUNDLE_IDENTIFIER: com.onesignal.example.LA
+ INFOPLIST_FILE: OneSignalWidget/Info.plist
+ TARGETED_DEVICE_FAMILY: "1,2"
+ LD_RUNPATH_SEARCH_PATHS: "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"
+ SKIP_INSTALL: YES
+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME: AccentColor
+ ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME: WidgetBackground
+ dependencies:
+ - target: OneSignalSDK/OneSignalLiveActivities
+ embed: false
diff --git a/iOS_SDK/OneSignalSDK.xcworkspace/contents.xcworkspacedata b/iOS_SDK/OneSignalSDK.xcworkspace/contents.xcworkspacedata
index 9e7617845..f182a1cc5 100644
--- a/iOS_SDK/OneSignalSDK.xcworkspace/contents.xcworkspacedata
+++ b/iOS_SDK/OneSignalSDK.xcworkspace/contents.xcworkspacedata
@@ -1,13 +1,13 @@
-
-
+
+
diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalNotificationServiceExtension/NotificationService.swift b/iOS_SDK/OneSignalSwiftUIExample/OneSignalNotificationServiceExtension/NotificationService.swift
deleted file mode 100644
index d536bdaff..000000000
--- a/iOS_SDK/OneSignalSwiftUIExample/OneSignalNotificationServiceExtension/NotificationService.swift
+++ /dev/null
@@ -1,37 +0,0 @@
-import UserNotifications
-import OneSignalExtension
-
-class NotificationService: UNNotificationServiceExtension {
-
- var contentHandler: ((UNNotificationContent) -> Void)?
- var receivedRequest: UNNotificationRequest?
- var bestAttemptContent: UNMutableNotificationContent?
-
- override func didReceive(
- _ request: UNNotificationRequest,
- withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void
- ) {
- self.receivedRequest = request
- self.contentHandler = contentHandler
- self.bestAttemptContent = request.content.mutableCopy() as? UNMutableNotificationContent
-
- if let bestAttemptContent = bestAttemptContent {
- OneSignalExtension.didReceiveNotificationExtensionRequest(
- request,
- with: bestAttemptContent,
- withContentHandler: contentHandler
- )
- }
- }
-
- override func serviceExtensionTimeWillExpire() {
- if let contentHandler = contentHandler,
- let bestAttemptContent = bestAttemptContent {
- OneSignalExtension.serviceExtensionTimeWillExpireRequest(
- receivedRequest!,
- with: bestAttemptContent
- )
- contentHandler(bestAttemptContent)
- }
- }
-}
diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample.xcodeproj/project.pbxproj b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample.xcodeproj/project.pbxproj
deleted file mode 100644
index a5578aa23..000000000
--- a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample.xcodeproj/project.pbxproj
+++ /dev/null
@@ -1,960 +0,0 @@
-// !$*UTF8*$!
-{
- archiveVersion = 1;
- classes = {
- };
- objectVersion = 63;
- objects = {
-
-/* Begin PBXBuildFile section */
- 076B2F5C2E9B739160493063 /* UserFetchService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59FD3768669ADB5E8E927B7F /* UserFetchService.swift */; };
- 0D666446CA5476DB5AC260EA /* OneSignalLiveActivities.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = A265470C544D20273EEB62B0 /* OneSignalLiveActivities.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
- 104E342C46E0870F7E30FB29 /* CoreLocation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 56C25916C718509D05DF03B2 /* CoreLocation.framework */; };
- 12A93EBB7D6C97B82814924A /* UserNotifications.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6CD2DA9DC09F1EF7D989C291 /* UserNotifications.framework */; };
- 14AB26AE3A2FF05C9F62CCD2 /* CustomNotificationSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF680E9246E2AE4D179CD3E /* CustomNotificationSheet.swift */; };
- 19FA9DF58988997FD6F5BD2A /* OneSignalLiveActivities.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A265470C544D20273EEB62B0 /* OneSignalLiveActivities.framework */; };
- 1D68D16D167B951BD57386ED /* OneSignalSwiftUIExampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE9826D274F62E747F3A931B /* OneSignalSwiftUIExampleApp.swift */; };
- 1FC9DF3B1B444569E585CA2B /* OneSignalNotificationServiceExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 50B6C9352575073F1873F639 /* OneSignalNotificationServiceExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
- 228DE2E45BE2EEA72F9436F3 /* SystemConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 985DE7F1C6162433D11943B0 /* SystemConfiguration.framework */; };
- 23578DF36320EFEB17E12FFA /* OneSignalInAppMessages.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7A6BA7282C93D0714AB7AE9D /* OneSignalInAppMessages.framework */; };
- 25D4FC203BE01E9A35ACA465 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = FEE98DA6A075AEB7709CCBE2 /* Assets.xcassets */; };
- 2E6D6E17EEE5D42AE3CAB9EA /* AddItemSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = F42140C8B10F3D06BDE1C79E /* AddItemSheet.swift */; };
- 2F52BD9F2A3002390A7C8896 /* OneSignalViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD249AC2E0DD1F282E33E3BF /* OneSignalViewModel.swift */; };
- 36AD6B646EA553AD54024FAC /* OneSignalLocation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B5B39E8FA865B4030F3009B4 /* OneSignalLocation.framework */; };
- 39D261592E207BCB8F453E6E /* OneSignalOSCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 501ED810C4C9B04785D4CCD4 /* OneSignalOSCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
- 3C25C0592F3409E9005E5E9A /* NotificationSender.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C25C0582F3409E9005E5E9A /* NotificationSender.swift */; };
- 44345BEEC3249B530635D91C /* TrackEventSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BBEDE187CB941C13F384CF0 /* TrackEventSection.swift */; };
- 4840204E727B4F405094C542 /* OneSignalExtension.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = B441216A875FE941AED4964E /* OneSignalExtension.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
- 49E74AFBFBE23D06B7D310EC /* SubscriptionSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09DF983228037C1B729C1419 /* SubscriptionSection.swift */; };
- 56FA425BC3C515132CF2098F /* OneSignalUser.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 7B9F08F02892844599AA8BDD /* OneSignalUser.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
- 59BF581E9C89F1D09BB5130A /* OneSignalOutcomes.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4E815F87FE680572C74E9D51 /* OneSignalOutcomes.framework */; };
- 5EEA4B007D60765502F2A6E5 /* MessagingSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 962607A12EFC53D2F77E5948 /* MessagingSection.swift */; };
- 644520A37F05E25FD17F5CF9 /* AppModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45255F2C6EB0B13E199BDC65 /* AppModels.swift */; };
- 6486DBFD33978F37842B8326 /* TooltipService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F6B2BF7C4E1DDEA6AD4AF40B /* TooltipService.swift */; };
- 6C7913B91320573B2AE46212 /* RemoveMultiSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = E77A427C94B9932B08E72DA9 /* RemoveMultiSheet.swift */; };
- 74920778F254B9DA27906AA3 /* AppInfoSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 564304E2BA4613FD7649116D /* AppInfoSection.swift */; };
- 7C93699D746B0B5807AD13BB /* NextScreenSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55F91FF35087706DBF9AD3BA /* NextScreenSection.swift */; };
- 7F7A92F525620CDF81EC46BE /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93A7E57EF192E9C9385A1484 /* NotificationService.swift */; };
- 85BAC3A9462800CB3A23C362 /* LogManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D676BEDC8A5CB48BBF8FFAF9 /* LogManager.swift */; };
- 861FCEE8AFB371A2FA3BCC93 /* OneSignalOSCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 501ED810C4C9B04785D4CCD4 /* OneSignalOSCore.framework */; };
- 8B3A2DC19BD16993EC4B617B /* GuidanceBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52B06024858612EFCB3F42CC /* GuidanceBanner.swift */; };
- 8EF4836F9252E9C8180804AB /* OneSignalOutcomes.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 4E815F87FE680572C74E9D51 /* OneSignalOutcomes.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
- 8F3FE40D8D603A8879C6A111 /* TrackEventSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C56E78CB208A9F1F53318D8 /* TrackEventSheet.swift */; };
- 8F69162AE31452F3956160BE /* OneSignalUser.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7B9F08F02892844599AA8BDD /* OneSignalUser.framework */; };
- 959F9DFCC1031992FB28E771 /* OneSignalCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 533347B40563BF22FB6EAC9C /* OneSignalCore.framework */; };
- 9EF9D8D46AAAD6F93B32FDCB /* OneSignalLocation.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = B5B39E8FA865B4030F3009B4 /* OneSignalLocation.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
- A8954DAC09761A3125EF37D3 /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A084A32F6E0CA6A6354705C0 /* WebKit.framework */; };
- A92101FA384AC15596D4A659 /* OneSignalNotifications.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E0805F3793886C9122DFB6E4 /* OneSignalNotifications.framework */; };
- AD19D28FDE8DAA7C8BC87939 /* LocationSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 60D373C8C00DE06FC5CD01C5 /* LocationSection.swift */; };
- B13E9460BDD0553FF05542A0 /* NotificationGrid.swift in Sources */ = {isa = PBXBuildFile; fileRef = 414EE3451468D29D4A9C87AC /* NotificationGrid.swift */; };
- B5C4A4AC0A2D2AF73C9A7DD3 /* KeyValueRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 41A4F866BFA1EE429ED1CECC /* KeyValueRow.swift */; };
- B86C1BD47DFEC7567B004FE8 /* OneSignalExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B441216A875FE941AED4964E /* OneSignalExtension.framework */; };
- BAE6133523D7D07386BD3172 /* AddMultiItemSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5ACF918029F336B9C36BD1C /* AddMultiItemSheet.swift */; };
- C9884D12CEDE50ACAE38DF0D /* NotificationSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = E237FD3F84CE3831CAB8A6DA /* NotificationSection.swift */; };
- C994C923037F602FA48A1490 /* OneSignalFramework.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FB1399100ED5DD83ED6CD889 /* OneSignalFramework.framework */; };
- CB5F82751F156695A7A03338 /* OneSignalService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85FF4424D702D8686CC819B8 /* OneSignalService.swift */; };
- D13A25FE5762A23125227A84 /* ToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8029F22CDBDB03B2CBA14212 /* ToastView.swift */; };
- D6A555B7B420B014299B9E50 /* OneSignalOutcomes.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4E815F87FE680572C74E9D51 /* OneSignalOutcomes.framework */; };
- D9E5ED5BA2BCFF6D9233AC66 /* OneSignalCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 533347B40563BF22FB6EAC9C /* OneSignalCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
- DAA6DDE0CF432A7BE50481EC /* OneSignalNotifications.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = E0805F3793886C9122DFB6E4 /* OneSignalNotifications.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
- DB2BA39FE32DB3D52154F48C /* TagsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = C49D0DCDB581563C015CEAFF /* TagsSection.swift */; };
- DD9942D5EB98C5F70FA8D3D8 /* OneSignalInAppMessages.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 7A6BA7282C93D0714AB7AE9D /* OneSignalInAppMessages.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
- E1BEF5E30555F0ADCBF62B7D /* UserSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D22580635AB6E6BE86F1396 /* UserSection.swift */; };
- E20A2AFD25AD52384A15794A /* OneSignalFramework.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = FB1399100ED5DD83ED6CD889 /* OneSignalFramework.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
- E26208CEB7DC4D217EAAE4E6 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3BF17A9E51F5A187FFAC1AC /* ContentView.swift */; };
- EFE6A330EF362418C68B3F6E /* OneSignalCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 533347B40563BF22FB6EAC9C /* OneSignalCore.framework */; };
- F8CF0C2C1A1F8842BCF86784 /* OneSignalExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B441216A875FE941AED4964E /* OneSignalExtension.framework */; };
- FEEC78AA5DE04832192D8F92 /* LogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22B8D8F4DF8E47F8BA53423A /* LogView.swift */; };
- 63C9FA76B516816B1B0AED19 /* ExampleAppWidgetAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29513EB5D1B9AF2511CAD95A /* ExampleAppWidgetAttributes.swift */; };
- 34446FC058592648ACCD90ED /* ExampleAppWidgetAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29513EB5D1B9AF2511CAD95A /* ExampleAppWidgetAttributes.swift */; };
- 1630D207F53B85B5C5A4806C /* LiveActivityController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 503C246BD2BCA26C96E0EF56 /* LiveActivityController.swift */; };
- 2BD9DE389DE56B72D031CA01 /* OneSignalWidgetExtensionBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CE2D8F93E1D14CD1F6D3078 /* OneSignalWidgetExtensionBundle.swift */; };
- 50091A346024B3DDB1053760 /* OneSignalWidgetExtensionLiveActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99719345F5489106C54EBEA8 /* OneSignalWidgetExtensionLiveActivity.swift */; };
- 544423F60A011F8B8D2971BF /* OneSignalWidgetExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 284A0F176D491DA3AF85DD6C /* OneSignalWidgetExtension.swift */; };
- 32AEC6117F13C0D50A3314D3 /* OneSignalLiveActivities.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A265470C544D20273EEB62B0 /* OneSignalLiveActivities.framework */; };
- 2D326C2E53A5678B824B52F5 /* OneSignalWidgetExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 14520AFC1CB985AE6C04E572 /* OneSignalWidgetExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
- 6DFB09B0E23B8B3D42ED4C33 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 70C8DB47C4C80E1D595FE1DE /* WidgetKit.framework */; };
- 2A7E93C9978EA3A5DD713D88 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2B5D4431B0C1F4352A359427 /* SwiftUI.framework */; };
- 45050FAF882B8B140E005816 /* LiveActivitySection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D896003C77EC90B7A3C75BD /* LiveActivitySection.swift */; };
-/* End PBXBuildFile section */
-
-/* Begin PBXContainerItemProxy section */
- DC39EC3039BB65E2D21F753F /* PBXContainerItemProxy */ = {
- isa = PBXContainerItemProxy;
- containerPortal = 7B7016AA7DF5AB54CD96701C /* Project object */;
- proxyType = 1;
- remoteGlobalIDString = D99FA6E45A25EB4DB04DA0CE;
- remoteInfo = OneSignalNotificationServiceExtension;
- };
- EEAB30880E225535B2F366FA /* PBXContainerItemProxy */ = {
- isa = PBXContainerItemProxy;
- containerPortal = 7B7016AA7DF5AB54CD96701C /* Project object */;
- proxyType = 1;
- remoteGlobalIDString = 7729C3101598B16A32244980;
- remoteInfo = OneSignalWidgetExtension;
- };
-/* End PBXContainerItemProxy section */
-
-/* Begin PBXCopyFilesBuildPhase section */
- 510E0294CE200E84088E8CC1 /* Embed Frameworks */ = {
- isa = PBXCopyFilesBuildPhase;
- buildActionMask = 2147483647;
- dstPath = "";
- dstSubfolderSpec = 10;
- files = (
- E20A2AFD25AD52384A15794A /* OneSignalFramework.framework in Embed Frameworks */,
- D9E5ED5BA2BCFF6D9233AC66 /* OneSignalCore.framework in Embed Frameworks */,
- 4840204E727B4F405094C542 /* OneSignalExtension.framework in Embed Frameworks */,
- 8EF4836F9252E9C8180804AB /* OneSignalOutcomes.framework in Embed Frameworks */,
- 39D261592E207BCB8F453E6E /* OneSignalOSCore.framework in Embed Frameworks */,
- 56FA425BC3C515132CF2098F /* OneSignalUser.framework in Embed Frameworks */,
- DAA6DDE0CF432A7BE50481EC /* OneSignalNotifications.framework in Embed Frameworks */,
- DD9942D5EB98C5F70FA8D3D8 /* OneSignalInAppMessages.framework in Embed Frameworks */,
- 9EF9D8D46AAAD6F93B32FDCB /* OneSignalLocation.framework in Embed Frameworks */,
- 0D666446CA5476DB5AC260EA /* OneSignalLiveActivities.framework in Embed Frameworks */,
- );
- name = "Embed Frameworks";
- runOnlyForDeploymentPostprocessing = 0;
- };
- D6AA72CEF78258953FC1C74B /* Embed App Extensions */ = {
- isa = PBXCopyFilesBuildPhase;
- buildActionMask = 2147483647;
- dstPath = "";
- dstSubfolderSpec = 13;
- files = (
- 1FC9DF3B1B444569E585CA2B /* OneSignalNotificationServiceExtension.appex in Embed App Extensions */,
- 2D326C2E53A5678B824B52F5 /* OneSignalWidgetExtension.appex in Embed App Extensions */,
- );
- name = "Embed App Extensions";
- runOnlyForDeploymentPostprocessing = 0;
- };
-/* End PBXCopyFilesBuildPhase section */
-
-/* Begin PBXFileReference section */
- 09DF983228037C1B729C1419 /* SubscriptionSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionSection.swift; sourceTree = ""; };
- 22B8D8F4DF8E47F8BA53423A /* LogView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = LogView.swift; path = Views/Components/LogView.swift; sourceTree = ""; };
- 2C56E78CB208A9F1F53318D8 /* TrackEventSheet.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TrackEventSheet.swift; path = Views/Components/TrackEventSheet.swift; sourceTree = ""; };
- 3C25C0582F3409E9005E5E9A /* NotificationSender.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSender.swift; sourceTree = ""; };
- 3CCBA2632F44DD4B009AFA72 /* OneSignalNotificationServiceExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = OneSignalNotificationServiceExtension.entitlements; sourceTree = ""; };
- 414EE3451468D29D4A9C87AC /* NotificationGrid.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationGrid.swift; sourceTree = ""; };
- 41A4F866BFA1EE429ED1CECC /* KeyValueRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyValueRow.swift; sourceTree = ""; };
- 45255F2C6EB0B13E199BDC65 /* AppModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppModels.swift; sourceTree = ""; };
- 4D22580635AB6E6BE86F1396 /* UserSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSection.swift; sourceTree = ""; };
- 4E815F87FE680572C74E9D51 /* OneSignalOutcomes.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = OneSignalOutcomes.framework; sourceTree = BUILT_PRODUCTS_DIR; };
- 501ED810C4C9B04785D4CCD4 /* OneSignalOSCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = OneSignalOSCore.framework; sourceTree = BUILT_PRODUCTS_DIR; };
- 50B6C9352575073F1873F639 /* OneSignalNotificationServiceExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = OneSignalNotificationServiceExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
- 52B06024858612EFCB3F42CC /* GuidanceBanner.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = GuidanceBanner.swift; path = Views/Components/GuidanceBanner.swift; sourceTree = ""; };
- 533347B40563BF22FB6EAC9C /* OneSignalCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = OneSignalCore.framework; sourceTree = BUILT_PRODUCTS_DIR; };
- 55F91FF35087706DBF9AD3BA /* NextScreenSection.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = NextScreenSection.swift; path = Views/Sections/NextScreenSection.swift; sourceTree = ""; };
- 564304E2BA4613FD7649116D /* AppInfoSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppInfoSection.swift; sourceTree = ""; };
- 56C25916C718509D05DF03B2 /* CoreLocation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreLocation.framework; path = System/Library/Frameworks/CoreLocation.framework; sourceTree = SDKROOT; };
- 59FD3768669ADB5E8E927B7F /* UserFetchService.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = UserFetchService.swift; path = Services/UserFetchService.swift; sourceTree = ""; };
- 60D373C8C00DE06FC5CD01C5 /* LocationSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationSection.swift; sourceTree = ""; };
- 6BBEDE187CB941C13F384CF0 /* TrackEventSection.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TrackEventSection.swift; path = Views/Sections/TrackEventSection.swift; sourceTree = ""; };
- 6CD2DA9DC09F1EF7D989C291 /* UserNotifications.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UserNotifications.framework; path = System/Library/Frameworks/UserNotifications.framework; sourceTree = SDKROOT; };
- 7A6BA7282C93D0714AB7AE9D /* OneSignalInAppMessages.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = OneSignalInAppMessages.framework; sourceTree = BUILT_PRODUCTS_DIR; };
- 7B9F08F02892844599AA8BDD /* OneSignalUser.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = OneSignalUser.framework; sourceTree = BUILT_PRODUCTS_DIR; };
- 7DD2010F08A0E2483F5FDDF2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
- 8029F22CDBDB03B2CBA14212 /* ToastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastView.swift; sourceTree = ""; };
- 85FF4424D702D8686CC819B8 /* OneSignalService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OneSignalService.swift; sourceTree = ""; };
- 93A7E57EF192E9C9385A1484 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = ""; };
- 962607A12EFC53D2F77E5948 /* MessagingSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessagingSection.swift; sourceTree = ""; };
- 985DE7F1C6162433D11943B0 /* SystemConfiguration.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SystemConfiguration.framework; path = System/Library/Frameworks/SystemConfiguration.framework; sourceTree = SDKROOT; };
- A0506934E0F27B864F3AC273 /* OneSignalSwiftUIExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = OneSignalSwiftUIExample.app; sourceTree = BUILT_PRODUCTS_DIR; };
- A084A32F6E0CA6A6354705C0 /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = System/Library/Frameworks/WebKit.framework; sourceTree = SDKROOT; };
- A265470C544D20273EEB62B0 /* OneSignalLiveActivities.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = OneSignalLiveActivities.framework; sourceTree = BUILT_PRODUCTS_DIR; };
- A3BF17A9E51F5A187FFAC1AC /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; };
- A5ACF918029F336B9C36BD1C /* AddMultiItemSheet.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AddMultiItemSheet.swift; path = Views/Components/AddMultiItemSheet.swift; sourceTree = ""; };
- B441216A875FE941AED4964E /* OneSignalExtension.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = OneSignalExtension.framework; sourceTree = BUILT_PRODUCTS_DIR; };
- B5B39E8FA865B4030F3009B4 /* OneSignalLocation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = OneSignalLocation.framework; sourceTree = BUILT_PRODUCTS_DIR; };
- C49D0DCDB581563C015CEAFF /* TagsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagsSection.swift; sourceTree = ""; };
- D676BEDC8A5CB48BBF8FFAF9 /* LogManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogManager.swift; sourceTree = ""; };
- DBF680E9246E2AE4D179CD3E /* CustomNotificationSheet.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = CustomNotificationSheet.swift; path = Views/Components/CustomNotificationSheet.swift; sourceTree = ""; };
- E0805F3793886C9122DFB6E4 /* OneSignalNotifications.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = OneSignalNotifications.framework; sourceTree = BUILT_PRODUCTS_DIR; };
- E237FD3F84CE3831CAB8A6DA /* NotificationSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSection.swift; sourceTree = ""; };
- E77A427C94B9932B08E72DA9 /* RemoveMultiSheet.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RemoveMultiSheet.swift; path = Views/Components/RemoveMultiSheet.swift; sourceTree = ""; };
- F42140C8B10F3D06BDE1C79E /* AddItemSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddItemSheet.swift; sourceTree = ""; };
- F6B2BF7C4E1DDEA6AD4AF40B /* TooltipService.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TooltipService.swift; path = Services/TooltipService.swift; sourceTree = ""; };
- FB1399100ED5DD83ED6CD889 /* OneSignalFramework.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = OneSignalFramework.framework; sourceTree = BUILT_PRODUCTS_DIR; };
- FD249AC2E0DD1F282E33E3BF /* OneSignalViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OneSignalViewModel.swift; sourceTree = ""; };
- FDEE7A98A0EBB6BF767A041D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; };
- FE9826D274F62E747F3A931B /* OneSignalSwiftUIExampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OneSignalSwiftUIExampleApp.swift; sourceTree = ""; };
- FEE98DA6A075AEB7709CCBE2 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
- 14520AFC1CB985AE6C04E572 /* OneSignalWidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = OneSignalWidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
- 1CE2D8F93E1D14CD1F6D3078 /* OneSignalWidgetExtensionBundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OneSignalWidgetExtensionBundle.swift; sourceTree = ""; };
- 99719345F5489106C54EBEA8 /* OneSignalWidgetExtensionLiveActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OneSignalWidgetExtensionLiveActivity.swift; sourceTree = ""; };
- 284A0F176D491DA3AF85DD6C /* OneSignalWidgetExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OneSignalWidgetExtension.swift; sourceTree = ""; };
- 18DDEB83BD3876BA863B546E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
- 552B0F025E1F9787E6AB2C3C /* OneSignalWidgetExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = OneSignalWidgetExtension.entitlements; sourceTree = ""; };
- 29513EB5D1B9AF2511CAD95A /* ExampleAppWidgetAttributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleAppWidgetAttributes.swift; sourceTree = ""; };
- 503C246BD2BCA26C96E0EF56 /* LiveActivityController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivityController.swift; sourceTree = ""; };
- 70C8DB47C4C80E1D595FE1DE /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; };
- 2B5D4431B0C1F4352A359427 /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; };
- 2D896003C77EC90B7A3C75BD /* LiveActivitySection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivitySection.swift; sourceTree = ""; };
-/* End PBXFileReference section */
-
-/* Begin PBXFrameworksBuildPhase section */
- 70893964E2D04EE9C32A8152 /* Frameworks */ = {
- isa = PBXFrameworksBuildPhase;
- buildActionMask = 2147483647;
- files = (
- B86C1BD47DFEC7567B004FE8 /* OneSignalExtension.framework in Frameworks */,
- 959F9DFCC1031992FB28E771 /* OneSignalCore.framework in Frameworks */,
- D6A555B7B420B014299B9E50 /* OneSignalOutcomes.framework in Frameworks */,
- );
- runOnlyForDeploymentPostprocessing = 0;
- };
- D1959F94F086AC39994A96BD /* Frameworks */ = {
- isa = PBXFrameworksBuildPhase;
- buildActionMask = 2147483647;
- files = (
- C994C923037F602FA48A1490 /* OneSignalFramework.framework in Frameworks */,
- EFE6A330EF362418C68B3F6E /* OneSignalCore.framework in Frameworks */,
- F8CF0C2C1A1F8842BCF86784 /* OneSignalExtension.framework in Frameworks */,
- 59BF581E9C89F1D09BB5130A /* OneSignalOutcomes.framework in Frameworks */,
- 861FCEE8AFB371A2FA3BCC93 /* OneSignalOSCore.framework in Frameworks */,
- 8F69162AE31452F3956160BE /* OneSignalUser.framework in Frameworks */,
- A92101FA384AC15596D4A659 /* OneSignalNotifications.framework in Frameworks */,
- 23578DF36320EFEB17E12FFA /* OneSignalInAppMessages.framework in Frameworks */,
- 36AD6B646EA553AD54024FAC /* OneSignalLocation.framework in Frameworks */,
- 19FA9DF58988997FD6F5BD2A /* OneSignalLiveActivities.framework in Frameworks */,
- 104E342C46E0870F7E30FB29 /* CoreLocation.framework in Frameworks */,
- 228DE2E45BE2EEA72F9436F3 /* SystemConfiguration.framework in Frameworks */,
- 12A93EBB7D6C97B82814924A /* UserNotifications.framework in Frameworks */,
- A8954DAC09761A3125EF37D3 /* WebKit.framework in Frameworks */,
- );
- runOnlyForDeploymentPostprocessing = 0;
- };
- D37A7E2DA634F0A44E3347CD /* Frameworks */ = {
- isa = PBXFrameworksBuildPhase;
- buildActionMask = 2147483647;
- files = (
- 6DFB09B0E23B8B3D42ED4C33 /* WidgetKit.framework in Frameworks */,
- 2A7E93C9978EA3A5DD713D88 /* SwiftUI.framework in Frameworks */,
- 32AEC6117F13C0D50A3314D3 /* OneSignalLiveActivities.framework in Frameworks */,
- );
- runOnlyForDeploymentPostprocessing = 0;
- };
-/* End PBXFrameworksBuildPhase section */
-
-/* Begin PBXGroup section */
- 130E67036ECE13331B1CFFFA /* Services */ = {
- isa = PBXGroup;
- children = (
- F6B2BF7C4E1DDEA6AD4AF40B /* TooltipService.swift */,
- 59FD3768669ADB5E8E927B7F /* UserFetchService.swift */,
- );
- name = Services;
- sourceTree = "";
- };
- 1E3DC3EAE97EBB641F71FE7D /* OneSignalNotificationServiceExtension */ = {
- isa = PBXGroup;
- children = (
- 3CCBA2632F44DD4B009AFA72 /* OneSignalNotificationServiceExtension.entitlements */,
- 93A7E57EF192E9C9385A1484 /* NotificationService.swift */,
- 7DD2010F08A0E2483F5FDDF2 /* Info.plist */,
- );
- path = OneSignalNotificationServiceExtension;
- sourceTree = "";
- };
- 3D3976C085CF6E7D61D7F075 /* Products */ = {
- isa = PBXGroup;
- children = (
- A0506934E0F27B864F3AC273 /* OneSignalSwiftUIExample.app */,
- 50B6C9352575073F1873F639 /* OneSignalNotificationServiceExtension.appex */,
- 14520AFC1CB985AE6C04E572 /* OneSignalWidgetExtension.appex */,
- );
- name = Products;
- sourceTree = "";
- };
- 1464CBA90C4A8A500815FA26 /* OneSignalWidgetExtension */ = {
- isa = PBXGroup;
- children = (
- 1CE2D8F93E1D14CD1F6D3078 /* OneSignalWidgetExtensionBundle.swift */,
- 99719345F5489106C54EBEA8 /* OneSignalWidgetExtensionLiveActivity.swift */,
- 284A0F176D491DA3AF85DD6C /* OneSignalWidgetExtension.swift */,
- 18DDEB83BD3876BA863B546E /* Info.plist */,
- );
- path = OneSignalWidgetExtension;
- sourceTree = "";
- };
- 4320E847C9D875EA2342DB5F /* Models */ = {
- isa = PBXGroup;
- children = (
- 45255F2C6EB0B13E199BDC65 /* AppModels.swift */,
- );
- path = Models;
- sourceTree = "";
- };
- 5ECE11F9FF76FF6082340576 /* Sections */ = {
- isa = PBXGroup;
- children = (
- 6BBEDE187CB941C13F384CF0 /* TrackEventSection.swift */,
- 55F91FF35087706DBF9AD3BA /* NextScreenSection.swift */,
- );
- name = Sections;
- sourceTree = "";
- };
- 676D7DC4DABFC37256C328C5 /* Components */ = {
- isa = PBXGroup;
- children = (
- F42140C8B10F3D06BDE1C79E /* AddItemSheet.swift */,
- 41A4F866BFA1EE429ED1CECC /* KeyValueRow.swift */,
- 414EE3451468D29D4A9C87AC /* NotificationGrid.swift */,
- 8029F22CDBDB03B2CBA14212 /* ToastView.swift */,
- );
- path = Components;
- sourceTree = "";
- };
- 6CC0A9DDAC1CB43170E00043 /* Views */ = {
- isa = PBXGroup;
- children = (
- C5DE3E27078ADECB8D1EDA06 /* Components */,
- 5ECE11F9FF76FF6082340576 /* Sections */,
- );
- name = Views;
- sourceTree = "";
- };
- 82619F0C016CC5B46729A454 /* Frameworks */ = {
- isa = PBXGroup;
- children = (
- 56C25916C718509D05DF03B2 /* CoreLocation.framework */,
- 533347B40563BF22FB6EAC9C /* OneSignalCore.framework */,
- B441216A875FE941AED4964E /* OneSignalExtension.framework */,
- FB1399100ED5DD83ED6CD889 /* OneSignalFramework.framework */,
- 7A6BA7282C93D0714AB7AE9D /* OneSignalInAppMessages.framework */,
- A265470C544D20273EEB62B0 /* OneSignalLiveActivities.framework */,
- B5B39E8FA865B4030F3009B4 /* OneSignalLocation.framework */,
- E0805F3793886C9122DFB6E4 /* OneSignalNotifications.framework */,
- 501ED810C4C9B04785D4CCD4 /* OneSignalOSCore.framework */,
- 4E815F87FE680572C74E9D51 /* OneSignalOutcomes.framework */,
- 7B9F08F02892844599AA8BDD /* OneSignalUser.framework */,
- 985DE7F1C6162433D11943B0 /* SystemConfiguration.framework */,
- 6CD2DA9DC09F1EF7D989C291 /* UserNotifications.framework */,
- A084A32F6E0CA6A6354705C0 /* WebKit.framework */,
- 70C8DB47C4C80E1D595FE1DE /* WidgetKit.framework */,
- 2B5D4431B0C1F4352A359427 /* SwiftUI.framework */,
- );
- name = Frameworks;
- sourceTree = "";
- };
- 8DB2942ED773E3D98EEC5396 = {
- isa = PBXGroup;
- children = (
- A21AEF40FA55F829CF1DA4B4 /* OneSignalSwiftUIExample */,
- 1E3DC3EAE97EBB641F71FE7D /* OneSignalNotificationServiceExtension */,
- 1464CBA90C4A8A500815FA26 /* OneSignalWidgetExtension */,
- 552B0F025E1F9787E6AB2C3C /* OneSignalWidgetExtension.entitlements */,
- 82619F0C016CC5B46729A454 /* Frameworks */,
- 3D3976C085CF6E7D61D7F075 /* Products */,
- );
- sourceTree = "";
- };
- 92778F3DF1B0939012E946A7 /* Sections */ = {
- isa = PBXGroup;
- children = (
- 564304E2BA4613FD7649116D /* AppInfoSection.swift */,
- 2D896003C77EC90B7A3C75BD /* LiveActivitySection.swift */,
- 60D373C8C00DE06FC5CD01C5 /* LocationSection.swift */,
- 962607A12EFC53D2F77E5948 /* MessagingSection.swift */,
- E237FD3F84CE3831CAB8A6DA /* NotificationSection.swift */,
- 09DF983228037C1B729C1419 /* SubscriptionSection.swift */,
- C49D0DCDB581563C015CEAFF /* TagsSection.swift */,
- 4D22580635AB6E6BE86F1396 /* UserSection.swift */,
- );
- path = Sections;
- sourceTree = "";
- };
- 9FC97545EE1DE4A1DD157B26 /* App */ = {
- isa = PBXGroup;
- children = (
- FE9826D274F62E747F3A931B /* OneSignalSwiftUIExampleApp.swift */,
- );
- path = App;
- sourceTree = "";
- };
- A21AEF40FA55F829CF1DA4B4 /* OneSignalSwiftUIExample */ = {
- isa = PBXGroup;
- children = (
- 9FC97545EE1DE4A1DD157B26 /* App */,
- 4320E847C9D875EA2342DB5F /* Models */,
- E905BAC55971B3066478F04B /* Services */,
- BAB41B49B9BD40FB4375BA33 /* ViewModels */,
- DE2A74C6876FFA7E42683810 /* Views */,
- FEE98DA6A075AEB7709CCBE2 /* Assets.xcassets */,
- FDEE7A98A0EBB6BF767A041D /* Info.plist */,
- 29513EB5D1B9AF2511CAD95A /* ExampleAppWidgetAttributes.swift */,
- 130E67036ECE13331B1CFFFA /* Services */,
- 6CC0A9DDAC1CB43170E00043 /* Views */,
- );
- path = OneSignalSwiftUIExample;
- sourceTree = "";
- };
- BAB41B49B9BD40FB4375BA33 /* ViewModels */ = {
- isa = PBXGroup;
- children = (
- FD249AC2E0DD1F282E33E3BF /* OneSignalViewModel.swift */,
- );
- path = ViewModels;
- sourceTree = "";
- };
- C5DE3E27078ADECB8D1EDA06 /* Components */ = {
- isa = PBXGroup;
- children = (
- 52B06024858612EFCB3F42CC /* GuidanceBanner.swift */,
- 22B8D8F4DF8E47F8BA53423A /* LogView.swift */,
- A5ACF918029F336B9C36BD1C /* AddMultiItemSheet.swift */,
- E77A427C94B9932B08E72DA9 /* RemoveMultiSheet.swift */,
- DBF680E9246E2AE4D179CD3E /* CustomNotificationSheet.swift */,
- 2C56E78CB208A9F1F53318D8 /* TrackEventSheet.swift */,
- );
- name = Components;
- sourceTree = "";
- };
- DE2A74C6876FFA7E42683810 /* Views */ = {
- isa = PBXGroup;
- children = (
- 676D7DC4DABFC37256C328C5 /* Components */,
- 92778F3DF1B0939012E946A7 /* Sections */,
- A3BF17A9E51F5A187FFAC1AC /* ContentView.swift */,
- );
- path = Views;
- sourceTree = "";
- };
- E905BAC55971B3066478F04B /* Services */ = {
- isa = PBXGroup;
- children = (
- 85FF4424D702D8686CC819B8 /* OneSignalService.swift */,
- 3C25C0582F3409E9005E5E9A /* NotificationSender.swift */,
- D676BEDC8A5CB48BBF8FFAF9 /* LogManager.swift */,
- 503C246BD2BCA26C96E0EF56 /* LiveActivityController.swift */,
- );
- path = Services;
- sourceTree = "";
- };
-/* End PBXGroup section */
-
-/* Begin PBXNativeTarget section */
- D99FA6E45A25EB4DB04DA0CE /* OneSignalNotificationServiceExtension */ = {
- isa = PBXNativeTarget;
- buildConfigurationList = A98864C40B6D20CA1FCC9136 /* Build configuration list for PBXNativeTarget "OneSignalNotificationServiceExtension" */;
- buildPhases = (
- FEA2231F20C185B148CF3295 /* Sources */,
- 70893964E2D04EE9C32A8152 /* Frameworks */,
- 496CC37F981649C67E534A5E /* Resources */,
- );
- buildRules = (
- );
- dependencies = (
- );
- name = OneSignalNotificationServiceExtension;
- productName = OneSignalNotificationServiceExtension;
- productReference = 50B6C9352575073F1873F639 /* OneSignalNotificationServiceExtension.appex */;
- productType = "com.apple.product-type.app-extension";
- };
- F27E45A0AADC4454C26D8C07 /* OneSignalSwiftUIExample */ = {
- isa = PBXNativeTarget;
- buildConfigurationList = 94A2EFFFED81A4B2280CE916 /* Build configuration list for PBXNativeTarget "OneSignalSwiftUIExample" */;
- buildPhases = (
- 1916C53ACCF1871776E6A261 /* Sources */,
- 8B945C9AF93265E68BBE57A8 /* Resources */,
- D1959F94F086AC39994A96BD /* Frameworks */,
- 510E0294CE200E84088E8CC1 /* Embed Frameworks */,
- D6AA72CEF78258953FC1C74B /* Embed App Extensions */,
- );
- buildRules = (
- );
- dependencies = (
- 454F2CC9F3CE935E9091072B /* PBXTargetDependency */,
- 330BABE7E258541AC08182DB /* PBXTargetDependency */,
- );
- name = OneSignalSwiftUIExample;
- productName = OneSignalSwiftUIExample;
- productReference = A0506934E0F27B864F3AC273 /* OneSignalSwiftUIExample.app */;
- productType = "com.apple.product-type.application";
- };
- 7729C3101598B16A32244980 /* OneSignalWidgetExtension */ = {
- isa = PBXNativeTarget;
- buildConfigurationList = 0A4805483C01DAEEFCD42066 /* Build configuration list for PBXNativeTarget "OneSignalWidgetExtension" */;
- buildPhases = (
- BB92D8E493D58D5BF63C9978 /* Sources */,
- D37A7E2DA634F0A44E3347CD /* Frameworks */,
- E119F524BCE81A602BBBBEB6 /* Resources */,
- );
- buildRules = (
- );
- dependencies = (
- );
- name = OneSignalWidgetExtension;
- productName = OneSignalWidgetExtension;
- productReference = 14520AFC1CB985AE6C04E572 /* OneSignalWidgetExtension.appex */;
- productType = "com.apple.product-type.app-extension";
- };
-/* End PBXNativeTarget section */
-
-/* Begin PBXProject section */
- 7B7016AA7DF5AB54CD96701C /* Project object */ = {
- isa = PBXProject;
- attributes = {
- BuildIndependentTargetsInParallel = YES;
- LastUpgradeCheck = 1300;
- TargetAttributes = {
- D99FA6E45A25EB4DB04DA0CE = {
- ProvisioningStyle = Automatic;
- };
- F27E45A0AADC4454C26D8C07 = {
- ProvisioningStyle = Automatic;
- };
- 7729C3101598B16A32244980 = {
- ProvisioningStyle = Automatic;
- };
- };
- };
- buildConfigurationList = 74CB95FE4C8E95D018B0A01A /* Build configuration list for PBXProject "OneSignalSwiftUIExample" */;
- compatibilityVersion = "Xcode 14.0";
- developmentRegion = en;
- hasScannedForEncodings = 0;
- knownRegions = (
- Base,
- en,
- );
- mainGroup = 8DB2942ED773E3D98EEC5396;
- minimizedProjectReferenceProxies = 1;
- productRefGroup = 3D3976C085CF6E7D61D7F075 /* Products */;
- projectDirPath = "";
- projectRoot = "";
- targets = (
- F27E45A0AADC4454C26D8C07 /* OneSignalSwiftUIExample */,
- D99FA6E45A25EB4DB04DA0CE /* OneSignalNotificationServiceExtension */,
- 7729C3101598B16A32244980 /* OneSignalWidgetExtension */,
- );
- };
-/* End PBXProject section */
-
-/* Begin PBXResourcesBuildPhase section */
- 496CC37F981649C67E534A5E /* Resources */ = {
- isa = PBXResourcesBuildPhase;
- buildActionMask = 2147483647;
- files = (
- );
- runOnlyForDeploymentPostprocessing = 0;
- };
- 8B945C9AF93265E68BBE57A8 /* Resources */ = {
- isa = PBXResourcesBuildPhase;
- buildActionMask = 2147483647;
- files = (
- 25D4FC203BE01E9A35ACA465 /* Assets.xcassets in Resources */,
- );
- runOnlyForDeploymentPostprocessing = 0;
- };
- E119F524BCE81A602BBBBEB6 /* Resources */ = {
- isa = PBXResourcesBuildPhase;
- buildActionMask = 2147483647;
- files = (
- );
- runOnlyForDeploymentPostprocessing = 0;
- };
-/* End PBXResourcesBuildPhase section */
-
-/* Begin PBXSourcesBuildPhase section */
- 1916C53ACCF1871776E6A261 /* Sources */ = {
- isa = PBXSourcesBuildPhase;
- buildActionMask = 2147483647;
- files = (
- 2E6D6E17EEE5D42AE3CAB9EA /* AddItemSheet.swift in Sources */,
- 74920778F254B9DA27906AA3 /* AppInfoSection.swift in Sources */,
- 644520A37F05E25FD17F5CF9 /* AppModels.swift in Sources */,
- E26208CEB7DC4D217EAAE4E6 /* ContentView.swift in Sources */,
- B5C4A4AC0A2D2AF73C9A7DD3 /* KeyValueRow.swift in Sources */,
- AD19D28FDE8DAA7C8BC87939 /* LocationSection.swift in Sources */,
- 5EEA4B007D60765502F2A6E5 /* MessagingSection.swift in Sources */,
- B13E9460BDD0553FF05542A0 /* NotificationGrid.swift in Sources */,
- C9884D12CEDE50ACAE38DF0D /* NotificationSection.swift in Sources */,
- CB5F82751F156695A7A03338 /* OneSignalService.swift in Sources */,
- 1D68D16D167B951BD57386ED /* OneSignalSwiftUIExampleApp.swift in Sources */,
- 2F52BD9F2A3002390A7C8896 /* OneSignalViewModel.swift in Sources */,
- 3C25C0592F3409E9005E5E9A /* NotificationSender.swift in Sources */,
- 49E74AFBFBE23D06B7D310EC /* SubscriptionSection.swift in Sources */,
- DB2BA39FE32DB3D52154F48C /* TagsSection.swift in Sources */,
- D13A25FE5762A23125227A84 /* ToastView.swift in Sources */,
- E1BEF5E30555F0ADCBF62B7D /* UserSection.swift in Sources */,
- 6486DBFD33978F37842B8326 /* TooltipService.swift in Sources */,
- 076B2F5C2E9B739160493063 /* UserFetchService.swift in Sources */,
- 8B3A2DC19BD16993EC4B617B /* GuidanceBanner.swift in Sources */,
- 85BAC3A9462800CB3A23C362 /* LogManager.swift in Sources */,
- FEEC78AA5DE04832192D8F92 /* LogView.swift in Sources */,
- BAE6133523D7D07386BD3172 /* AddMultiItemSheet.swift in Sources */,
- 6C7913B91320573B2AE46212 /* RemoveMultiSheet.swift in Sources */,
- 14AB26AE3A2FF05C9F62CCD2 /* CustomNotificationSheet.swift in Sources */,
- 8F3FE40D8D603A8879C6A111 /* TrackEventSheet.swift in Sources */,
- 44345BEEC3249B530635D91C /* TrackEventSection.swift in Sources */,
- 7C93699D746B0B5807AD13BB /* NextScreenSection.swift in Sources */,
- 63C9FA76B516816B1B0AED19 /* ExampleAppWidgetAttributes.swift in Sources */,
- 1630D207F53B85B5C5A4806C /* LiveActivityController.swift in Sources */,
- 45050FAF882B8B140E005816 /* LiveActivitySection.swift in Sources */,
- );
- runOnlyForDeploymentPostprocessing = 0;
- };
- FEA2231F20C185B148CF3295 /* Sources */ = {
- isa = PBXSourcesBuildPhase;
- buildActionMask = 2147483647;
- files = (
- 7F7A92F525620CDF81EC46BE /* NotificationService.swift in Sources */,
- );
- runOnlyForDeploymentPostprocessing = 0;
- };
- BB92D8E493D58D5BF63C9978 /* Sources */ = {
- isa = PBXSourcesBuildPhase;
- buildActionMask = 2147483647;
- files = (
- 2BD9DE389DE56B72D031CA01 /* OneSignalWidgetExtensionBundle.swift in Sources */,
- 50091A346024B3DDB1053760 /* OneSignalWidgetExtensionLiveActivity.swift in Sources */,
- 544423F60A011F8B8D2971BF /* OneSignalWidgetExtension.swift in Sources */,
- 34446FC058592648ACCD90ED /* ExampleAppWidgetAttributes.swift in Sources */,
- );
- runOnlyForDeploymentPostprocessing = 0;
- };
-/* End PBXSourcesBuildPhase section */
-
-/* Begin PBXTargetDependency section */
- 454F2CC9F3CE935E9091072B /* PBXTargetDependency */ = {
- isa = PBXTargetDependency;
- target = D99FA6E45A25EB4DB04DA0CE /* OneSignalNotificationServiceExtension */;
- targetProxy = DC39EC3039BB65E2D21F753F /* PBXContainerItemProxy */;
- };
- 330BABE7E258541AC08182DB /* PBXTargetDependency */ = {
- isa = PBXTargetDependency;
- target = 7729C3101598B16A32244980 /* OneSignalWidgetExtension */;
- targetProxy = EEAB30880E225535B2F366FA /* PBXContainerItemProxy */;
- };
-/* End PBXTargetDependency section */
-
-/* Begin XCBuildConfiguration section */
- 09FF5B5EE6F70E991CEAE373 /* Debug */ = {
- isa = XCBuildConfiguration;
- buildSettings = {
- ALWAYS_SEARCH_USER_PATHS = NO;
- CLANG_ANALYZER_NONNULL = YES;
- CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
- CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
- CLANG_CXX_LIBRARY = "libc++";
- CLANG_ENABLE_MODULES = YES;
- CLANG_ENABLE_OBJC_ARC = YES;
- CLANG_ENABLE_OBJC_WEAK = YES;
- CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
- CLANG_WARN_BOOL_CONVERSION = YES;
- CLANG_WARN_COMMA = YES;
- CLANG_WARN_CONSTANT_CONVERSION = YES;
- CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
- CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
- CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
- CLANG_WARN_EMPTY_BODY = YES;
- CLANG_WARN_ENUM_CONVERSION = YES;
- CLANG_WARN_INFINITE_RECURSION = YES;
- CLANG_WARN_INT_CONVERSION = YES;
- CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
- CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
- CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
- CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
- CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
- CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
- CLANG_WARN_STRICT_PROTOTYPES = YES;
- CLANG_WARN_SUSPICIOUS_MOVE = YES;
- CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
- CLANG_WARN_UNREACHABLE_CODE = YES;
- CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
- CODE_SIGN_STYLE = Automatic;
- COPY_PHASE_STRIP = NO;
- CURRENT_PROJECT_VERSION = 1;
- DEBUG_INFORMATION_FORMAT = dwarf;
- DEVELOPMENT_TEAM = "";
- ENABLE_STRICT_OBJC_MSGSEND = YES;
- ENABLE_TESTABILITY = YES;
- GCC_C_LANGUAGE_STANDARD = gnu11;
- GCC_DYNAMIC_NO_PIC = NO;
- GCC_NO_COMMON_BLOCKS = YES;
- GCC_OPTIMIZATION_LEVEL = 0;
- GCC_PREPROCESSOR_DEFINITIONS = (
- "$(inherited)",
- "DEBUG=1",
- );
- GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
- GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
- GCC_WARN_UNDECLARED_SELECTOR = YES;
- GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
- GCC_WARN_UNUSED_FUNCTION = YES;
- GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 16.0;
- MARKETING_VERSION = 5.4.1;
- MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
- MTL_FAST_MATH = YES;
- ONLY_ACTIVE_ARCH = YES;
- PRODUCT_NAME = "$(TARGET_NAME)";
- SDKROOT = iphoneos;
- SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
- SWIFT_OPTIMIZATION_LEVEL = "-Onone";
- SWIFT_VERSION = 5.0;
- };
- name = Debug;
- };
- 19B2A627D6CEDFA231590A4B /* Release */ = {
- isa = XCBuildConfiguration;
- buildSettings = {
- CODE_SIGN_ENTITLEMENTS = OneSignalNotificationServiceExtension/OneSignalNotificationServiceExtension.entitlements;
- CODE_SIGN_IDENTITY = "iPhone Developer";
- CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 1;
- DEVELOPMENT_TEAM = 99SW8E36CT;
- INFOPLIST_FILE = OneSignalNotificationServiceExtension/Info.plist;
- IPHONEOS_DEPLOYMENT_TARGET = 16.0;
- LD_RUNPATH_SEARCH_PATHS = (
- "$(inherited)",
- "@executable_path/Frameworks",
- "@executable_path/../../Frameworks",
- );
- MARKETING_VERSION = 5.4.1;
- PRODUCT_BUNDLE_IDENTIFIER = com.onesignal.example.OneSignalNotificationServiceExtensionA;
- PRODUCT_NAME = "$(TARGET_NAME)";
- SDKROOT = iphoneos;
- SKIP_INSTALL = YES;
- SWIFT_VERSION = 5.0;
- TARGETED_DEVICE_FAMILY = "1,2";
- };
- name = Release;
- };
- 70CAD4886778AACB94FDDB37 /* Release */ = {
- isa = XCBuildConfiguration;
- buildSettings = {
- ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
- ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
- CODE_SIGN_ENTITLEMENTS = OneSignalSwiftUIExample.entitlements;
- CODE_SIGN_IDENTITY = "iPhone Developer";
- DEVELOPMENT_TEAM = 99SW8E36CT;
- ENABLE_PREVIEWS = YES;
- INFOPLIST_FILE = OneSignalSwiftUIExample/Info.plist;
- IPHONEOS_DEPLOYMENT_TARGET = 16.0;
- LD_RUNPATH_SEARCH_PATHS = (
- "$(inherited)",
- "@executable_path/Frameworks",
- );
- MARKETING_VERSION = 5.4.1;
- PRODUCT_BUNDLE_IDENTIFIER = com.onesignal.example;
- SDKROOT = iphoneos;
- SUPPORTS_MACCATALYST = NO;
- SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
- TARGETED_DEVICE_FAMILY = "1,2";
- };
- name = Release;
- };
- 81971FC2118996AD12380AFF /* Release */ = {
- isa = XCBuildConfiguration;
- buildSettings = {
- ALWAYS_SEARCH_USER_PATHS = NO;
- CLANG_ANALYZER_NONNULL = YES;
- CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
- CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
- CLANG_CXX_LIBRARY = "libc++";
- CLANG_ENABLE_MODULES = YES;
- CLANG_ENABLE_OBJC_ARC = YES;
- CLANG_ENABLE_OBJC_WEAK = YES;
- CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
- CLANG_WARN_BOOL_CONVERSION = YES;
- CLANG_WARN_COMMA = YES;
- CLANG_WARN_CONSTANT_CONVERSION = YES;
- CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
- CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
- CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
- CLANG_WARN_EMPTY_BODY = YES;
- CLANG_WARN_ENUM_CONVERSION = YES;
- CLANG_WARN_INFINITE_RECURSION = YES;
- CLANG_WARN_INT_CONVERSION = YES;
- CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
- CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
- CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
- CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
- CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
- CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
- CLANG_WARN_STRICT_PROTOTYPES = YES;
- CLANG_WARN_SUSPICIOUS_MOVE = YES;
- CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
- CLANG_WARN_UNREACHABLE_CODE = YES;
- CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
- CODE_SIGN_STYLE = Automatic;
- COPY_PHASE_STRIP = NO;
- CURRENT_PROJECT_VERSION = 1;
- DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
- DEVELOPMENT_TEAM = "";
- ENABLE_NS_ASSERTIONS = NO;
- ENABLE_STRICT_OBJC_MSGSEND = YES;
- GCC_C_LANGUAGE_STANDARD = gnu11;
- GCC_NO_COMMON_BLOCKS = YES;
- GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
- GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
- GCC_WARN_UNDECLARED_SELECTOR = YES;
- GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
- GCC_WARN_UNUSED_FUNCTION = YES;
- GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 16.0;
- MARKETING_VERSION = 5.4.1;
- MTL_ENABLE_DEBUG_INFO = NO;
- MTL_FAST_MATH = YES;
- PRODUCT_NAME = "$(TARGET_NAME)";
- SDKROOT = iphoneos;
- SWIFT_COMPILATION_MODE = wholemodule;
- SWIFT_OPTIMIZATION_LEVEL = "-O";
- SWIFT_VERSION = 5.0;
- };
- name = Release;
- };
- 8BDA36EF0097197153C97356 /* Debug */ = {
- isa = XCBuildConfiguration;
- buildSettings = {
- ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
- ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
- CODE_SIGN_ENTITLEMENTS = OneSignalSwiftUIExample.entitlements;
- CODE_SIGN_IDENTITY = "iPhone Developer";
- DEVELOPMENT_TEAM = 99SW8E36CT;
- ENABLE_PREVIEWS = YES;
- INFOPLIST_FILE = OneSignalSwiftUIExample/Info.plist;
- IPHONEOS_DEPLOYMENT_TARGET = 16.0;
- LD_RUNPATH_SEARCH_PATHS = (
- "$(inherited)",
- "@executable_path/Frameworks",
- );
- MARKETING_VERSION = 5.4.1;
- PRODUCT_BUNDLE_IDENTIFIER = com.onesignal.example;
- SDKROOT = iphoneos;
- SUPPORTS_MACCATALYST = NO;
- SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
- TARGETED_DEVICE_FAMILY = "1,2";
- };
- name = Debug;
- };
- F647D1CFE40BD3C0C6511C73 /* Debug */ = {
- isa = XCBuildConfiguration;
- buildSettings = {
- CODE_SIGN_ENTITLEMENTS = OneSignalNotificationServiceExtension/OneSignalNotificationServiceExtension.entitlements;
- CODE_SIGN_IDENTITY = "iPhone Developer";
- CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 1;
- DEVELOPMENT_TEAM = 99SW8E36CT;
- INFOPLIST_FILE = OneSignalNotificationServiceExtension/Info.plist;
- IPHONEOS_DEPLOYMENT_TARGET = 16.0;
- LD_RUNPATH_SEARCH_PATHS = (
- "$(inherited)",
- "@executable_path/Frameworks",
- "@executable_path/../../Frameworks",
- );
- MARKETING_VERSION = 5.4.1;
- PRODUCT_BUNDLE_IDENTIFIER = com.onesignal.example.OneSignalNotificationServiceExtensionA;
- PRODUCT_NAME = "$(TARGET_NAME)";
- SDKROOT = iphoneos;
- SKIP_INSTALL = YES;
- SWIFT_VERSION = 5.0;
- TARGETED_DEVICE_FAMILY = "1,2";
- };
- name = Debug;
- };
- D80553ADD3452D4E48E4EB6E /* Debug */ = {
- isa = XCBuildConfiguration;
- buildSettings = {
- ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
- CODE_SIGN_ENTITLEMENTS = OneSignalWidgetExtension.entitlements;
- CODE_SIGN_IDENTITY = "iPhone Developer";
- CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 1;
- DEVELOPMENT_TEAM = 99SW8E36CT;
- GENERATE_INFOPLIST_FILE = YES;
- INFOPLIST_FILE = OneSignalWidgetExtension/Info.plist;
- INFOPLIST_KEY_CFBundleDisplayName = OneSignalWidgetExtension;
- INFOPLIST_KEY_NSHumanReadableCopyright = "";
- IPHONEOS_DEPLOYMENT_TARGET = 16.1;
- LD_RUNPATH_SEARCH_PATHS = (
- "$(inherited)",
- "@executable_path/Frameworks",
- "@executable_path/../../Frameworks",
- );
- MARKETING_VERSION = 5.4.1;
- PRODUCT_BUNDLE_IDENTIFIER = com.onesignal.example.OneSignalWidgetExtension;
- PRODUCT_NAME = "$(TARGET_NAME)";
- SDKROOT = iphoneos;
- SKIP_INSTALL = YES;
- SWIFT_EMIT_LOC_STRINGS = YES;
- SWIFT_VERSION = 5.0;
- TARGETED_DEVICE_FAMILY = "1,2";
- };
- name = Debug;
- };
- C93E6DBD405CD230ED4C302B /* Release */ = {
- isa = XCBuildConfiguration;
- buildSettings = {
- ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
- CODE_SIGN_ENTITLEMENTS = OneSignalWidgetExtension.entitlements;
- CODE_SIGN_IDENTITY = "iPhone Developer";
- CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 1;
- DEVELOPMENT_TEAM = 99SW8E36CT;
- GENERATE_INFOPLIST_FILE = YES;
- INFOPLIST_FILE = OneSignalWidgetExtension/Info.plist;
- INFOPLIST_KEY_CFBundleDisplayName = OneSignalWidgetExtension;
- INFOPLIST_KEY_NSHumanReadableCopyright = "";
- IPHONEOS_DEPLOYMENT_TARGET = 16.1;
- LD_RUNPATH_SEARCH_PATHS = (
- "$(inherited)",
- "@executable_path/Frameworks",
- "@executable_path/../../Frameworks",
- );
- MARKETING_VERSION = 5.4.1;
- PRODUCT_BUNDLE_IDENTIFIER = com.onesignal.example.OneSignalWidgetExtension;
- PRODUCT_NAME = "$(TARGET_NAME)";
- SDKROOT = iphoneos;
- SKIP_INSTALL = YES;
- SWIFT_EMIT_LOC_STRINGS = YES;
- SWIFT_VERSION = 5.0;
- TARGETED_DEVICE_FAMILY = "1,2";
- };
- name = Release;
- };
-/* End XCBuildConfiguration section */
-
-/* Begin XCConfigurationList section */
- 74CB95FE4C8E95D018B0A01A /* Build configuration list for PBXProject "OneSignalSwiftUIExample" */ = {
- isa = XCConfigurationList;
- buildConfigurations = (
- 09FF5B5EE6F70E991CEAE373 /* Debug */,
- 81971FC2118996AD12380AFF /* Release */,
- );
- defaultConfigurationIsVisible = 0;
- defaultConfigurationName = Debug;
- };
- 94A2EFFFED81A4B2280CE916 /* Build configuration list for PBXNativeTarget "OneSignalSwiftUIExample" */ = {
- isa = XCConfigurationList;
- buildConfigurations = (
- 8BDA36EF0097197153C97356 /* Debug */,
- 70CAD4886778AACB94FDDB37 /* Release */,
- );
- defaultConfigurationIsVisible = 0;
- defaultConfigurationName = Debug;
- };
- A98864C40B6D20CA1FCC9136 /* Build configuration list for PBXNativeTarget "OneSignalNotificationServiceExtension" */ = {
- isa = XCConfigurationList;
- buildConfigurations = (
- F647D1CFE40BD3C0C6511C73 /* Debug */,
- 19B2A627D6CEDFA231590A4B /* Release */,
- );
- defaultConfigurationIsVisible = 0;
- defaultConfigurationName = Debug;
- };
- 0A4805483C01DAEEFCD42066 /* Build configuration list for PBXNativeTarget "OneSignalWidgetExtension" */ = {
- isa = XCConfigurationList;
- buildConfigurations = (
- D80553ADD3452D4E48E4EB6E /* Debug */,
- C93E6DBD405CD230ED4C302B /* Release */,
- );
- defaultConfigurationIsVisible = 0;
- defaultConfigurationName = Debug;
- };
-/* End XCConfigurationList section */
- };
- rootObject = 7B7016AA7DF5AB54CD96701C /* Project object */;
-}
diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample.xcodeproj/xcshareddata/xcschemes/OneSignalSwiftUIExample.xcscheme b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample.xcodeproj/xcshareddata/xcschemes/OneSignalSwiftUIExample.xcscheme
deleted file mode 100644
index c1bea564d..000000000
--- a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample.xcodeproj/xcshareddata/xcschemes/OneSignalSwiftUIExample.xcscheme
+++ /dev/null
@@ -1,93 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/App/OneSignalSwiftUIExampleApp.swift b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/App/OneSignalSwiftUIExampleApp.swift
deleted file mode 100644
index fe39a3a77..000000000
--- a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/App/OneSignalSwiftUIExampleApp.swift
+++ /dev/null
@@ -1,257 +0,0 @@
-/**
- * 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 SwiftUI
-import UserNotifications
-import OneSignalFramework
-
-@main
-struct OneSignalSwiftUIExampleApp: App {
- @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
- @StateObject private var viewModel = OneSignalViewModel()
-
- var body: some Scene {
- WindowGroup {
- ContentView()
- .environmentObject(viewModel)
- .onOpenURL { url in
- let originalURL = OneSignal.LiveActivities.trackClickAndReturnOriginal(url)
- LogManager.shared.i("LiveActivity", "Opened with URL: \(url), original: \(String(describing: originalURL))")
- }
- }
- }
-}
-
-// MARK: - App Delegate
-
-class AppDelegate: NSObject, UIApplicationDelegate, UNUserNotificationCenterDelegate {
-
- // Keys for caching SDK state in UserDefaults
- private let cachedIAMPausedKey = "CachedInAppMessagesPaused"
- private let cachedLocationSharedKey = "CachedLocationShared"
- private let cachedConsentRequiredKey = "CachedConsentRequired"
- private let cachedPrivacyConsentKey = "CachedPrivacyConsent"
-
- func application(
- _ application: UIApplication,
- didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
- ) -> Bool {
- UNUserNotificationCenter.current().delegate = self
-
- // Set consent required before init (must be set before initWithContext)
- let consentRequired = UserDefaults.standard.bool(forKey: cachedConsentRequiredKey)
- let privacyConsent = UserDefaults.standard.bool(forKey: cachedPrivacyConsentKey)
- OneSignal.setConsentRequired(consentRequired)
- OneSignal.setConsentGiven(privacyConsent)
-
- // Initialize OneSignal
- OneSignalService.shared.initialize(launchOptions: launchOptions)
-
- // Start Live Activity listeners
- if #available(iOS 16.1, *) {
- LiveActivityController.start()
- }
-
- // Restore cached SDK states before UI loads
- restoreCachedStates()
-
- // Set up notification lifecycle listeners
- setupNotificationListeners()
-
- // Set up in-app message listeners
- setupInAppMessageListeners()
-
- // Set up SDK log listener for LogView
- setupLogListener()
-
- // Initialize tooltip service (fetches on background thread, non-blocking)
- TooltipService.shared.initialize()
-
- return true
- }
-
- // MARK: - Manual Integration APIs (for use when swizzling is disabled)
-
- func application(_ application: UIApplication,
- didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
- OneSignal.Notifications.didRegisterForRemoteNotifications(deviceToken: deviceToken)
- }
-
- func application(_ application: UIApplication,
- didFailToRegisterForRemoteNotificationsWithError error: Error) {
- OneSignal.Notifications.didFailToRegisterForRemoteNotifications(error: error as NSError)
- }
-
- func application(_ application: UIApplication,
- didReceiveRemoteNotification userInfo: [AnyHashable: Any],
- fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
- OneSignal.Notifications.didReceiveRemoteNotification(userInfo: userInfo,
- completionHandler: completionHandler)
- }
-
- func userNotificationCenter(_ center: UNUserNotificationCenter,
- willPresent notification: UNNotification,
- withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
- OneSignal.Notifications.willPresentNotification(
- payload: notification.request.content.userInfo) { notif in
- if notif != nil {
- if #available(iOS 14.0, *) {
- completionHandler([.banner, .list, .sound])
- } else {
- completionHandler([.alert, .sound])
- }
- } else {
- completionHandler([])
- }
- }
- }
-
- func userNotificationCenter(_ center: UNUserNotificationCenter,
- didReceive response: UNNotificationResponse,
- withCompletionHandler completionHandler: @escaping () -> Void) {
- OneSignal.Notifications.didReceiveNotificationResponse(response)
- completionHandler()
- }
-
- private func setupLogListener() {
- OneSignal.Debug.setLogLevel(.LL_VERBOSE)
- OneSignal.Debug.addLogListener(SDKLogListener.shared)
- }
-
- private func restoreCachedStates() {
- // Restore IAM paused status
- let iamPaused = UserDefaults.standard.object(forKey: cachedIAMPausedKey) == nil
- ? true
- : UserDefaults.standard.bool(forKey: cachedIAMPausedKey)
- OneSignal.InAppMessages.paused = iamPaused
-
- // Restore location shared status
- let locationShared = UserDefaults.standard.bool(forKey: cachedLocationSharedKey)
- OneSignal.Location.isShared = locationShared
- }
-
- private func setupNotificationListeners() {
- // Foreground notification display
- OneSignal.Notifications.addForegroundLifecycleListener(NotificationLifecycleHandler.shared)
-
- // Notification click handling
- OneSignal.Notifications.addClickListener(NotificationClickHandler.shared)
- }
-
- private func setupInAppMessageListeners() {
- // In-app message lifecycle
- OneSignal.InAppMessages.addLifecycleListener(InAppMessageLifecycleHandler.shared)
-
- // In-app message click handling
- OneSignal.InAppMessages.addClickListener(InAppMessageClickHandler.shared)
- }
-}
-
-// MARK: - Notification Handlers
-
-class NotificationLifecycleHandler: NSObject, OSNotificationLifecycleListener {
- static let shared = NotificationLifecycleHandler()
-
- func onWillDisplay(event: OSNotificationWillDisplayEvent) {
- Task { @MainActor in
- LogManager.shared.i("Notification", "Will display: \(event.notification.title ?? "No title")")
- }
- }
-}
-
-class NotificationClickHandler: NSObject, OSNotificationClickListener {
- static let shared = NotificationClickHandler()
-
- func onClick(event: OSNotificationClickEvent) {
- Task { @MainActor in
- LogManager.shared.i("Notification", "Clicked: \(event.notification.title ?? "No title")")
- }
- }
-}
-
-// MARK: - In-App Message Handlers
-
-class InAppMessageLifecycleHandler: NSObject, OSInAppMessageLifecycleListener {
- static let shared = InAppMessageLifecycleHandler()
-
- func onWillDisplay(event: OSInAppMessageWillDisplayEvent) {
- Task { @MainActor in
- LogManager.shared.i("IAM", "Will display: \(event.message.messageId)")
- }
- }
-
- func onDidDisplay(event: OSInAppMessageDidDisplayEvent) {
- Task { @MainActor in
- LogManager.shared.i("IAM", "Did display: \(event.message.messageId)")
- }
- }
-
- func onWillDismiss(event: OSInAppMessageWillDismissEvent) {
- Task { @MainActor in
- LogManager.shared.i("IAM", "Will dismiss: \(event.message.messageId)")
- }
- }
-
- func onDidDismiss(event: OSInAppMessageDidDismissEvent) {
- Task { @MainActor in
- LogManager.shared.i("IAM", "Did dismiss: \(event.message.messageId)")
- }
- }
-}
-
-class InAppMessageClickHandler: NSObject, OSInAppMessageClickListener {
- static let shared = InAppMessageClickHandler()
-
- func onClick(event: OSInAppMessageClickEvent) {
- Task { @MainActor in
- LogManager.shared.i("IAM", "Clicked: \(event.result.actionId ?? "No action ID")")
- }
- }
-}
-
-// MARK: - SDK Log Listener
-
-class SDKLogListener: NSObject, OSLogListener {
- static let shared = SDKLogListener()
-
- func onLogEvent(_ event: OneSignalLogEvent) {
- let level: LogLevel
- switch event.level {
- case .LL_FATAL, .LL_ERROR:
- level = .error
- case .LL_WARN:
- level = .warning
- case .LL_INFO:
- level = .info
- default:
- level = .debug
- }
- Task { @MainActor in
- LogManager.shared.log("SDK", event.entry, level: level)
- }
- }
-}
diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AccentColor.colorset/Contents.json b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AccentColor.colorset/Contents.json
deleted file mode 100644
index 2c54006ed..000000000
--- a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AccentColor.colorset/Contents.json
+++ /dev/null
@@ -1,38 +0,0 @@
-{
- "colors" : [
- {
- "color" : {
- "color-space" : "srgb",
- "components" : {
- "alpha" : "1.000",
- "blue" : "0x4D",
- "green" : "0x4B",
- "red" : "0xE5"
- }
- },
- "idiom" : "universal"
- },
- {
- "appearances" : [
- {
- "appearance" : "luminosity",
- "value" : "dark"
- }
- ],
- "color" : {
- "color-space" : "srgb",
- "components" : {
- "alpha" : "1.000",
- "blue" : "0x6D",
- "green" : "0x6B",
- "red" : "0xF5"
- }
- },
- "idiom" : "universal"
- }
- ],
- "info" : {
- "author" : "xcode",
- "version" : 1
- }
-}
diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/100.png b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/100.png
deleted file mode 100644
index 57855f96d..000000000
Binary files a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/100.png and /dev/null differ
diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/1024.png b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/1024.png
deleted file mode 100644
index 0d61ec818..000000000
Binary files a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/1024.png and /dev/null differ
diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/114.png b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/114.png
deleted file mode 100644
index 61d5f8962..000000000
Binary files a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/114.png and /dev/null differ
diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/120.png b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/120.png
deleted file mode 100644
index 5ba28fc5b..000000000
Binary files a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/120.png and /dev/null differ
diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/128.png b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/128.png
deleted file mode 100644
index 84d021320..000000000
Binary files a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/128.png and /dev/null differ
diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/144.png b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/144.png
deleted file mode 100644
index e30ed7b19..000000000
Binary files a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/144.png and /dev/null differ
diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/152.png b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/152.png
deleted file mode 100644
index 195516d96..000000000
Binary files a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/152.png and /dev/null differ
diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/16.png b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/16.png
deleted file mode 100644
index 4e4b3be94..000000000
Binary files a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/16.png and /dev/null differ
diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/167.png b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/167.png
deleted file mode 100644
index bc74b6f98..000000000
Binary files a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/167.png and /dev/null differ
diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/172.png b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/172.png
deleted file mode 100644
index 772e8878b..000000000
Binary files a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/172.png and /dev/null differ
diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/180.png b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/180.png
deleted file mode 100644
index 54b94c46a..000000000
Binary files a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/180.png and /dev/null differ
diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/196.png b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/196.png
deleted file mode 100644
index f975b2c0c..000000000
Binary files a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/196.png and /dev/null differ
diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/20.png b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/20.png
deleted file mode 100644
index db15cd57c..000000000
Binary files a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/20.png and /dev/null differ
diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/216.png b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/216.png
deleted file mode 100644
index 41629097d..000000000
Binary files a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/216.png and /dev/null differ
diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/256.png b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/256.png
deleted file mode 100644
index f88c1cfe0..000000000
Binary files a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/256.png and /dev/null differ
diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/29.png b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/29.png
deleted file mode 100644
index f66c8e999..000000000
Binary files a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/29.png and /dev/null differ
diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/32.png b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/32.png
deleted file mode 100644
index 2285ee766..000000000
Binary files a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/32.png and /dev/null differ
diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/40.png b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/40.png
deleted file mode 100644
index 302c40feb..000000000
Binary files a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/40.png and /dev/null differ
diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/48.png b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/48.png
deleted file mode 100644
index f4ee0060d..000000000
Binary files a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/48.png and /dev/null differ
diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/50.png b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/50.png
deleted file mode 100644
index a8bfadee4..000000000
Binary files a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/50.png and /dev/null differ
diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/512.png b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/512.png
deleted file mode 100644
index a2d9aeee6..000000000
Binary files a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/512.png and /dev/null differ
diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/55.png b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/55.png
deleted file mode 100644
index 1f3d4c2d5..000000000
Binary files a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/55.png and /dev/null differ
diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/57.png b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/57.png
deleted file mode 100644
index 03585df4f..000000000
Binary files a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/57.png and /dev/null differ
diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/58.png b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/58.png
deleted file mode 100644
index 0576f9976..000000000
Binary files a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/58.png and /dev/null differ
diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/60.png b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/60.png
deleted file mode 100644
index 6211b6bbf..000000000
Binary files a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/60.png and /dev/null differ
diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/64.png b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/64.png
deleted file mode 100644
index 3d0afeaf7..000000000
Binary files a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/64.png and /dev/null differ
diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/66.png b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/66.png
deleted file mode 100644
index 50bfed1bc..000000000
Binary files a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/66.png and /dev/null differ
diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/72.png b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/72.png
deleted file mode 100644
index fa283d7b4..000000000
Binary files a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/72.png and /dev/null differ
diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/76.png b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/76.png
deleted file mode 100644
index 4c555c02a..000000000
Binary files a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/76.png and /dev/null differ
diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/80.png b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/80.png
deleted file mode 100644
index f7454c62c..000000000
Binary files a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/80.png and /dev/null differ
diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/87.png b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/87.png
deleted file mode 100644
index e44819f04..000000000
Binary files a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/87.png and /dev/null differ
diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/88.png b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/88.png
deleted file mode 100644
index d1e764b6f..000000000
Binary files a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/88.png and /dev/null differ
diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/92.png b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/92.png
deleted file mode 100644
index 997cdf27c..000000000
Binary files a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/92.png and /dev/null differ
diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/Contents.json b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/Contents.json
deleted file mode 100644
index 8e70699a7..000000000
--- a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/Contents.json
+++ /dev/null
@@ -1,346 +0,0 @@
-{
- "images" : [
- {
- "filename" : "40.png",
- "idiom" : "iphone",
- "scale" : "2x",
- "size" : "20x20"
- },
- {
- "filename" : "60.png",
- "idiom" : "iphone",
- "scale" : "3x",
- "size" : "20x20"
- },
- {
- "filename" : "29.png",
- "idiom" : "iphone",
- "scale" : "1x",
- "size" : "29x29"
- },
- {
- "filename" : "58.png",
- "idiom" : "iphone",
- "scale" : "2x",
- "size" : "29x29"
- },
- {
- "filename" : "87.png",
- "idiom" : "iphone",
- "scale" : "3x",
- "size" : "29x29"
- },
- {
- "filename" : "80.png",
- "idiom" : "iphone",
- "scale" : "2x",
- "size" : "40x40"
- },
- {
- "filename" : "120.png",
- "idiom" : "iphone",
- "scale" : "3x",
- "size" : "40x40"
- },
- {
- "filename" : "57.png",
- "idiom" : "iphone",
- "scale" : "1x",
- "size" : "57x57"
- },
- {
- "filename" : "114.png",
- "idiom" : "iphone",
- "scale" : "2x",
- "size" : "57x57"
- },
- {
- "filename" : "120.png",
- "idiom" : "iphone",
- "scale" : "2x",
- "size" : "60x60"
- },
- {
- "filename" : "180.png",
- "idiom" : "iphone",
- "scale" : "3x",
- "size" : "60x60"
- },
- {
- "filename" : "20.png",
- "idiom" : "ipad",
- "scale" : "1x",
- "size" : "20x20"
- },
- {
- "filename" : "40.png",
- "idiom" : "ipad",
- "scale" : "2x",
- "size" : "20x20"
- },
- {
- "filename" : "29.png",
- "idiom" : "ipad",
- "scale" : "1x",
- "size" : "29x29"
- },
- {
- "filename" : "58.png",
- "idiom" : "ipad",
- "scale" : "2x",
- "size" : "29x29"
- },
- {
- "filename" : "40.png",
- "idiom" : "ipad",
- "scale" : "1x",
- "size" : "40x40"
- },
- {
- "filename" : "80.png",
- "idiom" : "ipad",
- "scale" : "2x",
- "size" : "40x40"
- },
- {
- "filename" : "50.png",
- "idiom" : "ipad",
- "scale" : "1x",
- "size" : "50x50"
- },
- {
- "filename" : "100.png",
- "idiom" : "ipad",
- "scale" : "2x",
- "size" : "50x50"
- },
- {
- "filename" : "72.png",
- "idiom" : "ipad",
- "scale" : "1x",
- "size" : "72x72"
- },
- {
- "filename" : "144.png",
- "idiom" : "ipad",
- "scale" : "2x",
- "size" : "72x72"
- },
- {
- "filename" : "76.png",
- "idiom" : "ipad",
- "scale" : "1x",
- "size" : "76x76"
- },
- {
- "filename" : "152.png",
- "idiom" : "ipad",
- "scale" : "2x",
- "size" : "76x76"
- },
- {
- "filename" : "167.png",
- "idiom" : "ipad",
- "scale" : "2x",
- "size" : "83.5x83.5"
- },
- {
- "filename" : "1024.png",
- "idiom" : "ios-marketing",
- "scale" : "1x",
- "size" : "1024x1024"
- },
- {
- "filename" : "16.png",
- "idiom" : "mac",
- "scale" : "1x",
- "size" : "16x16"
- },
- {
- "filename" : "32.png",
- "idiom" : "mac",
- "scale" : "2x",
- "size" : "16x16"
- },
- {
- "filename" : "32.png",
- "idiom" : "mac",
- "scale" : "1x",
- "size" : "32x32"
- },
- {
- "filename" : "64.png",
- "idiom" : "mac",
- "scale" : "2x",
- "size" : "32x32"
- },
- {
- "filename" : "128.png",
- "idiom" : "mac",
- "scale" : "1x",
- "size" : "128x128"
- },
- {
- "filename" : "256.png",
- "idiom" : "mac",
- "scale" : "2x",
- "size" : "128x128"
- },
- {
- "filename" : "256.png",
- "idiom" : "mac",
- "scale" : "1x",
- "size" : "256x256"
- },
- {
- "filename" : "512.png",
- "idiom" : "mac",
- "scale" : "2x",
- "size" : "256x256"
- },
- {
- "filename" : "512.png",
- "idiom" : "mac",
- "scale" : "1x",
- "size" : "512x512"
- },
- {
- "filename" : "1024.png",
- "idiom" : "mac",
- "scale" : "2x",
- "size" : "512x512"
- },
- {
- "filename" : "48.png",
- "idiom" : "watch",
- "role" : "notificationCenter",
- "scale" : "2x",
- "size" : "24x24",
- "subtype" : "38mm"
- },
- {
- "filename" : "55.png",
- "idiom" : "watch",
- "role" : "notificationCenter",
- "scale" : "2x",
- "size" : "27.5x27.5",
- "subtype" : "42mm"
- },
- {
- "filename" : "58.png",
- "idiom" : "watch",
- "role" : "companionSettings",
- "scale" : "2x",
- "size" : "29x29"
- },
- {
- "filename" : "87.png",
- "idiom" : "watch",
- "role" : "companionSettings",
- "scale" : "3x",
- "size" : "29x29"
- },
- {
- "filename" : "66.png",
- "idiom" : "watch",
- "role" : "notificationCenter",
- "scale" : "2x",
- "size" : "33x33",
- "subtype" : "45mm"
- },
- {
- "filename" : "80.png",
- "idiom" : "watch",
- "role" : "appLauncher",
- "scale" : "2x",
- "size" : "40x40",
- "subtype" : "38mm"
- },
- {
- "filename" : "88.png",
- "idiom" : "watch",
- "role" : "appLauncher",
- "scale" : "2x",
- "size" : "44x44",
- "subtype" : "40mm"
- },
- {
- "filename" : "92.png",
- "idiom" : "watch",
- "role" : "appLauncher",
- "scale" : "2x",
- "size" : "46x46",
- "subtype" : "41mm"
- },
- {
- "filename" : "100.png",
- "idiom" : "watch",
- "role" : "appLauncher",
- "scale" : "2x",
- "size" : "50x50",
- "subtype" : "44mm"
- },
- {
- "idiom" : "watch",
- "role" : "appLauncher",
- "scale" : "2x",
- "size" : "51x51",
- "subtype" : "45mm"
- },
- {
- "idiom" : "watch",
- "role" : "appLauncher",
- "scale" : "2x",
- "size" : "54x54",
- "subtype" : "49mm"
- },
- {
- "filename" : "172.png",
- "idiom" : "watch",
- "role" : "quickLook",
- "scale" : "2x",
- "size" : "86x86",
- "subtype" : "38mm"
- },
- {
- "filename" : "196.png",
- "idiom" : "watch",
- "role" : "quickLook",
- "scale" : "2x",
- "size" : "98x98",
- "subtype" : "42mm"
- },
- {
- "filename" : "216.png",
- "idiom" : "watch",
- "role" : "quickLook",
- "scale" : "2x",
- "size" : "108x108",
- "subtype" : "44mm"
- },
- {
- "idiom" : "watch",
- "role" : "quickLook",
- "scale" : "2x",
- "size" : "117x117",
- "subtype" : "45mm"
- },
- {
- "idiom" : "watch",
- "role" : "quickLook",
- "scale" : "2x",
- "size" : "129x129",
- "subtype" : "49mm"
- },
- {
- "filename" : "1024.png",
- "idiom" : "watch-marketing",
- "scale" : "1x",
- "size" : "1024x1024"
- }
- ],
- "info" : {
- "author" : "xcode",
- "version" : 1
- }
-}
diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/Contents.json b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/Contents.json
deleted file mode 100644
index 73c00596a..000000000
--- a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/Contents.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "info" : {
- "author" : "xcode",
- "version" : 1
- }
-}
diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/OneSignalLogo.imageset/onesignal_rectangle.png b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/OneSignalLogo.imageset/onesignal_rectangle.png
deleted file mode 100644
index 827725338..000000000
Binary files a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/OneSignalLogo.imageset/onesignal_rectangle.png and /dev/null differ
diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/ExampleAppWidgetAttributes.swift b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/ExampleAppWidgetAttributes.swift
deleted file mode 100644
index 6a8ed79a3..000000000
--- a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/ExampleAppWidgetAttributes.swift
+++ /dev/null
@@ -1,37 +0,0 @@
-#if targetEnvironment(macCatalyst)
-#else
-import ActivityKit
-import OneSignalLiveActivities
-
-struct ExampleAppFirstWidgetAttributes: OneSignalLiveActivityAttributes {
- public struct ContentState: OneSignalLiveActivityContentState {
- var message: String
- var onesignal: OneSignalLiveActivityContentStateData?
- }
-
- var title: String
- var onesignal: OneSignalLiveActivityAttributeData
-}
-
-struct ExampleAppSecondWidgetAttributes: OneSignalLiveActivityAttributes {
- public struct ContentState: OneSignalLiveActivityContentState {
- var message: String
- var status: String
- var progress: Double
- var bugs: Int
- var onesignal: OneSignalLiveActivityContentStateData?
- }
-
- var title: String
- var onesignal: OneSignalLiveActivityAttributeData
-}
-
-struct ExampleAppThirdWidgetAttributes: ActivityAttributes {
- public struct ContentState: Codable, Hashable {
- var message: String
- }
-
- var title: String
- var isPushToStart: Bool
-}
-#endif
diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Info.plist b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Info.plist
deleted file mode 100644
index 287ffecb7..000000000
--- a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Info.plist
+++ /dev/null
@@ -1,60 +0,0 @@
-
-
-
-
- CFBundleDevelopmentRegion
- $(DEVELOPMENT_LANGUAGE)
- CFBundleExecutable
- $(EXECUTABLE_NAME)
- CFBundleIdentifier
- $(PRODUCT_BUNDLE_IDENTIFIER)
- CFBundleInfoDictionaryVersion
- 6.0
- CFBundleName
- $(PRODUCT_NAME)
- CFBundlePackageType
- $(PRODUCT_BUNDLE_PACKAGE_TYPE)
- CFBundleShortVersionString
- 1.0
- CFBundleVersion
- 1
- LSRequiresIPhoneOS
-
- NSLocationAlwaysAndWhenInUseUsageDescription
- This app uses your location to provide location-based notifications and services.
- NSLocationWhenInUseUsageDescription
- This app uses your location to provide location-based notifications.
- UIApplicationSceneManifest
-
- UIApplicationSupportsMultipleScenes
-
-
- NSSupportsLiveActivities
-
- UIBackgroundModes
-
- remote-notification
-
- UILaunchScreen
-
- UIRequiredDeviceCapabilities
-
- armv7
-
- UISupportedInterfaceOrientations
-
- UIInterfaceOrientationPortrait
- UIInterfaceOrientationLandscapeLeft
- UIInterfaceOrientationLandscapeRight
-
- UISupportedInterfaceOrientations~ipad
-
- UIInterfaceOrientationPortrait
- UIInterfaceOrientationPortraitUpsideDown
- UIInterfaceOrientationLandscapeLeft
- UIInterfaceOrientationLandscapeRight
-
- OneSignal_disable_swizzling
-
-
-
diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Models/AppModels.swift b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Models/AppModels.swift
deleted file mode 100644
index 7bd1f3a7c..000000000
--- a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Models/AppModels.swift
+++ /dev/null
@@ -1,179 +0,0 @@
-/**
- * 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 UIKit
-
-// MARK: - Key-Value Item
-
-/// A generic key-value pair used for aliases, tags, and triggers
-struct KeyValueItem: Identifiable, Equatable {
- let id = UUID()
- let key: String
- let value: String
-}
-
-// MARK: - Notification Type
-
-/// Types of test push notifications that can be sent (matching Android: Simple, With Image, Custom)
-enum NotificationType: String, CaseIterable, Identifiable {
- case simple = "Simple Notification"
- case withImage = "Notification With Image"
- case custom = "Custom Notification"
-
- var id: String { rawValue }
-}
-
-// MARK: - In-App Message Type
-
-/// Types of in-app messages that can be displayed
-enum InAppMessageType: String, CaseIterable, Identifiable {
- case topBanner = "Top Banner"
- case bottomBanner = "Bottom Banner"
- case centerModal = "Center Modal"
- case fullScreen = "Full Screen"
-
- var id: String { rawValue }
-
- var iconName: String {
- switch self {
- case .topBanner: return "arrow.up.to.line"
- case .bottomBanner: return "arrow.down.to.line"
- case .centerModal: return "square"
- case .fullScreen: return "arrow.up.left.and.arrow.down.right"
- }
- }
-}
-
-// MARK: - Add Item Type
-
-/// Types of items that can be added via the add sheet
-enum AddItemType {
- case alias
- case email
- case sms
- case tag
- case trigger
- case externalUserId
- case customNotification
- case trackEvent
-
- var title: String {
- switch self {
- case .alias: return "Add Alias"
- case .email: return "Add Email"
- case .sms: return "Add SMS"
- case .tag: return "Add Tag"
- case .trigger: return "Add Trigger"
- case .externalUserId: return "Login User"
- case .customNotification: return "Custom Notification"
- case .trackEvent: return "Track Event"
- }
- }
-
- var requiresKeyValue: Bool {
- switch self {
- case .alias, .tag, .trigger, .customNotification: return true
- case .email, .sms, .externalUserId, .trackEvent: return false
- }
- }
-
- var keyPlaceholder: String {
- switch self {
- case .alias: return "Label"
- case .tag: return "Key"
- case .trigger: return "Key"
- case .customNotification: return "Title"
- default: return "Key"
- }
- }
-
- var valuePlaceholder: String {
- switch self {
- case .alias: return "ID"
- case .email: return "Email"
- case .sms: return "SMS"
- case .tag: return "Value"
- case .trigger: return "Value"
- case .externalUserId: return "External User Id"
- case .customNotification: return "Body"
- case .trackEvent: return "Event Name"
- }
- }
-
- var keyboardType: UIKeyboardType {
- switch self {
- case .email: return .emailAddress
- case .sms: return .phonePad
- default: return .default
- }
- }
-}
-
-// MARK: - Multi-Add Item Type
-
-/// Types for the multi-pair add dialog (Add Aliases, Add Tags, Add Triggers)
-enum MultiAddItemType: String {
- case aliases = "Add Multiple Aliases"
- case tags = "Add Multiple Tags"
- case triggers = "Add Multiple Triggers"
-}
-
-// MARK: - Remove Multi Item Type
-
-/// Types for the remove-multi checkbox dialog
-enum RemoveMultiItemType: String {
- case aliases = "Remove Aliases"
- case tags = "Remove Tags"
- case triggers = "Remove Triggers"
-}
-
-// MARK: - User Data
-
-/// Model for user data fetched from the OneSignal REST API
-struct UserData {
- let aliases: [String: String]
- let tags: [String: String]
- let emails: [String]
- let smsNumbers: [String]
- let externalId: String?
-}
-
-// MARK: - Tooltip Models
-
-/// Tooltip content fetched from the shared sdk-shared repo
-struct TooltipData {
- let title: String
- let description: String
- let options: [TooltipOption]?
-}
-
-/// An individual option within a tooltip
-struct TooltipOption {
- let name: String
- let description: String
-}
diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Services/LiveActivityController.swift b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Services/LiveActivityController.swift
deleted file mode 100644
index 090a51ba4..000000000
--- a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Services/LiveActivityController.swift
+++ /dev/null
@@ -1,83 +0,0 @@
-import Foundation
-import OneSignalFramework
-#if targetEnvironment(macCatalyst)
-#else
-import ActivityKit
-import OneSignalLiveActivities
-
-class LiveActivityController {
-
- @available(iOS 16.1, *)
- static func start() {
- OneSignal.LiveActivities.setup(ExampleAppFirstWidgetAttributes.self)
- OneSignal.LiveActivities.setup(ExampleAppSecondWidgetAttributes.self)
- OneSignal.LiveActivities.setupDefault()
-
- if #available(iOS 17.2, *) {
- Task {
- for try await data in Activity.pushToStartTokenUpdates {
- let token = data.map { String(format: "%02x", $0) }.joined()
- OneSignal.LiveActivities.setPushToStartToken(ExampleAppThirdWidgetAttributes.self, withToken: token)
- }
- }
- Task {
- for await activity in Activity.activityUpdates
- where activity.attributes.isPushToStart {
- Task {
- for await pushToken in activity.pushTokenUpdates {
- let token = pushToken.map { String(format: "%02x", $0) }.joined()
- OneSignal.LiveActivities.enter("my-activity-id", withToken: token)
- }
- }
- }
- }
- }
- }
-
- static var counter1 = 0
-
- @available(iOS 16.1, *)
- static func createOneSignalAwareActivity(activityId: String) {
- counter1 += 1
- let oneSignalAttribute = OneSignalLiveActivityAttributeData.create(activityId: activityId)
- let attributes = ExampleAppFirstWidgetAttributes(title: "#\(counter1) Live Activity", onesignal: oneSignalAttribute)
- let contentState = ExampleAppFirstWidgetAttributes.ContentState(message: "Update this message through push")
- do {
- _ = try Activity.request(
- attributes: attributes,
- contentState: contentState,
- pushType: .token)
- } catch {
- print(error.localizedDescription)
- }
- }
-
- @available(iOS 16.1, *)
- static func createDefaultActivity(activityId: String) {
- let attributeData: [String: Any] = ["title": "in-app-title"]
- let contentData: [String: Any] = ["message": ["en": "HELLO", "es": "HOLA"], "progress": 0.58, "status": "1/15", "bugs": 2]
- OneSignal.LiveActivities.startDefault(activityId, attributes: attributeData, content: contentData)
- }
-
- static var counter2 = 0
-
- @available(iOS 16.1, *)
- static func createActivity(activityId: String) async {
- counter2 += 1
- let attributes = ExampleAppThirdWidgetAttributes(title: "#\(counter2) Live Activity", isPushToStart: false)
- let contentState = ExampleAppThirdWidgetAttributes.ContentState(message: "Update this message through push")
- do {
- let activity = try Activity.request(
- attributes: attributes,
- contentState: contentState,
- pushType: .token)
- for await data in activity.pushTokenUpdates {
- let myToken = data.map { String(format: "%02x", $0) }.joined()
- OneSignal.LiveActivities.enter(activityId, withToken: myToken)
- }
- } catch {
- print(error.localizedDescription)
- }
- }
-}
-#endif
diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Services/LogManager.swift b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Services/LogManager.swift
deleted file mode 100644
index d5a576208..000000000
--- a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Services/LogManager.swift
+++ /dev/null
@@ -1,92 +0,0 @@
-/**
- * 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 SwiftUI
-
-/// Log level for categorizing log entries
-enum LogLevel: String {
- case debug = "D"
- case info = "I"
- case warning = "W"
- case error = "E"
-
- var color: Color {
- switch self {
- case .debug: return .blue
- case .info: return .green
- case .warning: return .orange
- case .error: return .red
- }
- }
-}
-
-/// A single log entry
-struct LogEntry: Identifiable {
- let id = UUID()
- let timestamp: Date
- let level: LogLevel
- let message: String
-
- var formattedTimestamp: String {
- let formatter = DateFormatter()
- formatter.dateFormat = "HH:mm:ss"
- return formatter.string(from: timestamp)
- }
-}
-
-/// Thread-safe pass-through logger that captures logs for UI display and prints to console
-@MainActor
-final class LogManager: ObservableObject {
- static let shared = LogManager()
-
- @Published var entries: [LogEntry] = []
-
- private let maxEntries = 100
-
- private init() {}
-
- func log(_ tag: String, _ message: String, level: LogLevel = .debug) {
- let entry = LogEntry(timestamp: Date(), level: level, message: "[\(tag)] \(message)")
- entries.append(entry)
- if entries.count > maxEntries {
- entries.removeFirst(entries.count - maxEntries)
- }
- // Also print to console
- print("\(entry.formattedTimestamp) \(level.rawValue) \(entry.message)")
- }
-
- func clear() {
- entries.removeAll()
- }
-
- // Convenience methods
- func d(_ tag: String, _ message: String) { log(tag, message, level: .debug) }
- func i(_ tag: String, _ message: String) { log(tag, message, level: .info) }
- func w(_ tag: String, _ message: String) { log(tag, message, level: .warning) }
- func e(_ tag: String, _ message: String) { log(tag, message, level: .error) }
-}
diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Services/NotificationSender.swift b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Services/NotificationSender.swift
deleted file mode 100644
index d1758006a..000000000
--- a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Services/NotificationSender.swift
+++ /dev/null
@@ -1,176 +0,0 @@
-/**
- * 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 OneSignalFramework
-
-/// Service for sending push notifications via OneSignal API
-/// Note: This is for demo purposes only. In production, API calls should be made from your backend.
-final class NotificationSender {
-
- static let shared = NotificationSender()
-
- private let apiURL = URL(string: "https://onesignal.com/api/v1/notifications")!
- private let imageURL = "https://media.onesignal.com/automated_push_templates/ratings_template.png"
-
- private init() {}
-
- // MARK: - Public Methods
-
- /// Send a simple push notification with a basic title and body
- func sendSimpleNotification(
- appId: String,
- completion: @escaping (Result) -> Void
- ) {
- guard let subscriptionId = getSubscriptionId(completion: completion) else { return }
-
- let payload: [String: Any] = [
- "app_id": appId,
- "include_subscription_ids": [subscriptionId],
- "headings": ["en": "Simple Notification"],
- "contents": ["en": "This is a simple test notification from OneSignal."],
- "ios_sound": "nil"
- ]
-
- sendRequest(payload: payload, completion: completion)
- }
-
- /// Send a push notification that includes a big image
- func sendNotificationWithImage(
- appId: String,
- completion: @escaping (Result) -> Void
- ) {
- guard let subscriptionId = getSubscriptionId(completion: completion) else { return }
-
- let payload: [String: Any] = [
- "app_id": appId,
- "include_subscription_ids": [subscriptionId],
- "headings": ["en": "Image Notification"],
- "contents": ["en": "This notification includes an image attachment."],
- "ios_attachments": ["image": imageURL],
- "big_picture": imageURL,
- "ios_sound": "nil"
- ]
-
- sendRequest(payload: payload, completion: completion)
- }
-
- /// Send a custom push notification with user-provided title and body
- func sendCustomNotification(
- title: String,
- body: String,
- appId: String,
- completion: @escaping (Result) -> Void
- ) {
- guard let subscriptionId = getSubscriptionId(completion: completion) else { return }
-
- let payload: [String: Any] = [
- "app_id": appId,
- "include_subscription_ids": [subscriptionId],
- "headings": ["en": title],
- "contents": ["en": body],
- "ios_sound": "nil"
- ]
-
- sendRequest(payload: payload, completion: completion)
- }
-
- // MARK: - Private Helpers
-
- private func getSubscriptionId(completion: @escaping (Result) -> Void) -> String? {
- guard let subscriptionId = OneSignal.User.pushSubscription.id else {
- completion(.failure(NotificationError.noSubscriptionId))
- return nil
- }
-
- guard OneSignal.User.pushSubscription.optedIn else {
- completion(.failure(NotificationError.notOptedIn))
- return nil
- }
-
- return subscriptionId
- }
-
- private func sendRequest(
- payload: [String: Any],
- completion: @escaping (Result) -> Void
- ) {
- var request = URLRequest(url: apiURL)
- request.httpMethod = "POST"
- request.setValue("application/json; charset=UTF-8", forHTTPHeaderField: "Content-Type")
- request.setValue("application/vnd.onesignal.v1+json", forHTTPHeaderField: "Accept")
- request.timeoutInterval = 30
-
- do {
- request.httpBody = try JSONSerialization.data(withJSONObject: payload)
- } catch {
- completion(.failure(error))
- return
- }
-
- URLSession.shared.dataTask(with: request) { data, response, error in
- if let error = error {
- print("[OneSignal] Failed to send notification: \(error.localizedDescription)")
- completion(.failure(error))
- return
- }
-
- if let httpResponse = response as? HTTPURLResponse {
- if httpResponse.statusCode == 200 || httpResponse.statusCode == 202 {
- if let data = data, let responseStr = String(data: data, encoding: .utf8) {
- print("[OneSignal] Success sending notification: \(responseStr)")
- }
- completion(.success(()))
- } else {
- if let data = data, let responseStr = String(data: data, encoding: .utf8) {
- print("[OneSignal] Failed (\(httpResponse.statusCode)): \(responseStr)")
- }
- completion(.failure(NotificationError.apiError(statusCode: httpResponse.statusCode)))
- }
- }
- }.resume()
- }
-}
-
-// MARK: - Errors
-
-enum NotificationError: LocalizedError {
- case noSubscriptionId
- case notOptedIn
- case apiError(statusCode: Int)
-
- var errorDescription: String? {
- switch self {
- case .noSubscriptionId:
- return "No push subscription ID available"
- case .notOptedIn:
- return "Push notifications not opted in"
- case .apiError(let statusCode):
- return "API error with status code: \(statusCode)"
- }
- }
-}
diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Services/TooltipService.swift b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Services/TooltipService.swift
deleted file mode 100644
index bbeb8dae8..000000000
--- a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Services/TooltipService.swift
+++ /dev/null
@@ -1,101 +0,0 @@
-/**
- * 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
-
-/// Service that fetches and provides tooltip content from the shared sdk-shared repo.
-/// Tooltips are non-critical; if the fetch fails, they are simply unavailable.
-final class TooltipService: ObservableObject {
-
- static let shared = TooltipService()
-
- private let tooltipURL = URL(string: "https://raw.githubusercontent.com/OneSignal/sdk-shared/main/demo/tooltip_content.json")!
-
- @Published private(set) var tooltips: [String: TooltipData] = [:]
- private var initialized = false
-
- private init() {}
-
- /// Fetch tooltip content on a background thread. Safe to call multiple times; only fetches once.
- func initialize() {
- guard !initialized else { return }
- initialized = true
-
- DispatchQueue.global(qos: .utility).async { [weak self] in
- self?.fetchTooltips()
- }
- }
-
- /// Returns tooltip data for the given section key, or nil if unavailable.
- func getTooltip(key: String) -> TooltipData? {
- tooltips[key]
- }
-
- // MARK: - Private
-
- private func fetchTooltips() {
- var request = URLRequest(url: tooltipURL)
- request.timeoutInterval = 10
-
- let task = URLSession.shared.dataTask(with: request) { [weak self] data, _, error in
- guard let data = data, error == nil else {
- print("[TooltipService] Failed to fetch tooltips: \(error?.localizedDescription ?? "unknown")")
- return
- }
-
- do {
- guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] else { return }
- var parsed: [String: TooltipData] = [:]
-
- for (key, value) in json {
- guard let dict = value as? [String: Any],
- let title = dict["title"] as? String,
- let description = dict["description"] as? String else { continue }
-
- var options: [TooltipOption]?
- if let optionsArray = dict["options"] as? [[String: Any]] {
- options = optionsArray.compactMap { optDict in
- guard let name = optDict["name"] as? String,
- let desc = optDict["description"] as? String else { return nil }
- return TooltipOption(name: name, description: desc)
- }
- }
-
- parsed[key] = TooltipData(title: title, description: description, options: options)
- }
-
- DispatchQueue.main.async {
- self?.tooltips = parsed
- print("[TooltipService] Loaded \(parsed.count) tooltips")
- }
- } catch {
- print("[TooltipService] Failed to parse tooltips: \(error.localizedDescription)")
- }
- }
- task.resume()
- }
-}
diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/ViewModels/OneSignalViewModel.swift b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/ViewModels/OneSignalViewModel.swift
deleted file mode 100644
index b7d4a0624..000000000
--- a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/ViewModels/OneSignalViewModel.swift
+++ /dev/null
@@ -1,627 +0,0 @@
-/**
- * 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 Combine
-import OneSignalFramework
-
-/// Main ViewModel managing all OneSignal SDK state and interactions
-@MainActor
-final class OneSignalViewModel: ObservableObject {
-
- // MARK: - Published Properties
-
- // App Info
- @Published var appId: String
-
- // User
- @Published var externalUserId: String?
- @Published var aliases: [KeyValueItem] = []
-
- // Push Subscription
- @Published var pushSubscriptionId: String?
- @Published var isPushEnabled: Bool = false
- @Published var notificationPermissionGranted: Bool = false
-
- // Email & SMS
- @Published var emails: [String] = []
- @Published var smsNumbers: [String] = []
-
- // Tags
- @Published var tags: [KeyValueItem] = []
-
- // In-App Messaging
- @Published var isInAppMessagesPaused: Bool = true
- @Published var triggers: [KeyValueItem] = []
-
- // Location
- @Published var isLocationShared: Bool = false
-
- // Consent
- @Published var consentRequired: Bool = UserDefaults.standard.bool(forKey: "CachedConsentRequired")
- @Published var consentGiven: Bool = UserDefaults.standard.bool(forKey: "CachedPrivacyConsent")
-
- // Loading
- @Published var isLoading: Bool = false
-
- // UI State
- @Published var showingAddSheet: Bool = false
- @Published var addItemType: AddItemType = .email
- @Published var showingMultiAddSheet: Bool = false
- @Published var multiAddType: MultiAddItemType = .tags
- @Published var showingRemoveMultiSheet: Bool = false
- @Published var removeMultiType: RemoveMultiItemType = .tags
- @Published var showingCustomNotificationSheet: Bool = false
- @Published var showingTrackEventSheet: Bool = false
- @Published var toastMessage: String?
-
- // MARK: - Computed Properties
-
- var isLoggedIn: Bool {
- externalUserId != nil && !(externalUserId?.isEmpty ?? true)
- }
-
- var loginButtonTitle: String {
- isLoggedIn ? "Switch User" : "Login User"
- }
-
- /// Items for remove-multi dialog based on current type
- var removeMultiItems: [KeyValueItem] {
- switch removeMultiType {
- case .aliases: return aliases
- case .tags: return tags
- case .triggers: return triggers
- }
- }
-
- // MARK: - Private Properties
-
- private let service: OneSignalService
- private var observers = Observers()
-
- // MARK: - Initialization
-
- init(service: OneSignalService = .shared) {
- self.service = service
- self.appId = service.appId
- self.notificationPermissionGranted = service.hasNotificationPermission
-
- // Load external user ID from SDK
- self.externalUserId = service.externalId
-
- // Initial state sync
- refreshState()
-
- // Set up observers
- setupObservers()
-
- // Fetch user data if we have a onesignalId
- if service.onesignalId != nil {
- Task {
- await fetchUserDataFromApi()
- }
- }
- }
-
- // MARK: - State Management
-
- func refreshState() {
- pushSubscriptionId = service.pushSubscriptionId
- isPushEnabled = service.isPushEnabled
- isInAppMessagesPaused = service.isInAppMessagesPaused
- isLocationShared = service.isLocationShared
- notificationPermissionGranted = service.hasNotificationPermission
- externalUserId = service.externalId
-
- // Sync tags from SDK
- let sdkTags = service.getTags()
- tags = sdkTags.map { KeyValueItem(key: $0.key, value: $0.value) }
- }
-
- // MARK: - User Data Fetching
-
- func fetchUserDataFromApi() async {
- guard let onesignalId = service.onesignalId else { return }
-
- isLoading = true
-
- if let userData = await UserFetchService.shared.fetchUser(appId: appId, onesignalId: onesignalId) {
- aliases = userData.aliases.map { KeyValueItem(key: $0.key, value: $0.value) }
- tags = userData.tags.map { KeyValueItem(key: $0.key, value: $0.value) }
- emails = userData.emails
- smsNumbers = userData.smsNumbers
- if let extId = userData.externalId, !extId.isEmpty {
- externalUserId = extId
- }
- }
-
- // Small delay to ensure UI populates before dismissing loading
- try? await Task.sleep(nanoseconds: 100_000_000)
- isLoading = false
- }
-
- // MARK: - Consent
-
- func toggleConsentRequired() {
- consentRequired.toggle()
- service.setConsentRequired(consentRequired)
- UserDefaults.standard.set(consentRequired, forKey: "CachedConsentRequired")
- if !consentRequired {
- // When turning off consent required, also grant consent
- consentGiven = true
- service.setConsentGiven(true)
- UserDefaults.standard.set(true, forKey: "CachedPrivacyConsent")
- }
- showToast(consentRequired ? "Consent required enabled" : "Consent required disabled")
- }
-
- func toggleConsent() {
- consentGiven.toggle()
- service.setConsentGiven(consentGiven)
- UserDefaults.standard.set(consentGiven, forKey: "CachedPrivacyConsent")
- showToast(consentGiven ? "Consent given" : "Consent revoked")
- }
-
- // MARK: - User Management
-
- func login(externalId: String) {
- isLoading = true
- service.login(externalId: externalId)
- externalUserId = externalId
-
- // Clear old data; will be repopulated by fetchUserDataFromApi when user state changes
- aliases.removeAll()
- emails.removeAll()
- smsNumbers.removeAll()
- tags.removeAll()
-
- showToast("Logged in as \(externalId)")
- }
-
- func logout() {
- isLoading = true
- service.logout()
- externalUserId = nil
- aliases.removeAll()
- emails.removeAll()
- smsNumbers.removeAll()
- tags.removeAll()
- triggers.removeAll()
- isLoading = false
- showToast("Logged out")
- }
-
- // MARK: - Aliases
-
- func addAlias(label: String, id: String) {
- service.addAlias(label: label, id: id)
- aliases.removeAll { $0.key == label }
- aliases.append(KeyValueItem(key: label, value: id))
- showToast("Alias added")
- }
-
- func addAliases(_ pairs: [(String, String)]) {
- let dict = Dictionary(pairs, uniquingKeysWith: { _, last in last })
- service.addAliases(dict)
- for (key, value) in pairs {
- aliases.removeAll { $0.key == key }
- aliases.append(KeyValueItem(key: key, value: value))
- }
- showToast("\(pairs.count) alias(es) added")
- }
-
- func removeAlias(_ item: KeyValueItem) {
- service.removeAlias(item.key)
- aliases.removeAll { $0.id == item.id }
- showToast("Alias removed")
- }
-
- func removeSelectedAliases(_ keys: [String]) {
- guard !keys.isEmpty else { return }
- service.removeAliases(keys)
- aliases.removeAll { keys.contains($0.key) }
- showToast("\(keys.count) alias(es) removed")
- }
-
- // MARK: - Push Subscription
-
- func togglePushEnabled() {
- if isPushEnabled {
- service.optOutPush()
- isPushEnabled = false
- showToast("Push disabled")
- } else {
- service.optInPush()
- isPushEnabled = true
- showToast("Push enabled")
- }
- }
-
- func requestPushPermission() {
- service.requestPushPermission { [weak self] accepted in
- Task { @MainActor in
- self?.notificationPermissionGranted = accepted
- self?.isPushEnabled = accepted
- self?.showToast(accepted ? "Push permission granted" : "Push permission denied")
- }
- }
- }
-
- // MARK: - Email
-
- func addEmail(_ email: String) {
- service.addEmail(email)
- if !emails.contains(email) {
- emails.append(email)
- }
- showToast("Email added")
- }
-
- func removeEmail(_ email: String) {
- service.removeEmail(email)
- emails.removeAll { $0 == email }
- showToast("Email removed")
- }
-
- // MARK: - SMS
-
- func addSms(_ number: String) {
- service.addSms(number)
- if !smsNumbers.contains(number) {
- smsNumbers.append(number)
- }
- showToast("SMS added")
- }
-
- func removeSms(_ number: String) {
- service.removeSms(number)
- smsNumbers.removeAll { $0 == number }
- showToast("SMS removed")
- }
-
- // MARK: - Tags
-
- func addTag(key: String, value: String) {
- service.addTag(key: key, value: value)
- tags.removeAll { $0.key == key }
- tags.append(KeyValueItem(key: key, value: value))
- showToast("Tag added")
- }
-
- func addTags(_ pairs: [(String, String)]) {
- let dict = Dictionary(pairs, uniquingKeysWith: { _, last in last })
- service.addTags(dict)
- for (key, value) in pairs {
- tags.removeAll { $0.key == key }
- tags.append(KeyValueItem(key: key, value: value))
- }
- showToast("\(pairs.count) tag(s) added")
- }
-
- func removeTag(_ item: KeyValueItem) {
- service.removeTag(item.key)
- tags.removeAll { $0.id == item.id }
- showToast("Tag removed")
- }
-
- func removeSelectedTags(_ keys: [String]) {
- guard !keys.isEmpty else { return }
- service.removeTags(keys)
- tags.removeAll { keys.contains($0.key) }
- showToast("\(keys.count) tag(s) removed")
- }
-
- // MARK: - Outcomes
-
- func sendOutcome(_ name: String) {
- service.sendOutcome(name)
- showToast("Outcome '\(name)' sent")
- }
-
- func sendOutcome(_ name: String, value: Double) {
- service.sendOutcome(name, value: NSNumber(value: value))
- showToast("Outcome '\(name)' with value \(value) sent")
- }
-
- func sendUniqueOutcome(_ name: String) {
- service.sendUniqueOutcome(name)
- showToast("Unique outcome '\(name)' sent")
- }
-
- // MARK: - In-App Messaging
-
- func toggleInAppMessagesPaused() {
- isInAppMessagesPaused.toggle()
- service.isInAppMessagesPaused = isInAppMessagesPaused
- UserDefaults.standard.set(isInAppMessagesPaused, forKey: "CachedInAppMessagesPaused")
- showToast(isInAppMessagesPaused ? "In-app messages paused" : "In-app messages resumed")
- }
-
- func addTrigger(key: String, value: String) {
- service.addTrigger(key: key, value: value)
- triggers.removeAll { $0.key == key }
- triggers.append(KeyValueItem(key: key, value: value))
- showToast("Trigger added")
- }
-
- func addTriggers(_ pairs: [(String, String)]) {
- let dict = Dictionary(pairs, uniquingKeysWith: { _, last in last })
- service.addTriggers(dict)
- for (key, value) in pairs {
- triggers.removeAll { $0.key == key }
- triggers.append(KeyValueItem(key: key, value: value))
- }
- showToast("\(pairs.count) trigger(s) added")
- }
-
- func removeTrigger(_ item: KeyValueItem) {
- service.removeTrigger(item.key)
- triggers.removeAll { $0.id == item.id }
- showToast("Trigger removed")
- }
-
- func removeSelectedTriggers(_ keys: [String]) {
- guard !keys.isEmpty else { return }
- service.removeTriggers(keys)
- triggers.removeAll { keys.contains($0.key) }
- showToast("\(keys.count) trigger(s) removed")
- }
-
- func clearTriggers() {
- service.clearTriggers()
- triggers.removeAll()
- showToast("All triggers cleared")
- }
-
- // MARK: - Track Event
-
- func trackEvent(name: String, properties: [String: Any]? = nil) {
- OneSignal.User.trackEvent(name: name, properties: properties)
- showToast("Event '\(name)' tracked")
- }
-
- // MARK: - Location
-
- func toggleLocationShared() {
- isLocationShared.toggle()
- service.isLocationShared = isLocationShared
- UserDefaults.standard.set(isLocationShared, forKey: "CachedLocationShared")
- showToast(isLocationShared ? "Location sharing enabled" : "Location sharing disabled")
- }
-
- func promptLocation() {
- service.requestLocationPermission()
- showToast("Location permission requested")
- }
-
- // MARK: - Live Activities
-
- func enterLiveActivity(activityId: String) {
- let id = activityId.trimmingCharacters(in: .whitespacesAndNewlines)
- guard !id.isEmpty else {
- showToast("Please enter an activity ID")
- return
- }
- if #available(iOS 16.1, *) {
- LiveActivityController.createOneSignalAwareActivity(activityId: id)
- showToast("Live Activity '\(id)' entered")
- } else {
- showToast("Live Activities require iOS 16.1+")
- }
- }
-
- func exitLiveActivity(activityId: String) {
- let id = activityId.trimmingCharacters(in: .whitespacesAndNewlines)
- guard !id.isEmpty else {
- showToast("Please enter an activity ID")
- return
- }
- OneSignal.LiveActivities.exit(id)
- showToast("Live Activity '\(id)' exited")
- }
-
- // MARK: - Observers
-
- private func setupObservers() {
- observers.viewModel = self
- service.addPushSubscriptionObserver(observers)
- service.addUserObserver(observers)
- service.addPermissionObserver(observers)
- }
-}
-
-// MARK: - Notifications
-
-extension OneSignalViewModel {
-
- func clearAllNotifications() {
- service.clearAllNotifications()
- showToast("All notifications cleared")
- }
-
- func sendSimpleNotification() {
- showToast("Sending simple notification...")
- NotificationSender.shared.sendSimpleNotification(appId: appId) { [weak self] result in
- Task { @MainActor in
- switch result {
- case .success:
- self?.showToast("Simple notification sent!")
- case .failure(let error):
- self?.showToast("Failed: \(error.localizedDescription)")
- }
- }
- }
- }
-
- func sendNotificationWithImage() {
- showToast("Sending image notification...")
- NotificationSender.shared.sendNotificationWithImage(appId: appId) { [weak self] result in
- Task { @MainActor in
- switch result {
- case .success:
- self?.showToast("Image notification sent!")
- case .failure(let error):
- self?.showToast("Failed: \(error.localizedDescription)")
- }
- }
- }
- }
-
- func sendCustomNotification(title: String, body: String) {
- showToast("Sending custom notification...")
- NotificationSender.shared.sendCustomNotification(title: title, body: body, appId: appId) { [weak self] result in
- Task { @MainActor in
- switch result {
- case .success:
- self?.showToast("Custom notification sent!")
- case .failure(let error):
- self?.showToast("Failed: \(error.localizedDescription)")
- }
- }
- }
- }
-
- func sendTestInAppMessage(_ type: InAppMessageType) {
- let triggerValue: String
- switch type {
- case .topBanner: triggerValue = "top_banner"
- case .bottomBanner: triggerValue = "bottom_banner"
- case .centerModal: triggerValue = "center_modal"
- case .fullScreen: triggerValue = "full_screen"
- }
- service.addTrigger(key: "iam_type", value: triggerValue)
- showToast("Sent In-App Message: \(type.rawValue)")
- }
-}
-
-// MARK: - Sheet Handling
-
-extension OneSignalViewModel {
-
- func showAddSheet(for type: AddItemType) {
- addItemType = type
- showingAddSheet = true
- }
-
- func showMultiAddSheet(for type: MultiAddItemType) {
- multiAddType = type
- showingMultiAddSheet = true
- }
-
- func showRemoveMultiSheet(for type: RemoveMultiItemType) {
- removeMultiType = type
- showingRemoveMultiSheet = true
- }
-
- func handleAddItem(key: String, value: String) {
- switch addItemType {
- case .alias:
- addAlias(label: key, id: value)
- case .email:
- addEmail(value)
- case .sms:
- addSms(value)
- case .tag:
- addTag(key: key, value: value)
- case .trigger:
- addTrigger(key: key, value: value)
- case .externalUserId:
- login(externalId: value)
- case .customNotification:
- sendCustomNotification(title: key, body: value)
- case .trackEvent:
- trackEvent(name: value)
- }
- showingAddSheet = false
- }
-
- func handleMultiAdd(pairs: [(String, String)]) {
- switch multiAddType {
- case .aliases:
- addAliases(pairs)
- case .tags:
- addTags(pairs)
- case .triggers:
- addTriggers(pairs)
- }
- showingMultiAddSheet = false
- }
-
- func handleRemoveMulti(keys: [String]) {
- switch removeMultiType {
- case .aliases:
- removeSelectedAliases(keys)
- case .tags:
- removeSelectedTags(keys)
- case .triggers:
- removeSelectedTriggers(keys)
- }
- showingRemoveMultiSheet = false
- }
-}
-
-// MARK: - Toast
-
-extension OneSignalViewModel {
-
- func showToast(_ message: String) {
- toastMessage = message
-
- Task {
- try? await Task.sleep(nanoseconds: 2_000_000_000)
- toastMessage = nil
- }
- }
-}
-
-// MARK: - Observer Classes
-
-private class Observers: NSObject, OSPushSubscriptionObserver, OSUserStateObserver, OSNotificationPermissionObserver {
- weak var viewModel: OneSignalViewModel?
-
- func onPushSubscriptionDidChange(state: OSPushSubscriptionChangedState) {
- Task { @MainActor in
- viewModel?.pushSubscriptionId = state.current.id
- viewModel?.isPushEnabled = state.current.optedIn
- }
- }
-
- func onUserStateDidChange(state: OSUserChangedState) {
- Task { @MainActor in
- LogManager.shared.i("User", "User state changed: \(state.jsonRepresentation())")
- // Fetch fresh user data from API when user state changes
- await viewModel?.fetchUserDataFromApi()
- }
- }
-
- func onNotificationPermissionDidChange(_ permission: Bool) {
- Task { @MainActor in
- viewModel?.notificationPermissionGranted = permission
- viewModel?.isPushEnabled = OneSignal.User.pushSubscription.optedIn
- }
- }
-}
diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Components/AddItemSheet.swift b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Components/AddItemSheet.swift
deleted file mode 100644
index 62ee72211..000000000
--- a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Components/AddItemSheet.swift
+++ /dev/null
@@ -1,176 +0,0 @@
-/**
- * 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 SwiftUI
-
-/// A sheet for adding items with one or two text fields (dialog style matching screenshots)
-struct AddItemSheet: View {
- let itemType: AddItemType
- let onAdd: (String, String) -> Void
- let onCancel: () -> Void
-
- @State private var keyText: String = ""
- @State private var valueText: String = ""
- @FocusState private var focusedField: Field?
-
- private enum Field {
- case key, value
- }
-
- var body: some View {
- NavigationStack {
- VStack(spacing: 24) {
- // Title
- Text(itemType.title)
- .font(.title2)
- .fontWeight(.semibold)
- .frame(maxWidth: .infinity, alignment: .leading)
-
- // Input Fields
- if itemType.requiresKeyValue {
- VStack(alignment: .leading, spacing: 8) {
- Text("Key")
- .font(.caption)
- .foregroundColor(.secondary)
- TextField(itemType.keyPlaceholder, text: $keyText)
- .textFieldStyle(UnderlineTextFieldStyle())
- .focused($focusedField, equals: .key)
- .textInputAutocapitalization(.never)
- .autocorrectionDisabled()
- }
-
- VStack(alignment: .leading, spacing: 8) {
- Text("Value")
- .font(.caption)
- .foregroundColor(.secondary)
- TextField(itemType.valuePlaceholder, text: $valueText)
- .textFieldStyle(UnderlineTextFieldStyle())
- .focused($focusedField, equals: .value)
- .textInputAutocapitalization(.never)
- .autocorrectionDisabled()
- .keyboardType(itemType.keyboardType)
- }
- } else {
- VStack(alignment: .leading, spacing: 8) {
- Text(singleFieldLabel)
- .font(.caption)
- .foregroundColor(.secondary)
- TextField(itemType.valuePlaceholder, text: $valueText)
- .textFieldStyle(UnderlineTextFieldStyle())
- .focused($focusedField, equals: .value)
- .textInputAutocapitalization(.never)
- .autocorrectionDisabled()
- .keyboardType(itemType.keyboardType)
- }
- }
-
- Spacer()
-
- // Action Buttons
- HStack(spacing: 24) {
- Spacer()
-
- Button("CANCEL") {
- onCancel()
- }
- .foregroundColor(.accentColor)
-
- Button(itemType == .externalUserId ? "LOGIN" : "ADD") {
- onAdd(keyText, valueText)
- }
- .foregroundColor(isValid ? .accentColor : .gray)
- .disabled(!isValid)
- }
- .font(.system(size: 16, weight: .semibold))
- }
- .padding(24)
- .onAppear {
- focusedField = itemType.requiresKeyValue ? .key : .value
- }
- }
- .presentationDetents([.medium])
- .presentationDragIndicator(.visible)
- }
-
- private var singleFieldLabel: String {
- switch itemType {
- case .email: return "New Email"
- case .sms: return "New SMS"
- case .externalUserId: return "External User Id"
- default: return "Value"
- }
- }
-
- private var isValid: Bool {
- if itemType.requiresKeyValue {
- return !keyText.trimmingCharacters(in: .whitespaces).isEmpty &&
- !valueText.trimmingCharacters(in: .whitespaces).isEmpty
- } else {
- return !valueText.trimmingCharacters(in: .whitespaces).isEmpty
- }
- }
-}
-
-/// A text field style with an underline instead of a border
-struct UnderlineTextFieldStyle: TextFieldStyle {
- // swiftlint:disable:next identifier_name
- func _body(configuration: TextField) -> some View {
- VStack(spacing: 0) {
- configuration
- .font(.system(size: 17))
- .padding(.vertical, 8)
-
- Rectangle()
- .fill(Color(.separator))
- .frame(height: 1)
- }
- }
-}
-
-#Preview("Add Alias") {
- AddItemSheet(
- itemType: .alias,
- onAdd: { key, value in print("Add: \(key) = \(value)") },
- onCancel: { print("Cancel") }
- )
-}
-
-#Preview("Add Email") {
- AddItemSheet(
- itemType: .email,
- onAdd: { _, value in print("Add: \(value)") },
- onCancel: { print("Cancel") }
- )
-}
-
-#Preview("Login User") {
- AddItemSheet(
- itemType: .externalUserId,
- onAdd: { _, value in print("Login: \(value)") },
- onCancel: { print("Cancel") }
- )
-}
diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Components/AddMultiItemSheet.swift b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Components/AddMultiItemSheet.swift
deleted file mode 100644
index 75e734bf2..000000000
--- a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Components/AddMultiItemSheet.swift
+++ /dev/null
@@ -1,140 +0,0 @@
-/**
- * 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 SwiftUI
-
-/// A multi-pair add dialog with dynamic rows, matching the Android "Add Tags/Aliases/Triggers" dialog.
-struct AddMultiItemSheet: View {
- let type: MultiAddItemType
- let onAdd: ([(String, String)]) -> Void
- let onCancel: () -> Void
-
- @State private var rows: [(key: String, value: String)] = [("", "")]
-
- var body: some View {
- NavigationStack {
- VStack(spacing: 16) {
- // Title
- Text(type.rawValue)
- .font(.title2)
- .fontWeight(.semibold)
- .frame(maxWidth: .infinity, alignment: .leading)
-
- // Rows
- ScrollView {
- VStack(spacing: 12) {
- ForEach(rows.indices, id: \.self) { index in
- HStack(spacing: 8) {
- TextField("", text: Binding(
- get: { rows[index].key },
- set: { rows[index].key = $0 }
- ))
- .textFieldStyle(UnderlineTextFieldStyle())
- .textInputAutocapitalization(.never)
- .autocorrectionDisabled()
-
- TextField("", text: Binding(
- get: { rows[index].value },
- set: { rows[index].value = $0 }
- ))
- .textFieldStyle(UnderlineTextFieldStyle())
- .textInputAutocapitalization(.never)
- .autocorrectionDisabled()
-
- if rows.count > 1 {
- Button {
- rows.remove(at: index)
- } label: {
- Image(systemName: "xmark")
- .font(.system(size: 14, weight: .medium))
- .foregroundColor(.red)
- }
- .buttonStyle(.borderless)
- }
- }
- }
- }
- }
-
- // Add Row button
- Button {
- rows.append(("", ""))
- } label: {
- Text("+ ADD ROW")
- .font(.system(size: 14, weight: .semibold))
- .foregroundColor(.accentColor)
- .frame(maxWidth: .infinity)
- .padding(.vertical, 10)
- .background(Color(.systemGray6))
- .cornerRadius(8)
- }
- .buttonStyle(.plain)
-
- Spacer()
-
- // Action Buttons
- HStack(spacing: 24) {
- Spacer()
-
- Button("CANCEL") {
- onCancel()
- }
- .foregroundColor(.accentColor)
-
- Button("ADD") {
- let pairs = rows
- .filter { !$0.key.trimmingCharacters(in: .whitespaces).isEmpty &&
- !$0.value.trimmingCharacters(in: .whitespaces).isEmpty }
- .map { ($0.key, $0.value) }
- onAdd(pairs)
- }
- .foregroundColor(isValid ? .accentColor : .gray)
- .disabled(!isValid)
- }
- .font(.system(size: 16, weight: .semibold))
- }
- .padding(24)
- }
- .presentationDetents([.medium])
- .presentationDragIndicator(.visible)
- }
-
- private var isValid: Bool {
- rows.allSatisfy {
- !$0.key.trimmingCharacters(in: .whitespaces).isEmpty &&
- !$0.value.trimmingCharacters(in: .whitespaces).isEmpty
- }
- }
-}
-
-#Preview {
- AddMultiItemSheet(
- type: .tags,
- onAdd: { pairs in print("Add: \(pairs)") },
- onCancel: { print("Cancel") }
- )
-}
diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Components/CustomNotificationSheet.swift b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Components/CustomNotificationSheet.swift
deleted file mode 100644
index 896f1f063..000000000
--- a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Components/CustomNotificationSheet.swift
+++ /dev/null
@@ -1,113 +0,0 @@
-/**
- * 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 SwiftUI
-
-/// A dialog for entering a custom notification title and body.
-struct CustomNotificationSheet: View {
- let onSend: (String, String) -> Void
- let onCancel: () -> Void
-
- @State private var titleText: String = ""
- @State private var bodyText: String = ""
- @FocusState private var focusedField: Field?
-
- private enum Field {
- case title, body
- }
-
- var body: some View {
- NavigationStack {
- VStack(spacing: 24) {
- // Title
- Text("Custom Notification")
- .font(.title2)
- .fontWeight(.semibold)
- .frame(maxWidth: .infinity, alignment: .leading)
-
- VStack(alignment: .leading, spacing: 8) {
- Text("Title")
- .font(.caption)
- .foregroundColor(.secondary)
- TextField("Notification title", text: $titleText)
- .textFieldStyle(UnderlineTextFieldStyle())
- .focused($focusedField, equals: .title)
- .textInputAutocapitalization(.sentences)
- .autocorrectionDisabled()
- }
-
- VStack(alignment: .leading, spacing: 8) {
- Text("Body")
- .font(.caption)
- .foregroundColor(.secondary)
- TextField("Notification body", text: $bodyText)
- .textFieldStyle(UnderlineTextFieldStyle())
- .focused($focusedField, equals: .body)
- .textInputAutocapitalization(.sentences)
- .autocorrectionDisabled()
- }
-
- Spacer()
-
- // Action Buttons
- HStack(spacing: 24) {
- Spacer()
-
- Button("CANCEL") {
- onCancel()
- }
- .foregroundColor(.accentColor)
-
- Button("SEND") {
- onSend(titleText, bodyText)
- }
- .foregroundColor(isValid ? .accentColor : .gray)
- .disabled(!isValid)
- }
- .font(.system(size: 16, weight: .semibold))
- }
- .padding(24)
- .onAppear {
- focusedField = .title
- }
- }
- .presentationDetents([.medium])
- .presentationDragIndicator(.visible)
- }
-
- private var isValid: Bool {
- !titleText.trimmingCharacters(in: .whitespaces).isEmpty &&
- !bodyText.trimmingCharacters(in: .whitespaces).isEmpty
- }
-}
-
-#Preview {
- CustomNotificationSheet(
- onSend: { title, body in print("Send: \(title) - \(body)") },
- onCancel: { print("Cancel") }
- )
-}
diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Components/KeyValueRow.swift b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Components/KeyValueRow.swift
deleted file mode 100644
index efee8e54a..000000000
--- a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Components/KeyValueRow.swift
+++ /dev/null
@@ -1,363 +0,0 @@
-/**
- * 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 SwiftUI
-
-// MARK: - Action Button Style
-
-/// A full-width red button with white uppercase text
-struct ActionButtonStyle: ButtonStyle {
- var isDestructive: Bool = false
-
- func makeBody(configuration: Configuration) -> some View {
- configuration.label
- .font(.system(size: 16, weight: .semibold))
- .foregroundColor(.white)
- .textCase(.uppercase)
- .frame(maxWidth: .infinity)
- .padding(.vertical, 14)
- .background(Color.accentColor.opacity(configuration.isPressed ? 0.8 : 1.0))
- .cornerRadius(8)
- }
-}
-
-/// A full-width action button matching the screenshot style
-struct ActionButton: View {
- let title: String
- let action: () -> Void
-
- var body: some View {
- Button(action: action) {
- Text(title)
- }
- .buttonStyle(ActionButtonStyle())
- }
-}
-
-/// Outlined button style: red border, white background, red text (for destructive actions like LOGOUT)
-struct OutlineActionButtonStyle: ButtonStyle {
- func makeBody(configuration: Configuration) -> some View {
- configuration.label
- .font(.system(size: 16, weight: .semibold))
- .foregroundColor(.accentColor)
- .textCase(.uppercase)
- .frame(maxWidth: .infinity)
- .padding(.vertical, 14)
- .background(Color(.systemBackground).opacity(configuration.isPressed ? 0.8 : 1.0))
- .cornerRadius(8)
- .overlay(
- RoundedRectangle(cornerRadius: 8)
- .stroke(Color.accentColor, lineWidth: 1.5)
- )
- }
-}
-
-/// A full-width outlined action button (red border, white background, red text)
-struct OutlineActionButton: View {
- let title: String
- let action: () -> Void
-
- var body: some View {
- Button(action: action) {
- Text(title)
- }
- .buttonStyle(OutlineActionButtonStyle())
- }
-}
-
-/// A full-width action button with a leading icon (for Send In-App Message buttons)
-struct ActionButtonWithIcon: View {
- let title: String
- let iconName: String
- let action: () -> Void
-
- var body: some View {
- Button(action: action) {
- HStack(spacing: 12) {
- Image(systemName: iconName)
- .font(.system(size: 18))
- Text(title)
- .font(.system(size: 16, weight: .semibold))
- .textCase(.uppercase)
- Spacer()
- }
- .foregroundColor(.white)
- .frame(maxWidth: .infinity)
- .padding(.vertical, 14)
- .padding(.horizontal, 16)
- .background(Color.accentColor)
- .cornerRadius(8)
- }
- .buttonStyle(.plain)
- }
-}
-
-// MARK: - Card Container
-
-/// A white card container with rounded corners
-struct CardContainer: View {
- let content: Content
-
- init(@ViewBuilder content: () -> Content) {
- self.content = content()
- }
-
- var body: some View {
- VStack(spacing: 0) {
- content
- }
- .background(Color(.systemBackground))
- .cornerRadius(12)
- }
-}
-
-// MARK: - Section Header
-
-/// A small gray section header with optional info tooltip button
-struct SectionHeader: View {
- let title: String
- var tooltipKey: String?
-
- @State private var showingTooltip = false
-
- var body: some View {
- HStack {
- Text(title)
- .font(.system(size: 14, weight: .medium))
- .foregroundColor(.secondary)
-
- Spacer()
-
- if tooltipKey != nil {
- Button {
- showingTooltip = true
- } label: {
- Image(systemName: "info.circle.fill")
- .font(.system(size: 16))
- .foregroundColor(.accentColor)
- }
- .buttonStyle(.borderless)
- }
- }
- .padding(.horizontal, 4)
- .padding(.top, 16)
- .padding(.bottom, 8)
- .alert(isPresented: $showingTooltip) {
- if let key = tooltipKey,
- let tooltip = TooltipService.shared.getTooltip(key: key) {
- var message = tooltip.description
- if let options = tooltip.options {
- message += "\n"
- for option in options {
- message += "\n\(option.name): \(option.description)"
- }
- }
- return Alert(
- title: Text(tooltip.title),
- message: Text(message),
- dismissButton: .default(Text("OK"))
- )
- } else {
- return Alert(
- title: Text(title),
- message: Text("Tooltip content not available."),
- dismissButton: .default(Text("OK"))
- )
- }
- }
- }
-}
-
-// MARK: - Key-Value Row
-
-/// A row displaying a key-value pair with optional delete action
-struct KeyValueRow: View {
- let item: KeyValueItem
- let onDelete: (() -> Void)?
-
- init(item: KeyValueItem, onDelete: (() -> Void)? = nil) {
- self.item = item
- self.onDelete = onDelete
- }
-
- var body: some View {
- HStack {
- VStack(alignment: .leading, spacing: 2) {
- Text(item.key)
- .font(.subheadline)
- .foregroundColor(.secondary)
- Text(item.value)
- .font(.body)
- }
-
- Spacer()
-
- if let onDelete = onDelete {
- Button(action: onDelete) {
- Image(systemName: "xmark")
- .font(.system(size: 14, weight: .medium))
- .foregroundColor(.red)
- }
- .buttonStyle(.borderless)
- }
- }
- .padding(.horizontal, 16)
- .padding(.vertical, 12)
- .contentShape(Rectangle())
- }
-}
-
-// MARK: - Single Value Row
-
-/// A row displaying a single value with optional delete action
-struct SingleValueRow: View {
- let value: String
- let onDelete: (() -> Void)?
-
- init(value: String, onDelete: (() -> Void)? = nil) {
- self.value = value
- self.onDelete = onDelete
- }
-
- var body: some View {
- HStack {
- Text(value)
- .font(.body)
-
- Spacer()
-
- if let onDelete = onDelete {
- Button(action: onDelete) {
- Image(systemName: "xmark")
- .font(.system(size: 14, weight: .medium))
- .foregroundColor(.red)
- }
- .buttonStyle(.borderless)
- }
- }
- .padding(.horizontal, 16)
- .padding(.vertical, 12)
- .contentShape(Rectangle())
- }
-}
-
-// MARK: - Info Row
-
-/// A row displaying a label and value (like "Push-Id: xxx")
-struct InfoRow: View {
- let label: String
- let value: String
- let isMonospaced: Bool
-
- init(label: String, value: String, isMonospaced: Bool = false) {
- self.label = label
- self.value = value
- self.isMonospaced = isMonospaced
- }
-
- var body: some View {
- HStack(alignment: .top) {
- Text(label)
- .font(.system(size: 15, weight: .medium))
- .foregroundColor(.secondary)
- Spacer()
- Text(value)
- .font(isMonospaced ? .system(size: 15, design: .monospaced) : .system(size: 15))
- .foregroundColor(.primary)
- .lineLimit(1)
- .truncationMode(.middle)
- }
- .padding(.horizontal, 16)
- .padding(.vertical, 12)
- }
-}
-
-// MARK: - Toggle Row
-
-/// A toggle row with title and optional subtitle
-struct ToggleRow: View {
- let title: String
- let subtitle: String?
- @Binding var isOn: Bool
- let isEnabled: Bool
-
- init(title: String, subtitle: String? = nil, isOn: Binding, isEnabled: Bool = true) {
- self.title = title
- self.subtitle = subtitle
- self._isOn = isOn
- self.isEnabled = isEnabled
- }
-
- var body: some View {
- HStack {
- VStack(alignment: .leading, spacing: 2) {
- Text(title)
- .font(.system(size: 15, weight: .medium))
- if let subtitle = subtitle {
- Text(subtitle)
- .font(.caption)
- .foregroundColor(.secondary)
- }
- }
-
- Spacer()
-
- Toggle("", isOn: $isOn)
- .labelsHidden()
- .disabled(!isEnabled)
- }
- .padding(.horizontal, 16)
- .padding(.vertical, 12)
- .opacity(isEnabled ? 1.0 : 0.5)
- }
-}
-
-// MARK: - Empty List Row
-
-/// A placeholder row for empty lists
-struct EmptyListRow: View {
- let message: String
-
- var body: some View {
- Text(message)
- .font(.system(size: 16, weight: .medium))
- .foregroundColor(.primary)
- .frame(maxWidth: .infinity, alignment: .center)
- .padding(.vertical, 16)
- }
-}
-
-// MARK: - Divider Line
-
-/// A subtle divider for card sections
-struct CardDivider: View {
- var body: some View {
- Rectangle()
- .fill(Color(.separator))
- .frame(height: 0.5)
- }
-}
diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Components/LogView.swift b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Components/LogView.swift
deleted file mode 100644
index fb03c3db9..000000000
--- a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Components/LogView.swift
+++ /dev/null
@@ -1,130 +0,0 @@
-/**
- * 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 SwiftUI
-
-/// Collapsible log view showing SDK and app logs, matching Android's LogView
-struct LogView: View {
- @ObservedObject var logManager: LogManager
- @State private var isExpanded = false
-
- var body: some View {
- VStack(spacing: 0) {
- // Header bar
- Button {
- withAnimation(.easeInOut(duration: 0.2)) {
- isExpanded.toggle()
- }
- } label: {
- HStack {
- Text("LOGS")
- .font(.system(size: 13, weight: .semibold))
- .foregroundColor(.primary)
-
- Text("(\(logManager.entries.count))")
- .font(.system(size: 13, weight: .medium))
- .foregroundColor(.secondary)
-
- Spacer()
-
- Button {
- logManager.clear()
- } label: {
- Image(systemName: "trash")
- .font(.system(size: 14))
- .foregroundColor(.secondary)
- }
- .buttonStyle(.borderless)
-
- Image(systemName: isExpanded ? "chevron.up" : "chevron.down")
- .font(.system(size: 12, weight: .medium))
- .foregroundColor(.secondary)
- }
- .padding(.horizontal, 16)
- .padding(.vertical, 10)
- }
- .buttonStyle(.plain)
-
- // Log entries (expanded)
- if isExpanded {
- Divider()
-
- if logManager.entries.isEmpty {
- Text("No logs yet")
- .font(.system(size: 13))
- .foregroundColor(.secondary)
- .frame(maxWidth: .infinity)
- .padding(.vertical, 16)
- } else {
- ScrollViewReader { proxy in
- ScrollView {
- LazyVStack(alignment: .leading, spacing: 2) {
- ForEach(logManager.entries) { entry in
- HStack(alignment: .top, spacing: 6) {
- Text(entry.formattedTimestamp)
- .font(.system(size: 11, design: .monospaced))
- .foregroundColor(.secondary)
-
- Text(entry.level.rawValue)
- .font(.system(size: 11, weight: .bold, design: .monospaced))
- .foregroundColor(entry.level.color)
-
- Text(entry.message)
- .font(.system(size: 11, design: .monospaced))
- .foregroundColor(.primary)
- .lineLimit(2)
- }
- .padding(.horizontal, 12)
- .padding(.vertical, 2)
- .id(entry.id)
- }
- }
- .padding(.vertical, 4)
- }
- .frame(height: 100)
- .onChange(of: logManager.entries.count) { _ in
- if let lastEntry = logManager.entries.last {
- withAnimation {
- proxy.scrollTo(lastEntry.id, anchor: .bottom)
- }
- }
- }
- }
- }
- }
- }
- .background(Color(.systemBackground))
- .cornerRadius(0)
- }
-}
-
-#Preview {
- VStack {
- LogView(logManager: LogManager.shared)
- }
- .background(Color(.systemGroupedBackground))
-}
diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Components/NotificationGrid.swift b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Components/NotificationGrid.swift
deleted file mode 100644
index 8059af4f0..000000000
--- a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Components/NotificationGrid.swift
+++ /dev/null
@@ -1,89 +0,0 @@
-/**
- * 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 SwiftUI
-
-/// Three full-width push notification buttons matching the Android layout:
-/// SIMPLE NOTIFICATION, NOTIFICATION WITH IMAGE, CUSTOM NOTIFICATION
-struct SendPushButtons: View {
- let onSimple: () -> Void
- let onWithImage: () -> Void
- let onCustom: () -> Void
- let onClearAll: () -> Void
-
- var body: some View {
- VStack(spacing: 8) {
- ActionButton(title: "Simple", action: onSimple)
- ActionButton(title: "With Image", action: onWithImage)
- ActionButton(title: "Custom", action: onCustom)
- ActionButton(title: "Clear All", action: onClearAll)
- }
- }
-}
-
-/// Four full-width in-app message buttons with trailing icons matching the Android layout
-struct SendInAppButtons: View {
- let onSelect: (InAppMessageType) -> Void
-
- var body: some View {
- VStack(spacing: 8) {
- ForEach(InAppMessageType.allCases) { type in
- ActionButtonWithIcon(
- title: type.rawValue,
- iconName: type.iconName
- ) {
- onSelect(type)
- }
- }
- }
- }
-}
-
-#Preview {
- ScrollView {
- VStack(alignment: .leading, spacing: 20) {
- Text("Send Push Notification")
- .font(.system(size: 14, weight: .medium))
- .foregroundColor(.secondary)
- SendPushButtons(
- onSimple: { print("Simple") },
- onWithImage: { print("With Image") },
- onCustom: { print("Custom") },
- onClearAll: { print("Clear All") }
- )
-
- Text("Send In-App Message")
- .font(.system(size: 14, weight: .medium))
- .foregroundColor(.secondary)
- SendInAppButtons(onSelect: { type in
- print("Selected: \(type.rawValue)")
- })
- }
- .padding()
- }
- .background(Color(.systemGroupedBackground))
-}
diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Components/RemoveMultiSheet.swift b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Components/RemoveMultiSheet.swift
deleted file mode 100644
index 85e0e5ed3..000000000
--- a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Components/RemoveMultiSheet.swift
+++ /dev/null
@@ -1,118 +0,0 @@
-/**
- * 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 SwiftUI
-
-/// A checkbox dialog for selectively removing items, matching the Android "Remove Tags/Aliases/Triggers" dialog.
-struct RemoveMultiSheet: View {
- let type: RemoveMultiItemType
- let items: [KeyValueItem]
- let onRemove: ([String]) -> Void
- let onCancel: () -> Void
-
- @State private var selectedKeys: Set = []
-
- var body: some View {
- NavigationStack {
- VStack(spacing: 16) {
- // Title
- Text(type.rawValue)
- .font(.title2)
- .fontWeight(.semibold)
- .frame(maxWidth: .infinity, alignment: .leading)
-
- // Checkbox list
- ScrollView {
- VStack(alignment: .leading, spacing: 0) {
- ForEach(items) { item in
- Button {
- if selectedKeys.contains(item.key) {
- selectedKeys.remove(item.key)
- } else {
- selectedKeys.insert(item.key)
- }
- } label: {
- HStack(spacing: 12) {
- Image(systemName: selectedKeys.contains(item.key) ? "checkmark.square.fill" : "square")
- .font(.system(size: 22))
- .foregroundColor(selectedKeys.contains(item.key) ? .accentColor : .secondary)
-
- Text("\(item.key): \(item.value)")
- .font(.system(size: 16))
- .foregroundColor(.primary)
-
- Spacer()
- }
- .padding(.vertical, 10)
- }
- .buttonStyle(.plain)
-
- if item.id != items.last?.id {
- Divider()
- }
- }
- }
- }
-
- Spacer()
-
- // Action Buttons
- HStack(spacing: 24) {
- Spacer()
-
- Button("CANCEL") {
- onCancel()
- }
- .foregroundColor(.accentColor)
-
- Button("REMOVE") {
- onRemove(Array(selectedKeys))
- }
- .foregroundColor(selectedKeys.isEmpty ? .gray : .accentColor)
- .disabled(selectedKeys.isEmpty)
- }
- .font(.system(size: 16, weight: .semibold))
- }
- .padding(24)
- }
- .presentationDetents([.medium])
- .presentationDragIndicator(.visible)
- }
-}
-
-#Preview {
- RemoveMultiSheet(
- type: .tags,
- items: [
- KeyValueItem(key: "name", value: "John"),
- KeyValueItem(key: "age", value: "25"),
- KeyValueItem(key: "city", value: "NYC")
- ],
- onRemove: { keys in print("Remove: \(keys)") },
- onCancel: { print("Cancel") }
- )
-}
diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Components/ToastView.swift b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Components/ToastView.swift
deleted file mode 100644
index eede3ce4d..000000000
--- a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Components/ToastView.swift
+++ /dev/null
@@ -1,80 +0,0 @@
-/**
- * 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 SwiftUI
-
-/// A toast notification view that appears at the bottom of the screen
-struct ToastView: View {
- let message: String
-
- var body: some View {
- Text(message)
- .font(.subheadline)
- .foregroundColor(.white)
- .padding(.horizontal, 16)
- .padding(.vertical, 12)
- .background(Color.black.opacity(0.8))
- .cornerRadius(8)
- .shadow(radius: 4)
- }
-}
-
-/// A view modifier that overlays a toast message
-struct ToastModifier: ViewModifier {
- @Binding var message: String?
-
- func body(content: Content) -> some View {
- ZStack {
- content
-
- if let message = message {
- VStack {
- Spacer()
- ToastView(message: message)
- .padding(.bottom, 32)
- .transition(.move(edge: .bottom).combined(with: .opacity))
- }
- .animation(.easeInOut(duration: 0.3), value: message)
- }
- }
- }
-}
-
-extension View {
- /// Adds a toast overlay to the view
- func toast(message: Binding) -> some View {
- modifier(ToastModifier(message: message))
- }
-}
-
-#Preview {
- VStack {
- Text("Content")
- }
- .frame(maxWidth: .infinity, maxHeight: .infinity)
- .toast(message: .constant("This is a toast message"))
-}
diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Components/TrackEventSheet.swift b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Components/TrackEventSheet.swift
deleted file mode 100644
index f3aa65c6d..000000000
--- a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Components/TrackEventSheet.swift
+++ /dev/null
@@ -1,150 +0,0 @@
-/**
- * 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 SwiftUI
-
-/// A dialog for tracking an event with an optional JSON properties string.
-struct TrackEventSheet: View {
- let onTrack: (String, [String: Any]?) -> Void
- let onCancel: () -> Void
-
- @State private var eventName: String = ""
- @State private var propertiesText: String = ""
- @State private var nameError: String?
- @State private var propertiesError: String?
- @FocusState private var focusedField: Field?
-
- private enum Field {
- case name, properties
- }
-
- var body: some View {
- NavigationStack {
- VStack(spacing: 24) {
- // Title
- Text("Track Event")
- .font(.title2)
- .fontWeight(.semibold)
- .frame(maxWidth: .infinity, alignment: .leading)
-
- VStack(alignment: .leading, spacing: 4) {
- Text("Event Name")
- .font(.caption)
- .foregroundColor(.secondary)
- TextField("", text: $eventName)
- .textFieldStyle(UnderlineTextFieldStyle())
- .focused($focusedField, equals: .name)
- .textInputAutocapitalization(.never)
- .autocorrectionDisabled()
- .onChange(of: eventName) { _ in
- nameError = nil
- }
- if let error = nameError {
- Text(error)
- .font(.caption2)
- .foregroundColor(.red)
- }
- }
-
- VStack(alignment: .leading, spacing: 4) {
- Text("Properties (optional, JSON)")
- .font(.caption)
- .foregroundColor(.secondary)
- TextField("{\"ABC\":123}", text: $propertiesText)
- .textFieldStyle(UnderlineTextFieldStyle())
- .focused($focusedField, equals: .properties)
- .textInputAutocapitalization(.never)
- .autocorrectionDisabled()
- .onChange(of: propertiesText) { _ in
- propertiesError = nil
- }
- if let error = propertiesError {
- Text(error)
- .font(.caption2)
- .foregroundColor(.red)
- }
- }
-
- Spacer()
-
- // Action Buttons
- HStack(spacing: 24) {
- Spacer()
-
- Button("CANCEL") {
- onCancel()
- }
- .foregroundColor(.accentColor)
-
- Button("TRACK") {
- submitForm()
- }
- .foregroundColor(.accentColor)
- }
- .font(.system(size: 16, weight: .semibold))
- }
- .padding(24)
- .onAppear {
- focusedField = .name
- }
- }
- .presentationDetents([.medium])
- .presentationDragIndicator(.visible)
- }
-
- private func submitForm() {
- let trimmedName = eventName.trimmingCharacters(in: .whitespaces)
-
- if trimmedName.isEmpty {
- nameError = "Required"
- return
- }
-
- var properties: [String: Any]?
-
- let trimmedProps = propertiesText.trimmingCharacters(in: .whitespaces)
- .replacingOccurrences(of: "\u{201C}", with: "\"")
- .replacingOccurrences(of: "\u{201D}", with: "\"")
- if !trimmedProps.isEmpty {
- guard let data = trimmedProps.data(using: .utf8),
- let parsed = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
- propertiesError = "Invalid JSON"
- return
- }
- properties = parsed
- }
-
- onTrack(trimmedName, properties)
- }
-}
-
-#Preview {
- TrackEventSheet(
- onTrack: { name, props in print("Track: \(name), \(String(describing: props))") },
- onCancel: { print("Cancel") }
- )
-}
diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/ContentView.swift b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/ContentView.swift
deleted file mode 100644
index d06a3a6a6..000000000
--- a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/ContentView.swift
+++ /dev/null
@@ -1,199 +0,0 @@
-/**
- * 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 SwiftUI
-
-/// Main content view composing all sections in the order matching the Android demo app
-struct ContentView: View {
- @EnvironmentObject var viewModel: OneSignalViewModel
-
- var body: some View {
- NavigationStack {
- ZStack {
- ScrollView {
- VStack(spacing: 0) {
- // Collapsible log view at top
- LogView(logManager: LogManager.shared)
-
- // 1. App (includes consent, guidance banner)
- AppInfoSection()
-
- // 2. User (status, external ID, login/logout)
- UserSection()
-
- // 3. Push
- PushSection()
-
- // 4. Send Push Notification
- SendPushSection()
-
- // 5. In-App Messaging
- InAppMessagingSection()
-
- // 6. Send In-App Message
- SendInAppSection()
-
- // 7. Aliases
- AliasesSection()
-
- // 8. Emails
- EmailsSection()
-
- // 9. SMS
- SMSSection()
-
- // 10. Tags
- TagsSection()
-
- // 11. Outcome Events
- OutcomeEventsSection()
-
- // 12. Triggers
- TriggersSection()
-
- // 13. Track Event
- TrackEventSection()
-
- // 14. Location
- LocationSection()
-
- // 15. Live Activities
- LiveActivitySection()
-
- // 16. Next Activity
- NextScreenSection()
- }
- .padding(.horizontal, 16)
- .padding(.bottom, 32)
- }
- .background(Color(.systemGroupedBackground))
-
- // Loading overlay
- if viewModel.isLoading {
- Color.black.opacity(0.3)
- .ignoresSafeArea()
- ProgressView()
- .scaleEffect(1.5)
- .tint(.white)
- }
- }
- .safeAreaInset(edge: .top) {
- // Compact header bar
- VStack(spacing: 0) {
- Color.accentColor
- .frame(height: UIApplication.shared.connectedScenes
- .compactMap { $0 as? UIWindowScene }
- .first?.statusBarManager?.statusBarFrame.height ?? 0)
- HStack(spacing: 10) {
- Image("OneSignalLogo")
- .renderingMode(.template)
- .resizable()
- .aspectRatio(contentMode: .fit)
- .frame(height: 24)
- Text("Sample App")
- .font(.subheadline)
- .opacity(0.9)
- Spacer()
- }
- .foregroundColor(.white)
- .padding(.horizontal, 16)
- .padding(.vertical, 12)
- .background(Color.accentColor)
- }
- .ignoresSafeArea(edges: .top)
- }
- .navigationBarHidden(true)
- // Single add sheet
- .sheet(isPresented: $viewModel.showingAddSheet) {
- AddItemSheet(
- itemType: viewModel.addItemType,
- onAdd: { key, value in
- viewModel.handleAddItem(key: key, value: value)
- },
- onCancel: {
- viewModel.showingAddSheet = false
- }
- )
- }
- // Multi-add sheet
- .sheet(isPresented: $viewModel.showingMultiAddSheet) {
- AddMultiItemSheet(
- type: viewModel.multiAddType,
- onAdd: { pairs in
- viewModel.handleMultiAdd(pairs: pairs)
- },
- onCancel: {
- viewModel.showingMultiAddSheet = false
- }
- )
- }
- // Remove-multi sheet
- .sheet(isPresented: $viewModel.showingRemoveMultiSheet) {
- RemoveMultiSheet(
- type: viewModel.removeMultiType,
- items: viewModel.removeMultiItems,
- onRemove: { keys in
- viewModel.handleRemoveMulti(keys: keys)
- },
- onCancel: {
- viewModel.showingRemoveMultiSheet = false
- }
- )
- }
- // Custom notification sheet
- .sheet(isPresented: $viewModel.showingCustomNotificationSheet) {
- CustomNotificationSheet(
- onSend: { title, body in
- viewModel.sendCustomNotification(title: title, body: body)
- viewModel.showingCustomNotificationSheet = false
- },
- onCancel: {
- viewModel.showingCustomNotificationSheet = false
- }
- )
- }
- // Track event sheet
- .sheet(isPresented: $viewModel.showingTrackEventSheet) {
- TrackEventSheet(
- onTrack: { name, properties in
- viewModel.trackEvent(name: name, properties: properties)
- viewModel.showingTrackEventSheet = false
- },
- onCancel: {
- viewModel.showingTrackEventSheet = false
- }
- )
- }
- }
- .toast(message: $viewModel.toastMessage)
- }
-}
-
-#Preview {
- ContentView()
- .environmentObject(OneSignalViewModel())
-}
diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Sections/AppInfoSection.swift b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Sections/AppInfoSection.swift
deleted file mode 100644
index d0b80f902..000000000
--- a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Sections/AppInfoSection.swift
+++ /dev/null
@@ -1,84 +0,0 @@
-/**
- * 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 SwiftUI
-
-/// Section displaying app information, consent, logged-in state, and login/logout.
-/// Merges the previous separate UserSection content.
-struct AppInfoSection: View {
- @EnvironmentObject var viewModel: OneSignalViewModel
-
- var body: some View {
- VStack(spacing: 0) {
- SectionHeader(title: "App")
-
- // App ID card
- CardContainer {
- InfoRow(label: "App ID", value: viewModel.appId, isMonospaced: true)
- }
-
- // Guidance banner
- GuidanceBanner()
- .padding(.top, 8)
-
- // Consent card with up to two toggles
- CardContainer {
- ToggleRow(
- title: "Consent Required",
- subtitle: "Require consent before SDK processes data",
- isOn: Binding(
- get: { viewModel.consentRequired },
- set: { _ in viewModel.toggleConsentRequired() }
- )
- )
-
- // Privacy Consent toggle (only visible when Consent Required is ON)
- if viewModel.consentRequired {
- CardDivider()
- ToggleRow(
- title: "Privacy Consent",
- subtitle: "Consent given for data collection",
- isOn: Binding(
- get: { viewModel.consentGiven },
- set: { _ in viewModel.toggleConsent() }
- )
- )
- }
- }
- .padding(.top, 8)
- }
- }
-}
-
-#Preview {
- ScrollView {
- AppInfoSection()
- .padding()
- }
- .background(Color(.systemGroupedBackground))
- .environmentObject(OneSignalViewModel())
-}
diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Sections/LiveActivitySection.swift b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Sections/LiveActivitySection.swift
deleted file mode 100644
index 294c904e6..000000000
--- a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Sections/LiveActivitySection.swift
+++ /dev/null
@@ -1,46 +0,0 @@
-import SwiftUI
-import OneSignalFramework
-
-struct LiveActivitySection: View {
- @EnvironmentObject var viewModel: OneSignalViewModel
- @State private var activityId: String = ""
-
- var body: some View {
- VStack(spacing: 0) {
- SectionHeader(title: "Live Activities", tooltipKey: "liveActivities")
-
- CardContainer {
- HStack {
- Text("Activity ID")
- .font(.system(size: 15, weight: .medium))
- .foregroundColor(.secondary)
- Spacer()
- TextField("Enter activity ID", text: $activityId)
- .font(.system(size: 15))
- .multilineTextAlignment(.trailing)
- .autocorrectionDisabled()
- .textInputAutocapitalization(.never)
- }
- .padding(.horizontal, 16)
- .padding(.vertical, 12)
- }
-
- ActionButton(title: "Enter Live Activity") {
- viewModel.enterLiveActivity(activityId: activityId)
- }
- .padding(.top, 12)
-
- OutlineActionButton(title: "Exit Live Activity") {
- viewModel.exitLiveActivity(activityId: activityId)
- }
- .padding(.top, 8)
- }
- }
-}
-
-#Preview {
- LiveActivitySection()
- .padding()
- .background(Color(.systemGroupedBackground))
- .environmentObject(OneSignalViewModel())
-}
diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Sections/MessagingSection.swift b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Sections/MessagingSection.swift
deleted file mode 100644
index 6e8c0b203..000000000
--- a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Sections/MessagingSection.swift
+++ /dev/null
@@ -1,272 +0,0 @@
-/**
- * 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 SwiftUI
-
-/// Section for outcome events
-struct OutcomeEventsSection: View {
- @EnvironmentObject var viewModel: OneSignalViewModel
- @State private var showingOutcomeSheet = false
-
- var body: some View {
- VStack(spacing: 0) {
- SectionHeader(title: "Outcome Events", tooltipKey: "outcomes")
-
- ActionButton(title: "Send Outcome") {
- showingOutcomeSheet = true
- }
- }
- .sheet(isPresented: $showingOutcomeSheet) {
- OutcomeSheet(
- onSendNormal: { name in
- viewModel.sendOutcome(name)
- showingOutcomeSheet = false
- },
- onSendUnique: { name in
- viewModel.sendUniqueOutcome(name)
- showingOutcomeSheet = false
- },
- onSendWithValue: { name, value in
- viewModel.sendOutcome(name, value: value)
- showingOutcomeSheet = false
- },
- onCancel: {
- showingOutcomeSheet = false
- }
- )
- }
- }
-}
-
-/// Section for in-app messaging controls
-struct InAppMessagingSection: View {
- @EnvironmentObject var viewModel: OneSignalViewModel
-
- var body: some View {
- VStack(spacing: 0) {
- SectionHeader(title: "In-App Messaging", tooltipKey: "inAppMessaging")
-
- CardContainer {
- ToggleRow(
- title: "Pause In-App Messages",
- subtitle: "Toggle in-app message display",
- isOn: Binding(
- get: { viewModel.isInAppMessagesPaused },
- set: { _ in viewModel.toggleInAppMessagesPaused() }
- )
- )
- }
- }
- }
-}
-
-/// Section for trigger management with Add Trigger, Add Triggers (multi), Remove Triggers, and Clear Triggers
-struct TriggersSection: View {
- @EnvironmentObject var viewModel: OneSignalViewModel
-
- var body: some View {
- VStack(spacing: 0) {
- SectionHeader(title: "Triggers", tooltipKey: "triggers")
-
- CardContainer {
- if viewModel.triggers.isEmpty {
- EmptyListRow(message: "No triggers added")
- } else {
- ForEach(Array(viewModel.triggers.enumerated()), id: \.element.id) { index, trigger in
- if index > 0 {
- CardDivider()
- }
- KeyValueRow(item: trigger) {
- viewModel.removeTrigger(trigger)
- }
- }
- }
- }
-
- ActionButton(title: "Add") {
- viewModel.showAddSheet(for: .trigger)
- }
- .padding(.top, 12)
-
- ActionButton(title: "Add Multiple") {
- viewModel.showMultiAddSheet(for: .triggers)
- }
- .padding(.top, 8)
-
- // Remove Selected and Clear All - only visible when triggers exist
- if !viewModel.triggers.isEmpty {
- OutlineActionButton(title: "Remove Selected") {
- viewModel.showRemoveMultiSheet(for: .triggers)
- }
- .padding(.top, 8)
-
- OutlineActionButton(title: "Clear All") {
- viewModel.clearTriggers()
- }
- .padding(.top, 8)
- }
- }
- }
-}
-
-/// Outcome type options matching Android's radio button selection
-private enum OutcomeType: Int, CaseIterable {
- case normal = 0
- case unique = 1
- case withValue = 2
-
- var label: String {
- switch self {
- case .normal: return "Normal Outcome"
- case .unique: return "Unique Outcome"
- case .withValue: return "Outcome with Value"
- }
- }
-}
-
-/// Sheet for sending outcomes with radio button selection (Normal/Unique/With Value)
-struct OutcomeSheet: View {
- let onSendNormal: (String) -> Void
- let onSendUnique: (String) -> Void
- let onSendWithValue: (String, Double) -> Void
- let onCancel: () -> Void
-
- @State private var selectedType: OutcomeType = .normal
- @State private var outcomeName = ""
- @State private var outcomeValue = ""
- @FocusState private var focusedField: Field?
-
- private enum Field {
- case name, value
- }
-
- private var isSendDisabled: Bool {
- let nameEmpty = outcomeName.trimmingCharacters(in: .whitespaces).isEmpty
- if selectedType == .withValue {
- return nameEmpty || Double(outcomeValue) == nil
- }
- return nameEmpty
- }
-
- var body: some View {
- NavigationStack {
- VStack(spacing: 20) {
- // Radio button selection
- VStack(alignment: .leading, spacing: 4) {
- ForEach(OutcomeType.allCases, id: \.rawValue) { type in
- Button {
- selectedType = type
- } label: {
- HStack(spacing: 8) {
- Image(systemName: selectedType == type ? "largecircle.fill.circle" : "circle")
- .font(.system(size: 20))
- .foregroundColor(selectedType == type ? .accentColor : .secondary)
- Text(type.label)
- .font(.system(size: 15))
- .foregroundColor(.primary)
- }
- .padding(.vertical, 6)
- }
- .buttonStyle(.plain)
- }
- }
-
- // Outcome name field (always shown)
- VStack(alignment: .leading, spacing: 8) {
- Text("Outcome Name")
- .font(.caption)
- .foregroundColor(.secondary)
- TextField("Outcome Name", text: $outcomeName)
- .textFieldStyle(.roundedBorder)
- .focused($focusedField, equals: .name)
- .textInputAutocapitalization(.never)
- .autocorrectionDisabled()
- }
-
- // Value field (only when "Outcome with Value" selected)
- if selectedType == .withValue {
- VStack(alignment: .leading, spacing: 8) {
- Text("Value")
- .font(.caption)
- .foregroundColor(.secondary)
- TextField("Value", text: $outcomeValue)
- .textFieldStyle(.roundedBorder)
- .focused($focusedField, equals: .value)
- .keyboardType(.decimalPad)
- }
- }
-
- Spacer()
-
- HStack(spacing: 16) {
- Button("Cancel") {
- onCancel()
- }
- .foregroundColor(.accentColor)
-
- Spacer()
-
- Button("Send") {
- switch selectedType {
- case .normal:
- onSendNormal(outcomeName)
- case .unique:
- onSendUnique(outcomeName)
- case .withValue:
- onSendWithValue(outcomeName, Double(outcomeValue) ?? 0)
- }
- }
- .foregroundColor(.accentColor)
- .disabled(isSendDisabled)
- }
- .textCase(.uppercase)
- .font(.system(size: 16, weight: .semibold))
- }
- .padding(24)
- .navigationTitle("Send Outcome")
- .navigationBarTitleDisplayMode(.inline)
- .onAppear {
- focusedField = .name
- }
- }
- .presentationDetents([.medium])
- .presentationDragIndicator(.visible)
- }
-}
-
-#Preview {
- ScrollView {
- VStack {
- OutcomeEventsSection()
- InAppMessagingSection()
- TriggersSection()
- }
- .padding()
- }
- .background(Color(.systemGroupedBackground))
- .environmentObject(OneSignalViewModel())
-}
diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Sections/NextScreenSection.swift b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Sections/NextScreenSection.swift
deleted file mode 100644
index 2361b4996..000000000
--- a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Sections/NextScreenSection.swift
+++ /dev/null
@@ -1,81 +0,0 @@
-/**
- * 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 SwiftUI
-
-/// Section with a button to navigate to a secondary placeholder view
-struct NextScreenSection: View {
- var body: some View {
- VStack(spacing: 0) {
- NavigationLink(destination: SecondaryView()) {
- Text("Next Activity")
- .font(.system(size: 16, weight: .semibold))
- .foregroundColor(.white)
- .textCase(.uppercase)
- .frame(maxWidth: .infinity)
- .padding(.vertical, 14)
- .background(Color.accentColor)
- .cornerRadius(8)
- }
- .buttonStyle(.plain)
- .padding(.top, 16)
- }
- }
-}
-
-/// A placeholder secondary view
-struct SecondaryView: View {
- var body: some View {
- VStack(spacing: 16) {
- Image(systemName: "bell.circle.fill")
- .font(.system(size: 60))
- .foregroundColor(.accentColor)
-
- Text("Secondary Activity")
- .font(.title2)
- .fontWeight(.semibold)
-
- Text("This is a placeholder secondary view for testing navigation and in-app message display on a different screen.")
- .font(.body)
- .foregroundColor(.secondary)
- .multilineTextAlignment(.center)
- .padding(.horizontal, 32)
- }
- .frame(maxWidth: .infinity, maxHeight: .infinity)
- .background(Color(.systemGroupedBackground))
- .navigationTitle("Secondary Activity")
- .navigationBarTitleDisplayMode(.inline)
- }
-}
-
-#Preview {
- NavigationStack {
- NextScreenSection()
- .padding()
- }
- .background(Color(.systemGroupedBackground))
-}
diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Sections/SubscriptionSection.swift b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Sections/SubscriptionSection.swift
deleted file mode 100644
index b0aec36f4..000000000
--- a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Sections/SubscriptionSection.swift
+++ /dev/null
@@ -1,180 +0,0 @@
-/**
- * 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 SwiftUI
-
-// MARK: - Push Section
-
-/// Section for push subscription management
-struct PushSection: View {
- @EnvironmentObject var viewModel: OneSignalViewModel
-
- var body: some View {
- VStack(spacing: 0) {
- SectionHeader(title: "Push", tooltipKey: "push")
-
- CardContainer {
- InfoRow(
- label: "Push ID",
- value: viewModel.pushSubscriptionId ?? "Not available",
- isMonospaced: true
- )
- CardDivider()
- ToggleRow(
- title: "Enabled",
- isOn: Binding(
- get: { viewModel.isPushEnabled },
- set: { _ in viewModel.togglePushEnabled() }
- ),
- isEnabled: viewModel.notificationPermissionGranted
- )
- }
-
- // Prompt Push button - only visible when permission not granted
- if !viewModel.notificationPermissionGranted {
- ActionButton(title: "Prompt Push") {
- viewModel.requestPushPermission()
- }
- .padding(.top, 12)
- }
- }
- }
-}
-
-// MARK: - Emails Section
-
-/// Section for email subscription management with collapsible >5 items
-struct EmailsSection: View {
- @EnvironmentObject var viewModel: OneSignalViewModel
- @State private var isExpanded = false
-
- var body: some View {
- VStack(spacing: 0) {
- SectionHeader(title: "Emails", tooltipKey: "emails")
-
- CardContainer {
- if viewModel.emails.isEmpty {
- EmptyListRow(message: "No emails added")
- } else {
- let displayEmails = isExpanded ? viewModel.emails : Array(viewModel.emails.prefix(5))
-
- ForEach(Array(displayEmails.enumerated()), id: \.element) { index, email in
- if index > 0 {
- CardDivider()
- }
- SingleValueRow(value: email) {
- viewModel.removeEmail(email)
- }
- }
-
- // "X more available" when collapsed and more than 5
- if !isExpanded && viewModel.emails.count > 5 {
- CardDivider()
- Button {
- isExpanded = true
- } label: {
- Text("\(viewModel.emails.count - 5) more available")
- .font(.system(size: 14, weight: .medium))
- .foregroundColor(.accentColor)
- .frame(maxWidth: .infinity)
- .padding(.vertical, 10)
- }
- .buttonStyle(.plain)
- }
- }
- }
-
- ActionButton(title: "Add Email") {
- viewModel.showAddSheet(for: .email)
- }
- .padding(.top, 12)
- }
- }
-}
-
-// MARK: - SMS Section
-
-/// Section for SMS subscription management with collapsible >5 items
-struct SMSSection: View {
- @EnvironmentObject var viewModel: OneSignalViewModel
- @State private var isExpanded = false
-
- var body: some View {
- VStack(spacing: 0) {
- SectionHeader(title: "SMS", tooltipKey: "sms")
-
- CardContainer {
- if viewModel.smsNumbers.isEmpty {
- EmptyListRow(message: "No SMS added")
- } else {
- let displaySms = isExpanded ? viewModel.smsNumbers : Array(viewModel.smsNumbers.prefix(5))
-
- ForEach(Array(displaySms.enumerated()), id: \.element) { index, sms in
- if index > 0 {
- CardDivider()
- }
- SingleValueRow(value: sms) {
- viewModel.removeSms(sms)
- }
- }
-
- if !isExpanded && viewModel.smsNumbers.count > 5 {
- CardDivider()
- Button {
- isExpanded = true
- } label: {
- Text("\(viewModel.smsNumbers.count - 5) more available")
- .font(.system(size: 14, weight: .medium))
- .foregroundColor(.accentColor)
- .frame(maxWidth: .infinity)
- .padding(.vertical, 10)
- }
- .buttonStyle(.plain)
- }
- }
- }
-
- ActionButton(title: "Add SMS") {
- viewModel.showAddSheet(for: .sms)
- }
- .padding(.top, 12)
- }
- }
-}
-
-#Preview {
- ScrollView {
- VStack {
- PushSection()
- EmailsSection()
- SMSSection()
- }
- .padding()
- }
- .background(Color(.systemGroupedBackground))
- .environmentObject(OneSignalViewModel())
-}
diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Sections/TagsSection.swift b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Sections/TagsSection.swift
deleted file mode 100644
index 5bea36558..000000000
--- a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Sections/TagsSection.swift
+++ /dev/null
@@ -1,79 +0,0 @@
-/**
- * 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 SwiftUI
-
-/// Section for managing user tags with Add Tag, Add Tags (multi), and Remove Tags
-struct TagsSection: View {
- @EnvironmentObject var viewModel: OneSignalViewModel
-
- var body: some View {
- VStack(spacing: 0) {
- SectionHeader(title: "Tags", tooltipKey: "tags")
-
- CardContainer {
- if viewModel.tags.isEmpty {
- EmptyListRow(message: "No tags added")
- } else {
- ForEach(Array(viewModel.tags.enumerated()), id: \.element.id) { index, tag in
- if index > 0 {
- CardDivider()
- }
- KeyValueRow(item: tag) {
- viewModel.removeTag(tag)
- }
- }
- }
- }
-
- ActionButton(title: "Add") {
- viewModel.showAddSheet(for: .tag)
- }
- .padding(.top, 12)
-
- ActionButton(title: "Add Multiple") {
- viewModel.showMultiAddSheet(for: .tags)
- }
- .padding(.top, 8)
-
- // Remove Selected - only visible when tags exist
- if !viewModel.tags.isEmpty {
- OutlineActionButton(title: "Remove Selected") {
- viewModel.showRemoveMultiSheet(for: .tags)
- }
- .padding(.top, 8)
- }
- }
- }
-}
-
-#Preview {
- TagsSection()
- .padding()
- .background(Color(.systemGroupedBackground))
- .environmentObject(OneSignalViewModel())
-}
diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Sections/UserSection.swift b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Sections/UserSection.swift
deleted file mode 100644
index 0cbe80a64..000000000
--- a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Sections/UserSection.swift
+++ /dev/null
@@ -1,132 +0,0 @@
-/**
- * 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 SwiftUI
-
-/// Section displaying user login status, external ID, and login/logout buttons.
-/// Matches the Android demo's USER section layout.
-struct UserSection: View {
- @EnvironmentObject var viewModel: OneSignalViewModel
-
- var body: some View {
- VStack(spacing: 0) {
- SectionHeader(title: "User")
-
- // Status / External ID card
- CardContainer {
- // Status row
- HStack {
- Text("Status")
- .font(.system(size: 15, weight: .medium))
- .foregroundColor(.secondary)
- Spacer()
- Text(viewModel.isLoggedIn ? "Logged In" : "Anonymous")
- .font(.system(size: 15, weight: .medium))
- .foregroundColor(viewModel.isLoggedIn ? Color(red: 0.20, green: 0.66, blue: 0.33) : .secondary)
- }
- .padding(.horizontal, 16)
- .padding(.vertical, 12)
-
- CardDivider()
-
- // External ID row
- HStack {
- Text("External ID")
- .font(.system(size: 15, weight: .medium))
- .foregroundColor(.secondary)
- Spacer()
- Text(viewModel.externalUserId ?? "\u{2014}")
- .font(.system(size: 15, weight: .medium))
- .lineLimit(1)
- .truncationMode(.middle)
- }
- .padding(.horizontal, 16)
- .padding(.vertical, 12)
- }
-
- // Login / Switch User button (filled)
- ActionButton(title: viewModel.loginButtonTitle) {
- viewModel.showAddSheet(for: .externalUserId)
- }
- .padding(.top, 12)
-
- // Logout button (outlined, only when logged in)
- if viewModel.isLoggedIn {
- OutlineActionButton(title: "Logout User") {
- viewModel.logout()
- }
- .padding(.top, 8)
- }
- }
- }
-}
-
-/// Section for alias management with Add and Add Multiple (read-only list, no delete icons)
-struct AliasesSection: View {
- @EnvironmentObject var viewModel: OneSignalViewModel
-
- var body: some View {
- VStack(spacing: 0) {
- SectionHeader(title: "Aliases", tooltipKey: "aliases")
-
- CardContainer {
- if viewModel.aliases.isEmpty {
- EmptyListRow(message: "No aliases added")
- } else {
- ForEach(Array(viewModel.aliases.enumerated()), id: \.element.id) { index, alias in
- if index > 0 {
- CardDivider()
- }
- KeyValueRow(item: alias)
- }
- }
- }
-
- ActionButton(title: "Add") {
- viewModel.showAddSheet(for: .alias)
- }
- .padding(.top, 12)
-
- ActionButton(title: "Add Multiple") {
- viewModel.showMultiAddSheet(for: .aliases)
- }
- .padding(.top, 8)
- }
- }
-}
-
-#Preview {
- ScrollView {
- VStack {
- UserSection()
- AliasesSection()
- }
- .padding()
- }
- .background(Color(.systemGroupedBackground))
- .environmentObject(OneSignalViewModel())
-}
diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalWidgetExtension.entitlements b/iOS_SDK/OneSignalSwiftUIExample/OneSignalWidgetExtension.entitlements
deleted file mode 100644
index ee95ab7e5..000000000
--- a/iOS_SDK/OneSignalSwiftUIExample/OneSignalWidgetExtension.entitlements
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
- com.apple.security.app-sandbox
-
- com.apple.security.network.client
-
-
-
diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalWidgetExtension/Info.plist b/iOS_SDK/OneSignalSwiftUIExample/OneSignalWidgetExtension/Info.plist
deleted file mode 100644
index 0f118fb75..000000000
--- a/iOS_SDK/OneSignalSwiftUIExample/OneSignalWidgetExtension/Info.plist
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
- NSExtension
-
- NSExtensionPointIdentifier
- com.apple.widgetkit-extension
-
-
-
diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalWidgetExtension/OneSignalWidgetExtension.swift b/iOS_SDK/OneSignalSwiftUIExample/OneSignalWidgetExtension/OneSignalWidgetExtension.swift
deleted file mode 100644
index 60d247cf3..000000000
--- a/iOS_SDK/OneSignalSwiftUIExample/OneSignalWidgetExtension/OneSignalWidgetExtension.swift
+++ /dev/null
@@ -1,43 +0,0 @@
-import WidgetKit
-import SwiftUI
-
-struct SimpleProvider: TimelineProvider {
- func placeholder(in context: Context) -> SimpleEntry {
- SimpleEntry(date: Date())
- }
-
- func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> Void) {
- completion(SimpleEntry(date: Date()))
- }
-
- func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) {
- var entries: [SimpleEntry] = []
- let currentDate = Date()
- for hourOffset in 0..<5 {
- let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
- entries.append(SimpleEntry(date: entryDate))
- }
- completion(Timeline(entries: entries, policy: .atEnd))
- }
-}
-
-struct SimpleEntry: TimelineEntry {
- let date: Date
-}
-
-struct OneSignalWidgetExtensionWidget: Widget {
- let kind: String = "OneSignalWidgetExtension"
-
- var body: some WidgetConfiguration {
- StaticConfiguration(kind: kind, provider: SimpleProvider()) { entry in
- if #available(iOS 17.0, *) {
- Text(entry.date, style: .time)
- .containerBackground(.fill.tertiary, for: .widget)
- } else {
- Text(entry.date, style: .time)
- }
- }
- .configurationDisplayName("OneSignal Widget")
- .description("An example widget.")
- }
-}
diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalWidgetExtension/OneSignalWidgetExtensionBundle.swift b/iOS_SDK/OneSignalSwiftUIExample/OneSignalWidgetExtension/OneSignalWidgetExtensionBundle.swift
deleted file mode 100644
index 1dd09de51..000000000
--- a/iOS_SDK/OneSignalSwiftUIExample/OneSignalWidgetExtension/OneSignalWidgetExtensionBundle.swift
+++ /dev/null
@@ -1,13 +0,0 @@
-import WidgetKit
-import SwiftUI
-
-@main
-struct OneSignalWidgetExtensionBundle: WidgetBundle {
- var body: some Widget {
- OneSignalWidgetExtensionWidget()
- ExampleAppFirstWidget()
- ExampleAppSecondWidget()
- ExampleAppThirdWidget()
- DefaultOneSignalLiveActivityWidget()
- }
-}
diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalWidgetExtension/OneSignalWidgetExtensionLiveActivity.swift b/iOS_SDK/OneSignalSwiftUIExample/OneSignalWidgetExtension/OneSignalWidgetExtensionLiveActivity.swift
deleted file mode 100644
index 4d5fbaaa2..000000000
--- a/iOS_SDK/OneSignalSwiftUIExample/OneSignalWidgetExtension/OneSignalWidgetExtensionLiveActivity.swift
+++ /dev/null
@@ -1,226 +0,0 @@
-import ActivityKit
-import WidgetKit
-import SwiftUI
-import OneSignalLiveActivities
-
-struct ExampleAppFirstWidget: Widget {
- var body: some WidgetConfiguration {
- ActivityConfiguration(for: ExampleAppFirstWidgetAttributes.self) { context in
- VStack {
- Spacer()
- Text("FIRST: " + context.attributes.title).font(.headline)
- Spacer()
- HStack {
- Spacer()
- Label {
- Text(String(context.state.message))
- } icon: {
- Image(systemName: "bell.circle.fill")
- .resizable()
- .scaledToFit()
- .frame(width: 40.0, height: 40.0)
- }
- Spacer()
- }
- Spacer()
- }
- .foregroundColor(.black)
- .onesignalWidgetURL(URL(string: "https://example.com/page?param1=value1¶m2=value2#section"), context: context)
- .activitySystemActionForegroundColor(.black)
- .activityBackgroundTint(.white)
- } dynamicIsland: { context in
- DynamicIsland {
- DynamicIslandExpandedRegion(.leading) {
- Text("Leading")
- }
- DynamicIslandExpandedRegion(.trailing) {
- Text("Trailing")
- }
- DynamicIslandExpandedRegion(.bottom) {
- Text("Bottom")
- }
- } compactLeading: {
- Text("L")
- } compactTrailing: {
- Text("T")
- } minimal: {
- Text("Min")
- }
- .onesignalWidgetURL(URL(string: "https://example.com/page?param1=value1¶m2=value2#section"), context: context)
- .keylineTint(Color.red)
- }
- }
-}
-
-struct ExampleAppSecondWidget: Widget {
- var body: some WidgetConfiguration {
- ActivityConfiguration(for: ExampleAppSecondWidgetAttributes.self) { context in
- VStack {
- Spacer()
- HStack {
- Image(systemName: "bell.circle.fill")
- .resizable()
- .scaledToFit()
- .frame(width: 40.0, height: 40.0)
- Spacer()
- Text(context.attributes.title).font(.headline)
- }
- Spacer()
- HStack(alignment: .firstTextBaseline, spacing: 16) {
- Text("Update: ").font(.title2)
- Spacer()
- Text(context.state.message)
- }
- Spacer()
- HStack(alignment: .firstTextBaseline, spacing: 16) {
- Text("Progress: ").font(.title2)
- ProgressView(value: context.state.progress)
- .padding([.bottom, .top], 5)
- Text(context.state.status)
- }
- HStack(alignment: .firstTextBaseline, spacing: 16) {
- Text("Bugs: ").font(.title2)
- Spacer()
- Text(String(context.state.bugs))
- }
- Spacer()
- }
- .foregroundColor(.black)
- .padding([.all], 20)
- .activitySystemActionForegroundColor(.black)
- .activityBackgroundTint(.white)
- .onesignalWidgetURL(URL(string: "https://example.com/page?param1=value1¶m2=value2#section"), context: context)
- } dynamicIsland: { context in
- DynamicIsland {
- DynamicIslandExpandedRegion(.leading) {
- Text("Leading")
- }
- DynamicIslandExpandedRegion(.trailing) {
- Text("Trailing")
- }
- DynamicIslandExpandedRegion(.bottom) {
- Text("Bottom")
- }
- } compactLeading: {
- Text("L")
- } compactTrailing: {
- Text("T")
- } minimal: {
- Text("Min")
- }
- .keylineTint(Color.red)
- .onesignalWidgetURL(URL(string: "https://example.com/page?param1=value1¶m2=value2#section"), context: context)
- }
- }
-}
-
-struct ExampleAppThirdWidget: Widget {
- var body: some WidgetConfiguration {
- ActivityConfiguration(for: ExampleAppThirdWidgetAttributes.self) { context in
- VStack {
- Spacer()
- Text("THIRD: " + context.attributes.title).font(.headline)
- Spacer()
- HStack {
- Spacer()
- Label {
- Text(context.state.message)
- } icon: {
- Image(systemName: "bell.circle.fill")
- .resizable()
- .scaledToFit()
- .frame(width: 40.0, height: 40.0)
- }
- Spacer()
- }
- Spacer()
- }
- .foregroundColor(.black)
- .activitySystemActionForegroundColor(.black)
- .activityBackgroundTint(.white)
- } dynamicIsland: { _ in
- DynamicIsland {
- DynamicIslandExpandedRegion(.leading) {
- Text("Leading")
- }
- DynamicIslandExpandedRegion(.trailing) {
- Text("Trailing")
- }
- DynamicIslandExpandedRegion(.bottom) {
- Text("Bottom")
- }
- } compactLeading: {
- Text("L")
- } compactTrailing: {
- Text("T")
- } minimal: {
- Text("Min")
- }
- .widgetURL(URL(string: "http://www.apple.com"))
- .keylineTint(Color.red)
- }
- }
-}
-
-struct DefaultOneSignalLiveActivityWidget: Widget {
- var body: some WidgetConfiguration {
- ActivityConfiguration(for: DefaultLiveActivityAttributes.self) { context in
- VStack {
- Spacer()
- HStack {
- Image(systemName: "bell.circle.fill")
- .resizable()
- .scaledToFit()
- .frame(width: 40.0, height: 40.0)
- Spacer()
- Text("DEFAULT: " + (context.attributes.data["title"]?.asString() ?? "")).font(.headline)
- }
- Spacer()
- HStack(alignment: .firstTextBaseline, spacing: 16) {
- Text("Update: ").font(.title2)
- Spacer()
- Text(context.state.data["message"]?.asDict()?["en"]?.asString() ?? "")
- }
- Spacer()
- HStack(alignment: .firstTextBaseline, spacing: 16) {
- Text("Progress: ").font(.title2)
- ProgressView(
- value: context.state.data["progress"]?.asDouble() ?? 0.0
- ).padding([.bottom, .top], 5)
- Text(context.state.data["status"]?.asString() ?? "")
- }
- HStack(alignment: .firstTextBaseline, spacing: 16) {
- Text("Bugs: ").font(.title2)
- Spacer()
- Text(String(context.state.data["bugs"]?.asInt() ?? 0))
- }
- Spacer()
- }
- .foregroundColor(.black)
- .padding([.all], 20)
- .activitySystemActionForegroundColor(.black)
- .activityBackgroundTint(.white)
- .onesignalWidgetURL(URL(string: "https://example.com/page?param1=value1¶m2=value2#section"), context: context)
- } dynamicIsland: { context in
- DynamicIsland {
- DynamicIslandExpandedRegion(.leading) {
- Text("Leading")
- }
- DynamicIslandExpandedRegion(.trailing) {
- Text("Trailing")
- }
- DynamicIslandExpandedRegion(.bottom) {
- Text("Bottom")
- }
- } compactLeading: {
- Text("L")
- } compactTrailing: {
- Text("T")
- } minimal: {
- Text("Min")
- }
- .keylineTint(Color.red)
- .onesignalWidgetURL(URL(string: "https://example.com/page?param1=value1¶m2=value2#section"), context: context)
- }
- }
-}
diff --git a/iOS_SDK/OneSignalSwiftUIExample/README.md b/iOS_SDK/OneSignalSwiftUIExample/README.md
deleted file mode 100644
index 1587befd4..000000000
--- a/iOS_SDK/OneSignalSwiftUIExample/README.md
+++ /dev/null
@@ -1,153 +0,0 @@
-# OneSignal SwiftUI Example App
-
-A modern SwiftUI example app demonstrating the OneSignal iOS SDK features using MVVM architecture.
-
-## Features
-
-This example app demonstrates all major OneSignal SDK capabilities:
-
-- **User Management**: Login/logout with external user ID
-- **Aliases**: Add and remove user aliases
-- **Push Subscriptions**: Enable/disable push notifications, view push ID
-- **Email & SMS**: Add and remove email and SMS subscriptions
-- **Tags**: Manage user tags for segmentation
-- **Outcomes**: Track outcome events with optional values
-- **In-App Messaging**: Pause/resume IAM, manage triggers
-- **Location**: Toggle location sharing, request permissions
-- **Test Notifications**: Grid of notification types for testing
-
-## Architecture
-
-The app follows the **MVVM (Model-View-ViewModel)** pattern with a service layer:
-
-```
-OneSignalSwiftUIExample/
-├── App/
-│ └── OneSignalSwiftUIExampleApp.swift # App entry point, AppDelegate, SDK initialization
-├── Models/
-│ └── AppModels.swift # Data models (KeyValueItem, NotificationType, etc.)
-├── Services/
-│ └── OneSignalService.swift # Singleton service wrapping all OneSignal SDK calls
-├── ViewModels/
-│ └── OneSignalViewModel.swift # Main ViewModel with state management & observers
-└── Views/
- ├── ContentView.swift # Root view composing all sections
- ├── Components/ # Reusable UI components
- │ ├── AddItemSheet.swift # Sheet for adding items (aliases, tags, etc.)
- │ ├── KeyValueRow.swift # Row components for displaying data
- │ ├── NotificationGrid.swift # Grid buttons for notification types
- │ └── ToastView.swift # Toast notification overlay
- └── Sections/ # Feature-specific sections
- ├── AppInfoSection.swift # App ID display and consent management
- ├── UserSection.swift # Login/logout and alias management
- ├── SubscriptionSection.swift # Push, email, and SMS subscriptions
- ├── TagsSection.swift # User tag management
- ├── MessagingSection.swift # Outcomes, IAM controls, and triggers
- ├── LocationSection.swift # Location sharing controls
- └── NotificationSection.swift # Test notification buttons
-```
-
-## Running the App
-
-This project is part of the `OneSignalSDK.xcworkspace` and is configured to work with the local OneSignal SDK frameworks.
-
-### Quick Start
-
-1. Open `iOS_SDK/OneSignalSDK.xcworkspace` in Xcode
-2. Select the **OneSignalSwiftUIExample** scheme
-3. Select a simulator or physical device
-4. Build and run (⌘R)
-5. Grant notification permissions when prompted
-6. Explore the various OneSignal features
-
-### Using Your Own App ID
-
-The default OneSignal App ID is configured in `OneSignalService.swift`. To use your own:
-
-1. Open `OneSignalSwiftUIExample/Services/OneSignalService.swift`
-2. Change the `defaultAppId` value to your OneSignal App ID
-
-```swift
-private let defaultAppId = "your-onesignal-app-id"
-```
-
-## Project Configuration
-
-### Required Capabilities
-
-The app requires the following capabilities (already configured):
-
-- **Push Notifications**
-- **Background Modes** → Remote notifications
-
-### Info.plist Keys
-
-The following keys are configured for location and background notifications:
-
-- `NSLocationWhenInUseUsageDescription`
-- `NSLocationAlwaysAndWhenInUseUsageDescription`
-- `UIBackgroundModes` with `remote-notification`
-
-### Framework Dependencies
-
-The project links against the following OneSignal frameworks (built from the workspace):
-
-- `OneSignalFramework`
-- `OneSignalInAppMessages`
-- `OneSignalLocation`
-- `OneSignalUser`
-- `OneSignalNotifications`
-- `OneSignalExtension`
-- `OneSignalOutcomes`
-- `OneSignalOSCore`
-
-## Key Implementation Details
-
-### SDK Initialization
-
-The OneSignal SDK is initialized in `AppDelegate` via `OneSignalService.shared.initialize()`:
-
-```swift
-class AppDelegate: NSObject, UIApplicationDelegate {
- func application(_ application: UIApplication,
- didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
- OneSignalService.shared.initialize(launchOptions: launchOptions)
- // Set up notification and IAM listeners...
- return true
- }
-}
-```
-
-### Service Layer Pattern
-
-All OneSignal SDK calls are encapsulated in `OneSignalService`, providing:
-
-- Centralized SDK access
-- Easy mocking for testing
-- Clean separation from UI code
-
-### Observer Pattern
-
-The ViewModel sets up observers for SDK state changes:
-
-- `OSPushSubscriptionObserver` - Push subscription state changes
-- `OSUserStateObserver` - User state changes
-- `OSNotificationPermissionObserver` - Permission changes
-
-### SwiftUI Best Practices
-
-- `@StateObject` for ViewModel ownership
-- `@EnvironmentObject` for dependency injection to child views
-- `@MainActor` for thread-safe UI updates
-- Reusable components for consistent UI
-
-## Requirements
-
-- iOS 16.0+
-- Xcode 15.0+
-- Swift 5.9+
-- OneSignal iOS SDK 5.0+
-
-## License
-
-Modified MIT License - See LICENSE file for details.
diff --git a/iOS_SDK/OneSignalSwiftUIExample/build_app_prompt.md b/iOS_SDK/OneSignalSwiftUIExample/build_app_prompt.md
deleted file mode 100644
index 35a15cb5f..000000000
--- a/iOS_SDK/OneSignalSwiftUIExample/build_app_prompt.md
+++ /dev/null
@@ -1,1117 +0,0 @@
-# OneSignal iOS Sample App - Build Guide
-
-This document contains all the prompts and requirements needed to build the OneSignal SwiftUI Sample App from scratch. Give these prompts to an AI assistant or follow them manually to recreate the app.
-
----
-
-## Phase 1: Initial Setup
-
-### Prompt 1.1 - Project Foundation
-
-```
-Build a sample iOS app with:
-- SwiftUI app lifecycle (@main App struct with UIApplicationDelegateAdaptor)
-- MVVM architecture with a single ObservableObject ViewModel
-- @MainActor ViewModel with @Published properties
-- @EnvironmentObject for passing ViewModel to views
-- iOS 16.0 minimum deployment target
-- Xcode project (not Swift Package Manager)
-- Bundle identifier: com.onesignal.example
-- All sheets should have EMPTY input fields (for test automation - test framework enters values)
-- OneSignal brand colors via AccentColor in asset catalog (#E54B4D red)
-- App name: "OneSignalSwiftUIExample"
-- Top header bar: OneSignal logo image + "Sample App" text, left-aligned, red background spanning full width including status bar area
-- Three targets: main app, Notification Service Extension, Widget Extension
-```
-
-### Prompt 1.2 - OneSignal Service Layer
-
-```
-Centralize all OneSignal SDK calls in a single OneSignalService.swift class (singleton):
-
-App ID:
-- Stored in UserDefaults with key "OneSignalAppId"
-- Default: "77e32082-ea27-42e3-a898-c72e141824ef"
-
-Initialization:
-- initialize(launchOptions:) -> sets log level verbose, calls OneSignal.initialize(), requests push permission
-
-Identity:
-- onesignalId: String? (reads OneSignal.User.onesignalId)
-- externalId: String? (reads OneSignal.User.externalId)
-
-Consent:
-- setConsentRequired(_ required: Bool)
-- setConsentGiven(_ granted: Bool)
-
-User operations:
-- login(externalId: String)
-- logout()
-
-Alias operations:
-- addAlias(label: String, id: String)
-- addAliases(_ aliases: [String: String])
-- removeAlias(_ label: String)
-- removeAliases(_ labels: [String])
-
-Push subscription:
-- pushSubscriptionId: String?
-- isPushEnabled: Bool
-- optInPush() / optOutPush()
-- requestPushPermission(completion: @escaping (Bool) -> Void) with fallbackToSettings: true
-
-Email operations:
-- addEmail(_ email: String)
-- removeEmail(_ email: String)
-
-SMS operations:
-- addSms(_ number: String)
-- removeSms(_ number: String)
-
-Tag operations:
-- addTag(key: String, value: String)
-- addTags(_ tags: [String: String])
-- removeTag(_ key: String)
-- removeTags(_ keys: [String])
-- getTags() -> [String: String]
-
-Outcome operations:
-- sendOutcome(_ name: String)
-- sendOutcome(_ name: String, value: NSNumber)
-- sendUniqueOutcome(_ name: String)
-
-In-App Messages:
-- isInAppMessagesPaused: Bool (get/set)
-- addTrigger(key: String, value: String)
-- addTriggers(_ triggers: [String: String])
-- removeTrigger(_ key: String)
-- removeTriggers(_ keys: [String])
-- clearTriggers()
-
-Location:
-- isLocationShared: Bool (get/set)
-- requestLocationPermission()
-
-Notifications:
-- clearAllNotifications()
-- hasNotificationPermission: Bool
-
-Observers:
-- addPushSubscriptionObserver(_ observer: OSPushSubscriptionObserver)
-- addUserObserver(_ observer: OSUserStateObserver)
-- addPermissionObserver(_ observer: OSNotificationPermissionObserver)
-- addNotificationClickListener(_ listener: OSNotificationClickListener)
-- addNotificationLifecycleListener(_ listener: OSNotificationLifecycleListener)
-- addInAppMessageClickListener(_ listener: OSInAppMessageClickListener)
-- addInAppMessageLifecycleListener(_ listener: OSInAppMessageLifecycleListener)
-```
-
-### Prompt 1.3 - NotificationSender (REST API Client)
-
-```
-Create NotificationSender.swift singleton for sending test notifications via REST API:
-
-Properties:
-- apiURL: "https://onesignal.com/api/v1/notifications"
-- imageURL: "https://media.onesignal.com/automated_push_templates/ratings_template.png"
-
-Methods:
-- sendSimpleNotification(appId:completion:)
-- sendNotificationWithImage(appId:completion:)
-- sendCustomNotification(title:body:appId:completion:)
-
-All methods:
-- Get subscription ID from OneSignal.User.pushSubscription.id
-- Check optedIn status
-- POST to API with "Accept: application/vnd.onesignal.v1+json" header
-- Use include_subscription_ids (not include_player_ids)
-- Image notification includes ios_attachments and big_picture
-- Completion handler returns Result
-
-Error enum NotificationError:
-- noSubscriptionId
-- notOptedIn
-- apiError(statusCode: Int)
-
-Note: REST API key is NOT required for sending to self via subscription ID.
-```
-
-### Prompt 1.4 - UserFetchService
-
-```
-Create UserFetchService.swift singleton:
-
-Method:
-- fetchUser(appId: String, onesignalId: String) async -> UserData?
-
-Endpoint:
-- GET https://api.onesignal.com/apps/{app_id}/users/by/onesignal_id/{onesignal_id}
-- NO Authorization header needed (public endpoint)
-
-Parsing:
-- identity object -> aliases (filter out "external_id" and "onesignal_id")
-- identity.external_id -> externalId
-- properties.tags -> tags (convert all values to String)
-- subscriptions where type="Email" -> emails (token field)
-- subscriptions where type="SMS" -> smsNumbers (token field)
-
-Returns UserData struct with aliases, tags, emails, smsNumbers, externalId.
-```
-
-### Prompt 1.5 - SDK Observers and App Delegate
-
-```
-In the AppDelegate within OneSignalSwiftUIExampleApp.swift, set up in didFinishLaunchingWithOptions:
-
-1. BEFORE SDK init: Restore consent state from UserDefaults:
- - OneSignal.setConsentRequired(cached value)
- - OneSignal.setConsentGiven(cached value)
-
-2. Initialize OneSignal via OneSignalService.shared.initialize()
-
-3. Start Live Activity listeners:
- if #available(iOS 16.1, *) { LiveActivityController.start() }
-
-4. AFTER init: Restore remaining cached states from UserDefaults:
- - OneSignal.InAppMessages.paused = cached paused status
- - OneSignal.Location.isShared = cached location shared status
-
-5. Set up listeners:
- - OSNotificationLifecycleListener (onWillDisplay -> log via LogManager)
- - OSNotificationClickListener (onClick -> log via LogManager)
- - OSInAppMessageLifecycleListener (onWillDisplay, onDidDisplay, onWillDismiss, onDidDismiss -> log)
- - OSInAppMessageClickListener (onClick -> log)
- - OSLogListener -> maps SDK log levels to LogManager levels, posts to main actor
-
-6. Initialize TooltipService (fetches on background thread, non-blocking)
-
-7. On the SwiftUI App body, add .onOpenURL handler:
- - Calls OneSignal.LiveActivities.trackClickAndReturnOriginal(url)
- - Logs via LogManager
-
-In OneSignalViewModel.swift, implement observers via private Observers class:
-- OSPushSubscriptionObserver -> update pushSubscriptionId, isPushEnabled
-- OSUserStateObserver -> log state change, call fetchUserDataFromApi()
-- OSNotificationPermissionObserver -> update notificationPermissionGranted, conditionally update isPushEnabled
-```
-
-### Prompt 1.6 - LogManager
-
-```
-Create LogManager.swift:
-
-@MainActor final class LogManager: ObservableObject {
- static let shared = LogManager()
- @Published var entries: [LogEntry] = []
- private let maxEntries = 100
-
- func log(_ tag: String, _ message: String, level: LogLevel)
- func clear()
- func d/i/w/e(_ tag: String, _ message: String) // Convenience
-}
-
-LogLevel enum: debug, info, warning, error
-- Each has a rawValue (D/I/W/E) and a SwiftUI Color (blue/green/orange/red)
-
-LogEntry struct: Identifiable with UUID, timestamp, level, message
-- formattedTimestamp using "HH:mm:ss" format
-
-Every log call also prints to console via print().
-Max 100 entries, oldest removed when exceeded.
-```
-
----
-
-## Phase 2: UI Sections
-
-### Section Order (top to bottom) - FINAL
-
-1. **App Section** (App ID, Guidance Banner, Consent Toggles)
-2. **User Section** (Status, External ID, Login/Logout)
-3. **Push Section** (Push ID, Enabled Toggle, Prompt Push)
-4. **Send Push Notification Section** (Simple, With Image, Custom)
-5. **In-App Messaging Section** (Pause toggle)
-6. **Send In-App Message Section** (Top Banner, Bottom Banner, Center Modal, Full Screen)
-7. **Aliases Section** (Add/Add Multiple, read-only list)
-8. **Emails Section** (Collapsible list >5 items)
-9. **SMS Section** (Collapsible list >5 items)
-10. **Tags Section** (Add/Add Multiple/Remove Selected)
-11. **Outcome Events Section** (Send Outcome sheet with type selection)
-12. **Triggers Section** (Add/Add Multiple/Remove Selected/Clear All - IN MEMORY ONLY)
-13. **Track Event Section** (Track Event with JSON validation)
-14. **Location Section** (Location Shared toggle, Prompt Location button)
-15. **Live Activities Section** (Activity ID field, Enter/Exit buttons)
-16. **Next Activity Button**
-
-### Prompt 2.1 - App Section
-
-```
-App Section layout:
-
-1. SectionHeader with title "App"
-
-2. CardContainer with App ID display (InfoRow, readonly)
-
-3. Sticky guidance banner below App ID:
- - Text: "Add your own App ID, then rebuild to fully test all functionality."
- - Link text: "Get your keys at onesignal.com" (clickable, opens browser)
- - Light cream/yellow background (Color(red: 1.0, green: 0.98, blue: 0.90))
- - Rounded corners (12pt)
-
-4. Consent card with up to two toggles:
- a. "Consent Required" toggle (always visible):
- - Subtitle: "Require consent before SDK processes data"
- - Sets OneSignal.consentRequired, persists to UserDefaults
- b. "Privacy Consent" toggle (only visible when Consent Required is ON):
- - Subtitle: "Consent given for data collection"
- - Sets OneSignal.consentGiven, persists to UserDefaults
- - Separated from above by CardDivider
- - NOT a blocking overlay - user can interact with app regardless
-
-5. App version display:
- - Reads from Bundle.main CFBundleShortVersionString
-```
-
-### Prompt 2.2 - User Section
-
-```
-User Section:
-- SectionHeader with title "User"
-- Status card (CardContainer) with two rows separated by CardDivider:
- - Row 1: "Status" label | value ("Anonymous" in gray, or "Logged In" in green)
- - Row 2: "External ID" label | value (actual ID or em dash "—")
- - Green color: Color(red: 0.20, green: 0.66, blue: 0.33)
-
-- LOGIN USER button (ActionButton):
- - Shows "LOGIN USER" when no user logged in
- - Shows "SWITCH USER" when user is logged in
- - Opens AddItemSheet with .externalUserId type
-
-- LOGOUT USER button (OutlineActionButton):
- - Only visible when a user is logged in
-```
-
-### Prompt 2.3 - Push Section
-
-```
-Push Section:
-- SectionHeader with title "Push" and tooltipKey "push"
-- CardContainer with:
- - InfoRow showing Push Subscription ID (readonly, truncated middle)
- - CardDivider
- - ToggleRow for "Enabled" (controls optIn/optOut)
- - isEnabled parameter bound to notificationPermissionGranted
- - When disabled (no permission): toggle appears dimmed at 50% opacity
-
-- PROMPT PUSH button (ActionButton):
- - Only visible when notification permission is NOT granted
- - Requests notification permission with fallbackToSettings
- - Hidden once permission is granted
-
-Notification permission is automatically requested during SDK initialization.
-```
-
-### Prompt 2.4 - Send Push Notification Section
-
-```
-Send Push Notification Section:
-- SectionHeader with title "Send Push Notification" and tooltipKey "sendPushNotification"
-- Three full-width ActionButtons stacked vertically with 8pt spacing:
- 1. SIMPLE - sends basic notification via NotificationSender
- 2. WITH IMAGE - sends notification with big picture attachment
- 3. CUSTOM - opens CustomNotificationSheet for custom title/body
-```
-
-### Prompt 2.5 - In-App Messaging Section
-
-```
-In-App Messaging Section:
-- SectionHeader with title "In-App Messaging" and tooltipKey "inAppMessaging"
-- CardContainer with ToggleRow:
- - Title: "Pause In-App Messages"
- - Subtitle: "Toggle in-app message display"
- - Persists to UserDefaults on toggle
-```
-
-### Prompt 2.6 - Send In-App Message Section
-
-```
-Send In-App Message Section:
-- SectionHeader with title "Send In-App Message" and tooltipKey "sendInAppMessage"
-- Four full-width ActionButtonWithIcon buttons with 8pt spacing:
- 1. TOP BANNER - icon "arrow.up.to.line", trigger: "iam_type" = "top_banner"
- 2. BOTTOM BANNER - icon "arrow.down.to.line", trigger: "iam_type" = "bottom_banner"
- 3. CENTER MODAL - icon "square", trigger: "iam_type" = "center_modal"
- 4. FULL SCREEN - icon "arrow.up.left.and.arrow.down.right", trigger: "iam_type" = "full_screen"
-- Button styling:
- - RED background (AccentColor)
- - WHITE text and icon
- - SF Symbol icon on LEFT side
- - Full width, left-aligned content
- - UPPERCASE text
-- On tap: adds trigger key/value and shows toast
-```
-
-### Prompt 2.7 - Aliases Section
-
-```
-Aliases Section:
-- SectionHeader with title "Aliases" and tooltipKey "aliases"
-- CardContainer list showing key-value pairs (read-only, NO delete icons)
-- Each item shows Label | ID via KeyValueRow (no onDelete)
-- Filter out "external_id" and "onesignal_id" from display
-- "No aliases added" EmptyListRow when empty
-- ADD button -> opens AddItemSheet with .alias type
-- ADD MULTIPLE button -> opens AddMultiItemSheet with .aliases type
-- No remove/delete functionality (aliases are add-only from the UI)
-```
-
-### Prompt 2.8 - Emails Section
-
-```
-Emails Section:
-- SectionHeader with title "Emails" and tooltipKey "emails"
-- CardContainer showing email addresses via SingleValueRow with delete (xmark) icon
-- "No emails added" EmptyListRow when empty
-- ADD EMAIL button -> opens AddItemSheet with .email type
-- Collapse behavior when >5 items:
- - Show first 5 items
- - Show "X more available" text (tappable, AccentColor)
- - Expand to show all when tapped
-```
-
-### Prompt 2.9 - SMS Section
-
-```
-SMS Section:
-- SectionHeader with title "SMS" and tooltipKey "sms"
-- Same pattern as Emails Section but for phone numbers
-- ADD SMS button -> opens AddItemSheet with .sms type
-- Same collapse behavior when >5 items
-```
-
-### Prompt 2.10 - Tags Section
-
-```
-Tags Section:
-- SectionHeader with title "Tags" and tooltipKey "tags"
-- CardContainer list of key-value pairs via KeyValueRow with delete icon
-- "No tags added" EmptyListRow when empty
-- ADD button -> opens AddItemSheet with .tag type
-- ADD MULTIPLE button -> opens AddMultiItemSheet with .tags type
-- REMOVE SELECTED button (OutlineActionButton):
- - Only visible when at least one tag exists
- - Opens RemoveMultiSheet with checkboxes
-```
-
-### Prompt 2.11 - Outcome Events Section
-
-```
-Outcome Events Section:
-- SectionHeader with title "Outcome Events" and tooltipKey "outcomes"
-- SEND OUTCOME button -> opens OutcomeSheet with 3 radio options:
- 1. Normal Outcome -> shows name input field
- 2. Unique Outcome -> shows name input field
- 3. Outcome with Value -> shows name and value (decimal) input fields
-- Radio buttons using SF Symbols: largecircle.fill.circle (selected) / circle (unselected)
-- Send button disabled until name is filled AND (if with value) value is valid number
-```
-
-### Prompt 2.12 - Triggers Section (IN MEMORY ONLY)
-
-```
-Triggers Section:
-- SectionHeader with title "Triggers" and tooltipKey "triggers"
-- CardContainer list of key-value pairs with delete icon
-- "No triggers added" EmptyListRow when empty
-- ADD button -> opens AddItemSheet with .trigger type
-- ADD MULTIPLE button -> opens AddMultiItemSheet with .triggers type
-- Two action buttons (only visible when triggers exist):
- - REMOVE SELECTED (OutlineActionButton) -> RemoveMultiSheet
- - CLEAR ALL (OutlineActionButton) -> removes all triggers at once
-
-IMPORTANT: Triggers are stored IN MEMORY ONLY during the app session.
-- triggers is a @Published [KeyValueItem] in ViewModel
-- Triggers are NOT persisted to UserDefaults
-- Triggers are cleared when the app is killed/restarted
-- This is intentional - triggers are transient test data for IAM testing
-```
-
-### Prompt 2.13 - Track Event Section
-
-```
-Track Event Section:
-- SectionHeader with title "Track Event" and tooltipKey "trackEvent"
-- TRACK EVENT button -> opens TrackEventSheet with:
- - "Event Name" label + empty text field (required, shows "Required" error if empty on submit)
- - "Properties (optional, JSON)" label + text field with placeholder {"ABC":123}
- - If non-empty and not valid JSON, shows "Invalid JSON" error
- - If valid JSON, parsed via JSONSerialization to [String: Any]
- - If empty, passes nil
- - IMPORTANT: Replace iOS smart quotes (U+201C, U+201D) with standard quotes before JSON parsing
- - Calls OneSignal.User.trackEvent(name:properties:)
-```
-
-### Prompt 2.14 - Location Section
-
-```
-Location Section:
-- SectionHeader with title "Location" and tooltipKey "location"
-- CardContainer with ToggleRow:
- - Title: "Location Shared"
- - Subtitle: "Share device location with OneSignal"
- - Persists to UserDefaults on toggle
-- PROMPT LOCATION button (ActionButton)
-```
-
-### Prompt 2.15 - Live Activities Section
-
-```
-Live Activities Section:
-- SectionHeader with title "Live Activities" and tooltipKey "liveActivities"
-- CardContainer with a text field for Activity ID:
- - Label "Activity ID" on left, TextField on right (trailing aligned)
- - autocorrectionDisabled, textInputAutocapitalization(.never)
-- ENTER LIVE ACTIVITY button (ActionButton):
- - Validates ID is non-empty
- - Calls LiveActivityController.createOneSignalAwareActivity(activityId:)
- - Guarded by @available(iOS 16.1, *)
-- EXIT LIVE ACTIVITY button (OutlineActionButton):
- - Validates ID is non-empty
- - Calls OneSignal.LiveActivities.exit(activityId)
-```
-
-### Prompt 2.16 - Secondary View
-
-```
-Next Activity section:
-- NavigationLink styled as full-width ActionButton
-- Navigates to SecondaryView
-
-SecondaryView:
-- Centered content: bell.circle.fill icon (60pt), "Secondary Activity" title, description text
-- Navigation title "Secondary Activity" with inline display mode
-- Simple screen for testing navigation and IAM display on different screen
-```
-
----
-
-## Phase 3: View User API Integration
-
-### Prompt 3.1 - Data Loading Flow
-
-```
-Loading indicator overlay:
-- Full-screen semi-transparent overlay (Color.black.opacity(0.3)) with centered ProgressView
-- isLoading @Published property in ViewModel
-- Show/hide based on isLoading state
-- IMPORTANT: Add 100ms delay after populating data before dismissing loading indicator
- - Use Task.sleep(nanoseconds: 100_000_000)
-
-On cold start (init):
-- Check if OneSignal.User.onesignalId is not null
-- If exists: call fetchUserDataFromApi() -> populate UI -> delay 100ms -> set isLoading = false
-- If null: just show empty state
-
-On login:
-- Set isLoading = true immediately
-- Call OneSignal.login(externalId)
-- Clear old data (aliases, emails, sms, tags)
-- Wait for onUserStateDidChange callback
-- Callback calls fetchUserDataFromApi()
-
-On logout:
-- Set isLoading = true
-- Call OneSignal.logout()
-- Clear local lists
-- Set isLoading = false
-
-On onUserStateDidChange:
-- Call fetchUserDataFromApi() to sync with server state
-
-Note: REST API key is NOT required for fetchUser endpoint.
-```
-
-### Prompt 3.2 - UserData Model
-
-```
-struct UserData {
- let aliases: [String: String] // From identity (filter out external_id, onesignal_id)
- let tags: [String: String] // From properties.tags
- let emails: [String] // From subscriptions where type="Email" -> token
- let smsNumbers: [String] // From subscriptions where type="SMS" -> token
- let externalId: String? // From identity.external_id
-}
-```
-
----
-
-## Phase 4: Info Tooltips
-
-### Prompt 4.1 - Tooltip Content (Remote)
-
-```
-Tooltip content is fetched at runtime from the sdk-shared repo. Do NOT bundle a local copy.
-
-URL:
-https://raw.githubusercontent.com/OneSignal/sdk-shared/main/demo/tooltip_content.json
-
-This file is maintained in the sdk-shared repo and shared across all platform demo apps.
-```
-
-### Prompt 4.2 - TooltipService
-
-```
-Create TooltipService.swift:
-
-final class TooltipService: ObservableObject {
- static let shared = TooltipService()
- @Published private(set) var tooltips: [String: TooltipData] = [:]
- private var initialized = false
-
- func initialize() {
- guard !initialized else { return }
- initialized = true
- // Fetch on background thread (DispatchQueue.global(qos: .utility))
- // Parse JSON into tooltips map
- // Update on main thread
- // On failure: leave tooltips empty - tooltips are non-critical
- }
-
- func getTooltip(key: String) -> TooltipData?
-}
-
-struct TooltipData {
- let title: String
- let description: String
- let options: [TooltipOption]?
-}
-
-struct TooltipOption {
- let name: String
- let description: String
-}
-```
-
-### Prompt 4.3 - Tooltip UI Integration
-
-```
-SectionHeader has an optional tooltipKey parameter.
-When tooltipKey is set, an info.circle.fill icon button appears.
-On tap, shows an Alert with:
-- Title from tooltip.title
-- Message from tooltip.description + options list
-- Single "OK" dismiss button
-If tooltip not available: shows "Tooltip content not available."
-```
-
----
-
-## Phase 5: Data Persistence & Initialization
-
-### What IS Persisted (UserDefaults)
-
-```
-UserDefaults stores:
-- "OneSignalAppId" - App ID
-- "CachedConsentRequired" - Consent required status
-- "CachedPrivacyConsent" - Privacy consent status
-- "CachedInAppMessagesPaused" - IAM paused status
-- "CachedLocationShared" - Location shared status
-
-Note: External user ID is NOT cached in UserDefaults.
-It is read from OneSignal.User.externalId on each app launch.
-```
-
-### Initialization Flow
-
-```
-On app startup, state is restored in two layers:
-
-1. AppDelegate.didFinishLaunchingWithOptions restores SDK state from UserDefaults BEFORE init:
- - OneSignal.setConsentRequired(cached)
- - OneSignal.setConsentGiven(cached)
- - OneSignalService.shared.initialize()
- Then AFTER init:
- - Start LiveActivityController
- - OneSignal.InAppMessages.paused = cached
- - OneSignal.Location.isShared = cached
-
-2. OneSignalViewModel.init() reads UI state from the SDK (not UserDefaults):
- - consentRequired and consentGiven read from UserDefaults at @Published declaration
- - All other state read from OneSignalService (which reads from SDK)
- - refreshState() syncs push ID, push enabled, IAM paused, location, permission, external ID, tags
-
-This two-layer approach ensures:
-- The SDK is configured before anything else runs
-- The ViewModel reads SDK's actual state as the source of truth
-- The UI always reflects what the SDK reports
-```
-
-### What is NOT Persisted (In-Memory Only)
-
-```
-ViewModel holds in memory:
-- triggers: [KeyValueItem] - session-only, cleared on restart
-- aliases: populated from REST API each session
-- emails, smsNumbers: populated from REST API each session
-- tags: can be read from SDK via getTags(), also fetched from API
-```
-
----
-
-## Phase 6: Reusable Components
-
-### Prompt 6.1 - Button Styles
-
-```
-ActionButtonStyle: ButtonStyle
-- 16pt semibold white text, uppercase
-- Full width, 14pt vertical padding
-- AccentColor background with 0.8 opacity on press
-- 8pt corner radius
-
-ActionButton: View (title: String, action: () -> Void)
-- Wraps Button with ActionButtonStyle
-
-OutlineActionButtonStyle: ButtonStyle
-- 16pt semibold AccentColor text, uppercase
-- Full width, 14pt vertical padding
-- systemBackground background
-- 1.5pt AccentColor border, 8pt corner radius
-
-OutlineActionButton: View (title: String, action: () -> Void)
-- Wraps Button with OutlineActionButtonStyle
-
-ActionButtonWithIcon: View (title: String, iconName: String, action: () -> Void)
-- HStack with SF Symbol icon (18pt) + text (16pt semibold uppercase) + Spacer
-- White text on AccentColor background, 8pt corner radius
-- Left-aligned content
-```
-
-### Prompt 6.2 - Card and Layout Components
-
-```
-CardContainer: View
-- VStack(spacing: 0) wrapping content
-- systemBackground color, 12pt corner radius
-
-SectionHeader: View (title: String, tooltipKey: String?)
-- HStack with title (14pt medium, secondary color) + Spacer + optional info icon
-- Padding: horizontal 4, top 16, bottom 8
-
-CardDivider: View
-- Rectangle, separator color, 0.5pt height
-
-InfoRow: View (label: String, value: String, isMonospaced: Bool = false)
-- HStack with label (15pt medium secondary) + Spacer + value (15pt primary, lineLimit 1, truncateMiddle)
-- 16pt horizontal, 12pt vertical padding
-
-ToggleRow: View (title: String, subtitle: String?, isOn: Binding, isEnabled: Bool = true)
-- HStack with VStack(title, subtitle) + Spacer + Toggle
-- When !isEnabled: toggle disabled, entire row at 50% opacity
-- 16pt horizontal, 12pt vertical padding
-
-KeyValueRow: View (item: KeyValueItem, onDelete: (() -> Void)?)
-- HStack with VStack(key as subheadline secondary, value as body) + Spacer + optional xmark delete button
-
-SingleValueRow: View (value: String, onDelete: (() -> Void)?)
-- HStack with value text + Spacer + optional xmark delete button
-
-EmptyListRow: View (message: String)
-- Centered text (16pt medium), 16pt vertical padding
-```
-
-### Prompt 6.3 - Sheets
-
-```
-AddItemSheet: View (itemType: AddItemType, onAdd: (String, String) -> Void, onCancel: () -> Void)
-- Presents title, one or two text fields based on itemType.requiresKeyValue
-- UnderlineTextFieldStyle (custom: font 17, 8pt vertical padding, 1pt separator line below)
-- CANCEL / ADD (or LOGIN) buttons at bottom right
-- ADD disabled until fields are valid (non-empty after trimming)
-- presentationDetents([.medium]), presentationDragIndicator(.visible)
-- autocorrectionDisabled, textInputAutocapitalization(.never)
-
-AddMultiItemSheet: View (type: MultiAddItemType, onAdd: ([(String, String)]) -> Void, onCancel: () -> Void)
-- Dynamic rows of key-value pairs
-- "+ ADD ROW" button to append new empty row
-- Remove button (xmark) per row, hidden when only one row
-- ADD disabled until ALL key AND value fields in every row are non-empty
-- Batch submit
-
-RemoveMultiSheet: View (type: RemoveMultiItemType, items: [KeyValueItem], onRemove: ([String]) -> Void, onCancel: () -> Void)
-- Checkbox list (checkmark.square.fill / square SF Symbols)
-- Each row shows "key: value"
-- REMOVE button disabled when nothing selected
-
-CustomNotificationSheet: View (onSend: (String, String) -> Void, onCancel: () -> Void)
-- Title and Body text fields
-- SEND disabled until both non-empty
-
-TrackEventSheet: View (onTrack: (String, [String: Any]?) -> Void, onCancel: () -> Void)
-- Event Name field (required, shows "Required" error)
-- Properties field (optional JSON, shows "Invalid JSON" error)
-- IMPORTANT: Replace smart quotes (\u{201C}, \u{201D}) with standard quotes before parsing
-- Parse via JSONSerialization.jsonObject as [String: Any]
-
-OutcomeSheet: View
-- Radio selection: Normal / Unique / With Value
-- Name field always shown
-- Value field only when "Outcome with Value" selected
-- Send button disabled until valid
-```
-
-### Prompt 6.4 - LogView
-
-```
-LogView: View (@ObservedObject logManager: LogManager)
-- Collapsible header bar (default collapsed):
- - "LOGS" text + "(N)" count + trash button + chevron
- - Tap to expand/collapse with animation
-- When expanded:
- - 100pt height ScrollView
- - LazyVStack of log entries
- - Each entry: timestamp (11pt mono secondary) + level indicator (11pt bold mono, color-coded) + message (11pt mono, 2 line limit)
- - Auto-scroll to bottom on new entries via ScrollViewReader + onChange
- - "No logs yet" when empty
-
-ToastView: View (message: String)
-- Subheadline white text
-- Black 80% opacity background, 8pt corner radius, 4pt shadow
-- ViewModifier that overlays at bottom with slide+opacity transition
-- Auto-dismiss after 2 seconds (handled in ViewModel's showToast method)
-
-GuidanceBanner: View
-- VStack with instruction text + Link to onesignal.com
-- Light cream background, 12pt corner radius
-```
-
----
-
-## Phase 7: Extensions
-
-### Prompt 7.1 - Notification Service Extension
-
-```
-Target: OneSignalNotificationServiceExtension
-- Bundle ID: com.onesignal.example.OneSignalNotificationServiceExtensionA
-- Deployment target: iOS 16.0
-- Frameworks: OneSignalExtension, OneSignalCore, OneSignalOutcomes
-
-NotificationService.swift (UNNotificationServiceExtension subclass):
-- didReceive: calls OneSignalExtension.didReceiveNotificationExtensionRequest()
-- serviceExtensionTimeWillExpire: calls OneSignalExtension.serviceExtensionTimeWillExpireRequest()
-
-Info.plist:
-- NSExtensionPointIdentifier: com.apple.usernotifications.service
-- NSExtensionPrincipalClass: $(PRODUCT_MODULE_NAME).NotificationService
-
-Entitlements:
-- com.apple.security.application-groups: group.com.onesignal.example.onesignal
-```
-
-### Prompt 7.2 - Widget Extension for Live Activities
-
-```
-Target: OneSignalWidgetExtension
-- Bundle ID: com.onesignal.example.OneSignalWidgetExtension
-- Deployment target: iOS 16.1
-- Frameworks: WidgetKit, SwiftUI, OneSignalLiveActivities
-
-REQUIRED: Add NSSupportsLiveActivities = true to main app's Info.plist
-
-Shared file (compiled into BOTH main app and widget extension targets):
-ExampleAppWidgetAttributes.swift:
-- Wrapped in #if targetEnvironment(macCatalyst) #else ... #endif
-- ExampleAppFirstWidgetAttributes: OneSignalLiveActivityAttributes (simple message)
-- ExampleAppSecondWidgetAttributes: OneSignalLiveActivityAttributes (message, status, progress, bugs)
-- ExampleAppThirdWidgetAttributes: ActivityAttributes (NOT OneSignal-aware, manual token management)
-
-Widget Extension files:
-1. OneSignalWidgetExtensionBundle.swift (@main WidgetBundle):
- - Registers: OneSignalWidgetExtensionWidget, ExampleAppFirstWidget, ExampleAppSecondWidget,
- ExampleAppThirdWidget, DefaultOneSignalLiveActivityWidget
-
-2. OneSignalWidgetExtensionLiveActivity.swift:
- - 4 widgets using ActivityConfiguration(for:) with Lock Screen and Dynamic Island UI
- - IMPORTANT: Apply .foregroundColor(.black) to each Lock Screen VStack (white background = invisible text otherwise)
- - Use .onesignalWidgetURL() instead of .widgetURL() for click tracking
- - Use .activityBackgroundTint(.white) and .activitySystemActionForegroundColor(.black)
-
-3. OneSignalWidgetExtension.swift:
- - Basic StaticConfiguration widget showing time
- - Uses containerBackground(.fill.tertiary, for: .widget) on iOS 17+ (required by Apple)
-
-4. Info.plist: NSExtensionPointIdentifier = com.apple.widgetkit-extension
-
-Entitlements:
-- com.apple.security.app-sandbox: true
-- com.apple.security.network.client: true
-```
-
-### Prompt 7.3 - LiveActivityController (Main App)
-
-```
-Create LiveActivityController.swift in Services:
-- Wrapped in #if targetEnvironment(macCatalyst) #else ... #endif
-
-static func start():
-- OneSignal.LiveActivities.setup(ExampleAppFirstWidgetAttributes.self)
-- OneSignal.LiveActivities.setup(ExampleAppSecondWidgetAttributes.self)
-- OneSignal.LiveActivities.setupDefault()
-- For iOS 17.2+: manually monitor pushToStartTokenUpdates and activityUpdates
- for ExampleAppThirdWidgetAttributes (non-OneSignal-aware type)
-
-static func createOneSignalAwareActivity(activityId:):
-- Creates ExampleAppFirstWidgetAttributes with OneSignalLiveActivityAttributeData
-- Requests Activity with .token push type
-
-static func createDefaultActivity(activityId:):
-- Uses OneSignal.LiveActivities.startDefault() with attribute/content dictionaries
-
-static func createActivity(activityId:) async:
-- Creates ExampleAppThirdWidgetAttributes (non-OneSignal-aware)
-- Manually monitors pushTokenUpdates and calls OneSignal.LiveActivities.enter()
-```
-
----
-
-## Phase 8: Important Implementation Details
-
-### Smart Quotes Handling
-
-```
-iOS automatically replaces straight double quotes with smart/curly quotes in text fields.
-This breaks JSON parsing. In TrackEventSheet, ALWAYS replace smart quotes before parsing:
-
-let trimmedProps = propertiesText
- .trimmingCharacters(in: .whitespaces)
- .replacingOccurrences(of: "\u{201C}", with: "\"") // Left double quotation mark
- .replacingOccurrences(of: "\u{201D}", with: "\"") // Right double quotation mark
-```
-
-### Consent Initialization Order
-
-```
-Consent state MUST be set BEFORE OneSignal.initialize():
-
-1. Read from UserDefaults
-2. OneSignal.setConsentRequired(cachedValue)
-3. OneSignal.setConsentGiven(cachedValue)
-4. OneSignal.initialize(appId, withLaunchOptions: launchOptions)
-
-If consent is set after init, the SDK may process data before consent is configured.
-```
-
-### Push Permission and Enabled Toggle
-
-```
-The Push "Enabled" toggle must be disabled when notification permission is not granted:
-- ToggleRow has isEnabled parameter
-- Pass isEnabled: viewModel.notificationPermissionGranted
-- When isEnabled is false: Toggle is .disabled(), row opacity is 0.5
-- This matches Android behavior where the toggle is grayed out without permission
-```
-
-### Live Activity Click Tracking
-
-```
-When a user taps a Live Activity on the Lock Screen, iOS opens the app via a URL.
-The URL is set in the widget via .onesignalWidgetURL().
-
-In the SwiftUI App body, intercept with .onOpenURL:
-- Call OneSignal.LiveActivities.trackClickAndReturnOriginal(url)
-- This sends the click event to OneSignal and returns the original URL
-- Log the event via LogManager
-```
-
-### Alias Management
-
-```
-Aliases use a hybrid approach:
-1. On app start/login: Fetched from REST API via fetchUserDataFromApi()
-2. When user adds locally: SDK call + immediate local list update (don't wait for API)
-3. On next launch: fresh data from API includes synced alias
-```
-
-### Toast Messages
-
-```
-All user actions display toast messages:
-- Login: "Logged in as {userId}"
-- Logout: "Logged out"
-- Add alias/tag/trigger: "Alias added", "Tag added", etc.
-- Add multiple: "{count} alias(es) added"
-- Notifications: "Simple notification sent!" or "Failed: {error}"
-- In-App Messages: "Sent In-App Message: {type}"
-- Outcomes: "Outcome '{name}' sent"
-- Events: "Event '{name}' tracked"
-- Location: "Location sharing enabled/disabled"
-- Push: "Push enabled/disabled"
-- Live Activities: "Live Activity '{id}' entered/exited"
-
-Implementation:
-- ViewModel has @Published toastMessage: String?
-- showToast() sets message and auto-nils after 2 seconds via Task.sleep
-- ToastModifier overlays at bottom of screen with animation
-```
-
----
-
-## Configuration
-
-### Info.plist Required Keys
-
-```xml
-
-NSSupportsLiveActivities
-
-NSLocationAlwaysAndWhenInUseUsageDescription
-This app uses your location to provide location-based notifications and services.
-NSLocationWhenInUseUsageDescription
-This app uses your location to provide location-based notifications.
-UIBackgroundModes
-
- remote-notification
-
-```
-
-### Entitlements
-
-```
-Main app (OneSignalSwiftUIExample.entitlements):
-- aps-environment: development
-- com.apple.security.application-groups: group.com.onesignal.example.onesignal
-
-NSE (OneSignalNotificationServiceExtension.entitlements):
-- com.apple.security.application-groups: group.com.onesignal.example.onesignal
-
-Widget Extension (OneSignalWidgetExtension.entitlements):
-- com.apple.security.app-sandbox: true
-- com.apple.security.network.client: true
-```
-
-### Bundle Identifiers
-
-```
-Main app: com.onesignal.example
-NSE: com.onesignal.example.OneSignalNotificationServiceExtensionA
-Widget: com.onesignal.example.OneSignalWidgetExtension
-```
-
-### OneSignal Frameworks
-
-```
-Main app links:
-- OneSignalFramework, OneSignalCore, OneSignalExtension, OneSignalOutcomes
-- OneSignalOSCore, OneSignalUser, OneSignalNotifications
-- OneSignalInAppMessages, OneSignalLocation, OneSignalLiveActivities
-- CoreLocation, SystemConfiguration, UserNotifications, WebKit
-
-NSE links:
-- OneSignalExtension, OneSignalCore, OneSignalOutcomes
-
-Widget Extension links:
-- WidgetKit, SwiftUI, OneSignalLiveActivities
-```
-
----
-
-## Key Files Structure
-
-```
-OneSignalSwiftUIExample/
-├── OneSignalSwiftUIExample.xcodeproj/
-├── OneSignalSwiftUIExample.entitlements
-├── OneSignalWidgetExtension.entitlements
-├── OneSignalSwiftUIExample/
-│ ├── App/
-│ │ └── OneSignalSwiftUIExampleApp.swift # @main App, AppDelegate, observers
-│ ├── Models/
-│ │ └── AppModels.swift # KeyValueItem, enums, UserData, TooltipData
-│ ├── Services/
-│ │ ├── OneSignalService.swift # SDK wrapper singleton
-│ │ ├── NotificationSender.swift # REST API notification sender
-│ │ ├── UserFetchService.swift # REST API user data fetcher
-│ │ ├── TooltipService.swift # Remote tooltip loader
-│ │ ├── LogManager.swift # Thread-safe pass-through logger
-│ │ └── LiveActivityController.swift # Live Activity setup and creation
-│ ├── ViewModels/
-│ │ └── OneSignalViewModel.swift # Main @MainActor ObservableObject
-│ ├── Views/
-│ │ ├── ContentView.swift # Root view composing all sections
-│ │ ├── Components/
-│ │ │ ├── KeyValueRow.swift # All reusable UI components
-│ │ │ ├── NotificationGrid.swift # Push and IAM button groups
-│ │ │ ├── AddItemSheet.swift # Single-item add sheet
-│ │ │ ├── AddMultiItemSheet.swift # Multi-pair add sheet
-│ │ │ ├── RemoveMultiSheet.swift # Checkbox remove sheet
-│ │ │ ├── CustomNotificationSheet.swift # Custom notification sheet
-│ │ │ ├── TrackEventSheet.swift # Track event with JSON sheet
-│ │ │ ├── LogView.swift # Collapsible log viewer
-│ │ │ ├── ToastView.swift # Toast overlay
-│ │ │ └── GuidanceBanner.swift # Setup instruction banner
-│ │ └── Sections/
-│ │ ├── AppInfoSection.swift # App ID, banner, consent
-│ │ ├── UserSection.swift # User + Aliases sections
-│ │ ├── SubscriptionSection.swift # Push + Emails + SMS sections
-│ │ ├── NotificationSection.swift # Send Push + Send IAM sections
-│ │ ├── MessagingSection.swift # IAM toggle + Triggers + Outcomes
-│ │ ├── TagsSection.swift # Tags section
-│ │ ├── TrackEventSection.swift # Track Event section
-│ │ ├── LocationSection.swift # Location section
-│ │ ├── LiveActivitySection.swift # Live Activities section
-│ │ └── NextScreenSection.swift # Navigation + SecondaryView
-│ ├── ExampleAppWidgetAttributes.swift # Shared ActivityAttributes (both targets)
-│ ├── Assets.xcassets/ # App icon, AccentColor, OneSignalLogo
-│ └── Info.plist
-├── OneSignalNotificationServiceExtension/
-│ ├── NotificationService.swift
-│ ├── Info.plist
-│ └── OneSignalNotificationServiceExtension.entitlements
-└── OneSignalWidgetExtension/
- ├── OneSignalWidgetExtensionBundle.swift
- ├── OneSignalWidgetExtensionLiveActivity.swift
- ├── OneSignalWidgetExtension.swift
- └── Info.plist
-```
-
-Note:
-
-- All UI is SwiftUI (no UIKit storyboards/xibs)
-- Tooltip content is fetched from remote URL (not bundled locally)
-- LogView at top of screen displays SDK and app logs for debugging
-- Multiple sections may share a single .swift file (e.g., MessagingSection.swift contains OutcomeEvents, IAM, and Triggers)
-
----
-
-## Summary
-
-This app demonstrates all OneSignal iOS SDK features:
-
-- User management (login/logout, aliases with batch add)
-- Push notifications (subscription, sending with images, permission handling)
-- Email and SMS subscriptions
-- Tags for segmentation (batch add/remove support)
-- Triggers for in-app message targeting (in-memory only, batch operations)
-- Outcomes for conversion tracking
-- Event tracking with JSON properties validation
-- In-app messages (display testing with type-specific icons)
-- Location sharing
-- Privacy consent management
-- Live Activities (enter/exit, push-to-start, widget extension, click tracking)
-- Notification Service Extension (rich notifications)
-
-The app is designed to be:
-
-1. **Testable** - Empty sheets for test automation
-2. **Comprehensive** - All SDK features demonstrated
-3. **Clean** - MVVM architecture with SwiftUI
-4. **Cross-platform ready** - Tooltip content shared via JSON across all platforms
-5. **Session-based triggers** - Triggers stored in memory only, cleared on restart
-6. **Responsive UI** - Loading indicator with delay to ensure UI populates before dismissing
-7. **Performant** - Tooltip JSON loaded on background thread
-8. **Modern UI** - SwiftUI with reusable components matching Android Material3 design
-9. **Batch Operations** - Add multiple items at once, select and remove multiple items
-10. **Extension-ready** - Notification Service Extension and Widget Extension for Live Activities