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 Level | APNs Priority | FCM Priority | Delivery Target |
|---|---|---|---|
| Low | 5 (normal) | normal | <30 minutes |
| Moderate | 5 (normal) | normal | <10 minutes |
| High | 10 (high) | high | <1 minute |
| Critical | 10 (high) + critical alert | high + 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 Type | Optimal Timing | Engagement Rate |
|---|---|---|
| Critical intervention | Immediate | 89% |
| Risk alert | Within 30 seconds | 76% |
| Daily briefing | 8:00 AM local time | 68% |
| Weekly summary | Sunday 7:00 PM | 54% |
| Goal reminder | Random 2-5 PM | 61% |
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
| Metric | Target | Actual |
|---|---|---|
| Delivery Rate | >95% | 97.3% |
| Time to Deliver (critical) | <10s | 4.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 FreeRelated: 8-Step Negotiation Engine | AI Financial Coach | 27 Risk Signals