Files
Kordant/packages/mobile/src/hooks/usePushNotifications.ts
Michael Freno a8a5930ced security: fix 10 security review findings (FRE-4572)
CRITICAL:
- SEC-001: Auth tokens now stored in SecureStore (Keychain/Keystore)
- SEC-002: Biometric bypass removed - alerts user and disables when unavailable

HIGH:
- SEC-003: Push projectId moved to EXPO_PUBLIC_EAS_PROJECT_ID env var
- SEC-004: Token refresh mechanism added with refreshSession/hydrateTokens
- SEC-005: debug already gated on __DEV__ (confirmed)

MEDIUM:
- SEC-006: All PII stores (darkwatch, voiceprint, spamshield, settings, auth) now use encrypted AsyncStorage
- SEC-007: Certificate pinning documented with TODO for production
- SEC-008: Login brute force protection: 5 attempts then 5-minute lockout

LOW:
- SEC-009: Watch list input validation with format checks per entity type
- SEC-010: Upgrade Plan button shows billing coming soon alert
2026-05-17 19:15:42 -04:00

87 lines
2.7 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';
import { EAS_PROJECT_ID } from '@/constants/theme';
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;
}
if (!EAS_PROJECT_ID) {
console.warn('EAS_PROJECT_ID not configured — push notifications disabled');
return null;
}
const token = (await Notifications.getExpoPushTokenAsync({
projectId: EAS_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 };
}