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 oldWLPushManager.showItem: - ✅ Provide convenience wrappers for
UNUserNotificationCenterDelegatemethods (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:
// 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 ✅
-
Step 1.1: Create
WarplyNotificationPayloadmodel- Swift struct to parse Warply push notification payloads
- Key fields:
action(from"_a"),sessionUuid(from"session_uuid"),message,customData - Parse
apsdictionary for alert, badge, sound, category - Equivalent of old
WLBaseItem.initWithAttributes: -
File: New section in
Models/or withinWarplySDK.swift
-
Step 1.2: Create
WarplyPushHandlerprotocol- Swift equivalent of
WLCustomPushHandler - Method:
func didReceiveWarplyNotification(_ payload: WarplyNotificationPayload, whileAppWasIn state: WarplyApplicationState) - Enum:
WarplyApplicationState { case active, background, closed } -
File: New protocol definition in
WarplySDK.swiftor dedicated file
- Swift equivalent of
Phase 2: Device Token Management ✅
-
Step 2.1: Fix
updateDeviceToken()inWarplySDK.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
-
Step 2.2: Enhance
Endpoint.sendDeviceInfoto 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 -
developmentflag (DEBUG vs release) -
File:
Network/Endpoints.swift— updatesendDeviceInfocase parameters and body
- Currently only sends
-
Step 2.3: Add
sendDeviceInfo()method toWarplySDK.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 ✅
-
Step 3.1: Fix
checkForLoyaltySDKNotification()inWarplySDK.swift- Change detection from
"loyalty_sdk"/"warply"keys to"_a"key (the actual Warply identifier) - Parse payload into
WarplyNotificationPayload - Return
trueif"_a"key exists in payload - Call
handleNotification()if it's a Warply notification
- Change detection from
-
Step 3.2: Implement
handleNotification()inWarplySDK.swift- Parse payload into
WarplyNotificationPayload - Determine app state (active/background/closed)
- Route based on
actionfield: -
action == 0: Default Warply handling (post event for host app to show campaign) -
action != 0: Call custom push handler delegate if set - Post
SwiftEventBus+EventDispatcherevents for the notification - Post Dynatrace analytics event
- Parse payload into
-
Step 3.3: Add push analytics events
-
logPushReceived(sessionUuid:)— sendsNB_PushReceivedevent (equivalent of oldlogUserReceivedPush:) -
logPushEngaged(sessionUuid:)— sendsNB_PushAckevent (equivalent of oldlogUserEngagedPush:) - Use existing
Endpoint.sendEventfor analytics - Follows existing Dynatrace event pattern
-
-
Step 3.4: Add pending notification support
- Store pending notification when app was closed (received via
didFinishLaunchingWithOptions) -
handlePendingNotification()— processes stored notification, returnsBool - Host app calls this from their root view controller's
viewDidAppear
- Store pending notification when app was closed (received via
-
Step 3.5: Rich push presentation via
CampaignViewController(WebView)- For
action == 0pushes (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, setcampaignUrlandparams - 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
- For
Phase 4: Public API & Delegate ✅
-
Step 4.1: Add
pushHandlerDelegateproperty toWarplySDKweak var pushHandlerDelegate: WarplyPushHandler?- Host app sets this to receive custom push notifications (action != 0)
- Modern Swift alternative to old
WLCustomPushHandlerprotocol
-
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.activestate, shows alert - If not Warply push: calls completionHandler with
.banner, .sound, .badge(pass-through)
- If Warply push: calls
-
didReceiveNotificationResponse(_ response: UNNotificationResponse, completionHandler: ...)— handles user tap on notification- Determines app state, calls
handleNotification(), logs analytics (NB_PushAck) - Calls completionHandler after processing
- Determines app state, calls
-
-
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 ✅
-
Step 5.1: Update
CLIENT_DOCUMENTATION.mdPush Notifications section- Replace current minimal docs with comprehensive guide
- Include all 3 AppDelegate methods needed:
-
didFinishLaunchingWithOptions— handle launch from push -
didRegisterForRemoteNotificationsWithDeviceToken— send token to SDK -
didReceiveRemoteNotification— route notification through SDK - Show custom push handler setup
- Show event subscription for push notifications
-
Step 5.2: Update
skill.mdwith push notification architecture- Document new
WarplyNotificationPayloadmodel - Document
WarplyPushHandlerprotocol - Document push analytics events
- Update API reference section
- Document new
-
Step 5.3: Update
QUICK_REFERENCE.mdpush 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
sendDeviceInfoendpoint -
checkForLoyaltySDKNotification()returnstruefor payloads with"_a"key -
checkForLoyaltySDKNotification()returnsfalsefor 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:)presentsCampaignViewControllermodally -
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 logsNB_PushAck - No compilation errors introduced
- All existing public API remains backward compatible
⚠️ Important Notes
"_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.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.Device info sending uses the event system — In old ObjC, device info was sent via
WLEventSimple(type:"device_info") throughaddEvent:priority:. In Swift, we use the existingEndpoint.sendDeviceInfowhich POSTs to/api/async/info/{appUUID}/.Rich push WebView handled by framework — The old
showItem:presented aWLInboxItemViewControllerWebView for rich pushes. The modern framework maintains this behavior by reusing the existingCampaignViewController— foraction == 0pushes, the framework builds a campaign URL fromsession_uuid({baseURL}/api/session/{session_uuid}) and presentsCampaignViewControllermodally. This ensures full consistency with the old implementation.Keychain vs UserDefaults for device token — Old code used
WLKeychain. New code usesUserDefaults. Device tokens are not sensitive (they're sent to servers in plain text), soUserDefaultsis acceptable. However, we could optionally useKeychainManagerfor 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