Compare commits
6 Commits
b8c241c1a0
...
dev_pr_tes
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fb1377eb4b | ||
|
|
c07f1f20d5 | ||
| 749ed2f05a | |||
|
|
da4a7de0ad | ||
|
|
146320052e | ||
|
|
4d94888bb8 |
65
App.tsx
65
App.tsx
@@ -1,17 +1,60 @@
|
|||||||
|
/**
|
||||||
|
* App Entry Point
|
||||||
|
*
|
||||||
|
* Main application component with authentication routing.
|
||||||
|
* Shows loading screen while restoring auth state.
|
||||||
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { StatusBar } from 'expo-status-bar';
|
import { StatusBar } from 'expo-status-bar';
|
||||||
import { NavigationContainer } from '@react-navigation/native';
|
import { NavigationContainer } from '@react-navigation/native';
|
||||||
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
||||||
import { StyleSheet } from 'react-native';
|
import { StyleSheet, View, ActivityIndicator, Text } from 'react-native';
|
||||||
import TabNavigator from './src/navigation/TabNavigator';
|
import TabNavigator from './src/navigation/TabNavigator';
|
||||||
|
import AuthNavigator from './src/navigation/AuthNavigator';
|
||||||
|
import { AuthProvider, useAuth } from './src/context/AuthContext';
|
||||||
|
import { colors } from './src/theme/colors';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loading screen shown while restoring auth state
|
||||||
|
*/
|
||||||
|
function LoadingScreen() {
|
||||||
|
return (
|
||||||
|
<View style={styles.loadingContainer}>
|
||||||
|
<ActivityIndicator size="large" color={colors.nautical.teal} />
|
||||||
|
<Text style={styles.loadingText}>Loading...</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main app content with auth-based routing
|
||||||
|
*/
|
||||||
|
function AppContent() {
|
||||||
|
const { user, isInitializing } = useAuth();
|
||||||
|
|
||||||
|
// Show loading screen while restoring auth state
|
||||||
|
if (isInitializing) {
|
||||||
|
return <LoadingScreen />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NavigationContainer>
|
||||||
|
<StatusBar style="auto" />
|
||||||
|
{user ? <TabNavigator /> : <AuthNavigator />}
|
||||||
|
</NavigationContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Root App component
|
||||||
|
*/
|
||||||
export default function App() {
|
export default function App() {
|
||||||
return (
|
return (
|
||||||
<GestureHandlerRootView style={styles.container}>
|
<GestureHandlerRootView style={styles.container}>
|
||||||
<NavigationContainer>
|
<AuthProvider>
|
||||||
<StatusBar style="auto" />
|
<AppContent />
|
||||||
<TabNavigator />
|
</AuthProvider>
|
||||||
</NavigationContainer>
|
|
||||||
</GestureHandlerRootView>
|
</GestureHandlerRootView>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -19,5 +62,17 @@ export default function App() {
|
|||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
|
backgroundColor: '#000',
|
||||||
|
},
|
||||||
|
loadingContainer: {
|
||||||
|
flex: 1,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
backgroundColor: colors.flow.backgroundGradientStart,
|
||||||
|
},
|
||||||
|
loadingText: {
|
||||||
|
marginTop: 16,
|
||||||
|
fontSize: 16,
|
||||||
|
color: colors.flow.textSecondary,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
95
README.md
95
README.md
@@ -44,8 +44,35 @@ Sentinel is a mobile application that helps users securely manage their digital
|
|||||||
- **Framework**: React Native (Expo SDK 52)
|
- **Framework**: React Native (Expo SDK 52)
|
||||||
- **Language**: TypeScript
|
- **Language**: TypeScript
|
||||||
- **Navigation**: React Navigation (Bottom Tabs)
|
- **Navigation**: React Navigation (Bottom Tabs)
|
||||||
- **Icons**: @expo/vector-icons (Feather, Ionicons, MaterialCommunityIcons, FontAwesome5)
|
- **Icons**: @expo/vector-icons (Feather, Ionicons, FontAwesome5)
|
||||||
- **Styling**: Custom nautical theme with gradients
|
- **Styling**: Custom nautical theme with gradients
|
||||||
|
- **State Management**: React Context (AuthContext)
|
||||||
|
- **Storage**: AsyncStorage for auth persistence
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
The application uses a centralized configuration file located at `src/config/index.ts`.
|
||||||
|
|
||||||
|
### Key Configuration Options
|
||||||
|
|
||||||
|
| Option | Description | Default |
|
||||||
|
|--------|-------------|---------|
|
||||||
|
| `NO_BACKEND_MODE` | Use mock data instead of real backend | `false` |
|
||||||
|
| `DEBUG_MODE` | Enable API debug logging | `true` |
|
||||||
|
| `API_BASE_URL` | Backend API server URL | `http://localhost:8000` |
|
||||||
|
| `API_TIMEOUT` | Request timeout (ms) | `30000` |
|
||||||
|
|
||||||
|
### API Endpoints
|
||||||
|
|
||||||
|
All backend API routes are defined in `API_ENDPOINTS`:
|
||||||
|
- **AUTH**: `/login`, `/register`
|
||||||
|
- **ASSETS**: `/assets/get`, `/assets/create`, `/assets/claim`, `/assets/assign`
|
||||||
|
- **AI**: `/ai/proxy`
|
||||||
|
- **ADMIN**: `/admin/declare-guale`
|
||||||
|
|
||||||
|
### Environment Setup
|
||||||
|
|
||||||
|
For development, you may need to modify `API_BASE_URL` in the config file to match your backend server address.
|
||||||
|
|
||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
||||||
@@ -56,14 +83,27 @@ src/
|
|||||||
│ ├── BiometricModal.tsx
|
│ ├── BiometricModal.tsx
|
||||||
│ ├── Icons.tsx
|
│ ├── Icons.tsx
|
||||||
│ └── VaultDoorAnimation.tsx
|
│ └── VaultDoorAnimation.tsx
|
||||||
|
├── config/
|
||||||
|
│ └── index.ts # Centralized configuration
|
||||||
|
├── context/
|
||||||
|
│ └── AuthContext.tsx # Authentication state management
|
||||||
├── navigation/
|
├── navigation/
|
||||||
│ └── TabNavigator.tsx
|
│ ├── AuthNavigator.tsx # Login/Register navigation
|
||||||
|
│ └── TabNavigator.tsx # Main app navigation
|
||||||
├── screens/
|
├── screens/
|
||||||
│ ├── FlowScreen.tsx
|
│ ├── FlowScreen.tsx # AI chat interface
|
||||||
│ ├── VaultScreen.tsx
|
│ ├── VaultScreen.tsx
|
||||||
│ ├── SentinelScreen.tsx
|
│ ├── SentinelScreen.tsx
|
||||||
│ ├── HeritageScreen.tsx
|
│ ├── HeritageScreen.tsx
|
||||||
│ └── MeScreen.tsx
|
│ ├── MeScreen.tsx
|
||||||
|
│ ├── LoginScreen.tsx
|
||||||
|
│ └── RegisterScreen.tsx
|
||||||
|
├── services/
|
||||||
|
│ ├── index.ts # Service exports
|
||||||
|
│ ├── ai.service.ts # AI API integration
|
||||||
|
│ ├── auth.service.ts # Authentication API
|
||||||
|
│ ├── assets.service.ts # Asset management API
|
||||||
|
│ └── admin.service.ts # Admin operations API
|
||||||
├── theme/
|
├── theme/
|
||||||
│ └── colors.ts
|
│ └── colors.ts
|
||||||
└── types/
|
└── types/
|
||||||
@@ -80,6 +120,15 @@ assets/
|
|||||||
└── captain-avatar.svg # Avatar placeholder
|
└── captain-avatar.svg # Avatar placeholder
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Services
|
||||||
|
|
||||||
|
The application uses a modular service architecture for API communication:
|
||||||
|
|
||||||
|
- **AuthService**: User authentication (login, register)
|
||||||
|
- **AIService**: AI conversation proxy with support for text and image input
|
||||||
|
- **AssetsService**: Digital asset management
|
||||||
|
- **AdminService**: Administrative operations
|
||||||
|
|
||||||
## Icons & Branding
|
## Icons & Branding
|
||||||
|
|
||||||
The Sentinel brand uses a nautical anchor-and-star logo on a teal (#459E9E) background.
|
The Sentinel brand uses a nautical anchor-and-star logo on a teal (#459E9E) background.
|
||||||
@@ -163,8 +212,44 @@ Sentinel 是一款帮助用户安全管理数字遗产的移动应用程序。
|
|||||||
- **框架**: React Native (Expo SDK 52)
|
- **框架**: React Native (Expo SDK 52)
|
||||||
- **语言**: TypeScript
|
- **语言**: TypeScript
|
||||||
- **导航**: React Navigation (底部标签)
|
- **导航**: React Navigation (底部标签)
|
||||||
- **图标**: @expo/vector-icons
|
- **图标**: @expo/vector-icons (Feather, Ionicons, FontAwesome5)
|
||||||
- **样式**: 自定义航海主题配渐变
|
- **样式**: 自定义航海主题配渐变
|
||||||
|
- **状态管理**: React Context (AuthContext)
|
||||||
|
- **存储**: AsyncStorage 用于认证持久化
|
||||||
|
|
||||||
|
## 配置说明
|
||||||
|
|
||||||
|
应用使用位于 `src/config/index.ts` 的集中配置文件。
|
||||||
|
|
||||||
|
### 主要配置项
|
||||||
|
|
||||||
|
| 选项 | 说明 | 默认值 |
|
||||||
|
|------|------|--------|
|
||||||
|
| `NO_BACKEND_MODE` | 使用模拟数据而非真实后端 | `false` |
|
||||||
|
| `DEBUG_MODE` | 启用 API 调试日志 | `true` |
|
||||||
|
| `API_BASE_URL` | 后端 API 服务器地址 | `http://localhost:8000` |
|
||||||
|
| `API_TIMEOUT` | 请求超时时间(毫秒) | `30000` |
|
||||||
|
|
||||||
|
### API 端点
|
||||||
|
|
||||||
|
所有后端 API 路由定义在 `API_ENDPOINTS` 中:
|
||||||
|
- **AUTH**: `/login`, `/register`
|
||||||
|
- **ASSETS**: `/assets/get`, `/assets/create`, `/assets/claim`, `/assets/assign`
|
||||||
|
- **AI**: `/ai/proxy`
|
||||||
|
- **ADMIN**: `/admin/declare-guale`
|
||||||
|
|
||||||
|
### 环境配置
|
||||||
|
|
||||||
|
开发时,您可能需要修改配置文件中的 `API_BASE_URL` 以匹配您的后端服务器地址。
|
||||||
|
|
||||||
|
## 服务层
|
||||||
|
|
||||||
|
应用使用模块化的服务架构进行 API 通信:
|
||||||
|
|
||||||
|
- **AuthService**: 用户认证(登录、注册)
|
||||||
|
- **AIService**: AI 对话代理,支持文本和图片输入
|
||||||
|
- **AssetsService**: 数字资产管理
|
||||||
|
- **AdminService**: 管理员操作
|
||||||
|
|
||||||
## 运行项目
|
## 运行项目
|
||||||
|
|
||||||
|
|||||||
137
package-lock.json
generated
137
package-lock.json
generated
@@ -10,13 +10,16 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@expo/metro-runtime": "~4.0.1",
|
"@expo/metro-runtime": "~4.0.1",
|
||||||
"@expo/vector-icons": "~14.0.4",
|
"@expo/vector-icons": "~14.0.4",
|
||||||
|
"@react-native-async-storage/async-storage": "^2.2.0",
|
||||||
"@react-navigation/bottom-tabs": "^6.6.1",
|
"@react-navigation/bottom-tabs": "^6.6.1",
|
||||||
"@react-navigation/native": "^6.1.18",
|
"@react-navigation/native": "^6.1.18",
|
||||||
|
"@react-navigation/native-stack": "^6.11.0",
|
||||||
"expo": "~52.0.0",
|
"expo": "~52.0.0",
|
||||||
"expo-asset": "~11.0.5",
|
"expo-asset": "~11.0.5",
|
||||||
"expo-constants": "~17.0.8",
|
"expo-constants": "~17.0.8",
|
||||||
"expo-font": "~13.0.4",
|
"expo-font": "~13.0.4",
|
||||||
"expo-haptics": "~14.0.0",
|
"expo-haptics": "~14.0.0",
|
||||||
|
"expo-image-picker": "^17.0.10",
|
||||||
"expo-linear-gradient": "~14.0.2",
|
"expo-linear-gradient": "~14.0.2",
|
||||||
"expo-status-bar": "~2.0.0",
|
"expo-status-bar": "~2.0.0",
|
||||||
"react": "18.3.1",
|
"react": "18.3.1",
|
||||||
@@ -26,6 +29,7 @@
|
|||||||
"react-native-reanimated": "~3.16.1",
|
"react-native-reanimated": "~3.16.1",
|
||||||
"react-native-safe-area-context": "4.12.0",
|
"react-native-safe-area-context": "4.12.0",
|
||||||
"react-native-screens": "~4.4.0",
|
"react-native-screens": "~4.4.0",
|
||||||
|
"react-native-view-shot": "^3.8.0",
|
||||||
"react-native-web": "~0.19.13"
|
"react-native-web": "~0.19.13"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -3245,6 +3249,18 @@
|
|||||||
"node": ">=14"
|
"node": ">=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@react-native-async-storage/async-storage": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-gvRvjR5JAaUZF8tv2Kcq/Gbt3JHwbKFYfmb445rhOj6NUMx3qPLixmDx5pZAyb9at1bYvJ4/eTUipU5aki45xw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"merge-options": "^3.0.4"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react-native": "^0.0.0-0 || >=0.65 <1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@react-native/assets-registry": {
|
"node_modules/@react-native/assets-registry": {
|
||||||
"version": "0.76.9",
|
"version": "0.76.9",
|
||||||
"resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.76.9.tgz",
|
"resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.76.9.tgz",
|
||||||
@@ -3684,6 +3700,23 @@
|
|||||||
"react-native": "*"
|
"react-native": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@react-navigation/native-stack": {
|
||||||
|
"version": "6.11.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@react-navigation/native-stack/-/native-stack-6.11.0.tgz",
|
||||||
|
"integrity": "sha512-U5EcUB9Q2NQspCFwYGGNJm0h6wBCOv7T30QjndmvlawLkNt7S7KWbpWyxS9XBHSIKF57RgWjfxuJNTgTstpXxw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@react-navigation/elements": "^1.3.31",
|
||||||
|
"warn-once": "^0.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@react-navigation/native": "^6.0.0",
|
||||||
|
"react": "*",
|
||||||
|
"react-native": "*",
|
||||||
|
"react-native-safe-area-context": ">= 3.0.0",
|
||||||
|
"react-native-screens": ">= 3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@react-navigation/routers": {
|
"node_modules/@react-navigation/routers": {
|
||||||
"version": "6.1.9",
|
"version": "6.1.9",
|
||||||
"resolved": "https://registry.npmjs.org/@react-navigation/routers/-/routers-6.1.9.tgz",
|
"resolved": "https://registry.npmjs.org/@react-navigation/routers/-/routers-6.1.9.tgz",
|
||||||
@@ -4311,6 +4344,15 @@
|
|||||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/base64-arraybuffer": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/base64-js": {
|
"node_modules/base64-js": {
|
||||||
"version": "1.5.1",
|
"version": "1.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||||
@@ -5097,6 +5139,15 @@
|
|||||||
"hyphenate-style-name": "^1.0.3"
|
"hyphenate-style-name": "^1.0.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/css-line-break": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"utrie": "^1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/csstype": {
|
"node_modules/csstype": {
|
||||||
"version": "3.2.3",
|
"version": "3.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||||
@@ -5678,6 +5729,27 @@
|
|||||||
"expo": "*"
|
"expo": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/expo-image-loader": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/expo-image-loader/-/expo-image-loader-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-nKs/xnOGw6ACb4g26xceBD57FKLFkSwEUTDXEDF3Gtcu3MqF3ZIYd3YM+sSb1/z9AKV1dYT7rMSGVNgsveXLIQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"expo": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/expo-image-picker": {
|
||||||
|
"version": "17.0.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/expo-image-picker/-/expo-image-picker-17.0.10.tgz",
|
||||||
|
"integrity": "sha512-a2xrowp2trmvXyUWgX3O6Q2rZaa2C59AqivKI7+bm+wLvMfTEbZgldLX4rEJJhM8xtmEDTNU+lzjtObwzBRGaw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"expo-image-loader": "~6.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"expo": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/expo-linear-gradient": {
|
"node_modules/expo-linear-gradient": {
|
||||||
"version": "14.0.2",
|
"version": "14.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/expo-linear-gradient/-/expo-linear-gradient-14.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/expo-linear-gradient/-/expo-linear-gradient-14.0.2.tgz",
|
||||||
@@ -6359,6 +6431,19 @@
|
|||||||
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
|
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/html2canvas": {
|
||||||
|
"version": "1.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
|
||||||
|
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"css-line-break": "^2.1.0",
|
||||||
|
"text-segmentation": "^1.0.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/http-errors": {
|
"node_modules/http-errors": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
|
||||||
@@ -6668,6 +6753,15 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-plain-obj": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-plain-object": {
|
"node_modules/is-plain-object": {
|
||||||
"version": "2.0.4",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
|
||||||
@@ -7547,6 +7641,18 @@
|
|||||||
"integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==",
|
"integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/merge-options": {
|
||||||
|
"version": "3.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/merge-options/-/merge-options-3.0.4.tgz",
|
||||||
|
"integrity": "sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"is-plain-obj": "^2.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/merge-stream": {
|
"node_modules/merge-stream": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
|
||||||
@@ -9260,6 +9366,19 @@
|
|||||||
"react-native": "*"
|
"react-native": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-native-view-shot": {
|
||||||
|
"version": "3.8.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-native-view-shot/-/react-native-view-shot-3.8.0.tgz",
|
||||||
|
"integrity": "sha512-4cU8SOhMn3YQIrskh+5Q8VvVRxQOu8/s1M9NAL4z5BY1Rm0HXMWkQJ4N0XsZ42+Yca+y86ISF3LC5qdLPvPuiA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"html2canvas": "^1.4.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "*",
|
||||||
|
"react-native": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-native-web": {
|
"node_modules/react-native-web": {
|
||||||
"version": "0.19.13",
|
"version": "0.19.13",
|
||||||
"resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.19.13.tgz",
|
"resolved": "https://registry.npmjs.org/react-native-web/-/react-native-web-0.19.13.tgz",
|
||||||
@@ -10509,6 +10628,15 @@
|
|||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/text-segmentation": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"utrie": "^1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/thenify": {
|
"node_modules/thenify": {
|
||||||
"version": "3.3.1",
|
"version": "3.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
|
||||||
@@ -10796,6 +10924,15 @@
|
|||||||
"node": ">= 0.4.0"
|
"node": ">= 0.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/utrie": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"base64-arraybuffer": "^1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/uuid": {
|
"node_modules/uuid": {
|
||||||
"version": "8.3.2",
|
"version": "8.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||||
|
|||||||
@@ -11,19 +11,23 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@expo/metro-runtime": "~4.0.1",
|
"@expo/metro-runtime": "~4.0.1",
|
||||||
"@expo/vector-icons": "~14.0.4",
|
"@expo/vector-icons": "~14.0.4",
|
||||||
|
"@react-native-async-storage/async-storage": "^2.2.0",
|
||||||
"@react-navigation/bottom-tabs": "^6.6.1",
|
"@react-navigation/bottom-tabs": "^6.6.1",
|
||||||
"@react-navigation/native": "^6.1.18",
|
"@react-navigation/native": "^6.1.18",
|
||||||
|
"@react-navigation/native-stack": "^6.11.0",
|
||||||
"expo": "~52.0.0",
|
"expo": "~52.0.0",
|
||||||
"expo-asset": "~11.0.5",
|
"expo-asset": "~11.0.5",
|
||||||
"expo-constants": "~17.0.8",
|
"expo-constants": "~17.0.8",
|
||||||
"expo-font": "~13.0.4",
|
"expo-font": "~13.0.4",
|
||||||
"expo-haptics": "~14.0.0",
|
"expo-haptics": "~14.0.0",
|
||||||
|
"expo-image-picker": "^17.0.10",
|
||||||
"expo-linear-gradient": "~14.0.2",
|
"expo-linear-gradient": "~14.0.2",
|
||||||
"expo-status-bar": "~2.0.0",
|
"expo-status-bar": "~2.0.0",
|
||||||
"react": "18.3.1",
|
"react": "18.3.1",
|
||||||
"react-dom": "18.3.1",
|
"react-dom": "18.3.1",
|
||||||
"react-native": "^0.76.9",
|
"react-native": "^0.76.9",
|
||||||
"react-native-gesture-handler": "~2.20.2",
|
"react-native-gesture-handler": "~2.20.2",
|
||||||
|
"react-native-view-shot": "^3.8.0",
|
||||||
"react-native-reanimated": "~3.16.1",
|
"react-native-reanimated": "~3.16.1",
|
||||||
"react-native-safe-area-context": "4.12.0",
|
"react-native-safe-area-context": "4.12.0",
|
||||||
"react-native-screens": "~4.4.0",
|
"react-native-screens": "~4.4.0",
|
||||||
|
|||||||
146
src/config/index.ts
Normal file
146
src/config/index.ts
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
/**
|
||||||
|
* Application Configuration
|
||||||
|
*
|
||||||
|
* This file contains all configuration settings for the frontend application.
|
||||||
|
* Centralized configuration makes it easier to manage environment-specific settings.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Environment Configuration
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set to true to use mock data instead of real backend calls.
|
||||||
|
* Useful for development and testing without a running backend.
|
||||||
|
*/
|
||||||
|
export const NO_BACKEND_MODE = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable debug logging for API calls
|
||||||
|
*/
|
||||||
|
export const DEBUG_MODE = true;
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// API Configuration
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base URL for the backend API server
|
||||||
|
*/
|
||||||
|
export const API_BASE_URL = 'http://localhost:8000';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API request timeout in milliseconds
|
||||||
|
*/
|
||||||
|
export const API_TIMEOUT = 30000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API Endpoints
|
||||||
|
* All backend API routes are defined here for easy reference and maintenance.
|
||||||
|
*/
|
||||||
|
export const API_ENDPOINTS = {
|
||||||
|
// Authentication
|
||||||
|
AUTH: {
|
||||||
|
LOGIN: '/login',
|
||||||
|
REGISTER: '/register',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Assets Management
|
||||||
|
ASSETS: {
|
||||||
|
GET: '/assets/get',
|
||||||
|
CREATE: '/assets/create',
|
||||||
|
CLAIM: '/assets/claim',
|
||||||
|
ASSIGN: '/assets/assign',
|
||||||
|
},
|
||||||
|
|
||||||
|
// AI Services
|
||||||
|
AI: {
|
||||||
|
PROXY: '/ai/proxy',
|
||||||
|
},
|
||||||
|
|
||||||
|
// Admin Operations
|
||||||
|
ADMIN: {
|
||||||
|
DECLARE_GUALE: '/admin/declare-guale',
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Helper Functions
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build full API URL from endpoint
|
||||||
|
* @param endpoint - API endpoint path (e.g., '/login')
|
||||||
|
* @returns Full URL (e.g., 'http://192.168.56.103:8000/login')
|
||||||
|
*/
|
||||||
|
export function buildApiUrl(endpoint: string): string {
|
||||||
|
return `${API_BASE_URL}${endpoint}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get default headers for API requests
|
||||||
|
* @param token - Optional JWT token for authenticated requests
|
||||||
|
* @returns Headers object
|
||||||
|
*/
|
||||||
|
export function getApiHeaders(token?: string): Record<string, string> {
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
headers['Authorization'] = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log API debug information
|
||||||
|
* Only logs when DEBUG_MODE is enabled
|
||||||
|
*/
|
||||||
|
export function logApiDebug(label: string, data: unknown): void {
|
||||||
|
if (DEBUG_MODE) {
|
||||||
|
console.log(`[API Debug] ${label}:`, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Mock User Configuration (for NO_BACKEND_MODE)
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export const MOCK_CONFIG = {
|
||||||
|
USER: {
|
||||||
|
id: 999,
|
||||||
|
username: 'MockCaptain',
|
||||||
|
public_key: 'mock_public_key',
|
||||||
|
is_admin: true,
|
||||||
|
guale: false,
|
||||||
|
tier: 'premium',
|
||||||
|
tier_expires_at: '2026-12-31T23:59:59Z',
|
||||||
|
last_active_at: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
ACCESS_TOKEN: 'mock_access_token',
|
||||||
|
RESPONSE_DELAY: 200, // milliseconds
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// AI Service Configuration
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export const AI_CONFIG = {
|
||||||
|
/**
|
||||||
|
* Default system prompt for AI conversations
|
||||||
|
*/
|
||||||
|
DEFAULT_SYSTEM_PROMPT: 'You are a helpful journal assistant. Help the user reflect on their thoughts and feelings.',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mock response delay in milliseconds (for NO_BACKEND_MODE)
|
||||||
|
*/
|
||||||
|
MOCK_RESPONSE_DELAY: 500,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Export Type Definitions
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export type ApiEndpoint = typeof API_ENDPOINTS;
|
||||||
175
src/context/AuthContext.tsx
Normal file
175
src/context/AuthContext.tsx
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
/**
|
||||||
|
* AuthContext - Authentication State Management
|
||||||
|
*
|
||||||
|
* Provides authentication state and methods throughout the app.
|
||||||
|
* Uses AsyncStorage for persistent login state.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
|
||||||
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
|
import { User, LoginRequest, RegisterRequest } from '../types';
|
||||||
|
import { authService } from '../services/auth.service';
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Type Definitions
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
interface AuthContextType {
|
||||||
|
user: User | null;
|
||||||
|
token: string | null;
|
||||||
|
isLoading: boolean;
|
||||||
|
isInitializing: boolean;
|
||||||
|
signIn: (credentials: LoginRequest) => Promise<void>;
|
||||||
|
signUp: (data: RegisterRequest) => Promise<void>;
|
||||||
|
signOut: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Storage keys
|
||||||
|
const STORAGE_KEYS = {
|
||||||
|
TOKEN: '@auth_token',
|
||||||
|
USER: '@auth_user',
|
||||||
|
};
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Context
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Provider Component
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export function AuthProvider({ children }: { children: ReactNode }) {
|
||||||
|
const [user, setUser] = useState<User | null>(null);
|
||||||
|
const [token, setToken] = useState<string | null>(null);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [isInitializing, setIsInitializing] = useState(true);
|
||||||
|
|
||||||
|
// Load saved auth state on app start
|
||||||
|
useEffect(() => {
|
||||||
|
loadStoredAuth();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load stored authentication from AsyncStorage
|
||||||
|
*/
|
||||||
|
const loadStoredAuth = async () => {
|
||||||
|
try {
|
||||||
|
const [storedToken, storedUser] = await Promise.all([
|
||||||
|
AsyncStorage.getItem(STORAGE_KEYS.TOKEN),
|
||||||
|
AsyncStorage.getItem(STORAGE_KEYS.USER),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (storedToken && storedUser) {
|
||||||
|
setToken(storedToken);
|
||||||
|
setUser(JSON.parse(storedUser));
|
||||||
|
console.log('[Auth] Restored session for user:', JSON.parse(storedUser).username);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Auth] Failed to load stored auth:', error);
|
||||||
|
} finally {
|
||||||
|
setIsInitializing(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save authentication to AsyncStorage
|
||||||
|
*/
|
||||||
|
const saveAuth = async (authToken: string, authUser: User) => {
|
||||||
|
try {
|
||||||
|
await Promise.all([
|
||||||
|
AsyncStorage.setItem(STORAGE_KEYS.TOKEN, authToken),
|
||||||
|
AsyncStorage.setItem(STORAGE_KEYS.USER, JSON.stringify(authUser)),
|
||||||
|
]);
|
||||||
|
console.log('[Auth] Session saved for user:', authUser.username);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Auth] Failed to save auth:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear authentication from AsyncStorage
|
||||||
|
*/
|
||||||
|
const clearAuth = async () => {
|
||||||
|
try {
|
||||||
|
await Promise.all([
|
||||||
|
AsyncStorage.removeItem(STORAGE_KEYS.TOKEN),
|
||||||
|
AsyncStorage.removeItem(STORAGE_KEYS.USER),
|
||||||
|
]);
|
||||||
|
console.log('[Auth] Session cleared');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Auth] Failed to clear auth:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sign in with username and password
|
||||||
|
*/
|
||||||
|
const signIn = async (credentials: LoginRequest) => {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
const response = await authService.login(credentials);
|
||||||
|
setToken(response.access_token);
|
||||||
|
setUser(response.user);
|
||||||
|
await saveAuth(response.access_token, response.user);
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sign up and automatically sign in
|
||||||
|
*/
|
||||||
|
const signUp = async (data: RegisterRequest) => {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
await authService.register(data);
|
||||||
|
// After successful registration, sign in automatically
|
||||||
|
await signIn({ username: data.username, password: data.password });
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sign out and clear stored auth
|
||||||
|
*/
|
||||||
|
const signOut = () => {
|
||||||
|
setUser(null);
|
||||||
|
setToken(null);
|
||||||
|
clearAuth();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AuthContext.Provider
|
||||||
|
value={{
|
||||||
|
user,
|
||||||
|
token,
|
||||||
|
isLoading,
|
||||||
|
isInitializing,
|
||||||
|
signIn,
|
||||||
|
signUp,
|
||||||
|
signOut
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</AuthContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Hook
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export function useAuth() {
|
||||||
|
const context = useContext(AuthContext);
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error('useAuth must be used within an AuthProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
20
src/navigation/AuthNavigator.tsx
Normal file
20
src/navigation/AuthNavigator.tsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { createNativeStackNavigator } from '@react-navigation/native-stack';
|
||||||
|
import LoginScreen from '../screens/LoginScreen';
|
||||||
|
import RegisterScreen from '../screens/RegisterScreen';
|
||||||
|
|
||||||
|
const Stack = createNativeStackNavigator();
|
||||||
|
|
||||||
|
export default function AuthNavigator() {
|
||||||
|
return (
|
||||||
|
<Stack.Navigator
|
||||||
|
screenOptions={{
|
||||||
|
headerShown: false,
|
||||||
|
animation: 'slide_from_right',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack.Screen name="Login" component={LoginScreen} />
|
||||||
|
<Stack.Screen name="Register" component={RegisterScreen} />
|
||||||
|
</Stack.Navigator>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -7,7 +7,7 @@ import { colors, borderRadius, typography } from '../theme/colors';
|
|||||||
// Screens
|
// Screens
|
||||||
import FlowScreen from '../screens/FlowScreen';
|
import FlowScreen from '../screens/FlowScreen';
|
||||||
import SentinelScreen from '../screens/SentinelScreen';
|
import SentinelScreen from '../screens/SentinelScreen';
|
||||||
import HeritageScreen from '../screens/HeritageScreen';
|
// import HeritageScreen from '../screens/HeritageScreen'; // Heritage functionality moved to Me and Sentinel
|
||||||
import MeScreen from '../screens/MeScreen';
|
import MeScreen from '../screens/MeScreen';
|
||||||
|
|
||||||
const Tab = createBottomTabNavigator();
|
const Tab = createBottomTabNavigator();
|
||||||
@@ -104,6 +104,7 @@ export default function TabNavigator() {
|
|||||||
tabBarStyle: styles.tabBarDark,
|
tabBarStyle: styles.tabBarDark,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
{/* Heritage tab commented out - functionality moved to Me (Fleet Legacy) and Sentinel (Shadow Vault)
|
||||||
<Tab.Screen
|
<Tab.Screen
|
||||||
name="Heritage"
|
name="Heritage"
|
||||||
component={HeritageScreen}
|
component={HeritageScreen}
|
||||||
@@ -118,6 +119,7 @@ export default function TabNavigator() {
|
|||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
*/}
|
||||||
<Tab.Screen
|
<Tab.Screen
|
||||||
name="Me"
|
name="Me"
|
||||||
component={MeScreen}
|
component={MeScreen}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
256
src/screens/LoginScreen.tsx
Normal file
256
src/screens/LoginScreen.tsx
Normal file
@@ -0,0 +1,256 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import {
|
||||||
|
View,
|
||||||
|
Text,
|
||||||
|
StyleSheet,
|
||||||
|
TextInput,
|
||||||
|
TouchableOpacity,
|
||||||
|
KeyboardAvoidingView,
|
||||||
|
Platform,
|
||||||
|
SafeAreaView,
|
||||||
|
Alert,
|
||||||
|
ActivityIndicator,
|
||||||
|
} from 'react-native';
|
||||||
|
import { LinearGradient } from 'expo-linear-gradient';
|
||||||
|
import { Feather, MaterialCommunityIcons } from '@expo/vector-icons';
|
||||||
|
import { colors, spacing, borderRadius, typography, shadows } from '../theme/colors';
|
||||||
|
import { useAuth } from '../context/AuthContext';
|
||||||
|
|
||||||
|
export default function LoginScreen({ navigation }: any) {
|
||||||
|
const [email, setEmail] = useState('');
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const { signIn, isLoading } = useAuth();
|
||||||
|
|
||||||
|
const handleLogin = async () => {
|
||||||
|
setError(null);
|
||||||
|
if (!email || !password) {
|
||||||
|
setError('Please enter both username and password.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await signIn({ username: email, password });
|
||||||
|
} catch (err: any) {
|
||||||
|
setError('Login failed. Please check your credentials.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<LinearGradient
|
||||||
|
colors={[colors.sentinel.backgroundGradientStart, colors.sentinel.backgroundGradientEnd]}
|
||||||
|
style={styles.gradient}
|
||||||
|
>
|
||||||
|
<SafeAreaView style={styles.safeArea}>
|
||||||
|
<KeyboardAvoidingView
|
||||||
|
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||||
|
style={styles.content}
|
||||||
|
>
|
||||||
|
<View style={styles.header}>
|
||||||
|
<View style={styles.logoContainer}>
|
||||||
|
<MaterialCommunityIcons name="anchor" size={48} color={colors.sentinel.primary} />
|
||||||
|
</View>
|
||||||
|
<Text style={styles.title}>Welcome Aboard!</Text>
|
||||||
|
<Text style={styles.subtitle}>Sign in to access your sanctuaryy</Text>
|
||||||
|
{error && (
|
||||||
|
<View style={styles.errorContainer}>
|
||||||
|
<MaterialCommunityIcons name="alert-circle-outline" size={20} color={colors.sentinel.statusCritical} />
|
||||||
|
<Text style={styles.errorText}>{error}</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.form}>
|
||||||
|
<View style={styles.inputContainer}>
|
||||||
|
<Feather name="mail" size={20} color={colors.sentinel.textSecondary} style={styles.inputIcon} />
|
||||||
|
<TextInput
|
||||||
|
style={styles.input}
|
||||||
|
placeholder="Username"
|
||||||
|
placeholderTextColor={colors.sentinel.textSecondary}
|
||||||
|
value={email}
|
||||||
|
onChangeText={setEmail}
|
||||||
|
autoCapitalize="none"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.inputContainer}>
|
||||||
|
<Feather name="lock" size={20} color={colors.sentinel.textSecondary} style={styles.inputIcon} />
|
||||||
|
<TextInput
|
||||||
|
style={styles.input}
|
||||||
|
placeholder="Password"
|
||||||
|
placeholderTextColor={colors.sentinel.textSecondary}
|
||||||
|
value={password}
|
||||||
|
onChangeText={setPassword}
|
||||||
|
secureTextEntry
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<TouchableOpacity style={styles.forgotButton}>
|
||||||
|
<Text style={styles.forgotText}>Forgot Password?</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.loginButton}
|
||||||
|
activeOpacity={0.8}
|
||||||
|
onPress={handleLogin}
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<ActivityIndicator color={colors.white} />
|
||||||
|
) : (
|
||||||
|
<Text style={styles.loginButtonText}>Login</Text>
|
||||||
|
)}
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
<View style={styles.divider}>
|
||||||
|
<View style={styles.dividerLine} />
|
||||||
|
<Text style={styles.dividerText}>OR</Text>
|
||||||
|
<View style={styles.dividerLine} />
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.registerLink}
|
||||||
|
onPress={() => navigation.navigate('Register')}
|
||||||
|
>
|
||||||
|
<Text style={styles.registerText}>
|
||||||
|
Don't have an account? <Text style={styles.registerHighlight}>Join the Fleet</Text>
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</KeyboardAvoidingView>
|
||||||
|
</SafeAreaView>
|
||||||
|
</LinearGradient>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
gradient: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
safeArea: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
flex: 1,
|
||||||
|
paddingHorizontal: spacing.lg,
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: spacing.xxl,
|
||||||
|
},
|
||||||
|
logoContainer: {
|
||||||
|
width: 80,
|
||||||
|
height: 80,
|
||||||
|
borderRadius: borderRadius.full,
|
||||||
|
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
marginBottom: spacing.lg,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: 'rgba(184, 224, 229, 0.2)',
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
fontSize: typography.fontSize.xxl,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
color: colors.sentinel.text,
|
||||||
|
marginBottom: spacing.xs,
|
||||||
|
letterSpacing: 0.5,
|
||||||
|
},
|
||||||
|
subtitle: {
|
||||||
|
fontSize: typography.fontSize.base,
|
||||||
|
color: colors.sentinel.textSecondary,
|
||||||
|
marginBottom: spacing.md,
|
||||||
|
},
|
||||||
|
errorContainer: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
backgroundColor: 'rgba(255, 68, 68, 0.1)',
|
||||||
|
padding: spacing.sm,
|
||||||
|
borderRadius: borderRadius.md,
|
||||||
|
marginTop: spacing.sm,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: 'rgba(255, 68, 68, 0.2)',
|
||||||
|
},
|
||||||
|
errorText: {
|
||||||
|
color: colors.sentinel.statusCritical,
|
||||||
|
fontSize: typography.fontSize.sm,
|
||||||
|
marginLeft: spacing.xs,
|
||||||
|
fontWeight: '500',
|
||||||
|
},
|
||||||
|
form: {
|
||||||
|
width: '100%',
|
||||||
|
},
|
||||||
|
inputContainer: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
backgroundColor: 'rgba(255, 255, 255, 0.05)',
|
||||||
|
borderRadius: borderRadius.lg,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: 'rgba(184, 224, 229, 0.1)',
|
||||||
|
marginBottom: spacing.base,
|
||||||
|
height: 56,
|
||||||
|
paddingHorizontal: spacing.base,
|
||||||
|
},
|
||||||
|
inputIcon: {
|
||||||
|
marginRight: spacing.md,
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
flex: 1,
|
||||||
|
color: colors.sentinel.text,
|
||||||
|
fontSize: typography.fontSize.base,
|
||||||
|
},
|
||||||
|
forgotButton: {
|
||||||
|
alignSelf: 'flex-end',
|
||||||
|
marginBottom: spacing.lg,
|
||||||
|
},
|
||||||
|
forgotText: {
|
||||||
|
color: colors.sentinel.primary,
|
||||||
|
fontSize: typography.fontSize.sm,
|
||||||
|
},
|
||||||
|
loginButton: {
|
||||||
|
backgroundColor: colors.nautical.teal,
|
||||||
|
height: 56,
|
||||||
|
borderRadius: borderRadius.lg,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
...shadows.glow,
|
||||||
|
},
|
||||||
|
loginButtonText: {
|
||||||
|
color: colors.white,
|
||||||
|
fontSize: typography.fontSize.md,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
letterSpacing: 0.5,
|
||||||
|
},
|
||||||
|
divider: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginVertical: spacing.xl,
|
||||||
|
},
|
||||||
|
dividerLine: {
|
||||||
|
flex: 1,
|
||||||
|
height: 1,
|
||||||
|
backgroundColor: 'rgba(184, 224, 229, 0.1)',
|
||||||
|
},
|
||||||
|
dividerText: {
|
||||||
|
color: colors.sentinel.textSecondary,
|
||||||
|
paddingHorizontal: spacing.md,
|
||||||
|
fontSize: typography.fontSize.sm,
|
||||||
|
},
|
||||||
|
registerLink: {
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
registerText: {
|
||||||
|
color: colors.sentinel.textSecondary,
|
||||||
|
fontSize: typography.fontSize.base,
|
||||||
|
},
|
||||||
|
registerHighlight: {
|
||||||
|
color: colors.sentinel.primary,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -9,10 +9,67 @@ import {
|
|||||||
SafeAreaView,
|
SafeAreaView,
|
||||||
Linking,
|
Linking,
|
||||||
Alert,
|
Alert,
|
||||||
|
TextInput,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { LinearGradient } from 'expo-linear-gradient';
|
import { LinearGradient } from 'expo-linear-gradient';
|
||||||
import { Ionicons, Feather, MaterialCommunityIcons, FontAwesome5 } from '@expo/vector-icons';
|
import { Ionicons, Feather, MaterialCommunityIcons, FontAwesome5 } from '@expo/vector-icons';
|
||||||
import { colors, typography, spacing, borderRadius, shadows } from '../theme/colors';
|
import { colors, typography, spacing, borderRadius, shadows } from '../theme/colors';
|
||||||
|
import { useAuth } from '../context/AuthContext';
|
||||||
|
import { Heir, HeirStatus, PaymentStrategy } from '../types';
|
||||||
|
import HeritageScreen from './HeritageScreen';
|
||||||
|
|
||||||
|
// Mock heirs data
|
||||||
|
const initialHeirs: Heir[] = [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
name: 'John Smith',
|
||||||
|
email: 'john.smith@email.com',
|
||||||
|
phone: '+1 415 555 0132',
|
||||||
|
status: 'confirmed',
|
||||||
|
releaseLevel: 3,
|
||||||
|
releaseOrder: 1,
|
||||||
|
paymentStrategy: 'prepaid',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
name: 'Jane Doe',
|
||||||
|
email: 'jane.doe@email.com',
|
||||||
|
phone: '+1 212 555 0184',
|
||||||
|
status: 'confirmed',
|
||||||
|
releaseLevel: 2,
|
||||||
|
releaseOrder: 2,
|
||||||
|
paymentStrategy: 'pay_on_access',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
name: 'Alice Johnson',
|
||||||
|
email: 'alice.j@email.com',
|
||||||
|
phone: '+1 646 555 0149',
|
||||||
|
status: 'invited',
|
||||||
|
releaseLevel: 1,
|
||||||
|
releaseOrder: 3,
|
||||||
|
paymentStrategy: 'pay_on_access',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Release level descriptions
|
||||||
|
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',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
// Sentinel Protocol Status
|
// Sentinel Protocol Status
|
||||||
const protocolStatus = {
|
const protocolStatus = {
|
||||||
@@ -160,6 +217,19 @@ export default function MeScreen() {
|
|||||||
const [showSanctumModal, setShowSanctumModal] = useState(false);
|
const [showSanctumModal, setShowSanctumModal] = useState(false);
|
||||||
const [showCaptainFull, setShowCaptainFull] = useState(false);
|
const [showCaptainFull, setShowCaptainFull] = useState(false);
|
||||||
const [showTriggerModal, setShowTriggerModal] = useState(false);
|
const [showTriggerModal, setShowTriggerModal] = useState(false);
|
||||||
|
const [showHeritageModal, setShowHeritageModal] = useState(false);
|
||||||
|
const [showThemeModal, setShowThemeModal] = useState(false);
|
||||||
|
const [isDarkMode, setIsDarkMode] = useState(false);
|
||||||
|
|
||||||
|
// Heritage / Fleet Legacy states
|
||||||
|
const [heirs, setHeirs] = useState<Heir[]>(initialHeirs);
|
||||||
|
const [showAddHeirModal, setShowAddHeirModal] = useState(false);
|
||||||
|
const [showHeirDetailModal, setShowHeirDetailModal] = 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 [tideLevel, setTideLevel] = useState<'low' | 'high' | 'red'>('low');
|
const [tideLevel, setTideLevel] = useState<'low' | 'high' | 'red'>('low');
|
||||||
const [tideChannels, setTideChannels] = useState({
|
const [tideChannels, setTideChannels] = useState({
|
||||||
push: true,
|
push: true,
|
||||||
@@ -179,21 +249,60 @@ export default function MeScreen() {
|
|||||||
const [triggerGraceDays, setTriggerGraceDays] = useState(15);
|
const [triggerGraceDays, setTriggerGraceDays] = useState(15);
|
||||||
const [triggerSource, setTriggerSource] = useState<'dual' | 'subscription' | 'activity'>('dual');
|
const [triggerSource, setTriggerSource] = useState<'dual' | 'subscription' | 'activity'>('dual');
|
||||||
const [triggerKillSwitch, setTriggerKillSwitch] = useState(true);
|
const [triggerKillSwitch, setTriggerKillSwitch] = useState(true);
|
||||||
|
const { user, signOut } = useAuth();
|
||||||
|
|
||||||
const handleOpenLink = (url: string) => {
|
const handleOpenLink = (url: string) => {
|
||||||
Linking.openURL(url).catch(() => {});
|
Linking.openURL(url).catch(() => { });
|
||||||
|
};
|
||||||
|
|
||||||
|
// Heritage / Fleet Legacy functions
|
||||||
|
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]);
|
||||||
|
resetAddHeirForm();
|
||||||
|
setShowAddHeirModal(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetAddHeirForm = () => {
|
||||||
|
setNewHeirName('');
|
||||||
|
setNewHeirEmail('');
|
||||||
|
setNewHeirLevel(1);
|
||||||
|
setNewHeirPayment('pay_on_access');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleHeirPress = (heir: Heir) => {
|
||||||
|
setSelectedHeir(heir);
|
||||||
|
setShowHeirDetailModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getHeirStatusBadge = (status: HeirStatus) => {
|
||||||
|
if (status === 'confirmed') {
|
||||||
|
return { text: 'Aboard', color: '#6BBF8A', icon: 'checkmark-circle' };
|
||||||
|
}
|
||||||
|
return { text: 'Invited', color: colors.nautical.gold, icon: 'time-outline' };
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAbandonIsland = () => {
|
const handleAbandonIsland = () => {
|
||||||
Alert.alert(
|
Alert.alert(
|
||||||
'Abandon Island',
|
'Sign Out',
|
||||||
'Are you sure you want to delete your account? This action is irreversible. All your data, including vault contents, will be permanently destroyed.',
|
'Are you sure you want to sign out?',
|
||||||
[
|
[
|
||||||
{ text: 'Cancel', style: 'cancel' },
|
{ text: 'Cancel', style: 'cancel' },
|
||||||
{
|
{
|
||||||
text: 'Delete Account',
|
text: 'Sign Out',
|
||||||
style: 'destructive',
|
style: 'destructive',
|
||||||
onPress: () => Alert.alert('Account Deletion', 'Please contact support to proceed with account deletion.')
|
onPress: signOut
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
@@ -211,14 +320,6 @@ export default function MeScreen() {
|
|||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
contentContainerStyle={styles.scrollContent}
|
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 */}
|
{/* Profile Card */}
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.profileCard}
|
style={styles.profileCard}
|
||||||
@@ -235,11 +336,11 @@ export default function MeScreen() {
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.profileInfo}>
|
<View style={styles.profileInfo}>
|
||||||
<Text style={styles.profileName}>Captain</Text>
|
<Text style={styles.profileName}>{user?.username || 'Captain'}</Text>
|
||||||
<Text style={styles.profileTitle}>MASTER OF THE SANCTUM</Text>
|
<Text style={styles.profileTitle}>MASTER OF THE SANCTUM</Text>
|
||||||
<View style={styles.profileBadge}>
|
<View style={styles.profileBadge}>
|
||||||
<MaterialCommunityIcons name="crown" size={12} color={colors.nautical.gold} />
|
<MaterialCommunityIcons name="crown" size={12} color={colors.nautical.gold} />
|
||||||
<Text style={styles.profileBadgeText}>Pro Member</Text>
|
<Text style={styles.profileBadgeText}>{user?.tier || 'Pro Member'}</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@@ -309,15 +410,23 @@ export default function MeScreen() {
|
|||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Abandon Island Button */}
|
{/* Fleet Legacy - Single Entry Point */}
|
||||||
|
<Text style={styles.sectionTitle}>FLEET LEGACY</Text>
|
||||||
|
<View style={styles.menuCard}>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.abandonButton}
|
style={styles.menuItem}
|
||||||
onPress={handleAbandonIsland}
|
onPress={() => setShowHeritageModal(true)}
|
||||||
activeOpacity={0.8}
|
|
||||||
>
|
>
|
||||||
<Feather name="log-out" size={18} color={colors.nautical.coral} />
|
<View style={[styles.menuIconContainer, { backgroundColor: `${colors.nautical.teal}20` }]}>
|
||||||
<Text style={styles.abandonButtonText}>ABANDON ISLAND</Text>
|
<MaterialCommunityIcons name="compass-outline" size={22} color={colors.nautical.teal} />
|
||||||
|
</View>
|
||||||
|
<View style={styles.menuContent}>
|
||||||
|
<Text style={styles.menuTitle}>Manage Heirs</Text>
|
||||||
|
<Text style={styles.menuSubtitle}>{heirs.length} trustees configured</Text>
|
||||||
|
</View>
|
||||||
|
<Feather name="chevron-right" size={18} color={colors.me.textSecondary} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
|
||||||
{/* Settings Menu */}
|
{/* Settings Menu */}
|
||||||
<Text style={styles.sectionTitle}>SETTINGS</Text>
|
<Text style={styles.sectionTitle}>SETTINGS</Text>
|
||||||
@@ -333,6 +442,9 @@ export default function MeScreen() {
|
|||||||
if (item.id === 'trigger') {
|
if (item.id === 'trigger') {
|
||||||
setShowTriggerModal(true);
|
setShowTriggerModal(true);
|
||||||
}
|
}
|
||||||
|
if (item.id === 'visual') {
|
||||||
|
setShowThemeModal(true);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<View style={[styles.menuIconContainer, { backgroundColor: `${item.color}15` }]}>
|
<View style={[styles.menuIconContainer, { backgroundColor: `${item.color}15` }]}>
|
||||||
@@ -347,44 +459,83 @@ export default function MeScreen() {
|
|||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* About Section */}
|
{/* About Section - Vertical List */}
|
||||||
<Text style={styles.sectionTitle}>ABOUT</Text>
|
<Text style={styles.sectionTitle}>ABOUT</Text>
|
||||||
<View style={styles.aboutGrid}>
|
<View style={styles.menuCard}>
|
||||||
{protocolExplainers.slice(0, 2).map((item) => (
|
{protocolExplainers.map((item, index) => (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
key={item.id}
|
key={item.id}
|
||||||
style={styles.aboutCard}
|
style={[
|
||||||
|
styles.menuItem,
|
||||||
|
index < protocolExplainers.length - 1 && styles.menuItemBorder
|
||||||
|
]}
|
||||||
onPress={() => setSelectedExplainer(item)}
|
onPress={() => setSelectedExplainer(item)}
|
||||||
>
|
>
|
||||||
<MaterialCommunityIcons name={item.icon as any} size={24} color={colors.me.primary} />
|
<View style={[styles.menuIconContainer, { backgroundColor: `${colors.me.primary}15` }]}>
|
||||||
<Text style={styles.aboutTitle}>{item.title}</Text>
|
<MaterialCommunityIcons name={item.icon as any} size={20} color={colors.me.primary} />
|
||||||
<Feather name="arrow-up-right" size={14} color={colors.me.textSecondary} />
|
</View>
|
||||||
|
<View style={styles.menuContent}>
|
||||||
|
<Text style={styles.menuTitle}>{item.title}</Text>
|
||||||
|
</View>
|
||||||
|
<Feather name="chevron-right" size={18} color={colors.me.textSecondary} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
))}
|
))}
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.menuItem, styles.menuItemBorder]}
|
||||||
|
onPress={() => handleOpenLink('https://github.com/sentinel')}
|
||||||
|
>
|
||||||
|
<View style={[styles.menuIconContainer, { backgroundColor: `${colors.nautical.navy}15` }]}>
|
||||||
|
<Ionicons name="logo-github" size={20} color={colors.nautical.navy} />
|
||||||
</View>
|
</View>
|
||||||
|
<View style={styles.menuContent}>
|
||||||
|
<Text style={styles.menuTitle}>GitHub</Text>
|
||||||
|
<Text style={styles.menuSubtitle}>View source code</Text>
|
||||||
|
</View>
|
||||||
|
<Feather name="external-link" size={16} color={colors.me.textSecondary} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.menuItem, styles.menuItemBorder]}
|
||||||
|
onPress={() => handleOpenLink('https://sentinel.app/privacy')}
|
||||||
|
>
|
||||||
|
<View style={[styles.menuIconContainer, { backgroundColor: `${colors.nautical.sage}15` }]}>
|
||||||
|
<Ionicons name="shield-checkmark-outline" size={20} color={colors.nautical.sage} />
|
||||||
|
</View>
|
||||||
|
<View style={styles.menuContent}>
|
||||||
|
<Text style={styles.menuTitle}>Privacy Policy</Text>
|
||||||
|
</View>
|
||||||
|
<Feather name="external-link" size={16} color={colors.me.textSecondary} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.menuItem}
|
||||||
|
onPress={() => handleOpenLink('https://sentinel.app/terms')}
|
||||||
|
>
|
||||||
|
<View style={[styles.menuIconContainer, { backgroundColor: `${colors.nautical.sage}15` }]}>
|
||||||
|
<Ionicons name="document-text-outline" size={20} color={colors.nautical.sage} />
|
||||||
|
</View>
|
||||||
|
<View style={styles.menuContent}>
|
||||||
|
<Text style={styles.menuTitle}>Terms of Service</Text>
|
||||||
|
</View>
|
||||||
|
<Feather name="external-link" size={16} color={colors.me.textSecondary} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Sign Out Button */}
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.abandonButton}
|
||||||
|
onPress={handleAbandonIsland}
|
||||||
|
activeOpacity={0.8}
|
||||||
|
>
|
||||||
|
<Feather name="log-out" size={18} color={colors.nautical.coral} />
|
||||||
|
<Text style={styles.abandonButtonText}>SIGN OUT</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<View style={styles.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.footerVersion}>A E T E R N A N A U T I C A V 2 . 0</Text> */}
|
||||||
<Text style={styles.footerTagline}>
|
<Text style={styles.footerTagline}>
|
||||||
The sea claims what is forgotten, but the Sanctuary keeps what is loved.
|
The sea claims what is forgotten, but the Sanctuary keeps what is loved.
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</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>
|
</ScrollView>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
</LinearGradient>
|
</LinearGradient>
|
||||||
@@ -425,6 +576,114 @@ export default function MeScreen() {
|
|||||||
</View>
|
</View>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
{/* Heritage Modal - Full HeritageScreen */}
|
||||||
|
<Modal
|
||||||
|
visible={showHeritageModal}
|
||||||
|
animationType="slide"
|
||||||
|
onRequestClose={() => setShowHeritageModal(false)}
|
||||||
|
>
|
||||||
|
<View style={styles.heritageModalContainer}>
|
||||||
|
<HeritageScreen />
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.heritageCloseButton}
|
||||||
|
onPress={() => setShowHeritageModal(false)}
|
||||||
|
activeOpacity={0.85}
|
||||||
|
>
|
||||||
|
<Ionicons name="close" size={20} color={colors.nautical.cream} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
{/* Theme Settings Modal */}
|
||||||
|
<Modal
|
||||||
|
visible={showThemeModal}
|
||||||
|
animationType="fade"
|
||||||
|
transparent
|
||||||
|
onRequestClose={() => setShowThemeModal(false)}
|
||||||
|
>
|
||||||
|
<View style={styles.spiritOverlay}>
|
||||||
|
<View style={styles.spiritModal}>
|
||||||
|
<View style={styles.spiritHeader}>
|
||||||
|
<View style={styles.spiritIcon}>
|
||||||
|
<MaterialCommunityIcons name="palette-outline" size={24} color={colors.me.primary} />
|
||||||
|
</View>
|
||||||
|
<Text style={styles.spiritTitle}>Visual Preferences</Text>
|
||||||
|
</View>
|
||||||
|
<ScrollView style={styles.spiritScroll} showsVerticalScrollIndicator={false}>
|
||||||
|
<View style={styles.sanctumSection}>
|
||||||
|
<Text style={styles.tideLabel}>APPEARANCE</Text>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.sanctumToggleRow}
|
||||||
|
onPress={() => setIsDarkMode(!isDarkMode)}
|
||||||
|
activeOpacity={0.85}
|
||||||
|
>
|
||||||
|
<View style={{ flexDirection: 'row', alignItems: 'center', gap: spacing.sm }}>
|
||||||
|
<Ionicons
|
||||||
|
name={isDarkMode ? 'moon' : 'sunny'}
|
||||||
|
size={18}
|
||||||
|
color={colors.me.primary}
|
||||||
|
/>
|
||||||
|
<Text style={styles.sanctumText}>Dark Mode</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[styles.sanctumToggle, isDarkMode && styles.sanctumToggleOn]}>
|
||||||
|
<Text style={styles.sanctumToggleText}>{isDarkMode ? 'ON' : 'OFF'}</Text>
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.sanctumSection}>
|
||||||
|
<Text style={styles.tideLabel}>CARD STYLE</Text>
|
||||||
|
<View style={styles.tideRow}>
|
||||||
|
{['Minimal', 'Standard', 'Rich'].map((style) => (
|
||||||
|
<TouchableOpacity
|
||||||
|
key={style}
|
||||||
|
style={[styles.tideButton, style === 'Standard' && styles.tideButtonActive]}
|
||||||
|
>
|
||||||
|
<Text style={[styles.tideButtonText, style === 'Standard' && styles.tideButtonTextActive]}>
|
||||||
|
{style}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.sanctumSection}>
|
||||||
|
<Text style={styles.tideLabel}>FONT SIZE</Text>
|
||||||
|
<View style={styles.tideRow}>
|
||||||
|
{['Small', 'Medium', 'Large'].map((size) => (
|
||||||
|
<TouchableOpacity
|
||||||
|
key={size}
|
||||||
|
style={[styles.tideButton, size === 'Medium' && styles.tideButtonActive]}
|
||||||
|
>
|
||||||
|
<Text style={[styles.tideButtonText, size === 'Medium' && styles.tideButtonTextActive]}>
|
||||||
|
{size}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</ScrollView>
|
||||||
|
<View style={styles.tideModalButtons}>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.confirmPulseButton}
|
||||||
|
activeOpacity={0.85}
|
||||||
|
onPress={() => setShowThemeModal(false)}
|
||||||
|
>
|
||||||
|
<Ionicons name="checkmark-circle" size={18} color={colors.nautical.teal} />
|
||||||
|
<Text style={styles.confirmPulseText}>Save</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.confirmPulseButton}
|
||||||
|
activeOpacity={0.85}
|
||||||
|
onPress={() => setShowThemeModal(false)}
|
||||||
|
>
|
||||||
|
<Ionicons name="close-circle" size={18} color={colors.nautical.teal} />
|
||||||
|
<Text style={styles.confirmPulseText}>Close</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
{/* Spirit Keys Modal */}
|
{/* Spirit Keys Modal */}
|
||||||
<Modal
|
<Modal
|
||||||
@@ -661,7 +920,7 @@ export default function MeScreen() {
|
|||||||
activeOpacity={1}
|
activeOpacity={1}
|
||||||
onPress={() => setShowTideModal(false)}
|
onPress={() => setShowTideModal(false)}
|
||||||
>
|
>
|
||||||
<TouchableOpacity activeOpacity={1} onPress={() => {}}>
|
<TouchableOpacity activeOpacity={1} onPress={() => { }}>
|
||||||
<View style={styles.spiritModal}>
|
<View style={styles.spiritModal}>
|
||||||
<View style={styles.spiritHeader}>
|
<View style={styles.spiritHeader}>
|
||||||
<View style={styles.spiritIcon}>
|
<View style={styles.spiritIcon}>
|
||||||
@@ -856,6 +1115,170 @@ export default function MeScreen() {
|
|||||||
</View>
|
</View>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
{/* Add Heir Modal */}
|
||||||
|
<Modal
|
||||||
|
visible={showAddHeirModal}
|
||||||
|
animationType="slide"
|
||||||
|
transparent
|
||||||
|
onRequestClose={() => setShowAddHeirModal(false)}
|
||||||
|
>
|
||||||
|
<View style={styles.modalOverlay}>
|
||||||
|
<View style={styles.modalContent}>
|
||||||
|
<View style={styles.modalHandle} />
|
||||||
|
<View style={styles.modalHeader}>
|
||||||
|
<View style={styles.modalIconContainer}>
|
||||||
|
<FontAwesome5 name="ship" size={20} color={colors.me.primary} />
|
||||||
|
</View>
|
||||||
|
<Text style={styles.modalTitle}>Add to Fleet</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<Text style={styles.inputLabel}>NAME *</Text>
|
||||||
|
<TextInput
|
||||||
|
style={styles.heirInput}
|
||||||
|
placeholder="Trustee name"
|
||||||
|
placeholderTextColor={colors.me.textSecondary}
|
||||||
|
value={newHeirName}
|
||||||
|
onChangeText={setNewHeirName}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Text style={styles.inputLabel}>EMAIL *</Text>
|
||||||
|
<TextInput
|
||||||
|
style={styles.heirInput}
|
||||||
|
placeholder="For invitations and notifications"
|
||||||
|
placeholderTextColor={colors.me.textSecondary}
|
||||||
|
value={newHeirEmail}
|
||||||
|
onChangeText={setNewHeirEmail}
|
||||||
|
keyboardType="email-address"
|
||||||
|
autoCapitalize="none"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Text style={styles.inputLabel}>INHERITANCE TIER</Text>
|
||||||
|
<View style={styles.tideRow}>
|
||||||
|
{[1, 2, 3].map((level) => (
|
||||||
|
<TouchableOpacity
|
||||||
|
key={level}
|
||||||
|
style={[styles.tideButton, newHeirLevel === level && styles.tideButtonActive]}
|
||||||
|
onPress={() => setNewHeirLevel(level)}
|
||||||
|
>
|
||||||
|
<Text style={[styles.tideButtonText, newHeirLevel === level && styles.tideButtonTextActive]}>
|
||||||
|
Tier {level}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<Text style={styles.inputLabel}>PAYMENT STRATEGY</Text>
|
||||||
|
<View style={styles.tideRow}>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.tideButton, newHeirPayment === 'prepaid' && styles.tideButtonActive]}
|
||||||
|
onPress={() => setNewHeirPayment('prepaid')}
|
||||||
|
>
|
||||||
|
<Text style={[styles.tideButtonText, newHeirPayment === 'prepaid' && styles.tideButtonTextActive]}>
|
||||||
|
Prepaid
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.tideButton, newHeirPayment === 'pay_on_access' && styles.tideButtonActive]}
|
||||||
|
onPress={() => setNewHeirPayment('pay_on_access')}
|
||||||
|
>
|
||||||
|
<Text style={[styles.tideButtonText, newHeirPayment === 'pay_on_access' && styles.tideButtonTextActive]}>
|
||||||
|
Pay on Access
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.tideModalButtons}>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.confirmPulseButton}
|
||||||
|
onPress={() => {
|
||||||
|
setShowAddHeirModal(false);
|
||||||
|
resetAddHeirForm();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text style={styles.confirmPulseText}>Cancel</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.confirmPulseButton, { backgroundColor: colors.nautical.teal }]}
|
||||||
|
onPress={handleAddHeir}
|
||||||
|
>
|
||||||
|
<Feather name="send" size={16} color="#fff" />
|
||||||
|
<Text style={[styles.confirmPulseText, { color: '#fff' }]}>Send Invitation</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
{/* Heir Detail Modal */}
|
||||||
|
<Modal
|
||||||
|
visible={showHeirDetailModal}
|
||||||
|
animationType="fade"
|
||||||
|
transparent
|
||||||
|
onRequestClose={() => setShowHeirDetailModal(false)}
|
||||||
|
>
|
||||||
|
<View style={styles.spiritOverlay}>
|
||||||
|
<View style={styles.spiritModal}>
|
||||||
|
{selectedHeir && (
|
||||||
|
<>
|
||||||
|
<View style={styles.heirDetailHeader}>
|
||||||
|
<View style={styles.heirDetailAvatar}>
|
||||||
|
<FontAwesome5 name="ship" size={28} color={colors.me.primary} />
|
||||||
|
</View>
|
||||||
|
<Text style={styles.heirDetailName}>{selectedHeir.name}</Text>
|
||||||
|
<View style={[
|
||||||
|
styles.heirStatusBadge,
|
||||||
|
{ backgroundColor: `${getHeirStatusBadge(selectedHeir.status).color}15` }
|
||||||
|
]}>
|
||||||
|
<Ionicons
|
||||||
|
name={getHeirStatusBadge(selectedHeir.status).icon as any}
|
||||||
|
size={12}
|
||||||
|
color={getHeirStatusBadge(selectedHeir.status).color}
|
||||||
|
/>
|
||||||
|
<Text style={[styles.heirStatusText, { color: getHeirStatusBadge(selectedHeir.status).color }]}>
|
||||||
|
{getHeirStatusBadge(selectedHeir.status).text}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.heirDetailRows}>
|
||||||
|
<View style={styles.heirDetailRow}>
|
||||||
|
<Feather name="mail" size={16} color={colors.me.textSecondary} />
|
||||||
|
<Text style={styles.heirDetailLabel}>Email</Text>
|
||||||
|
<Text style={styles.heirDetailValue}>{selectedHeir.email}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={styles.heirDetailRow}>
|
||||||
|
<Feather name="hash" size={16} color={colors.me.textSecondary} />
|
||||||
|
<Text style={styles.heirDetailLabel}>Order</Text>
|
||||||
|
<Text style={styles.heirDetailValue}>#{selectedHeir.releaseOrder}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={styles.heirDetailRow}>
|
||||||
|
<Feather name="layers" size={16} color={colors.me.textSecondary} />
|
||||||
|
<Text style={styles.heirDetailLabel}>Tier</Text>
|
||||||
|
<Text style={styles.heirDetailValue}>
|
||||||
|
{selectedHeir.releaseLevel} · {releaseLevelConfig[selectedHeir.releaseLevel].label}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View style={styles.heirDetailRow}>
|
||||||
|
<FontAwesome5 name="coins" size={14} color={colors.me.textSecondary} />
|
||||||
|
<Text style={styles.heirDetailLabel}>Payment</Text>
|
||||||
|
<Text style={styles.heirDetailValue}>
|
||||||
|
{selectedHeir.paymentStrategy === 'prepaid' ? 'Prepaid' : 'Pay on Access'}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.closeButton}
|
||||||
|
onPress={() => setShowHeirDetailModal(false)}
|
||||||
|
>
|
||||||
|
<Text style={styles.closeButtonText}>Close</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
{/* Trigger Logic Modal */}
|
{/* Trigger Logic Modal */}
|
||||||
<Modal
|
<Modal
|
||||||
visible={showTriggerModal}
|
visible={showTriggerModal}
|
||||||
@@ -1008,28 +1431,9 @@ const styles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
scrollContent: {
|
scrollContent: {
|
||||||
paddingHorizontal: spacing.base,
|
paddingHorizontal: spacing.base,
|
||||||
|
paddingTop: spacing.md,
|
||||||
paddingBottom: 100,
|
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
|
// Profile Card
|
||||||
profileCard: {
|
profileCard: {
|
||||||
backgroundColor: colors.me.cardBackground,
|
backgroundColor: colors.me.cardBackground,
|
||||||
@@ -1758,4 +2162,175 @@ const styles = StyleSheet.create({
|
|||||||
color: colors.me.primary,
|
color: colors.me.primary,
|
||||||
fontWeight: '600',
|
fontWeight: '600',
|
||||||
},
|
},
|
||||||
|
// Fleet Legacy styles
|
||||||
|
fleetLegalNotice: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
backgroundColor: colors.me.cardBackground,
|
||||||
|
borderRadius: borderRadius.xl,
|
||||||
|
padding: spacing.base,
|
||||||
|
marginBottom: spacing.md,
|
||||||
|
borderLeftWidth: 4,
|
||||||
|
borderLeftColor: colors.nautical.teal,
|
||||||
|
...shadows.soft,
|
||||||
|
},
|
||||||
|
fleetLegalIcon: {
|
||||||
|
marginRight: spacing.md,
|
||||||
|
},
|
||||||
|
fleetLegalContent: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
fleetLegalTitle: {
|
||||||
|
fontSize: typography.fontSize.base,
|
||||||
|
fontWeight: '600',
|
||||||
|
color: colors.me.text,
|
||||||
|
marginBottom: spacing.xs,
|
||||||
|
},
|
||||||
|
fleetLegalText: {
|
||||||
|
fontSize: typography.fontSize.sm,
|
||||||
|
color: colors.me.textSecondary,
|
||||||
|
lineHeight: typography.fontSize.sm * 1.5,
|
||||||
|
},
|
||||||
|
fleetHeader: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: spacing.sm,
|
||||||
|
padding: spacing.base,
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderBottomColor: colors.me.cardBorder,
|
||||||
|
},
|
||||||
|
fleetHeaderTitle: {
|
||||||
|
flex: 1,
|
||||||
|
fontSize: typography.fontSize.xs,
|
||||||
|
fontWeight: '700',
|
||||||
|
color: colors.me.textSecondary,
|
||||||
|
letterSpacing: typography.letterSpacing.widest,
|
||||||
|
},
|
||||||
|
fleetCountBadge: {
|
||||||
|
backgroundColor: colors.nautical.lightMint,
|
||||||
|
paddingHorizontal: spacing.sm,
|
||||||
|
paddingVertical: 4,
|
||||||
|
borderRadius: borderRadius.full,
|
||||||
|
},
|
||||||
|
fleetCountText: {
|
||||||
|
fontSize: typography.fontSize.xs,
|
||||||
|
fontWeight: '600',
|
||||||
|
color: colors.nautical.teal,
|
||||||
|
},
|
||||||
|
heirAvatar: {
|
||||||
|
width: 44,
|
||||||
|
height: 44,
|
||||||
|
borderRadius: 22,
|
||||||
|
backgroundColor: colors.nautical.lightMint,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginRight: spacing.md,
|
||||||
|
},
|
||||||
|
heirNameRow: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: spacing.sm,
|
||||||
|
},
|
||||||
|
heirStatusBadge: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 4,
|
||||||
|
paddingHorizontal: spacing.sm,
|
||||||
|
paddingVertical: 2,
|
||||||
|
borderRadius: borderRadius.full,
|
||||||
|
},
|
||||||
|
heirStatusText: {
|
||||||
|
fontSize: typography.fontSize.xs,
|
||||||
|
fontWeight: '600',
|
||||||
|
},
|
||||||
|
addTrusteeButton: {
|
||||||
|
borderRadius: borderRadius.lg,
|
||||||
|
overflow: 'hidden',
|
||||||
|
marginBottom: spacing.md,
|
||||||
|
...shadows.soft,
|
||||||
|
},
|
||||||
|
addTrusteeGradient: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
paddingVertical: spacing.md,
|
||||||
|
gap: spacing.sm,
|
||||||
|
},
|
||||||
|
addTrusteeText: {
|
||||||
|
fontSize: typography.fontSize.base,
|
||||||
|
fontWeight: '700',
|
||||||
|
color: '#fff',
|
||||||
|
},
|
||||||
|
inputLabel: {
|
||||||
|
fontSize: typography.fontSize.xs,
|
||||||
|
color: colors.me.textSecondary,
|
||||||
|
marginBottom: spacing.xs,
|
||||||
|
marginTop: spacing.md,
|
||||||
|
fontWeight: '600',
|
||||||
|
letterSpacing: typography.letterSpacing.wide,
|
||||||
|
},
|
||||||
|
heirInput: {
|
||||||
|
backgroundColor: colors.nautical.paleAqua,
|
||||||
|
borderRadius: borderRadius.lg,
|
||||||
|
padding: spacing.base,
|
||||||
|
fontSize: typography.fontSize.base,
|
||||||
|
color: colors.me.text,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: colors.me.cardBorder,
|
||||||
|
},
|
||||||
|
heirDetailHeader: {
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: spacing.lg,
|
||||||
|
paddingBottom: spacing.md,
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderBottomColor: colors.me.cardBorder,
|
||||||
|
},
|
||||||
|
heirDetailAvatar: {
|
||||||
|
width: 72,
|
||||||
|
height: 72,
|
||||||
|
borderRadius: 36,
|
||||||
|
backgroundColor: colors.nautical.lightMint,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: spacing.md,
|
||||||
|
},
|
||||||
|
heirDetailName: {
|
||||||
|
fontSize: typography.fontSize.lg,
|
||||||
|
fontWeight: '600',
|
||||||
|
color: colors.me.text,
|
||||||
|
marginBottom: spacing.sm,
|
||||||
|
},
|
||||||
|
heirDetailRows: {
|
||||||
|
gap: spacing.md,
|
||||||
|
},
|
||||||
|
heirDetailRow: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: spacing.sm,
|
||||||
|
},
|
||||||
|
heirDetailLabel: {
|
||||||
|
flex: 1,
|
||||||
|
fontSize: typography.fontSize.base,
|
||||||
|
color: colors.me.textSecondary,
|
||||||
|
},
|
||||||
|
heirDetailValue: {
|
||||||
|
fontSize: typography.fontSize.base,
|
||||||
|
color: colors.me.text,
|
||||||
|
fontWeight: '600',
|
||||||
|
},
|
||||||
|
// Heritage Modal
|
||||||
|
heritageModalContainer: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
heritageCloseButton: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: spacing.xl + spacing.lg,
|
||||||
|
right: spacing.lg,
|
||||||
|
width: 36,
|
||||||
|
height: 36,
|
||||||
|
borderRadius: 18,
|
||||||
|
backgroundColor: 'rgba(26, 58, 74, 0.65)',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
zIndex: 10,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
290
src/screens/RegisterScreen.tsx
Normal file
290
src/screens/RegisterScreen.tsx
Normal file
@@ -0,0 +1,290 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import {
|
||||||
|
View,
|
||||||
|
Text,
|
||||||
|
StyleSheet,
|
||||||
|
TextInput,
|
||||||
|
TouchableOpacity,
|
||||||
|
KeyboardAvoidingView,
|
||||||
|
Platform,
|
||||||
|
SafeAreaView,
|
||||||
|
ScrollView,
|
||||||
|
Alert,
|
||||||
|
} from 'react-native';
|
||||||
|
import { LinearGradient } from 'expo-linear-gradient';
|
||||||
|
import { Feather, MaterialCommunityIcons } from '@expo/vector-icons';
|
||||||
|
import { colors, spacing, borderRadius, typography, shadows } from '../theme/colors';
|
||||||
|
import { useAuth } from '../context/AuthContext';
|
||||||
|
import { ActivityIndicator } from 'react-native';
|
||||||
|
|
||||||
|
export default function RegisterScreen({ navigation }: any) {
|
||||||
|
const [name, setName] = useState('');
|
||||||
|
const [email, setEmail] = useState('');
|
||||||
|
const [password, setPassword] = useState('');
|
||||||
|
const [confirmPassword, setConfirmPassword] = useState('');
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const { signUp, isLoading } = useAuth();
|
||||||
|
|
||||||
|
const handleRegister = async () => {
|
||||||
|
setError(null);
|
||||||
|
if (!name || !email || !password || !confirmPassword) {
|
||||||
|
setError('Please fill in all fields.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password !== confirmPassword) {
|
||||||
|
setError('Passwords do not match.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||||
|
if (!emailRegex.test(email)) {
|
||||||
|
setError('Please enter a valid email address.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await signUp({ username: name, email, password });
|
||||||
|
} catch (err: any) {
|
||||||
|
setError(err.message || 'Registration failed. Please try again.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<LinearGradient
|
||||||
|
colors={[colors.sentinel.backgroundGradientStart, colors.sentinel.backgroundGradientEnd]}
|
||||||
|
style={styles.gradient}
|
||||||
|
>
|
||||||
|
<SafeAreaView style={styles.safeArea}>
|
||||||
|
<KeyboardAvoidingView
|
||||||
|
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||||
|
style={styles.content}
|
||||||
|
>
|
||||||
|
<ScrollView
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
contentContainerStyle={styles.scrollContent}
|
||||||
|
>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.backButton}
|
||||||
|
onPress={() => navigation.goBack()}
|
||||||
|
>
|
||||||
|
<Feather name="arrow-left" size={24} color={colors.sentinel.text} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
<View style={styles.header}>
|
||||||
|
<View style={styles.logoContainer}>
|
||||||
|
<MaterialCommunityIcons name="compass-outline" size={48} color={colors.sentinel.primary} />
|
||||||
|
</View>
|
||||||
|
<Text style={styles.title}>Join the Fleet</Text>
|
||||||
|
<Text style={styles.subtitle}>Begin your journey with Sentinel</Text>
|
||||||
|
{error && (
|
||||||
|
<View style={styles.errorContainer}>
|
||||||
|
<MaterialCommunityIcons name="alert-circle-outline" size={20} color={colors.sentinel.statusCritical} />
|
||||||
|
<Text style={styles.errorText}>{error}</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.form}>
|
||||||
|
<View style={styles.inputContainer}>
|
||||||
|
<Feather name="user" size={20} color={colors.sentinel.textSecondary} style={styles.inputIcon} />
|
||||||
|
<TextInput
|
||||||
|
style={styles.input}
|
||||||
|
placeholder="Captain's Name"
|
||||||
|
placeholderTextColor={colors.sentinel.textSecondary}
|
||||||
|
value={name}
|
||||||
|
onChangeText={setName}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.inputContainer}>
|
||||||
|
<Feather name="mail" size={20} color={colors.sentinel.textSecondary} style={styles.inputIcon} />
|
||||||
|
<TextInput
|
||||||
|
style={styles.input}
|
||||||
|
placeholder="Email"
|
||||||
|
placeholderTextColor={colors.sentinel.textSecondary}
|
||||||
|
value={email}
|
||||||
|
onChangeText={setEmail}
|
||||||
|
autoCapitalize="none"
|
||||||
|
keyboardType="email-address"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.inputContainer}>
|
||||||
|
<Feather name="lock" size={20} color={colors.sentinel.textSecondary} style={styles.inputIcon} />
|
||||||
|
<TextInput
|
||||||
|
style={styles.input}
|
||||||
|
placeholder="Password"
|
||||||
|
placeholderTextColor={colors.sentinel.textSecondary}
|
||||||
|
value={password}
|
||||||
|
onChangeText={setPassword}
|
||||||
|
secureTextEntry
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.inputContainer}>
|
||||||
|
<Feather name="check-circle" size={20} color={colors.sentinel.textSecondary} style={styles.inputIcon} />
|
||||||
|
<TextInput
|
||||||
|
style={styles.input}
|
||||||
|
placeholder="Confirm Password"
|
||||||
|
placeholderTextColor={colors.sentinel.textSecondary}
|
||||||
|
value={confirmPassword}
|
||||||
|
onChangeText={setConfirmPassword}
|
||||||
|
secureTextEntry
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.registerButton, isLoading && styles.disabledButton]}
|
||||||
|
activeOpacity={0.8}
|
||||||
|
onPress={handleRegister}
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<ActivityIndicator color={colors.white} />
|
||||||
|
) : (
|
||||||
|
<Text style={styles.registerButtonText}>Create Account</Text>
|
||||||
|
)}
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.loginLink}
|
||||||
|
onPress={() => navigation.navigate('Login')}
|
||||||
|
>
|
||||||
|
<Text style={styles.loginLinkText}>
|
||||||
|
Already have an account? <Text style={styles.highlight}>Login</Text>
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</ScrollView>
|
||||||
|
</KeyboardAvoidingView>
|
||||||
|
</SafeAreaView>
|
||||||
|
</LinearGradient>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
gradient: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
safeArea: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
scrollContent: {
|
||||||
|
flexGrow: 1,
|
||||||
|
paddingHorizontal: spacing.lg,
|
||||||
|
paddingBottom: spacing.xl,
|
||||||
|
},
|
||||||
|
backButton: {
|
||||||
|
marginTop: spacing.md,
|
||||||
|
marginBottom: spacing.lg,
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
borderRadius: borderRadius.full,
|
||||||
|
backgroundColor: 'rgba(255, 255, 255, 0.05)',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: spacing.xl,
|
||||||
|
},
|
||||||
|
logoContainer: {
|
||||||
|
width: 80,
|
||||||
|
height: 80,
|
||||||
|
borderRadius: borderRadius.full,
|
||||||
|
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
marginBottom: spacing.lg,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: 'rgba(184, 224, 229, 0.2)',
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
fontSize: typography.fontSize.xxl,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
color: colors.sentinel.text,
|
||||||
|
marginBottom: spacing.xs,
|
||||||
|
letterSpacing: 0.5,
|
||||||
|
},
|
||||||
|
subtitle: {
|
||||||
|
fontSize: typography.fontSize.base,
|
||||||
|
color: colors.sentinel.textSecondary,
|
||||||
|
},
|
||||||
|
form: {
|
||||||
|
width: '100%',
|
||||||
|
},
|
||||||
|
inputContainer: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
backgroundColor: 'rgba(255, 255, 255, 0.05)',
|
||||||
|
borderRadius: borderRadius.lg,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: 'rgba(184, 224, 229, 0.1)',
|
||||||
|
marginBottom: spacing.base,
|
||||||
|
height: 56,
|
||||||
|
paddingHorizontal: spacing.base,
|
||||||
|
},
|
||||||
|
inputIcon: {
|
||||||
|
marginRight: spacing.md,
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
flex: 1,
|
||||||
|
color: colors.sentinel.text,
|
||||||
|
fontSize: typography.fontSize.base,
|
||||||
|
},
|
||||||
|
registerButton: {
|
||||||
|
backgroundColor: colors.nautical.seafoam,
|
||||||
|
height: 56,
|
||||||
|
borderRadius: borderRadius.lg,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
marginTop: spacing.md,
|
||||||
|
...shadows.glow,
|
||||||
|
},
|
||||||
|
registerButtonText: {
|
||||||
|
color: colors.white,
|
||||||
|
fontSize: typography.fontSize.md,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
letterSpacing: 0.5,
|
||||||
|
},
|
||||||
|
loginLink: {
|
||||||
|
alignItems: 'center',
|
||||||
|
marginTop: spacing.xl,
|
||||||
|
},
|
||||||
|
loginLinkText: {
|
||||||
|
color: colors.sentinel.textSecondary,
|
||||||
|
fontSize: typography.fontSize.base,
|
||||||
|
},
|
||||||
|
highlight: {
|
||||||
|
color: colors.nautical.seafoam,
|
||||||
|
fontWeight: 'bold',
|
||||||
|
},
|
||||||
|
disabledButton: {
|
||||||
|
opacity: 0.7,
|
||||||
|
},
|
||||||
|
errorContainer: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
backgroundColor: 'rgba(255, 68, 68, 0.1)',
|
||||||
|
padding: spacing.sm,
|
||||||
|
borderRadius: borderRadius.md,
|
||||||
|
marginTop: spacing.md,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: 'rgba(255, 68, 68, 0.2)',
|
||||||
|
},
|
||||||
|
errorText: {
|
||||||
|
color: colors.sentinel.statusCritical,
|
||||||
|
fontSize: typography.fontSize.sm,
|
||||||
|
marginLeft: spacing.xs,
|
||||||
|
fontWeight: '500',
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
Text,
|
Text,
|
||||||
@@ -7,17 +7,101 @@ import {
|
|||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
SafeAreaView,
|
SafeAreaView,
|
||||||
Animated,
|
Animated,
|
||||||
|
Modal,
|
||||||
|
TextInput,
|
||||||
|
KeyboardAvoidingView,
|
||||||
|
Platform,
|
||||||
|
Share,
|
||||||
|
Alert,
|
||||||
|
Linking,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { LinearGradient } from 'expo-linear-gradient';
|
import { LinearGradient } from 'expo-linear-gradient';
|
||||||
import { Ionicons, Feather, MaterialCommunityIcons, FontAwesome5 } from '@expo/vector-icons';
|
import { Ionicons, Feather, MaterialCommunityIcons, FontAwesome5 } from '@expo/vector-icons';
|
||||||
|
import { captureRef } from 'react-native-view-shot';
|
||||||
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
import { colors, typography, spacing, borderRadius, shadows } from '../theme/colors';
|
import { colors, typography, spacing, borderRadius, shadows } from '../theme/colors';
|
||||||
import { SystemStatus, KillSwitchLog } from '../types';
|
import { SystemStatus, KillSwitchLog } from '../types';
|
||||||
|
import VaultScreen from './VaultScreen';
|
||||||
|
import {
|
||||||
|
SSSShare,
|
||||||
|
mnemonicToEntropy,
|
||||||
|
splitSecret,
|
||||||
|
formatShareCompact,
|
||||||
|
serializeShare,
|
||||||
|
verifyShares,
|
||||||
|
} from '../utils/sss';
|
||||||
|
|
||||||
|
// Nautical-themed mnemonic word list (unique words only)
|
||||||
|
const MNEMONIC_WORDS = [
|
||||||
|
'anchor', 'harbor', 'compass', 'lighthouse', 'current', 'ocean', 'tide', 'voyage',
|
||||||
|
'keel', 'stern', 'bow', 'mast', 'sail', 'port', 'starboard', 'reef',
|
||||||
|
'signal', 'beacon', 'chart', 'helm', 'gale', 'calm', 'cove', 'isle',
|
||||||
|
'horizon', 'sextant', 'sound', 'drift', 'wake', 'mariner', 'pilot', 'fathom',
|
||||||
|
'buoy', 'lantern', 'harpoon', 'lagoon', 'bay', 'strait', 'riptide', 'foam',
|
||||||
|
'coral', 'pearl', 'trident', 'ebb', 'flow', 'vault', 'cipher', 'shroud',
|
||||||
|
'salt', 'wave', 'grotto', 'storm', 'north', 'south', 'east', 'west',
|
||||||
|
'ember', 'cabin', 'ledger', 'torch', 'sanctum', 'oath', 'depths', 'captain',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
// Animation timing constants
|
||||||
|
const ANIMATION_DURATION = {
|
||||||
|
pulse: 1200,
|
||||||
|
glow: 1500,
|
||||||
|
rotate: 30000,
|
||||||
|
heartbeatPress: 150,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const generateMnemonic = (wordCount = 12) => {
|
||||||
|
const words: string[] = [];
|
||||||
|
for (let i = 0; i < wordCount; i += 1) {
|
||||||
|
const index = Math.floor(Math.random() * MNEMONIC_WORDS.length);
|
||||||
|
words.push(MNEMONIC_WORDS[index]);
|
||||||
|
}
|
||||||
|
return words;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate SSS shares from mnemonic words
|
||||||
|
* Uses Shamir's Secret Sharing (3,2) threshold scheme
|
||||||
|
*/
|
||||||
|
const generateSSSShares = (words: string[]): SSSShare[] => {
|
||||||
|
try {
|
||||||
|
// Convert mnemonic to entropy (big integer)
|
||||||
|
const entropy = mnemonicToEntropy(words, MNEMONIC_WORDS);
|
||||||
|
|
||||||
|
// Split entropy into 3 shares using SSS
|
||||||
|
const shares = splitSecret(entropy);
|
||||||
|
|
||||||
|
// Verify shares can recover the original (optional, for debugging)
|
||||||
|
if (__DEV__) {
|
||||||
|
const isValid = verifyShares(shares, entropy);
|
||||||
|
if (!isValid) {
|
||||||
|
console.warn('SSS verification failed!');
|
||||||
|
} else {
|
||||||
|
console.log('SSS shares verified successfully');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return shares;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to generate SSS shares:', error);
|
||||||
|
// Fallback: return empty shares (should not happen in production)
|
||||||
|
return [
|
||||||
|
{ x: 1, y: BigInt(0), label: 'device' },
|
||||||
|
{ x: 2, y: BigInt(0), label: 'cloud' },
|
||||||
|
{ x: 3, y: BigInt(0), label: 'heir' },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Icon names type for type safety
|
||||||
|
type StatusIconName = 'checkmark-circle' | 'warning' | 'alert-circle';
|
||||||
|
|
||||||
// Status configuration with nautical theme
|
// Status configuration with nautical theme
|
||||||
const statusConfig: Record<SystemStatus, {
|
const statusConfig: Record<SystemStatus, {
|
||||||
color: string;
|
color: string;
|
||||||
label: string;
|
label: string;
|
||||||
icon: string;
|
icon: StatusIconName;
|
||||||
description: string;
|
description: string;
|
||||||
gradientColors: [string, string];
|
gradientColors: [string, string];
|
||||||
}> = {
|
}> = {
|
||||||
@@ -76,72 +160,155 @@ export default function SentinelScreen() {
|
|||||||
const [pulseAnim] = useState(new Animated.Value(1));
|
const [pulseAnim] = useState(new Animated.Value(1));
|
||||||
const [glowAnim] = useState(new Animated.Value(0.5));
|
const [glowAnim] = useState(new Animated.Value(0.5));
|
||||||
const [rotateAnim] = useState(new Animated.Value(0));
|
const [rotateAnim] = useState(new Animated.Value(0));
|
||||||
|
const [showVault, setShowVault] = useState(false);
|
||||||
|
const [showMnemonic, setShowMnemonic] = useState(false);
|
||||||
|
const [mnemonicWords, setMnemonicWords] = useState<string[]>([]);
|
||||||
|
const [sssShares, setSssShares] = useState<SSSShare[]>([]);
|
||||||
|
const [showEmailForm, setShowEmailForm] = useState(false);
|
||||||
|
const [emailAddress, setEmailAddress] = useState('');
|
||||||
|
const [isCapturing, setIsCapturing] = useState(false);
|
||||||
|
const mnemonicRef = useRef<View>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Pulse animation
|
// Pulse animation
|
||||||
Animated.loop(
|
const pulseAnimation = Animated.loop(
|
||||||
Animated.sequence([
|
Animated.sequence([
|
||||||
Animated.timing(pulseAnim, {
|
Animated.timing(pulseAnim, {
|
||||||
toValue: 1.06,
|
toValue: 1.06,
|
||||||
duration: 1200,
|
duration: ANIMATION_DURATION.pulse,
|
||||||
useNativeDriver: true,
|
useNativeDriver: true,
|
||||||
}),
|
}),
|
||||||
Animated.timing(pulseAnim, {
|
Animated.timing(pulseAnim, {
|
||||||
toValue: 1,
|
toValue: 1,
|
||||||
duration: 1200,
|
duration: ANIMATION_DURATION.pulse,
|
||||||
useNativeDriver: true,
|
useNativeDriver: true,
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
).start();
|
);
|
||||||
|
pulseAnimation.start();
|
||||||
|
|
||||||
// Glow animation
|
// Glow animation
|
||||||
Animated.loop(
|
const glowAnimation = Animated.loop(
|
||||||
Animated.sequence([
|
Animated.sequence([
|
||||||
Animated.timing(glowAnim, {
|
Animated.timing(glowAnim, {
|
||||||
toValue: 1,
|
toValue: 1,
|
||||||
duration: 1500,
|
duration: ANIMATION_DURATION.glow,
|
||||||
useNativeDriver: true,
|
useNativeDriver: true,
|
||||||
}),
|
}),
|
||||||
Animated.timing(glowAnim, {
|
Animated.timing(glowAnim, {
|
||||||
toValue: 0.5,
|
toValue: 0.5,
|
||||||
duration: 1500,
|
duration: ANIMATION_DURATION.glow,
|
||||||
useNativeDriver: true,
|
useNativeDriver: true,
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
).start();
|
);
|
||||||
|
glowAnimation.start();
|
||||||
|
|
||||||
// Slow rotate for ship wheel
|
// Slow rotate for ship wheel
|
||||||
Animated.loop(
|
const rotateAnimation = Animated.loop(
|
||||||
Animated.timing(rotateAnim, {
|
Animated.timing(rotateAnim, {
|
||||||
toValue: 1,
|
toValue: 1,
|
||||||
duration: 30000,
|
duration: ANIMATION_DURATION.rotate,
|
||||||
useNativeDriver: true,
|
useNativeDriver: true,
|
||||||
})
|
})
|
||||||
).start();
|
);
|
||||||
}, []);
|
rotateAnimation.start();
|
||||||
|
|
||||||
|
// Cleanup animations on unmount to prevent memory leaks
|
||||||
|
return () => {
|
||||||
|
pulseAnimation.stop();
|
||||||
|
glowAnimation.stop();
|
||||||
|
rotateAnimation.stop();
|
||||||
|
};
|
||||||
|
}, [pulseAnim, glowAnim, rotateAnim]);
|
||||||
|
|
||||||
|
const openVaultWithMnemonic = () => {
|
||||||
|
const words = generateMnemonic();
|
||||||
|
const shares = generateSSSShares(words);
|
||||||
|
setMnemonicWords(words);
|
||||||
|
setSssShares(shares);
|
||||||
|
setShowMnemonic(true);
|
||||||
|
setShowVault(false);
|
||||||
|
setShowEmailForm(false);
|
||||||
|
setEmailAddress('');
|
||||||
|
|
||||||
|
// Store Share A (device share) locally
|
||||||
|
if (shares[0]) {
|
||||||
|
AsyncStorage.setItem('sentinel_share_device', serializeShare(shares[0])).catch(() => {
|
||||||
|
// Best-effort local store; UI remains available
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleScreenshot = async () => {
|
||||||
|
try {
|
||||||
|
setIsCapturing(true);
|
||||||
|
const uri = await captureRef(mnemonicRef, {
|
||||||
|
format: 'png',
|
||||||
|
quality: 1,
|
||||||
|
result: 'tmpfile',
|
||||||
|
});
|
||||||
|
await Share.share({
|
||||||
|
url: uri,
|
||||||
|
message: 'Sentinel key backup',
|
||||||
|
});
|
||||||
|
setShowMnemonic(false);
|
||||||
|
setShowVault(true);
|
||||||
|
} catch (error) {
|
||||||
|
Alert.alert('Screenshot failed', 'Please try again or use email backup.');
|
||||||
|
} finally {
|
||||||
|
setIsCapturing(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEmailBackup = () => {
|
||||||
|
setShowEmailForm(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSendEmail = async () => {
|
||||||
|
const trimmed = emailAddress.trim();
|
||||||
|
if (!trimmed || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(trimmed)) {
|
||||||
|
Alert.alert('Invalid email', 'Please enter a valid email address.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const subject = encodeURIComponent('Sentinel Vault Recovery Key');
|
||||||
|
const body = encodeURIComponent(`Your 12-word mnemonic:\n${mnemonicWords.join(' ')}`);
|
||||||
|
const mailtoUrl = `mailto:${trimmed}?subject=${subject}&body=${body}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Linking.openURL(mailtoUrl);
|
||||||
|
setShowMnemonic(false);
|
||||||
|
setShowEmailForm(false);
|
||||||
|
setEmailAddress('');
|
||||||
|
setShowVault(true);
|
||||||
|
} catch (error) {
|
||||||
|
Alert.alert('Email failed', 'Unable to open email client.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleHeartbeat = () => {
|
const handleHeartbeat = () => {
|
||||||
// Animate pulse
|
// Animate pulse
|
||||||
Animated.sequence([
|
Animated.sequence([
|
||||||
Animated.timing(pulseAnim, {
|
Animated.timing(pulseAnim, {
|
||||||
toValue: 1.15,
|
toValue: 1.15,
|
||||||
duration: 150,
|
duration: ANIMATION_DURATION.heartbeatPress,
|
||||||
useNativeDriver: true,
|
useNativeDriver: true,
|
||||||
}),
|
}),
|
||||||
Animated.timing(pulseAnim, {
|
Animated.timing(pulseAnim, {
|
||||||
toValue: 1,
|
toValue: 1,
|
||||||
duration: 150,
|
duration: ANIMATION_DURATION.heartbeatPress,
|
||||||
useNativeDriver: true,
|
useNativeDriver: true,
|
||||||
}),
|
}),
|
||||||
]).start();
|
]).start();
|
||||||
|
|
||||||
// Add new log
|
// Add new log using functional update to avoid stale closure
|
||||||
const newLog: KillSwitchLog = {
|
const newLog: KillSwitchLog = {
|
||||||
id: Date.now().toString(),
|
id: Date.now().toString(),
|
||||||
action: 'HEARTBEAT_CONFIRMED',
|
action: 'HEARTBEAT_CONFIRMED',
|
||||||
timestamp: new Date(),
|
timestamp: new Date(),
|
||||||
};
|
};
|
||||||
setLogs([newLog, ...logs]);
|
setLogs((prevLogs) => [newLog, ...prevLogs]);
|
||||||
|
|
||||||
// Reset status if warning
|
// Reset status if warning
|
||||||
if (status === 'warning') {
|
if (status === 'warning') {
|
||||||
@@ -219,7 +386,7 @@ export default function SentinelScreen() {
|
|||||||
colors={currentStatus.gradientColors}
|
colors={currentStatus.gradientColors}
|
||||||
style={styles.statusCircle}
|
style={styles.statusCircle}
|
||||||
>
|
>
|
||||||
<Ionicons name={currentStatus.icon as any} size={56} color="#fff" />
|
<Ionicons name={currentStatus.icon} size={56} color="#fff" />
|
||||||
</LinearGradient>
|
</LinearGradient>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
<Text style={[styles.statusLabel, { color: currentStatus.color }]}>
|
<Text style={[styles.statusLabel, { color: currentStatus.color }]}>
|
||||||
@@ -270,11 +437,35 @@ export default function SentinelScreen() {
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
{/* Shadow Vault Access */}
|
||||||
|
<View style={styles.vaultAccessCard}>
|
||||||
|
<View style={styles.vaultAccessIcon}>
|
||||||
|
<MaterialCommunityIcons name="treasure-chest" size={22} color={colors.nautical.teal} />
|
||||||
|
</View>
|
||||||
|
<View style={styles.vaultAccessContent}>
|
||||||
|
<Text style={styles.vaultAccessTitle}>Shadow Vault</Text>
|
||||||
|
<Text style={styles.vaultAccessText}>
|
||||||
|
Access sealed assets from the Lighthouse.
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.vaultAccessButton}
|
||||||
|
onPress={openVaultWithMnemonic}
|
||||||
|
activeOpacity={0.8}
|
||||||
|
accessibilityLabel="Open Shadow Vault"
|
||||||
|
accessibilityRole="button"
|
||||||
|
>
|
||||||
|
<Text style={styles.vaultAccessButtonText}>Open</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
|
||||||
{/* Heartbeat Button */}
|
{/* Heartbeat Button */}
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.heartbeatButton}
|
style={styles.heartbeatButton}
|
||||||
onPress={handleHeartbeat}
|
onPress={handleHeartbeat}
|
||||||
activeOpacity={0.9}
|
activeOpacity={0.9}
|
||||||
|
accessibilityLabel="Signal the watch - Confirm your presence"
|
||||||
|
accessibilityRole="button"
|
||||||
>
|
>
|
||||||
<LinearGradient
|
<LinearGradient
|
||||||
colors={[colors.nautical.teal, colors.nautical.seafoam]}
|
colors={[colors.nautical.teal, colors.nautical.seafoam]}
|
||||||
@@ -313,6 +504,136 @@ export default function SentinelScreen() {
|
|||||||
</ScrollView>
|
</ScrollView>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
</LinearGradient>
|
</LinearGradient>
|
||||||
|
|
||||||
|
{/* Vault Modal */}
|
||||||
|
<Modal
|
||||||
|
visible={showVault}
|
||||||
|
animationType="slide"
|
||||||
|
onRequestClose={() => setShowVault(false)}
|
||||||
|
>
|
||||||
|
<View style={styles.vaultModalContainer}>
|
||||||
|
<VaultScreen />
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.vaultCloseButton}
|
||||||
|
onPress={() => setShowVault(false)}
|
||||||
|
activeOpacity={0.85}
|
||||||
|
accessibilityLabel="Close vault"
|
||||||
|
accessibilityRole="button"
|
||||||
|
>
|
||||||
|
<Ionicons name="close" size={20} color={colors.nautical.cream} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
{/* Mnemonic Modal */}
|
||||||
|
<Modal
|
||||||
|
visible={showMnemonic}
|
||||||
|
animationType="fade"
|
||||||
|
transparent
|
||||||
|
onRequestClose={() => setShowMnemonic(false)}
|
||||||
|
>
|
||||||
|
<KeyboardAvoidingView
|
||||||
|
style={styles.mnemonicOverlay}
|
||||||
|
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
||||||
|
>
|
||||||
|
<View ref={mnemonicRef} collapsable={false}>
|
||||||
|
<LinearGradient
|
||||||
|
colors={[colors.sentinel.cardBackground, colors.sentinel.backgroundGradientEnd]}
|
||||||
|
style={styles.mnemonicCard}
|
||||||
|
>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.mnemonicClose}
|
||||||
|
onPress={() => setShowMnemonic(false)}
|
||||||
|
activeOpacity={0.85}
|
||||||
|
accessibilityLabel="Close mnemonic modal"
|
||||||
|
accessibilityRole="button"
|
||||||
|
>
|
||||||
|
<Ionicons name="close" size={18} color={colors.sentinel.textSecondary} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
<View style={styles.mnemonicHeader}>
|
||||||
|
<MaterialCommunityIcons name="key-variant" size={22} color={colors.sentinel.primary} />
|
||||||
|
<Text style={styles.mnemonicTitle}>12-Word Mnemonic</Text>
|
||||||
|
</View>
|
||||||
|
<Text style={styles.mnemonicSubtitle}>
|
||||||
|
Your seed is protected by SSS (3,2) threshold encryption. Any 2 shares can restore your vault.
|
||||||
|
</Text>
|
||||||
|
<View style={styles.mnemonicBlock}>
|
||||||
|
<Text style={styles.mnemonicBlockText}>
|
||||||
|
{mnemonicWords.join(' ')}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View style={styles.partGrid}>
|
||||||
|
<View style={[styles.partCard, styles.partCardStored]}>
|
||||||
|
<Text style={styles.partLabel}>SHARE A • DEVICE</Text>
|
||||||
|
<Text style={styles.partValue}>
|
||||||
|
{sssShares[0] ? formatShareCompact(sssShares[0]) : '---'}
|
||||||
|
</Text>
|
||||||
|
<Text style={styles.partHint}>Stored on this device</Text>
|
||||||
|
</View>
|
||||||
|
<View style={styles.partCard}>
|
||||||
|
<Text style={styles.partLabel}>SHARE B • CLOUD</Text>
|
||||||
|
<Text style={styles.partValue}>
|
||||||
|
{sssShares[1] ? formatShareCompact(sssShares[1]) : '---'}
|
||||||
|
</Text>
|
||||||
|
<Text style={styles.partHint}>To be synced to Sentinel</Text>
|
||||||
|
</View>
|
||||||
|
<View style={styles.partCard}>
|
||||||
|
<Text style={styles.partLabel}>SHARE C • HEIR</Text>
|
||||||
|
<Text style={styles.partValue}>
|
||||||
|
{sssShares[2] ? formatShareCompact(sssShares[2]) : '---'}
|
||||||
|
</Text>
|
||||||
|
<Text style={styles.partHint}>For your heir (2-of-3 required)</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[styles.mnemonicPrimaryButton, isCapturing && styles.mnemonicButtonDisabled]}
|
||||||
|
onPress={handleScreenshot}
|
||||||
|
activeOpacity={0.85}
|
||||||
|
disabled={isCapturing}
|
||||||
|
accessibilityLabel="Take screenshot backup of mnemonic"
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityState={{ disabled: isCapturing }}
|
||||||
|
>
|
||||||
|
<Text style={styles.mnemonicPrimaryText}>
|
||||||
|
{isCapturing ? 'CAPTURING...' : 'PHYSICAL BACKUP (SCREENSHOT)'}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.mnemonicSecondaryButton}
|
||||||
|
onPress={handleEmailBackup}
|
||||||
|
activeOpacity={0.85}
|
||||||
|
accessibilityLabel="Send mnemonic backup via email"
|
||||||
|
accessibilityRole="button"
|
||||||
|
>
|
||||||
|
<Text style={styles.mnemonicSecondaryText}>EMAIL BACKUP</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
{showEmailForm ? (
|
||||||
|
<View style={styles.emailForm}>
|
||||||
|
<TextInput
|
||||||
|
style={styles.emailInput}
|
||||||
|
value={emailAddress}
|
||||||
|
onChangeText={setEmailAddress}
|
||||||
|
placeholder="you@email.com"
|
||||||
|
placeholderTextColor={colors.sentinel.textSecondary}
|
||||||
|
keyboardType="email-address"
|
||||||
|
autoCapitalize="none"
|
||||||
|
autoCorrect={false}
|
||||||
|
/>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.emailSendButton}
|
||||||
|
onPress={handleSendEmail}
|
||||||
|
activeOpacity={0.85}
|
||||||
|
accessibilityLabel="Send backup email"
|
||||||
|
accessibilityRole="button"
|
||||||
|
>
|
||||||
|
<Text style={styles.emailSendText}>SEND EMAIL</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
|
</LinearGradient>
|
||||||
|
</View>
|
||||||
|
</KeyboardAvoidingView>
|
||||||
|
</Modal>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -513,4 +834,208 @@ const styles = StyleSheet.create({
|
|||||||
color: colors.sentinel.textSecondary,
|
color: colors.sentinel.textSecondary,
|
||||||
fontFamily: typography.fontFamily.mono,
|
fontFamily: typography.fontFamily.mono,
|
||||||
},
|
},
|
||||||
|
// Shadow Vault Access Card
|
||||||
|
vaultAccessCard: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
backgroundColor: colors.sentinel.cardBackground,
|
||||||
|
borderRadius: borderRadius.xl,
|
||||||
|
padding: spacing.base,
|
||||||
|
marginBottom: spacing.lg,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: colors.sentinel.cardBorder,
|
||||||
|
},
|
||||||
|
vaultAccessIcon: {
|
||||||
|
width: 44,
|
||||||
|
height: 44,
|
||||||
|
borderRadius: 22,
|
||||||
|
backgroundColor: `${colors.nautical.teal}20`,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
marginRight: spacing.md,
|
||||||
|
},
|
||||||
|
vaultAccessContent: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
vaultAccessTitle: {
|
||||||
|
fontSize: typography.fontSize.base,
|
||||||
|
fontWeight: '600',
|
||||||
|
color: colors.sentinel.text,
|
||||||
|
marginBottom: 2,
|
||||||
|
},
|
||||||
|
vaultAccessText: {
|
||||||
|
fontSize: typography.fontSize.sm,
|
||||||
|
color: colors.sentinel.textSecondary,
|
||||||
|
},
|
||||||
|
vaultAccessButton: {
|
||||||
|
backgroundColor: colors.nautical.teal,
|
||||||
|
paddingHorizontal: spacing.md,
|
||||||
|
paddingVertical: spacing.sm,
|
||||||
|
borderRadius: borderRadius.full,
|
||||||
|
},
|
||||||
|
vaultAccessButtonText: {
|
||||||
|
color: colors.nautical.cream,
|
||||||
|
fontWeight: '700',
|
||||||
|
fontSize: typography.fontSize.sm,
|
||||||
|
},
|
||||||
|
// Vault Modal
|
||||||
|
vaultModalContainer: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: colors.vault.background,
|
||||||
|
},
|
||||||
|
vaultCloseButton: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: spacing.xl + spacing.lg,
|
||||||
|
right: spacing.lg,
|
||||||
|
width: 36,
|
||||||
|
height: 36,
|
||||||
|
borderRadius: 18,
|
||||||
|
backgroundColor: 'rgba(26, 58, 74, 0.65)',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
mnemonicOverlay: {
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: 'rgba(11, 20, 24, 0.72)',
|
||||||
|
justifyContent: 'center',
|
||||||
|
padding: spacing.lg,
|
||||||
|
},
|
||||||
|
mnemonicCard: {
|
||||||
|
borderRadius: borderRadius.xl,
|
||||||
|
padding: spacing.lg,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: colors.sentinel.cardBorder,
|
||||||
|
...shadows.glow,
|
||||||
|
},
|
||||||
|
mnemonicHeader: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: spacing.sm,
|
||||||
|
marginBottom: spacing.sm,
|
||||||
|
},
|
||||||
|
mnemonicClose: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: spacing.sm,
|
||||||
|
right: spacing.sm,
|
||||||
|
width: 32,
|
||||||
|
height: 32,
|
||||||
|
borderRadius: 16,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
backgroundColor: 'rgba(26, 58, 74, 0.35)',
|
||||||
|
},
|
||||||
|
mnemonicTitle: {
|
||||||
|
fontSize: typography.fontSize.lg,
|
||||||
|
fontWeight: '700',
|
||||||
|
color: colors.sentinel.text,
|
||||||
|
letterSpacing: typography.letterSpacing.wide,
|
||||||
|
},
|
||||||
|
mnemonicSubtitle: {
|
||||||
|
fontSize: typography.fontSize.sm,
|
||||||
|
color: colors.sentinel.textSecondary,
|
||||||
|
marginBottom: spacing.md,
|
||||||
|
},
|
||||||
|
mnemonicBlock: {
|
||||||
|
backgroundColor: colors.sentinel.cardBackground,
|
||||||
|
borderRadius: borderRadius.lg,
|
||||||
|
paddingVertical: spacing.md,
|
||||||
|
paddingHorizontal: spacing.md,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: colors.sentinel.cardBorder,
|
||||||
|
marginBottom: spacing.lg,
|
||||||
|
},
|
||||||
|
partGrid: {
|
||||||
|
gap: spacing.sm,
|
||||||
|
marginBottom: spacing.lg,
|
||||||
|
},
|
||||||
|
partCard: {
|
||||||
|
backgroundColor: colors.sentinel.cardBackground,
|
||||||
|
borderRadius: borderRadius.lg,
|
||||||
|
paddingVertical: spacing.sm,
|
||||||
|
paddingHorizontal: spacing.md,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: colors.sentinel.cardBorder,
|
||||||
|
},
|
||||||
|
partCardStored: {
|
||||||
|
borderColor: colors.sentinel.primary,
|
||||||
|
},
|
||||||
|
partLabel: {
|
||||||
|
fontSize: typography.fontSize.xs,
|
||||||
|
color: colors.sentinel.textSecondary,
|
||||||
|
letterSpacing: typography.letterSpacing.wide,
|
||||||
|
marginBottom: 4,
|
||||||
|
fontWeight: '600',
|
||||||
|
},
|
||||||
|
partValue: {
|
||||||
|
fontSize: typography.fontSize.md,
|
||||||
|
color: colors.sentinel.text,
|
||||||
|
fontFamily: typography.fontFamily.mono,
|
||||||
|
fontWeight: '700',
|
||||||
|
marginBottom: 2,
|
||||||
|
},
|
||||||
|
partHint: {
|
||||||
|
fontSize: typography.fontSize.xs,
|
||||||
|
color: colors.sentinel.textSecondary,
|
||||||
|
},
|
||||||
|
mnemonicBlockText: {
|
||||||
|
fontSize: typography.fontSize.sm,
|
||||||
|
color: colors.sentinel.text,
|
||||||
|
fontFamily: typography.fontFamily.mono,
|
||||||
|
fontWeight: '600',
|
||||||
|
lineHeight: 22,
|
||||||
|
textAlign: 'center',
|
||||||
|
},
|
||||||
|
mnemonicPrimaryButton: {
|
||||||
|
backgroundColor: colors.sentinel.primary,
|
||||||
|
paddingVertical: spacing.sm,
|
||||||
|
borderRadius: borderRadius.full,
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: spacing.sm,
|
||||||
|
},
|
||||||
|
mnemonicButtonDisabled: {
|
||||||
|
opacity: 0.6,
|
||||||
|
},
|
||||||
|
mnemonicPrimaryText: {
|
||||||
|
color: colors.nautical.cream,
|
||||||
|
fontWeight: '700',
|
||||||
|
letterSpacing: typography.letterSpacing.wide,
|
||||||
|
},
|
||||||
|
mnemonicSecondaryButton: {
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
paddingVertical: spacing.sm,
|
||||||
|
borderRadius: borderRadius.full,
|
||||||
|
alignItems: 'center',
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: colors.sentinel.cardBorder,
|
||||||
|
},
|
||||||
|
mnemonicSecondaryText: {
|
||||||
|
color: colors.sentinel.text,
|
||||||
|
fontWeight: '700',
|
||||||
|
letterSpacing: typography.letterSpacing.wide,
|
||||||
|
},
|
||||||
|
emailForm: {
|
||||||
|
marginTop: spacing.sm,
|
||||||
|
},
|
||||||
|
emailInput: {
|
||||||
|
height: 44,
|
||||||
|
borderRadius: borderRadius.full,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: colors.sentinel.cardBorder,
|
||||||
|
paddingHorizontal: spacing.md,
|
||||||
|
color: colors.sentinel.text,
|
||||||
|
fontSize: typography.fontSize.sm,
|
||||||
|
backgroundColor: 'rgba(255, 255, 255, 0.02)',
|
||||||
|
marginBottom: spacing.sm,
|
||||||
|
},
|
||||||
|
emailSendButton: {
|
||||||
|
backgroundColor: colors.nautical.teal,
|
||||||
|
paddingVertical: spacing.sm,
|
||||||
|
borderRadius: borderRadius.full,
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
emailSendText: {
|
||||||
|
color: colors.nautical.cream,
|
||||||
|
fontWeight: '700',
|
||||||
|
letterSpacing: typography.letterSpacing.wide,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
79
src/services/admin.service.ts
Normal file
79
src/services/admin.service.ts
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
/**
|
||||||
|
* Admin Service
|
||||||
|
*
|
||||||
|
* Handles admin-only API operations.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
NO_BACKEND_MODE,
|
||||||
|
API_ENDPOINTS,
|
||||||
|
MOCK_CONFIG,
|
||||||
|
buildApiUrl,
|
||||||
|
getApiHeaders,
|
||||||
|
logApiDebug,
|
||||||
|
} from '../config';
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Type Definitions
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export interface DeclareGualeRequest {
|
||||||
|
username: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DeclareGualeResponse {
|
||||||
|
message: string;
|
||||||
|
username: string;
|
||||||
|
guale: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Admin Service
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export const adminService = {
|
||||||
|
/**
|
||||||
|
* Declare a user as deceased (guale)
|
||||||
|
* Admin only operation
|
||||||
|
* @param request - Username to declare as deceased
|
||||||
|
* @param token - JWT token for authentication (must be admin)
|
||||||
|
* @returns Success response
|
||||||
|
*/
|
||||||
|
async declareGuale(request: DeclareGualeRequest, token: string): Promise<DeclareGualeResponse> {
|
||||||
|
if (NO_BACKEND_MODE) {
|
||||||
|
logApiDebug('Declare Guale', 'Using mock mode');
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
resolve({
|
||||||
|
message: `User ${request.username} has been declared as deceased`,
|
||||||
|
username: request.username,
|
||||||
|
guale: true,
|
||||||
|
});
|
||||||
|
}, MOCK_CONFIG.RESPONSE_DELAY);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = buildApiUrl(API_ENDPOINTS.ADMIN.DECLARE_GUALE);
|
||||||
|
logApiDebug('Declare Guale URL', url);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: getApiHeaders(token),
|
||||||
|
body: JSON.stringify(request),
|
||||||
|
});
|
||||||
|
|
||||||
|
logApiDebug('Declare Guale Response Status', response.status);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json().catch(() => ({}));
|
||||||
|
throw new Error(errorData.detail || 'Failed to declare user as deceased');
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Declare guale error:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
243
src/services/ai.service.ts
Normal file
243
src/services/ai.service.ts
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
/**
|
||||||
|
* AI Service
|
||||||
|
*
|
||||||
|
* Handles communication with the AI proxy endpoint for chat completions.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
NO_BACKEND_MODE,
|
||||||
|
API_ENDPOINTS,
|
||||||
|
AI_CONFIG,
|
||||||
|
buildApiUrl,
|
||||||
|
getApiHeaders,
|
||||||
|
logApiDebug,
|
||||||
|
} from '../config';
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Type Definitions
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export interface AIMessage {
|
||||||
|
role: 'system' | 'user' | 'assistant';
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AIRequest {
|
||||||
|
messages: AIMessage[];
|
||||||
|
model?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AIResponse {
|
||||||
|
id: string;
|
||||||
|
object: string;
|
||||||
|
created: number;
|
||||||
|
model: string;
|
||||||
|
choices: Array<{
|
||||||
|
index: number;
|
||||||
|
message: AIMessage;
|
||||||
|
finish_reason: string;
|
||||||
|
}>;
|
||||||
|
usage: {
|
||||||
|
prompt_tokens: number;
|
||||||
|
completion_tokens: number;
|
||||||
|
total_tokens: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Mock Response Generator
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
const createMockResponse = (userMessage: string): AIResponse => {
|
||||||
|
return {
|
||||||
|
id: `mock-${Date.now()}`,
|
||||||
|
object: 'chat.completion',
|
||||||
|
created: Math.floor(Date.now() / 1000),
|
||||||
|
model: 'mock-model',
|
||||||
|
choices: [
|
||||||
|
{
|
||||||
|
index: 0,
|
||||||
|
message: {
|
||||||
|
role: 'assistant',
|
||||||
|
content: `I received your message: "${userMessage}". This is a mock response since the backend is not connected.`,
|
||||||
|
},
|
||||||
|
finish_reason: 'stop',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
usage: {
|
||||||
|
prompt_tokens: 10,
|
||||||
|
completion_tokens: 20,
|
||||||
|
total_tokens: 30,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// AI Service
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export const aiService = {
|
||||||
|
/**
|
||||||
|
* Send chat messages to the AI proxy
|
||||||
|
* @param messages - Array of chat messages
|
||||||
|
* @param token - JWT token for authentication
|
||||||
|
* @returns AI response
|
||||||
|
*/
|
||||||
|
async chat(messages: AIMessage[], token?: string): Promise<AIResponse> {
|
||||||
|
if (NO_BACKEND_MODE) {
|
||||||
|
logApiDebug('AI Chat', 'Using mock mode');
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
const lastUserMessage = messages.filter((m) => m.role === 'user').pop();
|
||||||
|
resolve(createMockResponse(lastUserMessage?.content || 'Hello'));
|
||||||
|
}, AI_CONFIG.MOCK_RESPONSE_DELAY);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = buildApiUrl(API_ENDPOINTS.AI.PROXY);
|
||||||
|
|
||||||
|
logApiDebug('AI Request', {
|
||||||
|
url,
|
||||||
|
hasToken: !!token,
|
||||||
|
messageCount: messages.length,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: getApiHeaders(token),
|
||||||
|
body: JSON.stringify({ messages } as AIRequest),
|
||||||
|
});
|
||||||
|
|
||||||
|
logApiDebug('AI Response Status', response.status);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text();
|
||||||
|
logApiDebug('AI Error Response', errorText);
|
||||||
|
|
||||||
|
let errorDetail = 'AI request failed';
|
||||||
|
try {
|
||||||
|
const errorData = JSON.parse(errorText);
|
||||||
|
errorDetail = errorData.detail || errorDetail;
|
||||||
|
} catch {
|
||||||
|
errorDetail = errorText || errorDetail;
|
||||||
|
}
|
||||||
|
throw new Error(`${response.status}: ${errorDetail}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
logApiDebug('AI Success', {
|
||||||
|
id: data.id,
|
||||||
|
model: data.model,
|
||||||
|
choicesCount: data.choices?.length,
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('AI proxy error:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple helper for single message chat
|
||||||
|
* @param content - User message content
|
||||||
|
* @param token - JWT token for authentication
|
||||||
|
* @returns AI response text
|
||||||
|
*/
|
||||||
|
async sendMessage(content: string, token?: string): Promise<string> {
|
||||||
|
const messages: AIMessage[] = [
|
||||||
|
{
|
||||||
|
role: 'system',
|
||||||
|
content: AI_CONFIG.DEFAULT_SYSTEM_PROMPT,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: 'user',
|
||||||
|
content,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const response = await this.chat(messages, token);
|
||||||
|
return response.choices[0]?.message?.content || 'No response';
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a message with an image to AI for analysis
|
||||||
|
* @param content - User message content
|
||||||
|
* @param imageBase64 - Base64 encoded image data
|
||||||
|
* @param token - JWT token for authentication
|
||||||
|
* @returns AI response text
|
||||||
|
*/
|
||||||
|
async sendMessageWithImage(content: string, imageBase64: string, token?: string): Promise<string> {
|
||||||
|
if (NO_BACKEND_MODE) {
|
||||||
|
logApiDebug('AI Image Analysis', 'Using mock mode');
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
resolve('This is a mock image analysis response. The image appears to show an interesting scene. In production, this would be analyzed by Gemini AI.');
|
||||||
|
}, AI_CONFIG.MOCK_RESPONSE_DELAY);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = buildApiUrl(API_ENDPOINTS.AI.PROXY);
|
||||||
|
|
||||||
|
logApiDebug('AI Image Request', {
|
||||||
|
url,
|
||||||
|
hasToken: !!token,
|
||||||
|
hasImage: !!imageBase64,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Gemini vision format - using multimodal content
|
||||||
|
const messages = [
|
||||||
|
{
|
||||||
|
role: 'user',
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: content,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'image_url',
|
||||||
|
image_url: {
|
||||||
|
url: `data:image/jpeg;base64,${imageBase64}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: getApiHeaders(token),
|
||||||
|
body: JSON.stringify({ messages }),
|
||||||
|
});
|
||||||
|
|
||||||
|
logApiDebug('AI Image Response Status', response.status);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text();
|
||||||
|
logApiDebug('AI Image Error Response', errorText);
|
||||||
|
|
||||||
|
let errorDetail = 'AI image request failed';
|
||||||
|
try {
|
||||||
|
const errorData = JSON.parse(errorText);
|
||||||
|
errorDetail = errorData.detail || errorDetail;
|
||||||
|
} catch {
|
||||||
|
errorDetail = errorText || errorDetail;
|
||||||
|
}
|
||||||
|
throw new Error(`${response.status}: ${errorDetail}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
logApiDebug('AI Image Success', {
|
||||||
|
id: data.id,
|
||||||
|
model: data.model,
|
||||||
|
});
|
||||||
|
|
||||||
|
return data.choices[0]?.message?.content || 'No response';
|
||||||
|
} catch (error) {
|
||||||
|
console.error('AI image proxy error:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
243
src/services/assets.service.ts
Normal file
243
src/services/assets.service.ts
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
/**
|
||||||
|
* Assets Service
|
||||||
|
*
|
||||||
|
* Handles all asset-related API operations including CRUD and inheritance.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
NO_BACKEND_MODE,
|
||||||
|
API_ENDPOINTS,
|
||||||
|
MOCK_CONFIG,
|
||||||
|
buildApiUrl,
|
||||||
|
getApiHeaders,
|
||||||
|
logApiDebug,
|
||||||
|
} from '../config';
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Type Definitions
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export interface Asset {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
author_id: number;
|
||||||
|
private_key_shard: string;
|
||||||
|
content_outer_encrypted: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AssetCreate {
|
||||||
|
title: string;
|
||||||
|
private_key_shard: string;
|
||||||
|
content_inner_encrypted: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AssetClaim {
|
||||||
|
asset_id: number;
|
||||||
|
private_key_shard: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AssetClaimResponse {
|
||||||
|
asset_id: number;
|
||||||
|
title: string;
|
||||||
|
decrypted_content: string;
|
||||||
|
server_shard_key: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AssetAssign {
|
||||||
|
asset_id: number;
|
||||||
|
heir_name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Mock Data
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
const MOCK_ASSETS: Asset[] = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: 'Mock Asset 1',
|
||||||
|
author_id: MOCK_CONFIG.USER.id,
|
||||||
|
private_key_shard: 'mock_shard_1',
|
||||||
|
content_outer_encrypted: 'mock_encrypted_content_1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: 'Mock Asset 2',
|
||||||
|
author_id: MOCK_CONFIG.USER.id,
|
||||||
|
private_key_shard: 'mock_shard_2',
|
||||||
|
content_outer_encrypted: 'mock_encrypted_content_2',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Assets Service
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export const assetsService = {
|
||||||
|
/**
|
||||||
|
* Get all assets for the current user
|
||||||
|
* @param token - JWT token for authentication
|
||||||
|
* @returns Array of user's assets
|
||||||
|
*/
|
||||||
|
async getMyAssets(token: string): Promise<Asset[]> {
|
||||||
|
if (NO_BACKEND_MODE) {
|
||||||
|
logApiDebug('Get Assets', 'Using mock mode');
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(() => resolve(MOCK_ASSETS), MOCK_CONFIG.RESPONSE_DELAY);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = buildApiUrl(API_ENDPOINTS.ASSETS.GET);
|
||||||
|
logApiDebug('Get Assets URL', url);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: getApiHeaders(token),
|
||||||
|
});
|
||||||
|
|
||||||
|
logApiDebug('Get Assets Response Status', response.status);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json().catch(() => ({}));
|
||||||
|
throw new Error(errorData.detail || 'Failed to fetch assets');
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Get assets error:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new asset
|
||||||
|
* @param asset - Asset creation data
|
||||||
|
* @param token - JWT token for authentication
|
||||||
|
* @returns Created asset
|
||||||
|
*/
|
||||||
|
async createAsset(asset: AssetCreate, token: string): Promise<Asset> {
|
||||||
|
if (NO_BACKEND_MODE) {
|
||||||
|
logApiDebug('Create Asset', 'Using mock mode');
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
resolve({
|
||||||
|
id: Date.now(),
|
||||||
|
title: asset.title,
|
||||||
|
author_id: MOCK_CONFIG.USER.id,
|
||||||
|
private_key_shard: asset.private_key_shard,
|
||||||
|
content_outer_encrypted: asset.content_inner_encrypted,
|
||||||
|
});
|
||||||
|
}, MOCK_CONFIG.RESPONSE_DELAY);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = buildApiUrl(API_ENDPOINTS.ASSETS.CREATE);
|
||||||
|
logApiDebug('Create Asset URL', url);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: getApiHeaders(token),
|
||||||
|
body: JSON.stringify(asset),
|
||||||
|
});
|
||||||
|
|
||||||
|
logApiDebug('Create Asset Response Status', response.status);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json().catch(() => ({}));
|
||||||
|
throw new Error(errorData.detail || 'Failed to create asset');
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Create asset error:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Claim an inherited asset
|
||||||
|
* @param claim - Asset claim data
|
||||||
|
* @param token - JWT token for authentication
|
||||||
|
* @returns Claimed asset with decrypted content
|
||||||
|
*/
|
||||||
|
async claimAsset(claim: AssetClaim, token: string): Promise<AssetClaimResponse> {
|
||||||
|
if (NO_BACKEND_MODE) {
|
||||||
|
logApiDebug('Claim Asset', 'Using mock mode');
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
resolve({
|
||||||
|
asset_id: claim.asset_id,
|
||||||
|
title: 'Mock Claimed Asset',
|
||||||
|
decrypted_content: 'This is the decrypted content of the claimed asset.',
|
||||||
|
server_shard_key: 'mock_server_shard',
|
||||||
|
});
|
||||||
|
}, MOCK_CONFIG.RESPONSE_DELAY);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = buildApiUrl(API_ENDPOINTS.ASSETS.CLAIM);
|
||||||
|
logApiDebug('Claim Asset URL', url);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: getApiHeaders(token),
|
||||||
|
body: JSON.stringify(claim),
|
||||||
|
});
|
||||||
|
|
||||||
|
logApiDebug('Claim Asset Response Status', response.status);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json().catch(() => ({}));
|
||||||
|
throw new Error(errorData.detail || 'Failed to claim asset');
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Claim asset error:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assign an asset to an heir
|
||||||
|
* @param assignment - Asset assignment data
|
||||||
|
* @param token - JWT token for authentication
|
||||||
|
* @returns Success message
|
||||||
|
*/
|
||||||
|
async assignAsset(assignment: AssetAssign, token: string): Promise<{ message: string }> {
|
||||||
|
if (NO_BACKEND_MODE) {
|
||||||
|
logApiDebug('Assign Asset', 'Using mock mode');
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
resolve({ message: `Asset assigned to ${assignment.heir_name}` });
|
||||||
|
}, MOCK_CONFIG.RESPONSE_DELAY);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = buildApiUrl(API_ENDPOINTS.ASSETS.ASSIGN);
|
||||||
|
logApiDebug('Assign Asset URL', url);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: getApiHeaders(token),
|
||||||
|
body: JSON.stringify(assignment),
|
||||||
|
});
|
||||||
|
|
||||||
|
logApiDebug('Assign Asset Response Status', response.status);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json().catch(() => ({}));
|
||||||
|
throw new Error(errorData.detail || 'Failed to assign asset');
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Assign asset error:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
87
src/services/auth.service.ts
Normal file
87
src/services/auth.service.ts
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import { LoginRequest, LoginResponse, RegisterRequest, User } from '../types';
|
||||||
|
import {
|
||||||
|
NO_BACKEND_MODE,
|
||||||
|
API_ENDPOINTS,
|
||||||
|
MOCK_CONFIG,
|
||||||
|
buildApiUrl,
|
||||||
|
getApiHeaders,
|
||||||
|
logApiDebug,
|
||||||
|
} from '../config';
|
||||||
|
|
||||||
|
export const authService = {
|
||||||
|
async login(credentials: LoginRequest): Promise<LoginResponse> {
|
||||||
|
if (NO_BACKEND_MODE) {
|
||||||
|
logApiDebug('Login', 'Using mock mode');
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
resolve({
|
||||||
|
access_token: MOCK_CONFIG.ACCESS_TOKEN,
|
||||||
|
token_type: 'bearer',
|
||||||
|
user: { ...MOCK_CONFIG.USER, username: credentials.username } as User,
|
||||||
|
});
|
||||||
|
}, MOCK_CONFIG.RESPONSE_DELAY);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = buildApiUrl(API_ENDPOINTS.AUTH.LOGIN);
|
||||||
|
logApiDebug('Login URL', url);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: getApiHeaders(),
|
||||||
|
body: JSON.stringify(credentials),
|
||||||
|
});
|
||||||
|
|
||||||
|
logApiDebug('Login Response Status', response.status);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json().catch(() => ({}));
|
||||||
|
throw new Error(errorData.detail || 'Login failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
logApiDebug('Login Success', { username: data.user?.username });
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Login error:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async register(data: RegisterRequest): Promise<User> {
|
||||||
|
if (NO_BACKEND_MODE) {
|
||||||
|
logApiDebug('Register', 'Using mock mode');
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
resolve({ ...MOCK_CONFIG.USER, username: data.username } as User);
|
||||||
|
}, MOCK_CONFIG.RESPONSE_DELAY);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = buildApiUrl(API_ENDPOINTS.AUTH.REGISTER);
|
||||||
|
logApiDebug('Register URL', url);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: getApiHeaders(),
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
|
||||||
|
logApiDebug('Register Response Status', response.status);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json().catch(() => ({}));
|
||||||
|
throw new Error(errorData.detail?.[0]?.msg || errorData.detail || 'Registration failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
logApiDebug('Register Success', { username: result.username });
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Registration error:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
25
src/services/index.ts
Normal file
25
src/services/index.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
/**
|
||||||
|
* Services Index
|
||||||
|
*
|
||||||
|
* Central export for all API services.
|
||||||
|
* Import services from here for cleaner imports.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* import { authService, aiService, assetsService, adminService } from '../services';
|
||||||
|
*/
|
||||||
|
|
||||||
|
export { authService } from './auth.service';
|
||||||
|
export { aiService, type AIMessage, type AIRequest, type AIResponse } from './ai.service';
|
||||||
|
export {
|
||||||
|
assetsService,
|
||||||
|
type Asset,
|
||||||
|
type AssetCreate,
|
||||||
|
type AssetClaim,
|
||||||
|
type AssetClaimResponse,
|
||||||
|
type AssetAssign
|
||||||
|
} from './assets.service';
|
||||||
|
export {
|
||||||
|
adminService,
|
||||||
|
type DeclareGualeRequest,
|
||||||
|
type DeclareGualeResponse
|
||||||
|
} from './admin.service';
|
||||||
@@ -72,3 +72,32 @@ export interface ProtocolInfo {
|
|||||||
version: string;
|
version: string;
|
||||||
lastUpdated: Date;
|
lastUpdated: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Auth Types
|
||||||
|
export interface User {
|
||||||
|
id: number;
|
||||||
|
username: string;
|
||||||
|
public_key: string;
|
||||||
|
is_admin: boolean;
|
||||||
|
guale: boolean;
|
||||||
|
tier: string;
|
||||||
|
tier_expires_at: string;
|
||||||
|
last_active_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoginRequest {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RegisterRequest {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
email: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoginResponse {
|
||||||
|
access_token: string;
|
||||||
|
token_type: string;
|
||||||
|
user: User;
|
||||||
|
}
|
||||||
|
|||||||
5
src/utils/index.ts
Normal file
5
src/utils/index.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
/**
|
||||||
|
* Utility functions for Sentinel
|
||||||
|
*/
|
||||||
|
|
||||||
|
export * from './sss';
|
||||||
263
src/utils/sss.ts
Normal file
263
src/utils/sss.ts
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
/**
|
||||||
|
* Shamir's Secret Sharing (SSS) Implementation
|
||||||
|
*
|
||||||
|
* This implements a (3,2) threshold scheme where:
|
||||||
|
* - Secret is split into 3 shares
|
||||||
|
* - Any 2 shares can recover the original secret
|
||||||
|
*
|
||||||
|
* Based on the Sentinel crypto_core_demo Python implementation.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Use a large prime for the finite field
|
||||||
|
// We use 2^127 - 1 (a Mersenne prime) which fits well in BigInt
|
||||||
|
// This is smaller than the Python version's 2^521 - 1 but sufficient for our 128-bit entropy
|
||||||
|
const PRIME = BigInt('170141183460469231731687303715884105727'); // 2^127 - 1
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents an SSS share as a coordinate point (x, y)
|
||||||
|
*/
|
||||||
|
export interface SSSShare {
|
||||||
|
x: number;
|
||||||
|
y: bigint;
|
||||||
|
label: string; // 'device' | 'cloud' | 'heir'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a cryptographically secure random BigInt in range [0, max)
|
||||||
|
*/
|
||||||
|
function secureRandomBigInt(max: bigint): bigint {
|
||||||
|
// Get the number of bytes needed
|
||||||
|
const byteLength = Math.ceil(max.toString(2).length / 8);
|
||||||
|
const randomBytes = new Uint8Array(byteLength);
|
||||||
|
|
||||||
|
// Use crypto.getRandomValues for secure randomness
|
||||||
|
if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
|
||||||
|
crypto.getRandomValues(randomBytes);
|
||||||
|
} else {
|
||||||
|
// Fallback for environments without crypto
|
||||||
|
for (let i = 0; i < byteLength; i++) {
|
||||||
|
randomBytes[i] = Math.floor(Math.random() * 256);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to BigInt
|
||||||
|
let result = BigInt(0);
|
||||||
|
for (let i = 0; i < randomBytes.length; i++) {
|
||||||
|
result = (result << BigInt(8)) + BigInt(randomBytes[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure result is within range
|
||||||
|
return result % max;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert mnemonic words to entropy (as BigInt)
|
||||||
|
* Each word is mapped to its index, then combined into a single large number
|
||||||
|
*/
|
||||||
|
export function mnemonicToEntropy(words: string[], wordList: readonly string[]): bigint {
|
||||||
|
let entropy = BigInt(0);
|
||||||
|
const wordListLength = BigInt(wordList.length);
|
||||||
|
|
||||||
|
for (const word of words) {
|
||||||
|
const index = wordList.indexOf(word);
|
||||||
|
if (index === -1) {
|
||||||
|
throw new Error(`Word "${word}" not found in word list`);
|
||||||
|
}
|
||||||
|
entropy = entropy * wordListLength + BigInt(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
return entropy;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert entropy back to mnemonic words
|
||||||
|
*/
|
||||||
|
export function entropyToMnemonic(entropy: bigint, wordCount: number, wordList: readonly string[]): string[] {
|
||||||
|
const words: string[] = [];
|
||||||
|
const wordListLength = BigInt(wordList.length);
|
||||||
|
let remaining = entropy;
|
||||||
|
|
||||||
|
for (let i = 0; i < wordCount; i++) {
|
||||||
|
const index = Number(remaining % wordListLength);
|
||||||
|
words.unshift(wordList[index]);
|
||||||
|
remaining = remaining / wordListLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
return words;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modular inverse using extended Euclidean algorithm
|
||||||
|
* Returns x such that (a * x) % p === 1
|
||||||
|
*/
|
||||||
|
function modInverse(a: bigint, p: bigint): bigint {
|
||||||
|
let [oldR, r] = [a % p, p];
|
||||||
|
let [oldS, s] = [BigInt(1), BigInt(0)];
|
||||||
|
|
||||||
|
while (r !== BigInt(0)) {
|
||||||
|
const quotient = oldR / r;
|
||||||
|
[oldR, r] = [r, oldR - quotient * r];
|
||||||
|
[oldS, s] = [s, oldS - quotient * s];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure positive result
|
||||||
|
return ((oldS % p) + p) % p;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modular arithmetic helper to ensure positive results
|
||||||
|
*/
|
||||||
|
function mod(n: bigint, p: bigint): bigint {
|
||||||
|
return ((n % p) + p) % p;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Split a secret into 3 shares using SSS (3,2) threshold scheme
|
||||||
|
*
|
||||||
|
* Uses linear polynomial: f(x) = secret + a*x (mod p)
|
||||||
|
* where 'a' is a random coefficient
|
||||||
|
*
|
||||||
|
* Any 2 points on this line can recover the y-intercept (secret)
|
||||||
|
*/
|
||||||
|
export function splitSecret(secret: bigint): SSSShare[] {
|
||||||
|
// Generate random coefficient for the polynomial
|
||||||
|
const a = secureRandomBigInt(PRIME);
|
||||||
|
|
||||||
|
// Polynomial: f(x) = secret + a*x (mod PRIME)
|
||||||
|
const f = (x: number): bigint => {
|
||||||
|
return mod(secret + a * BigInt(x), PRIME);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Generate 3 shares at x = 1, 2, 3
|
||||||
|
return [
|
||||||
|
{ x: 1, y: f(1), label: 'device' },
|
||||||
|
{ x: 2, y: f(2), label: 'cloud' },
|
||||||
|
{ x: 3, y: f(3), label: 'heir' },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recover the secret from any 2 shares using Lagrange interpolation
|
||||||
|
*
|
||||||
|
* For 2 points (x1, y1) and (x2, y2), the secret (y-intercept) is:
|
||||||
|
* S = (x2*y1 - x1*y2) / (x2 - x1) (mod p)
|
||||||
|
*/
|
||||||
|
export function recoverSecret(shareA: SSSShare, shareB: SSSShare): bigint {
|
||||||
|
const { x: x1, y: y1 } = shareA;
|
||||||
|
const { x: x2, y: y2 } = shareB;
|
||||||
|
|
||||||
|
// Numerator: x2*y1 - x1*y2
|
||||||
|
const numerator = mod(
|
||||||
|
BigInt(x2) * y1 - BigInt(x1) * y2,
|
||||||
|
PRIME
|
||||||
|
);
|
||||||
|
|
||||||
|
// Denominator: x2 - x1
|
||||||
|
const denominator = mod(BigInt(x2 - x1), PRIME);
|
||||||
|
|
||||||
|
// Division in modular arithmetic = multiply by modular inverse
|
||||||
|
const invDenominator = modInverse(denominator, PRIME);
|
||||||
|
|
||||||
|
return mod(numerator * invDenominator, PRIME);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format a share for display (truncated for readability)
|
||||||
|
* Shows first 8 and last 4 characters of the y-value
|
||||||
|
*/
|
||||||
|
export function formatShareForDisplay(share: SSSShare): string {
|
||||||
|
const yStr = share.y.toString();
|
||||||
|
if (yStr.length <= 16) {
|
||||||
|
return `(${share.x}, ${yStr})`;
|
||||||
|
}
|
||||||
|
return `(${share.x}, ${yStr.slice(0, 8)}...${yStr.slice(-4)})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format a share as a compact display string (for UI cards)
|
||||||
|
* Returns a shorter format showing the share index and a hash-like preview
|
||||||
|
*/
|
||||||
|
export function formatShareCompact(share: SSSShare): string {
|
||||||
|
const yStr = share.y.toString();
|
||||||
|
// Create a "fingerprint" from the y value
|
||||||
|
const fingerprint = yStr.slice(0, 4) + '-' + yStr.slice(4, 8) + '-' + yStr.slice(-4);
|
||||||
|
return fingerprint;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serialize a share to a string for storage/transmission
|
||||||
|
*/
|
||||||
|
export function serializeShare(share: SSSShare): string {
|
||||||
|
return JSON.stringify({
|
||||||
|
x: share.x,
|
||||||
|
y: share.y.toString(),
|
||||||
|
label: share.label,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deserialize a share from a string
|
||||||
|
*/
|
||||||
|
export function deserializeShare(str: string): SSSShare {
|
||||||
|
const parsed = JSON.parse(str);
|
||||||
|
return {
|
||||||
|
x: parsed.x,
|
||||||
|
y: BigInt(parsed.y),
|
||||||
|
label: parsed.label,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main function to generate mnemonic and SSS shares
|
||||||
|
* This is the entry point for the vault initialization flow
|
||||||
|
*/
|
||||||
|
export interface VaultKeyData {
|
||||||
|
mnemonic: string[];
|
||||||
|
shares: SSSShare[];
|
||||||
|
entropy: bigint;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateVaultKeys(
|
||||||
|
wordList: readonly string[],
|
||||||
|
wordCount: number = 12
|
||||||
|
): VaultKeyData {
|
||||||
|
// Generate random mnemonic
|
||||||
|
const mnemonic: string[] = [];
|
||||||
|
for (let i = 0; i < wordCount; i++) {
|
||||||
|
const index = Math.floor(Math.random() * wordList.length);
|
||||||
|
mnemonic.push(wordList[index]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to entropy
|
||||||
|
const entropy = mnemonicToEntropy(mnemonic, wordList);
|
||||||
|
|
||||||
|
// Split into shares
|
||||||
|
const shares = splitSecret(entropy);
|
||||||
|
|
||||||
|
return { mnemonic, shares, entropy };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify that shares can recover the original entropy
|
||||||
|
* Useful for testing and validation
|
||||||
|
*/
|
||||||
|
export function verifyShares(
|
||||||
|
shares: SSSShare[],
|
||||||
|
originalEntropy: bigint
|
||||||
|
): boolean {
|
||||||
|
// Test all 3 combinations of 2 shares
|
||||||
|
const combinations = [
|
||||||
|
[shares[0], shares[1]], // Device + Cloud
|
||||||
|
[shares[1], shares[2]], // Cloud + Heir
|
||||||
|
[shares[0], shares[2]], // Device + Heir
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const [a, b] of combinations) {
|
||||||
|
const recovered = recoverSecret(a, b);
|
||||||
|
if (recovered !== originalEntropy) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user