I’m making an attempt to implement VoIP push notifications in my React Native app utilizing react-native-voip-push-notification. Common push notifications (FCM & Apple notifications) are working, however the VoIP token by no means arrives.
I’ve:
Added Push Notifications entitlement within the provisioning profile and entitlements file.
Enabled VoIP in background modes in Xcode.
Up to date Data.plist:
UIBackgroundModes
audio
remote-notification
voip
UIFileSharingEnabled
UILaunchStoryboardName
LaunchScreen
UIRequiredDeviceCapabilities
arm64
UISupportedInterfaceOrientations
UIInterfaceOrientationPortrait
UISupportedInterfaceOrientations~ipad
UIInterfaceOrientationPortrait
UIInterfaceOrientationLandscapeLeft
UIInterfaceOrientationLandscapeRight
UIViewControllerBasedStatusBarAppearance
AppDelegate header (AppDelegate.h):
#import
#import
#import
#import
@interface AppDelegate : RCTAppDelegate
@finish
AppDelegate implementation (AppDelegate.mm):
#import "AppDelegate.h"
#import
#import
#import
@implementation AppDelegate
- (BOOL)software:(UIApplication *)software didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
NSLog(@" [NATIVE] ========================================");
NSLog(@" [NATIVE] App Launching - didFinishLaunchingWithOptions");
NSLog(@" [NATIVE] ========================================");
self.moduleName = @"sihspatient";
self.initialProps = @{};
if ([FIRApp defaultApp] == nil) {
[FIRApp configure];
NSLog(@" [NATIVE] Firebase configured");
}
BOOL consequence = [super application:application didFinishLaunchingWithOptions:launchOptions];
NSLog(@" [NATIVE] React Native bridge initialized");
[self voipRegistration];
return consequence;
}
- (void)voipRegistration {
NSLog(@" [NATIVE-VOIP] ========================================");
NSLog(@" [NATIVE-VOIP] Beginning VoIP Registration");
NSLog(@" [NATIVE-VOIP] ========================================");
PKPushRegistry *pushRegistry = [[PKPushRegistry alloc] initWithQueue:dispatch_get_main_queue()];
NSLog(@" [NATIVE-VOIP] PKPushRegistry occasion created");
pushRegistry.delegate = self;
NSLog(@" [NATIVE-VOIP] Delegate set to AppDelegate");
pushRegistry.desiredPushTypes = [NSSet setWithObject:PKPushTypeVoIP];
NSLog(@" [NATIVE-VOIP] Desired push sorts set: PKPushTypeVoIP");
NSLog(@" [NATIVE-VOIP] Ready for Apple to name didUpdatePushCredentials...");
NSLog(@" [NATIVE-VOIP] This will likely take 2-10 seconds on first launch");
NSLog(@" [NATIVE-VOIP] Have to be working on bodily machine (not simulator)");
NSLog(@" [NATIVE-VOIP] Should have Push Notifications functionality enabled");
}
- (void)pushRegistry:(PKPushRegistry *)registry
didUpdatePushCredentials:(PKPushCredentials *)credentials
forType:(PKPushType)kind {
NSLog(@" [NATIVE-VOIP] ========================================");
NSLog(@" [NATIVE-VOIP] didUpdatePushCredentials CALLED!");
NSLog(@" [NATIVE-VOIP] ========================================");
NSLog(@" [NATIVE-VOIP] Push Kind: %@", kind);
NSLog(@" [NATIVE-VOIP] Credentials: %@", credentials);
NSData *tokenData = credentials.token;
NSString *tokenString = [[tokenData description] stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@""]];
tokenString = [tokenString stringByReplacingOccurrencesOfString:@" " withString:@""];
NSLog(@" [NATIVE-VOIP] Token (hex): %@", tokenString);
NSLog(@" [NATIVE-VOIP] Token size: %lu bytes", (unsigned lengthy)tokenData.size);
NSLog(@" [NATIVE-VOIP] Forwarding token to React Native bridge...");
[RNVoipPushNotificationManager didUpdatePushCredentials:credentials forType:(NSString *)type];
NSLog(@" [NATIVE-VOIP] Token forwarded to RNVoipPushNotificationManager");
}
- (void)pushRegistry:(PKPushRegistry *)registry
didInvalidatePushTokenForType:(PKPushType)kind {
NSLog(@" [NATIVE-VOIP] Push token invalidated for kind: %@", kind);
}
- (void)pushRegistry:(PKPushRegistry *)registry
didReceiveIncomingPushWithPayload:(PKPushPayload *)payload
forType:(PKPushType)kind
withCompletionHandler:(void (^)(void))completion {
NSLog(@" [NATIVE-VOIP] Incoming VoIP push acquired");
NSLog(@" [NATIVE-VOIP] Payload: %@", payload.dictionaryPayload);
[RNVoipPushNotificationManager didReceiveIncomingPushWithPayload:payload forType:(NSString *)type];
if (completion) {
completion();
}
}
- (NSURL *)sourceURLForBridge:(RCTBridge *)bridge
{
return [self bundleURL];
}
- (NSURL *)bundleURL
{
#if DEBUG
return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"];
#else
return [[NSBundle mainBundle] URLForResource:@"principal" withExtension:@"jsbundle"];
#endif
}
@finish
VoIP service in TypeScript:
import {Platform} from 'react-native';
import VoipPushNotification from 'react-native-voip-push-notification';
import AsyncStorage from '@react-native-async-storage/async-storage';
class VoIPService {
personal static occasion: VoIPService;
static getInstance(): VoIPService {
if (!VoIPService.occasion) {
VoIPService.occasion = new VoIPService();
}
return VoIPService.occasion;
}
/**
* Initialize VoIP - ONLY for token retrieval
*/
async initialize(): Promise {
if (Platform.OS !== 'ios') {
console.log(
'⚠️ [VOIP] VoIP solely accessible on iOS, present platform:',
Platform.OS,
);
return;
}
console.log('🚀 [VOIP] ========================================');
console.log('🚀 [VOIP] Beginning VoIP Token Retrieval');
console.log('🚀 [VOIP] ========================================');
attempt {
// ✅ Step 1: Examine if token already exists in storage
console.log(
'📱 [VOIP] Step 1: Checking AsyncStorage for present token...',
);
const existingToken = await AsyncStorage.getItem('VOIP_TOKEN');
if (existingToken) {
console.log('✅ [VOIP] Discovered present token in storage!');
console.log(
'📱 [VOIP] Token (first 30 chars):',
existingToken.substring(0, 30) + '...',
);
console.log('📱 [VOIP] Token size:', existingToken.size);
} else {
console.log('⚠️ [VOIP] No present token present in storage');
}
// ✅ Step 2: Setup listener for brand spanking new token
console.log('📱 [VOIP] Step 2: Establishing token listener...');
VoipPushNotification.addEventListener(
'register',
async (token: string) => {
console.log('🎉 [VOIP] ========================================');
console.log('🎉 [VOIP] TOKEN RECEIVED FROM APPLE!');
console.log('🎉 [VOIP] ========================================');
console.log(
'📱 [VOIP] Token (first 30 chars):',
token.substring(0, 30) + '...',
);
console.log('📱 [VOIP] Token size:', token.size);
console.log('📱 [VOIP] Full token:', token);
attempt {
await AsyncStorage.setItem('VOIP_TOKEN', token);
console.log('✅ [VOIP] Token saved to AsyncStorage efficiently!');
// Confirm it was saved
const savedToken = await AsyncStorage.getItem('VOIP_TOKEN');
if (savedToken === token) {
console.log(
'✅ [VOIP] Token verification profitable - token matches!',
);
} else {
console.log(
'❌ [VOIP] Token verification FAILED - tokens don't match!',
);
}
} catch (saveError) {
console.error(
'❌ [VOIP] Error saving token to AsyncStorage:',
saveError,
);
}
},
);
console.log('✅ [VOIP] Token listener registered');
console.log('⏳ [VOIP] Ready for token from Apple PushKit...');
console.log(
'📝 [VOIP] Observe: Token comes from AppDelegate.mm native code',
);
} catch (error) {
console.error('❌ [VOIP] Error throughout initialization:', error);
console.error('❌ [VOIP] Error particulars:', JSON.stringify(error, null, 2));
}
}
/**
* Get saved VoIP token
*/
async getVoIPToken(): Promise {
console.log('🔍 [VOIP] Trying to retrieve token from storage...');
attempt {
const token = await AsyncStorage.getItem('VOIP_TOKEN');
if (token) {
console.log('✅ [VOIP] Token present in storage!');
console.log(
'📱 [VOIP] Token (first 30 chars):',
token.substring(0, 30) + '...',
);
console.log('📱 [VOIP] Token size:', token.size);
return token;
} else {
console.log('⚠️ [VOIP] No token present in storage');
console.log(
'💡 [VOIP] Token ought to be acquired from AppDelegate.mm after app launch',
);
return null;
}
} catch (error) {
console.error('❌ [VOIP] Error retrieving token from storage:', error);
return null;
}
}
/**
* Clear up listener
*/
async cleanup(): Promise {
if (Platform.OS === 'ios') {
console.log('🧹 [VOIP] Cleansing up token listener...');
VoipPushNotification.removeEventListener('register');
console.log('✅ [VOIP] Cleanup full');
}
}
}
export default VoIPService;
VoIP Debug Display in React Native:
import React, {useEffect, useState} from 'react';
import {
View,
Textual content,
StyleSheet,
ScrollView,
TouchableOpacity,
Platform,
Alert,
Clipboard,
} from 'react-native';
import VoIPService from './providers/voip.service';
import AsyncStorage from '@react-native-async-storage/async-storage';
const VoIPDebugScreen = () => {
const [voipToken, setVoipToken] = useState(null);
const [loading, setLoading] = useState(true);
const [logs, setLogs] = useState([]);
const addLog = (message: string) => {
const timestamp = new Date().toLocaleTimeString();
setLogs(prev => [`[${timestamp}] ${message}`, ...prev].slice(0, 50));
};
useEffect(() => {
checkVoIPToken();
// Examine token each 2 seconds
const interval = setInterval(checkVoIPToken, 2000);
return () => clearInterval(interval);
}, []);
const checkVoIPToken = async () => {
attempt {
addLog('Checking VoIP token...');
const token = await VoIPService.getInstance().getVoIPToken();
setVoipToken(token);
setLoading(false);
if (token) {
addLog(`✅ Token discovered: ${token.substring(0, 20)}...`);
} else {
addLog('⚠️ No token discovered but');
}
} catch (error) {
addLog(`❌ Error: ${error}`);
setLoading(false);
}
};
const copyToClipboard = () => {
if (voipToken) {
Clipboard.setString(voipToken);
Alert.alert('Copied!', 'VoIP token copied to clipboard');
addLog('📋 Token copied to clipboard');
}
};
const reinitializeVoIP = async () => {
attempt {
addLog('🔄 Reinitializing VoIP service...');
await VoIPService.getInstance().initialize();
addLog('✅ VoIP service reinitialized');
setTimeout(checkVoIPToken, 1000);
} catch (error) {
addLog(`❌ Reinitialization error: ${error}`);
}
};
const clearToken = async () => {
attempt {
await AsyncStorage.removeItem('VOIP_TOKEN');
setVoipToken(null);
addLog('🗑️ Token cleared from storage');
Alert.alert('Success', 'VoIP token cleared');
} catch (error) {
addLog(`❌ Clear error: ${error}`);
}
};
if (Platform.OS !== 'ios') {
return (
VoIP is simply accessible on iOS
);
}
return (
VoIP Debug Display
Platform: {Platform.OS}
{/* Token Standing */}
Token Standing
{loading
? '⏳ Loading...'
: voipToken
? '✅ Token Available'
: '❌ No Token'}
{/* Token Display */}
{voipToken && (
VoIP Token
{voipToken}
📋 Copy Token
)}
{/* Actions */}
Actions
🔄 Refresh Token
🔧 Reinitialize VoIP
🗑️ Clear Token
{/* Logs */}
Logs (Last 50)
{logs.length === 0 ? (
No logs yet...
) : (
logs.map((log, index) => (
{log}
))
)}
{/* Instructions */}
📖 Instructions
1. Install this build on a physical iOS device{'n'}
2. Launch the app{'n'}
3. Wait 5-10 seconds for token registration{'n'}
4. Token should appear above{'n'}
5. Copy and save the token for backend testing{'n'}
6. Remove this screen before production release
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f5f5',
},
header: {
backgroundColor: '#007AFF',
padding: 20,
paddingTop: 60,
},
title: {
fontSize: 24,
fontWeight: 'bold',
color: '#fff',
marginBottom: 5,
},
subtitle: {
fontSize: 14,
color: '#fff',
opacity: 0.8,
},
section: {
backgroundColor: '#fff',
margin: 10,
padding: 15,
borderRadius: 10,
shadowColor: '#000',
shadowOffset: {width: 0, height: 2},
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3,
},
sectionTitle: {
fontSize: 18,
fontWeight: '600',
marginBottom: 10,
color: '#333',
},
statusCard: {
padding: 15,
borderRadius: 8,
alignItems: 'center',
},
statusText: {
fontSize: 16,
fontWeight: '600',
},
tokenCard: {
backgroundColor: '#f8f9fa',
padding: 15,
borderRadius: 8,
borderWidth: 1,
borderColor: '#dee2e6',
marginBottom: 10,
},
tokenText: {
fontSize: 12,
fontFamily: 'Courier',
color: '#495057',
},
button: {
padding: 15,
borderRadius: 8,
alignItems: 'center',
marginTop: 10,
},
primaryButton: {
backgroundColor: '#007AFF',
},
secondaryButton: {
backgroundColor: '#6c757d',
},
dangerButton: {
backgroundColor: '#dc3545',
},
buttonText: {
color: '#fff',
fontSize: 16,
fontWeight: '600',
},
logsContainer: {
backgroundColor: '#000',
padding: 10,
borderRadius: 8,
maxHeight: 300,
},
logText: {
color: '#0f0',
fontSize: 11,
fontFamily: 'Courier',
marginBottom: 3,
},
noLogsText: {
color: '#888',
textAlign: 'center',
fontStyle: 'italic',
},
instructionText: {
fontSize: 14,
color: '#666',
lineHeight: 22,
},
errorText: {
fontSize: 18,
color: '#dc3545',
textAlign: 'center',
marginTop: 100,
},
});
export default VoIPDebugScreen;
Problem:
I’m running this on a physical device via TestFlight.
Regular push notifications work.
VoIP push token never arrives.
Cannot check device logs because my iPhone 16 is iOS 26.1, which Xcode 15.1 does not support, and idevicesyslog fails (ERROR: Could not connect to lockdownd: -18).
I read that starting iOS 15+, the old PushKit entitlement key is removed and no longer required.
Am I missing any setup to get the VoIP token?
Is there a known issue with TestFlight builds not receiving VoIP push tokens?
Are there alternative ways to verify the VoIP token when device logs cannot be accessed?
Any guidance would be really appreciated.

