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 <noreply@paperclip.ing>
This commit is contained in:
2026-05-17 10:12:46 -04:00
parent 7fb8b83810
commit a071aa736e
50 changed files with 3026 additions and 13 deletions

View File

@@ -0,0 +1,281 @@
import React from 'react';
import { StyleSheet, Text, View, SafeAreaView, ScrollView, TouchableOpacity, Switch, Alert } from 'react-native';
import { useAuthStore } from '@/store/authStore';
import { useSettingsStore } from '@/store/settingsStore';
import { useBiometricAuth } from '@/hooks';
import { Card, Button } from '@/components';
import { COLORS, FONT_SIZES, SPACING } from '@/constants/theme';
import { TIER_FEATURES } from '@/constants/theme';
export function SettingsScreen() {
const { user, logout } = useAuthStore();
const { preferences, updatePreferences, isBiometricEnabled } = useSettingsStore();
const { enableBiometric, disableBiometric } = useBiometricAuth();
const handleLogout = () => {
Alert.alert(
'Logout',
'Are you sure you want to logout?',
[
{ text: 'Cancel', style: 'cancel' },
{
text: 'Logout',
style: 'destructive',
onPress: () => logout(),
},
]
);
};
const handleBiometricToggle = async (value: boolean) => {
if (value) {
const success = await enableBiometric();
if (!success) {
updatePreferences({});
}
} else {
disableBiometric();
}
};
const tier = user?.tier || 'free';
const features = TIER_FEATURES[tier as keyof typeof TIER_FEATURES];
return (
<SafeAreaView style={styles.container}>
<View style={styles.header}>
<Text style={styles.title}>Settings</Text>
</View>
<ScrollView style={styles.content}>
<Card title="Account">
<View style={styles.profileRow}>
<View style={styles.avatar}>
<Text style={styles.avatarText}>
{user?.firstName?.charAt(0) || 'U'}
</Text>
</View>
<View style={styles.profileInfo}>
<Text style={styles.profileName}>
{user?.firstName} {user?.lastName}
</Text>
<Text style={styles.profileEmail}>{user?.email}</Text>
</View>
</View>
</Card>
<Card title="Subscription">
<View style={styles.tierRow}>
<Text style={styles.tierLabel}>Current Plan</Text>
<View style={[styles.tierBadge, { backgroundColor: COLORS.primary }]}>
<Text style={styles.tierBadgeText}>{features.name}</Text>
</View>
</View>
<View style={styles.featuresList}>
<FeatureRow label="Spam Protection" available={features.spamProtection} />
<FeatureRow label="VoicePrint" available={features.voicePrint} />
<FeatureRow label="DarkWatch" available={features.darkWatch} />
<FeatureRow
label="Max Exposures/Month"
value={features.maxExposures === -1 ? 'Unlimited' : `${features.maxExposures}`}
/>
</View>
<Button title="Upgrade Plan" onPress={() => {}} variant="secondary" fullWidth />
</Card>
<Card title="Notifications">
<ToggleRow
label="Push Notifications"
value={preferences.pushNotifications}
onToggle={() => updatePreferences({ pushNotifications: !preferences.pushNotifications })}
/>
<ToggleRow
label="Email Notifications"
value={preferences.emailNotifications}
onToggle={() => updatePreferences({ emailNotifications: !preferences.emailNotifications })}
/>
<ToggleRow
label="DarkWatch Alerts"
value={preferences.darkwatchAlert}
onToggle={() => updatePreferences({ darkwatchAlert: !preferences.darkwatchAlert })}
/>
<ToggleRow
label="Spam Blocked Alerts"
value={preferences.spamBlocked}
onToggle={() => updatePreferences({ spamBlocked: !preferences.spamBlocked })}
/>
<ToggleRow
label="VoicePrint Analysis"
value={preferences.voiceprintAnalysis}
onToggle={() => updatePreferences({ voiceprintAnalysis: !preferences.voiceprintAnalysis })}
/>
</Card>
<Card title="Security">
<ToggleRow
label="Biometric Authentication"
value={isBiometricEnabled}
onToggle={handleBiometricToggle}
/>
</Card>
<View style={styles.logoutSection}>
<Button title="Logout" onPress={handleLogout} variant="danger" fullWidth />
</View>
<View style={styles.version}>
<Text style={styles.versionText}>ShieldAI v1.0.0</Text>
</View>
</ScrollView>
</SafeAreaView>
);
}
interface ToggleRowProps {
label: string;
value: boolean;
onToggle: (value: boolean) => void;
}
function ToggleRow({ label, value, onToggle }: ToggleRowProps) {
return (
<View style={styles.toggleRow}>
<Text style={styles.toggleLabel}>{label}</Text>
<Switch
value={value}
onValueChange={onToggle}
trackColor={{ true: COLORS.primary, false: COLORS.border }}
thumbColor="#fff"
/>
</View>
);
}
interface FeatureRowProps {
label: string;
available?: boolean;
value?: string;
}
function FeatureRow({ label, available, value }: FeatureRowProps) {
return (
<View style={styles.featureRow}>
<Text style={styles.featureLabel}>{label}</Text>
<Text style={styles.featureValue}>
{value ?? (available !== undefined ? (available ? '✓' : '✕') : '')}
</Text>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: COLORS.background,
},
header: {
padding: SPACING.md,
borderBottomWidth: 1,
borderBottomColor: COLORS.border,
},
title: {
color: COLORS.text,
fontSize: FONT_SIZES.xxl,
fontWeight: 'bold',
},
content: {
flex: 1,
},
profileRow: {
flexDirection: 'row',
alignItems: 'center',
},
avatar: {
width: 56,
height: 56,
borderRadius: 28,
backgroundColor: COLORS.primary,
justifyContent: 'center',
alignItems: 'center',
marginRight: SPACING.md,
},
avatarText: {
color: '#fff',
fontSize: FONT_SIZES.xxl,
fontWeight: 'bold',
},
profileInfo: {
flex: 1,
},
profileName: {
color: COLORS.text,
fontSize: FONT_SIZES.lg,
fontWeight: '600',
},
profileEmail: {
color: COLORS.textSecondary,
fontSize: FONT_SIZES.sm,
marginTop: 2,
},
tierRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: SPACING.md,
},
tierLabel: {
color: COLORS.textSecondary,
fontSize: FONT_SIZES.md,
},
tierBadge: {
paddingHorizontal: SPACING.md,
paddingVertical: SPACING.xs,
borderRadius: 16,
},
tierBadgeText: {
color: '#fff',
fontSize: FONT_SIZES.sm,
fontWeight: '600',
},
featuresList: {
gap: SPACING.sm,
},
featureRow: {
flexDirection: 'row',
justifyContent: 'space-between',
paddingVertical: SPACING.xs,
},
featureLabel: {
color: COLORS.textSecondary,
fontSize: FONT_SIZES.sm,
},
featureValue: {
color: COLORS.text,
fontSize: FONT_SIZES.sm,
fontWeight: '500',
},
toggleRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingVertical: SPACING.sm,
borderBottomWidth: 1,
borderBottomColor: COLORS.border,
},
toggleLabel: {
color: COLORS.text,
fontSize: FONT_SIZES.md,
},
logoutSection: {
paddingHorizontal: SPACING.md,
paddingVertical: SPACING.md,
},
version: {
alignItems: 'center',
paddingVertical: SPACING.lg,
},
versionText: {
color: COLORS.textMuted,
fontSize: FONT_SIZES.xs,
},
});

View File

@@ -0,0 +1 @@
export * from './SettingsScreen';