/** * Push-Token-Registration mit Expo. * * Flow: * 1. Permission anfragen (falls noch nicht entschieden) * 2. ExponentPushToken[xxx] via getExpoPushTokenAsync() holen * 3. Token an Backend POST /api/users/me/push-token senden * 4. Bei Logout: DELETE /api/users/me/push-token?token=… * * Wird einmal pro App-Start nach Login aufgerufen — idempotent durch * upsert im Backend. */ import { useEffect, useRef } from 'react'; import { Platform } from 'react-native'; import * as Notifications from 'expo-notifications'; import * as Device from 'expo-device'; import Constants from 'expo-constants'; import { apiFetch } from '../lib/api'; import { getDeviceId } from '../lib/deviceId'; const lastRegisteredToken: { current: string | null } = { current: null }; export async function registerPushTokenWithBackend(): Promise { // Simulator/Emulator support: Expo Push funktioniert auf physical devices only // — Simulator gibt Permission denied. Wir loggen aber crashen nicht. if (!Device.isDevice) { if (__DEV__) console.log('[push] skipped (simulator)'); return null; } try { // 1) Permission const { status: existing } = await Notifications.getPermissionsAsync(); let status = existing; if (existing !== 'granted') { const { status: requested } = await Notifications.requestPermissionsAsync(); status = requested; } if (status !== 'granted') { if (__DEV__) console.log('[push] permission denied'); return null; } // 2) Android-Channel (muss vor getExpoPushTokenAsync existieren) if (Platform.OS === 'android') { await Notifications.setNotificationChannelAsync('chat', { name: 'Chat-Nachrichten', importance: Notifications.AndroidImportance.HIGH, vibrationPattern: [0, 250, 250, 250], lightColor: '#007AFF', sound: 'default', }); } // 3) Token holen const projectId = Constants.expoConfig?.extra?.eas?.projectId ?? Constants.easConfig?.projectId; if (!projectId) { console.warn('[push] EAS projectId missing — token cannot be issued'); return null; } const tokenData = await Notifications.getExpoPushTokenAsync({ projectId }); const token = tokenData.data; if (!token) return null; // 4) Idempotenz-Skip: wenn schon registriert in dieser Session, nicht nochmal if (lastRegisteredToken.current === token) return token; // 5) Senden an Backend const deviceId = await getDeviceId(); await apiFetch('/api/users/me/push-token', { method: 'POST', body: { token, platform: Platform.OS as 'ios' | 'android', deviceId, }, }); lastRegisteredToken.current = token; if (__DEV__) console.log('[push] token registered:', token.slice(0, 30) + '…'); return token; } catch (err) { console.warn('[push] registration failed:', err); return null; } } export async function unregisterPushTokenFromBackend(): Promise { const token = lastRegisteredToken.current; if (!token) return; try { await apiFetch( `/api/users/me/push-token?token=${encodeURIComponent(token)}`, { method: 'DELETE' }, ); lastRegisteredToken.current = null; } catch (err) { console.warn('[push] unregister failed:', err); } } /** * Hook: registriert Push-Token sobald User authenticated ist. * Verwendet in app/_layout.tsx nach Auth-Init. */ export function usePushTokenRegistration(userId: string | null | undefined) { const lastUserId = useRef(null); useEffect(() => { if (!userId || userId === lastUserId.current) return; lastUserId.current = userId; void registerPushTokenWithBackend(); }, [userId]); }