У меня:
Добавлено право на push-уведомления в профиле обеспечения и файле прав.
Включен VoIP в фоновых режимах в Xcode.
Обновлен Info.plist:
Код: Выделить всё
UIBackgroundModes
audio
remote-notification
voip
UIFileSharingEnabled
UILaunchStoryboardName
LaunchScreen
UIRequiredDeviceCapabilities
arm64
UISupportedInterfaceOrientations
UIInterfaceOrientationPortrait
UISupportedInterfaceOrientations~ipad
UIInterfaceOrientationPortrait
UIInterfaceOrientationLandscapeLeft
UIInterfaceOrientationLandscapeRight
UIViewControllerBasedStatusBarAppearance
Код: Выделить всё
#import
#import
#import
#import
@interface AppDelegate : RCTAppDelegate
@end
AppDelegate implementation (AppDelegate.mm):
#import "AppDelegate.h"
#import
#import
#import
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application 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 result = [super application:application didFinishLaunchingWithOptions:launchOptions];
NSLog(@" [NATIVE] React Native bridge initialized");
[self voipRegistration];
return result;
}
- (void)voipRegistration {
NSLog(@" [NATIVE-VOIP] ========================================");
NSLog(@" [NATIVE-VOIP] Starting VoIP Registration");
NSLog(@" [NATIVE-VOIP] ========================================");
PKPushRegistry *pushRegistry = [[PKPushRegistry alloc] initWithQueue:dispatch_get_main_queue()];
NSLog(@" [NATIVE-VOIP] PKPushRegistry instance created");
pushRegistry.delegate = self;
NSLog(@" [NATIVE-VOIP] Delegate set to AppDelegate");
pushRegistry.desiredPushTypes = [NSSet setWithObject:PKPushTypeVoIP];
NSLog(@" [NATIVE-VOIP] Desired push types set: PKPushTypeVoIP");
NSLog(@" [NATIVE-VOIP] Waiting for Apple to call didUpdatePushCredentials...");
NSLog(@" [NATIVE-VOIP] This may take 2-10 seconds on first launch");
NSLog(@" [NATIVE-VOIP] Must be running on physical device (not simulator)");
NSLog(@" [NATIVE-VOIP] Must have Push Notifications capability enabled");
}
- (void)pushRegistry:(PKPushRegistry *)registry
didUpdatePushCredentials:(PKPushCredentials *)credentials
forType:(PKPushType)type {
NSLog(@" [NATIVE-VOIP] ========================================");
NSLog(@" [NATIVE-VOIP] didUpdatePushCredentials CALLED!");
NSLog(@" [NATIVE-VOIP] ========================================");
NSLog(@" [NATIVE-VOIP] Push Type: %@", type);
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 length: %lu bytes", (unsigned long)tokenData.length);
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)type {
NSLog(@" [NATIVE-VOIP] Push token invalidated for type: %@", type);
}
- (void)pushRegistry:(PKPushRegistry *)registry
didReceiveIncomingPushWithPayload:(PKPushPayload *)payload
forType:(PKPushType)type
withCompletionHandler:(void (^)(void))completion {
NSLog(@" [NATIVE-VOIP] Incoming VoIP push received");
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:@"main" withExtension:@"jsbundle"];
#endif
}
@end
Код: Выделить всё
import {Platform} from 'react-native';
import VoipPushNotification from 'react-native-voip-push-notification';
import AsyncStorage from '@react-native-async-storage/async-storage';
class VoIPService {
private static instance: VoIPService;
static getInstance(): VoIPService {
if (!VoIPService.instance) {
VoIPService.instance = new VoIPService();
}
return VoIPService.instance;
}
/**
* Initialize VoIP - ONLY for token retrieval
*/
async initialize(): Promise {
if (Platform.OS !== 'ios') {
console.log(
'⚠️ [VOIP] VoIP only available on iOS, current platform:',
Platform.OS,
);
return;
}
console.log('🚀 [VOIP] ========================================');
console.log('🚀 [VOIP] Starting VoIP Token Retrieval');
console.log('🚀 [VOIP] ========================================');
try {
// ✅ Step 1: Check if token already exists in storage
console.log(
'📱 [VOIP] Step 1: Checking AsyncStorage for existing token...',
);
const existingToken = await AsyncStorage.getItem('VOIP_TOKEN');
if (existingToken) {
console.log('✅ [VOIP] Found existing token in storage!');
console.log(
'📱 [VOIP] Token (first 30 chars):',
existingToken.substring(0, 30) + '...',
);
console.log('📱 [VOIP] Token length:', existingToken.length);
} else {
console.log('⚠️ [VOIP] No existing token found in storage');
}
// ✅ Step 2: Setup listener for new token
console.log('📱 [VOIP] Step 2: Setting up 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 length:', token.length);
console.log('📱 [VOIP] Full token:', token);
try {
await AsyncStorage.setItem('VOIP_TOKEN', token);
console.log('✅ [VOIP] Token saved to AsyncStorage successfully!');
// Verify it was saved
const savedToken = await AsyncStorage.getItem('VOIP_TOKEN');
if (savedToken === token) {
console.log(
'✅ [VOIP] Token verification successful - token matches!',
);
} else {
console.log(
'❌ [VOIP] Token verification FAILED - tokens do not match!',
);
}
} catch (saveError) {
console.error(
'❌ [VOIP] Error saving token to AsyncStorage:',
saveError,
);
}
},
);
console.log('✅ [VOIP] Token listener registered');
console.log('⏳ [VOIP] Waiting for token from Apple PushKit...');
console.log(
'📝 [VOIP] Note: Token comes from AppDelegate.mm native code',
);
} catch (error) {
console.error('❌ [VOIP] Error during initialization:', error);
console.error('❌ [VOIP] Error details:', JSON.stringify(error, null, 2));
}
}
/**
* Get stored VoIP token
*/
async getVoIPToken(): Promise {
console.log('🔍 [VOIP] Attempting to retrieve token from storage...');
try {
const token = await AsyncStorage.getItem('VOIP_TOKEN');
if (token) {
console.log('✅ [VOIP] Token found in storage!');
console.log(
'📱 [VOIP] Token (first 30 chars):',
token.substring(0, 30) + '...',
);
console.log('📱 [VOIP] Token length:', token.length);
return token;
} else {
console.log('⚠️ [VOIP] No token found in storage');
console.log(
'💡 [VOIP] Token should be received from AppDelegate.mm after app launch',
);
return null;
}
} catch (error) {
console.error('❌ [VOIP] Error retrieving token from storage:', error);
return null;
}
}
/**
* Clean up listener
*/
async cleanup(): Promise {
if (Platform.OS === 'ios') {
console.log('🧹 [VOIP] Cleaning up token listener...');
VoipPushNotification.removeEventListener('register');
console.log('✅ [VOIP] Cleanup complete');
}
}
}
export default VoIPService;
Код: Выделить всё
import React, {useEffect, useState} from 'react';
import {
View,
Text,
StyleSheet,
ScrollView,
TouchableOpacity,
Platform,
Alert,
Clipboard,
} from 'react-native';
import VoIPService from './services/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();
// Check token every 2 seconds
const interval = setInterval(checkVoIPToken, 2000);
return () => clearInterval(interval);
}, []);
const checkVoIPToken = async () => {
try {
addLog('Checking VoIP token...');
const token = await VoIPService.getInstance().getVoIPToken();
setVoipToken(token);
setLoading(false);
if (token) {
addLog(`✅ Token found: ${token.substring(0, 20)}...`);
} else {
addLog('⚠️ No token found yet');
}
} 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 () => {
try {
addLog('🔄 Reinitializing VoIP service...');
await VoIPService.getInstance().initialize();
addLog('✅ VoIP service reinitialized');
setTimeout(checkVoIPToken, 1000);
} catch (error) {
addLog(`❌ Reinitialization error: ${error}`);
}
};
const clearToken = async () => {
try {
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 only available on iOS
);
}
return (
VoIP Debug Screen
Platform: {Platform.OS}
{/* Token Status */}
Token Status
{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;
Я запускаю это на физическом устройстве через TestFlight.
Обычные push-уведомления работают.
Push-токен VoIP никогда не приходит.
Невозможно проверить журналы устройства, поскольку на моем iPhone 16 установлена iOS 26.1, которую Xcode 15.1 не поддерживает, и idevicesyslog завершается сбоем (ОШИБКА: Не удалось подключиться к lockdownd: -18).
Я читал, что начиная с iOS 15+ старый ключ разрешения PushKit удаляется и больше не требуется.
Мне не хватает каких-либо настроек для получения токена VoIP?
Известна ли проблема со сборками TestFlight, не получающими push-токены VoIP?
Существуют ли альтернативные способы проверить токен VoIP, когда доступ к журналам устройства недоступен?
Будем очень признательны за любые рекомендации.
Подробнее здесь: https://stackoverflow.com/questions/798 ... seem-to-do