Push Notification Delivery Optimization

When Whistl detects elevated risk, push notifications deliver life-saving intervention messages. This technical guide explains APNs and FCM integration, delivery optimization, timing strategies, and how critical notifications bypass Do Not Disturb while respecting user preferences.

Why Notification Delivery Matters

Intervention notifications are time-critical:

  • Impulse window: Average urge lasts 15-30 minutes
  • Delivery latency: Every second counts
  • Engagement rate: 73% of users act on intervention notifications
  • Prevention impact: Notifications prevent $420 average loss per episode

Reliable, timely delivery is essential for effective intervention.

Push Notification Architecture

Whistl uses platform-specific push services for reliable delivery:

iOS: Apple Push Notification Service (APNs)

import UserNotifications

class NotificationManager {
    func requestAuthorization() async -> Bool {
        let center = UNUserNotificationCenter.current()
        
        // Request permissions
        let granted = try? await center.requestAuthorization(
            options: [.alert, .badge, .sound, .criticalAlert]
        )
        
        // Register for remote notifications
        if granted == true {
            await MainActor.run {
                UIApplication.shared.registerForRemoteNotifications()
            }
        }
        
        return granted ?? false
    }
    
    func getDeviceToken() async -> String? {
        // Device token provided by AppDelegate
        // Sent to Whistl server for targeting
        return await deviceToken
    }
}

Android: Firebase Cloud Messaging (FCM)

import com.google.firebase.messaging.FirebaseMessaging

class NotificationManager(private val context: Context) {
    fun getToken(): String? {
        return FirebaseMessaging.getInstance().token
            .getResult { token ->
                // Send token to Whistl server
                sendTokenToServer(token)
            }
            .exception { e ->
                // Handle error
            }
    }
}

// FirebaseMessagingService for receiving notifications
class WhistlMessagingService : FirebaseMessagingService() {
    override fun onMessageReceived(remoteMessage: RemoteMessage) {
        // Handle notification payload
        val notification = remoteMessage.notification
        val data = remoteMessage.data
        
        showNotification(notification, data)
    }
    
    override fun onNewToken(token: String) {
        // Token refreshed - update server
        sendTokenToServer(token)
    }
}

Notification Payload Structure

Whistl notifications include rich payload data for intervention:

APNs Payload

{
  "aps": {
    "alert": {
      "title": "Your risk is elevated",
      "body": "We noticed you're near Crown Casino. Want to call your sponsor?"
    },
    "sound": "intervention.caf",
    "badge": 1,
    "category": "INTERVENTION",
    "thread-id": "whistl-intervention",
    "interruption-level": "time-sensitive"
  },
  "whistl": {
    "type": "venue_proximity",
    "risk_score": 0.78,
    "venue_name": "Crown Casino",
    "action_url": "whistl://intervention/venue",
    "intervention_id": "int-12345",
    "priority": "high"
  }
}

FCM Payload

{
  "to": "device_token",
  "priority": "high",
  "notification": {
    "title": "Your risk is elevated",
    "body": "We noticed you're near Crown Casino. Want to call your sponsor?",
    "sound": "intervention.wav",
    "android_channel_id": "whistl-intervention",
    "click_action": "whistl://intervention/venue"
  },
  "data": {
    "type": "venue_proximity",
    "risk_score": "0.78",
    "venue_name": "Crown Casino",
    "intervention_id": "int-12345",
    "priority": "high"
  },
  "android": {
    "priority": "high",
    "ttl": "300s",
    "notification": {
      "priority": "high",
      "visibility": "public"
    }
  }
}

Notification Priority Levels

Whistl uses different priority levels based on risk:

Priority Classification

Risk LevelAPNs PriorityFCM PriorityDelivery Target
Low5 (normal)normal<30 minutes
Moderate5 (normal)normal<10 minutes
High10 (high)high<1 minute
Critical10 (high) + critical alerthigh + high priority<10 seconds

Critical Alerts (iOS)

Critical alerts bypass Do Not Disturb and silent mode:

// Requires special entitlement from Apple
// com.apple.developer.usernotifications.critical-alerts

let content = UNMutableNotificationContent()
content.title = "Critical: Spending detected"
content.body = "Transaction at Sportsbet blocked. Tap to review."
content.sound = UNNotificationSound(
    named: UNNotificationSoundName("critical-intervention.caf"),
    withAudioVolume: 1.0  // Maximum volume
)
content.interruptionLevel = .critical

let request = UNNotificationRequest(
    identifier: "critical-intervention",
    content: content,
    trigger: nil  // Immediate
)

UNUserNotificationCenter.current().add(request)

Delivery Optimization Strategies

Multiple techniques ensure reliable delivery:

1. Token Freshness

class TokenManager {
    func validateToken(_ token: String) async -> Bool {
        // Send test notification
        let delivered = await pushService.sendTest(token)
        
        if !delivered {
            // Token expired - request new one
            await requestNewToken()
            return false
        }
        
        return true
    }
    
    // Refresh tokens weekly
    func scheduleTokenRefresh() {
        Timer.scheduledTimer(withTimeInterval: 7 * 24 * 3600, repeats: true) { _ in
            Task {
                await self.refreshToken()
            }
        }
    }
}

2. Retry Logic

class NotificationSender {
    func sendWithRetry(
        notification: Notification,
        maxRetries: Int = 3
    ) async -> Bool {
        var lastError: Error?
        
        for attempt in 1...maxRetries {
            do {
                try await pushService.send(notification)
                return true
            } catch {
                lastError = error
                
                // Exponential backoff
                let delay = pow(2.0, Double(attempt))
                try? await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000))
            }
        }
        
        // All retries failed - log and alert
        logDeliveryFailure(notification, error: lastError)
        return false
    }
}

3. Fallback Channels

If push fails, Whistl uses alternative channels:

  • SMS fallback: Critical alerts via Twilio
  • Email fallback: Non-urgent notifications
  • In-app messages: When app is opened
  • Partner notification: Alert accountability partner

Timing Optimization

Notification timing affects engagement:

Optimal Send Times

Notification TypeOptimal TimingEngagement Rate
Critical interventionImmediate89%
Risk alertWithin 30 seconds76%
Daily briefing8:00 AM local time68%
Weekly summarySunday 7:00 PM54%
Goal reminderRandom 2-5 PM61%

Quiet Hours

Respect user sleep while maintaining critical protection:

struct QuietHours {
    let start: Int  // 22 = 10 PM
    let end: Int    // 7 = 7 AM
    
    func shouldDeliver(_ notification: Notification) -> Bool {
        let hour = Calendar.current.component(.hour, from: Date())
        
        // Critical alerts always deliver
        if notification.priority == .critical {
            return true
        }
        
        // Non-critical during quiet hours: suppress
        if hour >= start || hour < end {
            return false
        }
        
        return true
    }
}

Notification Categories

Whistl defines custom notification categories with actions:

iOS Notification Categories

func registerNotificationCategories() {
    // Intervention category with actions
    let callAction = UNNotificationAction(
        identifier: "CALL_SPONSOR",
        title: "Call Sponsor",
        options: .foreground
    )
    
    let breatheAction = UNNotificationAction(
        identifier: "START_BREATHING",
        title: "Breathe",
        options: .foreground
    )
    
    let dismissAction = UNNotificationAction(
        identifier: "DISMISS",
        title: "I'm OK",
        options: .destructive
    )
    
    let interventionCategory = UNNotificationCategory(
        identifier: "INTERVENTION",
        actions: [callAction, breatheAction, dismissAction],
        intentIdentifiers: [],
        options: .customDismissAction
    )
    
    UNUserNotificationCenter.current().setNotificationCategories([interventionCategory])
}

Android Notification Channels

fun createNotificationChannels() {
    val notificationManager = getSystemService(NotificationManager::class.java)
    
    // Critical intervention channel
    val criticalChannel = NotificationChannel(
        "whistl-critical",
        "Critical Interventions",
        NotificationManager.IMPORTANCE_HIGH
    ).apply {
        description = "Life-saving intervention notifications"
        enableVibration(true)
        vibrationPattern = longArrayOf(0, 500, 200, 500)
        lockscreenVisibility = Notification.VISIBILITY_PUBLIC
        bypassDnd = true
    }
    
    // Standard intervention channel
    val interventionChannel = NotificationChannel(
        "whistl-intervention",
        "Interventions",
        NotificationManager.IMPORTANCE_DEFAULT
    ).apply {
        description = "Risk alerts and coaching messages"
        enableVibration(true)
        lockscreenVisibility = Notification.VISIBILITY_PUBLIC
    }
    
    // Info channel
    val infoChannel = NotificationChannel(
        "whistl-info",
        "Updates & Tips",
        NotificationManager.IMPORTANCE_LOW
    ).apply {
        description = "Educational content and progress updates"
        enableVibration(false)
        lockscreenVisibility = Notification.VISIBILITY_PRIVATE
    }
    
    notificationManager.createNotificationChannels(
        listOf(criticalChannel, interventionChannel, infoChannel)
    )
}

Delivery Metrics

Whistl tracks comprehensive notification metrics:

Key Performance Indicators

MetricTargetActual
Delivery Rate>95%97.3%
Time to Deliver (critical)<10s4.2s average
Open Rate (intervention)>70%73%
Action Rate>50%58%
Token Refresh Success>99%99.4%

Delivery Funnel Analysis

// Track each stage of delivery
struct DeliveryFunnel {
    var sent: Int = 0
    var delivered: Int = 0
    var displayed: Int = 0
    var opened: Int = 0
    var actionTaken: Int = 0
    
    var deliveryRate: Double { Double(delivered) / Double(sent) }
    var displayRate: Double { Double(displayed) / Double(delivered) }
    var openRate: Double { Double(opened) / Double(displayed) }
    var actionRate: Double { Double(actionTaken) / Double(opened) }
}

Privacy Considerations

Notification content balances urgency with privacy:

Lock Screen Privacy

  • Critical alerts: Full content (safety priority)
  • Intervention alerts: Generic message on lock screen
  • Info notifications: Hidden until unlocked

Notification Content Options

// User can choose privacy level
enum NotificationPrivacy {
    case full  // Show all details
    case generic  // "You have a Whistl alert"
    case hidden  // No content on lock screen
    
    func formatContent(_ notification: Notification) -> String {
        switch self {
        case .full:
            return notification.body
        case .generic:
            return "You have a Whistl notification"
        case .hidden:
            return ""
        }
    }
}

Troubleshooting

Common notification issues and solutions:

Problem: Notifications Not Arriving

  • Cause: Token expired
  • Solution: Reinstall app or toggle notifications

Problem: Delayed Delivery

  • Cause: Battery optimization (Android)
  • Solution: Disable battery optimization for Whistl

Problem: No Sound

  • Cause: Notification channel muted
  • Solution: Check system notification settings

Conclusion

Reliable push notification delivery is critical for Whistl's intervention system. Through APNs and FCM integration, priority-based delivery, retry logic, and fallback channels, Whistl ensures life-saving messages reach users when they need them most.

All while respecting user preferences, quiet hours, and privacy settings.

Get Real-Time Intervention

Whistl's push notifications deliver life-saving interventions when you need them most. Download free and enable notifications for complete protection.

Download Whistl Free

Related: 8-Step Negotiation Engine | AI Financial Coach | 27 Risk Signals