Manos Chorianopoulos

Push Notifications implementation

......@@ -1249,35 +1249,128 @@ class LegacyViewController: UIViewController {
## 📱 Push Notifications
### Setup
### Complete AppDelegate Setup
The framework provides full push notification support. Add these methods to your `AppDelegate.swift`:
```swift
// In AppDelegate
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
let tokenString = deviceToken.map { String(format: "%02.2hhx", $0) }.joined()
WarplySDK.shared.updateDeviceToken(tokenString)
import SwiftWarplyFramework
import UserNotifications
@main
class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// 1️⃣ Configure and initialize SDK (as shown in Setup section above)
WarplySDK.shared.configure(appUuid: "YOUR_APP_UUID", merchantId: "YOUR_MERCHANT_ID")
WarplySDK.shared.initialize { success in
print("SDK Ready: \(success)")
}
// 2️⃣ Check if app was launched from a push notification
WarplySDK.shared.handleLaunchOptions(launchOptions)
// 3️⃣ Register for push notifications
UNUserNotificationCenter.current().delegate = self
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { granted, error in
if granted {
DispatchQueue.main.async {
UIApplication.shared.registerForRemoteNotifications()
}
}
}
return true
}
// 4️⃣ Send device token to Warply server
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
let tokenString = deviceToken.map { String(format: "%02.2hhx", $0) }.joined()
WarplySDK.shared.updateDeviceToken(tokenString)
}
// 5️⃣ Forward foreground notifications to SDK
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
let userInfo = notification.request.content.userInfo as? [String: Any] ?? [:]
if WarplySDK.shared.checkForLoyaltySDKNotification(userInfo) {
// Warply handles it (shows alert in foreground)
completionHandler([])
} else {
// Not a Warply notification — show normally
completionHandler([.banner, .sound, .badge])
}
}
// 6️⃣ Forward notification taps to SDK
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
let userInfo = response.notification.request.content.userInfo as? [String: Any] ?? [:]
// Let the SDK handle Warply notifications (opens campaign WebView)
_ = WarplySDK.shared.checkForLoyaltySDKNotification(userInfo)
completionHandler()
}
}
```
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any]) {
// Check if it's a Warply notification
if WarplySDK.shared.checkForLoyaltySDKNotification(userInfo) {
print("Warply notification handled")
} else {
// Handle other notifications
### Handle Pending Notifications
When the app is launched from a push notification (cold start), call this in your root view controller:
```swift
class MainViewController: UIViewController {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// Handle any pending push notification from cold start
WarplySDK.shared.handlePendingNotification()
}
}
```
### Handle Notifications
### Optional — Clear Badge on App Open
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:
```swift
// Manual notification handling
WarplySDK.shared.handleNotification(notificationPayload)
// In AppDelegate — clear the badge when user opens the app
func applicationDidBecomeActive(_ application: UIApplication) {
WarplySDK.shared.resetBadge()
}
```
### Optional — Listen for Push Events
// Check if notification belongs to Warply SDK
let isWarplyNotification = WarplySDK.shared.checkForLoyaltySDKNotification(payload)
If you want to react to push notifications in your own code (in addition to the SDK's automatic handling):
```swift
// Modern EventDispatcher approach
let subscription = WarplySDK.shared.subscribe(PushNotificationReceivedEvent.self) { event in
print("Push received! Session: \(event.sessionUuid ?? "unknown")")
}
```
### Advanced — Custom Push Handler (Most apps don't need this)
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:
```swift
class MyPushHandler: WarplyPushHandler {
func didReceiveWarplyNotification(_ payload: WarplyNotificationPayload, whileAppWasIn state: WarplyApplicationState) {
// This is only called for pushes with action != 0
// Standard offer pushes (action == 0) are handled automatically by the SDK
print("Custom action push: \(payload.action)")
}
}
// Set the delegate
WarplySDK.shared.pushHandlerDelegate = myPushHandler
```
> **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.
---
## ⚠️ Error Handling
......
This diff is collapsed. Click to expand it.
......@@ -177,18 +177,46 @@ Task {
## 📱 Push Notifications
### Required Setup (AppDelegate)
```swift
// AppDelegate
// 1️⃣ In didFinishLaunchingWithOptions — check if app launched from push
WarplySDK.shared.handleLaunchOptions(launchOptions)
// 2️⃣ Send device token to server
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
let tokenString = deviceToken.map { String(format: "%02.2hhx", $0) }.joined()
WarplySDK.shared.updateDeviceToken(tokenString)
}
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any]) {
// 3️⃣ Forward foreground notifications (UNUserNotificationCenterDelegate)
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, ...) {
let userInfo = notification.request.content.userInfo as? [String: Any] ?? [:]
if WarplySDK.shared.checkForLoyaltySDKNotification(userInfo) {
// Warply notification handled
completionHandler([]) // SDK handles it (shows alert)
} else {
completionHandler([.banner, .sound, .badge]) // Not a Warply push
}
}
// 4️⃣ Forward notification taps (UNUserNotificationCenterDelegate)
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, ...) {
let userInfo = response.notification.request.content.userInfo as? [String: Any] ?? [:]
_ = WarplySDK.shared.checkForLoyaltySDKNotification(userInfo)
completionHandler()
}
// 5️⃣ Handle pending notifications (in root ViewController's viewDidAppear)
WarplySDK.shared.handlePendingNotification()
```
### Optional — Clear Badge on App Open
```swift
// In applicationDidBecomeActive — clears the red number on the app icon
func applicationDidBecomeActive(_ application: UIApplication) {
WarplySDK.shared.resetBadge()
}
```
---
......
......@@ -226,16 +226,50 @@ public struct SeasonalsRetrievedEvent: WarplyEvent {
/// Coupons fetched event
public struct CouponsRetrievedEvent: WarplyEvent {
public let name: String = "coupons_fetched"
public let name: String = "coupons_fetched"
public let timestamp: Date
public let data: Any?
public init(data: Any? = nil) {
self.timestamp = Date()
self.data = data
}
}
/// Event posted when a Warply push notification is received
/// Equivalent of old ObjC `NB_PushReceived` analytics event
public struct PushNotificationReceivedEvent: WarplyEvent {
public let name: String = "push_notification_received"
public let timestamp: Date
public let data: Any?
/// The session UUID from the push notification payload
public let sessionUuid: String?
public init(data: Any? = nil, sessionUuid: String? = nil) {
self.timestamp = Date()
self.data = data
self.sessionUuid = sessionUuid
}
}
/// Event posted when a user interacts with (engages) a Warply push notification
/// Equivalent of old ObjC `NB_PushAck` analytics event
public struct PushNotificationEngagedEvent: WarplyEvent {
public let name: String = "push_notification_engaged"
public let timestamp: Date
public let data: Any?
/// The session UUID from the push notification payload
public let sessionUuid: String?
public init(data: Any? = nil, sessionUuid: String? = nil) {
self.timestamp = Date()
self.data = data
self.sessionUuid = sessionUuid
}
}
// MARK: - Convenience Extensions
extension EventDispatcher {
......@@ -275,4 +309,20 @@ extension EventDispatcher {
public func postCouponsRetrievedEvent(sender: Any? = nil) {
post(CouponsRetrievedEvent(data: sender))
}
/// Post a push notification received event
/// - Parameters:
/// - sender: The event data (typically the raw push payload)
/// - sessionUuid: The session UUID from the push notification
public func postPushNotificationReceivedEvent(sender: Any? = nil, sessionUuid: String? = nil) {
post(PushNotificationReceivedEvent(data: sender, sessionUuid: sessionUuid))
}
/// Post a push notification engaged event
/// - Parameters:
/// - sender: The event data (typically the raw push payload)
/// - sessionUuid: The session UUID from the push notification
public func postPushNotificationEngagedEvent(sender: Any? = nil, sessionUuid: String? = nil) {
post(PushNotificationEngagedEvent(data: sender, sessionUuid: sessionUuid))
}
}
......
......@@ -97,7 +97,7 @@ public enum Endpoint {
case sendEvent(eventName: String, priority: Bool)
// Device
case sendDeviceInfo(deviceToken: String)
case sendDeviceInfo(deviceInfo: [String: Any])
// Network status
case getNetworkStatus
......@@ -428,13 +428,10 @@ public enum Endpoint {
]
]
// Device Info endpoints - device structure
case .sendDeviceInfo(let deviceToken):
return [
"device": [
"device_token": deviceToken
]
]
// Device Info endpoints - comprehensive device info structure
// Matches old ObjC WLPushManager.deviceInfo + WLPushManager.applicationData
case .sendDeviceInfo(let deviceInfo):
return deviceInfo
// Network status - no body needed
case .getNetworkStatus:
......
......@@ -860,14 +860,16 @@ extension NetworkService {
}
}
/// Update device token
public func updateDeviceToken(_ deviceToken: String) async {
let endpoint = Endpoint.sendDeviceInfo(deviceToken: deviceToken)
/// Send comprehensive device info to server
/// - Parameter deviceInfo: Full device info dictionary (built by WarplySDK.buildDeviceInfo())
public func sendDeviceInfo(_ deviceInfo: [String: Any]) async {
let endpoint = Endpoint.sendDeviceInfo(deviceInfo: deviceInfo)
do {
_ = try await requestRaw(endpoint)
print("✅ [NetworkService] Device info sent to server successfully")
} catch {
print("Failed to update device token: \(error)")
print("❌ [NetworkService] Failed to send device info: \(error)")
}
}
......
......@@ -496,6 +496,71 @@ self.postFrameworkEvent("dynatrace", sender: dynatraceEvent)
```
Event naming convention: `custom_{success|error}_{operation}_loyalty`
## 9.5 Push Notification System
### Architecture (Option A: Host App Forwards)
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.
### Push Notification Types
| Type | Location | Purpose |
|------|----------|---------|
| `WarplyApplicationState` | `WarplySDK.swift` | Enum: `.active`, `.background`, `.closed` |
| `WarplyPushHandler` | `WarplySDK.swift` | Protocol for custom push handling (action != 0) |
| `WarplyNotificationPayload` | `WarplySDK.swift` | Parsed push payload (action, sessionUuid, message, aps, customData) |
| `WarplyAPSItem` | `WarplySDK.swift` | Parsed APS dictionary (alert, badge, sound, category) |
| `PushNotificationReceivedEvent` | `EventDispatcher.swift` | Event posted when push received |
| `PushNotificationEngagedEvent` | `EventDispatcher.swift` | Event posted when user interacts with push |
### Push Notification Detection
Warply push notifications are identified by the `"_a"` key in the payload:
```json
{"_a": 0, "session_uuid": "abc-123", "aps": {"alert": "New offer!", "badge": 1}}
```
- `"_a" == 0`: Default handling — framework presents `CampaignViewController` WebView
- `"_a" != 0`: Custom action — forwarded to `pushHandlerDelegate`
### Push Notification Methods
| Method | Purpose |
|--------|---------|
| `updateDeviceToken(_:)` | Store token + send comprehensive device info to server |
| `checkForLoyaltySDKNotification(_:)` | Detect Warply push by `"_a"` key, auto-handle if found |
| `handleNotification(_:appState:)` | Route notification by action and app state |
| `handleLaunchOptions(_:)` | Check launch options for push (cold start) |
| `handlePendingNotification()` | Process stored notification from cold start |
| `showPushCampaign(sessionUuid:)` | Present campaign WebView for a session UUID |
| `resetBadge()` | Reset app icon badge to 0 |
| `sendDeviceInfo()` | Force send device info to server |
| `sendDeviceInfoIfNeeded()` | Smart send — only if info changed |
| `buildDeviceInfo()` | Build comprehensive device info dictionary |
### Push Analytics
| Event Name | When Sent | Equivalent ObjC |
|------------|-----------|-----------------|
| `NB_PushReceived` | When Warply push is received | `logUserReceivedPush:` |
| `NB_PushAck` | When user engages with push | `logUserEngagedPush:` |
### Device Info Payload
Sent to `/api/async/info/{appUUID}/` via `Endpoint.sendDeviceInfo`. Contains:
- `device_token`, `platform` ("ios"), `vendor` ("apple"), `os_version`
- `unique_device_id` (IDFV), `ios_system_name`, `ios_system_version`, `ios_model`
- `ios_locale`, `ios_languages`, `screen_resolution`
- `bundle_identifier`, `app_version`, `app_build`, `sdk_version`
- `development` flag (DEBUG vs release)
### Rich Push Presentation
For `action == 0` pushes, the framework presents `CampaignViewController` internally:
- URL: `{baseURL}/api/session/{session_uuid}`
- **Active state**: Shows alert first, then WebView on user confirmation
- **Background state**: Presents WebView immediately
- **Closed state**: Stores as pending, shows via `handlePendingNotification()`
## 10. Configuration System
### WarplyConfiguration (`Configuration/WarplyConfiguration.swift`)
......