diff --git a/OneSignalSwiftUIExample/OneSignalSwiftUIExample.entitlements b/OneSignalSwiftUIExample/OneSignalSwiftUIExample.entitlements new file mode 100644 index 000000000..903def2af --- /dev/null +++ b/OneSignalSwiftUIExample/OneSignalSwiftUIExample.entitlements @@ -0,0 +1,8 @@ + + + + + aps-environment + development + + diff --git a/OneSignalSwiftUIExample/OneSignalSwiftUIExample/App/OneSignalSwiftUIExampleApp.swift b/OneSignalSwiftUIExample/OneSignalSwiftUIExample/App/OneSignalSwiftUIExampleApp.swift new file mode 100644 index 000000000..891c6a1ca --- /dev/null +++ b/OneSignalSwiftUIExample/OneSignalSwiftUIExample/App/OneSignalSwiftUIExampleApp.swift @@ -0,0 +1,137 @@ +/** + * 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 OneSignalFramework +import OneSignalInAppMessages +import OneSignalLocation + +@main +struct OneSignalSwiftUIExampleApp: App { + @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate + @StateObject private var viewModel = OneSignalViewModel() + + var body: some Scene { + WindowGroup { + ContentView() + .environmentObject(viewModel) + } + } +} + +// MARK: - App Delegate + +class AppDelegate: NSObject, UIApplicationDelegate { + + func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil + ) -> Bool { + // Initialize OneSignal + OneSignalService.shared.initialize(launchOptions: launchOptions) + + // Set up notification lifecycle listeners + setupNotificationListeners() + + // Set up in-app message listeners + setupInAppMessageListeners() + + return true + } + + 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) + + // Start with IAM paused + OneSignal.InAppMessages.paused = true + } +} + +// MARK: - Notification Handlers + +class NotificationLifecycleHandler: NSObject, OSNotificationLifecycleListener { + static let shared = NotificationLifecycleHandler() + + func onWillDisplay(event: OSNotificationWillDisplayEvent) { + print("[OneSignal] Notification will display: \(event.notification.title ?? "No title")") + // Optionally modify display behavior + // event.preventDefault() // Prevent automatic display + // event.notification.display() // Manually display later + } +} + +class NotificationClickHandler: NSObject, OSNotificationClickListener { + static let shared = NotificationClickHandler() + + func onClick(event: OSNotificationClickEvent) { + print("[OneSignal] Notification clicked: \(event.notification.title ?? "No title")") + // Handle notification click - navigate to specific screen, etc. + } +} + +// MARK: - In-App Message Handlers + +class InAppMessageLifecycleHandler: NSObject, OSInAppMessageLifecycleListener { + static let shared = InAppMessageLifecycleHandler() + + func onWillDisplay(event: OSInAppMessageWillDisplayEvent) { + print("[OneSignal] IAM will display: \(event.message.messageId)") + } + + func onDidDisplay(event: OSInAppMessageDidDisplayEvent) { + print("[OneSignal] IAM did display: \(event.message.messageId)") + } + + func onWillDismiss(event: OSInAppMessageWillDismissEvent) { + print("[OneSignal] IAM will dismiss: \(event.message.messageId)") + } + + func onDidDismiss(event: OSInAppMessageDidDismissEvent) { + print("[OneSignal] IAM did dismiss: \(event.message.messageId)") + } +} + +class InAppMessageClickHandler: NSObject, OSInAppMessageClickListener { + static let shared = InAppMessageClickHandler() + + func onClick(event: OSInAppMessageClickEvent) { + print("[OneSignal] IAM clicked: \(event.result.actionId ?? "No action ID")") + // Handle IAM click - navigate, track event, etc. + } +} diff --git a/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AccentColor.colorset/Contents.json b/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 000000000..2c54006ed --- /dev/null +++ b/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "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/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/Contents.json b/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000..13613e3ee --- /dev/null +++ b/OneSignalSwiftUIExample/OneSignalSwiftUIExample/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/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/Contents.json b/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Info.plist b/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Info.plist new file mode 100644 index 000000000..aba8d06a4 --- /dev/null +++ b/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Info.plist @@ -0,0 +1,58 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + OneSignal SwiftUI + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + 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. + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + + UIBackgroundModes + + remote-notification + + UILaunchScreen + + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Models/AppModels.swift b/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Models/AppModels.swift new file mode 100644 index 000000000..85a9bfc37 --- /dev/null +++ b/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Models/AppModels.swift @@ -0,0 +1,145 @@ +/** + * 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 new file mode 100644 index 000000000..5684a2a34 --- /dev/null +++ b/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Services/OneSignalService.swift @@ -0,0 +1,236 @@ +/** + * 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 new file mode 100644 index 000000000..7e8e8dae9 --- /dev/null +++ b/OneSignalSwiftUIExample/OneSignalSwiftUIExample/ViewModels/OneSignalViewModel.swift @@ -0,0 +1,350 @@ +/** + * 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 new file mode 100644 index 000000000..ed1c621d6 --- /dev/null +++ b/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Components/AddItemSheet.swift @@ -0,0 +1,126 @@ +/** + * 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 new file mode 100644 index 000000000..af50dc8b2 --- /dev/null +++ b/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Components/KeyValueRow.swift @@ -0,0 +1,158 @@ +/** + * 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 new file mode 100644 index 000000000..9a54202eb --- /dev/null +++ b/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Components/NotificationGrid.swift @@ -0,0 +1,137 @@ +/** + * 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/Components/ToastView.swift b/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Components/ToastView.swift new file mode 100644 index 000000000..eede3ce4d --- /dev/null +++ b/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Components/ToastView.swift @@ -0,0 +1,80 @@ +/** + * 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/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/ContentView.swift b/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/ContentView.swift new file mode 100644 index 000000000..80a7db871 --- /dev/null +++ b/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/ContentView.swift @@ -0,0 +1,75 @@ +/** + * 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 +struct ContentView: View { + @EnvironmentObject var viewModel: OneSignalViewModel + + var body: some View { + NavigationStack { + List { + AppInfoSection() + UserSection() + SubscriptionSection() + TagsSection() + MessagingSection() + LocationSection() + NotificationSection() + } + .listStyle(.insetGrouped) + .navigationTitle("OneSignal") + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button { + viewModel.refreshState() + } label: { + Image(systemName: "arrow.clockwise") + } + } + } + .sheet(isPresented: $viewModel.showingAddSheet) { + AddItemSheet( + itemType: viewModel.addItemType, + onAdd: { key, value in + viewModel.handleAddItem(key: key, value: value) + }, + onCancel: { + viewModel.showingAddSheet = false + } + ) + } + } + .toast(message: $viewModel.toastMessage) + } +} + +#Preview { + ContentView() + .environmentObject(OneSignalViewModel()) +} diff --git a/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Sections/AppInfoSection.swift b/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Sections/AppInfoSection.swift new file mode 100644 index 000000000..4bab9ebc2 --- /dev/null +++ b/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Sections/AppInfoSection.swift @@ -0,0 +1,69 @@ +/** + * 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 and consent management +struct AppInfoSection: View { + @EnvironmentObject var viewModel: OneSignalViewModel + + 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") + } + } +} + +#Preview { + List { + AppInfoSection() + } + .environmentObject(OneSignalViewModel()) +} diff --git a/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Sections/LocationSection.swift b/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Sections/LocationSection.swift new file mode 100644 index 000000000..2d89f9077 --- /dev/null +++ b/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Sections/LocationSection.swift @@ -0,0 +1,69 @@ +/** + * 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 location sharing and permissions +struct LocationSection: 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) + } + } + + Button { + viewModel.promptLocation() + } label: { + HStack { + Spacer() + Text("Prompt Location") + .fontWeight(.medium) + Spacer() + } + } + } header: { + Text("Location") + } + } +} + +#Preview { + List { + LocationSection() + } + .environmentObject(OneSignalViewModel()) +} diff --git a/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Sections/MessagingSection.swift b/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Sections/MessagingSection.swift new file mode 100644 index 000000000..ef21501a0 --- /dev/null +++ b/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Sections/MessagingSection.swift @@ -0,0 +1,179 @@ +/** + * 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/NotificationSection.swift b/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Sections/NotificationSection.swift new file mode 100644 index 000000000..92b7bcdfd --- /dev/null +++ b/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Sections/NotificationSection.swift @@ -0,0 +1,60 @@ +/** + * 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 sending test push notifications and in-app messages +struct NotificationSection: View { + @EnvironmentObject var viewModel: OneSignalViewModel + + var body: some View { + // Send Push Notification Section + Section { + NotificationTypeGrid { type in + viewModel.sendTestNotification(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/Sections/SubscriptionSection.swift b/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Sections/SubscriptionSection.swift new file mode 100644 index 000000000..24ea67eca --- /dev/null +++ b/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Sections/SubscriptionSection.swift @@ -0,0 +1,116 @@ +/** + * 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/TagsSection.swift b/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Sections/TagsSection.swift new file mode 100644 index 000000000..586169e30 --- /dev/null +++ b/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Sections/TagsSection.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 + +/// Section for managing user tags +struct TagsSection: View { + @EnvironmentObject var viewModel: OneSignalViewModel + + 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) + } + } + } + + Button { + viewModel.showAddSheet(for: .tag) + } label: { + HStack { + Spacer() + Label("Add Tag", systemImage: "plus") + .fontWeight(.medium) + Spacer() + } + } + } header: { + Text("Tags") + } + } +} + +#Preview { + List { + TagsSection() + } + .environmentObject(OneSignalViewModel()) +} diff --git a/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Sections/UserSection.swift b/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Sections/UserSection.swift new file mode 100644 index 000000000..77b8c130b --- /dev/null +++ b/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Sections/UserSection.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 + +/// 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 new file mode 100644 index 000000000..f20899df8 --- /dev/null +++ b/OneSignalSwiftUIExample/README.md @@ -0,0 +1,136 @@ +# 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/iOS_SDK/OneSignalSDK.xcworkspace/contents.xcworkspacedata b/iOS_SDK/OneSignalSDK.xcworkspace/contents.xcworkspacedata index 9979cb733..9e7617845 100644 --- a/iOS_SDK/OneSignalSDK.xcworkspace/contents.xcworkspacedata +++ b/iOS_SDK/OneSignalSDK.xcworkspace/contents.xcworkspacedata @@ -1,6 +1,9 @@ + + diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalNotificationServiceExtension/Info.plist b/iOS_SDK/OneSignalSwiftUIExample/OneSignalNotificationServiceExtension/Info.plist new file mode 100644 index 000000000..9c03e92f1 --- /dev/null +++ b/iOS_SDK/OneSignalSwiftUIExample/OneSignalNotificationServiceExtension/Info.plist @@ -0,0 +1,31 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + OneSignalNotificationServiceExtension + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + $(MARKETING_VERSION) + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + NSExtension + + NSExtensionPointIdentifier + com.apple.usernotifications.service + NSExtensionPrincipalClass + $(PRODUCT_MODULE_NAME).NotificationService + + + diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalNotificationServiceExtension/NotificationService.swift b/iOS_SDK/OneSignalSwiftUIExample/OneSignalNotificationServiceExtension/NotificationService.swift new file mode 100644 index 000000000..d536bdaff --- /dev/null +++ b/iOS_SDK/OneSignalSwiftUIExample/OneSignalNotificationServiceExtension/NotificationService.swift @@ -0,0 +1,37 @@ +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/OneSignalNotificationServiceExtension/OneSignalNotificationServiceExtension.entitlements b/iOS_SDK/OneSignalSwiftUIExample/OneSignalNotificationServiceExtension/OneSignalNotificationServiceExtension.entitlements new file mode 100644 index 000000000..c70461e82 --- /dev/null +++ b/iOS_SDK/OneSignalSwiftUIExample/OneSignalNotificationServiceExtension/OneSignalNotificationServiceExtension.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.com.onesignal.example.onesignal + + + diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample.entitlements b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample.entitlements new file mode 100644 index 000000000..344636495 --- /dev/null +++ b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample.entitlements @@ -0,0 +1,12 @@ + + + + + aps-environment + development + com.apple.security.application-groups + + group.com.onesignal.example.onesignal + + + diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample.xcodeproj/project.pbxproj b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample.xcodeproj/project.pbxproj new file mode 100644 index 000000000..a5578aa23 --- /dev/null +++ b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample.xcodeproj/project.pbxproj @@ -0,0 +1,960 @@ +// !$*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 new file mode 100644 index 000000000..c1bea564d --- /dev/null +++ b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample.xcodeproj/xcshareddata/xcschemes/OneSignalSwiftUIExample.xcscheme @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/App/OneSignalSwiftUIExampleApp.swift b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/App/OneSignalSwiftUIExampleApp.swift new file mode 100644 index 000000000..5ffabb522 --- /dev/null +++ b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/App/OneSignalSwiftUIExampleApp.swift @@ -0,0 +1,209 @@ +/** + * 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 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 { + + // 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 { + // 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 + } + + private func setupLogListener() { + OneSignal.Debug.setLogLevel(.LL_VERBOSE) + OneSignal.Debug.addLogListener(SDKLogListener.shared) + } + + private func restoreCachedStates() { + // Restore IAM paused status + let iamPaused = 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 new file mode 100644 index 000000000..2c54006ed --- /dev/null +++ b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "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 new file mode 100644 index 000000000..57855f96d Binary files /dev/null and b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/100.png 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 new file mode 100644 index 000000000..0d61ec818 Binary files /dev/null and b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/1024.png 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 new file mode 100644 index 000000000..61d5f8962 Binary files /dev/null and b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/114.png 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 new file mode 100644 index 000000000..5ba28fc5b Binary files /dev/null and b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/120.png 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 new file mode 100644 index 000000000..84d021320 Binary files /dev/null and b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/128.png 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 new file mode 100644 index 000000000..e30ed7b19 Binary files /dev/null and b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/144.png 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 new file mode 100644 index 000000000..195516d96 Binary files /dev/null and b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/152.png 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 new file mode 100644 index 000000000..4e4b3be94 Binary files /dev/null and b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/16.png 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 new file mode 100644 index 000000000..bc74b6f98 Binary files /dev/null and b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/167.png 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 new file mode 100644 index 000000000..772e8878b Binary files /dev/null and b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/172.png 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 new file mode 100644 index 000000000..54b94c46a Binary files /dev/null and b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/180.png 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 new file mode 100644 index 000000000..f975b2c0c Binary files /dev/null and b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/196.png 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 new file mode 100644 index 000000000..db15cd57c Binary files /dev/null and b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/20.png 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 new file mode 100644 index 000000000..41629097d Binary files /dev/null and b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/216.png 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 new file mode 100644 index 000000000..f88c1cfe0 Binary files /dev/null and b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/256.png 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 new file mode 100644 index 000000000..f66c8e999 Binary files /dev/null and b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/29.png 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 new file mode 100644 index 000000000..2285ee766 Binary files /dev/null and b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/32.png 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 new file mode 100644 index 000000000..302c40feb Binary files /dev/null and b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/40.png 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 new file mode 100644 index 000000000..f4ee0060d Binary files /dev/null and b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/48.png 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 new file mode 100644 index 000000000..a8bfadee4 Binary files /dev/null and b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/50.png 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 new file mode 100644 index 000000000..a2d9aeee6 Binary files /dev/null and b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/512.png 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 new file mode 100644 index 000000000..1f3d4c2d5 Binary files /dev/null and b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/55.png 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 new file mode 100644 index 000000000..03585df4f Binary files /dev/null and b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/57.png 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 new file mode 100644 index 000000000..0576f9976 Binary files /dev/null and b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/58.png 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 new file mode 100644 index 000000000..6211b6bbf Binary files /dev/null and b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/60.png 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 new file mode 100644 index 000000000..3d0afeaf7 Binary files /dev/null and b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/64.png 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 new file mode 100644 index 000000000..50bfed1bc Binary files /dev/null and b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/66.png 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 new file mode 100644 index 000000000..fa283d7b4 Binary files /dev/null and b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/72.png 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 new file mode 100644 index 000000000..4c555c02a Binary files /dev/null and b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/76.png 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 new file mode 100644 index 000000000..f7454c62c Binary files /dev/null and b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/80.png 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 new file mode 100644 index 000000000..e44819f04 Binary files /dev/null and b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/87.png 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 new file mode 100644 index 000000000..d1e764b6f Binary files /dev/null and b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/88.png 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 new file mode 100644 index 000000000..997cdf27c Binary files /dev/null and b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/92.png 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 new file mode 100644 index 000000000..8e70699a7 --- /dev/null +++ b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,346 @@ +{ + "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 new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/OneSignalLogo.imageset/Contents.json b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/OneSignalLogo.imageset/Contents.json new file mode 100644 index 000000000..cbc496abc --- /dev/null +++ b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/OneSignalLogo.imageset/Contents.json @@ -0,0 +1,24 @@ +{ + "images" : [ + { + "filename" : "onesignal_rectangle.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} 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 new file mode 100644 index 000000000..827725338 Binary files /dev/null and b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Assets.xcassets/OneSignalLogo.imageset/onesignal_rectangle.png differ diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/ExampleAppWidgetAttributes.swift b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/ExampleAppWidgetAttributes.swift new file mode 100644 index 000000000..6a8ed79a3 --- /dev/null +++ b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/ExampleAppWidgetAttributes.swift @@ -0,0 +1,37 @@ +#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 new file mode 100644 index 000000000..82b16a177 --- /dev/null +++ b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Info.plist @@ -0,0 +1,58 @@ + + + + + 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 + + + diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Models/AppModels.swift b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Models/AppModels.swift new file mode 100644 index 000000000..7bd1f3a7c --- /dev/null +++ b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Models/AppModels.swift @@ -0,0 +1,179 @@ +/** + * 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 new file mode 100644 index 000000000..090a51ba4 --- /dev/null +++ b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Services/LiveActivityController.swift @@ -0,0 +1,83 @@ +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 new file mode 100644 index 000000000..d5a576208 --- /dev/null +++ b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Services/LogManager.swift @@ -0,0 +1,92 @@ +/** + * 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 new file mode 100644 index 000000000..d1758006a --- /dev/null +++ b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Services/NotificationSender.swift @@ -0,0 +1,176 @@ +/** + * 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/OneSignalService.swift b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Services/OneSignalService.swift new file mode 100644 index 000000000..981fe99a9 --- /dev/null +++ b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Services/OneSignalService.swift @@ -0,0 +1,274 @@ +/** + * 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 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) + OneSignal.Notifications.requestPermission() + } + + // MARK: - Identity + + var onesignalId: String? { + OneSignal.User.onesignalId + } + + var externalId: String? { + OneSignal.User.externalId + } + + // MARK: - Consent + + func setConsentRequired(_ required: Bool) { + OneSignal.setConsentRequired(required) + } + + func setConsentGiven(_ granted: Bool) { + OneSignal.setConsentGiven(granted) + } + + func revokeConsent() { + // Must set consent as required first, then revoke it + OneSignal.setConsentRequired(true) + 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: 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() + } + + 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 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) + } + + // 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 addTriggers(_ triggers: [String: String]) { + OneSignal.InAppMessages.addTriggers(triggers) + } + + func removeTrigger(_ key: String) { + OneSignal.InAppMessages.removeTrigger(key) + } + + func removeTriggers(_ keys: [String]) { + OneSignal.InAppMessages.removeTriggers(keys) + } + + 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 } + } + + 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/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Services/TooltipService.swift b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Services/TooltipService.swift new file mode 100644 index 000000000..bbeb8dae8 --- /dev/null +++ b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Services/TooltipService.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 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/Services/UserFetchService.swift b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Services/UserFetchService.swift new file mode 100644 index 000000000..eee835141 --- /dev/null +++ b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Services/UserFetchService.swift @@ -0,0 +1,125 @@ +/** + * 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 for fetching user data from the OneSignal REST API. +/// No API key is required for this public endpoint. +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") + return nil + } + + guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] else { + return nil + } + + return parseUserData(json) + } catch { + print("[UserFetchService] Fetch failed: \(error.localizedDescription)") + return nil + } + } + + // MARK: - Private + + 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 + } + } + } + + // 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)" + } + } + } + + // 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) + } + } + } + + return UserData( + aliases: aliases, + tags: tags, + emails: emails, + smsNumbers: smsNumbers, + externalId: externalId + ) + } +} diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/ViewModels/OneSignalViewModel.swift b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/ViewModels/OneSignalViewModel.swift new file mode 100644 index 000000000..50e0f1bad --- /dev/null +++ b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/ViewModels/OneSignalViewModel.swift @@ -0,0 +1,627 @@ +/** + * 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 = permission && (viewModel?.isPushEnabled ?? false) + } + } +} diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Components/AddItemSheet.swift b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Components/AddItemSheet.swift new file mode 100644 index 000000000..62ee72211 --- /dev/null +++ b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Components/AddItemSheet.swift @@ -0,0 +1,176 @@ +/** + * 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 new file mode 100644 index 000000000..75e734bf2 --- /dev/null +++ b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Components/AddMultiItemSheet.swift @@ -0,0 +1,140 @@ +/** + * 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 new file mode 100644 index 000000000..896f1f063 --- /dev/null +++ b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Components/CustomNotificationSheet.swift @@ -0,0 +1,113 @@ +/** + * 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/GuidanceBanner.swift b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Components/GuidanceBanner.swift new file mode 100644 index 000000000..a2f73edf0 --- /dev/null +++ b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Components/GuidanceBanner.swift @@ -0,0 +1,54 @@ +/** + * 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 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) + + Link("Get your keys at onesignal.com", destination: URL(string: "https://onesignal.com")!) + .font(.system(size: 14, weight: .medium)) + .foregroundColor(.accentColor) + } + .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/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Components/KeyValueRow.swift b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Components/KeyValueRow.swift new file mode 100644 index 000000000..efee8e54a --- /dev/null +++ b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Components/KeyValueRow.swift @@ -0,0 +1,363 @@ +/** + * 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 new file mode 100644 index 000000000..fb03c3db9 --- /dev/null +++ b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Components/LogView.swift @@ -0,0 +1,130 @@ +/** + * 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 new file mode 100644 index 000000000..1545313c5 --- /dev/null +++ b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Components/NotificationGrid.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 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 + + var body: some View { + VStack(spacing: 8) { + ActionButton(title: "Simple", action: onSimple) + ActionButton(title: "With Image", action: onWithImage) + ActionButton(title: "Custom", action: onCustom) + } + } +} + +/// 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") } + ) + + 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 new file mode 100644 index 000000000..85e0e5ed3 --- /dev/null +++ b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Components/RemoveMultiSheet.swift @@ -0,0 +1,118 @@ +/** + * 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 new file mode 100644 index 000000000..eede3ce4d --- /dev/null +++ b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Components/ToastView.swift @@ -0,0 +1,80 @@ +/** + * 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 new file mode 100644 index 000000000..f3aa65c6d --- /dev/null +++ b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Components/TrackEventSheet.swift @@ -0,0 +1,150 @@ +/** + * 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 new file mode 100644 index 000000000..d06a3a6a6 --- /dev/null +++ b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/ContentView.swift @@ -0,0 +1,199 @@ +/** + * 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 new file mode 100644 index 000000000..d0b80f902 --- /dev/null +++ b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Sections/AppInfoSection.swift @@ -0,0 +1,84 @@ +/** + * 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 new file mode 100644 index 000000000..294c904e6 --- /dev/null +++ b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Sections/LiveActivitySection.swift @@ -0,0 +1,46 @@ +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/LocationSection.swift b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Sections/LocationSection.swift new file mode 100644 index 000000000..8554cdc6e --- /dev/null +++ b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Sections/LocationSection.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 + +/// Section for location sharing and permissions +struct LocationSection: View { + @EnvironmentObject var viewModel: OneSignalViewModel + + var body: some View { + VStack(spacing: 0) { + SectionHeader(title: "Location", tooltipKey: "location") + + CardContainer { + ToggleRow( + title: "Location Shared", + subtitle: "Share device location with OneSignal", + isOn: Binding( + get: { viewModel.isLocationShared }, + set: { _ in viewModel.toggleLocationShared() } + ) + ) + } + + ActionButton(title: "Prompt Location") { + viewModel.promptLocation() + } + .padding(.top, 12) + } + } +} + +#Preview { + LocationSection() + .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 new file mode 100644 index 000000000..6e8c0b203 --- /dev/null +++ b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Sections/MessagingSection.swift @@ -0,0 +1,272 @@ +/** + * 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 new file mode 100644 index 000000000..2361b4996 --- /dev/null +++ b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Sections/NextScreenSection.swift @@ -0,0 +1,81 @@ +/** + * 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/NotificationSection.swift b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Sections/NotificationSection.swift new file mode 100644 index 000000000..444f77ac2 --- /dev/null +++ b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Sections/NotificationSection.swift @@ -0,0 +1,78 @@ +/** + * 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 sending test push notifications (3 full-width buttons) +struct SendPushSection: View { + @EnvironmentObject var viewModel: OneSignalViewModel + + 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 + } + ) + } + } +} + +/// 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) + } + } + } +} + +#Preview { + ScrollView { + VStack { + SendPushSection() + SendInAppSection() + } + .padding() + } + .background(Color(.systemGroupedBackground)) + .environmentObject(OneSignalViewModel()) +} diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Sections/SubscriptionSection.swift b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Sections/SubscriptionSection.swift new file mode 100644 index 000000000..b0aec36f4 --- /dev/null +++ b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Sections/SubscriptionSection.swift @@ -0,0 +1,180 @@ +/** + * 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 new file mode 100644 index 000000000..5bea36558 --- /dev/null +++ b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Sections/TagsSection.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 + +/// 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/TrackEventSection.swift b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Sections/TrackEventSection.swift new file mode 100644 index 000000000..803342735 --- /dev/null +++ b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Sections/TrackEventSection.swift @@ -0,0 +1,50 @@ +/** + * 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 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 + } + } + } +} + +#Preview { + TrackEventSection() + .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 new file mode 100644 index 000000000..0cbe80a64 --- /dev/null +++ b/iOS_SDK/OneSignalSwiftUIExample/OneSignalSwiftUIExample/Views/Sections/UserSection.swift @@ -0,0 +1,132 @@ +/** + * 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 new file mode 100644 index 000000000..ee95ab7e5 --- /dev/null +++ b/iOS_SDK/OneSignalSwiftUIExample/OneSignalWidgetExtension.entitlements @@ -0,0 +1,10 @@ + + + + + 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 new file mode 100644 index 000000000..0f118fb75 --- /dev/null +++ b/iOS_SDK/OneSignalSwiftUIExample/OneSignalWidgetExtension/Info.plist @@ -0,0 +1,11 @@ + + + + + NSExtension + + NSExtensionPointIdentifier + com.apple.widgetkit-extension + + + diff --git a/iOS_SDK/OneSignalSwiftUIExample/OneSignalWidgetExtension/OneSignalWidgetExtension.swift b/iOS_SDK/OneSignalSwiftUIExample/OneSignalWidgetExtension/OneSignalWidgetExtension.swift new file mode 100644 index 000000000..60d247cf3 --- /dev/null +++ b/iOS_SDK/OneSignalSwiftUIExample/OneSignalWidgetExtension/OneSignalWidgetExtension.swift @@ -0,0 +1,43 @@ +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 new file mode 100644 index 000000000..1dd09de51 --- /dev/null +++ b/iOS_SDK/OneSignalSwiftUIExample/OneSignalWidgetExtension/OneSignalWidgetExtensionBundle.swift @@ -0,0 +1,13 @@ +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 new file mode 100644 index 000000000..4d5fbaaa2 --- /dev/null +++ b/iOS_SDK/OneSignalSwiftUIExample/OneSignalWidgetExtension/OneSignalWidgetExtensionLiveActivity.swift @@ -0,0 +1,226 @@ +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 new file mode 100644 index 000000000..1587befd4 --- /dev/null +++ b/iOS_SDK/OneSignalSwiftUIExample/README.md @@ -0,0 +1,153 @@ +# 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 new file mode 100644 index 000000000..35a15cb5f --- /dev/null +++ b/iOS_SDK/OneSignalSwiftUIExample/build_app_prompt.md @@ -0,0 +1,1117 @@ +# 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