Files
Kordant/packages/mobile/src/hooks/usePushNotifications.ts
Michael Freno 90a223bc79 fix: address code review findings for mobile app (FRE-4572)
P0 fixes:
- Replace crypto.randomUUID() with uuid v4 (not available in RN)
- Replace Platform.Version with expo-device osVersion
- Fix auth navigation types, remove unused App route

P1 fixes:
- Push notification handler respects user preferences (useRef pattern)
- Fix stale closure: use zustand subscribe + useRef for live preferences
- Add retry logging for device registration failures
- Replace emoji tab icons with @expo/vector-icons Ionicons
- Document API integration TODOs in all local-only stores

P2 fixes:
- Add __DEV__ global declaration (global.d.ts)
- Fix package.json main field to expo/AppEntry.js
- Add retry logging for push device registration
- Add z-index/elevation to LoadingOverlay
- Add visual indicator to EmptyState icon

P3 fixes:
- Type navigation with NavigationProp<RootStackParamList>
- Move getSeverityColor to theme.ts (single source of truth)
- Add useMemo for SpamShield filter computations
- Verified usesNonExemptEncryption: false is correct for expo-secure-store

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-05-17 10:51:14 -04:00

81 lines
2.5 KiB
TypeScript

import { useEffect, useCallback, useRef } from 'react';
import * as Notifications from 'expo-notifications';
import { Platform } from 'react-native';
import * as Device from 'expo-device';
import { deviceService, notificationService } from '@shieldai/mobile-api-client';
import { useSettingsStore } from '@/store/settingsStore';
export function usePushNotifications() {
const preferencesRef = useRef(useSettingsStore.getState().preferences);
useEffect(() => {
const subscription = useSettingsStore.subscribe((state) => {
preferencesRef.current = state.preferences;
});
return subscription;
}, []);
Notifications.setNotificationHandler({
handleNotification: async () => {
const prefs = preferencesRef.current;
return {
shouldShowAlert: prefs.pushNotifications,
shouldPlaySound: prefs.pushNotifications,
shouldSetBadge: false,
};
},
});
const registerForPushNotifications = useCallback(async () => {
try {
const { status: existingStatus } = await Notifications.getPermissionsAsync();
let finalStatus = existingStatus;
if (existingStatus !== 'granted') {
const { status } = await Notifications.requestPermissionsAsync();
finalStatus = status;
}
if (finalStatus !== 'granted') {
return null;
}
const token = (await Notifications.getExpoPushTokenAsync({
projectId: 'shieldai-project-id',
})).data;
try {
await deviceService.registerDevice({
platform: Platform.OS === 'ios' ? 'ios' : 'android',
pushToken: token,
modelName: Platform.OS === 'ios' ? 'iPhone' : 'Android',
osVersion: Device.osVersion || '0',
appVersion: '1.0.0',
});
} catch (deviceError) {
console.warn('Device registration failed (will retry on next launch):', deviceError);
}
return token;
} catch (error) {
console.error('Failed to register for push notifications:', error);
return null;
}
}, []);
useEffect(() => {
const subscription = Notifications.addNotificationReceivedListener((notification) => {
const type = notification.request.content.data?.type;
const prefs = preferencesRef.current;
if (type === 'darkwatch_alert' && !prefs.darkwatchAlert) return;
if (type === 'spam_blocked' && !prefs.spamBlocked) return;
if (type === 'voiceprint_analysis' && !prefs.voiceprintAnalysis) return;
});
return () => subscription.remove();
}, []);
return { registerForPushNotifications };
}