PUSH_NOTIFICATIONS_MIGRATION_PLAN.md 18.8 KB

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:

// 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 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
  • 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 ✅

  • 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
  • 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
  • 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 ✅

  • 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
  • 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
  • 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
  • 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
  • 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 ✅

  • 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
  • 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
  • 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.md Push 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.md with push notification architecture

    • Document new WarplyNotificationPayload model
    • Document WarplyPushHandler protocol
    • Document push analytics events
    • Update API reference section
  • 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
deviceInfoNSDictionary buildDeviceInfo()[String: Any] Comprehensive device info dict
applicationDataNSDictionary (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