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
87 lines
2.7 KiB
TypeScript
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 };
|
|
}
|