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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions examples/demo/App.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

/* Begin PBXBuildFile section */
00B9E4C45782A6AD1CD3FE48 /* OneSignalExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B2F82BD207897EB78739182A /* OneSignalExtension.framework */; };
06E5B0ABA65D52900ECE8DE8 /* LogService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B360D269E2338137D6FD023 /* LogService.swift */; };
08C5E83ABC14EC2FC88276B9 /* OneSignalLiveActivities.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F5EBC48D807036E572D97F0E /* OneSignalLiveActivities.framework */; };
0BC1978B56970258E20C24C4 /* SendPushSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B81D7E1A7EB9BB4466C768F /* SendPushSection.swift */; };
0C4F40193331204CC2E05743 /* OneSignalNotifications.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 283B132CEE12D26D1FA1AADF /* OneSignalNotifications.framework */; };
Expand Down Expand Up @@ -73,6 +74,7 @@
B3AE701075398C6A369DBBE0 /* LocationSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C0EE23773C63B6EC3FB563A /* LocationSection.swift */; };
C31F603E294D69E30CE65EA3 /* OneSignalInAppMessages.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 919F0D129D0FA271E24F0BBD /* OneSignalInAppMessages.framework */; };
C6E9E2C6716059856A1AC571 /* OneSignalInAppMessages.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 919F0D129D0FA271E24F0BBD /* OneSignalInAppMessages.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
C7A9A815CF43E7216A29908D /* LogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F7899AD7E97A0FABAF6E533 /* LogView.swift */; };
CAB90AEC123A57B401881DBC /* OneSignalOSCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 1636BCBAB444EAC7BB34CEDE /* OneSignalOSCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
CE2C54D552E42C2538FB95C4 /* UserSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E2383E8AA63BF04ADA2CF0D /* UserSection.swift */; };
D154E0266C8AB372F928D42A /* OneSignalLocation.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = FE9834773C437CC373607693 /* OneSignalLocation.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
Expand Down Expand Up @@ -314,11 +316,13 @@
1155DE423DAA8B5396948C9B /* TriggersSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TriggersSection.swift; sourceTree = "<group>"; };
1892E9B40F0E8FB23DD64206 /* EmailsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmailsSection.swift; sourceTree = "<group>"; };
1EE449E8308FCB038408D7CF /* vine_boom.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = vine_boom.wav; sourceTree = "<group>"; };
1F7899AD7E97A0FABAF6E533 /* LogView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogView.swift; sourceTree = "<group>"; };
225DEBDFE699D266D5BDE7ED /* AliasesSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AliasesSection.swift; sourceTree = "<group>"; };
24CBCBDE9F48219D7EC827C3 /* App-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "App-Bridging-Header.h"; sourceTree = "<group>"; };
27856D76807C31B23CD10CFD /* LiveActivitySection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivitySection.swift; sourceTree = "<group>"; };
280B23B41935EAB89C8C6FCB /* ListWidgets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListWidgets.swift; sourceTree = "<group>"; };
291D83300C20BA3831824AFD /* OneSignalSDK */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = OneSignalSDK; path = ../../iOS_SDK/OneSignalSDK/OneSignal.xcodeproj; sourceTree = "<group>"; };
2B360D269E2338137D6FD023 /* LogService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogService.swift; sourceTree = "<group>"; };
2D959B1636916DAEE5FE6278 /* CustomNotificationDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomNotificationDialog.swift; sourceTree = "<group>"; };
35F726E64F9B6817F917227C /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
38067B9DDA25E12809D9A245 /* TooltipDialog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TooltipDialog.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -496,6 +500,7 @@
isa = PBXGroup;
children = (
0542854462194E28D9E4638D /* LiveActivityController.swift */,
2B360D269E2338137D6FD023 /* LogService.swift */,
38138523A8A81A60A77800CA /* NotificationSender.swift */,
5E0F5CBE80CF861238E1A9AA /* OneSignalService.swift */,
C76153AD2F6C6EB8F77138B9 /* PreferencesService.swift */,
Expand All @@ -515,6 +520,7 @@
5984B93007C6B85AFE09045A /* KeyValueRow.swift */,
280B23B41935EAB89C8C6FCB /* ListWidgets.swift */,
3DD26AED9EA84B0308C1BFE8 /* LoginUserDialog.swift */,
1F7899AD7E97A0FABAF6E533 /* LogView.swift */,
4072202943CA64D2CBC38CB5 /* MultiPairInputDialog.swift */,
D261D46C404E325CBA87A9E0 /* OSDialog.swift */,
D54B9DAAEDBE67B73893C522 /* OutcomeDialog.swift */,
Expand Down Expand Up @@ -831,6 +837,8 @@
7D2BA9022E77B00205453467 /* LiveActivityController.swift in Sources */,
DBDA727D6317A3CCC73A1699 /* LiveActivitySection.swift in Sources */,
B3AE701075398C6A369DBBE0 /* LocationSection.swift in Sources */,
06E5B0ABA65D52900ECE8DE8 /* LogService.swift in Sources */,
C7A9A815CF43E7216A29908D /* LogView.swift in Sources */,
673E0146B4B27EB81FCF50DA /* LoginUserDialog.swift in Sources */,
E3725231A3FD5F5A88BAA758 /* MultiPairInputDialog.swift in Sources */,
4C18E3D284BB28BD846162F3 /* NotificationSender.swift in Sources */,
Expand Down
22 changes: 13 additions & 9 deletions examples/demo/App/App.swift
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,10 @@ class NotificationLifecycleHandler: NSObject, OSNotificationLifecycleListener {
static let shared = NotificationLifecycleHandler()

func onWillDisplay(event: OSNotificationWillDisplayEvent) {
print("[OneSignal] Notification will display: \(event.notification.title ?? "No title")")
let notification = event.notification
LogService.show(
"Notification foregroundWillDisplay: \(notification.notificationId ?? "null") \(notification.title ?? "")"
)
// Optionally modify display behavior
// event.preventDefault() // Prevent automatic display
// event.notification.display() // Manually display later
Expand All @@ -104,8 +107,10 @@ 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.
let notification = event.notification
LogService.show(
"Notification click: \(notification.notificationId ?? "null") \(notification.title ?? "")"
)
}
}

Expand All @@ -115,27 +120,26 @@ class InAppMessageLifecycleHandler: NSObject, OSInAppMessageLifecycleListener {
static let shared = InAppMessageLifecycleHandler()

func onWillDisplay(event: OSInAppMessageWillDisplayEvent) {
print("[OneSignal] IAM will display: \(event.message.messageId)")
LogService.show("IAM willDisplay: \(event.message.messageId)")
}

func onDidDisplay(event: OSInAppMessageDidDisplayEvent) {
print("[OneSignal] IAM did display: \(event.message.messageId)")
LogService.show("IAM didDisplay: \(event.message.messageId)")
}

func onWillDismiss(event: OSInAppMessageWillDismissEvent) {
print("[OneSignal] IAM will dismiss: \(event.message.messageId)")
LogService.show("IAM willDismiss: \(event.message.messageId)")
}

func onDidDismiss(event: OSInAppMessageDidDismissEvent) {
print("[OneSignal] IAM did dismiss: \(event.message.messageId)")
LogService.show("IAM didDismiss: \(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.
LogService.show("IAM click: \(event.message.messageId)")
}
}
93 changes: 93 additions & 0 deletions examples/demo/App/Services/LogService.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/**
* Modified MIT License
*
* Copyright 2026 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

struct LogEntry: Identifiable, Equatable {
let id: Int
let time: String
let message: String
}

/// Demo logging that marks every console line with `[OneSignal]`.
///
/// `show` and `error` also append to the in-app Log View, which the shared Appium suite reads.
/// All methods are safe to call from any thread.
final class LogService: ObservableObject {
static let shared = LogService()

private static let maxEntries = 100

// In memory only, so every launch (including a cold start from a notification tap) starts empty.
@Published private(set) var entries: [LogEntry] = []
private var nextId = 0

private let timeFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.dateFormat = "HH:mm:ss"
return formatter
}()

private init() {}

static func info(_ message: String) {
print("[OneSignal] \(message)")
}

static func show(_ message: String) {
info(message)
shared.addToView(message)
}

static func error(_ message: String) {
FileHandle.standardError.write(Data("[OneSignal] \(message)\n".utf8))
shared.addToView(message)
}
Comment thread
fadi-george marked this conversation as resolved.

func clear() {
entries.removeAll()
}

private func addToView(_ message: String) {
let time = timeFormatter.string(from: Date())
// Append inline on main so a queued append can't land after a later clear().
guard Thread.isMainThread else {
DispatchQueue.main.async { self.append(time: time, message: message) }
return
}
append(time: time, message: message)
}

private func append(time: String, message: String) {
entries.append(LogEntry(id: nextId, time: time, message: message))
nextId += 1
if entries.count > Self.maxEntries {
entries.removeFirst(entries.count - Self.maxEntries)
}
}
}
35 changes: 26 additions & 9 deletions examples/demo/App/ViewModels/OneSignalViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ final class OneSignalViewModel: ObservableObject {
userFetchStatus = "OK"
case .failure(let error):
userFetchStatus = "\(aliasLabel) fetch failed: \(error)"
print("[OneSignal] /users fetch by \(aliasLabel) failed: \(error)")
LogService.info("/users fetch by \(aliasLabel) failed: \(error)")
}
isLoading = false
}
Expand Down Expand Up @@ -236,7 +236,7 @@ final class OneSignalViewModel: ObservableObject {
if jwtAskExternalId == trimmedId {
jwtAskExternalId = nil
}
print("[OneSignal] Updated JWT for: \(trimmedId)")
LogService.info("Updated JWT for: \(trimmedId)")
if currentUser {
// The demo now holds a fresh bearer for this user, and no user-state
// event is guaranteed, so refetch instead of leaving the stale failure.
Expand All @@ -248,7 +248,7 @@ final class OneSignalViewModel: ObservableObject {
func handleJwtInvalidated(externalId: String) {
service.clearSessionJwtToken()
jwtAskExternalId = externalId
print("[OneSignal] JWT invalidated for externalId: \(externalId)")
LogService.info("JWT invalidated for externalId: \(externalId)")
}

func logout() {
Expand All @@ -267,7 +267,7 @@ final class OneSignalViewModel: ObservableObject {
func setUseIdentityVerification(_ enabled: Bool) {
useIdentityVerification = enabled
service.useIdentityVerification = enabled
print("[OneSignal] Fetch by external_id \(enabled ? "enabled" : "disabled")")
LogService.info("Fetch by external_id \(enabled ? "enabled" : "disabled")")
Task { await fetchUserDataFromApi() }
}

Expand Down Expand Up @@ -387,7 +387,7 @@ final class OneSignalViewModel: ObservableObject {

func checkLocationShared() -> Bool {
let shared = service.isLocationShared
print("[OneSignal] Location shared: \(shared)")
LogService.info("Location shared: \(shared)")
return shared
}

Expand Down Expand Up @@ -425,17 +425,17 @@ extension OneSignalViewModel {

func sendOutcome(_ name: String) {
service.sendOutcome(name)
print("[OneSignal] Outcome sent: \(name)")
LogService.info("Outcome sent: \(name)")
}

func sendUniqueOutcome(_ name: String) {
service.sendUniqueOutcome(name)
print("[OneSignal] Unique outcome sent: \(name)")
LogService.info("Unique outcome sent: \(name)")
}

func sendOutcome(_ name: String, value: Double) {
service.sendOutcome(name, value: NSNumber(value: value))
print("[OneSignal] Outcome sent: \(name) = \(value)")
LogService.info("Outcome sent: \(name) = \(value)")
}

// MARK: - In-App
Expand Down Expand Up @@ -488,7 +488,7 @@ extension OneSignalViewModel {

func trackEvent(name: String, properties: [String: Any]?) {
service.trackEvent(name: name, properties: properties)
print("[OneSignal] Event tracked: \(name)")
LogService.info("Event tracked: \(name)")
}

// MARK: - Notifications
Expand Down Expand Up @@ -557,19 +557,31 @@ private final class Observers: NSObject,
weak var viewModel: OneSignalViewModel?

func onPushSubscriptionDidChange(state: OSPushSubscriptionChangedState) {
let previous = state.previous
let current = state.current
LogService.show(
"Push subscription changed: id=\(previous.id ?? "null") → \(current.id ?? "null"), "
+ "optedIn=\(previous.optedIn) → \(current.optedIn), "
+ "token=\(Self.truncatedToken(previous.token)) → \(Self.truncatedToken(current.token))"
)
Task { @MainActor in
viewModel?.pushSubscriptionId = state.current.id
viewModel?.isPushEnabled = state.current.optedIn
}
}

func onUserStateDidChange(state: OSUserChangedState) {
LogService.show(
"User changed: onesignalId=\(state.current.onesignalId ?? "null"), "
+ "externalId=\(state.current.externalId ?? "null")"
)
Task { @MainActor in
await viewModel?.fetchUserDataFromApi()
}
}

func onNotificationPermissionDidChange(_ permission: Bool) {
LogService.show("Permission changed: \(permission)")
Task { @MainActor in
viewModel?.hasNotificationPermission = permission
viewModel?.isPushEnabled = OneSignal.User.pushSubscription.optedIn
Expand All @@ -581,4 +593,9 @@ private final class Observers: NSObject,
viewModel?.handleJwtInvalidated(externalId: event.externalId)
}
}

private static func truncatedToken(_ token: String?) -> String {
guard let token, !token.isEmpty else { return "null" }
return "\(token.prefix(8))…"
}
}
Loading
Loading