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) {
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] ?? [:]
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any]) {
// Check if it's a Warply notification
if WarplySDK.shared.checkForLoyaltySDKNotification(userInfo) {
print("Warply notification handled")
// Warply handles it (shows alert in foreground)
completionHandler([])
} else {
// Handle other notifications
// 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()
}
}
```
### Handle 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()
}
}
```
### 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
// In AppDelegate — clear the badge when user opens the app
func applicationDidBecomeActive(_ application: UIApplication) {
WarplySDK.shared.resetBadge()
}
```
### Optional — Listen for Push Events
If you want to react to push notifications in your own code (in addition to the SDK's automatic handling):
```swift
// Manual notification handling
WarplySDK.shared.handleNotification(notificationPayload)
// 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)
// Check if notification belongs to Warply SDK
let isWarplyNotification = WarplySDK.shared.checkForLoyaltySDKNotification(payload)
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
......
# Push Notifications Migration Plan: ObjC → Swift
## 📋 Overview
**Date**: July 3, 2025
**Scope**: Migrate push notification functionality from old ObjC `WLPushManager` to the Swift `WarplySDK`
**Source**: `/Users/manoschorianopoulos/Desktop/warply_projects/cosmote_sdk/warply_sdk_framework/WarplySDKFrameworkIOS/Warply/managers/WLPushManager.h/.m`
**Target**: `SwiftWarplyFramework/SwiftWarplyFramework/Core/WarplySDK.swift` and related files
**Priority**: 🔴 Critical — Push notifications are currently non-functional (stub implementations only)
---
## 🔍 Current State Analysis
### Old ObjC Implementation (WLPushManager) — What Worked
| Feature | Implementation | Status |
|---------|---------------|--------|
| Device token conversion (NSData → hex string) | `didRegisterForRemoteNotificationsWithDeviceToken:` | ✅ Full |
| Device token persistence | Stored in `WLKeychain` | ✅ Full |
| Device token → server | Via `sendDeviceInfo` event system | ✅ Full |
| Warply notification detection | Check for `"_a"` key in payload | ✅ Full |
| Notification routing by app state | Active/Background/Closed handling | ✅ Full |
| Custom push handler protocol | `WLCustomPushHandler` protocol | ✅ Full |
| Push analytics | `NB_PushAck` (engaged) + `NB_PushReceived` events | ✅ Full |
| UNUserNotificationCenter delegate | Foreground + response handling | ✅ Full |
| Pending notification queue | Store for later handling when app was closed | ✅ Full |
| Badge reset | `resetBadge` method | ✅ Full |
| Comprehensive device info | Carrier, IDFV, jailbreak, screen, etc. | ✅ Full |
| Notification action categories | share, favorite, book categories | ✅ Full |
### Current Swift Implementation — What's Broken
| Feature | Current State | Problem |
|---------|--------------|---------|
| `updateDeviceToken()` | Saves to UserDefaults only | ❌ **Never sends token to server** |
| `checkForLoyaltySDKNotification()` | Checks for `"loyalty_sdk"` / `"warply"` keys | ❌ **Wrong keys** — should check for `"_a"` |
| `handleNotification()` | Just `print()` | ❌ **Does nothing** |
| Push analytics | None | ❌ **Missing entirely** |
| Custom push handler | None | ❌ **Missing entirely** |
| Device info sending | Only during registration | ⚠️ **Partial** — not sent on token update |
| Pending notification handling | None | ❌ **Missing entirely** |
| Badge management | None | ❌ **Missing entirely** |
---
## 🏗️ Architecture Decision
### Design Philosophy — Option A: Host App Forwards (Modern Pattern)
The **modern Swift framework should NOT**:
- ❌ Register for push notifications itself (host app's responsibility)
- ❌ Set itself as `UNUserNotificationCenterDelegate` (conflicts with host app's own delegate, Firebase, etc.)
- ❌ Call `UIApplication.shared.registerForRemoteNotifications()` (host app controls this)
The **modern Swift framework SHOULD**:
- ✅ Accept device tokens from the host app and send them to the Warply server
- ✅ Detect Warply push notifications from payload
- ✅ Process and route Warply notifications
-**Present rich push content via `CampaignViewController` (WebView)** — consistent with old `WLPushManager.showItem:`
- ✅ Provide convenience wrappers for `UNUserNotificationCenterDelegate` methods (host app just forwards)
- ✅ Provide a delegate/protocol for custom push handling
- ✅ Send push analytics events
- ✅ Send comprehensive device info to server
- ✅ Handle pending notifications
- ✅ Provide badge management utilities
This is the modern iOS best practice: the framework is a **helper**, not the **owner** of push notification registration.
### Why Option A (Host App Forwards) Over Option B (SDK is Delegate)
The old ObjC `WLPushManager` set itself as `UNUserNotificationCenter.delegate`. This created conflicts when the host app also had its own delegate (Firebase, OneSignal, etc.) — only one delegate can exist at a time.
**Option A approach**: The host app remains the delegate and forwards calls to the SDK:
```swift
// Host app's AppDelegate — just forward these 2 calls to the SDK:
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
WarplySDK.shared.willPresentNotification(notification, completionHandler: completionHandler)
}
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
WarplySDK.shared.didReceiveNotificationResponse(response, completionHandler: completionHandler)
}
```
**Benefits:**
- ✅ No delegate conflicts with Firebase, OneSignal, etc.
- ✅ Host app has full control over notification behavior
- ✅ Only requires 2-3 lines of forwarding code
- ✅ End-user experience is 100% identical to old implementation
- ✅ Every modern SDK (Firebase, OneSignal, Airship) uses this pattern
---
## 📝 Migration Steps
### Phase 1: Core Infrastructure ✅
- [x] **Step 1.1**: Create `WarplyNotificationPayload` model
- Swift struct to parse Warply push notification payloads
- Key fields: `action` (from `"_a"`), `sessionUuid` (from `"session_uuid"`), `message`, `customData`
- Parse `aps` dictionary for alert, badge, sound, category
- Equivalent of old `WLBaseItem.initWithAttributes:`
- **File**: New section in `Models/` or within `WarplySDK.swift`
- [x] **Step 1.2**: Create `WarplyPushHandler` protocol
- Swift equivalent of `WLCustomPushHandler`
- Method: `func didReceiveWarplyNotification(_ payload: WarplyNotificationPayload, whileAppWasIn state: WarplyApplicationState)`
- Enum: `WarplyApplicationState { case active, background, closed }`
- **File**: New protocol definition in `WarplySDK.swift` or dedicated file
### Phase 2: Device Token Management ✅
- [x] **Step 2.1**: Fix `updateDeviceToken()` in `WarplySDK.swift`
- Store token in UserDefaults (keep existing behavior)
- **Also call `NetworkService.updateDeviceToken()`** to send to server
- Add Dynatrace analytics event for success/failure
- Log properly with emoji convention
- [x] **Step 2.2**: Enhance `Endpoint.sendDeviceInfo` to send comprehensive device info
- Currently only sends `{"device": {"device_token": token}}`
- Add all device info fields from old `WLPushManager.deviceInfo`:
- `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`
- `development` flag (DEBUG vs release)
- **File**: `Network/Endpoints.swift` — update `sendDeviceInfo` case parameters and body
- [x] **Step 2.3**: Add `sendDeviceInfo()` method to `WarplySDK.swift`
- Public method that collects all device info and sends to server
- Called automatically after `updateDeviceToken()`
- Can also be called manually by host app
- Tracks whether device info has been sent this session (like old `hasSentDeviceInfo`)
- Only sends if device info has changed (compare with last sent, stored in UserDefaults)
### Phase 3: Notification Detection & Handling ✅
- [x] **Step 3.1**: Fix `checkForLoyaltySDKNotification()` in `WarplySDK.swift`
- Change detection from `"loyalty_sdk"` / `"warply"` keys to `"_a"` key (the actual Warply identifier)
- Parse payload into `WarplyNotificationPayload`
- Return `true` if `"_a"` key exists in payload
- Call `handleNotification()` if it's a Warply notification
- [x] **Step 3.2**: Implement `handleNotification()` in `WarplySDK.swift`
- Parse payload into `WarplyNotificationPayload`
- Determine app state (active/background/closed)
- Route based on `action` field:
- `action == 0`: Default Warply handling (post event for host app to show campaign)
- `action != 0`: Call custom push handler delegate if set
- Post `SwiftEventBus` + `EventDispatcher` events for the notification
- Post Dynatrace analytics event
- [x] **Step 3.3**: Add push analytics events
- `logPushReceived(sessionUuid:)` — sends `NB_PushReceived` event (equivalent of old `logUserReceivedPush:`)
- `logPushEngaged(sessionUuid:)` — sends `NB_PushAck` event (equivalent of old `logUserEngagedPush:`)
- Use existing `Endpoint.sendEvent` for analytics
- Follows existing Dynatrace event pattern
- [x] **Step 3.4**: Add pending notification support
- Store pending notification when app was closed (received via `didFinishLaunchingWithOptions`)
- `handlePendingNotification()` — processes stored notification, returns `Bool`
- Host app calls this from their root view controller's `viewDidAppear`
- [x] **Step 3.5**: Rich push presentation via `CampaignViewController` (WebView)
- For `action == 0` pushes (default Warply handling), the **framework handles UI presentation internally**:
- Build campaign URL from `session_uuid`: `{baseURL}/api/session/{session_uuid}`
- Build params JSON via `constructCampaignParams()`
- Create `CampaignViewController`, set `campaignUrl` and `params`
- Present modally (wrapped in `UINavigationController`) on the topmost view controller
- Behavior by app state (consistent with old `WLPushManager.showItem:`):
- **Active** state: Show an alert first ("You have a new offer"), present WebView on user confirmation
- **Background** state: Present WebView immediately when app returns to foreground
- **Closed** state: Store as pending, show when `handlePendingNotification()` is called
- Add helper: `showPushCampaign(sessionUuid:)` — public method host apps can call directly
- Add helper: `getTopViewController()` — finds topmost presented VC for modal presentation
- **Reuses existing `CampaignViewController`** — no new screen needed
- **File**: `Core/WarplySDK.swift`
### Phase 4: Public API & Delegate ✅
- [x] **Step 4.1**: Add `pushHandlerDelegate` property to `WarplySDK`
- `weak var pushHandlerDelegate: WarplyPushHandler?`
- Host app sets this to receive custom push notifications (action != 0)
- Modern Swift alternative to old `WLCustomPushHandler` protocol
- [x] **Step 4.2**: Add convenience methods to `WarplySDK`
- `resetBadge()` — resets app icon badge to 0
- `handleLaunchOptions(_ launchOptions: [UIApplication.LaunchOptionsKey: Any]?)` — checks for push in launch options
- `didReceiveRemoteNotification(_ userInfo: [AnyHashable: Any], appState: WarplyApplicationState)` — main entry point for push handling
- **UNUserNotificationCenter convenience wrappers** (host app forwards these):
- `willPresentNotification(_ notification: UNNotification, completionHandler: ...)` — handles foreground push display
- If Warply push: calls `handleNotification()` with `.active` state, shows alert
- If not Warply push: calls completionHandler with `.banner, .sound, .badge` (pass-through)
- `didReceiveNotificationResponse(_ response: UNNotificationResponse, completionHandler: ...)` — handles user tap on notification
- Determines app state, calls `handleNotification()`, logs analytics (`NB_PushAck`)
- Calls completionHandler after processing
- [x] **Step 4.3**: Add new push-related events to `EventDispatcher`
- `PushNotificationReceivedEvent` — posted when Warply push is received
- `PushNotificationEngagedEvent` — posted when user interacts with push
- Both SwiftEventBus and EventDispatcher (dual posting pattern)
- Event names: `"push_notification_received"`, `"push_notification_engaged"`
### Phase 5: Documentation & Client Integration ✅
- [x] **Step 5.1**: Update `CLIENT_DOCUMENTATION.md` Push Notifications section
- Replace current minimal docs with comprehensive guide
- Include all 3 AppDelegate methods needed:
1. `didFinishLaunchingWithOptions` — handle launch from push
2. `didRegisterForRemoteNotificationsWithDeviceToken` — send token to SDK
3. `didReceiveRemoteNotification` — route notification through SDK
- Show custom push handler setup
- Show event subscription for push notifications
- [x] **Step 5.2**: Update `skill.md` with push notification architecture
- Document new `WarplyNotificationPayload` model
- Document `WarplyPushHandler` protocol
- Document push analytics events
- Update API reference section
- [x] **Step 5.3**: Update `QUICK_REFERENCE.md` push section
- Updated code examples matching the new implementation
- Quick copy-paste setup guide
---
## 📄 Files to Modify
| File | Changes | Impact |
|------|---------|--------|
| `Core/WarplySDK.swift` | Replace stub push methods with full implementation, add delegate, add device info methods | 🔴 High |
| `Network/Endpoints.swift` | Enhance `sendDeviceInfo` endpoint with full device info payload | 🟡 Medium |
| `Network/NetworkService.swift` | Enhance `updateDeviceToken` to send full device info | 🟡 Medium |
| `Events/EventDispatcher.swift` | Add `PushNotificationReceivedEvent` and `PushNotificationEngagedEvent` types | 🟢 Low |
| `CLIENT_DOCUMENTATION.md` | Complete rewrite of push notifications section | 🟡 Medium |
| `skill.md` | Add push notification architecture documentation | 🟢 Low |
| `QUICK_REFERENCE.md` | Update push notification code examples | 🟢 Low |
---
## 🗺️ ObjC → Swift Method Mapping
| Old ObjC (WLPushManager) | New Swift (WarplySDK) | Notes |
|---------------------------|----------------------|-------|
| `didRegisterForRemoteNotificationsWithDeviceToken:` | `updateDeviceToken(_:)` | Host app converts Data→String, calls this |
| `didFailToRegisterForRemoteNotificationsWithError:` | *(not needed)* | Host app handles the failure |
| `registerForRemoteNotifications` | *(not needed)* | Host app registers directly |
| `didFinishLaunchingWithOptions:` | `handleLaunchOptions(_:)` | Check for push in launch options |
| `didReceiveRemoteNotification:whileAppWasInState:` | `didReceiveRemoteNotification(_:appState:)` | Main notification entry point |
| `application:didReceiveRemoteNotification:` | `checkForLoyaltySDKNotification(_:)` | Detection + handling |
| `handlePendingNotification` | `handlePendingNotification()` | Process queued notification |
| `showItem:` | `showPushCampaign(sessionUuid:)` | Presents `CampaignViewController` modally with campaign URL |
| `resetBadge` | `resetBadge()` | Direct port |
| `sendDeviceInfoIfNecessary` | `sendDeviceInfoIfNeeded()` | Smart send with change detection |
| `sendDeviceInfo` | `sendDeviceInfo()` | Force send device info |
| `deviceInfo``NSDictionary` | `buildDeviceInfo()``[String: Any]` | Comprehensive device info dict |
| `applicationData``NSDictionary` | *(merged into buildDeviceInfo)* | Combined into single method |
| `WLCustomPushHandler` protocol | `WarplyPushHandler` protocol | Swift protocol |
| `WLBaseItem.initWithAttributes:` | `WarplyNotificationPayload(userInfo:)` | Swift struct with init |
| `WLAnalyticsManager.logUserEngagedPush:` | `logPushEngaged(sessionUuid:)` | Sends `NB_PushAck` |
| `WLAnalyticsManager.logUserReceivedPush:` | `logPushReceived(sessionUuid:)` | Sends `NB_PushReceived` |
| `willPresentNotification:` (UNDelegate) | `willPresentNotification(_:completionHandler:)` | Host app forwards; SDK handles Warply pushes |
| `didReceiveNotificationResponse:` (UNDelegate) | `didReceiveNotificationResponse(_:completionHandler:)` | Host app forwards; SDK routes + logs analytics |
---
## 🧪 Testing Checklist
After implementation, verify:
- [ ] Device token is stored in UserDefaults when `updateDeviceToken()` called
- [ ] Device token is sent to server via `sendDeviceInfo` endpoint
- [ ] `checkForLoyaltySDKNotification()` returns `true` for payloads with `"_a"` key
- [ ] `checkForLoyaltySDKNotification()` returns `false` for non-Warply payloads
- [ ] `handleNotification()` posts events (both SwiftEventBus + EventDispatcher)
- [ ] `handleNotification()` calls custom push handler delegate for action != 0
- [ ] Push analytics events (`NB_PushAck`, `NB_PushReceived`) are sent
- [ ] `handleLaunchOptions()` detects push payload in launch options
- [ ] `handlePendingNotification()` processes stored notification
- [ ] `resetBadge()` sets badge to 0
- [ ] `sendDeviceInfo()` sends comprehensive device info to server
- [ ] Device info is only sent when it has changed (not on every launch)
- [ ] Dynatrace events are posted for push operations (success + failure)
- [ ] Rich push: `showPushCampaign(sessionUuid:)` presents `CampaignViewController` modally
- [ ] Rich push: Campaign URL correctly built as `{baseURL}/api/session/{session_uuid}`
- [ ] Rich push: Alert shown when push received in active (foreground) state
- [ ] Rich push: WebView shown immediately when push received in background state
- [ ] Rich push: Pending notification stored and shown via `handlePendingNotification()` for closed state
- [ ] `willPresentNotification()` correctly handles Warply vs non-Warply pushes
- [ ] `didReceiveNotificationResponse()` routes notification and logs `NB_PushAck`
- [ ] No compilation errors introduced
- [ ] All existing public API remains backward compatible
---
## ⚠️ Important Notes
1. **`"_a"` is the Warply Push Identifier** — The old ObjC code uses `[userInfo valueForKey:@"_a"]` to detect Warply pushes. The current Swift stub incorrectly checks for `"loyalty_sdk"` and `"warply"` keys.
2. **The framework should NOT be UNUserNotificationCenterDelegate** — In the old ObjC code, WLPushManager set itself as `UNUserNotificationCenter.delegate`. In modern iOS, the host app owns this. The framework should only provide helper methods.
3. **Device info sending uses the event system** — In old ObjC, device info was sent via `WLEventSimple` (type: `"device_info"`) through `addEvent:priority:`. In Swift, we use the existing `Endpoint.sendDeviceInfo` which POSTs to `/api/async/info/{appUUID}/`.
4. **Rich push WebView handled by framework** — The old `showItem:` presented a `WLInboxItemViewController` WebView for rich pushes. The modern framework maintains this behavior by reusing the existing `CampaignViewController` — for `action == 0` pushes, the framework builds a campaign URL from `session_uuid` (`{baseURL}/api/session/{session_uuid}`) and presents `CampaignViewController` modally. This ensures full consistency with the old implementation.
5. **Keychain vs UserDefaults for device token** — Old code used `WLKeychain`. New code uses `UserDefaults`. Device tokens are not sensitive (they're sent to servers in plain text), so `UserDefaults` is acceptable. However, we could optionally use `KeychainManager` for consistency.
---
## 📊 Estimated Effort
| Phase | Estimated Time | Complexity |
|-------|---------------|------------|
| Phase 1: Core Infrastructure | 30 min | Medium |
| Phase 2: Device Token Management | 45 min | Medium |
| Phase 3: Notification Detection & Handling | 45 min | Medium-High |
| Phase 4: Public API & Delegate | 30 min | Low-Medium |
| Phase 5: Documentation | 30 min | Low |
| **Total** | **~3 hours** | **Medium** |
---
*Plan created: July 3, 2025*
*Author: AI Migration Assistant*
......@@ -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()
}
```
---
......
......@@ -50,6 +50,213 @@ public enum WarplyError: Error {
}
}
// MARK: - Push Notification Types
/// Application state when a push notification is received
/// Equivalent of old ObjC `WLApplicationState` enum
public enum WarplyApplicationState: Int {
/// The application is running in the foreground and currently receiving events
case active = 0
/// The application is running in the background
case background = 1
/// The application was not running (cold start from push)
case closed = 2
}
/// Protocol for custom push notification handling
/// Equivalent of old ObjC `WLCustomPushHandler` protocol
///
/// Implement this protocol and set `WarplySDK.shared.pushHandlerDelegate` to receive
/// push notifications with `action != 0` (non-default Warply pushes).
///
/// For `action == 0` pushes, the framework handles them automatically by presenting
/// a `CampaignViewController` WebView with the campaign content.
public protocol WarplyPushHandler: AnyObject {
/// Called when a Warply push notification is received with a custom action (action != 0)
/// - Parameters:
/// - payload: The parsed notification payload
/// - state: The application state when the notification was received
func didReceiveWarplyNotification(_ payload: WarplyNotificationPayload, whileAppWasIn state: WarplyApplicationState)
}
/// APS (Apple Push Notification Service) item parsed from push payload
/// Equivalent of old ObjC `WLAPSItem`
public struct WarplyAPSItem {
/// The alert text or dictionary from the push notification
public let alert: String?
/// The alert title (if alert is a dictionary)
public let alertTitle: String?
/// The alert body (if alert is a dictionary)
public let alertBody: String?
/// The badge number to display on the app icon
public let badge: Int?
/// The sound to play when the notification is received
public let sound: String?
/// The notification category for action buttons
public let category: String?
/// The content-available flag for silent pushes
public let contentAvailable: Bool
/// The mutable-content flag for notification service extensions
public let mutableContent: Bool
/// Initialize from the "aps" dictionary in a push payload
public init(dictionary: [String: Any]?) {
guard let dict = dictionary else {
self.alert = nil
self.alertTitle = nil
self.alertBody = nil
self.badge = nil
self.sound = nil
self.category = nil
self.contentAvailable = false
self.mutableContent = false
return
}
// Parse alert — can be a string or a dictionary
if let alertString = dict["alert"] as? String {
self.alert = alertString
self.alertTitle = nil
self.alertBody = alertString
} else if let alertDict = dict["alert"] as? [String: Any] {
self.alertTitle = alertDict["title"] as? String
self.alertBody = alertDict["body"] as? String
self.alert = self.alertBody ?? self.alertTitle
} else {
self.alert = nil
self.alertTitle = nil
self.alertBody = nil
}
self.badge = dict["badge"] as? Int
self.sound = dict["sound"] as? String
self.category = dict["category"] as? String
self.contentAvailable = (dict["content-available"] as? Int) == 1
self.mutableContent = (dict["mutable-content"] as? Int) == 1
}
}
/// Parsed Warply push notification payload
/// Equivalent of old ObjC `WLBaseItem` initialized via `initWithAttributes:`
///
/// The Warply push notification payload has this structure:
/// ```json
/// {
/// "_a": 0, // action: 0 = default Warply handling, != 0 = custom
/// "session_uuid": "abc-123", // unique campaign session identifier
/// "aps": {
/// "alert": "You have a new offer!",
/// "badge": 1,
/// "sound": "default",
/// "category": "shareCategory"
/// },
/// ... // any additional custom data
/// }
/// ```
public struct WarplyNotificationPayload {
/// The action type for this notification
/// - `0`: Default Warply handling (show rich push WebView via CampaignViewController)
/// - `!= 0`: Custom action — forwarded to `WarplyPushHandler` delegate
public let action: Int
/// The unique session UUID for the campaign/offer associated with this push
public let sessionUuid: String?
/// The trace identifier for tracking
public let trace: String?
/// The parsed APS (Apple Push Service) data
public let apsItem: WarplyAPSItem
/// The alert message text from the push notification
public var message: String? {
return apsItem.alert
}
/// Any additional custom data from the push payload (keys not recognized by the parser)
public let customData: [String: Any]
/// The raw push notification payload dictionary
public let rawPayload: [String: Any]
/// Whether this is a Warply push notification (contains the "_a" key)
public var isWarplyNotification: Bool {
return rawPayload["_a"] != nil
}
/// The number of times the offer has been opened (if tracked)
public var openedCount: Int = 0
// MARK: - Initialization
/// Initialize from a push notification userInfo dictionary
/// Equivalent of old ObjC `WLBaseItem.initWithAttributes:`
/// - Parameter userInfo: The push notification payload dictionary
public init(userInfo: [AnyHashable: Any]) {
// Store raw payload
var rawDict: [String: Any] = [:]
for (key, value) in userInfo {
if let stringKey = key as? String {
rawDict[stringKey] = value
}
}
self.rawPayload = rawDict
// Parse action from "_a" key (Warply's push identifier)
if let actionValue = rawDict["_a"] {
if let actionInt = actionValue as? Int {
self.action = actionInt
} else if let actionNumber = actionValue as? NSNumber {
self.action = actionNumber.intValue
} else if let actionString = actionValue as? String, let actionInt = Int(actionString) {
self.action = actionInt
} else {
self.action = 0
}
} else {
self.action = 0
}
// Parse session_uuid
self.sessionUuid = rawDict["session_uuid"] as? String
// Parse trace
self.trace = rawDict["trace"] as? String
// Parse APS dictionary
self.apsItem = WarplyAPSItem(dictionary: rawDict["aps"] as? [String: Any])
// Collect custom data (everything except known keys)
let knownKeys: Set<String> = ["_a", "session_uuid", "trace", "aps"]
var custom: [String: Any] = [:]
for (key, value) in rawDict {
if !knownKeys.contains(key) {
custom[key] = value
}
}
self.customData = custom
}
// MARK: - URL Construction
/// Get the campaign page URL for this push notification
/// Uses the session_uuid to construct the URL: `{baseURL}/api/session/{session_uuid}`
/// - Returns: The campaign URL string, or nil if no session_uuid exists
public func getPageURL() -> String? {
guard let sessionUuid = sessionUuid, !sessionUuid.isEmpty else { return nil }
let baseURL = Configuration.baseURL
return "\(baseURL)/api/session/\(sessionUuid)"
}
/// Get the campaign image URL for this push notification
/// - Returns: The image URL string, or nil if no session_uuid exists
public func getImageURL() -> String? {
guard let sessionUuid = sessionUuid, !sessionUuid.isEmpty else { return nil }
let baseURL = Configuration.baseURL
return "\(baseURL)/api/session/\(sessionUuid)/image"
}
}
// MARK: - Error Handling Utilities
extension WarplySDK {
......@@ -2995,39 +3202,416 @@ public final class WarplySDK {
// MARK: - Push Notifications
/// Handle notification
public func handleNotification(_ payload: [String: Any]) {
// Pure Swift notification handling - access Warply directly if needed
// For now, we'll implement basic notification handling
print("Handling notification: \(payload)")
/// Whether device info has been sent during this session
private var hasSentDeviceInfo: Bool = false
/// Stored pending notification (received when app was closed)
private var pendingNotificationPayload: WarplyNotificationPayload?
/// Custom push handler delegate for action != 0 notifications
public weak var pushHandlerDelegate: WarplyPushHandler?
/// Handle notification payload
/// Parses the payload and routes based on action type
/// - Parameters:
/// - payload: Raw push notification payload dictionary
/// - state: Application state when notification was received (defaults to .active)
public func handleNotification(_ payload: [String: Any], appState state: WarplyApplicationState = .active) {
let notificationPayload = WarplyNotificationPayload(userInfo: payload)
// TODO: Implement pure Swift notification handling
// This may require accessing Warply/ directory directly for infrastructure
guard notificationPayload.isWarplyNotification else {
print("ℹ️ [WarplySDK] Push notification is not from Warply (no '_a' key) — ignoring")
return
}
/// Check for loyalty SDK notification
public func checkForLoyaltySDKNotification(_ payload: [String: Any]) -> Bool {
// Pure Swift notification checking - basic implementation
// TODO: Implement proper notification checking logic
print("Checking for loyalty SDK notification: \(payload)")
print("📱 [WarplySDK] Handling Warply notification:")
print(" Action: \(notificationPayload.action)")
print(" Session UUID: \(notificationPayload.sessionUuid ?? "nil")")
print(" Message: \(notificationPayload.message ?? "nil")")
print(" App State: \(state)")
// Log push received analytics
if let sessionUuid = notificationPayload.sessionUuid {
logPushReceived(sessionUuid: sessionUuid)
}
// Post push notification received event (dual system)
postFrameworkEvent("push_notification_received", sender: notificationPayload.rawPayload)
// For now, assume it's a loyalty SDK notification if it contains certain keys
if payload["loyalty_sdk"] != nil || payload["warply"] != nil {
// Route based on action
if notificationPayload.action != 0 {
// Custom action — forward to delegate
print("📱 [WarplySDK] Custom push action (\(notificationPayload.action)) — forwarding to pushHandlerDelegate")
pushHandlerDelegate?.didReceiveWarplyNotification(notificationPayload, whileAppWasIn: state)
} else {
// Default Warply handling (action == 0) — show rich push via CampaignViewController
print("📱 [WarplySDK] Default Warply push (action == 0) — will present campaign WebView")
switch state {
case .active:
// App is in foreground — show alert first, then present on confirmation
showPushAlertAndPresent(notificationPayload)
case .background:
// App was in background — present immediately
if let sessionUuid = notificationPayload.sessionUuid {
showPushCampaign(sessionUuid: sessionUuid)
}
case .closed:
// App was closed — store as pending for later handling
pendingNotificationPayload = notificationPayload
print("📱 [WarplySDK] Notification stored as pending (app was closed)")
}
}
// Post Dynatrace analytics
let dynatraceEvent = LoyaltySDKDynatraceEventModel()
dynatraceEvent._eventName = "custom_success_push_notification_handled_loyalty"
dynatraceEvent._parameters = nil
postFrameworkEvent("dynatrace", sender: dynatraceEvent)
}
/// Check if a push notification belongs to the Warply/Loyalty SDK
/// Uses the `"_a"` key which is the Warply push notification identifier
/// - Parameter payload: Raw push notification payload dictionary
/// - Returns: `true` if this is a Warply notification, `false` otherwise
public func checkForLoyaltySDKNotification(_ payload: [String: Any]) -> Bool {
// Check for "_a" key — the actual Warply push notification identifier
// (Old ObjC code: [userInfo valueForKey:@"_a"])
if payload["_a"] != nil {
print("✅ [WarplySDK] Warply push notification detected (contains '_a' key)")
handleNotification(payload)
return true
}
print("ℹ️ [WarplySDK] Not a Warply push notification (no '_a' key)")
return false
}
/// Update device token
/// Update device token and send comprehensive device info to server
/// Called by the host app from `didRegisterForRemoteNotificationsWithDeviceToken:`
/// - Parameter newDeviceToken: The APNS device token as a hex string
public func updateDeviceToken(_ newDeviceToken: String) {
// Pure Swift device token handling - basic implementation
// TODO: Implement proper device token handling
print("Updating device token: \(newDeviceToken)")
print("📱 [WarplySDK] Updating device token: \(newDeviceToken.prefix(16))...")
// Store device token for future use
// Store device token locally
UserDefaults.standard.set(newDeviceToken, forKey: "device_token")
// Send comprehensive device info to server (includes the token)
sendDeviceInfoIfNeeded()
// Post Dynatrace analytics
let dynatraceEvent = LoyaltySDKDynatraceEventModel()
dynatraceEvent._eventName = "custom_success_device_token_updated_loyalty"
dynatraceEvent._parameters = nil
postFrameworkEvent("dynatrace", sender: dynatraceEvent)
}
// MARK: - Device Info Management
/// Build comprehensive device info dictionary
/// Equivalent of old ObjC `WLPushManager.deviceInfo` + `WLPushManager.applicationData`
/// - Returns: Dictionary with all device and application information
public func buildDeviceInfo() -> [String: Any] {
var deviceInfo: [String: Any] = [:]
// Device token
let deviceToken = UserDefaults.standard.string(forKey: "device_token") ?? ""
if !deviceToken.isEmpty {
deviceInfo["device_token"] = deviceToken
}
// Platform identification
deviceInfo["vendor"] = "apple"
deviceInfo["platform"] = "ios"
deviceInfo["os_version"] = UIDevice.current.systemVersion
// Device identification
deviceInfo["unique_device_id"] = UIDevice.current.identifierForVendor?.uuidString ?? ""
deviceInfo["ios_system_name"] = UIDevice.current.systemName
deviceInfo["ios_system_version"] = UIDevice.current.systemVersion
deviceInfo["ios_model"] = UIDevice.current.modelName
deviceInfo["ios_localized_model"] = UIDevice.current.localizedModel
// Locale & Language
deviceInfo["ios_locale"] = Locale.current.identifier
let prefLangs = Locale.preferredLanguages
let langsString = prefLangs.prefix(5).joined(separator: ", ")
if !langsString.isEmpty {
deviceInfo["ios_languages"] = langsString
}
// Screen resolution
let screenWidth = UIScreen.main.bounds.width * UIScreen.main.scale
let screenHeight = UIScreen.main.bounds.height * UIScreen.main.scale
deviceInfo["screen_resolution"] = String(format: "%.0fx%.0f", screenWidth, screenHeight)
// Identifier for vendor (IDFV)
deviceInfo["identifier_for_vendor"] = UIDevice.current.identifierForVendor?.uuidString ?? ""
// Development flag
#if DEBUG
deviceInfo["development"] = "true"
#else
deviceInfo["development"] = "false"
#endif
// Application data (merged — was separate `applicationData` in old ObjC)
let mainBundle = Bundle.main
if let bundleId = mainBundle.bundleIdentifier, !bundleId.isEmpty {
deviceInfo["bundle_identifier"] = bundleId
}
if let appVersion = mainBundle.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String, !appVersion.isEmpty {
deviceInfo["app_version"] = appVersion
}
if let buildVersion = mainBundle.object(forInfoDictionaryKey: "CFBundleVersion") as? String, !buildVersion.isEmpty {
deviceInfo["app_build"] = buildVersion
}
deviceInfo["sdk_version"] = "2.3.0"
return deviceInfo
}
/// Send device info to server if it has changed since last send
/// Equivalent of old ObjC `WLPushManager.sendDeviceInfoIfNecessary`
public func sendDeviceInfoIfNeeded() {
let currentDeviceInfo = buildDeviceInfo()
// Compare with last sent device info
let oldDeviceInfo = UserDefaults.standard.dictionary(forKey: "warply_last_sent_device_info")
let hasChanged = !NSDictionary(dictionary: currentDeviceInfo).isEqual(to: oldDeviceInfo ?? [:])
if hasChanged || !hasSentDeviceInfo {
print("📱 [WarplySDK] Device info has changed or not yet sent — sending to server")
sendDeviceInfo()
} else {
print("ℹ️ [WarplySDK] Device info unchanged — skipping send")
}
}
/// Force send comprehensive device info to server
/// Equivalent of old ObjC `WLPushManager.sendDeviceInfo`
public func sendDeviceInfo() {
let deviceInfo = buildDeviceInfo()
print("📤 [WarplySDK] Sending device info to server:")
print(" Device token: \((deviceInfo["device_token"] as? String)?.prefix(16) ?? "none")...")
print(" Platform: \(deviceInfo["platform"] ?? "unknown")")
print(" OS Version: \(deviceInfo["os_version"] ?? "unknown")")
print(" Model: \(deviceInfo["ios_model"] ?? "unknown")")
print(" Bundle ID: \(deviceInfo["bundle_identifier"] ?? "unknown")")
// Wrap in event structure matching old ObjC format
let deviceInfoPayload: [String: Any] = [
"device_info": deviceInfo
]
let applicationData: [String: Any] = [
"bundle_identifier": deviceInfo["bundle_identifier"] ?? "",
"app_version": deviceInfo["app_version"] ?? "",
"app_build": deviceInfo["app_build"] ?? "",
"sdk_version": deviceInfo["sdk_version"] ?? ""
]
let applicationDataPayload: [String: Any] = [
"application_data": applicationData
]
Task {
// Send device_info event
await networkService.sendDeviceInfo(deviceInfoPayload)
// Send application_data event
await networkService.sendDeviceInfo(applicationDataPayload)
await MainActor.run {
self.hasSentDeviceInfo = true
// Store last sent device info for change detection
UserDefaults.standard.set(deviceInfo, forKey: "warply_last_sent_device_info")
print("✅ [WarplySDK] Device info sent to server successfully")
}
}
}
// MARK: - Push Analytics
/// Log push notification received analytics event
/// Equivalent of old ObjC `WLAnalyticsManager.logUserReceivedPush:`
/// - Parameter sessionUuid: The session UUID from the push notification
private func logPushReceived(sessionUuid: String) {
print("📊 [WarplySDK] Logging push received: NB_PushReceived (session: \(sessionUuid))")
Task {
await networkService.sendEvent(
eventName: "NB_PushReceived",
priority: true
)
}
}
/// Log push notification engaged analytics event
/// Equivalent of old ObjC `WLAnalyticsManager.logUserEngagedPush:`
/// - Parameter sessionUuid: The session UUID from the push notification
private func logPushEngaged(sessionUuid: String) {
print("📊 [WarplySDK] Logging push engaged: NB_PushAck (session: \(sessionUuid))")
Task {
await networkService.sendEvent(
eventName: "NB_PushAck",
priority: true
)
}
}
// MARK: - Rich Push Presentation
/// Show alert when push received in foreground, then present campaign on confirmation
/// Equivalent of old ObjC UIAlertView in `didReceiveRemoteNotification:whileAppWasInState:` for `.active`
private func showPushAlertAndPresent(_ payload: WarplyNotificationPayload) {
guard let topVC = getTopViewController() else {
print("⚠️ [WarplySDK] Cannot show push alert — no top view controller found")
return
}
let alertMessage = payload.message ?? "You have a new notification"
let alert = UIAlertController(
title: alertMessage,
message: nil,
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: NSLocalizedString("Close", comment: "Warply"), style: .cancel, handler: nil))
alert.addAction(UIAlertAction(title: NSLocalizedString("View", comment: "Warply"), style: .default, handler: { [weak self] _ in
// User chose to view — log engagement and show campaign
if let sessionUuid = payload.sessionUuid {
self?.logPushEngaged(sessionUuid: sessionUuid)
self?.showPushCampaign(sessionUuid: sessionUuid)
}
}))
topVC.present(alert, animated: true, completion: nil)
}
/// Present a campaign WebView for a push notification
/// Equivalent of old ObjC `WLPushManager.showItem:`
/// - Parameter sessionUuid: The session UUID from the push notification
public func showPushCampaign(sessionUuid: String) {
guard !sessionUuid.isEmpty else {
print("⚠️ [WarplySDK] Cannot show push campaign — empty session UUID")
return
}
guard let topVC = getTopViewController() else {
print("⚠️ [WarplySDK] Cannot show push campaign — no top view controller found")
return
}
// Build campaign URL: {baseURL}/api/session/{session_uuid}
let campaignUrl = "\(Configuration.baseURL)/api/session/\(sessionUuid)"
// Build params JSON
let tempCampaign = CampaignItemModel()
tempCampaign.session_uuid = sessionUuid
let params = constructCampaignParams(tempCampaign)
print("📱 [WarplySDK] Presenting push campaign WebView:")
print(" URL: \(campaignUrl)")
print(" Session UUID: \(sessionUuid)")
// Create and present CampaignViewController
let campaignVC = CampaignViewController()
campaignVC.campaignUrl = campaignUrl
campaignVC.params = params
campaignVC.showHeader = false
campaignVC.isPresented = true
let navController = UINavigationController(rootViewController: campaignVC)
navController.navigationBar.isHidden = true
navController.modalPresentationStyle = .fullScreen
topVC.present(navController, animated: true, completion: nil)
}
/// Handle pending notification that was received when app was closed
/// Call this from your root view controller's `viewDidAppear`
/// - Returns: `true` if a pending notification existed and was handled
@discardableResult
public func handlePendingNotification() -> Bool {
guard let pendingPayload = pendingNotificationPayload else {
return false
}
print("📱 [WarplySDK] Handling pending notification (was stored when app was closed)")
// Log engagement
if let sessionUuid = pendingPayload.sessionUuid {
logPushEngaged(sessionUuid: sessionUuid)
showPushCampaign(sessionUuid: sessionUuid)
}
// Clear pending notification
pendingNotificationPayload = nil
return true
}
// MARK: - Push Notification Helpers
/// Get the topmost presented view controller for modal presentation
/// - Returns: The topmost view controller, or nil if not found
private func getTopViewController() -> UIViewController? {
guard let windowScene = UIApplication.shared.connectedScenes
.compactMap({ $0 as? UIWindowScene })
.first(where: { $0.activationState == .foregroundActive }),
let window = windowScene.windows.first(where: { $0.isKeyWindow }),
var topController = window.rootViewController else {
return nil
}
while let presented = topController.presentedViewController {
topController = presented
}
return topController
}
/// Handle launch options — check if app was launched from a push notification
/// Call this from `application(_:didFinishLaunchingWithOptions:)`
/// - Parameter launchOptions: The launch options dictionary
public func handleLaunchOptions(_ launchOptions: [UIApplication.LaunchOptionsKey: Any]?) {
guard let launchOptions = launchOptions else { return }
// Check for remote notification in launch options
if let remoteNotification = launchOptions[.remoteNotification] as? [String: Any] {
print("📱 [WarplySDK] App launched from push notification")
// Check if it's a Warply notification
if remoteNotification["_a"] != nil {
let payload = WarplyNotificationPayload(userInfo: remoteNotification)
pendingNotificationPayload = payload
print("📱 [WarplySDK] Warply push notification stored as pending (app was closed)")
// Log received
if let sessionUuid = payload.sessionUuid {
logPushReceived(sessionUuid: sessionUuid)
}
}
}
}
/// Reset the app badge number to 0
/// Equivalent of old ObjC `WLPushManager.resetBadge`
public func resetBadge() {
DispatchQueue.main.async {
UIApplication.shared.applicationIconBadgeNumber = 0
print("📱 [WarplySDK] Badge reset to 0")
}
}
// MARK: - Event System (Public Access for Clients)
......
......@@ -226,7 +226,7 @@ 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?
......@@ -236,6 +236,40 @@ public struct CouponsRetrievedEvent: WarplyEvent {
}
}
/// 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`)
......