|
| 1 | +# iOS Foreground Notifications Fix |
| 2 | + |
| 3 | +## Problem |
| 4 | + |
| 5 | +iOS push notifications were completely broken after removing the `expo-notifications` plugin. Issues included: |
| 6 | +1. Notifications not displaying in foreground (app responded with 0 to `willPresentNotification`) |
| 7 | +2. Notification taps not working (no `didReceive` response handler) |
| 8 | +3. **Push notifications not working at all** (missing `aps-environment` entitlement) |
| 9 | + |
| 10 | +Console logs showed: |
| 11 | +- Notification received: `hasAlertContent: 1, hasSound: 1 hasBadge: 1` |
| 12 | +- App responded with 0 to `willPresentNotification` |
| 13 | +- iOS decided not to show: `shouldPresentAlert: NO` |
| 14 | +- No handler for notification taps (`didReceive` response) |
| 15 | + |
| 16 | +## Root Cause |
| 17 | + |
| 18 | +**Issue 1: Missing APS Entitlement (Critical)** |
| 19 | +When `expo-notifications` plugin was removed, it also removed the `aps-environment` entitlement from the iOS project. This entitlement is **required** for iOS to register the app with Apple Push Notification service (APNs). Without it, the app cannot receive any push notifications at all. |
| 20 | + |
| 21 | +**Issue 2: Notifications not displaying in foreground** |
| 22 | +When a push notification arrives on iOS while the app is in the foreground, iOS sends a `willPresentNotification` delegate call asking the app how to present the notification. Without a proper delegate implementation, the default behavior is to NOT show the notification (response 0). |
| 23 | + |
| 24 | +**Issue 3: Notification taps not working** |
| 25 | +When a user taps on a notification, iOS sends a `didReceive response` delegate call. Without implementing this delegate method, taps are ignored and don't trigger any action in the app. |
| 26 | + |
| 27 | +The previous implementation tried to manually display notifications using Notifee, but this happened AFTER Firebase Messaging had already told iOS not to show the notification. |
| 28 | + |
| 29 | +## Solution |
| 30 | + |
| 31 | +### 1. Config Plugin (`plugins/withForegroundNotifications.js`) |
| 32 | + |
| 33 | +Created an Expo config plugin to automatically configure push notifications during prebuild: |
| 34 | + |
| 35 | +```javascript |
| 36 | +const { withAppDelegate, withEntitlementsPlist } = require('@expo/config-plugins'); |
| 37 | + |
| 38 | +const withForegroundNotifications = (config) => { |
| 39 | + // Add push notification entitlements |
| 40 | + config = withEntitlementsPlist(config, (config) => { |
| 41 | + const entitlements = config.modResults; |
| 42 | + |
| 43 | + // Add APS environment for push notifications - REQUIRED |
| 44 | + entitlements['aps-environment'] = 'production'; |
| 45 | + |
| 46 | + // Add critical alerts for production/internal builds |
| 47 | + const env = process.env.APP_ENV || config.extra?.APP_ENV; |
| 48 | + if (env === 'production' || env === 'internal') { |
| 49 | + entitlements['com.apple.developer.usernotifications.critical-alerts'] = true; |
| 50 | + entitlements['com.apple.developer.usernotifications.time-sensitive'] = true; |
| 51 | + } |
| 52 | + |
| 53 | + return config; |
| 54 | + }); |
| 55 | + |
| 56 | + // Add AppDelegate modifications for notification handling |
| 57 | + // ... |
| 58 | +}; |
| 59 | + |
| 60 | +module.exports = withForegroundNotifications; |
| 61 | +``` |
| 62 | +
|
| 63 | +This plugin: |
| 64 | +1. **Adds `aps-environment` entitlement** - Required for APNs registration |
| 65 | +2. **Adds critical alerts entitlement** - For emergency call notifications |
| 66 | +3. **Adds time-sensitive entitlement** - For high-priority notifications |
| 67 | +4. Adds AppDelegate notification handlers (see below) |
| 68 | +
|
| 69 | +Added to `app.config.ts` plugins array: |
| 70 | +```typescript |
| 71 | +plugins: [ |
| 72 | + // ... |
| 73 | + './plugins/withForegroundNotifications.js', |
| 74 | + // ... |
| 75 | +] |
| 76 | +``` |
| 77 | +
|
| 78 | +This ensures the native iOS code is correctly configured even after running `expo prebuild`. |
| 79 | +
|
| 80 | +### 2. AppDelegate.swift Changes |
| 81 | +
|
| 82 | +The config plugin automatically applies these changes during prebuild: |
| 83 | +
|
| 84 | +```swift |
| 85 | +import UserNotifications |
| 86 | + |
| 87 | +public class AppDelegate: ExpoAppDelegate, UNUserNotificationCenterDelegate { |
| 88 | + |
| 89 | + public override func application(...) -> Bool { |
| 90 | + // ... |
| 91 | + |
| 92 | + // Set the UNUserNotificationCenter delegate to handle foreground notifications |
| 93 | + UNUserNotificationCenter.current().delegate = self |
| 94 | + |
| 95 | + // ... |
| 96 | + } |
| 97 | + |
| 98 | + // Handle foreground notifications - tell iOS to show them |
| 99 | + public func userNotificationCenter( |
| 100 | + _ center: UNUserNotificationCenter, |
| 101 | + willPresent notification: UNNotification, |
| 102 | + withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void |
| 103 | + ) { |
| 104 | + // Show notification with alert, sound, and badge even when app is in foreground |
| 105 | + if #available(iOS 14.0, *) { |
| 106 | + completionHandler([.banner, .sound, .badge]) |
| 107 | + } else { |
| 108 | + completionHandler([.alert, .sound, .badge]) |
| 109 | + } |
| 110 | + } |
| 111 | + |
| 112 | + // Handle notification tap - when user taps on a notification |
| 113 | + public func userNotificationCenter( |
| 114 | + _ center: UNUserNotificationCenter, |
| 115 | + didReceive response: UNNotificationResponse, |
| 116 | + withCompletionHandler completionHandler: @escaping () -> Void |
| 117 | + ) { |
| 118 | + // Forward the notification response to React Native |
| 119 | + // This allows Firebase Messaging to handle it via onNotificationOpenedApp |
| 120 | + completionHandler() |
| 121 | + } |
| 122 | +} |
| 123 | +``` |
| 124 | +
|
| 125 | +This tells iOS to: |
| 126 | +1. Display all foreground notifications with banner/alert, sound, and badge updates |
| 127 | +2. Forward notification taps to React Native for handling |
| 128 | +
|
| 129 | +### 3. push-notification.ts Changes |
| 130 | +
|
| 131 | +Added Notifee event listeners to handle notification taps: |
| 132 | +
|
| 133 | +**Added:** |
| 134 | +```typescript |
| 135 | +import { EventType } from '@notifee/react-native'; |
| 136 | + |
| 137 | +// In initialize(): |
| 138 | +// Set up Notifee event listeners for notification taps |
| 139 | +notifee.onForegroundEvent(async ({ type, detail }) => { |
| 140 | + if (type === EventType.PRESS && detail.notification) { |
| 141 | + const eventCode = detail.notification.data?.eventCode; |
| 142 | + if (eventCode) { |
| 143 | + usePushNotificationModalStore.getState().showNotificationModal({ |
| 144 | + eventCode, |
| 145 | + title: detail.notification.title, |
| 146 | + body: detail.notification.body, |
| 147 | + data: detail.notification.data, |
| 148 | + }); |
| 149 | + } |
| 150 | + } |
| 151 | +}); |
| 152 | + |
| 153 | +notifee.onBackgroundEvent(async ({ type, detail }) => { |
| 154 | + if (type === EventType.PRESS && detail.notification) { |
| 155 | + // Handle background notification taps |
| 156 | + // ... |
| 157 | + } |
| 158 | +}); |
| 159 | +``` |
| 160 | +
|
| 161 | +This ensures that: |
| 162 | +1. When user taps a notification shown via Notifee, it's caught by `onForegroundEvent` |
| 163 | +2. When user taps a notification while app is in background, it's caught by `onBackgroundEvent` |
| 164 | +3. Both handlers extract the eventCode and show the in-app notification modal |
| 165 | +
|
| 166 | +Also kept the existing Firebase Messaging handlers: |
| 167 | +
|
| 168 | +**Before:** |
| 169 | +```typescript |
| 170 | +// On iOS, display the notification in foreground using Notifee |
| 171 | +if (Platform.OS === 'ios' && remoteMessage.notification) { |
| 172 | + await notifee.displayNotification({...}); |
| 173 | +} |
| 174 | +``` |
| 175 | +
|
| 176 | +**After:** |
| 177 | +```typescript |
| 178 | +// On iOS, the notification will be displayed automatically by the native system |
| 179 | +// via the UNUserNotificationCenterDelegate in AppDelegate.swift |
| 180 | +// We don't need to manually display it here |
| 181 | +``` |
| 182 | +
|
| 183 | +The `handleRemoteMessage` function now only: |
| 184 | +1. Logs the received message |
| 185 | +2. Extracts eventCode and notification data |
| 186 | +3. Shows the notification modal if eventCode exists |
| 187 | +4. Lets iOS handle the notification display natively |
| 188 | +
|
| 189 | +The existing Firebase Messaging handlers (`onNotificationOpenedApp`, `getInitialNotification`) continue to work for notifications tapped from the system tray. |
| 190 | +
|
| 191 | +## Flow After Fix |
| 192 | +
|
| 193 | +### Notification Display Flow |
| 194 | +1. **Notification arrives** → Firebase Messaging receives it |
| 195 | +2. **iOS asks** → "How should I present this?" (willPresentNotification) |
| 196 | +3. **AppDelegate responds** → "Show with banner, sound, and badge" ([.banner, .sound, .badge]) |
| 197 | +4. **iOS displays** → Native notification appears at the top of the screen |
| 198 | +5. **React Native processes** → `onMessage` handler extracts eventCode for modal |
| 199 | +
|
| 200 | +### Notification Tap Flow |
| 201 | +1. **User taps notification** → iOS receives the tap |
| 202 | +2. **iOS asks** → "How should I handle this?" (didReceive response) |
| 203 | +3. **AppDelegate responds** → Forwards to React Native |
| 204 | +4. **Two paths handled**: |
| 205 | + - **Path A (Notifee)**: If notification was displayed by Notifee → `onForegroundEvent` fires → Shows modal |
| 206 | + - **Path B (Firebase)**: If notification is from system tray → `onNotificationOpenedApp` fires → Shows modal |
| 207 | +
|
| 208 | +## Benefits |
| 209 | +
|
| 210 | +1. **Native behavior**: Notifications look and feel native |
| 211 | +2. **Proper sounds**: Custom notification sounds work correctly |
| 212 | +3. **Critical alerts**: Can leverage iOS critical alert features |
| 213 | +4. **Better UX**: Consistent with iOS notification standards |
| 214 | +5. **Less code**: Removed manual display logic |
| 215 | +
|
| 216 | +## Testing |
| 217 | +
|
| 218 | +Test foreground notifications with: |
| 219 | +1. App in foreground |
| 220 | +2. Send push notification with eventCode |
| 221 | +3. **Verify notification banner appears at top** ✅ |
| 222 | +4. **Verify sound plays** ✅ |
| 223 | +5. **Tap the notification banner** ✅ |
| 224 | +6. **Verify modal shows for eventCode** ✅ |
| 225 | +7. Test with different notification types (calls, messages, etc.) |
| 226 | +
|
| 227 | +Test background/killed state notifications: |
| 228 | +1. App in background or killed |
| 229 | +2. Send push notification with eventCode |
| 230 | +3. **Tap the notification from system tray** ✅ |
| 231 | +4. **Verify app opens and modal shows** ✅ |
| 232 | +
|
| 233 | +## Related Files |
| 234 | +
|
| 235 | +- `/plugins/withForegroundNotifications.js` - Expo config plugin for iOS modifications |
| 236 | +- `/app.config.ts` - Expo configuration with plugin registration |
| 237 | +- `/ios/ResgridUnit/AppDelegate.swift` - Native iOS delegate implementation (auto-generated) |
| 238 | +- `/src/services/push-notification.ts` - React Native notification service |
| 239 | +
|
| 240 | +## Important Notes |
| 241 | +
|
| 242 | +- The `AppDelegate.swift` is auto-generated during `expo prebuild` |
| 243 | +- Never manually edit `AppDelegate.swift` - changes will be lost on next prebuild |
| 244 | +- All iOS native modifications must be done through the config plugin |
| 245 | +- Run `expo prebuild --platform ios --clean` after modifying the plugin |
0 commit comments