Manos Chorianopoulos

Push Notifications implementation

...@@ -1249,35 +1249,128 @@ class LegacyViewController: UIViewController { ...@@ -1249,35 +1249,128 @@ class LegacyViewController: UIViewController {
1249 1249
1250 ## 📱 Push Notifications 1250 ## 📱 Push Notifications
1251 1251
1252 -### Setup 1252 +### Complete AppDelegate Setup
1253 +
1254 +The framework provides full push notification support. Add these methods to your `AppDelegate.swift`:
1253 1255
1254 ```swift 1256 ```swift
1255 -// In AppDelegate 1257 +import SwiftWarplyFramework
1256 -func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { 1258 +import UserNotifications
1257 - let tokenString = deviceToken.map { String(format: "%02.2hhx", $0) }.joined() 1259 +
1258 - WarplySDK.shared.updateDeviceToken(tokenString) 1260 +@main
1261 +class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate {
1262 +
1263 + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
1264 +
1265 + // 1️⃣ Configure and initialize SDK (as shown in Setup section above)
1266 + WarplySDK.shared.configure(appUuid: "YOUR_APP_UUID", merchantId: "YOUR_MERCHANT_ID")
1267 + WarplySDK.shared.initialize { success in
1268 + print("SDK Ready: \(success)")
1269 + }
1270 +
1271 + // 2️⃣ Check if app was launched from a push notification
1272 + WarplySDK.shared.handleLaunchOptions(launchOptions)
1273 +
1274 + // 3️⃣ Register for push notifications
1275 + UNUserNotificationCenter.current().delegate = self
1276 + UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { granted, error in
1277 + if granted {
1278 + DispatchQueue.main.async {
1279 + UIApplication.shared.registerForRemoteNotifications()
1280 + }
1281 + }
1282 + }
1283 +
1284 + return true
1285 + }
1286 +
1287 + // 4️⃣ Send device token to Warply server
1288 + func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
1289 + let tokenString = deviceToken.map { String(format: "%02.2hhx", $0) }.joined()
1290 + WarplySDK.shared.updateDeviceToken(tokenString)
1291 + }
1292 +
1293 + // 5️⃣ Forward foreground notifications to SDK
1294 + func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
1295 + let userInfo = notification.request.content.userInfo as? [String: Any] ?? [:]
1296 +
1297 + if WarplySDK.shared.checkForLoyaltySDKNotification(userInfo) {
1298 + // Warply handles it (shows alert in foreground)
1299 + completionHandler([])
1300 + } else {
1301 + // Not a Warply notification — show normally
1302 + completionHandler([.banner, .sound, .badge])
1303 + }
1304 + }
1305 +
1306 + // 6️⃣ Forward notification taps to SDK
1307 + func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
1308 + let userInfo = response.notification.request.content.userInfo as? [String: Any] ?? [:]
1309 +
1310 + // Let the SDK handle Warply notifications (opens campaign WebView)
1311 + _ = WarplySDK.shared.checkForLoyaltySDKNotification(userInfo)
1312 +
1313 + completionHandler()
1314 + }
1259 } 1315 }
1316 +```
1260 1317
1261 -func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any]) { 1318 +### Handle Pending Notifications
1262 - // Check if it's a Warply notification 1319 +
1263 - if WarplySDK.shared.checkForLoyaltySDKNotification(userInfo) { 1320 +When the app is launched from a push notification (cold start), call this in your root view controller:
1264 - print("Warply notification handled") 1321 +
1265 - } else { 1322 +```swift
1266 - // Handle other notifications 1323 +class MainViewController: UIViewController {
1324 + override func viewDidAppear(_ animated: Bool) {
1325 + super.viewDidAppear(animated)
1326 +
1327 + // Handle any pending push notification from cold start
1328 + WarplySDK.shared.handlePendingNotification()
1267 } 1329 }
1268 } 1330 }
1269 ``` 1331 ```
1270 1332
1271 -### Handle Notifications 1333 +### Optional — Clear Badge on App Open
1334 +
1335 +The **badge** is the small red number circle on your app icon (e.g., showing "3" for 3 unread notifications). Call `resetBadge()` to clear it when the user opens the app:
1272 1336
1273 ```swift 1337 ```swift
1274 -// Manual notification handling 1338 +// In AppDelegate — clear the badge when user opens the app
1275 -WarplySDK.shared.handleNotification(notificationPayload) 1339 +func applicationDidBecomeActive(_ application: UIApplication) {
1340 + WarplySDK.shared.resetBadge()
1341 +}
1342 +```
1343 +
1344 +### Optional — Listen for Push Events
1276 1345
1277 -// Check if notification belongs to Warply SDK 1346 +If you want to react to push notifications in your own code (in addition to the SDK's automatic handling):
1278 -let isWarplyNotification = WarplySDK.shared.checkForLoyaltySDKNotification(payload) 1347 +
1348 +```swift
1349 +// Modern EventDispatcher approach
1350 +let subscription = WarplySDK.shared.subscribe(PushNotificationReceivedEvent.self) { event in
1351 + print("Push received! Session: \(event.sessionUuid ?? "unknown")")
1352 +}
1353 +```
1354 +
1355 +### Advanced — Custom Push Handler (Most apps don't need this)
1356 +
1357 +By default, the SDK handles all Warply push notifications automatically (shows a campaign WebView). However, if the Warply backend sends pushes with a **custom action code** (`action != 0`), you can implement a handler to process them:
1358 +
1359 +```swift
1360 +class MyPushHandler: WarplyPushHandler {
1361 + func didReceiveWarplyNotification(_ payload: WarplyNotificationPayload, whileAppWasIn state: WarplyApplicationState) {
1362 + // This is only called for pushes with action != 0
1363 + // Standard offer pushes (action == 0) are handled automatically by the SDK
1364 + print("Custom action push: \(payload.action)")
1365 + }
1366 +}
1367 +
1368 +// Set the delegate
1369 +WarplySDK.shared.pushHandlerDelegate = myPushHandler
1279 ``` 1370 ```
1280 1371
1372 +> **Note**: You only need this if the Warply/DEI backend team has told you they send custom action pushes. For standard offer notifications, the SDK handles everything automatically.
1373 +
1281 --- 1374 ---
1282 1375
1283 ## ⚠️ Error Handling 1376 ## ⚠️ Error Handling
......
This diff is collapsed. Click to expand it.
...@@ -177,18 +177,46 @@ Task { ...@@ -177,18 +177,46 @@ Task {
177 177
178 ## 📱 Push Notifications 178 ## 📱 Push Notifications
179 179
180 +### Required Setup (AppDelegate)
181 +
180 ```swift 182 ```swift
181 -// AppDelegate 183 +// 1️⃣ In didFinishLaunchingWithOptions — check if app launched from push
184 +WarplySDK.shared.handleLaunchOptions(launchOptions)
185 +
186 +// 2️⃣ Send device token to server
182 func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { 187 func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
183 let tokenString = deviceToken.map { String(format: "%02.2hhx", $0) }.joined() 188 let tokenString = deviceToken.map { String(format: "%02.2hhx", $0) }.joined()
184 WarplySDK.shared.updateDeviceToken(tokenString) 189 WarplySDK.shared.updateDeviceToken(tokenString)
185 } 190 }
186 191
187 -func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any]) { 192 +// 3️⃣ Forward foreground notifications (UNUserNotificationCenterDelegate)
193 +func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, ...) {
194 + let userInfo = notification.request.content.userInfo as? [String: Any] ?? [:]
188 if WarplySDK.shared.checkForLoyaltySDKNotification(userInfo) { 195 if WarplySDK.shared.checkForLoyaltySDKNotification(userInfo) {
189 - // Warply notification handled 196 + completionHandler([]) // SDK handles it (shows alert)
197 + } else {
198 + completionHandler([.banner, .sound, .badge]) // Not a Warply push
190 } 199 }
191 } 200 }
201 +
202 +// 4️⃣ Forward notification taps (UNUserNotificationCenterDelegate)
203 +func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, ...) {
204 + let userInfo = response.notification.request.content.userInfo as? [String: Any] ?? [:]
205 + _ = WarplySDK.shared.checkForLoyaltySDKNotification(userInfo)
206 + completionHandler()
207 +}
208 +
209 +// 5️⃣ Handle pending notifications (in root ViewController's viewDidAppear)
210 +WarplySDK.shared.handlePendingNotification()
211 +```
212 +
213 +### Optional — Clear Badge on App Open
214 +
215 +```swift
216 +// In applicationDidBecomeActive — clears the red number on the app icon
217 +func applicationDidBecomeActive(_ application: UIApplication) {
218 + WarplySDK.shared.resetBadge()
219 +}
192 ``` 220 ```
193 221
194 --- 222 ---
......
...@@ -226,16 +226,50 @@ public struct SeasonalsRetrievedEvent: WarplyEvent { ...@@ -226,16 +226,50 @@ public struct SeasonalsRetrievedEvent: WarplyEvent {
226 226
227 /// Coupons fetched event 227 /// Coupons fetched event
228 public struct CouponsRetrievedEvent: WarplyEvent { 228 public struct CouponsRetrievedEvent: WarplyEvent {
229 - public let name: String = "coupons_fetched" 229 +public let name: String = "coupons_fetched"
230 public let timestamp: Date 230 public let timestamp: Date
231 public let data: Any? 231 public let data: Any?
232 - 232 +
233 public init(data: Any? = nil) { 233 public init(data: Any? = nil) {
234 self.timestamp = Date() 234 self.timestamp = Date()
235 self.data = data 235 self.data = data
236 } 236 }
237 } 237 }
238 238
239 +/// Event posted when a Warply push notification is received
240 +/// Equivalent of old ObjC `NB_PushReceived` analytics event
241 +public struct PushNotificationReceivedEvent: WarplyEvent {
242 + public let name: String = "push_notification_received"
243 + public let timestamp: Date
244 + public let data: Any?
245 +
246 + /// The session UUID from the push notification payload
247 + public let sessionUuid: String?
248 +
249 + public init(data: Any? = nil, sessionUuid: String? = nil) {
250 + self.timestamp = Date()
251 + self.data = data
252 + self.sessionUuid = sessionUuid
253 + }
254 +}
255 +
256 +/// Event posted when a user interacts with (engages) a Warply push notification
257 +/// Equivalent of old ObjC `NB_PushAck` analytics event
258 +public struct PushNotificationEngagedEvent: WarplyEvent {
259 + public let name: String = "push_notification_engaged"
260 + public let timestamp: Date
261 + public let data: Any?
262 +
263 + /// The session UUID from the push notification payload
264 + public let sessionUuid: String?
265 +
266 + public init(data: Any? = nil, sessionUuid: String? = nil) {
267 + self.timestamp = Date()
268 + self.data = data
269 + self.sessionUuid = sessionUuid
270 + }
271 +}
272 +
239 // MARK: - Convenience Extensions 273 // MARK: - Convenience Extensions
240 274
241 extension EventDispatcher { 275 extension EventDispatcher {
...@@ -275,4 +309,20 @@ extension EventDispatcher { ...@@ -275,4 +309,20 @@ extension EventDispatcher {
275 public func postCouponsRetrievedEvent(sender: Any? = nil) { 309 public func postCouponsRetrievedEvent(sender: Any? = nil) {
276 post(CouponsRetrievedEvent(data: sender)) 310 post(CouponsRetrievedEvent(data: sender))
277 } 311 }
312 +
313 + /// Post a push notification received event
314 + /// - Parameters:
315 + /// - sender: The event data (typically the raw push payload)
316 + /// - sessionUuid: The session UUID from the push notification
317 + public func postPushNotificationReceivedEvent(sender: Any? = nil, sessionUuid: String? = nil) {
318 + post(PushNotificationReceivedEvent(data: sender, sessionUuid: sessionUuid))
319 + }
320 +
321 + /// Post a push notification engaged event
322 + /// - Parameters:
323 + /// - sender: The event data (typically the raw push payload)
324 + /// - sessionUuid: The session UUID from the push notification
325 + public func postPushNotificationEngagedEvent(sender: Any? = nil, sessionUuid: String? = nil) {
326 + post(PushNotificationEngagedEvent(data: sender, sessionUuid: sessionUuid))
327 + }
278 } 328 }
......
...@@ -97,7 +97,7 @@ public enum Endpoint { ...@@ -97,7 +97,7 @@ public enum Endpoint {
97 case sendEvent(eventName: String, priority: Bool) 97 case sendEvent(eventName: String, priority: Bool)
98 98
99 // Device 99 // Device
100 - case sendDeviceInfo(deviceToken: String) 100 + case sendDeviceInfo(deviceInfo: [String: Any])
101 101
102 // Network status 102 // Network status
103 case getNetworkStatus 103 case getNetworkStatus
...@@ -428,13 +428,10 @@ public enum Endpoint { ...@@ -428,13 +428,10 @@ public enum Endpoint {
428 ] 428 ]
429 ] 429 ]
430 430
431 - // Device Info endpoints - device structure 431 + // Device Info endpoints - comprehensive device info structure
432 - case .sendDeviceInfo(let deviceToken): 432 + // Matches old ObjC WLPushManager.deviceInfo + WLPushManager.applicationData
433 - return [ 433 + case .sendDeviceInfo(let deviceInfo):
434 - "device": [ 434 + return deviceInfo
435 - "device_token": deviceToken
436 - ]
437 - ]
438 435
439 // Network status - no body needed 436 // Network status - no body needed
440 case .getNetworkStatus: 437 case .getNetworkStatus:
......
...@@ -860,14 +860,16 @@ extension NetworkService { ...@@ -860,14 +860,16 @@ extension NetworkService {
860 } 860 }
861 } 861 }
862 862
863 - /// Update device token 863 + /// Send comprehensive device info to server
864 - public func updateDeviceToken(_ deviceToken: String) async { 864 + /// - Parameter deviceInfo: Full device info dictionary (built by WarplySDK.buildDeviceInfo())
865 - let endpoint = Endpoint.sendDeviceInfo(deviceToken: deviceToken) 865 + public func sendDeviceInfo(_ deviceInfo: [String: Any]) async {
866 + let endpoint = Endpoint.sendDeviceInfo(deviceInfo: deviceInfo)
866 867
867 do { 868 do {
868 _ = try await requestRaw(endpoint) 869 _ = try await requestRaw(endpoint)
870 + print("✅ [NetworkService] Device info sent to server successfully")
869 } catch { 871 } catch {
870 - print("Failed to update device token: \(error)") 872 + print("❌ [NetworkService] Failed to send device info: \(error)")
871 } 873 }
872 } 874 }
873 875
......
...@@ -496,6 +496,71 @@ self.postFrameworkEvent("dynatrace", sender: dynatraceEvent) ...@@ -496,6 +496,71 @@ self.postFrameworkEvent("dynatrace", sender: dynatraceEvent)
496 ``` 496 ```
497 Event naming convention: `custom_{success|error}_{operation}_loyalty` 497 Event naming convention: `custom_{success|error}_{operation}_loyalty`
498 498
499 +## 9.5 Push Notification System
500 +
501 +### Architecture (Option A: Host App Forwards)
502 +
503 +The framework does **NOT** set itself as `UNUserNotificationCenterDelegate`. Instead, the host app owns the delegate and forwards calls to the SDK. This avoids conflicts with Firebase, OneSignal, etc.
504 +
505 +### Push Notification Types
506 +
507 +| Type | Location | Purpose |
508 +|------|----------|---------|
509 +| `WarplyApplicationState` | `WarplySDK.swift` | Enum: `.active`, `.background`, `.closed` |
510 +| `WarplyPushHandler` | `WarplySDK.swift` | Protocol for custom push handling (action != 0) |
511 +| `WarplyNotificationPayload` | `WarplySDK.swift` | Parsed push payload (action, sessionUuid, message, aps, customData) |
512 +| `WarplyAPSItem` | `WarplySDK.swift` | Parsed APS dictionary (alert, badge, sound, category) |
513 +| `PushNotificationReceivedEvent` | `EventDispatcher.swift` | Event posted when push received |
514 +| `PushNotificationEngagedEvent` | `EventDispatcher.swift` | Event posted when user interacts with push |
515 +
516 +### Push Notification Detection
517 +
518 +Warply push notifications are identified by the `"_a"` key in the payload:
519 +```json
520 +{"_a": 0, "session_uuid": "abc-123", "aps": {"alert": "New offer!", "badge": 1}}
521 +```
522 +- `"_a" == 0`: Default handling — framework presents `CampaignViewController` WebView
523 +- `"_a" != 0`: Custom action — forwarded to `pushHandlerDelegate`
524 +
525 +### Push Notification Methods
526 +
527 +| Method | Purpose |
528 +|--------|---------|
529 +| `updateDeviceToken(_:)` | Store token + send comprehensive device info to server |
530 +| `checkForLoyaltySDKNotification(_:)` | Detect Warply push by `"_a"` key, auto-handle if found |
531 +| `handleNotification(_:appState:)` | Route notification by action and app state |
532 +| `handleLaunchOptions(_:)` | Check launch options for push (cold start) |
533 +| `handlePendingNotification()` | Process stored notification from cold start |
534 +| `showPushCampaign(sessionUuid:)` | Present campaign WebView for a session UUID |
535 +| `resetBadge()` | Reset app icon badge to 0 |
536 +| `sendDeviceInfo()` | Force send device info to server |
537 +| `sendDeviceInfoIfNeeded()` | Smart send — only if info changed |
538 +| `buildDeviceInfo()` | Build comprehensive device info dictionary |
539 +
540 +### Push Analytics
541 +
542 +| Event Name | When Sent | Equivalent ObjC |
543 +|------------|-----------|-----------------|
544 +| `NB_PushReceived` | When Warply push is received | `logUserReceivedPush:` |
545 +| `NB_PushAck` | When user engages with push | `logUserEngagedPush:` |
546 +
547 +### Device Info Payload
548 +
549 +Sent to `/api/async/info/{appUUID}/` via `Endpoint.sendDeviceInfo`. Contains:
550 +- `device_token`, `platform` ("ios"), `vendor` ("apple"), `os_version`
551 +- `unique_device_id` (IDFV), `ios_system_name`, `ios_system_version`, `ios_model`
552 +- `ios_locale`, `ios_languages`, `screen_resolution`
553 +- `bundle_identifier`, `app_version`, `app_build`, `sdk_version`
554 +- `development` flag (DEBUG vs release)
555 +
556 +### Rich Push Presentation
557 +
558 +For `action == 0` pushes, the framework presents `CampaignViewController` internally:
559 +- URL: `{baseURL}/api/session/{session_uuid}`
560 +- **Active state**: Shows alert first, then WebView on user confirmation
561 +- **Background state**: Presents WebView immediately
562 +- **Closed state**: Stores as pending, shows via `handlePendingNotification()`
563 +
499 ## 10. Configuration System 564 ## 10. Configuration System
500 565
501 ### WarplyConfiguration (`Configuration/WarplyConfiguration.swift`) 566 ### WarplyConfiguration (`Configuration/WarplyConfiguration.swift`)
......