frontend first version

This commit is contained in:
Ada
2026-01-20 21:11:04 -08:00
commit 7944b9f7ed
29 changed files with 16468 additions and 0 deletions

97
.gitignore vendored Normal file
View File

@@ -0,0 +1,97 @@
# Dependencies
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
yarn.lock
# Expo
.expo/
.expo-shared/
dist/
web-build/
# Native
*.orig.*
*.jks
*.p8
*.p12
*.key
*.mobileprovision
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
xcuserdata
*.xccheckout
*.moved-aside
DerivedData
*.hmap
*.ipa
*.xcuserstate
ios/.xcode.env.local
# Android
*.apk
*.ap_
*.aab
*.dex
*.class
bin/
gen/
out/
.gradle/
local.properties
*.iml
.idea/
*.hprof
.cxx/
*.keystore
!debug.keystore
# macOS
.DS_Store
*.pem
# Environment variables
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# Logs
logs
*.log
# Testing
coverage/
.nyc_output/
# Temporary files
*.tmp
*.temp
*.swp
*.swo
*~
# IDE
.vscode/
.idea/
*.sublime-project
*.sublime-workspace
# TypeScript
*.tsbuildinfo
# Metro
.metro-health-check*
# Misc
*.zip
*.tar.gz
*.rar

23
App.tsx Normal file
View File

@@ -0,0 +1,23 @@
import React from 'react';
import { StatusBar } from 'expo-status-bar';
import { NavigationContainer } from '@react-navigation/native';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { StyleSheet } from 'react-native';
import TabNavigator from './src/navigation/TabNavigator';
export default function App() {
return (
<GestureHandlerRootView style={styles.container}>
<NavigationContainer>
<StatusBar style="auto" />
<TabNavigator />
</NavigationContainer>
</GestureHandlerRootView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
});

202
README.md Normal file
View File

@@ -0,0 +1,202 @@
# Sentinel App
[中文版](#中文版)
## Digital Legacy Management
Sentinel is a mobile application that helps users securely manage their digital legacy. Built with React Native (Expo) and TypeScript.
## Features
### 🗞️ Flow - Captain's Journal
- Record daily thoughts, emotions, and reflections
- AI-inferred emotional state tracking
- Archive entries to the encrypted Vault
- Support for text, voice, and image entries
### 📦 Vault - The Deep Vault
- End-to-end encrypted asset storage
- Support for game accounts, private keys, documents, photos, wills
- Biometric authentication required for access
- Zero-knowledge architecture
### ⚓ Sentinel - Lighthouse Watch
- Dead Man's Switch monitoring system
- Heartbeat confirmation mechanism
- Subscription and activity tracking
- Configurable grace periods
### 🧭 Heritage - Fleet Legacy
- Heir management with release levels
- Customizable release order and timing
- Payment strategy configuration
- Legal document-style interface
### ⛵ Me - Captain's Quarters
- Subscription and protocol status
- Sentinel configuration
- Security center
- Data export and backup
- Social responsibility program
## Tech Stack
- **Framework**: React Native (Expo SDK 52)
- **Language**: TypeScript
- **Navigation**: React Navigation (Bottom Tabs)
- **Icons**: @expo/vector-icons (Feather, Ionicons, MaterialCommunityIcons, FontAwesome5)
- **Styling**: Custom nautical theme with gradients
## Project Structure
```
src/
├── components/
│ └── common/
│ ├── BiometricModal.tsx
│ ├── Icons.tsx
│ └── VaultDoorAnimation.tsx
├── navigation/
│ └── TabNavigator.tsx
├── screens/
│ ├── FlowScreen.tsx
│ ├── VaultScreen.tsx
│ ├── SentinelScreen.tsx
│ ├── HeritageScreen.tsx
│ └── MeScreen.tsx
├── theme/
│ └── colors.ts
└── types/
└── index.ts
assets/
├── icon.png # App icon (1024x1024)
├── adaptive-icon.png # Android adaptive icon
├── splash.png # Splash screen
├── favicon.png # Web favicon (32x32)
├── favicon.svg # SVG favicon for web
├── logo.svg # Vector logo (512x512)
└── images/
└── captain-avatar.svg # Avatar placeholder
```
## Icons & Branding
The Sentinel brand uses a nautical anchor-and-star logo on a teal (#459E9E) background.
### Logo Elements
- **Anchor**: Symbolizes stability and anchoring your digital legacy
- **Star/Compass**: Represents guidance and direction for heirs
- **Teal Color**: Evokes ocean depth and calm security
### Generating Icons
```bash
# View icon specifications
node scripts/generate-icons.js
```
Use the `assets/logo.svg` as the source and export to required sizes.
## Getting Started
```bash
# Install dependencies
npm install
# Start the development server
npx expo start
```
## Design Philosophy
- **Nautical Theme**: Captain's sanctum aesthetic with anchors, ship wheels, and ocean colors
- **Emotional Balance**: Warm and secure feeling across different tabs
- **Privacy First**: Zero-knowledge architecture, local encryption
- **Elegant UI**: Mint gradients, serif typography, subtle shadows
---
# 中文版
[English Version](#sentinel-app)
## 数字遗产管理
Sentinel 是一款帮助用户安全管理数字遗产的移动应用程序。使用 React Native (Expo) 和 TypeScript 构建。
## 功能特性
### 🗞️ Flow - 船长日志
- 记录日常想法、情感和反思
- AI 推断情感状态追踪
- 将条目归档到加密保险库
- 支持文本、语音和图像条目
### 📦 Vault - 深海宝库
- 端到端加密资产存储
- 支持游戏账号、私钥、文档、照片、遗嘱
- 需要生物识别认证才能访问
- 零知识架构
### ⚓ Sentinel - 灯塔守望
- 死人开关监控系统
- 心跳确认机制
- 订阅和活动追踪
- 可配置的冷静期
### 🧭 Heritage - 舰队遗产
- 继承人管理与释放等级
- 可自定义释放顺序和时间
- 付款策略配置
- 法律文书风格界面
### ⛵ Me - 船长室
- 订阅和协议状态
- 哨兵配置
- 安全中心
- 数据导出和备份
- 社会责任计划
## 技术栈
- **框架**: React Native (Expo SDK 52)
- **语言**: TypeScript
- **导航**: React Navigation (底部标签)
- **图标**: @expo/vector-icons
- **样式**: 自定义航海主题配渐变
## 运行项目
```bash
# 安装依赖
npm install
# 启动开发服务器
npx expo start
```
## 图标与品牌
Sentinel 品牌使用青色(#459E9E)背景上的航海锚与星星标志。
### 标志元素
- **锚**: 象征稳定性和锚定你的数字遗产
- **星星/指南针**: 代表对继承人的指引和方向
- **青色**: 唤起海洋深度和平静的安全感
### 生成图标
```bash
# 查看图标规格
node scripts/generate-icons.js
```
使用 `assets/logo.svg` 作为源文件并导出所需尺寸。
## 设计理念
- **航海主题**: 船长圣殿美学,配以锚、船舵和海洋色彩
- **情感平衡**: 不同标签页带来温暖而安全的感觉
- **隐私优先**: 零知识架构,本地加密
- **优雅界面**: 薄荷渐变、衬线字体、柔和阴影

33
app.json Normal file
View File

@@ -0,0 +1,33 @@
{
"expo": {
"name": "Sentinel",
"slug": "sentinel-app",
"version": "2.0.0",
"orientation": "portrait",
"icon": "./assets/icon.png",
"userInterfaceStyle": "automatic",
"splash": {
"image": "./assets/splash.png",
"resizeMode": "contain",
"backgroundColor": "#459E9E"
},
"assetBundlePatterns": [
"**/*"
],
"ios": {
"supportsTablet": true,
"bundleIdentifier": "com.sentinel.app"
},
"android": {
"adaptiveIcon": {
"foregroundImage": "./assets/adaptive-icon.png",
"backgroundColor": "#459E9E"
},
"package": "com.sentinel.app"
},
"web": {
"favicon": "./assets/favicon.png",
"bundler": "metro"
}
}
}

2
assets/.gitkeep Normal file
View File

@@ -0,0 +1,2 @@
# Placeholder for app assets
# Add icon.png, splash.png, and adaptive-icon.png here

1
assets/adaptive-icon.png Normal file
View File

@@ -0,0 +1 @@
Placeholder - Replace with a 1024x1024 PNG adaptive icon with the Sentinel anchor logo

1
assets/favicon.png Normal file
View File

@@ -0,0 +1 @@
Placeholder - Replace with a 32x32 PNG favicon with the Sentinel anchor logo on teal (#459E9E) background

6
assets/favicon.svg Normal file
View File

@@ -0,0 +1,6 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="32" height="32" rx="8" fill="#459E9E"/>
<path d="M16 6L18 12H24L19 16L21 22L16 18L11 22L13 16L8 12H14L16 6Z" fill="white"/>
<circle cx="16" cy="20" r="3" fill="white" opacity="0.8"/>
<path d="M16 23V26M12 24L16 26L20 24" stroke="white" stroke-width="1.5" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 404 B

1
assets/icon.png Normal file
View File

@@ -0,0 +1 @@
This is a placeholder. Replace with actual PNG icon.

View File

@@ -0,0 +1,39 @@
<svg width="200" height="200" viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- Background -->
<rect width="200" height="200" rx="40" fill="#459E9E"/>
<!-- Face -->
<ellipse cx="100" cy="105" rx="55" ry="60" fill="#F5D5C8"/>
<!-- Hair -->
<path d="M50 85C50 55 70 35 100 35C130 35 150 55 150 85C150 75 140 50 100 50C60 50 50 75 50 85Z" fill="#E8D070"/>
<path d="M45 95C45 70 65 45 100 45C135 45 155 70 155 95C155 80 140 55 100 55C60 55 45 80 45 95Z" fill="#F0DC80"/>
<ellipse cx="135" cy="80" rx="15" ry="25" fill="#F0DC80"/>
<!-- Ears -->
<ellipse cx="48" cy="105" rx="8" ry="12" fill="#F5D5C8"/>
<ellipse cx="152" cy="105" rx="8" ry="12" fill="#F5D5C8"/>
<!-- Eyes -->
<circle cx="80" cy="100" r="6" fill="#2D2D2D"/>
<circle cx="120" cy="100" r="6" fill="#2D2D2D"/>
<circle cx="82" cy="98" r="2" fill="white"/>
<circle cx="122" cy="98" r="2" fill="white"/>
<!-- Eyebrows -->
<path d="M70 88C75 85 85 85 90 88" stroke="#C4A050" stroke-width="3" stroke-linecap="round"/>
<path d="M110 88C115 85 125 85 130 88" stroke="#C4A050" stroke-width="3" stroke-linecap="round"/>
<!-- Nose -->
<path d="M100 105C100 110 98 118 100 120C102 118 100 110 100 105" stroke="#E5C5B8" stroke-width="2" stroke-linecap="round"/>
<!-- Smile -->
<path d="M85 130C90 140 110 140 115 130" stroke="#2D2D2D" stroke-width="2.5" stroke-linecap="round" fill="none"/>
<!-- Collar -->
<path d="M65 165L80 150L100 160L120 150L135 165" stroke="white" stroke-width="4" fill="none"/>
<!-- Sweater -->
<path d="M40 200V170C40 160 60 150 100 150C140 150 160 160 160 170V200" fill="#E85A3A"/>
<path d="M80 150L100 165L120 150" fill="#E85A3A"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

20
assets/logo.svg Normal file
View File

@@ -0,0 +1,20 @@
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- Background -->
<rect width="512" height="512" rx="112" fill="#459E9E"/>
<!-- Anchor Ring -->
<circle cx="256" cy="180" r="50" stroke="white" stroke-width="16" fill="none"/>
<!-- Anchor Shaft -->
<path d="M256 230V420" stroke="white" stroke-width="16" stroke-linecap="round"/>
<!-- Anchor Flukes (curved arms) -->
<path d="M136 380C136 330 186 290 256 290C326 290 376 330 376 380" stroke="white" stroke-width="16" stroke-linecap="round" fill="none"/>
<path d="M156 400L256 430L356 400" stroke="white" stroke-width="16" stroke-linecap="round" stroke-linejoin="round"/>
<!-- Compass Star (above anchor) -->
<path d="M256 70L268 110H310L276 136L288 176L256 152L224 176L236 136L202 110H244L256 70Z" fill="white"/>
<!-- Anchor Crossbar -->
<path d="M206 200H306" stroke="white" stroke-width="12" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 968 B

1
assets/splash.png Normal file
View File

@@ -0,0 +1 @@
Placeholder - Replace with a 1284x2778 PNG splash screen with the Sentinel logo on teal (#459E9E) background

7
babel.config.js Normal file
View File

@@ -0,0 +1,7 @@
module.exports = function(api) {
api.cache(true);
return {
presets: ['babel-preset-expo'],
plugins: ['react-native-reanimated/plugin'],
};
};

11236
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

38
package.json Normal file
View File

@@ -0,0 +1,38 @@
{
"name": "sentinel-app",
"version": "1.0.0",
"main": "expo/AppEntry.js",
"scripts": {
"start": "expo start",
"android": "expo start --android",
"ios": "expo start --ios",
"web": "expo start --web"
},
"dependencies": {
"@expo/metro-runtime": "~4.0.1",
"@expo/vector-icons": "~14.0.4",
"@react-navigation/bottom-tabs": "^6.6.1",
"@react-navigation/native": "^6.1.18",
"expo": "~52.0.0",
"expo-asset": "~11.0.5",
"expo-constants": "~17.0.8",
"expo-font": "~13.0.4",
"expo-haptics": "~14.0.0",
"expo-linear-gradient": "~14.0.2",
"expo-status-bar": "~2.0.0",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-native": "^0.76.9",
"react-native-gesture-handler": "~2.20.2",
"react-native-reanimated": "~3.16.1",
"react-native-safe-area-context": "4.12.0",
"react-native-screens": "~4.4.0",
"react-native-web": "~0.19.13"
},
"devDependencies": {
"@babel/core": "^7.25.2",
"@types/react": "~18.3.12",
"typescript": "~5.3.3"
},
"private": true
}

82
scripts/generate-icons.js Normal file
View File

@@ -0,0 +1,82 @@
/**
* Icon Generation Script for Sentinel App
*
* This script documents the icon specifications for the Sentinel app.
* Use a design tool (Figma, Sketch) or image editor to create these icons.
*
* Design Guidelines:
* - Primary Color: #459E9E (Teal)
* - Background: #459E9E (Teal) or #1A3A4A (Deep Navy)
* - Icon: White anchor with star/compass element
* - Style: Clean, nautical, minimal
*/
const iconSpecs = {
// App Icon (used for app launcher)
'icon.png': {
size: '1024x1024',
background: '#459E9E',
borderRadius: '224px (22%)',
description: 'Main app icon with anchor logo'
},
// Adaptive Icon (Android)
'adaptive-icon.png': {
size: '1024x1024',
background: 'Transparent or #459E9E',
description: 'Foreground image for Android adaptive icons'
},
// Favicon (Web)
'favicon.png': {
size: '32x32',
background: '#459E9E',
borderRadius: '6px',
description: 'Web browser tab icon'
},
// Splash Screen
'splash.png': {
size: '1284x2778',
background: '#459E9E',
description: 'Full-screen splash with centered logo'
}
};
// SVG Template for the Sentinel Logo
const logoSVG = `
<svg viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- Anchor Ring -->
<circle cx="50" cy="35" r="12" stroke="white" stroke-width="4" fill="none"/>
<!-- Anchor Shaft -->
<path d="M50 47V85" stroke="white" stroke-width="4" stroke-linecap="round"/>
<!-- Anchor Flukes -->
<path d="M30 75C30 65 40 60 50 60C60 60 70 65 70 75" stroke="white" stroke-width="4" stroke-linecap="round" fill="none"/>
<path d="M35 80L50 85L65 80" stroke="white" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
<!-- Compass Star (above anchor) -->
<path d="M50 15L52 22H59L53 26L55 33L50 29L45 33L47 26L41 22H48L50 15Z" fill="white"/>
</svg>
`;
console.log('Sentinel Icon Specifications');
console.log('============================\n');
Object.entries(iconSpecs).forEach(([filename, spec]) => {
console.log(`${filename}:`);
console.log(` Size: ${spec.size}`);
console.log(` Background: ${spec.background}`);
if (spec.borderRadius) console.log(` Border Radius: ${spec.borderRadius}`);
console.log(` Description: ${spec.description}`);
console.log('');
});
console.log('\nLogo SVG Template:');
console.log(logoSVG);
console.log('\nTo generate icons:');
console.log('1. Open logo.svg in assets/ folder');
console.log('2. Export at required sizes');
console.log('3. Replace placeholder files in assets/');

View 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',
},
});

View 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} />
);

View 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',
},
});

View 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
View 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',
},
});

View 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
View 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',
},
});

View 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
View 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
View 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
View 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;
}

11
tsconfig.json Normal file
View File

@@ -0,0 +1,11 @@
{
"extends": "expo/tsconfig.base",
"compilerOptions": {
"strict": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"include": ["**/*.ts", "**/*.tsx"]
}

36
web/index.html Normal file
View File

@@ -0,0 +1,36 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta httpEquiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<title>Sentinel - Aeterna Nautica</title>
<!-- Favicon -->
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%23459E9E'/%3E%3Ccircle cx='16' cy='11' r='4' stroke='white' stroke-width='1.5' fill='none'/%3E%3Cpath d='M16 15V27' stroke='white' stroke-width='1.5' stroke-linecap='round'/%3E%3Cpath d='M10 24C10 21 13 19 16 19C19 19 22 21 22 24' stroke='white' stroke-width='1.5' stroke-linecap='round' fill='none'/%3E%3Cpath d='M11 26L16 28L21 26' stroke='white' stroke-width='1.5' stroke-linecap='round'/%3E%3Cpath d='M16 5L17 7H19L17.5 8.5L18 10L16 9L14 10L14.5 8.5L13 7H15L16 5Z' fill='white'/%3E%3C/svg%3E" />
<!-- Apple Touch Icon -->
<link rel="apple-touch-icon" href="/assets/icon.png" />
<!-- Theme Color -->
<meta name="theme-color" content="#459E9E" />
<!-- Description -->
<meta name="description" content="Sentinel - Your digital legacy guardian. The sea claims what is forgotten, but the Sanctuary keeps what is loved." />
<style>
html, body, #root {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
overflow: hidden;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
background-color: #E8F6F8;
}
</style>
</head>
<body>
<div id="root"></div>
</body>
</html>