Skip to content

Commit 1a76916

Browse files
committed
Add custom events feature
1 parent adbf5a5 commit 1a76916

File tree

4 files changed

+141
-0
lines changed

4 files changed

+141
-0
lines changed

iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@
9191
3C6299A92BEEA46C00649187 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 3C6299A82BEEA46C00649187 /* PrivacyInfo.xcprivacy */; };
9292
3C6299AB2BEEA4C000649187 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 3C6299AA2BEEA4C000649187 /* PrivacyInfo.xcprivacy */; };
9393
3C67F77A2BEB2B710085A0F0 /* SwitchUserIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C67F7792BEB2B710085A0F0 /* SwitchUserIntegrationTests.swift */; };
94+
3C68EFE52D93195E00F0896B /* OSCustomEventsExecutor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C68EFE42D93195E00F0896B /* OSCustomEventsExecutor.swift */; };
95+
3C68EFE72D931BA600F0896B /* OSRequestCustomEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C68EFE62D931BA600F0896B /* OSRequestCustomEvents.swift */; };
9496
3C70FA672D0B68A100031066 /* OneSignalClientError.h in Headers */ = {isa = PBXBuildFile; fileRef = 3C70FA652D0B68A100031066 /* OneSignalClientError.h */; settings = {ATTRIBUTES = (Public, ); }; };
9597
3C70FA682D0B68A100031066 /* OneSignalClientError.m in Sources */ = {isa = PBXBuildFile; fileRef = 3C70FA662D0B68A100031066 /* OneSignalClientError.m */; };
9698
3C789DBD293C2206004CF83D /* OSFocusInfluenceParam.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A600B432453790700514A53 /* OSFocusInfluenceParam.m */; };
@@ -1265,6 +1267,8 @@
12651267
3C6299A82BEEA46C00649187 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
12661268
3C6299AA2BEEA4C000649187 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
12671269
3C67F7792BEB2B710085A0F0 /* SwitchUserIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwitchUserIntegrationTests.swift; sourceTree = "<group>"; };
1270+
3C68EFE42D93195E00F0896B /* OSCustomEventsExecutor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSCustomEventsExecutor.swift; sourceTree = "<group>"; };
1271+
3C68EFE62D931BA600F0896B /* OSRequestCustomEvents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSRequestCustomEvents.swift; sourceTree = "<group>"; };
12681272
3C70FA652D0B68A100031066 /* OneSignalClientError.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OneSignalClientError.h; sourceTree = "<group>"; };
12691273
3C70FA662D0B68A100031066 /* OneSignalClientError.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OneSignalClientError.m; sourceTree = "<group>"; };
12701274
3C7A39D42B7C18EE0082665E /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Platforms/iPhoneOS.platform/Developer/Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; };
@@ -2140,6 +2144,7 @@
21402144
isa = PBXGroup;
21412145
children = (
21422146
3C8E6E0028AC0BA10031E48A /* OSIdentityOperationExecutor.swift */,
2147+
3C68EFE42D93195E00F0896B /* OSCustomEventsExecutor.swift */,
21432148
3C8E6DFE28AB09AE0031E48A /* OSPropertyOperationExecutor.swift */,
21442149
3CE795FA28DBDCE700736BD4 /* OSSubscriptionOperationExecutor.swift */,
21452150
3C9AD6BB2B2285FB00BC1540 /* OSUserExecutor.swift */,
@@ -2162,6 +2167,7 @@
21622167
3C9AD6C62B228A9800BC1540 /* OSRequestTransferSubscription.swift */,
21632168
3C9AD6C02B22886600BC1540 /* OSRequestUpdateSubscription.swift */,
21642169
3C9AD6C42B228A7300BC1540 /* OSRequestDeleteSubscription.swift */,
2170+
3C68EFE62D931BA600F0896B /* OSRequestCustomEvents.swift */,
21652171
);
21662172
path = Requests;
21672173
sourceTree = "<group>";
@@ -4394,8 +4400,10 @@
43944400
3C277D7E2BD76E0000857606 /* OSIdentityModelRepo.swift in Sources */,
43954401
3CEE90A72BFE6ABD00B0FB5B /* OSPropertiesSupportedProperty.swift in Sources */,
43964402
3C9AD6C12B22886600BC1540 /* OSRequestUpdateSubscription.swift in Sources */,
4403+
3C68EFE72D931BA600F0896B /* OSRequestCustomEvents.swift in Sources */,
43974404
3C0EF49E28A1DBCB00E5434B /* OSUserInternalImpl.swift in Sources */,
43984405
3C8E6DFF28AB09AE0031E48A /* OSPropertyOperationExecutor.swift in Sources */,
4406+
3C68EFE52D93195E00F0896B /* OSCustomEventsExecutor.swift in Sources */,
43994407
3C9AD6CB2B228B5200BC1540 /* OSRequestIdentifyUser.swift in Sources */,
44004408
3C9AD6BC2B2285FB00BC1540 /* OSUserExecutor.swift in Sources */,
44014409
3C9AD6C32B22887700BC1540 /* OSRequestCreateUser.swift in Sources */,

iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCommonDefines.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,7 @@ typedef enum {ATTRIBUTED, NOT_ATTRIBUTED} FocusAttributionState;
203203
#define IDENTITY_EXECUTOR_BACKGROUND_TASK @"IDENTITY_EXECUTOR_BACKGROUND_TASK_"
204204
#define PROPERTIES_EXECUTOR_BACKGROUND_TASK @"PROPERTIES_EXECUTOR_BACKGROUND_TASK_"
205205
#define SUBSCRIPTION_EXECUTOR_BACKGROUND_TASK @"SUBSCRIPTION_EXECUTOR_BACKGROUND_TASK_"
206+
#define CUSTOM_EVENTS_EXECUTOR_BACKGROUND_TASK @"CUSTOM_EVENTS_EXECUTOR_BACKGROUND_TASK_"
206207

207208
// OneSignal constants
208209
#define OS_PUSH @"push"
@@ -337,6 +338,8 @@ typedef enum {GET, POST, HEAD, PUT, DELETE, OPTIONS, CONNECT, TRACE, PATCH} HTTP
337338
#define OS_REMOVE_SUBSCRIPTION_DELTA @"OS_REMOVE_SUBSCRIPTION_DELTA"
338339
#define OS_UPDATE_SUBSCRIPTION_DELTA @"OS_UPDATE_SUBSCRIPTION_DELTA"
339340

341+
#define OS_CUSTOM_EVENT_DELTA @"OS_CUSTOM_EVENT_DELTA"
342+
340343
// Operation Repo
341344
#define OS_OPERATION_REPO_DELTA_QUEUE_KEY @"OS_OPERATION_REPO_DELTA_QUEUE_KEY"
342345

@@ -359,6 +362,10 @@ typedef enum {GET, POST, HEAD, PUT, DELETE, OPTIONS, CONNECT, TRACE, PATCH} HTTP
359362
#define OS_SUBSCRIPTION_EXECUTOR_REMOVE_REQUEST_QUEUE_KEY @"OS_SUBSCRIPTION_EXECUTOR_REMOVE_REQUEST_QUEUE_KEY"
360363
#define OS_SUBSCRIPTION_EXECUTOR_UPDATE_REQUEST_QUEUE_KEY @"OS_SUBSCRIPTION_EXECUTOR_UPDATE_REQUEST_QUEUE_KEY"
361364

365+
// Custom Events Executor
366+
#define OS_CUSTOM_EVENTS_EXECUTOR_DELTA_QUEUE_KEY @"OS_CUSTOM_EVENTS_EXECUTOR_DELTA_QUEUE_KEY"
367+
#define OS_CUSTOM_EVENTS_EXECUTOR_REQUEST_QUEUE_KEY @"OS_CUSTOM_EVENTS_EXECUTOR_REQUEST_QUEUE_KEY"
368+
362369
// Live Activies Executor
363370
#define OS_LIVE_ACTIVITIES_EXECUTOR_UPDATE_TOKENS_KEY @"OS_LIVE_ACTIVITIES_EXECUTOR_UPDATE_TOKENS_KEY"
364371
#define OS_LIVE_ACTIVITIES_EXECUTOR_START_TOKENS_KEY @"OS_LIVE_ACTIVITIES_EXECUTOR_START_TOKENS_KEY"

iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,15 @@ import OneSignalNotifications
7575
func removeSms(_ number: String)
7676
// Language
7777
func setLanguage(_ language: String)
78+
// Events
79+
/**
80+
Track an event performed by the current user.
81+
- Parameters:
82+
- name: Name of the event, e.g., 'Started Free Trial'
83+
- properties: Optional properties specific to the event. For example, an event with the name 'Started Free Trial' might have properties like promo code used or expiration date.
84+
*/
85+
func trackEvent(name: String, properties: [String: Any]?)
86+
// ^ TODO: After alpha feedback, confirm value type for properties dict
7887
// JWT Token Expire
7988
typealias OSJwtCompletionBlock = (_ newJwtToken: String) -> Void
8089
typealias OSJwtExpiredHandler = (_ externalId: String, _ completion: OSJwtCompletionBlock) -> Void
@@ -183,6 +192,7 @@ public class OneSignalUserManagerImpl: NSObject, OneSignalUserManager {
183192
var propertyExecutor: OSPropertyOperationExecutor?
184193
var identityExecutor: OSIdentityOperationExecutor?
185194
var subscriptionExecutor: OSSubscriptionOperationExecutor?
195+
var customEventsExecutor: OSCustomEventsExecutor?
186196

187197
private override init() {
188198
self.identityModelStoreListener = OSIdentityModelStoreListener(store: identityModelStore)
@@ -231,12 +241,15 @@ public class OneSignalUserManagerImpl: NSObject, OneSignalUserManager {
231241
let propertyExecutor = OSPropertyOperationExecutor(newRecordsState: newRecordsState)
232242
let identityExecutor = OSIdentityOperationExecutor(newRecordsState: newRecordsState)
233243
let subscriptionExecutor = OSSubscriptionOperationExecutor(newRecordsState: newRecordsState)
244+
let customEventsExecutor = OSCustomEventsExecutor(newRecordsState: newRecordsState)
234245
self.propertyExecutor = propertyExecutor
235246
self.identityExecutor = identityExecutor
236247
self.subscriptionExecutor = subscriptionExecutor
248+
self.customEventsExecutor = customEventsExecutor
237249
OSOperationRepo.sharedInstance.addExecutor(identityExecutor)
238250
OSOperationRepo.sharedInstance.addExecutor(propertyExecutor)
239251
OSOperationRepo.sharedInstance.addExecutor(subscriptionExecutor)
252+
OSOperationRepo.sharedInstance.addExecutor(customEventsExecutor)
240253

241254
// Path 2. There is a legacy player to migrate
242255
if let legacyPlayerId = OneSignalUserDefaults.initShared().getSavedString(forKey: OSUD_LEGACY_PLAYER_ID, defaultValue: nil) {
@@ -795,6 +808,32 @@ extension OneSignalUserManagerImpl: OSUser {
795808

796809
user.setLanguage(language)
797810
}
811+
812+
public func trackEvent(name: String, properties: [String: Any]?) {
813+
guard !OneSignalConfigManager.shouldAwaitAppIdAndLogMissingPrivacyConsent(forMethod: "trackEvent") else {
814+
return
815+
}
816+
817+
let processedProperties = properties ?? [:]
818+
819+
// Make sure the properties are serializable as JSON object
820+
guard JSONSerialization.isValidJSONObject(processedProperties) else {
821+
OneSignalLog.onesignalLog(.LL_ERROR, message: "trackEvent called with invalid properties \(processedProperties), dropping this event.")
822+
return
823+
}
824+
825+
// Get the identity model of the current user
826+
let identityModel = user.identityModel
827+
828+
let delta = OSDelta(
829+
name: OS_CUSTOM_EVENT_DELTA,
830+
identityModelId: identityModel.modelId,
831+
model: identityModel,
832+
property: name,
833+
value: processedProperties
834+
)
835+
OSOperationRepo.sharedInstance.enqueueDelta(delta)
836+
}
798837
}
799838

800839
extension OneSignalUserManagerImpl {
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/*
2+
Modified MIT License
3+
4+
Copyright 2025 OneSignal
5+
6+
Permission is hereby granted, free of charge, to any person obtaining a copy
7+
of this software and associated documentation files (the "Software"), to deal
8+
in the Software without restriction, including without limitation the rights
9+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
copies of the Software, and to permit persons to whom the Software is
11+
furnished to do so, subject to the following conditions:
12+
13+
1. The above copyright notice and this permission notice shall be included in
14+
all copies or substantial portions of the Software.
15+
16+
2. All copies of substantial portions of the Software may only be used in connection
17+
with services provided by OneSignal.
18+
19+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25+
THE SOFTWARE.
26+
*/
27+
28+
import OneSignalCore
29+
import OneSignalOSCore
30+
31+
class OSRequestCustomEvents: OneSignalRequest, OSUserRequest {
32+
var sentToClient = false
33+
let stringDescription: String
34+
override var description: String {
35+
return stringDescription
36+
}
37+
38+
var identityModel: OSIdentityModel
39+
40+
func prepareForExecution(newRecordsState: OSNewRecordsState) -> Bool {
41+
if let onesignalId = identityModel.onesignalId,
42+
newRecordsState.canAccess(onesignalId),
43+
let appId = OneSignalConfigManager.getAppId()
44+
{
45+
_ = self.addPushSubscriptionIdToAdditionalHeaders()
46+
self.path = "apps/\(appId)/integrations/custom_events"
47+
return true
48+
} else {
49+
return false
50+
}
51+
}
52+
53+
init(events: [[String: Any]], identityModel: OSIdentityModel) {
54+
self.identityModel = identityModel
55+
self.stringDescription = "<OSRequestCustomEvents with events: \(events)>"
56+
super.init()
57+
self.parameters = [
58+
"events": events
59+
]
60+
self.method = POST
61+
}
62+
63+
func encode(with coder: NSCoder) {
64+
coder.encode(identityModel, forKey: "identityModel")
65+
coder.encode(parameters, forKey: "parameters")
66+
coder.encode(method.rawValue, forKey: "method") // Encodes as String
67+
coder.encode(timestamp, forKey: "timestamp")
68+
}
69+
70+
required init?(coder: NSCoder) {
71+
guard
72+
let identityModel = coder.decodeObject(forKey: "identityModel") as? OSIdentityModel,
73+
let rawMethod = coder.decodeObject(forKey: "method") as? UInt32,
74+
let parameters = coder.decodeObject(forKey: "parameters") as? [String: Any],
75+
let timestamp = coder.decodeObject(forKey: "timestamp") as? Date
76+
else {
77+
// Log error
78+
return nil
79+
}
80+
self.identityModel = identityModel
81+
self.stringDescription = "<OSRequestCustomEvents with parameters: \(parameters)>"
82+
super.init()
83+
self.parameters = parameters
84+
self.method = HTTPMethod(rawValue: rawMethod)
85+
self.timestamp = timestamp
86+
}
87+
}

0 commit comments

Comments
 (0)