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>
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
"name": "@shieldai/mobile",
|
"name": "@shieldai/mobile",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"main": "expo-router/entry",
|
"main": "node_modules/expo/AppEntry.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "expo start",
|
"dev": "expo start",
|
||||||
"dev:ios": "expo run:ios",
|
"dev:ios": "expo run:ios",
|
||||||
|
|||||||
@@ -25,7 +25,9 @@ export function EmptyState({ title, message }: EmptyStateProps) {
|
|||||||
return (
|
return (
|
||||||
<View style={styles.emptyContainer}>
|
<View style={styles.emptyContainer}>
|
||||||
<View style={styles.emptyContent}>
|
<View style={styles.emptyContent}>
|
||||||
<View style={styles.emptyIcon} />
|
<View style={styles.emptyIcon}>
|
||||||
|
<Text style={styles.emptyIconText}>📭</Text>
|
||||||
|
</View>
|
||||||
<View style={styles.emptyText}>
|
<View style={styles.emptyText}>
|
||||||
<Text style={styles.emptyTitle}>{title}</Text>
|
<Text style={styles.emptyTitle}>{title}</Text>
|
||||||
{message && <Text style={styles.emptyMessage}>{message}</Text>}
|
{message && <Text style={styles.emptyMessage}>{message}</Text>}
|
||||||
@@ -41,6 +43,8 @@ const styles = StyleSheet.create({
|
|||||||
backgroundColor: 'rgba(0, 0, 0, 0.3)',
|
backgroundColor: 'rgba(0, 0, 0, 0.3)',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
zIndex: 999,
|
||||||
|
elevation: 999,
|
||||||
},
|
},
|
||||||
emptyContainer: {
|
emptyContainer: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
@@ -57,6 +61,11 @@ const styles = StyleSheet.create({
|
|||||||
borderRadius: 24,
|
borderRadius: 24,
|
||||||
backgroundColor: COLORS.card,
|
backgroundColor: COLORS.card,
|
||||||
marginBottom: SPACING.md,
|
marginBottom: SPACING.md,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
emptyIconText: {
|
||||||
|
fontSize: 24,
|
||||||
},
|
},
|
||||||
emptyText: {
|
emptyText: {
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
|||||||
@@ -52,6 +52,15 @@ export const SHADOWS = {
|
|||||||
|
|
||||||
export const API_URL = process.env.EXPO_PUBLIC_API_URL || 'https://api.shieldai.freno.me/api/v1';
|
export const API_URL = process.env.EXPO_PUBLIC_API_URL || 'https://api.shieldai.freno.me/api/v1';
|
||||||
|
|
||||||
|
export const getSeverityColor = (severity: string): string => {
|
||||||
|
switch (severity) {
|
||||||
|
case 'critical': return COLORS.danger;
|
||||||
|
case 'high': return COLORS.warning;
|
||||||
|
case 'medium': return COLORS.accent;
|
||||||
|
default: return COLORS.textSecondary;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const TIER_FEATURES = {
|
export const TIER_FEATURES = {
|
||||||
free: {
|
free: {
|
||||||
name: 'Free',
|
name: 'Free',
|
||||||
|
|||||||
1
packages/mobile/src/global.d.ts
vendored
Normal file
1
packages/mobile/src/global.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
declare const __DEV__: boolean;
|
||||||
@@ -1,19 +1,30 @@
|
|||||||
import { useEffect, useCallback } from 'react';
|
import { useEffect, useCallback, useRef } from 'react';
|
||||||
import * as Notifications from 'expo-notifications';
|
import * as Notifications from 'expo-notifications';
|
||||||
import { Platform } from 'react-native';
|
import { Platform } from 'react-native';
|
||||||
|
import * as Device from 'expo-device';
|
||||||
import { deviceService, notificationService } from '@shieldai/mobile-api-client';
|
import { deviceService, notificationService } from '@shieldai/mobile-api-client';
|
||||||
import { useSettingsStore } from '@/store/settingsStore';
|
import { useSettingsStore } from '@/store/settingsStore';
|
||||||
|
|
||||||
Notifications.setNotificationHandler({
|
|
||||||
handleNotification: async () => ({
|
|
||||||
shouldShowAlert: true,
|
|
||||||
shouldPlaySound: true,
|
|
||||||
shouldSetBadge: false,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
export function usePushNotifications() {
|
export function usePushNotifications() {
|
||||||
const { preferences } = useSettingsStore();
|
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 () => {
|
const registerForPushNotifications = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -33,13 +44,17 @@ export function usePushNotifications() {
|
|||||||
projectId: 'shieldai-project-id',
|
projectId: 'shieldai-project-id',
|
||||||
})).data;
|
})).data;
|
||||||
|
|
||||||
|
try {
|
||||||
await deviceService.registerDevice({
|
await deviceService.registerDevice({
|
||||||
platform: Platform.OS === 'ios' ? 'ios' : 'android',
|
platform: Platform.OS === 'ios' ? 'ios' : 'android',
|
||||||
pushToken: token,
|
pushToken: token,
|
||||||
modelName: Platform.OS === 'ios' ? 'iPhone' : 'Android',
|
modelName: Platform.OS === 'ios' ? 'iPhone' : 'Android',
|
||||||
osVersion: Platform.Version.toString(),
|
osVersion: Device.osVersion || '0',
|
||||||
appVersion: '1.0.0',
|
appVersion: '1.0.0',
|
||||||
});
|
});
|
||||||
|
} catch (deviceError) {
|
||||||
|
console.warn('Device registration failed (will retry on next launch):', deviceError);
|
||||||
|
}
|
||||||
|
|
||||||
return token;
|
return token;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -51,7 +66,7 @@ export function usePushNotifications() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const subscription = Notifications.addNotificationReceivedListener((notification) => {
|
const subscription = Notifications.addNotificationReceivedListener((notification) => {
|
||||||
const type = notification.request.content.data?.type;
|
const type = notification.request.content.data?.type;
|
||||||
const prefs = useSettingsStore.getState().preferences;
|
const prefs = preferencesRef.current;
|
||||||
|
|
||||||
if (type === 'darkwatch_alert' && !prefs.darkwatchAlert) return;
|
if (type === 'darkwatch_alert' && !prefs.darkwatchAlert) return;
|
||||||
if (type === 'spam_blocked' && !prefs.spamBlocked) return;
|
if (type === 'spam_blocked' && !prefs.spamBlocked) return;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Text, ViewStyle, StyleSheet } from 'react-native';
|
import { Text, ViewStyle, StyleSheet } from 'react-native';
|
||||||
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
|
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
|
||||||
import { DashboardScreen } from '@/screens/dashboard';
|
import { DashboardScreen } from '@/screens/dashboard';
|
||||||
import { DarkWatchScreen } from '@/screens/darkwatch';
|
import { DarkWatchScreen } from '@/screens/darkwatch';
|
||||||
@@ -18,25 +19,29 @@ type MainTabParamList = {
|
|||||||
|
|
||||||
const Tab = createBottomTabNavigator<MainTabParamList>();
|
const Tab = createBottomTabNavigator<MainTabParamList>();
|
||||||
|
|
||||||
const iconMap: Record<string, string> = {
|
const iconMap: Record<string, keyof typeof Ionicons.glyphMap> = {
|
||||||
Dashboard: '\u{1F6E1}\u{FE0F}',
|
Dashboard: 'shield-outline',
|
||||||
DarkWatch: '\u{1F441}\u{FE0F}',
|
DarkWatch: 'eye-outline',
|
||||||
SpamShield: '\u{1F6AB}',
|
SpamShield: 'ban-outline',
|
||||||
VoicePrint: '\u{1F399}\u{FE0F}',
|
VoicePrint: 'mic-outline',
|
||||||
Settings: '\u{2699}\u{FE0F}',
|
Settings: 'settings-outline',
|
||||||
};
|
};
|
||||||
|
|
||||||
function TabIcon({ routeName, color }: { routeName: string; color: string }) {
|
const iconActiveMap: Record<string, keyof typeof Ionicons.glyphMap> = {
|
||||||
return (
|
Dashboard: 'shield',
|
||||||
<Text style={[styles.icon, { color }]}>{iconMap[routeName] || '\u{2022}'}</Text>
|
DarkWatch: 'eye',
|
||||||
);
|
SpamShield: 'ban',
|
||||||
}
|
VoicePrint: 'mic',
|
||||||
|
Settings: 'settings',
|
||||||
|
};
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
function TabIcon({ routeName, color, focused }: { routeName: string; color: string; focused: boolean }) {
|
||||||
icon: {
|
const iconName = focused
|
||||||
fontSize: 22,
|
? (iconActiveMap[routeName] as keyof typeof Ionicons.glyphMap)
|
||||||
},
|
: (iconMap[routeName] as keyof typeof Ionicons.glyphMap);
|
||||||
});
|
|
||||||
|
return <Ionicons name={iconName} size={24} color={color} />;
|
||||||
|
}
|
||||||
|
|
||||||
export function MainTabNavigator() {
|
export function MainTabNavigator() {
|
||||||
return (
|
return (
|
||||||
@@ -75,7 +80,7 @@ export function MainTabNavigator() {
|
|||||||
options={{
|
options={{
|
||||||
headerTitle: 'Dashboard',
|
headerTitle: 'Dashboard',
|
||||||
tabBarLabel: 'Home',
|
tabBarLabel: 'Home',
|
||||||
tabBarIcon: ({ color }) => <TabIcon routeName="Dashboard" color={color} />,
|
tabBarIcon: ({ color, focused }) => <TabIcon routeName="Dashboard" color={color} focused={focused} />,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Tab.Screen
|
<Tab.Screen
|
||||||
@@ -84,7 +89,7 @@ export function MainTabNavigator() {
|
|||||||
options={{
|
options={{
|
||||||
headerTitle: 'DarkWatch',
|
headerTitle: 'DarkWatch',
|
||||||
tabBarLabel: 'DarkWatch',
|
tabBarLabel: 'DarkWatch',
|
||||||
tabBarIcon: ({ color }) => <TabIcon routeName="DarkWatch" color={color} />,
|
tabBarIcon: ({ color, focused }) => <TabIcon routeName="DarkWatch" color={color} focused={focused} />,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Tab.Screen
|
<Tab.Screen
|
||||||
@@ -93,7 +98,7 @@ export function MainTabNavigator() {
|
|||||||
options={{
|
options={{
|
||||||
headerTitle: 'SpamShield',
|
headerTitle: 'SpamShield',
|
||||||
tabBarLabel: 'SpamShield',
|
tabBarLabel: 'SpamShield',
|
||||||
tabBarIcon: ({ color }) => <TabIcon routeName="SpamShield" color={color} />,
|
tabBarIcon: ({ color, focused }) => <TabIcon routeName="SpamShield" color={color} focused={focused} />,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Tab.Screen
|
<Tab.Screen
|
||||||
@@ -102,7 +107,7 @@ export function MainTabNavigator() {
|
|||||||
options={{
|
options={{
|
||||||
headerTitle: 'VoicePrint',
|
headerTitle: 'VoicePrint',
|
||||||
tabBarLabel: 'VoicePrint',
|
tabBarLabel: 'VoicePrint',
|
||||||
tabBarIcon: ({ color }) => <TabIcon routeName="VoicePrint" color={color} />,
|
tabBarIcon: ({ color, focused }) => <TabIcon routeName="VoicePrint" color={color} focused={focused} />,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Tab.Screen
|
<Tab.Screen
|
||||||
@@ -111,7 +116,7 @@ export function MainTabNavigator() {
|
|||||||
options={{
|
options={{
|
||||||
headerTitle: 'Settings',
|
headerTitle: 'Settings',
|
||||||
tabBarLabel: 'Settings',
|
tabBarLabel: 'Settings',
|
||||||
tabBarIcon: ({ color }) => <TabIcon routeName="Settings" color={color} />,
|
tabBarIcon: ({ color, focused }) => <TabIcon routeName="Settings" color={color} focused={focused} />,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Tab.Navigator>
|
</Tab.Navigator>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { StyleSheet, Text, View, SafeAreaView, KeyboardAvoidingView, Platform, ScrollView, Alert } from 'react-native';
|
import { StyleSheet, Text, View, SafeAreaView, KeyboardAvoidingView, Platform, ScrollView, Alert } from 'react-native';
|
||||||
import { RouteProp, useRoute, useNavigation } from '@react-navigation/native';
|
import { useNavigation, NavigationProp } from '@react-navigation/native';
|
||||||
import { useAuthStore } from '@/store/authStore';
|
import { useAuthStore } from '@/store/authStore';
|
||||||
import { Button, Input } from '@/components';
|
import { Button, Input } from '@/components';
|
||||||
import { COLORS, FONT_SIZES, SPACING } from '@/constants/theme';
|
import { COLORS, FONT_SIZES, SPACING } from '@/constants/theme';
|
||||||
@@ -8,15 +8,11 @@ import { COLORS, FONT_SIZES, SPACING } from '@/constants/theme';
|
|||||||
type RootStackParamList = {
|
type RootStackParamList = {
|
||||||
Login: undefined;
|
Login: undefined;
|
||||||
Register: undefined;
|
Register: undefined;
|
||||||
App: undefined;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type LoginScreenRouteProp = RouteProp<RootStackParamList, 'Login'>;
|
|
||||||
|
|
||||||
export function LoginScreen() {
|
export function LoginScreen() {
|
||||||
const route = useRoute<LoginScreenRouteProp>();
|
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
||||||
const navigation = useNavigation<any>();
|
const { login, isLoading, clearError } = useAuthStore();
|
||||||
const { login, isLoading, error, clearError } = useAuthStore();
|
|
||||||
|
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState('');
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { StyleSheet, Text, View, SafeAreaView, KeyboardAvoidingView, Platform, ScrollView, Alert } from 'react-native';
|
import { StyleSheet, Text, View, SafeAreaView, KeyboardAvoidingView, Platform, ScrollView, Alert } from 'react-native';
|
||||||
import { useNavigation } from '@react-navigation/native';
|
import { useNavigation, NavigationProp } from '@react-navigation/native';
|
||||||
import { useAuthStore } from '@/store/authStore';
|
import { useAuthStore } from '@/store/authStore';
|
||||||
import { Button, Input } from '@/components';
|
import { Button, Input } from '@/components';
|
||||||
import { COLORS, FONT_SIZES, SPACING } from '@/constants/theme';
|
import { COLORS, FONT_SIZES, SPACING } from '@/constants/theme';
|
||||||
|
|
||||||
|
type RootStackParamList = {
|
||||||
|
Login: undefined;
|
||||||
|
Register: undefined;
|
||||||
|
};
|
||||||
|
|
||||||
export function RegisterScreen() {
|
export function RegisterScreen() {
|
||||||
const navigation = useNavigation<any>();
|
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
||||||
const { register, isLoading } = useAuthStore();
|
const { register, isLoading } = useAuthStore();
|
||||||
|
|
||||||
const [firstName, setFirstName] = useState('');
|
const [firstName, setFirstName] = useState('');
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React, { useState } from 'react';
|
|||||||
import { StyleSheet, Text, View, SafeAreaView, ScrollView, FlatList, TouchableOpacity, Alert, Modal } from 'react-native';
|
import { StyleSheet, Text, View, SafeAreaView, ScrollView, FlatList, TouchableOpacity, Alert, Modal } from 'react-native';
|
||||||
import { useDarkWatchStore } from '@/store/darkWatchStore';
|
import { useDarkWatchStore } from '@/store/darkWatchStore';
|
||||||
import { Card, Button, Input, EmptyState } from '@/components';
|
import { Card, Button, Input, EmptyState } from '@/components';
|
||||||
import { COLORS, FONT_SIZES, SPACING, BORDER_RADIUS } from '@/constants/theme';
|
import { COLORS, FONT_SIZES, SPACING, BORDER_RADIUS, getSeverityColor } from '@/constants/theme';
|
||||||
import type { WatchListItem } from '@/types';
|
import type { WatchListItem } from '@/types';
|
||||||
|
|
||||||
export function DarkWatchScreen() {
|
export function DarkWatchScreen() {
|
||||||
@@ -178,15 +178,6 @@ export function DarkWatchScreen() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const getSeverityColor = (severity: string) => {
|
|
||||||
switch (severity) {
|
|
||||||
case 'critical': return COLORS.danger;
|
|
||||||
case 'high': return COLORS.warning;
|
|
||||||
case 'medium': return COLORS.accent;
|
|
||||||
default: return COLORS.textSecondary;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState, useMemo } from 'react';
|
||||||
import { StyleSheet, Text, View, SafeAreaView, ScrollView, TouchableOpacity, Alert } from 'react-native';
|
import { StyleSheet, Text, View, SafeAreaView, ScrollView, TouchableOpacity, Alert } from 'react-native';
|
||||||
import { useSpamShieldStore } from '@/store/spamShieldStore';
|
import { useSpamShieldStore } from '@/store/spamShieldStore';
|
||||||
import { Card, Button, Input, StatCard, EmptyState } from '@/components';
|
import { Card, Button, Input, StatCard, EmptyState } from '@/components';
|
||||||
@@ -22,6 +22,15 @@ export function SpamShieldScreen() {
|
|||||||
const [newNumber, setNewNumber] = useState('');
|
const [newNumber, setNewNumber] = useState('');
|
||||||
const [newLabel, setNewLabel] = useState('');
|
const [newLabel, setNewLabel] = useState('');
|
||||||
|
|
||||||
|
const blockedCount = useMemo(
|
||||||
|
() => callHistory.filter((r) => r.isBlocked).length,
|
||||||
|
[callHistory]
|
||||||
|
);
|
||||||
|
const totalCount = useMemo(
|
||||||
|
() => callHistory.length + textHistory.length,
|
||||||
|
[callHistory, textHistory]
|
||||||
|
);
|
||||||
|
|
||||||
const handleAddToList = (list: 'whitelist' | 'blacklist') => {
|
const handleAddToList = (list: 'whitelist' | 'blacklist') => {
|
||||||
if (!newNumber) return;
|
if (!newNumber) return;
|
||||||
|
|
||||||
@@ -62,8 +71,8 @@ export function SpamShieldScreen() {
|
|||||||
|
|
||||||
<ScrollView style={styles.content}>
|
<ScrollView style={styles.content}>
|
||||||
<Card>
|
<Card>
|
||||||
<StatCard title="Blocked Today" value={callHistory.filter(r => r.isBlocked).length} color={COLORS.success} />
|
<StatCard title="Blocked Today" value={blockedCount} color={COLORS.success} />
|
||||||
<StatCard title="Total Blocked" value={callHistory.length + textHistory.length} color={COLORS.accent} />
|
<StatCard title="Total Blocked" value={totalCount} color={COLORS.accent} />
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<View style={styles.tabs}>
|
<View style={styles.tabs}>
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
import { persist, createJSONStorage } from 'zustand/middleware';
|
import { persist, createJSONStorage } from 'zustand/middleware';
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import type { WatchListItem, Exposure } from '@/types';
|
import type { WatchListItem, Exposure } from '@/types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO: Wire store operations to @shieldai/mobile-api-client for production.
|
||||||
|
* Current implementation is local-only (AsyncStorage) for offline-first MVP.
|
||||||
|
*/
|
||||||
|
|
||||||
interface DarkWatchState {
|
interface DarkWatchState {
|
||||||
watchList: WatchListItem[];
|
watchList: WatchListItem[];
|
||||||
exposures: Exposure[];
|
exposures: Exposure[];
|
||||||
@@ -23,7 +29,7 @@ export const useDarkWatchStore = create<DarkWatchState>()(
|
|||||||
addWatchItem: async (item) => {
|
addWatchItem: async (item) => {
|
||||||
const newItem: WatchListItem = {
|
const newItem: WatchListItem = {
|
||||||
...item,
|
...item,
|
||||||
id: crypto.randomUUID(),
|
id: uuidv4(),
|
||||||
lastChecked: new Date().toISOString(),
|
lastChecked: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
set((state) => ({ watchList: [...state.watchList, newItem] }));
|
set((state) => ({ watchList: [...state.watchList, newItem] }));
|
||||||
|
|||||||
@@ -3,6 +3,11 @@ import { persist, createJSONStorage } from 'zustand/middleware';
|
|||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
import type { NotificationPreference } from '@/types';
|
import type { NotificationPreference } from '@/types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO: Wire updatePreferences to notificationService.updatePreferences() for production.
|
||||||
|
* Current implementation is local-only (AsyncStorage) for offline-first MVP.
|
||||||
|
*/
|
||||||
|
|
||||||
interface SettingsState {
|
interface SettingsState {
|
||||||
preferences: NotificationPreference;
|
preferences: NotificationPreference;
|
||||||
isBiometricEnabled: boolean;
|
isBiometricEnabled: boolean;
|
||||||
|
|||||||
@@ -3,6 +3,11 @@ import { persist, createJSONStorage } from 'zustand/middleware';
|
|||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
import type { SpamRecord } from '@/types';
|
import type { SpamRecord } from '@/types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO: Wire store operations to @shieldai/mobile-api-client for production.
|
||||||
|
* Current implementation is local-only (AsyncStorage) for offline-first MVP.
|
||||||
|
*/
|
||||||
|
|
||||||
type PhoneList = { number: string; label: string }[];
|
type PhoneList = { number: string; label: string }[];
|
||||||
|
|
||||||
interface SpamShieldState {
|
interface SpamShieldState {
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
import { persist, createJSONStorage } from 'zustand/middleware';
|
import { persist, createJSONStorage } from 'zustand/middleware';
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import type { VoiceProfile, VoiceAnalysis } from '@/types';
|
import type { VoiceProfile, VoiceAnalysis } from '@/types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO: Wire store operations to @shieldai/mobile-api-client for production.
|
||||||
|
* Current implementation is local-only (AsyncStorage) for offline-first MVP.
|
||||||
|
*/
|
||||||
|
|
||||||
interface VoicePrintState {
|
interface VoicePrintState {
|
||||||
profiles: VoiceProfile[];
|
profiles: VoiceProfile[];
|
||||||
analyses: VoiceAnalysis[];
|
analyses: VoiceAnalysis[];
|
||||||
@@ -24,7 +30,7 @@ export const useVoicePrintStore = create<VoicePrintState>()(
|
|||||||
|
|
||||||
addProfile: async (name: string, relationship: string) => {
|
addProfile: async (name: string, relationship: string) => {
|
||||||
const newProfile: VoiceProfile = {
|
const newProfile: VoiceProfile = {
|
||||||
id: crypto.randomUUID(),
|
id: uuidv4(),
|
||||||
name,
|
name,
|
||||||
relationship,
|
relationship,
|
||||||
enrolledAt: new Date().toISOString(),
|
enrolledAt: new Date().toISOString(),
|
||||||
|
|||||||
@@ -18,7 +18,8 @@
|
|||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"incremental": true
|
"incremental": true,
|
||||||
|
"types": ["react-native", "node"]
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"src/**/*",
|
"src/**/*",
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user