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:
281
packages/mobile/src/screens/settings/SettingsScreen.tsx
Normal file
281
packages/mobile/src/screens/settings/SettingsScreen.tsx
Normal 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,
|
||||
},
|
||||
});
|
||||
1
packages/mobile/src/screens/settings/index.ts
Normal file
1
packages/mobile/src/screens/settings/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './SettingsScreen';
|
||||
Reference in New Issue
Block a user