From a071aa736ef311895e97e314d0a1b78ca6696ee1 Mon Sep 17 00:00:00 2001 From: Michael Freno Date: Sun, 17 May 2026 10:12:46 -0400 Subject: [PATCH] feat: scaffold ShieldAI React Native mobile app MVP (FRE-4572) Build complete Expo/React Native mobile app with: - Auth flow: email/password login, registration, biometric auth - Dashboard: exposure summary, spam stats, voice protection status - DarkWatch: watch list management, exposure feed, alert toggles - SpamShield: call/text history, whitelist/blacklist management - VoicePrint: family member enrollment, voice analysis - Settings: tier management, notification preferences, security - Push notification integration via FCM/APNs - Offline-first state management with Zustand + AsyncStorage - Integration with @shieldai/mobile-api-client for API services - React Navigation with auth-aware routing (stack + bottom tabs) - Dark theme with consistent design system (colors, spacing, typography) - Network status monitoring and offline request queuing Co-Authored-By: Paperclip --- packages/mobile/.gitignore | 20 ++ packages/mobile/App.tsx | 29 ++ packages/mobile/app.json | 66 ++++ packages/mobile/assets/adaptive-icon.png | Bin 0 -> 70 bytes packages/mobile/assets/favicon.png | Bin 0 -> 70 bytes packages/mobile/assets/icon.png | Bin 0 -> 70 bytes packages/mobile/assets/notification-icon.png | Bin 0 -> 70 bytes packages/mobile/assets/splash.png | Bin 0 -> 70 bytes packages/mobile/babel.config.js | 9 + packages/mobile/metro.config.js | 8 + packages/mobile/package.json | 65 +++- packages/mobile/src/components/Button.tsx | 65 ++++ packages/mobile/src/components/Card.tsx | 69 ++++ packages/mobile/src/components/Input.tsx | 51 +++ packages/mobile/src/components/Loading.tsx | 73 ++++ packages/mobile/src/components/StatCard.tsx | 45 +++ packages/mobile/src/components/index.ts | 5 + packages/mobile/src/constants/index.ts | 11 + packages/mobile/src/constants/theme.ts | 84 +++++ packages/mobile/src/hooks/index.ts | 3 + packages/mobile/src/hooks/useBiometricAuth.ts | 53 +++ packages/mobile/src/hooks/useNetworkStatus.ts | 18 + .../mobile/src/hooks/usePushNotifications.ts | 65 ++++ .../mobile/src/navigation/AuthNavigator.tsx | 24 ++ .../src/navigation/MainTabNavigator.tsx | 119 +++++++ packages/mobile/src/navigation/index.ts | 2 + .../mobile/src/screens/auth/LoginScreen.tsx | 146 ++++++++ .../src/screens/auth/RegisterScreen.tsx | 173 ++++++++++ packages/mobile/src/screens/auth/index.ts | 2 + .../src/screens/darkwatch/DarkWatchScreen.tsx | 321 ++++++++++++++++++ .../mobile/src/screens/darkwatch/index.ts | 1 + .../src/screens/dashboard/DashboardScreen.tsx | 134 ++++++++ .../mobile/src/screens/dashboard/index.ts | 1 + .../src/screens/settings/SettingsScreen.tsx | 281 +++++++++++++++ packages/mobile/src/screens/settings/index.ts | 1 + .../screens/spamshield/SpamShieldScreen.tsx | 294 ++++++++++++++++ .../mobile/src/screens/spamshield/index.ts | 1 + .../screens/voiceprint/VoicePrintScreen.tsx | 316 +++++++++++++++++ .../mobile/src/screens/voiceprint/index.ts | 1 + packages/mobile/src/services/api.ts | 8 + packages/mobile/src/services/index.ts | 1 + packages/mobile/src/store/authStore.ts | 108 ++++++ packages/mobile/src/store/darkWatchStore.ts | 60 ++++ packages/mobile/src/store/dashboardStore.ts | 46 +++ packages/mobile/src/store/settingsStore.ts | 40 +++ packages/mobile/src/store/spamShieldStore.ts | 58 ++++ packages/mobile/src/store/voicePrintStore.ts | 50 +++ packages/mobile/src/types/index.ts | 79 +++++ packages/mobile/tsconfig.json | 32 ++ packages/mobile/tsconfig.tsbuildinfo | 1 + 50 files changed, 3026 insertions(+), 13 deletions(-) create mode 100644 packages/mobile/.gitignore create mode 100644 packages/mobile/App.tsx create mode 100644 packages/mobile/app.json create mode 100644 packages/mobile/assets/adaptive-icon.png create mode 100644 packages/mobile/assets/favicon.png create mode 100644 packages/mobile/assets/icon.png create mode 100644 packages/mobile/assets/notification-icon.png create mode 100644 packages/mobile/assets/splash.png create mode 100644 packages/mobile/babel.config.js create mode 100644 packages/mobile/metro.config.js create mode 100644 packages/mobile/src/components/Button.tsx create mode 100644 packages/mobile/src/components/Card.tsx create mode 100644 packages/mobile/src/components/Input.tsx create mode 100644 packages/mobile/src/components/Loading.tsx create mode 100644 packages/mobile/src/components/StatCard.tsx create mode 100644 packages/mobile/src/components/index.ts create mode 100644 packages/mobile/src/constants/index.ts create mode 100644 packages/mobile/src/constants/theme.ts create mode 100644 packages/mobile/src/hooks/index.ts create mode 100644 packages/mobile/src/hooks/useBiometricAuth.ts create mode 100644 packages/mobile/src/hooks/useNetworkStatus.ts create mode 100644 packages/mobile/src/hooks/usePushNotifications.ts create mode 100644 packages/mobile/src/navigation/AuthNavigator.tsx create mode 100644 packages/mobile/src/navigation/MainTabNavigator.tsx create mode 100644 packages/mobile/src/navigation/index.ts create mode 100644 packages/mobile/src/screens/auth/LoginScreen.tsx create mode 100644 packages/mobile/src/screens/auth/RegisterScreen.tsx create mode 100644 packages/mobile/src/screens/auth/index.ts create mode 100644 packages/mobile/src/screens/darkwatch/DarkWatchScreen.tsx create mode 100644 packages/mobile/src/screens/darkwatch/index.ts create mode 100644 packages/mobile/src/screens/dashboard/DashboardScreen.tsx create mode 100644 packages/mobile/src/screens/dashboard/index.ts create mode 100644 packages/mobile/src/screens/settings/SettingsScreen.tsx create mode 100644 packages/mobile/src/screens/settings/index.ts create mode 100644 packages/mobile/src/screens/spamshield/SpamShieldScreen.tsx create mode 100644 packages/mobile/src/screens/spamshield/index.ts create mode 100644 packages/mobile/src/screens/voiceprint/VoicePrintScreen.tsx create mode 100644 packages/mobile/src/screens/voiceprint/index.ts create mode 100644 packages/mobile/src/services/api.ts create mode 100644 packages/mobile/src/services/index.ts create mode 100644 packages/mobile/src/store/authStore.ts create mode 100644 packages/mobile/src/store/darkWatchStore.ts create mode 100644 packages/mobile/src/store/dashboardStore.ts create mode 100644 packages/mobile/src/store/settingsStore.ts create mode 100644 packages/mobile/src/store/spamShieldStore.ts create mode 100644 packages/mobile/src/store/voicePrintStore.ts create mode 100644 packages/mobile/src/types/index.ts create mode 100644 packages/mobile/tsconfig.json create mode 100644 packages/mobile/tsconfig.tsbuildinfo diff --git a/packages/mobile/.gitignore b/packages/mobile/.gitignore new file mode 100644 index 0000000..2799b87 --- /dev/null +++ b/packages/mobile/.gitignore @@ -0,0 +1,20 @@ +node_modules/ +.expo/ +.expo-shared/ +dist/ +ios/ +android/ +*.jks +*.p8 +*.p12 +*.mobileprovision +*.orig.* +*.pub +.jscache +*.log +*.pid +*.tgz +*.npm +*.lock +.DS_Store +.env diff --git a/packages/mobile/App.tsx b/packages/mobile/App.tsx new file mode 100644 index 0000000..29c53ab --- /dev/null +++ b/packages/mobile/App.tsx @@ -0,0 +1,29 @@ +import React, { useEffect } from 'react'; +import { StatusBar } from 'expo-status-bar'; +import { NavigationContainer } from '@react-navigation/native'; +import { GestureHandlerRootView } from 'react-native-gesture-handler'; +import { useAuthStore } from '@/store/authStore'; +import { AuthNavigator } from '@/navigation/AuthNavigator'; +import { MainTabNavigator } from '@/navigation/MainTabNavigator'; +import { usePushNotifications } from '@/hooks'; +import '@/services/api'; + +export default function App() { + const { isAuthenticated } = useAuthStore(); + const { registerForPushNotifications } = usePushNotifications(); + + useEffect(() => { + if (isAuthenticated) { + registerForPushNotifications(); + } + }, [isAuthenticated, registerForPushNotifications]); + + return ( + + + + {isAuthenticated ? : } + + + ); +} diff --git a/packages/mobile/app.json b/packages/mobile/app.json new file mode 100644 index 0000000..5615752 --- /dev/null +++ b/packages/mobile/app.json @@ -0,0 +1,66 @@ +{ + "expo": { + "name": "ShieldAI", + "slug": "shieldai", + "version": "1.0.0", + "orientation": "portrait", + "icon": "./assets/icon.png", + "scheme": "shieldai", + "userInterfaceStyle": "automatic", + "splash": { + "image": "./assets/splash.png", + "resizeMode": "contain", + "backgroundColor": "#0a0e1a" + }, + "ios": { + "supportsTablet": true, + "bundleIdentifier": "com.frenocorp.shieldai", + "infoPlist": { + "NSFaceIDUsageDescription": "ShieldAI uses Face ID to securely access your account.", + "NSCameraUsageDescription": "ShieldAI needs camera access for VoicePrint enrollment.", + "NSMicrophoneUsageDescription": "ShieldAI needs microphone access for voice analysis." + }, + "config": { + "usesNonExemptEncryption": false + } + }, + "android": { + "adaptiveIcon": { + "foregroundImage": "./assets/adaptive-icon.png", + "backgroundColor": "#0a0e1a" + }, + "package": "com.frenocorp.shieldai", + "permissions": [ + "RECEIVE_BOOT_COMPLETED", + "VIBRATE", + "android.permission.CAMERA", + "android.permission.RECORD_AUDIO" + ] + }, + "web": { + "bundler": "metro", + "output": "static", + "favicon": "./assets/favicon.png" + }, + "plugins": [ + "expo-secure-store", + "expo-local-authentication", + [ + "expo-notifications", + { + "icon": "./assets/notification-icon.png", + "color": "#4f8cff", + "sounds": [] + } + ] + ], + "experiments": { + "typedRoutes": true + }, + "extra": { + "eas": { + "projectId": "shieldai-project-id" + } + } + } +} diff --git a/packages/mobile/assets/adaptive-icon.png b/packages/mobile/assets/adaptive-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..08cd6f2bfd1b53ec5a4db72bed55f40907e8bdfa GIT binary patch literal 70 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx1|;Q0k92}1TpU9xZY8JuI3K{zz}&{z5M@%E Q4U}N;boFyt=akR{0J void; + variant?: 'primary' | 'secondary' | 'danger' | 'ghost'; + disabled?: boolean; + loading?: boolean; + fullWidth?: boolean; +} + +export function Button({ + title, + onPress, + variant = 'primary', + disabled = false, + loading = false, + fullWidth = false, +}: ButtonProps) { + const variantColors = { + primary: { bg: COLORS.primary, text: '#fff' }, + secondary: { bg: COLORS.secondary, text: '#fff' }, + danger: { bg: COLORS.danger, text: '#fff' }, + ghost: { bg: 'transparent', text: COLORS.primary }, + }; + + const colors = variantColors[variant]; + + return ( + + + {loading ? '...' : title} + + + ); +} + +const styles = StyleSheet.create({ + button: { + borderRadius: BORDER_RADIUS.md, + paddingVertical: 12, + paddingHorizontal: 24, + alignItems: 'center', + justifyContent: 'center', + marginVertical: 4, + }, + fullWidth: { + width: '100%', + }, + text: { + fontSize: FONT_SIZES.md, + fontWeight: '600', + }, +}); diff --git a/packages/mobile/src/components/Card.tsx b/packages/mobile/src/components/Card.tsx new file mode 100644 index 0000000..152a0cb --- /dev/null +++ b/packages/mobile/src/components/Card.tsx @@ -0,0 +1,69 @@ +import React from 'react'; +import { StyleSheet, Text, View, ViewStyle } from 'react-native'; +import { COLORS, BORDER_RADIUS, FONT_SIZES, SPACING } from '@/constants/theme'; + +interface SectionHeaderProps { + title: string; + action?: string; + onActionPress?: () => void; +} + +export function SectionHeader({ title, action, onActionPress }: SectionHeaderProps) { + return ( + + {title} + {action && onActionPress && ( + + {action} + + )} + + ); +} + +interface CardProps { + title?: string; + children: React.ReactNode; + style?: ViewStyle; +} + +export function Card({ title, children, style }: CardProps) { + return ( + + {title && {title}} + {children} + + ); +} + +const styles = StyleSheet.create({ + container: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingHorizontal: SPACING.md, + paddingVertical: SPACING.sm, + }, + title: { + color: COLORS.text, + fontSize: FONT_SIZES.lg, + fontWeight: '600', + }, + action: { + color: COLORS.primary, + fontSize: FONT_SIZES.sm, + }, + card: { + backgroundColor: COLORS.card, + borderRadius: BORDER_RADIUS.lg, + padding: SPACING.md, + marginHorizontal: SPACING.md, + marginVertical: SPACING.xs, + }, + cardTitle: { + color: COLORS.text, + fontSize: FONT_SIZES.md, + fontWeight: '600', + marginBottom: SPACING.sm, + }, +}); diff --git a/packages/mobile/src/components/Input.tsx b/packages/mobile/src/components/Input.tsx new file mode 100644 index 0000000..09d18e9 --- /dev/null +++ b/packages/mobile/src/components/Input.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import { StyleSheet, TextInput, TextInputProps, Text, View } from 'react-native'; +import { COLORS, BORDER_RADIUS, FONT_SIZES } from '@/constants/theme'; + +interface InputProps extends Omit { + label?: string; + error?: string; + containerStyle?: object; +} + +export function Input({ label, error, containerStyle, ...props }: InputProps) { + return ( + + {label && {label}} + + {error && {error}} + + ); +} + +const styles = StyleSheet.create({ + container: { + marginBottom: 16, + }, + label: { + color: COLORS.textSecondary, + fontSize: FONT_SIZES.sm, + marginBottom: 6, + }, + input: { + backgroundColor: COLORS.backgroundLight, + borderColor: COLORS.border, + borderWidth: 1, + borderRadius: BORDER_RADIUS.md, + padding: 12, + color: COLORS.text, + fontSize: FONT_SIZES.md, + }, + error: { + color: COLORS.danger, + fontSize: FONT_SIZES.xs, + marginTop: 4, + }, +}); diff --git a/packages/mobile/src/components/Loading.tsx b/packages/mobile/src/components/Loading.tsx new file mode 100644 index 0000000..c244562 --- /dev/null +++ b/packages/mobile/src/components/Loading.tsx @@ -0,0 +1,73 @@ +import React from 'react'; +import { StyleSheet, View, ActivityIndicator, Text } from 'react-native'; +import { COLORS, SPACING } from '@/constants/theme'; + +interface LoadingOverlayProps { + visible: boolean; +} + +export function LoadingOverlay({ visible }: LoadingOverlayProps) { + if (!visible) return null; + + return ( + + + + ); +} + +interface EmptyStateProps { + title: string; + message?: string; +} + +export function EmptyState({ title, message }: EmptyStateProps) { + return ( + + + + + {title} + {message && {message}} + + + + ); +} + +const styles = StyleSheet.create({ + overlay: { + ...StyleSheet.absoluteFillObject, + backgroundColor: 'rgba(0, 0, 0, 0.3)', + justifyContent: 'center', + alignItems: 'center', + }, + emptyContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + padding: SPACING.xl, + }, + emptyContent: { + alignItems: 'center', + }, + emptyIcon: { + width: 48, + height: 48, + borderRadius: 24, + backgroundColor: COLORS.card, + marginBottom: SPACING.md, + }, + emptyText: { + alignItems: 'center', + }, + emptyTitle: { + color: COLORS.textSecondary, + fontSize: 16, + fontWeight: '600', + }, + emptyMessage: { + color: COLORS.textMuted, + fontSize: 14, + }, +}); diff --git a/packages/mobile/src/components/StatCard.tsx b/packages/mobile/src/components/StatCard.tsx new file mode 100644 index 0000000..334a73f --- /dev/null +++ b/packages/mobile/src/components/StatCard.tsx @@ -0,0 +1,45 @@ +import React from 'react'; +import { StyleSheet, Text, View } from 'react-native'; +import { COLORS, FONT_SIZES, SPACING } from '@/constants/theme'; + +interface StatCardProps { + title: string; + value: string | number; + icon?: string; + color?: string; + subtitle?: string; +} + +export function StatCard({ title, value, color = COLORS.primary, subtitle }: StatCardProps) { + return ( + + {title} + {value} + {subtitle && {subtitle}} + + ); +} + +const styles = StyleSheet.create({ + card: { + backgroundColor: COLORS.card, + borderRadius: 8, + padding: SPACING.md, + marginBottom: SPACING.sm, + borderLeftWidth: 3, + }, + title: { + color: COLORS.textSecondary, + fontSize: FONT_SIZES.sm, + marginBottom: SPACING.xs, + }, + value: { + fontSize: FONT_SIZES.xxl, + fontWeight: 'bold', + }, + subtitle: { + color: COLORS.textMuted, + fontSize: FONT_SIZES.xs, + marginTop: SPACING.xs, + }, +}); diff --git a/packages/mobile/src/components/index.ts b/packages/mobile/src/components/index.ts new file mode 100644 index 0000000..b22ae78 --- /dev/null +++ b/packages/mobile/src/components/index.ts @@ -0,0 +1,5 @@ +export * from './Button'; +export * from './Input'; +export * from './StatCard'; +export * from './Card'; +export * from './Loading'; diff --git a/packages/mobile/src/constants/index.ts b/packages/mobile/src/constants/index.ts new file mode 100644 index 0000000..c81f1fd --- /dev/null +++ b/packages/mobile/src/constants/index.ts @@ -0,0 +1,11 @@ +export const TAB_ORDER = ['Dashboard', 'DarkWatch', 'SpamShield', 'VoicePrint', 'Settings'] as const; + +export const AUTH_STORAGE_KEY = '@shieldai_auth'; +export const BIOMETRIC_ENABLED_KEY = '@shieldai_biometric'; +export const ONBOARDING_COMPLETE_KEY = '@shieldai_onboarding'; + +export const NOTIFICATION_TYPES = { + DARKWATCH_ALERT: 'darkwatch_alert', + SPAM_BLOCKED: 'spam_blocked', + VOICEPRINT_ANALYSIS: 'voiceprint_analysis', +} as const; diff --git a/packages/mobile/src/constants/theme.ts b/packages/mobile/src/constants/theme.ts new file mode 100644 index 0000000..8eba894 --- /dev/null +++ b/packages/mobile/src/constants/theme.ts @@ -0,0 +1,84 @@ +export const COLORS = { + primary: '#4f8cff', + primaryDark: '#3a6fd8', + secondary: '#6c5ce7', + accent: '#00cec9', + success: '#00b894', + warning: '#fdcb6e', + danger: '#ff6b6b', + background: '#0a0e1a', + backgroundLight: '#111827', + card: '#1a2035', + cardLight: '#243049', + text: '#e8eaf0', + textSecondary: '#8b95a8', + textMuted: '#5a6577', + border: '#2a3550', + overlay: 'rgba(0, 0, 0, 0.6)', +} as const; + +export const SPACING = { + xs: 4, + sm: 8, + md: 16, + lg: 24, + xl: 32, + xxl: 48, +} as const; + +export const FONT_SIZES = { + xs: 12, + sm: 14, + md: 16, + lg: 18, + xl: 20, + xxl: 24, + xxxl: 32, +} as const; + +export const BORDER_RADIUS = { + sm: 4, + md: 8, + lg: 12, + xl: 16, + round: 999, +} as const; + +export const SHADOWS = { + sm: { shadowColor: '#000', shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.1, shadowRadius: 2, elevation: 2 }, + md: { shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.15, shadowRadius: 4, elevation: 4 }, + lg: { shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.2, shadowRadius: 8, elevation: 8 }, +} as const; + +export const API_URL = process.env.EXPO_PUBLIC_API_URL || 'https://api.shieldai.freno.me/api/v1'; + +export const TIER_FEATURES = { + free: { + name: 'Free', + maxExposures: 5, + spamProtection: false, + voicePrint: false, + darkWatch: false, + }, + basic: { + name: 'Basic', + maxExposures: 50, + spamProtection: true, + voicePrint: false, + darkWatch: true, + }, + premium: { + name: 'Premium', + maxExposures: 200, + spamProtection: true, + voicePrint: true, + darkWatch: true, + }, + enterprise: { + name: 'Enterprise', + maxExposures: -1, + spamProtection: true, + voicePrint: true, + darkWatch: true, + }, +} as const; diff --git a/packages/mobile/src/hooks/index.ts b/packages/mobile/src/hooks/index.ts new file mode 100644 index 0000000..b3a55b8 --- /dev/null +++ b/packages/mobile/src/hooks/index.ts @@ -0,0 +1,3 @@ +export * from './usePushNotifications'; +export * from './useBiometricAuth'; +export * from './useNetworkStatus'; diff --git a/packages/mobile/src/hooks/useBiometricAuth.ts b/packages/mobile/src/hooks/useBiometricAuth.ts new file mode 100644 index 0000000..501f23e --- /dev/null +++ b/packages/mobile/src/hooks/useBiometricAuth.ts @@ -0,0 +1,53 @@ +import { useCallback } from 'react'; +import * as LocalAuthentication from 'expo-local-authentication'; +import { useSettingsStore } from '@/store/settingsStore'; + +export function useBiometricAuth() { + const { isBiometricEnabled } = useSettingsStore(); + + const authenticate = useCallback(async (): Promise => { + if (!isBiometricEnabled) return true; + + try { + const isAvailable = await LocalAuthentication.hasHardwareAsync(); + if (!isAvailable) return true; + + const isEnrolled = await LocalAuthentication.isEnrolledAsync(); + if (!isEnrolled) return true; + + const result = await LocalAuthentication.authenticateAsync({ + promptMessage: 'Authenticate with biometrics', + fallbackLabel: 'Use passcode', + cancelLabel: 'Cancel', + }); + + return result.success; + } catch { + return false; + } + }, [isBiometricEnabled]); + + const enableBiometric = useCallback(async (): Promise => { + const isAvailable = await LocalAuthentication.hasHardwareAsync(); + const isEnrolled = await LocalAuthentication.isEnrolledAsync(); + + if (!isAvailable || !isEnrolled) return false; + + const result = await LocalAuthentication.authenticateAsync({ + promptMessage: 'Enable biometric authentication', + fallbackLabel: 'Use passcode', + }); + + if (result.success) { + useSettingsStore.getState().toggleBiometric(true); + } + + return result.success; + }, []); + + const disableBiometric = useCallback(() => { + useSettingsStore.getState().toggleBiometric(false); + }, []); + + return { authenticate, enableBiometric, disableBiometric, isBiometricEnabled }; +} diff --git a/packages/mobile/src/hooks/useNetworkStatus.ts b/packages/mobile/src/hooks/useNetworkStatus.ts new file mode 100644 index 0000000..8d94b79 --- /dev/null +++ b/packages/mobile/src/hooks/useNetworkStatus.ts @@ -0,0 +1,18 @@ +import { useEffect, useState } from 'react'; +import NetInfo from '@react-native-community/netinfo'; + +export function useNetworkStatus() { + const [isConnected, setIsConnected] = useState(true); + const [isOnline, setIsOnline] = useState(true); + + useEffect(() => { + const unsubscribe = NetInfo.addEventListener((state) => { + setIsConnected(!!state.isConnected); + setIsOnline(!!state.isInternetReachable); + }); + + return unsubscribe; + }, []); + + return { isConnected, isOnline, isOffline: !isOnline }; +} diff --git a/packages/mobile/src/hooks/usePushNotifications.ts b/packages/mobile/src/hooks/usePushNotifications.ts new file mode 100644 index 0000000..412a0de --- /dev/null +++ b/packages/mobile/src/hooks/usePushNotifications.ts @@ -0,0 +1,65 @@ +import { useEffect, useCallback } from 'react'; +import * as Notifications from 'expo-notifications'; +import { Platform } from 'react-native'; +import { deviceService, notificationService } from '@shieldai/mobile-api-client'; +import { useSettingsStore } from '@/store/settingsStore'; + +Notifications.setNotificationHandler({ + handleNotification: async () => ({ + shouldShowAlert: true, + shouldPlaySound: true, + shouldSetBadge: false, + }), +}); + +export function usePushNotifications() { + const { preferences } = useSettingsStore(); + + 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; + + await deviceService.registerDevice({ + platform: Platform.OS === 'ios' ? 'ios' : 'android', + pushToken: token, + modelName: Platform.OS === 'ios' ? 'iPhone' : 'Android', + osVersion: Platform.Version.toString(), + appVersion: '1.0.0', + }); + + 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 = useSettingsStore.getState().preferences; + + 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 }; +} diff --git a/packages/mobile/src/navigation/AuthNavigator.tsx b/packages/mobile/src/navigation/AuthNavigator.tsx new file mode 100644 index 0000000..ecac723 --- /dev/null +++ b/packages/mobile/src/navigation/AuthNavigator.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { createStackNavigator } from '@react-navigation/stack'; +import { LoginScreen, RegisterScreen } from '@/screens/auth'; + +type AuthStackParamList = { + Login: undefined; + Register: undefined; +}; + +const Stack = createStackNavigator(); + +export function AuthNavigator() { + return ( + + + + + ); +} diff --git a/packages/mobile/src/navigation/MainTabNavigator.tsx b/packages/mobile/src/navigation/MainTabNavigator.tsx new file mode 100644 index 0000000..fa66ac9 --- /dev/null +++ b/packages/mobile/src/navigation/MainTabNavigator.tsx @@ -0,0 +1,119 @@ +import React from 'react'; +import { Text, ViewStyle, StyleSheet } from 'react-native'; +import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; +import { DashboardScreen } from '@/screens/dashboard'; +import { DarkWatchScreen } from '@/screens/darkwatch'; +import { SpamShieldScreen } from '@/screens/spamshield'; +import { VoicePrintScreen } from '@/screens/voiceprint'; +import { SettingsScreen } from '@/screens/settings'; +import { COLORS, FONT_SIZES } from '@/constants/theme'; + +type MainTabParamList = { + Dashboard: undefined; + DarkWatch: undefined; + SpamShield: undefined; + VoicePrint: undefined; + Settings: undefined; +}; + +const Tab = createBottomTabNavigator(); + +const iconMap: Record = { + Dashboard: '\u{1F6E1}\u{FE0F}', + DarkWatch: '\u{1F441}\u{FE0F}', + SpamShield: '\u{1F6AB}', + VoicePrint: '\u{1F399}\u{FE0F}', + Settings: '\u{2699}\u{FE0F}', +}; + +function TabIcon({ routeName, color }: { routeName: string; color: string }) { + return ( + {iconMap[routeName] || '\u{2022}'} + ); +} + +const styles = StyleSheet.create({ + icon: { + fontSize: 22, + }, +}); + +export function MainTabNavigator() { + return ( + + , + }} + /> + , + }} + /> + , + }} + /> + , + }} + /> + , + }} + /> + + ); +} diff --git a/packages/mobile/src/navigation/index.ts b/packages/mobile/src/navigation/index.ts new file mode 100644 index 0000000..03db8c5 --- /dev/null +++ b/packages/mobile/src/navigation/index.ts @@ -0,0 +1,2 @@ +export * from './AuthNavigator'; +export * from './MainTabNavigator'; diff --git a/packages/mobile/src/screens/auth/LoginScreen.tsx b/packages/mobile/src/screens/auth/LoginScreen.tsx new file mode 100644 index 0000000..8ae20fb --- /dev/null +++ b/packages/mobile/src/screens/auth/LoginScreen.tsx @@ -0,0 +1,146 @@ +import React, { useState } from 'react'; +import { StyleSheet, Text, View, SafeAreaView, KeyboardAvoidingView, Platform, ScrollView, Alert } from 'react-native'; +import { RouteProp, useRoute, useNavigation } from '@react-navigation/native'; +import { useAuthStore } from '@/store/authStore'; +import { Button, Input } from '@/components'; +import { COLORS, FONT_SIZES, SPACING } from '@/constants/theme'; + +type RootStackParamList = { + Login: undefined; + Register: undefined; + App: undefined; +}; + +type LoginScreenRouteProp = RouteProp; + +export function LoginScreen() { + const route = useRoute(); + const navigation = useNavigation(); + const { login, isLoading, error, clearError } = useAuthStore(); + + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [formError, setFormError] = useState(''); + + const handleLogin = async () => { + setFormError(''); + clearError(); + + if (!email || !password) { + setFormError('Please fill in all fields'); + return; + } + + try { + await login(email, password); + } catch (err: any) { + setFormError(err.message || 'Login failed. Please try again.'); + Alert.alert('Login Failed', err.message || 'Please check your credentials and try again.'); + } + }; + + return ( + + + + + ShieldAI + Your digital protection suite + + + + {formError && {formError}} + + + + + +