๐ Solving Expo Push Notification Headaches
If you are running a React Native project using the Expo Bare Workflow (SDK 50+), you've likely hit a wall with iOS push notifications. Youโve set up your APNs .p8 key, but things still break.
๐ด The Problems
- The JS Timeout:
getExpoPushTokenAsyncconsistently throwsToken fetch timed out after 6000ms. - The Xcode Error:
error: cannot find 'EXNotificationsDelegate' in scope.
๐ Phase 1: Fixing the JavaScript Race Condition
On iOS, the first token generation is slow. If multiple components call getExpoPushToken at the same time, the native bridge can hang.
The Solution: A Global Fetch Lock
We use a Singleton Promise pattern. This ensures that even if 10 components call your token function at once, only one actual request is sent to the native side.
typescript
import * as Notifications from 'expo-notifications';
// ๐ Global lock to prevent bridge congestion
let globalTokenFetchPromise: Promise<string | null> | null = null;
export async function getExpoPushToken() {
if (globalTokenFetchPromise) {
return await globalTokenFetchPromise;
}
globalTokenFetchPromise = (async () => {
try {
// We wrap the Expo call in a race to control the timeout manually
const token = await Promise.race([
Notifications.getExpoPushTokenAsync({
projectId: 'your-project-id'
}),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Push Token Timeout')), 10000)
)
]) as Notifications.ExpoPushToken;
return token.data;
} catch (error) {
console.error("โ Push Token Error:", error);
return null;
}
})();
try {
return await globalTokenFetchPromise;
} finally {
// Reset so the next call can try again if the first failed
globalTokenFetchPromise = null;
}
}
## ๐ Phase 2: Solving the 'EXNotificationsDelegate' Build Error
In Expo SDK 50+, many developers encounter the error: `error: cannot find 'EXNotificationsDelegate' in scope`. This happens because `EXNotificationsDelegate` is an internal Objective-C class that is no longer exposed to Swift in the same way.
### The Modern Solution: Inherit from `ExpoAppDelegate`
Instead of manually trying to bridge Objective-C delegates, you should inherit your `AppDelegate` from `ExpoAppDelegate`. This class is part of the newer Expo Modules architecture and automatically handles the plumbing for APNs tokens and notification events.
---
import UIKit
import Expo
import ExpoModulesCore
import React
import React_RCTAppDelegate
@main
class AppDelegate: ExpoAppDelegate {
var window: UIWindow?
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
) -> Bool {
let delegate = ReactNativeDelegate()
let factory = RCTReactNativeFactory(delegate: delegate)
self.reactNativeDelegate = delegate
self.factory = factory
window = UIWindow(frame: UIScreen.main.bounds)
// โ ๏ธ Replace "YourAppName" with the name in your app.json
factory.startReactNative(withModuleName: "YourAppName", in: window, launchOptions: launchOptions)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
override func application(_ application: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
return super.application(application, open: url, options: options) || RCTLinkingManager.application(application, open: url, options: options)
}
}
Top comments (0)