frontend first version
This commit is contained in:
238
src/components/common/BiometricModal.tsx
Normal file
238
src/components/common/BiometricModal.tsx
Normal file
@@ -0,0 +1,238 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
Modal,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
Animated,
|
||||
} from 'react-native';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import { Ionicons, MaterialCommunityIcons } from '@expo/vector-icons';
|
||||
import { colors, borderRadius, spacing, typography, shadows } from '../../theme/colors';
|
||||
|
||||
interface BiometricModalProps {
|
||||
visible: boolean;
|
||||
onSuccess: () => void;
|
||||
onCancel: () => void;
|
||||
title?: string;
|
||||
message?: string;
|
||||
isDark?: boolean;
|
||||
}
|
||||
|
||||
export default function BiometricModal({
|
||||
visible,
|
||||
onSuccess,
|
||||
onCancel,
|
||||
title = 'Captain\'s Verification',
|
||||
message = 'Verify your identity to continue',
|
||||
isDark = false,
|
||||
}: BiometricModalProps) {
|
||||
const [scanAnimation] = useState(new Animated.Value(0));
|
||||
const [pulseAnimation] = useState(new Animated.Value(1));
|
||||
const [isScanning, setIsScanning] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
setIsScanning(false);
|
||||
scanAnimation.setValue(0);
|
||||
|
||||
// Pulse animation
|
||||
Animated.loop(
|
||||
Animated.sequence([
|
||||
Animated.timing(pulseAnimation, {
|
||||
toValue: 1.08,
|
||||
duration: 1000,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(pulseAnimation, {
|
||||
toValue: 1,
|
||||
duration: 1000,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
])
|
||||
).start();
|
||||
}
|
||||
}, [visible]);
|
||||
|
||||
const handleScan = () => {
|
||||
setIsScanning(true);
|
||||
|
||||
Animated.loop(
|
||||
Animated.sequence([
|
||||
Animated.timing(scanAnimation, {
|
||||
toValue: 1,
|
||||
duration: 800,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(scanAnimation, {
|
||||
toValue: 0,
|
||||
duration: 800,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
]),
|
||||
{ iterations: 2 }
|
||||
).start(() => {
|
||||
setTimeout(() => {
|
||||
onSuccess();
|
||||
}, 300);
|
||||
});
|
||||
};
|
||||
|
||||
const backgroundColor = isDark ? colors.vault.cardBackground : colors.white;
|
||||
const textColor = isDark ? colors.vault.text : colors.nautical.navy;
|
||||
const accentColor = isDark ? colors.vault.primary : colors.nautical.teal;
|
||||
const accentGradient: [string, string] = isDark
|
||||
? [colors.vault.primary, colors.vault.secondary]
|
||||
: [colors.nautical.teal, colors.nautical.seafoam];
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible={visible}
|
||||
transparent
|
||||
animationType="fade"
|
||||
onRequestClose={onCancel}
|
||||
>
|
||||
<View style={styles.overlay}>
|
||||
<View style={[styles.container, { backgroundColor }, shadows.medium]}>
|
||||
{/* Ship wheel watermark */}
|
||||
<View style={styles.watermark}>
|
||||
<MaterialCommunityIcons
|
||||
name="ship-wheel"
|
||||
size={150}
|
||||
color={isDark ? colors.vault.primary : colors.nautical.lightMint}
|
||||
style={{ opacity: 0.15 }}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<Text style={[styles.title, { color: textColor }]}>{title}</Text>
|
||||
<Text style={[styles.message, { color: isDark ? colors.vault.textSecondary : colors.nautical.sage }]}>
|
||||
{message}
|
||||
</Text>
|
||||
|
||||
<TouchableOpacity
|
||||
style={styles.fingerprintButton}
|
||||
onPress={handleScan}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.fingerprintOuter,
|
||||
{
|
||||
backgroundColor: `${accentColor}15`,
|
||||
transform: [{ scale: pulseAnimation }],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.fingerprintContainer,
|
||||
{
|
||||
opacity: scanAnimation.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [1, 0.7],
|
||||
}),
|
||||
transform: [
|
||||
{
|
||||
scale: scanAnimation.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [1, 1.05],
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
>
|
||||
<LinearGradient
|
||||
colors={accentGradient}
|
||||
style={styles.fingerprintGradient}
|
||||
>
|
||||
<Ionicons
|
||||
name={isScanning ? "finger-print" : "finger-print-outline"}
|
||||
size={48}
|
||||
color="#fff"
|
||||
/>
|
||||
</LinearGradient>
|
||||
</Animated.View>
|
||||
<Text style={[styles.scanText, { color: accentColor }]}>
|
||||
{isScanning ? 'Verifying...' : 'Tap to Verify'}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity style={styles.cancelButton} onPress={onCancel}>
|
||||
<Text style={[styles.cancelText, { color: isDark ? colors.vault.textSecondary : colors.nautical.sage }]}>
|
||||
Cancel
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
overlay: {
|
||||
flex: 1,
|
||||
backgroundColor: 'rgba(26, 58, 74, 0.7)',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
padding: spacing.lg,
|
||||
},
|
||||
container: {
|
||||
width: '100%',
|
||||
borderRadius: borderRadius.xxl,
|
||||
padding: spacing.xl,
|
||||
alignItems: 'center',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
watermark: {
|
||||
position: 'absolute',
|
||||
top: -30,
|
||||
right: -30,
|
||||
},
|
||||
title: {
|
||||
fontSize: typography.fontSize.lg,
|
||||
fontWeight: '600',
|
||||
marginBottom: spacing.sm,
|
||||
textAlign: 'center',
|
||||
fontFamily: typography.fontFamily.serif,
|
||||
},
|
||||
message: {
|
||||
fontSize: typography.fontSize.base,
|
||||
textAlign: 'center',
|
||||
marginBottom: spacing.xl,
|
||||
lineHeight: typography.fontSize.base * 1.5,
|
||||
},
|
||||
fingerprintButton: {
|
||||
alignItems: 'center',
|
||||
marginBottom: spacing.lg,
|
||||
},
|
||||
fingerprintOuter: {
|
||||
position: 'absolute',
|
||||
width: 150,
|
||||
height: 150,
|
||||
borderRadius: 75,
|
||||
},
|
||||
fingerprintContainer: {
|
||||
marginBottom: spacing.md,
|
||||
},
|
||||
fingerprintGradient: {
|
||||
width: 110,
|
||||
height: 110,
|
||||
borderRadius: 55,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
scanText: {
|
||||
fontSize: typography.fontSize.base,
|
||||
fontWeight: '600',
|
||||
},
|
||||
cancelButton: {
|
||||
paddingVertical: spacing.md,
|
||||
paddingHorizontal: spacing.xl,
|
||||
},
|
||||
cancelText: {
|
||||
fontSize: typography.fontSize.base,
|
||||
fontWeight: '500',
|
||||
},
|
||||
});
|
||||
169
src/components/common/Icons.tsx
Normal file
169
src/components/common/Icons.tsx
Normal file
@@ -0,0 +1,169 @@
|
||||
import React from 'react';
|
||||
import { ViewStyle, TextStyle } from 'react-native';
|
||||
import { Feather, Ionicons, MaterialCommunityIcons, FontAwesome5 } from '@expo/vector-icons';
|
||||
|
||||
interface IconProps {
|
||||
name: string;
|
||||
size?: number;
|
||||
color?: string;
|
||||
style?: ViewStyle | TextStyle;
|
||||
}
|
||||
|
||||
// Icon component with unified interface
|
||||
export const Icon: React.FC<IconProps> = ({
|
||||
name,
|
||||
size = 24,
|
||||
color = '#000',
|
||||
style
|
||||
}) => {
|
||||
// Map custom names to actual icon library icons
|
||||
switch (name) {
|
||||
// Tab icons
|
||||
case 'flow':
|
||||
return <Ionicons name="water-outline" size={size} color={color} style={style} />;
|
||||
case 'vault':
|
||||
return <MaterialCommunityIcons name="safe-square-outline" size={size} color={color} style={style} />;
|
||||
case 'sentinel':
|
||||
return <Ionicons name="shield-checkmark-outline" size={size} color={color} style={style} />;
|
||||
case 'heritage':
|
||||
return <MaterialCommunityIcons name="file-document-outline" size={size} color={color} style={style} />;
|
||||
case 'me':
|
||||
return <Feather name="user" size={size} color={color} style={style} />;
|
||||
|
||||
// Asset type icons
|
||||
case 'game':
|
||||
return <Ionicons name="game-controller-outline" size={size} color={color} style={style} />;
|
||||
case 'key':
|
||||
return <Feather name="key" size={size} color={color} style={style} />;
|
||||
case 'document':
|
||||
return <Ionicons name="document-text-outline" size={size} color={color} style={style} />;
|
||||
case 'photo':
|
||||
return <Ionicons name="image-outline" size={size} color={color} style={style} />;
|
||||
case 'will':
|
||||
return <MaterialCommunityIcons name="script-text-outline" size={size} color={color} style={style} />;
|
||||
case 'custom':
|
||||
return <Feather name="package" size={size} color={color} style={style} />;
|
||||
|
||||
// Action icons
|
||||
case 'add':
|
||||
return <Feather name="plus" size={size} color={color} style={style} />;
|
||||
case 'check':
|
||||
return <Feather name="check" size={size} color={color} style={style} />;
|
||||
case 'lock':
|
||||
return <Feather name="lock" size={size} color={color} style={style} />;
|
||||
case 'unlock':
|
||||
return <Feather name="unlock" size={size} color={color} style={style} />;
|
||||
case 'shield':
|
||||
return <Ionicons name="shield-checkmark" size={size} color={color} style={style} />;
|
||||
case 'fingerprint':
|
||||
return <Ionicons name="finger-print" size={size} color={color} style={style} />;
|
||||
case 'encrypted':
|
||||
return <MaterialCommunityIcons name="lock-check" size={size} color={color} style={style} />;
|
||||
|
||||
// Input type icons
|
||||
case 'text':
|
||||
return <MaterialCommunityIcons name="format-text" size={size} color={color} style={style} />;
|
||||
case 'voice':
|
||||
return <Ionicons name="mic-outline" size={size} color={color} style={style} />;
|
||||
case 'image':
|
||||
return <Ionicons name="camera-outline" size={size} color={color} style={style} />;
|
||||
|
||||
// Status icons
|
||||
case 'statusnormal':
|
||||
return <Ionicons name="checkmark-circle" size={size} color={color} style={style} />;
|
||||
case 'statuswarning':
|
||||
return <Ionicons name="warning" size={size} color={color} style={style} />;
|
||||
case 'statusreleasing':
|
||||
return <Ionicons name="alert-circle" size={size} color={color} style={style} />;
|
||||
|
||||
// Navigation icons
|
||||
case 'arrowRight':
|
||||
return <Feather name="arrow-right" size={size} color={color} style={style} />;
|
||||
case 'arrowUp':
|
||||
return <Feather name="arrow-up-right" size={size} color={color} style={style} />;
|
||||
case 'chevronRight':
|
||||
return <Feather name="chevron-right" size={size} color={color} style={style} />;
|
||||
case 'close':
|
||||
return <Feather name="x" size={size} color={color} style={style} />;
|
||||
|
||||
// Other icons
|
||||
case 'heartbeat':
|
||||
return <MaterialCommunityIcons name="heart-pulse" size={size} color={color} style={style} />;
|
||||
case 'warningSign':
|
||||
return <Ionicons name="warning-outline" size={size} color={color} style={style} />;
|
||||
case 'info':
|
||||
return <Feather name="info" size={size} color={color} style={style} />;
|
||||
case 'settings':
|
||||
return <Feather name="settings" size={size} color={color} style={style} />;
|
||||
case 'clock':
|
||||
return <Feather name="clock" size={size} color={color} style={style} />;
|
||||
case 'calendar':
|
||||
return <Feather name="calendar" size={size} color={color} style={style} />;
|
||||
case 'link':
|
||||
return <Feather name="external-link" size={size} color={color} style={style} />;
|
||||
case 'github':
|
||||
return <Feather name="github" size={size} color={color} style={style} />;
|
||||
case 'mail':
|
||||
return <Feather name="mail" size={size} color={color} style={style} />;
|
||||
case 'privacy':
|
||||
return <Feather name="eye-off" size={size} color={color} style={style} />;
|
||||
case 'terms':
|
||||
return <MaterialCommunityIcons name="file-sign" size={size} color={color} style={style} />;
|
||||
case 'wave':
|
||||
return <MaterialCommunityIcons name="wave" size={size} color={color} style={style} />;
|
||||
case 'palm':
|
||||
return <FontAwesome5 name="tree" size={size} color={color} style={style} />;
|
||||
case 'sun':
|
||||
return <Feather name="sun" size={size} color={color} style={style} />;
|
||||
case 'star':
|
||||
return <Feather name="star" size={size} color={color} style={style} />;
|
||||
case 'heart':
|
||||
return <Feather name="heart" size={size} color={color} style={style} />;
|
||||
case 'users':
|
||||
return <Feather name="users" size={size} color={color} style={style} />;
|
||||
case 'gift':
|
||||
return <Feather name="gift" size={size} color={color} style={style} />;
|
||||
case 'zap':
|
||||
return <Feather name="zap" size={size} color={color} style={style} />;
|
||||
|
||||
default:
|
||||
return <Feather name="circle" size={size} color={color} style={style} />;
|
||||
}
|
||||
};
|
||||
|
||||
// Predefined icon components for common use cases
|
||||
export const GameIcon = (props: Omit<IconProps, 'name'>) => (
|
||||
<Icon name="game" {...props} />
|
||||
);
|
||||
|
||||
export const KeyIcon = (props: Omit<IconProps, 'name'>) => (
|
||||
<Icon name="key" {...props} />
|
||||
);
|
||||
|
||||
export const DocumentIcon = (props: Omit<IconProps, 'name'>) => (
|
||||
<Icon name="document" {...props} />
|
||||
);
|
||||
|
||||
export const PhotoIcon = (props: Omit<IconProps, 'name'>) => (
|
||||
<Icon name="photo" {...props} />
|
||||
);
|
||||
|
||||
export const WillIcon = (props: Omit<IconProps, 'name'>) => (
|
||||
<Icon name="will" {...props} />
|
||||
);
|
||||
|
||||
export const CustomIcon = (props: Omit<IconProps, 'name'>) => (
|
||||
<Icon name="custom" {...props} />
|
||||
);
|
||||
|
||||
export const CheckIcon = (props: Omit<IconProps, 'name'>) => (
|
||||
<Icon name="check" {...props} />
|
||||
);
|
||||
|
||||
export const LockIcon = (props: Omit<IconProps, 'name'>) => (
|
||||
<Icon name="lock" {...props} />
|
||||
);
|
||||
|
||||
export const ShieldIcon = (props: Omit<IconProps, 'name'>) => (
|
||||
<Icon name="shield" {...props} />
|
||||
);
|
||||
250
src/components/common/VaultDoorAnimation.tsx
Normal file
250
src/components/common/VaultDoorAnimation.tsx
Normal file
@@ -0,0 +1,250 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { View, Text, Animated, StyleSheet, Modal } from 'react-native';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import { MaterialCommunityIcons, Ionicons, FontAwesome5 } from '@expo/vector-icons';
|
||||
import { colors, typography, spacing, borderRadius } from '../../theme/colors';
|
||||
|
||||
interface VaultDoorAnimationProps {
|
||||
visible: boolean;
|
||||
onComplete: () => void;
|
||||
}
|
||||
|
||||
export default function VaultDoorAnimation({
|
||||
visible,
|
||||
onComplete,
|
||||
}: VaultDoorAnimationProps) {
|
||||
const doorLeft = useRef(new Animated.Value(0)).current;
|
||||
const doorRight = useRef(new Animated.Value(0)).current;
|
||||
const lockRotation = useRef(new Animated.Value(0)).current;
|
||||
const lockScale = useRef(new Animated.Value(1)).current;
|
||||
const fadeOut = useRef(new Animated.Value(1)).current;
|
||||
const checkScale = useRef(new Animated.Value(0)).current;
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
// Reset animations
|
||||
doorLeft.setValue(0);
|
||||
doorRight.setValue(0);
|
||||
lockRotation.setValue(0);
|
||||
lockScale.setValue(1);
|
||||
fadeOut.setValue(1);
|
||||
checkScale.setValue(0);
|
||||
|
||||
// Play closing animation sequence
|
||||
Animated.sequence([
|
||||
// Doors slide in
|
||||
Animated.parallel([
|
||||
Animated.timing(doorLeft, {
|
||||
toValue: 1,
|
||||
duration: 600,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(doorRight, {
|
||||
toValue: 1,
|
||||
duration: 600,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
]),
|
||||
// Lock rotates and pulses
|
||||
Animated.parallel([
|
||||
Animated.timing(lockRotation, {
|
||||
toValue: 1,
|
||||
duration: 400,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.sequence([
|
||||
Animated.timing(lockScale, {
|
||||
toValue: 1.2,
|
||||
duration: 200,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(lockScale, {
|
||||
toValue: 1,
|
||||
duration: 200,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
]),
|
||||
]),
|
||||
// Show checkmark
|
||||
Animated.spring(checkScale, {
|
||||
toValue: 1,
|
||||
friction: 5,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
// Pause
|
||||
Animated.delay(500),
|
||||
// Fade out
|
||||
Animated.timing(fadeOut, {
|
||||
toValue: 0,
|
||||
duration: 300,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
]).start(() => {
|
||||
onComplete();
|
||||
});
|
||||
}
|
||||
}, [visible]);
|
||||
|
||||
if (!visible) return null;
|
||||
|
||||
return (
|
||||
<Modal transparent visible={visible} animationType="fade">
|
||||
<Animated.View style={[styles.container, { opacity: fadeOut }]}>
|
||||
<View style={styles.doorContainer}>
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.door,
|
||||
styles.doorLeft,
|
||||
{
|
||||
transform: [
|
||||
{
|
||||
translateX: doorLeft.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [-200, 0],
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
>
|
||||
<LinearGradient
|
||||
colors={[colors.nautical.teal, colors.nautical.deepTeal]}
|
||||
style={styles.doorGradient}
|
||||
>
|
||||
<FontAwesome5 name="anchor" size={32} color={colors.vault.primary} style={{ opacity: 0.3 }} />
|
||||
</LinearGradient>
|
||||
</Animated.View>
|
||||
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.door,
|
||||
styles.doorRight,
|
||||
{
|
||||
transform: [
|
||||
{
|
||||
translateX: doorRight.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [200, 0],
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
>
|
||||
<LinearGradient
|
||||
colors={[colors.nautical.deepTeal, colors.nautical.teal]}
|
||||
style={styles.doorGradient}
|
||||
>
|
||||
<FontAwesome5 name="anchor" size={32} color={colors.vault.primary} style={{ opacity: 0.3 }} />
|
||||
</LinearGradient>
|
||||
</Animated.View>
|
||||
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.lockContainer,
|
||||
{
|
||||
transform: [
|
||||
{
|
||||
rotate: lockRotation.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: ['0deg', '90deg'],
|
||||
}),
|
||||
},
|
||||
{ scale: lockScale },
|
||||
],
|
||||
},
|
||||
]}
|
||||
>
|
||||
<LinearGradient
|
||||
colors={[colors.vault.primary, colors.vault.secondary]}
|
||||
style={styles.lockGradient}
|
||||
>
|
||||
<MaterialCommunityIcons name="lock" size={28} color={colors.vault.background} />
|
||||
</LinearGradient>
|
||||
</Animated.View>
|
||||
</View>
|
||||
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.checkContainer,
|
||||
{
|
||||
transform: [{ scale: checkScale }],
|
||||
opacity: checkScale,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Ionicons name="checkmark-circle" size={52} color={colors.vault.success} />
|
||||
</Animated.View>
|
||||
|
||||
<Text style={styles.statusText}>Treasure Sealed</Text>
|
||||
<Text style={styles.subText}>Your legacy is now protected</Text>
|
||||
</Animated.View>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: 'rgba(26, 58, 74, 0.98)',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
doorContainer: {
|
||||
width: 200,
|
||||
height: 200,
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
borderRadius: borderRadius.xl,
|
||||
marginBottom: spacing.lg,
|
||||
},
|
||||
door: {
|
||||
position: 'absolute',
|
||||
width: 100,
|
||||
height: 200,
|
||||
},
|
||||
doorLeft: {
|
||||
left: 0,
|
||||
},
|
||||
doorRight: {
|
||||
right: 0,
|
||||
},
|
||||
doorGradient: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
borderWidth: 2,
|
||||
borderColor: colors.vault.primary,
|
||||
},
|
||||
lockContainer: {
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
marginTop: -32,
|
||||
marginLeft: -32,
|
||||
width: 64,
|
||||
height: 64,
|
||||
zIndex: 10,
|
||||
},
|
||||
lockGradient: {
|
||||
flex: 1,
|
||||
borderRadius: 32,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
checkContainer: {
|
||||
marginBottom: spacing.md,
|
||||
},
|
||||
statusText: {
|
||||
fontSize: typography.fontSize.xl,
|
||||
color: colors.vault.primary,
|
||||
fontWeight: '600',
|
||||
marginBottom: spacing.xs,
|
||||
fontFamily: typography.fontFamily.serif,
|
||||
},
|
||||
subText: {
|
||||
fontSize: typography.fontSize.base,
|
||||
color: colors.vault.textSecondary,
|
||||
fontStyle: 'italic',
|
||||
},
|
||||
});
|
||||
198
src/navigation/TabNavigator.tsx
Normal file
198
src/navigation/TabNavigator.tsx
Normal file
@@ -0,0 +1,198 @@
|
||||
import React from 'react';
|
||||
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
|
||||
import { View, Text, StyleSheet } from 'react-native';
|
||||
import { Feather, MaterialCommunityIcons, FontAwesome5 } from '@expo/vector-icons';
|
||||
import { colors, borderRadius, typography } from '../theme/colors';
|
||||
|
||||
// Screens
|
||||
import FlowScreen from '../screens/FlowScreen';
|
||||
import VaultScreen from '../screens/VaultScreen';
|
||||
import SentinelScreen from '../screens/SentinelScreen';
|
||||
import HeritageScreen from '../screens/HeritageScreen';
|
||||
import MeScreen from '../screens/MeScreen';
|
||||
|
||||
const Tab = createBottomTabNavigator();
|
||||
|
||||
// Custom Tab Bar Button with icon on top and label below
|
||||
const TabBarItem = ({
|
||||
name,
|
||||
label,
|
||||
focused,
|
||||
color,
|
||||
isDark = false
|
||||
}: {
|
||||
name: string;
|
||||
label: string;
|
||||
focused: boolean;
|
||||
color: string;
|
||||
isDark?: boolean;
|
||||
}) => {
|
||||
const iconSize = 22;
|
||||
|
||||
const renderIcon = () => {
|
||||
switch (name) {
|
||||
case 'Flow':
|
||||
return <FontAwesome5 name="scroll" size={iconSize - 2} color={color} />;
|
||||
case 'Vault':
|
||||
return <MaterialCommunityIcons name="treasure-chest" size={iconSize} color={color} />;
|
||||
case 'Sentinel':
|
||||
return <FontAwesome5 name="anchor" size={iconSize - 2} color={color} />;
|
||||
case 'Heritage':
|
||||
return <MaterialCommunityIcons name="compass-outline" size={iconSize} color={color} />;
|
||||
case 'Me':
|
||||
return <MaterialCommunityIcons name="ship-wheel" size={iconSize} color={color} />;
|
||||
default:
|
||||
return <Feather name="circle" size={iconSize} color={color} />;
|
||||
}
|
||||
};
|
||||
|
||||
const bgColor = focused
|
||||
? (isDark ? 'rgba(184, 224, 229, 0.12)' : colors.nautical.lightMint)
|
||||
: 'transparent';
|
||||
|
||||
return (
|
||||
<View style={[styles.tabItem, { backgroundColor: bgColor }]}>
|
||||
<View style={styles.iconWrapper}>
|
||||
{renderIcon()}
|
||||
</View>
|
||||
<Text style={[
|
||||
styles.tabLabel,
|
||||
{ color },
|
||||
focused && styles.tabLabelActive
|
||||
]}>
|
||||
{label}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default function TabNavigator() {
|
||||
return (
|
||||
<Tab.Navigator
|
||||
screenOptions={{
|
||||
headerShown: false,
|
||||
tabBarShowLabel: false,
|
||||
tabBarStyle: styles.tabBar,
|
||||
}}
|
||||
>
|
||||
<Tab.Screen
|
||||
name="Flow"
|
||||
component={FlowScreen}
|
||||
options={{
|
||||
tabBarIcon: ({ focused }) => (
|
||||
<TabBarItem
|
||||
name="Flow"
|
||||
label="Flow"
|
||||
focused={focused}
|
||||
color={focused ? colors.flow.primary : colors.nautical.sage}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name="Vault"
|
||||
component={VaultScreen}
|
||||
options={{
|
||||
tabBarIcon: ({ focused }) => (
|
||||
<TabBarItem
|
||||
name="Vault"
|
||||
label="Vault"
|
||||
focused={focused}
|
||||
color={focused ? colors.vault.primary : colors.vault.textSecondary}
|
||||
isDark
|
||||
/>
|
||||
),
|
||||
tabBarStyle: styles.tabBarDark,
|
||||
}}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name="Sentinel"
|
||||
component={SentinelScreen}
|
||||
options={{
|
||||
tabBarIcon: ({ focused }) => (
|
||||
<TabBarItem
|
||||
name="Sentinel"
|
||||
label="Sentinel"
|
||||
focused={focused}
|
||||
color={focused ? colors.sentinel.primary : colors.sentinel.textSecondary}
|
||||
isDark
|
||||
/>
|
||||
),
|
||||
tabBarStyle: styles.tabBarDark,
|
||||
}}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name="Heritage"
|
||||
component={HeritageScreen}
|
||||
options={{
|
||||
tabBarIcon: ({ focused }) => (
|
||||
<TabBarItem
|
||||
name="Heritage"
|
||||
label="Heritage"
|
||||
focused={focused}
|
||||
color={focused ? colors.heritage.primary : colors.nautical.sage}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name="Me"
|
||||
component={MeScreen}
|
||||
options={{
|
||||
tabBarIcon: ({ focused }) => (
|
||||
<TabBarItem
|
||||
name="Me"
|
||||
label="Me"
|
||||
focused={focused}
|
||||
color={focused ? colors.me.primary : colors.nautical.sage}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Tab.Navigator>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
tabBar: {
|
||||
backgroundColor: colors.nautical.cream,
|
||||
borderTopWidth: 0,
|
||||
height: 80,
|
||||
paddingHorizontal: 4,
|
||||
shadowColor: colors.nautical.navy,
|
||||
shadowOffset: { width: 0, height: -6 },
|
||||
shadowOpacity: 0.06,
|
||||
shadowRadius: 16,
|
||||
elevation: 12,
|
||||
},
|
||||
tabBarDark: {
|
||||
backgroundColor: colors.nautical.navy,
|
||||
borderTopWidth: 0,
|
||||
height: 80,
|
||||
paddingHorizontal: 4,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: -4 },
|
||||
shadowOpacity: 0.15,
|
||||
shadowRadius: 12,
|
||||
elevation: 12,
|
||||
},
|
||||
tabItem: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingVertical: 8,
|
||||
paddingHorizontal: 16,
|
||||
borderRadius: borderRadius.lg,
|
||||
minWidth: 64,
|
||||
},
|
||||
iconWrapper: {
|
||||
marginBottom: 4,
|
||||
},
|
||||
tabLabel: {
|
||||
fontSize: 10,
|
||||
fontWeight: '500',
|
||||
letterSpacing: 0.3,
|
||||
},
|
||||
tabLabelActive: {
|
||||
fontWeight: '700',
|
||||
},
|
||||
});
|
||||
625
src/screens/FlowScreen.tsx
Normal file
625
src/screens/FlowScreen.tsx
Normal file
@@ -0,0 +1,625 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
ScrollView,
|
||||
TouchableOpacity,
|
||||
Modal,
|
||||
TextInput,
|
||||
SafeAreaView,
|
||||
} from 'react-native';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import { Ionicons, Feather, MaterialCommunityIcons, FontAwesome5 } from '@expo/vector-icons';
|
||||
import { colors, typography, spacing, borderRadius, shadows } from '../theme/colors';
|
||||
import { FlowRecord } from '../types';
|
||||
import BiometricModal from '../components/common/BiometricModal';
|
||||
import VaultDoorAnimation from '../components/common/VaultDoorAnimation';
|
||||
|
||||
// Mock data
|
||||
const initialRecords: FlowRecord[] = [
|
||||
{
|
||||
id: '1',
|
||||
type: 'text',
|
||||
content: 'Beautiful sunny day today. Went to the park with family. Felt the simple joy of life.',
|
||||
createdAt: new Date('2024-01-18T10:30:00'),
|
||||
emotion: 'Calm',
|
||||
isArchived: false,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
type: 'text',
|
||||
content: 'Completed an important project. The process was challenging, but the result is satisfying.',
|
||||
createdAt: new Date('2024-01-17T16:45:00'),
|
||||
emotion: 'Content',
|
||||
isArchived: false,
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
type: 'text',
|
||||
content: 'Archived a reflection',
|
||||
createdAt: new Date('2024-01-15T09:20:00'),
|
||||
isArchived: true,
|
||||
archivedAt: new Date('2024-01-16T14:00:00'),
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
type: 'voice',
|
||||
content: '[Voice recording 2:30]',
|
||||
createdAt: new Date('2024-01-14T20:15:00'),
|
||||
emotion: 'Grateful',
|
||||
isArchived: false,
|
||||
},
|
||||
];
|
||||
|
||||
// Emotion config
|
||||
const emotionConfig: Record<string, { color: string; icon: string }> = {
|
||||
'Calm': { color: colors.nautical.seafoam, icon: 'water' },
|
||||
'Content': { color: colors.nautical.teal, icon: 'checkmark-circle' },
|
||||
'Grateful': { color: colors.nautical.gold, icon: 'star' },
|
||||
'Nostalgic': { color: colors.nautical.sage, icon: 'time' },
|
||||
'Hopeful': { color: colors.nautical.mint, icon: 'sunny' },
|
||||
};
|
||||
|
||||
export default function FlowScreen() {
|
||||
const [records, setRecords] = useState<FlowRecord[]>(initialRecords);
|
||||
const [showAddModal, setShowAddModal] = useState(false);
|
||||
const [newContent, setNewContent] = useState('');
|
||||
const [selectedRecord, setSelectedRecord] = useState<FlowRecord | null>(null);
|
||||
const [showArchiveWarning, setShowArchiveWarning] = useState(false);
|
||||
const [showBiometric, setShowBiometric] = useState(false);
|
||||
const [showVaultAnimation, setShowVaultAnimation] = useState(false);
|
||||
const [selectedInputType, setSelectedInputType] = useState<'text' | 'voice' | 'image'>('text');
|
||||
|
||||
const today = new Date();
|
||||
const dateStr = today.toLocaleDateString('en-US', {
|
||||
weekday: 'long',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
});
|
||||
|
||||
const handleAddRecord = () => {
|
||||
if (!newContent.trim()) return;
|
||||
const newRecord: FlowRecord = {
|
||||
id: Date.now().toString(),
|
||||
type: selectedInputType,
|
||||
content: newContent,
|
||||
createdAt: new Date(),
|
||||
emotion: 'Calm',
|
||||
isArchived: false,
|
||||
};
|
||||
setRecords([newRecord, ...records]);
|
||||
setNewContent('');
|
||||
setShowAddModal(false);
|
||||
};
|
||||
|
||||
const handleSendToVault = (record: FlowRecord) => {
|
||||
setSelectedRecord(record);
|
||||
setShowArchiveWarning(true);
|
||||
};
|
||||
|
||||
const handleConfirmArchive = () => {
|
||||
setShowArchiveWarning(false);
|
||||
setShowBiometric(true);
|
||||
};
|
||||
|
||||
const handleBiometricSuccess = () => {
|
||||
setShowBiometric(false);
|
||||
setShowVaultAnimation(true);
|
||||
};
|
||||
|
||||
const handleVaultAnimationComplete = () => {
|
||||
setShowVaultAnimation(false);
|
||||
if (selectedRecord) {
|
||||
setRecords(
|
||||
records.map((r) =>
|
||||
r.id === selectedRecord.id
|
||||
? { ...r, content: 'Archived a reflection', isArchived: true, archivedAt: new Date() }
|
||||
: r
|
||||
)
|
||||
);
|
||||
setSelectedRecord(null);
|
||||
}
|
||||
};
|
||||
|
||||
const formatTime = (date: Date) => {
|
||||
return date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' });
|
||||
};
|
||||
|
||||
const formatDate = (date: Date) => {
|
||||
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<LinearGradient
|
||||
colors={[colors.flow.backgroundGradientStart, colors.flow.backgroundGradientEnd]}
|
||||
style={styles.gradient}
|
||||
>
|
||||
<SafeAreaView style={styles.safeArea}>
|
||||
{/* Header */}
|
||||
<View style={styles.header}>
|
||||
<View style={styles.headerLeft}>
|
||||
<View style={styles.iconCircle}>
|
||||
<FontAwesome5 name="scroll" size={18} color={colors.flow.primary} />
|
||||
</View>
|
||||
<View>
|
||||
<Text style={styles.headerTitle}>Journal</Text>
|
||||
<Text style={styles.headerDate}>{dateStr}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.moodBadge}>
|
||||
<MaterialCommunityIcons name="weather-sunny" size={14} color={colors.nautical.gold} />
|
||||
<Text style={styles.moodText}>Serene</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Timeline */}
|
||||
<ScrollView
|
||||
style={styles.timeline}
|
||||
showsVerticalScrollIndicator={false}
|
||||
contentContainerStyle={styles.timelineContent}
|
||||
>
|
||||
{records.map((record, index) => {
|
||||
const emotionInfo = record.emotion ? emotionConfig[record.emotion] : null;
|
||||
const showDateHeader = index === 0 ||
|
||||
formatDate(record.createdAt) !== formatDate(records[index - 1].createdAt);
|
||||
|
||||
return (
|
||||
<View key={record.id}>
|
||||
{showDateHeader && (
|
||||
<View style={styles.dateHeader}>
|
||||
<View style={styles.dateLine} />
|
||||
<Text style={styles.dateHeaderText}>{formatDate(record.createdAt)}</Text>
|
||||
<View style={styles.dateLine} />
|
||||
</View>
|
||||
)}
|
||||
<View style={[styles.card, record.isArchived && styles.archivedCard]}>
|
||||
<View style={styles.cardHeader}>
|
||||
<Text style={[styles.cardTime, record.isArchived && styles.archivedText]}>
|
||||
{formatTime(record.createdAt)}
|
||||
</Text>
|
||||
<View style={styles.cardBadges}>
|
||||
{emotionInfo && !record.isArchived && (
|
||||
<View style={[styles.emotionBadge, { backgroundColor: `${emotionInfo.color}15` }]}>
|
||||
<Ionicons name={emotionInfo.icon as any} size={11} color={emotionInfo.color} />
|
||||
<Text style={[styles.emotionText, { color: emotionInfo.color }]}>{record.emotion}</Text>
|
||||
</View>
|
||||
)}
|
||||
{record.type === 'voice' && !record.isArchived && (
|
||||
<Ionicons name="mic" size={14} color={colors.flow.secondary} />
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Text style={[styles.cardContent, record.isArchived && styles.archivedText]}>
|
||||
{record.isArchived
|
||||
? `📦 ${record.archivedAt?.toLocaleDateString('en-US')} — ${record.content}`
|
||||
: record.content
|
||||
}
|
||||
</Text>
|
||||
|
||||
{!record.isArchived && (
|
||||
<TouchableOpacity
|
||||
style={styles.vaultButton}
|
||||
onPress={() => handleSendToVault(record)}
|
||||
>
|
||||
<MaterialCommunityIcons name="treasure-chest" size={14} color={colors.flow.primary} />
|
||||
<Text style={styles.vaultButtonText}>Seal in Vault</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
<View style={{ height: 100 }} />
|
||||
</ScrollView>
|
||||
|
||||
{/* FAB */}
|
||||
<TouchableOpacity
|
||||
style={styles.fab}
|
||||
onPress={() => setShowAddModal(true)}
|
||||
activeOpacity={0.9}
|
||||
>
|
||||
<LinearGradient
|
||||
colors={[colors.nautical.teal, colors.nautical.seafoam]}
|
||||
style={styles.fabGradient}
|
||||
>
|
||||
<Feather name="feather" size={24} color="#fff" />
|
||||
</LinearGradient>
|
||||
</TouchableOpacity>
|
||||
</SafeAreaView>
|
||||
</LinearGradient>
|
||||
|
||||
{/* Add Modal */}
|
||||
<Modal visible={showAddModal} animationType="slide" transparent onRequestClose={() => setShowAddModal(false)}>
|
||||
<View style={styles.modalOverlay}>
|
||||
<View style={styles.modalContent}>
|
||||
<View style={styles.modalHandle} />
|
||||
<Text style={styles.modalTitle}>New Entry</Text>
|
||||
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="What's on your mind, Captain?"
|
||||
placeholderTextColor={colors.flow.textSecondary}
|
||||
value={newContent}
|
||||
onChangeText={setNewContent}
|
||||
multiline
|
||||
textAlignVertical="top"
|
||||
/>
|
||||
|
||||
<View style={styles.inputTypeRow}>
|
||||
{[
|
||||
{ type: 'text', icon: 'type', label: 'Text' },
|
||||
{ type: 'voice', icon: 'mic', label: 'Voice' },
|
||||
{ type: 'image', icon: 'image', label: 'Photo' },
|
||||
].map((item) => (
|
||||
<TouchableOpacity
|
||||
key={item.type}
|
||||
style={[styles.inputTypeButton, selectedInputType === item.type && styles.inputTypeActive]}
|
||||
onPress={() => setSelectedInputType(item.type as any)}
|
||||
>
|
||||
<Feather name={item.icon as any} size={20} color={selectedInputType === item.type ? colors.flow.primary : colors.flow.textSecondary} />
|
||||
<Text style={[styles.inputTypeLabel, selectedInputType === item.type && styles.inputTypeLabelActive]}>{item.label}</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
|
||||
<View style={styles.modalButtons}>
|
||||
<TouchableOpacity style={styles.cancelButton} onPress={() => { setShowAddModal(false); setNewContent(''); }}>
|
||||
<Text style={styles.cancelButtonText}>Cancel</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={styles.confirmButton} onPress={handleAddRecord}>
|
||||
<LinearGradient colors={[colors.nautical.teal, colors.nautical.seafoam]} style={styles.confirmButtonGradient}>
|
||||
<Feather name="check" size={18} color="#fff" />
|
||||
<Text style={styles.confirmButtonText}>Save</Text>
|
||||
</LinearGradient>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
|
||||
{/* Archive Warning */}
|
||||
<Modal visible={showArchiveWarning} animationType="fade" transparent onRequestClose={() => setShowArchiveWarning(false)}>
|
||||
<View style={styles.warningOverlay}>
|
||||
<View style={styles.warningModal}>
|
||||
<View style={styles.warningIcon}>
|
||||
<MaterialCommunityIcons name="treasure-chest" size={40} color={colors.nautical.teal} />
|
||||
</View>
|
||||
<Text style={styles.warningTitle}>Seal Entry?</Text>
|
||||
<Text style={styles.warningText}>
|
||||
This entry will be encrypted and stored in the vault. The original will be replaced with a sealed record.
|
||||
</Text>
|
||||
<View style={styles.warningButtons}>
|
||||
<TouchableOpacity style={styles.warningCancelButton} onPress={() => setShowArchiveWarning(false)}>
|
||||
<Text style={styles.warningCancelText}>Cancel</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={styles.warningConfirmButton} onPress={handleConfirmArchive}>
|
||||
<MaterialCommunityIcons name="lock" size={16} color="#fff" />
|
||||
<Text style={styles.warningConfirmText}>Seal</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
|
||||
<BiometricModal
|
||||
visible={showBiometric}
|
||||
onSuccess={handleBiometricSuccess}
|
||||
onCancel={() => { setShowBiometric(false); setSelectedRecord(null); }}
|
||||
title="Verify Identity"
|
||||
message="Authenticate to seal this entry"
|
||||
/>
|
||||
|
||||
<VaultDoorAnimation visible={showVaultAnimation} onComplete={handleVaultAnimationComplete} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: { flex: 1 },
|
||||
gradient: { flex: 1 },
|
||||
safeArea: { flex: 1 },
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: spacing.base,
|
||||
paddingTop: spacing.sm,
|
||||
paddingBottom: spacing.md,
|
||||
},
|
||||
headerLeft: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: spacing.sm,
|
||||
},
|
||||
iconCircle: {
|
||||
width: 44,
|
||||
height: 44,
|
||||
borderRadius: 14,
|
||||
backgroundColor: colors.flow.cardBackground,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
...shadows.soft,
|
||||
},
|
||||
headerTitle: {
|
||||
fontSize: typography.fontSize.xl,
|
||||
fontWeight: '700',
|
||||
color: colors.flow.text,
|
||||
},
|
||||
headerDate: {
|
||||
fontSize: typography.fontSize.sm,
|
||||
color: colors.flow.textSecondary,
|
||||
},
|
||||
moodBadge: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
backgroundColor: `${colors.nautical.gold}15`,
|
||||
paddingHorizontal: spacing.sm,
|
||||
paddingVertical: 6,
|
||||
borderRadius: borderRadius.full,
|
||||
},
|
||||
moodText: {
|
||||
fontSize: typography.fontSize.sm,
|
||||
fontWeight: '600',
|
||||
color: colors.nautical.gold,
|
||||
},
|
||||
timeline: { flex: 1 },
|
||||
timelineContent: { padding: spacing.base, paddingTop: 0 },
|
||||
dateHeader: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginVertical: spacing.md,
|
||||
},
|
||||
dateLine: {
|
||||
flex: 1,
|
||||
height: 1,
|
||||
backgroundColor: colors.flow.cardBorder,
|
||||
},
|
||||
dateHeaderText: {
|
||||
fontSize: typography.fontSize.xs,
|
||||
fontWeight: '600',
|
||||
color: colors.flow.textSecondary,
|
||||
paddingHorizontal: spacing.md,
|
||||
},
|
||||
card: {
|
||||
backgroundColor: colors.flow.cardBackground,
|
||||
borderRadius: borderRadius.xl,
|
||||
padding: spacing.base,
|
||||
marginBottom: spacing.sm,
|
||||
...shadows.soft,
|
||||
},
|
||||
archivedCard: {
|
||||
backgroundColor: colors.flow.archived,
|
||||
shadowOpacity: 0,
|
||||
},
|
||||
cardHeader: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: spacing.sm,
|
||||
},
|
||||
cardTime: {
|
||||
fontSize: typography.fontSize.xs,
|
||||
color: colors.flow.textSecondary,
|
||||
fontWeight: '500',
|
||||
},
|
||||
archivedText: { color: colors.flow.archivedText },
|
||||
cardBadges: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: spacing.sm,
|
||||
},
|
||||
emotionBadge: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 3,
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 3,
|
||||
borderRadius: borderRadius.full,
|
||||
},
|
||||
emotionText: {
|
||||
fontSize: 10,
|
||||
fontWeight: '600',
|
||||
},
|
||||
cardContent: {
|
||||
fontSize: typography.fontSize.base,
|
||||
color: colors.flow.text,
|
||||
lineHeight: typography.fontSize.base * 1.6,
|
||||
},
|
||||
vaultButton: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-end',
|
||||
gap: 4,
|
||||
marginTop: spacing.md,
|
||||
paddingTop: spacing.sm,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: colors.flow.cardBorder,
|
||||
},
|
||||
vaultButtonText: {
|
||||
fontSize: typography.fontSize.sm,
|
||||
color: colors.flow.primary,
|
||||
fontWeight: '600',
|
||||
},
|
||||
fab: {
|
||||
position: 'absolute',
|
||||
bottom: 100,
|
||||
right: spacing.base,
|
||||
...shadows.medium,
|
||||
},
|
||||
fabGradient: {
|
||||
width: 56,
|
||||
height: 56,
|
||||
borderRadius: 18,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
modalOverlay: {
|
||||
flex: 1,
|
||||
backgroundColor: 'rgba(26, 58, 74, 0.4)',
|
||||
justifyContent: 'flex-end',
|
||||
},
|
||||
modalContent: {
|
||||
backgroundColor: colors.flow.cardBackground,
|
||||
borderTopLeftRadius: borderRadius.xxl,
|
||||
borderTopRightRadius: borderRadius.xxl,
|
||||
padding: spacing.lg,
|
||||
paddingBottom: spacing.xxl,
|
||||
},
|
||||
modalHandle: {
|
||||
width: 36,
|
||||
height: 4,
|
||||
backgroundColor: colors.flow.cardBorder,
|
||||
borderRadius: 2,
|
||||
alignSelf: 'center',
|
||||
marginBottom: spacing.md,
|
||||
},
|
||||
modalTitle: {
|
||||
fontSize: typography.fontSize.lg,
|
||||
fontWeight: '700',
|
||||
color: colors.flow.text,
|
||||
marginBottom: spacing.md,
|
||||
},
|
||||
input: {
|
||||
backgroundColor: colors.nautical.paleAqua,
|
||||
borderRadius: borderRadius.lg,
|
||||
padding: spacing.base,
|
||||
fontSize: typography.fontSize.base,
|
||||
color: colors.flow.text,
|
||||
minHeight: 100,
|
||||
marginBottom: spacing.md,
|
||||
},
|
||||
inputTypeRow: {
|
||||
flexDirection: 'row',
|
||||
gap: spacing.sm,
|
||||
marginBottom: spacing.lg,
|
||||
},
|
||||
inputTypeButton: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
paddingVertical: spacing.md,
|
||||
borderRadius: borderRadius.lg,
|
||||
backgroundColor: colors.nautical.paleAqua,
|
||||
borderWidth: 2,
|
||||
borderColor: 'transparent',
|
||||
},
|
||||
inputTypeActive: {
|
||||
borderColor: colors.flow.primary,
|
||||
backgroundColor: colors.nautical.lightMint,
|
||||
},
|
||||
inputTypeLabel: {
|
||||
fontSize: typography.fontSize.xs,
|
||||
color: colors.flow.textSecondary,
|
||||
marginTop: 4,
|
||||
fontWeight: '500',
|
||||
},
|
||||
inputTypeLabelActive: {
|
||||
color: colors.flow.primary,
|
||||
fontWeight: '600',
|
||||
},
|
||||
modalButtons: {
|
||||
flexDirection: 'row',
|
||||
gap: spacing.md,
|
||||
},
|
||||
cancelButton: {
|
||||
flex: 1,
|
||||
paddingVertical: spacing.md,
|
||||
borderRadius: borderRadius.lg,
|
||||
backgroundColor: colors.nautical.paleAqua,
|
||||
alignItems: 'center',
|
||||
},
|
||||
cancelButtonText: {
|
||||
fontSize: typography.fontSize.base,
|
||||
color: colors.flow.textSecondary,
|
||||
fontWeight: '600',
|
||||
},
|
||||
confirmButton: {
|
||||
flex: 1,
|
||||
borderRadius: borderRadius.lg,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
confirmButtonGradient: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingVertical: spacing.md,
|
||||
gap: spacing.sm,
|
||||
},
|
||||
confirmButtonText: {
|
||||
fontSize: typography.fontSize.base,
|
||||
color: '#fff',
|
||||
fontWeight: '600',
|
||||
},
|
||||
warningOverlay: {
|
||||
flex: 1,
|
||||
backgroundColor: 'rgba(26, 58, 74, 0.6)',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
padding: spacing.lg,
|
||||
},
|
||||
warningModal: {
|
||||
backgroundColor: colors.flow.cardBackground,
|
||||
borderRadius: borderRadius.xxl,
|
||||
padding: spacing.xl,
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
...shadows.medium,
|
||||
},
|
||||
warningIcon: {
|
||||
width: 72,
|
||||
height: 72,
|
||||
borderRadius: 24,
|
||||
backgroundColor: colors.nautical.lightMint,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginBottom: spacing.md,
|
||||
},
|
||||
warningTitle: {
|
||||
fontSize: typography.fontSize.lg,
|
||||
fontWeight: '700',
|
||||
color: colors.flow.text,
|
||||
marginBottom: spacing.sm,
|
||||
},
|
||||
warningText: {
|
||||
fontSize: typography.fontSize.base,
|
||||
color: colors.flow.textSecondary,
|
||||
textAlign: 'center',
|
||||
lineHeight: typography.fontSize.base * 1.5,
|
||||
marginBottom: spacing.lg,
|
||||
},
|
||||
warningButtons: {
|
||||
flexDirection: 'row',
|
||||
gap: spacing.md,
|
||||
width: '100%',
|
||||
},
|
||||
warningCancelButton: {
|
||||
flex: 1,
|
||||
paddingVertical: spacing.md,
|
||||
borderRadius: borderRadius.lg,
|
||||
backgroundColor: colors.nautical.paleAqua,
|
||||
alignItems: 'center',
|
||||
},
|
||||
warningCancelText: {
|
||||
fontSize: typography.fontSize.base,
|
||||
color: colors.flow.textSecondary,
|
||||
fontWeight: '600',
|
||||
},
|
||||
warningConfirmButton: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingVertical: spacing.md,
|
||||
borderRadius: borderRadius.lg,
|
||||
backgroundColor: colors.nautical.teal,
|
||||
gap: spacing.xs,
|
||||
},
|
||||
warningConfirmText: {
|
||||
fontSize: typography.fontSize.base,
|
||||
color: '#fff',
|
||||
fontWeight: '600',
|
||||
},
|
||||
});
|
||||
886
src/screens/HeritageScreen.tsx
Normal file
886
src/screens/HeritageScreen.tsx
Normal file
@@ -0,0 +1,886 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
ScrollView,
|
||||
TouchableOpacity,
|
||||
Modal,
|
||||
TextInput,
|
||||
SafeAreaView,
|
||||
} from 'react-native';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import { Ionicons, Feather, MaterialCommunityIcons, FontAwesome5 } from '@expo/vector-icons';
|
||||
import { colors, typography, spacing, borderRadius, shadows } from '../theme/colors';
|
||||
import { Heir, HeirStatus, PaymentStrategy } from '../types';
|
||||
|
||||
// Mock heirs data
|
||||
const initialHeirs: Heir[] = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'John Smith',
|
||||
email: 'john.smith@email.com',
|
||||
status: 'confirmed',
|
||||
releaseLevel: 3,
|
||||
releaseOrder: 1,
|
||||
paymentStrategy: 'prepaid',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Jane Doe',
|
||||
email: 'jane.doe@email.com',
|
||||
status: 'confirmed',
|
||||
releaseLevel: 2,
|
||||
releaseOrder: 2,
|
||||
paymentStrategy: 'pay_on_access',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'Alice Johnson',
|
||||
email: 'alice.j@email.com',
|
||||
status: 'invited',
|
||||
releaseLevel: 1,
|
||||
releaseOrder: 3,
|
||||
paymentStrategy: 'pay_on_access',
|
||||
},
|
||||
];
|
||||
|
||||
// Release level descriptions with nautical theme
|
||||
const releaseLevelConfig: Record<number, { label: string; description: string; icon: string }> = {
|
||||
1: {
|
||||
label: 'Journal Summaries',
|
||||
description: 'Captain\'s log excerpts and emotional records',
|
||||
icon: 'book-open',
|
||||
},
|
||||
2: {
|
||||
label: 'Treasure Map',
|
||||
description: 'Asset inventory and metadata',
|
||||
icon: 'map',
|
||||
},
|
||||
3: {
|
||||
label: 'Full Inheritance',
|
||||
description: 'Complete encrypted treasure chest',
|
||||
icon: 'key',
|
||||
},
|
||||
};
|
||||
|
||||
export default function HeritageScreen() {
|
||||
const [heirs, setHeirs] = useState<Heir[]>(initialHeirs);
|
||||
const [showAddModal, setShowAddModal] = useState(false);
|
||||
const [showDetailModal, setShowDetailModal] = useState(false);
|
||||
const [selectedHeir, setSelectedHeir] = useState<Heir | null>(null);
|
||||
const [newHeirName, setNewHeirName] = useState('');
|
||||
const [newHeirEmail, setNewHeirEmail] = useState('');
|
||||
const [newHeirLevel, setNewHeirLevel] = useState(1);
|
||||
const [newHeirPayment, setNewHeirPayment] = useState<PaymentStrategy>('pay_on_access');
|
||||
|
||||
const handleAddHeir = () => {
|
||||
if (!newHeirName.trim() || !newHeirEmail.trim()) return;
|
||||
|
||||
const newHeir: Heir = {
|
||||
id: Date.now().toString(),
|
||||
name: newHeirName,
|
||||
email: newHeirEmail,
|
||||
status: 'invited',
|
||||
releaseLevel: newHeirLevel,
|
||||
releaseOrder: heirs.length + 1,
|
||||
paymentStrategy: newHeirPayment,
|
||||
};
|
||||
|
||||
setHeirs([...heirs, newHeir]);
|
||||
resetAddForm();
|
||||
setShowAddModal(false);
|
||||
};
|
||||
|
||||
const resetAddForm = () => {
|
||||
setNewHeirName('');
|
||||
setNewHeirEmail('');
|
||||
setNewHeirLevel(1);
|
||||
setNewHeirPayment('pay_on_access');
|
||||
};
|
||||
|
||||
const handleHeirPress = (heir: Heir) => {
|
||||
setSelectedHeir(heir);
|
||||
setShowDetailModal(true);
|
||||
};
|
||||
|
||||
const getStatusBadge = (status: HeirStatus) => {
|
||||
if (status === 'confirmed') {
|
||||
return { text: 'Aboard', color: colors.heritage.confirmed, icon: 'checkmark-circle' };
|
||||
}
|
||||
return { text: 'Invited', color: colors.heritage.pending, icon: 'time-outline' };
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<LinearGradient
|
||||
colors={[colors.heritage.backgroundGradientStart, colors.heritage.backgroundGradientEnd]}
|
||||
style={styles.gradient}
|
||||
>
|
||||
<SafeAreaView style={styles.safeArea}>
|
||||
<ScrollView
|
||||
style={styles.scrollView}
|
||||
showsVerticalScrollIndicator={false}
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
>
|
||||
{/* Header */}
|
||||
<View style={styles.header}>
|
||||
<View style={styles.headerTitleRow}>
|
||||
<MaterialCommunityIcons name="compass-outline" size={28} color={colors.heritage.primary} />
|
||||
<Text style={styles.title}>Fleet Legacy</Text>
|
||||
</View>
|
||||
<Text style={styles.subtitle}>
|
||||
Your trusted crew for the final voyage
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Legal Notice */}
|
||||
<View style={styles.legalNotice}>
|
||||
<View style={styles.legalIconContainer}>
|
||||
<MaterialCommunityIcons name="ship-wheel" size={24} color={colors.heritage.accent} />
|
||||
</View>
|
||||
<View style={styles.legalContent}>
|
||||
<Text style={styles.legalTitle}>Captain's Orders</Text>
|
||||
<Text style={styles.legalText}>
|
||||
Your legacy will be distributed according to the order and levels you specify here.
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Release Stages Info */}
|
||||
<View style={styles.stagesSection}>
|
||||
<View style={styles.sectionHeader}>
|
||||
<FontAwesome5 name="layer-group" size={16} color={colors.heritage.primary} />
|
||||
<Text style={styles.sectionTitle}>INHERITANCE TIERS</Text>
|
||||
</View>
|
||||
<View style={styles.stagesContainer}>
|
||||
{[1, 2, 3].map((level) => (
|
||||
<View key={level} style={styles.stageItem}>
|
||||
<LinearGradient
|
||||
colors={[colors.nautical.teal, colors.nautical.deepTeal]}
|
||||
style={styles.stageNumber}
|
||||
>
|
||||
<Text style={styles.stageNumberText}>{level}</Text>
|
||||
</LinearGradient>
|
||||
<View style={styles.stageInfo}>
|
||||
<View style={styles.stageLabelRow}>
|
||||
<Feather name={releaseLevelConfig[level].icon as any} size={14} color={colors.heritage.primary} />
|
||||
<Text style={styles.stageLabel}>
|
||||
{releaseLevelConfig[level].label}
|
||||
</Text>
|
||||
</View>
|
||||
<Text style={styles.stageDescription}>
|
||||
{releaseLevelConfig[level].description}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Heirs List */}
|
||||
<View style={styles.heirsSection}>
|
||||
<View style={styles.sectionHeader}>
|
||||
<FontAwesome5 name="users" size={16} color={colors.heritage.primary} />
|
||||
<Text style={styles.sectionTitle}>TRUSTEE FLEET</Text>
|
||||
<View style={styles.heirsCountBadge}>
|
||||
<Text style={styles.heirsCount}>{heirs.length} Ships</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{heirs.map((heir) => {
|
||||
const statusBadge = getStatusBadge(heir.status);
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={heir.id}
|
||||
style={styles.heirCard}
|
||||
onPress={() => handleHeirPress(heir)}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<View style={styles.heirHeader}>
|
||||
<View style={styles.heirOrderBadge}>
|
||||
<Text style={styles.heirOrderText}>#{heir.releaseOrder}</Text>
|
||||
</View>
|
||||
<View style={[styles.statusBadge, { backgroundColor: `${statusBadge.color}15` }]}>
|
||||
<Ionicons name={statusBadge.icon as any} size={12} color={statusBadge.color} />
|
||||
<Text style={[styles.statusText, { color: statusBadge.color }]}>
|
||||
{statusBadge.text}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.heirMainInfo}>
|
||||
<View style={styles.heirAvatar}>
|
||||
<FontAwesome5 name="ship" size={18} color={colors.heritage.primary} />
|
||||
</View>
|
||||
<View style={styles.heirDetails}>
|
||||
<Text style={styles.heirName}>{heir.name}</Text>
|
||||
<Text style={styles.heirEmail}>{heir.email}</Text>
|
||||
</View>
|
||||
<Feather name="chevron-right" size={20} color={colors.heritage.textSecondary} />
|
||||
</View>
|
||||
|
||||
<View style={styles.heirMeta}>
|
||||
<View style={styles.metaItem}>
|
||||
<Feather name="layers" size={14} color={colors.heritage.textSecondary} />
|
||||
<Text style={styles.metaLabel}>Tier {heir.releaseLevel}</Text>
|
||||
</View>
|
||||
<View style={styles.metaItem}>
|
||||
<FontAwesome5 name="coins" size={12} color={colors.heritage.textSecondary} />
|
||||
<Text style={styles.metaLabel}>
|
||||
{heir.paymentStrategy === 'prepaid' ? 'Prepaid' : 'Pay on Access'}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
|
||||
{/* Add Heir Button */}
|
||||
<TouchableOpacity
|
||||
style={styles.addButton}
|
||||
onPress={() => setShowAddModal(true)}
|
||||
activeOpacity={0.9}
|
||||
>
|
||||
<LinearGradient
|
||||
colors={[colors.nautical.teal, colors.nautical.seafoam]}
|
||||
style={styles.addButtonGradient}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 0 }}
|
||||
>
|
||||
<FontAwesome5 name="ship" size={18} color="#fff" />
|
||||
<Text style={styles.addButtonText}>Add Trustee</Text>
|
||||
</LinearGradient>
|
||||
</TouchableOpacity>
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
</LinearGradient>
|
||||
|
||||
{/* Add Heir Modal */}
|
||||
<Modal
|
||||
visible={showAddModal}
|
||||
animationType="slide"
|
||||
transparent
|
||||
onRequestClose={() => setShowAddModal(false)}
|
||||
>
|
||||
<View style={styles.modalOverlay}>
|
||||
<View style={styles.modalContent}>
|
||||
<View style={styles.modalHandle} />
|
||||
<View style={styles.modalHeader}>
|
||||
<FontAwesome5 name="ship" size={22} color={colors.heritage.primary} />
|
||||
<Text style={styles.modalTitle}>Add to Fleet</Text>
|
||||
</View>
|
||||
|
||||
<Text style={styles.inputLabel}>NAME *</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="Trustee name"
|
||||
placeholderTextColor={colors.heritage.textSecondary}
|
||||
value={newHeirName}
|
||||
onChangeText={setNewHeirName}
|
||||
/>
|
||||
|
||||
<Text style={styles.inputLabel}>EMAIL *</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="For invitations and notifications"
|
||||
placeholderTextColor={colors.heritage.textSecondary}
|
||||
value={newHeirEmail}
|
||||
onChangeText={setNewHeirEmail}
|
||||
keyboardType="email-address"
|
||||
autoCapitalize="none"
|
||||
/>
|
||||
|
||||
<Text style={styles.inputLabel}>INHERITANCE TIER</Text>
|
||||
<View style={styles.levelButtons}>
|
||||
{[1, 2, 3].map((level) => (
|
||||
<TouchableOpacity
|
||||
key={level}
|
||||
style={[styles.levelButton, newHeirLevel === level && styles.levelButtonActive]}
|
||||
onPress={() => setNewHeirLevel(level)}
|
||||
>
|
||||
<Feather
|
||||
name={releaseLevelConfig[level].icon as any}
|
||||
size={20}
|
||||
color={newHeirLevel === level ? colors.heritage.primary : colors.heritage.textSecondary}
|
||||
/>
|
||||
<Text style={[styles.levelButtonText, newHeirLevel === level && styles.levelButtonTextActive]}>
|
||||
Tier {level}
|
||||
</Text>
|
||||
<Text style={[styles.levelButtonLabel, newHeirLevel === level && styles.levelButtonLabelActive]}>
|
||||
{releaseLevelConfig[level].label}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
|
||||
<Text style={styles.inputLabel}>PAYMENT STRATEGY</Text>
|
||||
<View style={styles.paymentButtons}>
|
||||
<TouchableOpacity
|
||||
style={[styles.paymentButton, newHeirPayment === 'prepaid' && styles.paymentButtonActive]}
|
||||
onPress={() => setNewHeirPayment('prepaid')}
|
||||
>
|
||||
<Feather name="check-circle" size={18} color={newHeirPayment === 'prepaid' ? colors.heritage.primary : colors.heritage.textSecondary} />
|
||||
<Text style={[styles.paymentButtonText, newHeirPayment === 'prepaid' && styles.paymentButtonTextActive]}>
|
||||
Prepaid
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={[styles.paymentButton, newHeirPayment === 'pay_on_access' && styles.paymentButtonActive]}
|
||||
onPress={() => setNewHeirPayment('pay_on_access')}
|
||||
>
|
||||
<Feather name="clock" size={18} color={newHeirPayment === 'pay_on_access' ? colors.heritage.primary : colors.heritage.textSecondary} />
|
||||
<Text style={[styles.paymentButtonText, newHeirPayment === 'pay_on_access' && styles.paymentButtonTextActive]}>
|
||||
Pay on Access
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<View style={styles.modalButtons}>
|
||||
<TouchableOpacity
|
||||
style={styles.cancelButton}
|
||||
onPress={() => {
|
||||
setShowAddModal(false);
|
||||
resetAddForm();
|
||||
}}
|
||||
>
|
||||
<Text style={styles.cancelButtonText}>Cancel</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={styles.confirmButton}
|
||||
onPress={handleAddHeir}
|
||||
>
|
||||
<LinearGradient
|
||||
colors={[colors.nautical.teal, colors.nautical.seafoam]}
|
||||
style={styles.confirmButtonGradient}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 0 }}
|
||||
>
|
||||
<Feather name="send" size={18} color="#fff" />
|
||||
<Text style={styles.confirmButtonText}>Send Invitation</Text>
|
||||
</LinearGradient>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
|
||||
{/* Heir Detail Modal */}
|
||||
<Modal
|
||||
visible={showDetailModal}
|
||||
animationType="fade"
|
||||
transparent
|
||||
onRequestClose={() => setShowDetailModal(false)}
|
||||
>
|
||||
<View style={styles.detailOverlay}>
|
||||
<View style={styles.detailModal}>
|
||||
{selectedHeir && (
|
||||
<>
|
||||
<View style={styles.detailHeader}>
|
||||
<View style={styles.detailAvatar}>
|
||||
<FontAwesome5 name="ship" size={28} color={colors.heritage.primary} />
|
||||
</View>
|
||||
<Text style={styles.detailTitle}>{selectedHeir.name}</Text>
|
||||
<View style={[
|
||||
styles.statusBadge,
|
||||
{ backgroundColor: `${getStatusBadge(selectedHeir.status).color}15` }
|
||||
]}>
|
||||
<Ionicons
|
||||
name={getStatusBadge(selectedHeir.status).icon as any}
|
||||
size={12}
|
||||
color={getStatusBadge(selectedHeir.status).color}
|
||||
/>
|
||||
<Text style={[styles.statusText, { color: getStatusBadge(selectedHeir.status).color }]}>
|
||||
{getStatusBadge(selectedHeir.status).text}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.detailRows}>
|
||||
<View style={styles.detailRow}>
|
||||
<Feather name="mail" size={16} color={colors.heritage.textSecondary} />
|
||||
<Text style={styles.detailLabel}>Email</Text>
|
||||
<Text style={styles.detailValue}>{selectedHeir.email}</Text>
|
||||
</View>
|
||||
<View style={styles.detailRow}>
|
||||
<Feather name="hash" size={16} color={colors.heritage.textSecondary} />
|
||||
<Text style={styles.detailLabel}>Order</Text>
|
||||
<Text style={styles.detailValue}>#{selectedHeir.releaseOrder}</Text>
|
||||
</View>
|
||||
<View style={styles.detailRow}>
|
||||
<Feather name="layers" size={16} color={colors.heritage.textSecondary} />
|
||||
<Text style={styles.detailLabel}>Tier</Text>
|
||||
<Text style={styles.detailValue}>
|
||||
{selectedHeir.releaseLevel} · {releaseLevelConfig[selectedHeir.releaseLevel].label}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.detailRow}>
|
||||
<FontAwesome5 name="coins" size={14} color={colors.heritage.textSecondary} />
|
||||
<Text style={styles.detailLabel}>Payment</Text>
|
||||
<Text style={styles.detailValue}>
|
||||
{selectedHeir.paymentStrategy === 'prepaid' ? 'Prepaid' : 'Pay on Access'}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
style={styles.closeButton}
|
||||
onPress={() => setShowDetailModal(false)}
|
||||
>
|
||||
<Text style={styles.closeButtonText}>Close</Text>
|
||||
</TouchableOpacity>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
gradient: {
|
||||
flex: 1,
|
||||
},
|
||||
safeArea: {
|
||||
flex: 1,
|
||||
},
|
||||
scrollView: {
|
||||
flex: 1,
|
||||
},
|
||||
scrollContent: {
|
||||
padding: spacing.lg,
|
||||
paddingBottom: 120,
|
||||
},
|
||||
header: {
|
||||
marginBottom: spacing.lg,
|
||||
},
|
||||
headerTitleRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: spacing.sm,
|
||||
marginBottom: spacing.xs,
|
||||
},
|
||||
title: {
|
||||
fontSize: typography.fontSize.xl,
|
||||
fontWeight: '600',
|
||||
color: colors.heritage.text,
|
||||
fontFamily: typography.fontFamily.serif,
|
||||
},
|
||||
subtitle: {
|
||||
fontSize: typography.fontSize.base,
|
||||
color: colors.heritage.textSecondary,
|
||||
marginLeft: spacing.xl + spacing.sm,
|
||||
fontStyle: 'italic',
|
||||
},
|
||||
legalNotice: {
|
||||
flexDirection: 'row',
|
||||
backgroundColor: colors.heritage.cardBackground,
|
||||
borderRadius: borderRadius.xl,
|
||||
padding: spacing.base,
|
||||
marginBottom: spacing.lg,
|
||||
borderLeftWidth: 4,
|
||||
borderLeftColor: colors.heritage.accent,
|
||||
...shadows.soft,
|
||||
},
|
||||
legalIconContainer: {
|
||||
marginRight: spacing.md,
|
||||
},
|
||||
legalContent: {
|
||||
flex: 1,
|
||||
},
|
||||
legalTitle: {
|
||||
fontSize: typography.fontSize.base,
|
||||
fontWeight: '600',
|
||||
color: colors.heritage.text,
|
||||
marginBottom: spacing.xs,
|
||||
},
|
||||
legalText: {
|
||||
fontSize: typography.fontSize.sm,
|
||||
color: colors.heritage.textSecondary,
|
||||
lineHeight: typography.fontSize.sm * 1.5,
|
||||
},
|
||||
stagesSection: {
|
||||
marginBottom: spacing.lg,
|
||||
},
|
||||
sectionHeader: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: spacing.sm,
|
||||
marginBottom: spacing.md,
|
||||
},
|
||||
sectionTitle: {
|
||||
flex: 1,
|
||||
fontSize: typography.fontSize.xs,
|
||||
fontWeight: '700',
|
||||
color: colors.heritage.textSecondary,
|
||||
letterSpacing: typography.letterSpacing.widest,
|
||||
},
|
||||
stagesContainer: {
|
||||
backgroundColor: colors.heritage.cardBackground,
|
||||
borderRadius: borderRadius.xl,
|
||||
padding: spacing.base,
|
||||
...shadows.soft,
|
||||
},
|
||||
stageItem: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingVertical: spacing.sm,
|
||||
},
|
||||
stageNumber: {
|
||||
width: 36,
|
||||
height: 36,
|
||||
borderRadius: 18,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginRight: spacing.md,
|
||||
},
|
||||
stageNumberText: {
|
||||
fontSize: typography.fontSize.base,
|
||||
fontWeight: '700',
|
||||
color: '#fff',
|
||||
},
|
||||
stageInfo: {
|
||||
flex: 1,
|
||||
},
|
||||
stageLabelRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: spacing.xs,
|
||||
marginBottom: 2,
|
||||
},
|
||||
stageLabel: {
|
||||
fontSize: typography.fontSize.base,
|
||||
fontWeight: '600',
|
||||
color: colors.heritage.text,
|
||||
},
|
||||
stageDescription: {
|
||||
fontSize: typography.fontSize.sm,
|
||||
color: colors.heritage.textSecondary,
|
||||
},
|
||||
heirsSection: {
|
||||
marginBottom: spacing.lg,
|
||||
},
|
||||
heirsCountBadge: {
|
||||
backgroundColor: colors.nautical.lightMint,
|
||||
paddingHorizontal: spacing.sm,
|
||||
paddingVertical: 4,
|
||||
borderRadius: borderRadius.full,
|
||||
},
|
||||
heirsCount: {
|
||||
fontSize: typography.fontSize.xs,
|
||||
fontWeight: '600',
|
||||
color: colors.nautical.teal,
|
||||
},
|
||||
heirCard: {
|
||||
backgroundColor: colors.heritage.cardBackground,
|
||||
borderRadius: borderRadius.xl,
|
||||
padding: spacing.base,
|
||||
marginBottom: spacing.md,
|
||||
...shadows.soft,
|
||||
},
|
||||
heirHeader: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: spacing.sm,
|
||||
},
|
||||
heirOrderBadge: {
|
||||
backgroundColor: colors.nautical.teal,
|
||||
paddingHorizontal: spacing.sm,
|
||||
paddingVertical: 2,
|
||||
borderRadius: borderRadius.sm,
|
||||
},
|
||||
heirOrderText: {
|
||||
fontSize: typography.fontSize.xs,
|
||||
fontWeight: '700',
|
||||
color: '#fff',
|
||||
},
|
||||
statusBadge: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
paddingHorizontal: spacing.sm,
|
||||
paddingVertical: 4,
|
||||
borderRadius: borderRadius.full,
|
||||
},
|
||||
statusText: {
|
||||
fontSize: typography.fontSize.xs,
|
||||
fontWeight: '600',
|
||||
},
|
||||
heirMainInfo: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginBottom: spacing.sm,
|
||||
},
|
||||
heirAvatar: {
|
||||
width: 48,
|
||||
height: 48,
|
||||
borderRadius: 24,
|
||||
backgroundColor: colors.nautical.lightMint,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginRight: spacing.md,
|
||||
},
|
||||
heirDetails: {
|
||||
flex: 1,
|
||||
},
|
||||
heirName: {
|
||||
fontSize: typography.fontSize.base,
|
||||
fontWeight: '600',
|
||||
color: colors.heritage.text,
|
||||
marginBottom: 2,
|
||||
},
|
||||
heirEmail: {
|
||||
fontSize: typography.fontSize.sm,
|
||||
color: colors.heritage.textSecondary,
|
||||
},
|
||||
heirMeta: {
|
||||
flexDirection: 'row',
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: colors.heritage.cardBorder,
|
||||
paddingTop: spacing.sm,
|
||||
gap: spacing.lg,
|
||||
},
|
||||
metaItem: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: spacing.xs,
|
||||
},
|
||||
metaLabel: {
|
||||
fontSize: typography.fontSize.sm,
|
||||
color: colors.heritage.textSecondary,
|
||||
},
|
||||
addButton: {
|
||||
borderRadius: borderRadius.lg,
|
||||
overflow: 'hidden',
|
||||
...shadows.soft,
|
||||
},
|
||||
addButtonGradient: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingVertical: spacing.md,
|
||||
gap: spacing.sm,
|
||||
},
|
||||
addButtonText: {
|
||||
fontSize: typography.fontSize.base,
|
||||
fontWeight: '700',
|
||||
color: '#fff',
|
||||
},
|
||||
modalOverlay: {
|
||||
flex: 1,
|
||||
backgroundColor: 'rgba(26, 58, 74, 0.5)',
|
||||
justifyContent: 'flex-end',
|
||||
},
|
||||
modalContent: {
|
||||
backgroundColor: colors.heritage.cardBackground,
|
||||
borderTopLeftRadius: borderRadius.xxl,
|
||||
borderTopRightRadius: borderRadius.xxl,
|
||||
padding: spacing.lg,
|
||||
paddingBottom: spacing.xxl,
|
||||
},
|
||||
modalHandle: {
|
||||
width: 40,
|
||||
height: 4,
|
||||
backgroundColor: colors.heritage.cardBorder,
|
||||
borderRadius: 2,
|
||||
alignSelf: 'center',
|
||||
marginBottom: spacing.lg,
|
||||
},
|
||||
modalHeader: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: spacing.sm,
|
||||
marginBottom: spacing.lg,
|
||||
},
|
||||
modalTitle: {
|
||||
fontSize: typography.fontSize.lg,
|
||||
fontWeight: '600',
|
||||
color: colors.heritage.text,
|
||||
},
|
||||
inputLabel: {
|
||||
fontSize: typography.fontSize.xs,
|
||||
color: colors.heritage.textSecondary,
|
||||
marginBottom: spacing.xs,
|
||||
fontWeight: '600',
|
||||
letterSpacing: typography.letterSpacing.wide,
|
||||
},
|
||||
input: {
|
||||
backgroundColor: colors.nautical.paleAqua,
|
||||
borderRadius: borderRadius.lg,
|
||||
padding: spacing.base,
|
||||
fontSize: typography.fontSize.base,
|
||||
color: colors.heritage.text,
|
||||
marginBottom: spacing.md,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.heritage.cardBorder,
|
||||
},
|
||||
levelButtons: {
|
||||
flexDirection: 'row',
|
||||
gap: spacing.sm,
|
||||
marginBottom: spacing.md,
|
||||
},
|
||||
levelButton: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
paddingVertical: spacing.md,
|
||||
borderRadius: borderRadius.lg,
|
||||
backgroundColor: colors.nautical.paleAqua,
|
||||
borderWidth: 2,
|
||||
borderColor: 'transparent',
|
||||
},
|
||||
levelButtonActive: {
|
||||
borderColor: colors.heritage.primary,
|
||||
backgroundColor: colors.nautical.lightMint,
|
||||
},
|
||||
levelButtonText: {
|
||||
fontSize: typography.fontSize.sm,
|
||||
fontWeight: '700',
|
||||
color: colors.heritage.textSecondary,
|
||||
marginVertical: spacing.xs,
|
||||
},
|
||||
levelButtonTextActive: {
|
||||
color: colors.heritage.primary,
|
||||
},
|
||||
levelButtonLabel: {
|
||||
fontSize: typography.fontSize.xs,
|
||||
color: colors.heritage.textSecondary,
|
||||
textAlign: 'center',
|
||||
},
|
||||
levelButtonLabelActive: {
|
||||
color: colors.heritage.primary,
|
||||
},
|
||||
paymentButtons: {
|
||||
flexDirection: 'row',
|
||||
gap: spacing.sm,
|
||||
marginBottom: spacing.lg,
|
||||
},
|
||||
paymentButton: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingVertical: spacing.md,
|
||||
borderRadius: borderRadius.lg,
|
||||
backgroundColor: colors.nautical.paleAqua,
|
||||
borderWidth: 2,
|
||||
borderColor: 'transparent',
|
||||
gap: spacing.xs,
|
||||
},
|
||||
paymentButtonActive: {
|
||||
borderColor: colors.heritage.primary,
|
||||
backgroundColor: colors.nautical.lightMint,
|
||||
},
|
||||
paymentButtonText: {
|
||||
fontSize: typography.fontSize.sm,
|
||||
fontWeight: '600',
|
||||
color: colors.heritage.textSecondary,
|
||||
},
|
||||
paymentButtonTextActive: {
|
||||
color: colors.heritage.primary,
|
||||
},
|
||||
modalButtons: {
|
||||
flexDirection: 'row',
|
||||
gap: spacing.md,
|
||||
},
|
||||
cancelButton: {
|
||||
flex: 1,
|
||||
paddingVertical: spacing.md,
|
||||
borderRadius: borderRadius.lg,
|
||||
backgroundColor: colors.nautical.paleAqua,
|
||||
alignItems: 'center',
|
||||
borderWidth: 1,
|
||||
borderColor: colors.heritage.cardBorder,
|
||||
},
|
||||
cancelButtonText: {
|
||||
fontSize: typography.fontSize.base,
|
||||
color: colors.heritage.textSecondary,
|
||||
fontWeight: '600',
|
||||
},
|
||||
confirmButton: {
|
||||
flex: 1,
|
||||
borderRadius: borderRadius.lg,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
confirmButtonGradient: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingVertical: spacing.md,
|
||||
gap: spacing.sm,
|
||||
},
|
||||
confirmButtonText: {
|
||||
fontSize: typography.fontSize.base,
|
||||
color: '#fff',
|
||||
fontWeight: '600',
|
||||
},
|
||||
detailOverlay: {
|
||||
flex: 1,
|
||||
backgroundColor: 'rgba(26, 58, 74, 0.5)',
|
||||
justifyContent: 'center',
|
||||
padding: spacing.lg,
|
||||
},
|
||||
detailModal: {
|
||||
backgroundColor: colors.heritage.cardBackground,
|
||||
borderRadius: borderRadius.xxl,
|
||||
padding: spacing.xl,
|
||||
...shadows.medium,
|
||||
},
|
||||
detailHeader: {
|
||||
alignItems: 'center',
|
||||
marginBottom: spacing.lg,
|
||||
paddingBottom: spacing.md,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.heritage.cardBorder,
|
||||
},
|
||||
detailAvatar: {
|
||||
width: 72,
|
||||
height: 72,
|
||||
borderRadius: 36,
|
||||
backgroundColor: colors.nautical.lightMint,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginBottom: spacing.md,
|
||||
},
|
||||
detailTitle: {
|
||||
fontSize: typography.fontSize.lg,
|
||||
fontWeight: '600',
|
||||
color: colors.heritage.text,
|
||||
marginBottom: spacing.sm,
|
||||
},
|
||||
detailRows: {
|
||||
gap: spacing.md,
|
||||
},
|
||||
detailRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: spacing.sm,
|
||||
},
|
||||
detailLabel: {
|
||||
flex: 1,
|
||||
fontSize: typography.fontSize.base,
|
||||
color: colors.heritage.textSecondary,
|
||||
},
|
||||
detailValue: {
|
||||
fontSize: typography.fontSize.base,
|
||||
color: colors.heritage.text,
|
||||
fontWeight: '600',
|
||||
},
|
||||
closeButton: {
|
||||
marginTop: spacing.lg,
|
||||
paddingVertical: spacing.md,
|
||||
backgroundColor: colors.nautical.lightMint,
|
||||
borderRadius: borderRadius.lg,
|
||||
alignItems: 'center',
|
||||
},
|
||||
closeButtonText: {
|
||||
fontSize: typography.fontSize.base,
|
||||
color: colors.heritage.primary,
|
||||
fontWeight: '600',
|
||||
},
|
||||
});
|
||||
749
src/screens/MeScreen.tsx
Normal file
749
src/screens/MeScreen.tsx
Normal file
@@ -0,0 +1,749 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
ScrollView,
|
||||
TouchableOpacity,
|
||||
Modal,
|
||||
SafeAreaView,
|
||||
Linking,
|
||||
Alert,
|
||||
} from 'react-native';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import { Ionicons, Feather, MaterialCommunityIcons, FontAwesome5 } from '@expo/vector-icons';
|
||||
import { colors, typography, spacing, borderRadius, shadows } from '../theme/colors';
|
||||
|
||||
// Sentinel Protocol Status
|
||||
const protocolStatus = {
|
||||
subscription: {
|
||||
status: 'Active',
|
||||
expiresAt: '2026-03',
|
||||
nextBilling: 'Feb 1',
|
||||
},
|
||||
heartbeat: 'Normal',
|
||||
guardLevel: 'L2',
|
||||
disconnectDays: 30,
|
||||
gracePeriod: 15,
|
||||
circuitBreaker: true,
|
||||
};
|
||||
|
||||
// Quick Action Cards
|
||||
const quickActions = [
|
||||
{ id: 'status', icon: 'shield-check', label: 'Status', value: 'Active', color: '#6BBF8A' },
|
||||
{ id: 'heartbeat', icon: 'heart-pulse', label: 'Heartbeat', value: 'OK', color: '#6BBF8A' },
|
||||
{ id: 'guard', icon: 'security', label: 'Guard', value: 'L2', color: colors.nautical.teal },
|
||||
{ id: 'fleet', icon: 'sail-boat', label: 'Fleet', value: '3', color: colors.nautical.seafoam },
|
||||
];
|
||||
|
||||
// The Captain's Protocols
|
||||
const captainProtocols = [
|
||||
{
|
||||
id: 'spirit-keys',
|
||||
icon: 'key-variant',
|
||||
title: 'Spirit Keys',
|
||||
subtitle: 'Manage recovery phrases & keys',
|
||||
color: colors.nautical.teal,
|
||||
},
|
||||
{
|
||||
id: 'tide-notifications',
|
||||
icon: 'weather-cloudy',
|
||||
title: 'Tide Notifications',
|
||||
subtitle: 'Alert preferences & channels',
|
||||
color: colors.nautical.seafoam,
|
||||
},
|
||||
{
|
||||
id: 'sanctum-settings',
|
||||
icon: 'cog-outline',
|
||||
title: 'Sanctum Settings',
|
||||
subtitle: 'Privacy, security & preferences',
|
||||
color: colors.nautical.sage,
|
||||
},
|
||||
];
|
||||
|
||||
// Settings menu (condensed)
|
||||
const settingsMenu = [
|
||||
{
|
||||
id: 'sentinel',
|
||||
icon: 'bell-ring-outline',
|
||||
title: 'Sentinel Status',
|
||||
subtitle: `Active · Next billing ${protocolStatus.subscription.nextBilling}`,
|
||||
color: colors.nautical.teal,
|
||||
},
|
||||
{
|
||||
id: 'trigger',
|
||||
icon: 'timer-sand',
|
||||
title: 'Trigger Logic',
|
||||
subtitle: `${protocolStatus.disconnectDays}d disconnect · ${protocolStatus.gracePeriod}d grace`,
|
||||
color: colors.nautical.gold,
|
||||
},
|
||||
{
|
||||
id: 'security',
|
||||
icon: 'fingerprint',
|
||||
title: 'Security Center',
|
||||
subtitle: 'Biometric · Device management',
|
||||
color: colors.nautical.deepTeal,
|
||||
},
|
||||
{
|
||||
id: 'visual',
|
||||
icon: 'palette-outline',
|
||||
title: 'Visual Preferences',
|
||||
subtitle: 'Theme · Cards · Typography',
|
||||
color: colors.nautical.seafoam,
|
||||
},
|
||||
{
|
||||
id: 'export',
|
||||
icon: 'download-outline',
|
||||
title: 'Data Export',
|
||||
subtitle: 'JSON backup · Recovery phrase',
|
||||
color: colors.nautical.sage,
|
||||
},
|
||||
{
|
||||
id: 'legal',
|
||||
icon: 'file-document-outline',
|
||||
title: 'Legal & Privacy',
|
||||
subtitle: 'Terms · Open source license',
|
||||
color: colors.nautical.navy,
|
||||
},
|
||||
];
|
||||
|
||||
// Protocol explainers
|
||||
const protocolExplainers = [
|
||||
{
|
||||
id: 'opensource',
|
||||
icon: 'code-tags',
|
||||
title: 'Open Source Protocol',
|
||||
content: `Sentinel's core encryption and protocol are fully open source on GitHub.
|
||||
|
||||
Core Modules:
|
||||
• sentinel-crypto: Encryption implementation
|
||||
• sentinel-protocol: Release specification
|
||||
• sentinel-verify: Verification tools
|
||||
|
||||
Transparency builds trust.`,
|
||||
},
|
||||
{
|
||||
id: 'deadman',
|
||||
icon: 'ship-wheel',
|
||||
title: 'Dead Man\'s Switch',
|
||||
content: `The core mechanism of Sentinel:
|
||||
|
||||
1. Confirm "I am alive" periodically
|
||||
2. System monitors activity signals
|
||||
3. No signal → Warning state
|
||||
4. Continued silence → Release begins
|
||||
|
||||
Current Config:
|
||||
• Disconnect: ${protocolStatus.disconnectDays} days
|
||||
• Grace Period: ${protocolStatus.gracePeriod} days
|
||||
• Circuit Breaker: ${protocolStatus.circuitBreaker ? 'ON' : 'OFF'}`,
|
||||
},
|
||||
{
|
||||
id: 'guardian',
|
||||
icon: 'shield-star',
|
||||
title: 'Guardian Program',
|
||||
content: `Social Responsibility Initiative:
|
||||
|
||||
• 1 sponsored user per 10 paying users
|
||||
• Hospice care partnerships
|
||||
• Digital heritage education
|
||||
|
||||
You may qualify for exemption.
|
||||
Contact: care@sentinel.app`,
|
||||
},
|
||||
];
|
||||
|
||||
export default function MeScreen() {
|
||||
const [selectedExplainer, setSelectedExplainer] = useState<typeof protocolExplainers[0] | null>(null);
|
||||
|
||||
const handleOpenLink = (url: string) => {
|
||||
Linking.openURL(url).catch(() => {});
|
||||
};
|
||||
|
||||
const handleAbandonIsland = () => {
|
||||
Alert.alert(
|
||||
'Abandon Island',
|
||||
'Are you sure you want to delete your account? This action is irreversible. All your data, including vault contents, will be permanently destroyed.',
|
||||
[
|
||||
{ text: 'Cancel', style: 'cancel' },
|
||||
{
|
||||
text: 'Delete Account',
|
||||
style: 'destructive',
|
||||
onPress: () => Alert.alert('Account Deletion', 'Please contact support to proceed with account deletion.')
|
||||
},
|
||||
]
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<LinearGradient
|
||||
colors={[colors.me.backgroundGradientStart, colors.me.backgroundGradientEnd]}
|
||||
style={styles.gradient}
|
||||
>
|
||||
<SafeAreaView style={styles.safeArea}>
|
||||
<ScrollView
|
||||
style={styles.scrollView}
|
||||
showsVerticalScrollIndicator={false}
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
>
|
||||
{/* Header with Settings */}
|
||||
<View style={styles.headerRow}>
|
||||
<View style={styles.headerSpacer} />
|
||||
<TouchableOpacity style={styles.settingsButton}>
|
||||
<Ionicons name="moon-outline" size={20} color={colors.me.primary} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* Profile Card */}
|
||||
<View style={styles.profileCard}>
|
||||
<View style={styles.avatarSection}>
|
||||
<View style={styles.avatarContainer}>
|
||||
<View style={styles.avatarImage}>
|
||||
<MaterialCommunityIcons name="account" size={40} color="#fff" />
|
||||
</View>
|
||||
<View style={styles.verifiedBadge}>
|
||||
<Ionicons name="checkmark" size={10} color="#fff" />
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.profileInfo}>
|
||||
<Text style={styles.profileName}>Captain</Text>
|
||||
<Text style={styles.profileTitle}>MASTER OF THE SANCTUM</Text>
|
||||
<View style={styles.profileBadge}>
|
||||
<MaterialCommunityIcons name="crown" size={12} color={colors.nautical.gold} />
|
||||
<Text style={styles.profileBadgeText}>Pro Member</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Quick Stats */}
|
||||
<View style={styles.statsGrid}>
|
||||
{quickActions.map((action) => (
|
||||
<View key={action.id} style={styles.statCard}>
|
||||
<MaterialCommunityIcons name={action.icon as any} size={22} color={action.color} />
|
||||
<Text style={styles.statValue}>{action.value}</Text>
|
||||
<Text style={styles.statLabel}>{action.label}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{/* Vessel Integrity */}
|
||||
<View style={styles.integrityCard}>
|
||||
<View style={styles.integrityHeader}>
|
||||
<View style={styles.integrityTitleRow}>
|
||||
<FontAwesome5 name="anchor" size={14} color={colors.me.primary} />
|
||||
<Text style={styles.integrityTitle}>Vessel Integrity</Text>
|
||||
</View>
|
||||
<Text style={styles.integrityPercent}>92%</Text>
|
||||
</View>
|
||||
<View style={styles.progressBar}>
|
||||
<LinearGradient
|
||||
colors={[colors.nautical.teal, colors.nautical.seafoam]}
|
||||
style={[styles.progressFill, { width: '92%' }]}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 0 }}
|
||||
/>
|
||||
</View>
|
||||
<Text style={styles.integrityHint}>Your legacy is sailing true on the Divine Path</Text>
|
||||
</View>
|
||||
|
||||
{/* The Captain's Protocols */}
|
||||
<Text style={styles.sectionTitle}>THE CAPTAIN'S PROTOCOLS</Text>
|
||||
<View style={styles.menuCard}>
|
||||
{captainProtocols.map((item, index) => (
|
||||
<TouchableOpacity
|
||||
key={item.id}
|
||||
style={[
|
||||
styles.menuItem,
|
||||
index < captainProtocols.length - 1 && styles.menuItemBorder
|
||||
]}
|
||||
>
|
||||
<View style={[styles.menuIconContainer, { backgroundColor: `${item.color}20` }]}>
|
||||
<MaterialCommunityIcons name={item.icon as any} size={22} color={item.color} />
|
||||
</View>
|
||||
<View style={styles.menuContent}>
|
||||
<Text style={styles.menuTitle}>{item.title}</Text>
|
||||
</View>
|
||||
<Feather name="chevron-right" size={18} color={colors.me.textSecondary} />
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{/* Abandon Island Button */}
|
||||
<TouchableOpacity
|
||||
style={styles.abandonButton}
|
||||
onPress={handleAbandonIsland}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<Feather name="log-out" size={18} color={colors.nautical.coral} />
|
||||
<Text style={styles.abandonButtonText}>ABANDON ISLAND</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* Settings Menu */}
|
||||
<Text style={styles.sectionTitle}>SETTINGS</Text>
|
||||
<View style={styles.menuCard}>
|
||||
{settingsMenu.map((item, index) => (
|
||||
<TouchableOpacity
|
||||
key={item.id}
|
||||
style={[
|
||||
styles.menuItem,
|
||||
index < settingsMenu.length - 1 && styles.menuItemBorder
|
||||
]}
|
||||
>
|
||||
<View style={[styles.menuIconContainer, { backgroundColor: `${item.color}15` }]}>
|
||||
<MaterialCommunityIcons name={item.icon as any} size={20} color={item.color} />
|
||||
</View>
|
||||
<View style={styles.menuContent}>
|
||||
<Text style={styles.menuTitle}>{item.title}</Text>
|
||||
<Text style={styles.menuSubtitle}>{item.subtitle}</Text>
|
||||
</View>
|
||||
<Feather name="chevron-right" size={18} color={colors.me.textSecondary} />
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{/* About Section */}
|
||||
<Text style={styles.sectionTitle}>ABOUT</Text>
|
||||
<View style={styles.aboutGrid}>
|
||||
{protocolExplainers.slice(0, 2).map((item) => (
|
||||
<TouchableOpacity
|
||||
key={item.id}
|
||||
style={styles.aboutCard}
|
||||
onPress={() => setSelectedExplainer(item)}
|
||||
>
|
||||
<MaterialCommunityIcons name={item.icon as any} size={24} color={colors.me.primary} />
|
||||
<Text style={styles.aboutTitle}>{item.title}</Text>
|
||||
<Feather name="arrow-up-right" size={14} color={colors.me.textSecondary} />
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{/* Footer */}
|
||||
<View style={styles.footer}>
|
||||
<Text style={styles.footerVersion}>A E T E R N A N A U T I C A V 2 . 0</Text>
|
||||
<Text style={styles.footerTagline}>
|
||||
The sea claims what is forgotten, but the Sanctuary keeps what is loved.
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Footer Links */}
|
||||
<View style={styles.footerLinks}>
|
||||
<TouchableOpacity onPress={() => handleOpenLink('https://github.com/sentinel')}>
|
||||
<Text style={styles.footerLink}>GitHub</Text>
|
||||
</TouchableOpacity>
|
||||
<Text style={styles.footerDot}>·</Text>
|
||||
<TouchableOpacity onPress={() => handleOpenLink('https://sentinel.app/privacy')}>
|
||||
<Text style={styles.footerLink}>Privacy</Text>
|
||||
</TouchableOpacity>
|
||||
<Text style={styles.footerDot}>·</Text>
|
||||
<TouchableOpacity onPress={() => handleOpenLink('https://sentinel.app/terms')}>
|
||||
<Text style={styles.footerLink}>Terms</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
</LinearGradient>
|
||||
|
||||
{/* Detail Modal */}
|
||||
<Modal
|
||||
visible={!!selectedExplainer}
|
||||
animationType="slide"
|
||||
transparent
|
||||
onRequestClose={() => setSelectedExplainer(null)}
|
||||
>
|
||||
<View style={styles.modalOverlay}>
|
||||
<View style={styles.modalContent}>
|
||||
<View style={styles.modalHandle} />
|
||||
{selectedExplainer && (
|
||||
<>
|
||||
<View style={styles.modalHeader}>
|
||||
<View style={styles.modalIconContainer}>
|
||||
<MaterialCommunityIcons name={selectedExplainer.icon as any} size={24} color={colors.me.primary} />
|
||||
</View>
|
||||
<Text style={styles.modalTitle}>{selectedExplainer.title}</Text>
|
||||
</View>
|
||||
<ScrollView
|
||||
style={styles.modalScroll}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
<Text style={styles.modalText}>{selectedExplainer.content}</Text>
|
||||
</ScrollView>
|
||||
<TouchableOpacity
|
||||
style={styles.closeButton}
|
||||
onPress={() => setSelectedExplainer(null)}
|
||||
>
|
||||
<Text style={styles.closeButtonText}>Close</Text>
|
||||
</TouchableOpacity>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
gradient: {
|
||||
flex: 1,
|
||||
},
|
||||
safeArea: {
|
||||
flex: 1,
|
||||
},
|
||||
scrollView: {
|
||||
flex: 1,
|
||||
},
|
||||
scrollContent: {
|
||||
paddingHorizontal: spacing.base,
|
||||
paddingBottom: 100,
|
||||
},
|
||||
// Header
|
||||
headerRow: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingTop: spacing.sm,
|
||||
paddingBottom: spacing.md,
|
||||
},
|
||||
headerSpacer: {
|
||||
width: 40,
|
||||
},
|
||||
settingsButton: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 20,
|
||||
backgroundColor: colors.me.cardBackground,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
...shadows.soft,
|
||||
},
|
||||
// Profile Card
|
||||
profileCard: {
|
||||
backgroundColor: colors.me.cardBackground,
|
||||
borderRadius: borderRadius.xl,
|
||||
padding: spacing.base,
|
||||
marginBottom: spacing.md,
|
||||
...shadows.soft,
|
||||
},
|
||||
avatarSection: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
avatarContainer: {
|
||||
position: 'relative',
|
||||
marginRight: spacing.md,
|
||||
},
|
||||
avatarImage: {
|
||||
width: 72,
|
||||
height: 72,
|
||||
borderRadius: 20,
|
||||
backgroundColor: colors.nautical.teal,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
verifiedBadge: {
|
||||
position: 'absolute',
|
||||
bottom: -2,
|
||||
right: -2,
|
||||
width: 22,
|
||||
height: 22,
|
||||
borderRadius: 11,
|
||||
backgroundColor: colors.nautical.teal,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
borderWidth: 2,
|
||||
borderColor: colors.me.cardBackground,
|
||||
},
|
||||
profileInfo: {
|
||||
flex: 1,
|
||||
},
|
||||
profileName: {
|
||||
fontSize: typography.fontSize.xl,
|
||||
fontWeight: '700',
|
||||
color: colors.me.text,
|
||||
marginBottom: 2,
|
||||
},
|
||||
profileTitle: {
|
||||
fontSize: typography.fontSize.xs,
|
||||
fontWeight: '600',
|
||||
color: colors.me.textSecondary,
|
||||
letterSpacing: typography.letterSpacing.wider,
|
||||
marginBottom: spacing.xs,
|
||||
},
|
||||
profileBadge: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
backgroundColor: `${colors.nautical.gold}15`,
|
||||
paddingHorizontal: spacing.sm,
|
||||
paddingVertical: 3,
|
||||
borderRadius: borderRadius.full,
|
||||
alignSelf: 'flex-start',
|
||||
},
|
||||
profileBadgeText: {
|
||||
fontSize: typography.fontSize.xs,
|
||||
fontWeight: '600',
|
||||
color: colors.nautical.gold,
|
||||
},
|
||||
// Stats Grid
|
||||
statsGrid: {
|
||||
flexDirection: 'row',
|
||||
gap: spacing.sm,
|
||||
marginBottom: spacing.md,
|
||||
},
|
||||
statCard: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.me.cardBackground,
|
||||
borderRadius: borderRadius.lg,
|
||||
padding: spacing.sm,
|
||||
alignItems: 'center',
|
||||
...shadows.soft,
|
||||
},
|
||||
statValue: {
|
||||
fontSize: typography.fontSize.lg,
|
||||
fontWeight: '700',
|
||||
color: colors.me.text,
|
||||
marginTop: spacing.xs,
|
||||
},
|
||||
statLabel: {
|
||||
fontSize: typography.fontSize.xs,
|
||||
color: colors.me.textSecondary,
|
||||
marginTop: 2,
|
||||
},
|
||||
// Integrity Card
|
||||
integrityCard: {
|
||||
backgroundColor: colors.me.cardBackground,
|
||||
borderRadius: borderRadius.xl,
|
||||
padding: spacing.base,
|
||||
marginBottom: spacing.lg,
|
||||
...shadows.soft,
|
||||
},
|
||||
integrityHeader: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: spacing.sm,
|
||||
},
|
||||
integrityTitleRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: spacing.xs,
|
||||
},
|
||||
integrityTitle: {
|
||||
fontSize: typography.fontSize.base,
|
||||
fontWeight: '600',
|
||||
color: colors.me.text,
|
||||
},
|
||||
integrityPercent: {
|
||||
fontSize: typography.fontSize.lg,
|
||||
fontWeight: '700',
|
||||
color: colors.nautical.teal,
|
||||
},
|
||||
progressBar: {
|
||||
height: 6,
|
||||
backgroundColor: colors.nautical.lightMint,
|
||||
borderRadius: 3,
|
||||
overflow: 'hidden',
|
||||
marginBottom: spacing.sm,
|
||||
},
|
||||
progressFill: {
|
||||
height: '100%',
|
||||
borderRadius: 3,
|
||||
},
|
||||
integrityHint: {
|
||||
fontSize: typography.fontSize.xs,
|
||||
color: colors.me.textSecondary,
|
||||
fontStyle: 'italic',
|
||||
textAlign: 'center',
|
||||
},
|
||||
// Section Title
|
||||
sectionTitle: {
|
||||
fontSize: typography.fontSize.xs,
|
||||
fontWeight: '700',
|
||||
color: colors.me.textSecondary,
|
||||
letterSpacing: typography.letterSpacing.widest,
|
||||
marginBottom: spacing.sm,
|
||||
marginTop: spacing.sm,
|
||||
},
|
||||
// Menu Card
|
||||
menuCard: {
|
||||
backgroundColor: colors.me.cardBackground,
|
||||
borderRadius: borderRadius.xl,
|
||||
marginBottom: spacing.md,
|
||||
...shadows.soft,
|
||||
},
|
||||
menuItem: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
padding: spacing.base,
|
||||
},
|
||||
menuItemBorder: {
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.me.cardBorder,
|
||||
},
|
||||
menuIconContainer: {
|
||||
width: 44,
|
||||
height: 44,
|
||||
borderRadius: 14,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginRight: spacing.md,
|
||||
},
|
||||
menuContent: {
|
||||
flex: 1,
|
||||
},
|
||||
menuTitle: {
|
||||
fontSize: typography.fontSize.base,
|
||||
fontWeight: '600',
|
||||
color: colors.me.text,
|
||||
marginBottom: 2,
|
||||
},
|
||||
menuSubtitle: {
|
||||
fontSize: typography.fontSize.sm,
|
||||
color: colors.me.textSecondary,
|
||||
},
|
||||
// Abandon Button
|
||||
abandonButton: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: spacing.sm,
|
||||
backgroundColor: '#FFE5E5',
|
||||
borderRadius: borderRadius.xl,
|
||||
paddingVertical: spacing.lg,
|
||||
marginBottom: spacing.lg,
|
||||
marginTop: spacing.sm,
|
||||
},
|
||||
abandonButtonText: {
|
||||
fontSize: typography.fontSize.base,
|
||||
fontWeight: '700',
|
||||
color: colors.nautical.coral,
|
||||
letterSpacing: typography.letterSpacing.wider,
|
||||
},
|
||||
// About Grid
|
||||
aboutGrid: {
|
||||
flexDirection: 'row',
|
||||
gap: spacing.sm,
|
||||
marginBottom: spacing.xl,
|
||||
},
|
||||
aboutCard: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.me.cardBackground,
|
||||
borderRadius: borderRadius.lg,
|
||||
padding: spacing.base,
|
||||
alignItems: 'center',
|
||||
...shadows.soft,
|
||||
},
|
||||
aboutTitle: {
|
||||
fontSize: typography.fontSize.sm,
|
||||
fontWeight: '600',
|
||||
color: colors.me.text,
|
||||
textAlign: 'center',
|
||||
marginTop: spacing.sm,
|
||||
marginBottom: spacing.xs,
|
||||
},
|
||||
// Footer
|
||||
footer: {
|
||||
alignItems: 'center',
|
||||
marginTop: spacing.lg,
|
||||
marginBottom: spacing.lg,
|
||||
},
|
||||
footerVersion: {
|
||||
fontSize: typography.fontSize.xs,
|
||||
fontWeight: '600',
|
||||
color: colors.me.textSecondary,
|
||||
letterSpacing: typography.letterSpacing.widest,
|
||||
marginBottom: spacing.md,
|
||||
opacity: 0.7,
|
||||
},
|
||||
footerTagline: {
|
||||
fontSize: typography.fontSize.sm,
|
||||
color: colors.me.textSecondary,
|
||||
fontStyle: 'italic',
|
||||
textAlign: 'center',
|
||||
lineHeight: typography.fontSize.sm * 1.6,
|
||||
paddingHorizontal: spacing.lg,
|
||||
},
|
||||
footerLinks: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginBottom: spacing.lg,
|
||||
},
|
||||
footerLink: {
|
||||
fontSize: typography.fontSize.sm,
|
||||
color: colors.me.primary,
|
||||
fontWeight: '500',
|
||||
},
|
||||
footerDot: {
|
||||
fontSize: typography.fontSize.sm,
|
||||
color: colors.me.textSecondary,
|
||||
marginHorizontal: spacing.sm,
|
||||
},
|
||||
// Modal
|
||||
modalOverlay: {
|
||||
flex: 1,
|
||||
backgroundColor: 'rgba(26, 58, 74, 0.6)',
|
||||
justifyContent: 'flex-end',
|
||||
},
|
||||
modalContent: {
|
||||
backgroundColor: colors.me.cardBackground,
|
||||
borderTopLeftRadius: borderRadius.xxl,
|
||||
borderTopRightRadius: borderRadius.xxl,
|
||||
padding: spacing.lg,
|
||||
maxHeight: '75%',
|
||||
},
|
||||
modalHandle: {
|
||||
width: 36,
|
||||
height: 4,
|
||||
backgroundColor: colors.me.cardBorder,
|
||||
borderRadius: 2,
|
||||
alignSelf: 'center',
|
||||
marginBottom: spacing.md,
|
||||
},
|
||||
modalHeader: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginBottom: spacing.md,
|
||||
paddingBottom: spacing.md,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.me.cardBorder,
|
||||
},
|
||||
modalIconContainer: {
|
||||
width: 44,
|
||||
height: 44,
|
||||
borderRadius: 14,
|
||||
backgroundColor: colors.nautical.lightMint,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginRight: spacing.md,
|
||||
},
|
||||
modalTitle: {
|
||||
fontSize: typography.fontSize.lg,
|
||||
fontWeight: '700',
|
||||
color: colors.me.text,
|
||||
},
|
||||
modalScroll: {
|
||||
marginBottom: spacing.md,
|
||||
},
|
||||
modalText: {
|
||||
fontSize: typography.fontSize.base,
|
||||
color: colors.me.text,
|
||||
lineHeight: typography.fontSize.base * 1.7,
|
||||
},
|
||||
closeButton: {
|
||||
paddingVertical: spacing.md,
|
||||
backgroundColor: colors.nautical.lightMint,
|
||||
borderRadius: borderRadius.lg,
|
||||
alignItems: 'center',
|
||||
},
|
||||
closeButtonText: {
|
||||
fontSize: typography.fontSize.base,
|
||||
color: colors.me.primary,
|
||||
fontWeight: '600',
|
||||
},
|
||||
});
|
||||
516
src/screens/SentinelScreen.tsx
Normal file
516
src/screens/SentinelScreen.tsx
Normal file
@@ -0,0 +1,516 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
ScrollView,
|
||||
TouchableOpacity,
|
||||
SafeAreaView,
|
||||
Animated,
|
||||
} from 'react-native';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import { Ionicons, Feather, MaterialCommunityIcons, FontAwesome5 } from '@expo/vector-icons';
|
||||
import { colors, typography, spacing, borderRadius, shadows } from '../theme/colors';
|
||||
import { SystemStatus, KillSwitchLog } from '../types';
|
||||
|
||||
// Status configuration with nautical theme
|
||||
const statusConfig: Record<SystemStatus, {
|
||||
color: string;
|
||||
label: string;
|
||||
icon: string;
|
||||
description: string;
|
||||
gradientColors: [string, string];
|
||||
}> = {
|
||||
normal: {
|
||||
color: colors.sentinel.statusNormal,
|
||||
label: 'ALL CLEAR',
|
||||
icon: 'checkmark-circle',
|
||||
description: 'The lighthouse burns bright. All systems nominal.',
|
||||
gradientColors: ['#6BBF8A', '#4A9F6A'],
|
||||
},
|
||||
warning: {
|
||||
color: colors.sentinel.statusWarning,
|
||||
label: 'STORM WARNING',
|
||||
icon: 'warning',
|
||||
description: 'Anomaly detected. Captain\'s attention required.',
|
||||
gradientColors: ['#E5B873', '#C99953'],
|
||||
},
|
||||
releasing: {
|
||||
color: colors.sentinel.statusCritical,
|
||||
label: 'RELEASE ACTIVE',
|
||||
icon: 'alert-circle',
|
||||
description: 'Legacy release protocol initiated.',
|
||||
gradientColors: ['#E57373', '#C55353'],
|
||||
},
|
||||
};
|
||||
|
||||
// Mock data
|
||||
const initialLogs: KillSwitchLog[] = [
|
||||
{
|
||||
id: '1',
|
||||
action: 'HEARTBEAT_CONFIRMED',
|
||||
timestamp: new Date('2024-01-18T09:30:00'),
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
action: 'SUBSCRIPTION_VERIFIED',
|
||||
timestamp: new Date('2024-01-17T00:00:00'),
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
action: 'JOURNAL_ACTIVITY',
|
||||
timestamp: new Date('2024-01-16T15:42:00'),
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
action: 'HEARTBEAT_CONFIRMED',
|
||||
timestamp: new Date('2024-01-15T11:20:00'),
|
||||
},
|
||||
];
|
||||
|
||||
export default function SentinelScreen() {
|
||||
const [status, setStatus] = useState<SystemStatus>('normal');
|
||||
const [lastSubscriptionCheck] = useState(new Date('2024-01-18T00:00:00'));
|
||||
const [lastFlowActivity] = useState(new Date('2024-01-18T10:30:00'));
|
||||
const [logs, setLogs] = useState<KillSwitchLog[]>(initialLogs);
|
||||
const [pulseAnim] = useState(new Animated.Value(1));
|
||||
const [glowAnim] = useState(new Animated.Value(0.5));
|
||||
const [rotateAnim] = useState(new Animated.Value(0));
|
||||
|
||||
useEffect(() => {
|
||||
// Pulse animation
|
||||
Animated.loop(
|
||||
Animated.sequence([
|
||||
Animated.timing(pulseAnim, {
|
||||
toValue: 1.06,
|
||||
duration: 1200,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(pulseAnim, {
|
||||
toValue: 1,
|
||||
duration: 1200,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
])
|
||||
).start();
|
||||
|
||||
// Glow animation
|
||||
Animated.loop(
|
||||
Animated.sequence([
|
||||
Animated.timing(glowAnim, {
|
||||
toValue: 1,
|
||||
duration: 1500,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(glowAnim, {
|
||||
toValue: 0.5,
|
||||
duration: 1500,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
])
|
||||
).start();
|
||||
|
||||
// Slow rotate for ship wheel
|
||||
Animated.loop(
|
||||
Animated.timing(rotateAnim, {
|
||||
toValue: 1,
|
||||
duration: 30000,
|
||||
useNativeDriver: true,
|
||||
})
|
||||
).start();
|
||||
}, []);
|
||||
|
||||
const handleHeartbeat = () => {
|
||||
// Animate pulse
|
||||
Animated.sequence([
|
||||
Animated.timing(pulseAnim, {
|
||||
toValue: 1.15,
|
||||
duration: 150,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(pulseAnim, {
|
||||
toValue: 1,
|
||||
duration: 150,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
]).start();
|
||||
|
||||
// Add new log
|
||||
const newLog: KillSwitchLog = {
|
||||
id: Date.now().toString(),
|
||||
action: 'HEARTBEAT_CONFIRMED',
|
||||
timestamp: new Date(),
|
||||
};
|
||||
setLogs([newLog, ...logs]);
|
||||
|
||||
// Reset status if warning
|
||||
if (status === 'warning') {
|
||||
setStatus('normal');
|
||||
}
|
||||
};
|
||||
|
||||
const formatDateTime = (date: Date) => {
|
||||
return date.toLocaleString('en-US', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
};
|
||||
|
||||
const formatTimeAgo = (date: Date) => {
|
||||
const now = new Date();
|
||||
const diff = now.getTime() - date.getTime();
|
||||
const hours = Math.floor(diff / (1000 * 60 * 60));
|
||||
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
|
||||
|
||||
if (hours > 24) {
|
||||
const days = Math.floor(hours / 24);
|
||||
return `${days} days ago`;
|
||||
}
|
||||
if (hours > 0) {
|
||||
return `${hours}h ${minutes}m ago`;
|
||||
}
|
||||
return `${minutes}m ago`;
|
||||
};
|
||||
|
||||
const currentStatus = statusConfig[status];
|
||||
const spin = rotateAnim.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: ['0deg', '360deg'],
|
||||
});
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<LinearGradient
|
||||
colors={[colors.sentinel.backgroundGradientStart, colors.sentinel.backgroundGradientEnd]}
|
||||
style={styles.gradient}
|
||||
>
|
||||
<SafeAreaView style={styles.safeArea}>
|
||||
<ScrollView
|
||||
style={styles.scrollView}
|
||||
showsVerticalScrollIndicator={false}
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
>
|
||||
{/* Header */}
|
||||
<View style={styles.header}>
|
||||
<View style={styles.headerTitleRow}>
|
||||
<FontAwesome5 name="anchor" size={24} color={colors.sentinel.primary} />
|
||||
<Text style={styles.title}>LIGHTHOUSE</Text>
|
||||
</View>
|
||||
<Text style={styles.subtitle}>The Watchful Guardian</Text>
|
||||
</View>
|
||||
|
||||
{/* Status Display */}
|
||||
<View style={styles.statusContainer}>
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.statusCircleOuter,
|
||||
{
|
||||
transform: [{ scale: pulseAnim }],
|
||||
opacity: glowAnim,
|
||||
backgroundColor: `${currentStatus.color}20`,
|
||||
}
|
||||
]}
|
||||
/>
|
||||
<Animated.View style={{ transform: [{ scale: pulseAnim }] }}>
|
||||
<LinearGradient
|
||||
colors={currentStatus.gradientColors}
|
||||
style={styles.statusCircle}
|
||||
>
|
||||
<Ionicons name={currentStatus.icon as any} size={56} color="#fff" />
|
||||
</LinearGradient>
|
||||
</Animated.View>
|
||||
<Text style={[styles.statusLabel, { color: currentStatus.color }]}>
|
||||
{currentStatus.label}
|
||||
</Text>
|
||||
<Text style={styles.statusDescription}>
|
||||
{currentStatus.description}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Ship Wheel Watermark */}
|
||||
<View style={styles.wheelWatermark}>
|
||||
<Animated.View style={{ transform: [{ rotate: spin }] }}>
|
||||
<MaterialCommunityIcons
|
||||
name="ship-wheel"
|
||||
size={200}
|
||||
color={colors.sentinel.primary}
|
||||
style={{ opacity: 0.03 }}
|
||||
/>
|
||||
</Animated.View>
|
||||
</View>
|
||||
|
||||
{/* Metrics Grid */}
|
||||
<View style={styles.metricsGrid}>
|
||||
<View style={styles.metricCard}>
|
||||
<View style={styles.metricIconContainer}>
|
||||
<FontAwesome5 name="anchor" size={16} color={colors.sentinel.primary} />
|
||||
</View>
|
||||
<Text style={styles.metricLabel}>SUBSCRIPTION</Text>
|
||||
<Text style={styles.metricValue}>
|
||||
{formatTimeAgo(lastSubscriptionCheck)}
|
||||
</Text>
|
||||
<Text style={styles.metricTime}>
|
||||
{formatDateTime(lastSubscriptionCheck)}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.metricCard}>
|
||||
<View style={styles.metricIconContainer}>
|
||||
<Feather name="edit-3" size={16} color={colors.sentinel.primary} />
|
||||
</View>
|
||||
<Text style={styles.metricLabel}>LAST JOURNAL</Text>
|
||||
<Text style={styles.metricValue}>
|
||||
{formatTimeAgo(lastFlowActivity)}
|
||||
</Text>
|
||||
<Text style={styles.metricTime}>
|
||||
{formatDateTime(lastFlowActivity)}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Heartbeat Button */}
|
||||
<TouchableOpacity
|
||||
style={styles.heartbeatButton}
|
||||
onPress={handleHeartbeat}
|
||||
activeOpacity={0.9}
|
||||
>
|
||||
<LinearGradient
|
||||
colors={[colors.nautical.teal, colors.nautical.seafoam]}
|
||||
style={styles.heartbeatGradient}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 0 }}
|
||||
>
|
||||
<View style={styles.heartbeatContent}>
|
||||
<MaterialCommunityIcons name="lighthouse" size={32} color="#fff" />
|
||||
<View>
|
||||
<Text style={styles.heartbeatText}>SIGNAL THE WATCH</Text>
|
||||
<Text style={styles.heartbeatSubtext}>Confirm your presence, Captain</Text>
|
||||
</View>
|
||||
</View>
|
||||
</LinearGradient>
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* Watch Log */}
|
||||
<View style={styles.logsSection}>
|
||||
<View style={styles.logsSectionHeader}>
|
||||
<Feather name="activity" size={18} color={colors.sentinel.primary} />
|
||||
<Text style={styles.logsSectionTitle}>WATCH LOG</Text>
|
||||
</View>
|
||||
{logs.map((log) => (
|
||||
<View key={log.id} style={styles.logItem}>
|
||||
<View style={styles.logDot} />
|
||||
<View style={styles.logContent}>
|
||||
<Text style={styles.logAction}>{log.action}</Text>
|
||||
<Text style={styles.logTime}>
|
||||
{formatDateTime(log.timestamp)}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
</LinearGradient>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
gradient: {
|
||||
flex: 1,
|
||||
},
|
||||
safeArea: {
|
||||
flex: 1,
|
||||
},
|
||||
scrollView: {
|
||||
flex: 1,
|
||||
},
|
||||
scrollContent: {
|
||||
padding: spacing.lg,
|
||||
paddingBottom: 120,
|
||||
},
|
||||
header: {
|
||||
marginBottom: spacing.xl,
|
||||
},
|
||||
headerTitleRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: spacing.sm,
|
||||
marginBottom: spacing.xs,
|
||||
},
|
||||
title: {
|
||||
fontSize: typography.fontSize.xl,
|
||||
fontWeight: '700',
|
||||
color: colors.sentinel.text,
|
||||
letterSpacing: typography.letterSpacing.widest,
|
||||
fontFamily: typography.fontFamily.serif,
|
||||
},
|
||||
subtitle: {
|
||||
fontSize: typography.fontSize.sm,
|
||||
color: colors.sentinel.textSecondary,
|
||||
marginLeft: spacing.xl + spacing.sm,
|
||||
fontStyle: 'italic',
|
||||
},
|
||||
statusContainer: {
|
||||
alignItems: 'center',
|
||||
paddingVertical: spacing.xl,
|
||||
marginBottom: spacing.lg,
|
||||
position: 'relative',
|
||||
},
|
||||
statusCircleOuter: {
|
||||
position: 'absolute',
|
||||
width: 170,
|
||||
height: 170,
|
||||
borderRadius: 85,
|
||||
},
|
||||
statusCircle: {
|
||||
width: 140,
|
||||
height: 140,
|
||||
borderRadius: 70,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginBottom: spacing.md,
|
||||
...shadows.glow,
|
||||
},
|
||||
statusLabel: {
|
||||
fontSize: typography.fontSize.xl,
|
||||
fontWeight: '700',
|
||||
letterSpacing: typography.letterSpacing.widest,
|
||||
marginBottom: spacing.sm,
|
||||
},
|
||||
statusDescription: {
|
||||
fontSize: typography.fontSize.base,
|
||||
color: colors.sentinel.textSecondary,
|
||||
textAlign: 'center',
|
||||
maxWidth: 280,
|
||||
fontStyle: 'italic',
|
||||
},
|
||||
wheelWatermark: {
|
||||
position: 'absolute',
|
||||
top: 200,
|
||||
right: -60,
|
||||
opacity: 0.5,
|
||||
},
|
||||
metricsGrid: {
|
||||
flexDirection: 'row',
|
||||
gap: spacing.md,
|
||||
marginBottom: spacing.lg,
|
||||
},
|
||||
metricCard: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.sentinel.cardBackground,
|
||||
borderRadius: borderRadius.xl,
|
||||
padding: spacing.base,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.sentinel.cardBorder,
|
||||
},
|
||||
metricIconContainer: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 20,
|
||||
backgroundColor: `${colors.sentinel.primary}15`,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginBottom: spacing.sm,
|
||||
},
|
||||
metricLabel: {
|
||||
fontSize: typography.fontSize.xs,
|
||||
color: colors.sentinel.textSecondary,
|
||||
letterSpacing: typography.letterSpacing.wide,
|
||||
marginBottom: spacing.xs,
|
||||
fontWeight: '600',
|
||||
},
|
||||
metricValue: {
|
||||
fontSize: typography.fontSize.md,
|
||||
color: colors.sentinel.text,
|
||||
fontWeight: '700',
|
||||
marginBottom: spacing.xs,
|
||||
},
|
||||
metricTime: {
|
||||
fontSize: typography.fontSize.xs,
|
||||
color: colors.sentinel.textSecondary,
|
||||
fontFamily: typography.fontFamily.mono,
|
||||
},
|
||||
heartbeatButton: {
|
||||
borderRadius: borderRadius.xl,
|
||||
overflow: 'hidden',
|
||||
marginBottom: spacing.xl,
|
||||
...shadows.medium,
|
||||
},
|
||||
heartbeatGradient: {
|
||||
padding: spacing.lg,
|
||||
},
|
||||
heartbeatContent: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: spacing.md,
|
||||
},
|
||||
heartbeatText: {
|
||||
fontSize: typography.fontSize.lg,
|
||||
fontWeight: '700',
|
||||
color: '#fff',
|
||||
letterSpacing: typography.letterSpacing.wider,
|
||||
},
|
||||
heartbeatSubtext: {
|
||||
fontSize: typography.fontSize.sm,
|
||||
color: 'rgba(255, 255, 255, 0.8)',
|
||||
marginTop: 2,
|
||||
fontStyle: 'italic',
|
||||
},
|
||||
logsSection: {
|
||||
backgroundColor: colors.sentinel.cardBackground,
|
||||
borderRadius: borderRadius.xl,
|
||||
padding: spacing.base,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.sentinel.cardBorder,
|
||||
},
|
||||
logsSectionHeader: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: spacing.sm,
|
||||
marginBottom: spacing.md,
|
||||
paddingBottom: spacing.sm,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.sentinel.cardBorder,
|
||||
},
|
||||
logsSectionTitle: {
|
||||
fontSize: typography.fontSize.xs,
|
||||
color: colors.sentinel.textSecondary,
|
||||
letterSpacing: typography.letterSpacing.widest,
|
||||
fontWeight: '700',
|
||||
},
|
||||
logItem: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-start',
|
||||
paddingVertical: spacing.sm,
|
||||
},
|
||||
logDot: {
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
backgroundColor: colors.sentinel.primary,
|
||||
marginTop: 6,
|
||||
marginRight: spacing.md,
|
||||
},
|
||||
logContent: {
|
||||
flex: 1,
|
||||
},
|
||||
logAction: {
|
||||
fontSize: typography.fontSize.sm,
|
||||
color: colors.sentinel.text,
|
||||
fontFamily: typography.fontFamily.mono,
|
||||
fontWeight: '500',
|
||||
marginBottom: 2,
|
||||
},
|
||||
logTime: {
|
||||
fontSize: typography.fontSize.xs,
|
||||
color: colors.sentinel.textSecondary,
|
||||
fontFamily: typography.fontFamily.mono,
|
||||
},
|
||||
});
|
||||
748
src/screens/VaultScreen.tsx
Normal file
748
src/screens/VaultScreen.tsx
Normal file
@@ -0,0 +1,748 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
ScrollView,
|
||||
TouchableOpacity,
|
||||
Modal,
|
||||
TextInput,
|
||||
SafeAreaView,
|
||||
Animated,
|
||||
} from 'react-native';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import { Ionicons, Feather, MaterialCommunityIcons, FontAwesome5 } from '@expo/vector-icons';
|
||||
import { colors, typography, spacing, borderRadius, shadows } from '../theme/colors';
|
||||
import { VaultAsset, VaultAssetType } from '../types';
|
||||
import BiometricModal from '../components/common/BiometricModal';
|
||||
|
||||
// Asset type configuration with nautical theme
|
||||
const assetTypeConfig: Record<VaultAssetType, { icon: string; iconType: 'ionicons' | 'feather' | 'material' | 'fontawesome5'; label: string }> = {
|
||||
game_account: { icon: 'gamepad', iconType: 'fontawesome5', label: 'Digital Account' },
|
||||
private_key: { icon: 'key', iconType: 'fontawesome5', label: 'Secret Key' },
|
||||
document: { icon: 'scroll', iconType: 'fontawesome5', label: 'Document' },
|
||||
photo: { icon: 'image', iconType: 'ionicons', label: 'Sealed Photo' },
|
||||
will: { icon: 'file-signature', iconType: 'fontawesome5', label: 'Testament' },
|
||||
custom: { icon: 'gem', iconType: 'fontawesome5', label: 'Treasure' },
|
||||
};
|
||||
|
||||
// Mock data
|
||||
const initialAssets: VaultAsset[] = [
|
||||
{
|
||||
id: '1',
|
||||
type: 'private_key',
|
||||
label: 'ETH Main Wallet Key',
|
||||
createdAt: new Date('2024-01-10'),
|
||||
updatedAt: new Date('2024-01-10'),
|
||||
isEncrypted: true,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
type: 'game_account',
|
||||
label: 'Steam Account Credentials',
|
||||
createdAt: new Date('2024-01-08'),
|
||||
updatedAt: new Date('2024-01-08'),
|
||||
isEncrypted: true,
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
type: 'document',
|
||||
label: 'Insurance Policy Scan',
|
||||
createdAt: new Date('2024-01-05'),
|
||||
updatedAt: new Date('2024-01-05'),
|
||||
isEncrypted: true,
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
type: 'will',
|
||||
label: 'Testament Draft v2',
|
||||
createdAt: new Date('2024-01-02'),
|
||||
updatedAt: new Date('2024-01-15'),
|
||||
isEncrypted: true,
|
||||
},
|
||||
];
|
||||
|
||||
const renderAssetTypeIcon = (config: typeof assetTypeConfig[VaultAssetType], size: number, color: string) => {
|
||||
switch (config.iconType) {
|
||||
case 'ionicons':
|
||||
return <Ionicons name={config.icon as any} size={size} color={color} />;
|
||||
case 'feather':
|
||||
return <Feather name={config.icon as any} size={size} color={color} />;
|
||||
case 'material':
|
||||
return <MaterialCommunityIcons name={config.icon as any} size={size} color={color} />;
|
||||
case 'fontawesome5':
|
||||
return <FontAwesome5 name={config.icon as any} size={size} color={color} />;
|
||||
}
|
||||
};
|
||||
|
||||
export default function VaultScreen() {
|
||||
const [isUnlocked, setIsUnlocked] = useState(false);
|
||||
const [showBiometric, setShowBiometric] = useState(false);
|
||||
const [assets, setAssets] = useState<VaultAsset[]>(initialAssets);
|
||||
const [showAddModal, setShowAddModal] = useState(false);
|
||||
const [selectedType, setSelectedType] = useState<VaultAssetType>('custom');
|
||||
const [newLabel, setNewLabel] = useState('');
|
||||
const [showUploadSuccess, setShowUploadSuccess] = useState(false);
|
||||
const [fadeAnim] = useState(new Animated.Value(0));
|
||||
const [pulseAnim] = useState(new Animated.Value(1));
|
||||
|
||||
useEffect(() => {
|
||||
if (!isUnlocked) {
|
||||
const timer = setTimeout(() => {
|
||||
setShowBiometric(true);
|
||||
}, 500);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (isUnlocked) {
|
||||
Animated.timing(fadeAnim, {
|
||||
toValue: 1,
|
||||
duration: 600,
|
||||
useNativeDriver: true,
|
||||
}).start();
|
||||
}
|
||||
}, [isUnlocked]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isUnlocked) {
|
||||
Animated.loop(
|
||||
Animated.sequence([
|
||||
Animated.timing(pulseAnim, {
|
||||
toValue: 1.05,
|
||||
duration: 1500,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(pulseAnim, {
|
||||
toValue: 1,
|
||||
duration: 1500,
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
])
|
||||
).start();
|
||||
}
|
||||
}, [isUnlocked]);
|
||||
|
||||
const handleUnlock = () => {
|
||||
setShowBiometric(false);
|
||||
setIsUnlocked(true);
|
||||
};
|
||||
|
||||
const handleAddAsset = () => {
|
||||
if (!newLabel.trim()) return;
|
||||
|
||||
const newAsset: VaultAsset = {
|
||||
id: Date.now().toString(),
|
||||
type: selectedType,
|
||||
label: newLabel,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
isEncrypted: true,
|
||||
};
|
||||
|
||||
setAssets([newAsset, ...assets]);
|
||||
setNewLabel('');
|
||||
setSelectedType('custom');
|
||||
setShowAddModal(false);
|
||||
|
||||
setShowUploadSuccess(true);
|
||||
setTimeout(() => setShowUploadSuccess(false), 2500);
|
||||
};
|
||||
|
||||
const formatDate = (date: Date) => {
|
||||
return date.toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
});
|
||||
};
|
||||
|
||||
// Lock screen
|
||||
if (!isUnlocked) {
|
||||
return (
|
||||
<View style={styles.lockContainer}>
|
||||
<LinearGradient
|
||||
colors={[colors.vault.backgroundGradientStart, colors.vault.backgroundGradientEnd]}
|
||||
style={styles.lockGradient}
|
||||
>
|
||||
<SafeAreaView style={styles.lockSafeArea}>
|
||||
<View style={styles.lockContent}>
|
||||
<Animated.View style={[styles.lockIconContainer, { transform: [{ scale: pulseAnim }] }]}>
|
||||
<LinearGradient
|
||||
colors={[colors.nautical.teal, colors.nautical.deepTeal]}
|
||||
style={styles.lockIconGradient}
|
||||
>
|
||||
<MaterialCommunityIcons name="treasure-chest" size={64} color={colors.vault.primary} />
|
||||
</LinearGradient>
|
||||
</Animated.View>
|
||||
|
||||
<Text style={styles.lockTitle}>THE DEEP VAULT</Text>
|
||||
<Text style={styles.lockSubtitle}>Where treasures rest in silence</Text>
|
||||
|
||||
<View style={styles.waveContainer}>
|
||||
<MaterialCommunityIcons name="waves" size={48} color={colors.vault.secondary} style={{ opacity: 0.3 }} />
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
style={styles.unlockButton}
|
||||
onPress={() => setShowBiometric(true)}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<LinearGradient
|
||||
colors={[colors.vault.primary, colors.vault.secondary]}
|
||||
style={styles.unlockButtonGradient}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 0 }}
|
||||
>
|
||||
<Ionicons name="finger-print" size={20} color={colors.vault.background} />
|
||||
<Text style={styles.unlockButtonText}>Captain's Verification</Text>
|
||||
</LinearGradient>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
</LinearGradient>
|
||||
|
||||
<BiometricModal
|
||||
visible={showBiometric}
|
||||
onSuccess={handleUnlock}
|
||||
onCancel={() => setShowBiometric(false)}
|
||||
title="Enter the Vault"
|
||||
message="Verify your identity to access your treasures"
|
||||
isDark
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<LinearGradient
|
||||
colors={[colors.vault.backgroundGradientStart, colors.vault.backgroundGradientEnd]}
|
||||
style={styles.gradient}
|
||||
>
|
||||
<SafeAreaView style={styles.safeArea}>
|
||||
<Animated.View style={[styles.content, { opacity: fadeAnim }]}>
|
||||
{/* Header */}
|
||||
<View style={styles.header}>
|
||||
<View style={styles.headerTop}>
|
||||
<View style={styles.headerTitleRow}>
|
||||
<MaterialCommunityIcons name="treasure-chest" size={26} color={colors.vault.primary} />
|
||||
<Text style={styles.title}>THE VAULT</Text>
|
||||
</View>
|
||||
<View style={styles.securityBadge}>
|
||||
<Ionicons name="shield-checkmark" size={14} color={colors.vault.success} />
|
||||
<Text style={styles.securityText}>SEALED</Text>
|
||||
</View>
|
||||
</View>
|
||||
<Text style={styles.subtitle}>
|
||||
{assets.length} treasures · Encrypted at rest
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Asset List */}
|
||||
<ScrollView
|
||||
style={styles.assetList}
|
||||
showsVerticalScrollIndicator={false}
|
||||
contentContainerStyle={styles.assetListContent}
|
||||
>
|
||||
{assets.map((asset) => {
|
||||
const config = assetTypeConfig[asset.type];
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={asset.id}
|
||||
style={styles.assetCard}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<View style={styles.assetIconContainer}>
|
||||
{renderAssetTypeIcon(config, 22, colors.vault.primary)}
|
||||
</View>
|
||||
<View style={styles.assetInfo}>
|
||||
<Text style={styles.assetType}>{config.label}</Text>
|
||||
<Text style={styles.assetLabel}>{asset.label}</Text>
|
||||
<View style={styles.assetMetaRow}>
|
||||
<Feather name="clock" size={10} color={colors.vault.textSecondary} />
|
||||
<Text style={styles.assetMeta}>Sealed {formatDate(asset.createdAt)}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.encryptedBadge}>
|
||||
<MaterialCommunityIcons name="lock" size={16} color="#fff" />
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
<View style={{ height: 100 }} />
|
||||
</ScrollView>
|
||||
|
||||
{/* Add Button */}
|
||||
<TouchableOpacity
|
||||
style={styles.addButton}
|
||||
onPress={() => setShowAddModal(true)}
|
||||
activeOpacity={0.9}
|
||||
>
|
||||
<LinearGradient
|
||||
colors={[colors.vault.primary, colors.vault.secondary]}
|
||||
style={styles.addButtonGradient}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 0 }}
|
||||
>
|
||||
<FontAwesome5 name="plus" size={16} color={colors.vault.background} />
|
||||
<Text style={styles.addButtonText}>Add Treasure</Text>
|
||||
</LinearGradient>
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* Upload Success Toast */}
|
||||
{showUploadSuccess && (
|
||||
<Animated.View style={styles.successToast}>
|
||||
<Ionicons name="checkmark-circle" size={20} color="#fff" />
|
||||
<Text style={styles.successText}>Treasure sealed successfully</Text>
|
||||
</Animated.View>
|
||||
)}
|
||||
</Animated.View>
|
||||
</SafeAreaView>
|
||||
</LinearGradient>
|
||||
|
||||
{/* Add Modal */}
|
||||
<Modal
|
||||
visible={showAddModal}
|
||||
animationType="slide"
|
||||
transparent
|
||||
onRequestClose={() => setShowAddModal(false)}
|
||||
>
|
||||
<View style={styles.modalOverlay}>
|
||||
<View style={styles.modalContent}>
|
||||
<View style={styles.modalHandle} />
|
||||
<View style={styles.modalHeader}>
|
||||
<FontAwesome5 name="gem" size={22} color={colors.nautical.teal} />
|
||||
<Text style={styles.modalTitle}>Add New Treasure</Text>
|
||||
</View>
|
||||
|
||||
<Text style={styles.modalLabel}>TREASURE TYPE</Text>
|
||||
<ScrollView
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
style={styles.typeScroll}
|
||||
contentContainerStyle={styles.typeScrollContent}
|
||||
>
|
||||
{(Object.keys(assetTypeConfig) as VaultAssetType[]).map((type) => {
|
||||
const config = assetTypeConfig[type];
|
||||
const isSelected = selectedType === type;
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={type}
|
||||
style={[styles.typeButton, isSelected && styles.typeButtonActive]}
|
||||
onPress={() => setSelectedType(type)}
|
||||
>
|
||||
<View style={[styles.typeIconContainer, isSelected && styles.typeIconContainerActive]}>
|
||||
{renderAssetTypeIcon(config, 22, isSelected ? colors.nautical.teal : colors.nautical.sage)}
|
||||
</View>
|
||||
<Text style={[styles.typeButtonLabel, isSelected && styles.typeButtonLabelActive]}>
|
||||
{config.label}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
</ScrollView>
|
||||
|
||||
<Text style={styles.modalLabel}>TREASURE NAME</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
placeholder="e.g., Main wallet mnemonic"
|
||||
placeholderTextColor={colors.nautical.sage}
|
||||
value={newLabel}
|
||||
onChangeText={setNewLabel}
|
||||
/>
|
||||
|
||||
<View style={styles.encryptionNote}>
|
||||
<MaterialCommunityIcons name="anchor" size={16} color={colors.nautical.teal} />
|
||||
<Text style={styles.encryptionNoteText}>
|
||||
Your treasure will be encrypted and sealed. Original data will be securely destroyed.
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.modalButtons}>
|
||||
<TouchableOpacity
|
||||
style={styles.cancelButton}
|
||||
onPress={() => {
|
||||
setShowAddModal(false);
|
||||
setNewLabel('');
|
||||
}}
|
||||
>
|
||||
<Text style={styles.cancelButtonText}>Cancel</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={styles.confirmButton}
|
||||
onPress={handleAddAsset}
|
||||
>
|
||||
<LinearGradient
|
||||
colors={[colors.nautical.teal, colors.nautical.seafoam]}
|
||||
style={styles.confirmButtonGradient}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 0 }}
|
||||
>
|
||||
<MaterialCommunityIcons name="lock" size={18} color="#fff" />
|
||||
<Text style={styles.confirmButtonText}>Seal Treasure</Text>
|
||||
</LinearGradient>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
gradient: {
|
||||
flex: 1,
|
||||
},
|
||||
safeArea: {
|
||||
flex: 1,
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
},
|
||||
lockContainer: {
|
||||
flex: 1,
|
||||
},
|
||||
lockGradient: {
|
||||
flex: 1,
|
||||
},
|
||||
lockSafeArea: {
|
||||
flex: 1,
|
||||
},
|
||||
lockContent: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: spacing.xl,
|
||||
},
|
||||
lockIconContainer: {
|
||||
marginBottom: spacing.lg,
|
||||
},
|
||||
lockIconGradient: {
|
||||
width: 130,
|
||||
height: 130,
|
||||
borderRadius: 65,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
...shadows.glow,
|
||||
},
|
||||
lockTitle: {
|
||||
fontSize: typography.fontSize.xxl,
|
||||
fontWeight: '700',
|
||||
color: colors.vault.text,
|
||||
letterSpacing: typography.letterSpacing.widest,
|
||||
marginBottom: spacing.sm,
|
||||
fontFamily: typography.fontFamily.serif,
|
||||
},
|
||||
lockSubtitle: {
|
||||
fontSize: typography.fontSize.base,
|
||||
color: colors.vault.textSecondary,
|
||||
marginBottom: spacing.xl,
|
||||
textAlign: 'center',
|
||||
fontStyle: 'italic',
|
||||
},
|
||||
waveContainer: {
|
||||
marginBottom: spacing.xl,
|
||||
},
|
||||
unlockButton: {
|
||||
borderRadius: borderRadius.lg,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
unlockButtonGradient: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingVertical: spacing.md,
|
||||
paddingHorizontal: spacing.xl,
|
||||
gap: spacing.sm,
|
||||
},
|
||||
unlockButtonText: {
|
||||
fontSize: typography.fontSize.base,
|
||||
color: colors.vault.background,
|
||||
fontWeight: '600',
|
||||
},
|
||||
header: {
|
||||
paddingHorizontal: spacing.lg,
|
||||
paddingTop: spacing.lg,
|
||||
paddingBottom: spacing.md,
|
||||
},
|
||||
headerTop: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: spacing.xs,
|
||||
},
|
||||
headerTitleRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: spacing.sm,
|
||||
},
|
||||
title: {
|
||||
fontSize: typography.fontSize.xl,
|
||||
fontWeight: '700',
|
||||
color: colors.vault.text,
|
||||
letterSpacing: typography.letterSpacing.wider,
|
||||
fontFamily: typography.fontFamily.serif,
|
||||
},
|
||||
securityBadge: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: `${colors.vault.success}20`,
|
||||
paddingHorizontal: spacing.sm,
|
||||
paddingVertical: spacing.xs,
|
||||
borderRadius: borderRadius.full,
|
||||
gap: spacing.xs,
|
||||
},
|
||||
securityText: {
|
||||
fontSize: typography.fontSize.xs,
|
||||
color: colors.vault.success,
|
||||
fontWeight: '700',
|
||||
letterSpacing: 1,
|
||||
},
|
||||
subtitle: {
|
||||
fontSize: typography.fontSize.sm,
|
||||
color: colors.vault.textSecondary,
|
||||
},
|
||||
assetList: {
|
||||
flex: 1,
|
||||
},
|
||||
assetListContent: {
|
||||
padding: spacing.lg,
|
||||
paddingTop: spacing.sm,
|
||||
},
|
||||
assetCard: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: colors.vault.cardBackground,
|
||||
borderRadius: borderRadius.xl,
|
||||
padding: spacing.base,
|
||||
marginBottom: spacing.md,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.vault.cardBorder,
|
||||
},
|
||||
assetIconContainer: {
|
||||
width: 52,
|
||||
height: 52,
|
||||
borderRadius: 26,
|
||||
backgroundColor: `${colors.vault.primary}15`,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginRight: spacing.base,
|
||||
},
|
||||
assetInfo: {
|
||||
flex: 1,
|
||||
},
|
||||
assetType: {
|
||||
fontSize: typography.fontSize.xs,
|
||||
color: colors.vault.textSecondary,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: 1,
|
||||
marginBottom: 2,
|
||||
fontWeight: '600',
|
||||
},
|
||||
assetLabel: {
|
||||
fontSize: typography.fontSize.base,
|
||||
color: colors.vault.text,
|
||||
fontWeight: '600',
|
||||
marginBottom: 4,
|
||||
},
|
||||
assetMetaRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: spacing.xs,
|
||||
},
|
||||
assetMeta: {
|
||||
fontSize: typography.fontSize.xs,
|
||||
color: colors.vault.textSecondary,
|
||||
},
|
||||
encryptedBadge: {
|
||||
width: 36,
|
||||
height: 36,
|
||||
borderRadius: 18,
|
||||
backgroundColor: colors.vault.success,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
addButton: {
|
||||
position: 'absolute',
|
||||
bottom: 100,
|
||||
left: spacing.lg,
|
||||
right: spacing.lg,
|
||||
borderRadius: borderRadius.lg,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
addButtonGradient: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingVertical: spacing.md,
|
||||
gap: spacing.sm,
|
||||
},
|
||||
addButtonText: {
|
||||
fontSize: typography.fontSize.base,
|
||||
color: colors.vault.background,
|
||||
fontWeight: '700',
|
||||
},
|
||||
successToast: {
|
||||
position: 'absolute',
|
||||
bottom: 170,
|
||||
left: spacing.lg,
|
||||
right: spacing.lg,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: colors.vault.success,
|
||||
paddingVertical: spacing.md,
|
||||
borderRadius: borderRadius.lg,
|
||||
gap: spacing.sm,
|
||||
},
|
||||
successText: {
|
||||
fontSize: typography.fontSize.base,
|
||||
color: '#fff',
|
||||
fontWeight: '600',
|
||||
},
|
||||
modalOverlay: {
|
||||
flex: 1,
|
||||
backgroundColor: 'rgba(26, 58, 74, 0.8)',
|
||||
justifyContent: 'flex-end',
|
||||
},
|
||||
modalContent: {
|
||||
backgroundColor: colors.nautical.cream,
|
||||
borderTopLeftRadius: borderRadius.xxl,
|
||||
borderTopRightRadius: borderRadius.xxl,
|
||||
padding: spacing.lg,
|
||||
paddingBottom: spacing.xxl,
|
||||
},
|
||||
modalHandle: {
|
||||
width: 40,
|
||||
height: 4,
|
||||
backgroundColor: colors.nautical.lightMint,
|
||||
borderRadius: 2,
|
||||
alignSelf: 'center',
|
||||
marginBottom: spacing.lg,
|
||||
},
|
||||
modalHeader: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: spacing.sm,
|
||||
marginBottom: spacing.lg,
|
||||
},
|
||||
modalTitle: {
|
||||
fontSize: typography.fontSize.lg,
|
||||
fontWeight: '600',
|
||||
color: colors.nautical.navy,
|
||||
},
|
||||
modalLabel: {
|
||||
fontSize: typography.fontSize.xs,
|
||||
color: colors.nautical.sage,
|
||||
marginBottom: spacing.sm,
|
||||
letterSpacing: typography.letterSpacing.wider,
|
||||
fontWeight: '600',
|
||||
},
|
||||
typeScroll: {
|
||||
marginBottom: spacing.lg,
|
||||
},
|
||||
typeScrollContent: {
|
||||
gap: spacing.sm,
|
||||
},
|
||||
typeButton: {
|
||||
alignItems: 'center',
|
||||
paddingVertical: spacing.sm,
|
||||
paddingHorizontal: spacing.base,
|
||||
borderRadius: borderRadius.lg,
|
||||
backgroundColor: colors.nautical.paleAqua,
|
||||
borderWidth: 2,
|
||||
borderColor: 'transparent',
|
||||
minWidth: 100,
|
||||
},
|
||||
typeButtonActive: {
|
||||
borderColor: colors.nautical.teal,
|
||||
backgroundColor: colors.nautical.lightMint,
|
||||
},
|
||||
typeIconContainer: {
|
||||
width: 44,
|
||||
height: 44,
|
||||
borderRadius: 22,
|
||||
backgroundColor: colors.nautical.cream,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginBottom: spacing.xs,
|
||||
},
|
||||
typeIconContainerActive: {
|
||||
backgroundColor: `${colors.nautical.teal}15`,
|
||||
},
|
||||
typeButtonLabel: {
|
||||
fontSize: typography.fontSize.xs,
|
||||
color: colors.nautical.sage,
|
||||
textAlign: 'center',
|
||||
fontWeight: '500',
|
||||
},
|
||||
typeButtonLabelActive: {
|
||||
color: colors.nautical.teal,
|
||||
fontWeight: '600',
|
||||
},
|
||||
input: {
|
||||
backgroundColor: colors.nautical.paleAqua,
|
||||
borderRadius: borderRadius.lg,
|
||||
padding: spacing.base,
|
||||
fontSize: typography.fontSize.base,
|
||||
color: colors.nautical.navy,
|
||||
marginBottom: spacing.md,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.nautical.lightMint,
|
||||
},
|
||||
encryptionNote: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: colors.nautical.lightMint,
|
||||
borderRadius: borderRadius.lg,
|
||||
padding: spacing.md,
|
||||
marginBottom: spacing.lg,
|
||||
gap: spacing.sm,
|
||||
},
|
||||
encryptionNoteText: {
|
||||
flex: 1,
|
||||
fontSize: typography.fontSize.sm,
|
||||
color: colors.nautical.teal,
|
||||
lineHeight: typography.fontSize.sm * 1.4,
|
||||
},
|
||||
modalButtons: {
|
||||
flexDirection: 'row',
|
||||
gap: spacing.md,
|
||||
},
|
||||
cancelButton: {
|
||||
flex: 1,
|
||||
paddingVertical: spacing.md,
|
||||
borderRadius: borderRadius.lg,
|
||||
backgroundColor: colors.nautical.paleAqua,
|
||||
alignItems: 'center',
|
||||
borderWidth: 1,
|
||||
borderColor: colors.nautical.lightMint,
|
||||
},
|
||||
cancelButtonText: {
|
||||
fontSize: typography.fontSize.base,
|
||||
color: colors.nautical.sage,
|
||||
fontWeight: '600',
|
||||
},
|
||||
confirmButton: {
|
||||
flex: 1,
|
||||
borderRadius: borderRadius.lg,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
confirmButtonGradient: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingVertical: spacing.md,
|
||||
gap: spacing.sm,
|
||||
},
|
||||
confirmButtonText: {
|
||||
fontSize: typography.fontSize.base,
|
||||
color: '#fff',
|
||||
fontWeight: '600',
|
||||
},
|
||||
});
|
||||
180
src/theme/colors.ts
Normal file
180
src/theme/colors.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
// Sentinel App - Captain's Sanctum Theme
|
||||
// Elegant nautical style with mint and teal
|
||||
|
||||
export const colors = {
|
||||
// Base colors
|
||||
white: '#FFFFFF',
|
||||
black: '#1A2F3A',
|
||||
|
||||
// Nautical palette
|
||||
nautical: {
|
||||
deepTeal: '#1B4D5C',
|
||||
teal: '#459E9E',
|
||||
seafoam: '#5BB5B5',
|
||||
mint: '#B8E0E5',
|
||||
lightMint: '#E0F2F2',
|
||||
paleAqua: '#F0F8F8',
|
||||
cream: '#F8FCFC',
|
||||
sand: '#F5F8F8',
|
||||
gold: '#D4A853',
|
||||
coral: '#E07070',
|
||||
navy: '#1A3A4A',
|
||||
sage: '#8CA5A5',
|
||||
},
|
||||
|
||||
// Flow - Captain's Journal
|
||||
flow: {
|
||||
background: '#E8F6F8',
|
||||
backgroundGradientStart: '#D4EEF2',
|
||||
backgroundGradientEnd: '#E8F6F8',
|
||||
cardBackground: '#FFFFFF',
|
||||
cardBorder: '#D4EEF2',
|
||||
primary: '#2A6B7C',
|
||||
secondary: '#3D8B9C',
|
||||
text: '#1A3A4A',
|
||||
textSecondary: '#5A7A8A',
|
||||
accent: '#1B4D5C',
|
||||
archived: '#E8F0F2',
|
||||
archivedText: '#7A9A9A',
|
||||
highlight: '#B8E0E5',
|
||||
},
|
||||
|
||||
// Vault - Ship's Vault
|
||||
vault: {
|
||||
background: '#1B4D5C',
|
||||
backgroundGradientStart: '#1A3A4A',
|
||||
backgroundGradientEnd: '#2A5A6C',
|
||||
cardBackground: '#234F5F',
|
||||
cardBorder: '#3A6A7A',
|
||||
primary: '#B8E0E5',
|
||||
secondary: '#7AB8C5',
|
||||
text: '#FFFFFF',
|
||||
textSecondary: '#9AC5D0',
|
||||
accent: '#D4EEF2',
|
||||
warning: '#E57373',
|
||||
success: '#6BBF8A',
|
||||
},
|
||||
|
||||
// Sentinel - Lighthouse Watch
|
||||
sentinel: {
|
||||
background: '#1A3A4A',
|
||||
backgroundGradientStart: '#152F3A',
|
||||
backgroundGradientEnd: '#1B4D5C',
|
||||
cardBackground: '#1F4555',
|
||||
cardBorder: '#2A5A6A',
|
||||
primary: '#B8E0E5',
|
||||
secondary: '#7AB8C5',
|
||||
text: '#FFFFFF',
|
||||
textSecondary: '#9AC5D0',
|
||||
statusNormal: '#6BBF8A',
|
||||
statusWarning: '#E5B873',
|
||||
statusCritical: '#E57373',
|
||||
},
|
||||
|
||||
// Heritage - Legacy Fleet
|
||||
heritage: {
|
||||
background: '#E8F6F8',
|
||||
backgroundGradientStart: '#D4EEF2',
|
||||
backgroundGradientEnd: '#E8F6F8',
|
||||
cardBackground: '#FFFFFF',
|
||||
cardBorder: '#D4EEF2',
|
||||
primary: '#2A6B7C',
|
||||
secondary: '#5A7A8A',
|
||||
text: '#1A3A4A',
|
||||
textSecondary: '#5A7A8A',
|
||||
accent: '#C9A962',
|
||||
confirmed: '#6BBF8A',
|
||||
pending: '#E5B873',
|
||||
},
|
||||
|
||||
// Me - Captain's Quarters
|
||||
me: {
|
||||
background: '#E8F6F8',
|
||||
backgroundGradientStart: '#D4EEF2',
|
||||
backgroundGradientEnd: '#E8F6F8',
|
||||
cardBackground: '#FFFFFF',
|
||||
cardBorder: '#D4EEF2',
|
||||
primary: '#2A6B7C',
|
||||
secondary: '#5A7A8A',
|
||||
text: '#1A3A4A',
|
||||
textSecondary: '#5A7A8A',
|
||||
accent: '#3D8B9C',
|
||||
link: '#2A6B7C',
|
||||
},
|
||||
};
|
||||
|
||||
export const typography = {
|
||||
fontFamily: {
|
||||
regular: 'System',
|
||||
medium: 'System',
|
||||
bold: 'System',
|
||||
serif: 'Georgia',
|
||||
mono: 'Menlo',
|
||||
},
|
||||
fontSize: {
|
||||
xs: 11,
|
||||
sm: 13,
|
||||
base: 15,
|
||||
md: 17,
|
||||
lg: 20,
|
||||
xl: 24,
|
||||
xxl: 32,
|
||||
hero: 40,
|
||||
},
|
||||
lineHeight: {
|
||||
tight: 1.2,
|
||||
normal: 1.5,
|
||||
relaxed: 1.75,
|
||||
},
|
||||
letterSpacing: {
|
||||
tight: -0.5,
|
||||
normal: 0,
|
||||
wide: 1,
|
||||
wider: 2,
|
||||
widest: 4,
|
||||
},
|
||||
};
|
||||
|
||||
export const spacing = {
|
||||
xs: 4,
|
||||
sm: 8,
|
||||
md: 12,
|
||||
base: 16,
|
||||
lg: 24,
|
||||
xl: 32,
|
||||
xxl: 48,
|
||||
};
|
||||
|
||||
export const borderRadius = {
|
||||
sm: 8,
|
||||
md: 12,
|
||||
lg: 20,
|
||||
xl: 28,
|
||||
xxl: 36,
|
||||
full: 9999,
|
||||
};
|
||||
|
||||
// Nautical shadows
|
||||
export const shadows = {
|
||||
soft: {
|
||||
shadowColor: '#1A3A4A',
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.06,
|
||||
shadowRadius: 16,
|
||||
elevation: 4,
|
||||
},
|
||||
medium: {
|
||||
shadowColor: '#1A3A4A',
|
||||
shadowOffset: { width: 0, height: 8 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 24,
|
||||
elevation: 8,
|
||||
},
|
||||
glow: {
|
||||
shadowColor: '#2A6B7C',
|
||||
shadowOffset: { width: 0, height: 0 },
|
||||
shadowOpacity: 0.25,
|
||||
shadowRadius: 20,
|
||||
elevation: 10,
|
||||
},
|
||||
};
|
||||
73
src/types/index.ts
Normal file
73
src/types/index.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
// Flow Types
|
||||
export type FlowRecordType = 'text' | 'voice' | 'image';
|
||||
|
||||
export interface FlowRecord {
|
||||
id: string;
|
||||
type: FlowRecordType;
|
||||
content: string;
|
||||
imageUri?: string;
|
||||
createdAt: Date;
|
||||
emotion?: string;
|
||||
isArchived: boolean;
|
||||
archivedAt?: Date;
|
||||
}
|
||||
|
||||
// Vault Types
|
||||
export type VaultAssetType =
|
||||
| 'game_account'
|
||||
| 'private_key'
|
||||
| 'document'
|
||||
| 'photo'
|
||||
| 'will'
|
||||
| 'custom';
|
||||
|
||||
export interface VaultAsset {
|
||||
id: string;
|
||||
type: VaultAssetType;
|
||||
label: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
isEncrypted: boolean;
|
||||
}
|
||||
|
||||
// Sentinel Types
|
||||
export type SystemStatus = 'normal' | 'warning' | 'releasing';
|
||||
|
||||
export interface SentinelState {
|
||||
status: SystemStatus;
|
||||
lastSubscriptionCheck: Date;
|
||||
lastFlowActivity: Date;
|
||||
killSwitchLogs: KillSwitchLog[];
|
||||
}
|
||||
|
||||
export interface KillSwitchLog {
|
||||
id: string;
|
||||
action: string;
|
||||
timestamp: Date;
|
||||
}
|
||||
|
||||
// Heritage Types
|
||||
export type HeirStatus = 'invited' | 'confirmed';
|
||||
export type PaymentStrategy = 'prepaid' | 'pay_on_access';
|
||||
|
||||
export interface Heir {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
status: HeirStatus;
|
||||
releaseLevel: number; // 1-3
|
||||
releaseOrder: number;
|
||||
paymentStrategy: PaymentStrategy;
|
||||
}
|
||||
|
||||
// Me Types
|
||||
export interface SubscriptionInfo {
|
||||
plan: string;
|
||||
expiresAt: Date;
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
export interface ProtocolInfo {
|
||||
version: string;
|
||||
lastUpdated: Date;
|
||||
}
|
||||
Reference in New Issue
Block a user