Compare commits
17 Commits
b8c241c1a0
...
feature/va
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
50e78c84c9 | ||
|
|
8e6c621f7b | ||
|
|
7b8511f080 | ||
|
|
f6fa19d0b2 | ||
|
|
536513ab3f | ||
|
|
240a7eea8b | ||
| d64a6557a8 | |||
|
|
22dc3abf65 | ||
|
|
ed1f6fc49d | ||
|
|
218b2e8b29 | ||
| 56bb72aab8 | |||
|
|
fb1377eb4b | ||
|
|
c07f1f20d5 | ||
| 749ed2f05a | |||
|
|
da4a7de0ad | ||
|
|
146320052e | ||
|
|
4d94888bb8 |
70
App.tsx
@@ -1,17 +1,65 @@
|
|||||||
|
/**
|
||||||
|
* 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 { Buffer } from 'buffer';
|
||||||
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';
|
||||||
|
|
||||||
|
if (typeof globalThis !== 'undefined' && !globalThis.Buffer) {
|
||||||
|
globalThis.Buffer = Buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 +67,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
@@ -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**: 管理员操作
|
||||||
|
|
||||||
## 运行项目
|
## 运行项目
|
||||||
|
|
||||||
|
|||||||
6
app.json
@@ -19,14 +19,10 @@
|
|||||||
"bundleIdentifier": "com.sentinel.app"
|
"bundleIdentifier": "com.sentinel.app"
|
||||||
},
|
},
|
||||||
"android": {
|
"android": {
|
||||||
"adaptiveIcon": {
|
|
||||||
"foregroundImage": "./assets/adaptive-icon.png",
|
|
||||||
"backgroundColor": "#459E9E"
|
|
||||||
},
|
|
||||||
"package": "com.sentinel.app"
|
"package": "com.sentinel.app"
|
||||||
},
|
},
|
||||||
"web": {
|
"web": {
|
||||||
"favicon": "./assets/favicon.png",
|
"favicon": "./assets/icon.png",
|
||||||
"bundler": "metro"
|
"bundler": "metro"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 86 B After Width: | Height: | Size: 70 B |
|
Before Width: | Height: | Size: 105 B After Width: | Height: | Size: 70 B |
BIN
assets/icon.png
|
Before Width: | Height: | Size: 52 B After Width: | Height: | Size: 70 B |
|
Before Width: | Height: | Size: 108 B After Width: | Height: | Size: 70 B |
234
package-lock.json
generated
@@ -10,13 +10,18 @@
|
|||||||
"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",
|
||||||
|
"bip39": "^3.1.0",
|
||||||
|
"buffer": "^6.0.3",
|
||||||
"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 +31,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": {
|
||||||
@@ -76,7 +82,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.6.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.6.tgz",
|
||||||
"integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==",
|
"integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.28.6",
|
"@babel/code-frame": "^7.28.6",
|
||||||
"@babel/generator": "^7.28.6",
|
"@babel/generator": "^7.28.6",
|
||||||
@@ -480,6 +485,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.28.5.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.28.5.tgz",
|
||||||
"integrity": "sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q==",
|
"integrity": "sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-plugin-utils": "^7.27.1",
|
"@babel/helper-plugin-utils": "^7.27.1",
|
||||||
"@babel/traverse": "^7.28.5"
|
"@babel/traverse": "^7.28.5"
|
||||||
@@ -496,6 +502,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.27.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.27.1.tgz",
|
||||||
"integrity": "sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==",
|
"integrity": "sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-plugin-utils": "^7.27.1"
|
"@babel/helper-plugin-utils": "^7.27.1"
|
||||||
},
|
},
|
||||||
@@ -511,6 +518,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.27.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.27.1.tgz",
|
||||||
"integrity": "sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==",
|
"integrity": "sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-plugin-utils": "^7.27.1"
|
"@babel/helper-plugin-utils": "^7.27.1"
|
||||||
},
|
},
|
||||||
@@ -526,6 +534,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz",
|
||||||
"integrity": "sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==",
|
"integrity": "sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-plugin-utils": "^7.27.1",
|
"@babel/helper-plugin-utils": "^7.27.1",
|
||||||
"@babel/helper-skip-transparent-expression-wrappers": "^7.27.1",
|
"@babel/helper-skip-transparent-expression-wrappers": "^7.27.1",
|
||||||
@@ -543,6 +552,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.28.6.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.28.6.tgz",
|
||||||
"integrity": "sha512-a0aBScVTlNaiUe35UtfxAN7A/tehvvG4/ByO6+46VPKTRSlfnAFsgKy0FUh+qAkQrDTmhDkT+IBOKlOoMUxQ0g==",
|
"integrity": "sha512-a0aBScVTlNaiUe35UtfxAN7A/tehvvG4/ByO6+46VPKTRSlfnAFsgKy0FUh+qAkQrDTmhDkT+IBOKlOoMUxQ0g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-plugin-utils": "^7.28.6",
|
"@babel/helper-plugin-utils": "^7.28.6",
|
||||||
"@babel/traverse": "^7.28.6"
|
"@babel/traverse": "^7.28.6"
|
||||||
@@ -643,6 +653,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz",
|
||||||
"integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==",
|
"integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
},
|
},
|
||||||
@@ -763,6 +774,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.28.6.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.28.6.tgz",
|
||||||
"integrity": "sha512-pSJUpFHdx9z5nqTSirOCMtYVP2wFgoWhP0p3g8ONK/4IHhLIBd0B9NYqAvIUAhq+OkhO4VM1tENCt0cjlsNShw==",
|
"integrity": "sha512-pSJUpFHdx9z5nqTSirOCMtYVP2wFgoWhP0p3g8ONK/4IHhLIBd0B9NYqAvIUAhq+OkhO4VM1tENCt0cjlsNShw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-plugin-utils": "^7.28.6"
|
"@babel/helper-plugin-utils": "^7.28.6"
|
||||||
},
|
},
|
||||||
@@ -949,6 +961,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz",
|
||||||
"integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==",
|
"integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-create-regexp-features-plugin": "^7.18.6",
|
"@babel/helper-create-regexp-features-plugin": "^7.18.6",
|
||||||
"@babel/helper-plugin-utils": "^7.18.6"
|
"@babel/helper-plugin-utils": "^7.18.6"
|
||||||
@@ -1014,6 +1027,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz",
|
||||||
"integrity": "sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==",
|
"integrity": "sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-plugin-utils": "^7.27.1"
|
"@babel/helper-plugin-utils": "^7.27.1"
|
||||||
},
|
},
|
||||||
@@ -1060,6 +1074,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.6.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.6.tgz",
|
||||||
"integrity": "sha512-rfQ++ghVwTWTqQ7w8qyDxL1XGihjBss4CmTgGRCTAC9RIbhVpyp4fOeZtta0Lbf+dTNIVJer6ych2ibHwkZqsQ==",
|
"integrity": "sha512-rfQ++ghVwTWTqQ7w8qyDxL1XGihjBss4CmTgGRCTAC9RIbhVpyp4fOeZtta0Lbf+dTNIVJer6ych2ibHwkZqsQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-create-class-features-plugin": "^7.28.6",
|
"@babel/helper-create-class-features-plugin": "^7.28.6",
|
||||||
"@babel/helper-plugin-utils": "^7.28.6"
|
"@babel/helper-plugin-utils": "^7.28.6"
|
||||||
@@ -1128,6 +1143,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.28.6.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.28.6.tgz",
|
||||||
"integrity": "sha512-SljjowuNKB7q5Oayv4FoPzeB74g3QgLt8IVJw9ADvWy3QnUb/01aw8I4AVv8wYnPvQz2GDDZ/g3GhcNyDBI4Bg==",
|
"integrity": "sha512-SljjowuNKB7q5Oayv4FoPzeB74g3QgLt8IVJw9ADvWy3QnUb/01aw8I4AVv8wYnPvQz2GDDZ/g3GhcNyDBI4Bg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-create-regexp-features-plugin": "^7.28.5",
|
"@babel/helper-create-regexp-features-plugin": "^7.28.5",
|
||||||
"@babel/helper-plugin-utils": "^7.28.6"
|
"@babel/helper-plugin-utils": "^7.28.6"
|
||||||
@@ -1144,6 +1160,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.27.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.27.1.tgz",
|
||||||
"integrity": "sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==",
|
"integrity": "sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-plugin-utils": "^7.27.1"
|
"@babel/helper-plugin-utils": "^7.27.1"
|
||||||
},
|
},
|
||||||
@@ -1159,6 +1176,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.28.6.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.28.6.tgz",
|
||||||
"integrity": "sha512-5suVoXjC14lUN6ZL9OLKIHCNVWCrqGqlmEp/ixdXjvgnEl/kauLvvMO/Xw9NyMc95Joj1AeLVPVMvibBgSoFlA==",
|
"integrity": "sha512-5suVoXjC14lUN6ZL9OLKIHCNVWCrqGqlmEp/ixdXjvgnEl/kauLvvMO/Xw9NyMc95Joj1AeLVPVMvibBgSoFlA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-create-regexp-features-plugin": "^7.28.5",
|
"@babel/helper-create-regexp-features-plugin": "^7.28.5",
|
||||||
"@babel/helper-plugin-utils": "^7.28.6"
|
"@babel/helper-plugin-utils": "^7.28.6"
|
||||||
@@ -1175,6 +1193,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.27.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.27.1.tgz",
|
||||||
"integrity": "sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==",
|
"integrity": "sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-plugin-utils": "^7.27.1"
|
"@babel/helper-plugin-utils": "^7.27.1"
|
||||||
},
|
},
|
||||||
@@ -1190,6 +1209,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.28.6.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.28.6.tgz",
|
||||||
"integrity": "sha512-Iao5Konzx2b6g7EPqTy40UZbcdXE126tTxVFr/nAIj+WItNxjKSYTEw3RC+A2/ZetmdJsgueL1KhaMCQHkLPIg==",
|
"integrity": "sha512-Iao5Konzx2b6g7EPqTy40UZbcdXE126tTxVFr/nAIj+WItNxjKSYTEw3RC+A2/ZetmdJsgueL1KhaMCQHkLPIg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-plugin-utils": "^7.28.6",
|
"@babel/helper-plugin-utils": "^7.28.6",
|
||||||
"@babel/plugin-transform-destructuring": "^7.28.5"
|
"@babel/plugin-transform-destructuring": "^7.28.5"
|
||||||
@@ -1206,6 +1226,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.28.6.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.28.6.tgz",
|
||||||
"integrity": "sha512-WitabqiGjV/vJ0aPOLSFfNY1u9U3R7W36B03r5I2KoNix+a3sOhJ3pKFB3R5It9/UiK78NiO0KE9P21cMhlPkw==",
|
"integrity": "sha512-WitabqiGjV/vJ0aPOLSFfNY1u9U3R7W36B03r5I2KoNix+a3sOhJ3pKFB3R5It9/UiK78NiO0KE9P21cMhlPkw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-plugin-utils": "^7.28.6"
|
"@babel/helper-plugin-utils": "^7.28.6"
|
||||||
},
|
},
|
||||||
@@ -1285,6 +1306,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.28.6.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.28.6.tgz",
|
||||||
"integrity": "sha512-Nr+hEN+0geQkzhbdgQVPoqr47lZbm+5fCUmO70722xJZd0Mvb59+33QLImGj6F+DkK3xgDi1YVysP8whD6FQAw==",
|
"integrity": "sha512-Nr+hEN+0geQkzhbdgQVPoqr47lZbm+5fCUmO70722xJZd0Mvb59+33QLImGj6F+DkK3xgDi1YVysP8whD6FQAw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-plugin-utils": "^7.28.6"
|
"@babel/helper-plugin-utils": "^7.28.6"
|
||||||
},
|
},
|
||||||
@@ -1330,6 +1352,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz",
|
||||||
"integrity": "sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==",
|
"integrity": "sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-plugin-utils": "^7.27.1"
|
"@babel/helper-plugin-utils": "^7.27.1"
|
||||||
},
|
},
|
||||||
@@ -1345,6 +1368,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.27.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.27.1.tgz",
|
||||||
"integrity": "sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==",
|
"integrity": "sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-module-transforms": "^7.27.1",
|
"@babel/helper-module-transforms": "^7.27.1",
|
||||||
"@babel/helper-plugin-utils": "^7.27.1"
|
"@babel/helper-plugin-utils": "^7.27.1"
|
||||||
@@ -1377,6 +1401,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.28.5.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.28.5.tgz",
|
||||||
"integrity": "sha512-vn5Jma98LCOeBy/KpeQhXcV2WZgaRUtjwQmjoBuLNlOmkg0fB5pdvYVeWRYI69wWKwK2cD1QbMiUQnoujWvrew==",
|
"integrity": "sha512-vn5Jma98LCOeBy/KpeQhXcV2WZgaRUtjwQmjoBuLNlOmkg0fB5pdvYVeWRYI69wWKwK2cD1QbMiUQnoujWvrew==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-module-transforms": "^7.28.3",
|
"@babel/helper-module-transforms": "^7.28.3",
|
||||||
"@babel/helper-plugin-utils": "^7.27.1",
|
"@babel/helper-plugin-utils": "^7.27.1",
|
||||||
@@ -1395,6 +1420,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.27.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.27.1.tgz",
|
||||||
"integrity": "sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==",
|
"integrity": "sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-module-transforms": "^7.27.1",
|
"@babel/helper-module-transforms": "^7.27.1",
|
||||||
"@babel/helper-plugin-utils": "^7.27.1"
|
"@babel/helper-plugin-utils": "^7.27.1"
|
||||||
@@ -1427,6 +1453,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.27.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.27.1.tgz",
|
||||||
"integrity": "sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==",
|
"integrity": "sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-plugin-utils": "^7.27.1"
|
"@babel/helper-plugin-utils": "^7.27.1"
|
||||||
},
|
},
|
||||||
@@ -1491,6 +1518,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz",
|
||||||
"integrity": "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==",
|
"integrity": "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-plugin-utils": "^7.27.1",
|
"@babel/helper-plugin-utils": "^7.27.1",
|
||||||
"@babel/helper-replace-supers": "^7.27.1"
|
"@babel/helper-replace-supers": "^7.27.1"
|
||||||
@@ -1586,6 +1614,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz",
|
||||||
"integrity": "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==",
|
"integrity": "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-plugin-utils": "^7.27.1"
|
"@babel/helper-plugin-utils": "^7.27.1"
|
||||||
},
|
},
|
||||||
@@ -1711,6 +1740,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.28.6.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.28.6.tgz",
|
||||||
"integrity": "sha512-QGWAepm9qxpaIs7UM9FvUSnCGlb8Ua1RhyM4/veAxLwt3gMat/LSGrZixyuj4I6+Kn9iwvqCyPTtbdxanYoWYg==",
|
"integrity": "sha512-QGWAepm9qxpaIs7UM9FvUSnCGlb8Ua1RhyM4/veAxLwt3gMat/LSGrZixyuj4I6+Kn9iwvqCyPTtbdxanYoWYg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-create-regexp-features-plugin": "^7.28.5",
|
"@babel/helper-create-regexp-features-plugin": "^7.28.5",
|
||||||
"@babel/helper-plugin-utils": "^7.28.6"
|
"@babel/helper-plugin-utils": "^7.28.6"
|
||||||
@@ -1727,6 +1757,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.27.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.27.1.tgz",
|
||||||
"integrity": "sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==",
|
"integrity": "sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-plugin-utils": "^7.27.1"
|
"@babel/helper-plugin-utils": "^7.27.1"
|
||||||
},
|
},
|
||||||
@@ -1823,6 +1854,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.1.tgz",
|
||||||
"integrity": "sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==",
|
"integrity": "sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-plugin-utils": "^7.27.1"
|
"@babel/helper-plugin-utils": "^7.27.1"
|
||||||
},
|
},
|
||||||
@@ -1857,6 +1889,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz",
|
||||||
"integrity": "sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==",
|
"integrity": "sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-plugin-utils": "^7.27.1"
|
"@babel/helper-plugin-utils": "^7.27.1"
|
||||||
},
|
},
|
||||||
@@ -1872,6 +1905,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.28.6.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.28.6.tgz",
|
||||||
"integrity": "sha512-4Wlbdl/sIZjzi/8St0evF0gEZrgOswVO6aOzqxh1kDZOl9WmLrHq2HtGhnOJZmHZYKP8WZ1MDLCt5DAWwRo57A==",
|
"integrity": "sha512-4Wlbdl/sIZjzi/8St0evF0gEZrgOswVO6aOzqxh1kDZOl9WmLrHq2HtGhnOJZmHZYKP8WZ1MDLCt5DAWwRo57A==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-create-regexp-features-plugin": "^7.28.5",
|
"@babel/helper-create-regexp-features-plugin": "^7.28.5",
|
||||||
"@babel/helper-plugin-utils": "^7.28.6"
|
"@babel/helper-plugin-utils": "^7.28.6"
|
||||||
@@ -1904,6 +1938,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.28.6.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.28.6.tgz",
|
||||||
"integrity": "sha512-/wHc/paTUmsDYN7SZkpWxogTOBNnlx7nBQYfy6JJlCT7G3mVhltk3e++N7zV0XfgGsrqBxd4rJQt9H16I21Y1Q==",
|
"integrity": "sha512-/wHc/paTUmsDYN7SZkpWxogTOBNnlx7nBQYfy6JJlCT7G3mVhltk3e++N7zV0XfgGsrqBxd4rJQt9H16I21Y1Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-create-regexp-features-plugin": "^7.28.5",
|
"@babel/helper-create-regexp-features-plugin": "^7.28.5",
|
||||||
"@babel/helper-plugin-utils": "^7.28.6"
|
"@babel/helper-plugin-utils": "^7.28.6"
|
||||||
@@ -2022,6 +2057,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz",
|
||||||
"integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==",
|
"integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-plugin-utils": "^7.0.0",
|
"@babel/helper-plugin-utils": "^7.0.0",
|
||||||
"@babel/types": "^7.4.4",
|
"@babel/types": "^7.4.4",
|
||||||
@@ -2639,7 +2675,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@expo/metro-runtime/-/metro-runtime-4.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@expo/metro-runtime/-/metro-runtime-4.0.1.tgz",
|
||||||
"integrity": "sha512-CRpbLvdJ1T42S+lrYa1iZp1KfDeBp4oeZOK3hdpiS5n0vR0nhD6sC1gGF0sTboCTp64tLteikz5Y3j53dvgOIw==",
|
"integrity": "sha512-CRpbLvdJ1T42S+lrYa1iZp1KfDeBp4oeZOK3hdpiS5n0vR0nhD6sC1gGF0sTboCTp64tLteikz5Y3j53dvgOIw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react-native": "*"
|
"react-native": "*"
|
||||||
}
|
}
|
||||||
@@ -3176,6 +3211,18 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@noble/hashes": {
|
||||||
|
"version": "1.8.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
|
||||||
|
"integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "^14.21.3 || >=16"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@nodelib/fs.scandir": {
|
"node_modules/@nodelib/fs.scandir": {
|
||||||
"version": "2.1.5",
|
"version": "2.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||||
@@ -3245,6 +3292,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",
|
||||||
@@ -3672,7 +3731,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-6.1.18.tgz",
|
"resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-6.1.18.tgz",
|
||||||
"integrity": "sha512-mIT9MiL/vMm4eirLcmw2h6h/Nm5FICtnYSdohq4vTLA2FF/6PNhByM7s8ffqoVfE5L0uAa6Xda1B7oddolUiGg==",
|
"integrity": "sha512-mIT9MiL/vMm4eirLcmw2h6h/Nm5FICtnYSdohq4vTLA2FF/6PNhByM7s8ffqoVfE5L0uAa6Xda1B7oddolUiGg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@react-navigation/core": "^6.4.17",
|
"@react-navigation/core": "^6.4.17",
|
||||||
"escape-string-regexp": "^4.0.0",
|
"escape-string-regexp": "^4.0.0",
|
||||||
@@ -3684,6 +3742,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",
|
||||||
@@ -3837,7 +3912,6 @@
|
|||||||
"integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==",
|
"integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/prop-types": "*",
|
"@types/prop-types": "*",
|
||||||
"csstype": "^3.2.2"
|
"csstype": "^3.2.2"
|
||||||
@@ -4311,6 +4385,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",
|
||||||
@@ -4378,6 +4461,15 @@
|
|||||||
"node": ">=0.6"
|
"node": ">=0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/bip39": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/bip39/-/bip39-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-c9kiwdk45Do5GL0vJMe7tS95VjCii65mYAH7DfWl3uW8AVzXKQVUm64i3hzVybBDMp9r7j9iNxR85+ul8MdN/A==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"@noble/hashes": "^1.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/bplist-creator": {
|
"node_modules/bplist-creator": {
|
||||||
"version": "0.0.7",
|
"version": "0.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.0.7.tgz",
|
||||||
@@ -4440,7 +4532,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.9.0",
|
"baseline-browser-mapping": "^2.9.0",
|
||||||
"caniuse-lite": "^1.0.30001759",
|
"caniuse-lite": "^1.0.30001759",
|
||||||
@@ -4465,9 +4556,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/buffer": {
|
"node_modules/buffer": {
|
||||||
"version": "5.7.1",
|
"version": "6.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
|
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
||||||
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
|
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -4485,7 +4576,7 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"base64-js": "^1.3.1",
|
"base64-js": "^1.3.1",
|
||||||
"ieee754": "^1.1.13"
|
"ieee754": "^1.2.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/buffer-alloc": {
|
"node_modules/buffer-alloc": {
|
||||||
@@ -5097,6 +5188,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",
|
||||||
@@ -5456,6 +5556,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
|
||||||
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
|
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@@ -5580,7 +5681,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/expo/-/expo-52.0.48.tgz",
|
"resolved": "https://registry.npmjs.org/expo/-/expo-52.0.48.tgz",
|
||||||
"integrity": "sha512-/HR/vuo57KGEWlvF3GWaquwEAjXuA5hrOCsaLcZ3pMSA8mQ27qKd1jva4GWzpxXYedlzs/7LLP1XpZo6hXTsog==",
|
"integrity": "sha512-/HR/vuo57KGEWlvF3GWaquwEAjXuA5hrOCsaLcZ3pMSA8mQ27qKd1jva4GWzpxXYedlzs/7LLP1XpZo6hXTsog==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.20.0",
|
"@babel/runtime": "^7.20.0",
|
||||||
"@expo/cli": "0.22.27",
|
"@expo/cli": "0.22.27",
|
||||||
@@ -5678,6 +5778,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 +6480,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 +6802,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 +7690,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",
|
||||||
@@ -9053,7 +9208,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
||||||
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"loose-envify": "^1.1.0"
|
"loose-envify": "^1.1.0"
|
||||||
},
|
},
|
||||||
@@ -9137,7 +9291,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-native/-/react-native-0.76.9.tgz",
|
"resolved": "https://registry.npmjs.org/react-native/-/react-native-0.76.9.tgz",
|
||||||
"integrity": "sha512-+LRwecWmTDco7OweGsrECIqJu0iyrREd6CTCgC/uLLYipiHvk+MH9nd6drFtCw/6Blz6eoKTcH9YTTJusNtrWg==",
|
"integrity": "sha512-+LRwecWmTDco7OweGsrECIqJu0iyrREd6CTCgC/uLLYipiHvk+MH9nd6drFtCw/6Blz6eoKTcH9YTTJusNtrWg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jest/create-cache-key-function": "^29.6.3",
|
"@jest/create-cache-key-function": "^29.6.3",
|
||||||
"@react-native/assets-registry": "0.76.9",
|
"@react-native/assets-registry": "0.76.9",
|
||||||
@@ -9239,7 +9392,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-4.12.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-4.12.0.tgz",
|
||||||
"integrity": "sha512-ukk5PxcF4p3yu6qMZcmeiZgowhb5AsKRnil54YFUUAXVIS7PJcMHGGC+q44fCiBg44/1AJk5njGMez1m9H0BVQ==",
|
"integrity": "sha512-ukk5PxcF4p3yu6qMZcmeiZgowhb5AsKRnil54YFUUAXVIS7PJcMHGGC+q44fCiBg44/1AJk5njGMez1m9H0BVQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": "*",
|
"react": "*",
|
||||||
"react-native": "*"
|
"react-native": "*"
|
||||||
@@ -9250,7 +9402,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-4.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-4.4.0.tgz",
|
||||||
"integrity": "sha512-c7zc7Zwjty6/pGyuuvh9gK3YBYqHPOxrhXfG1lF4gHlojQSmIx2piNbNaV+Uykj+RDTmFXK0e/hA+fucw/Qozg==",
|
"integrity": "sha512-c7zc7Zwjty6/pGyuuvh9gK3YBYqHPOxrhXfG1lF4gHlojQSmIx2piNbNaV+Uykj+RDTmFXK0e/hA+fucw/Qozg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react-freeze": "^1.0.0",
|
"react-freeze": "^1.0.0",
|
||||||
"warn-once": "^0.1.0"
|
"warn-once": "^0.1.0"
|
||||||
@@ -9260,6 +9411,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 +10673,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 +10969,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",
|
||||||
@@ -10898,6 +11080,30 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/whatwg-url-without-unicode/node_modules/buffer": {
|
||||||
|
"version": "5.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
|
||||||
|
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"base64-js": "^1.3.1",
|
||||||
|
"ieee754": "^1.1.13"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/whatwg-url-without-unicode/node_modules/webidl-conversions": {
|
"node_modules/whatwg-url-without-unicode/node_modules/webidl-conversions": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz",
|
||||||
|
|||||||
@@ -11,19 +11,25 @@
|
|||||||
"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",
|
||||||
|
"bip39": "^3.1.0",
|
||||||
|
"buffer": "^6.0.3",
|
||||||
"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",
|
||||||
|
|||||||
218
src/config/index.ts
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Vault storage (user-isolated, multi-account)
|
||||||
|
// =============================================================================
|
||||||
|
// - AsyncStorage keys for vault state (S0 share, initialized flag).
|
||||||
|
// - User-scoped: each account has its own keys so vault state is isolated.
|
||||||
|
// - Store: use getVaultStorageKeys(userId) and write to INITIALIZED / SHARE_DEVICE.
|
||||||
|
// - Clear: use same keys in multiRemove (e.g. MeScreen Reset Vault State).
|
||||||
|
// - Multi-account: same device, multiple users → each has independent vault (no cross-user leakage).
|
||||||
|
|
||||||
|
const VAULT_KEY_PREFIX = 'sentinel_vault';
|
||||||
|
|
||||||
|
/** Base key names (for reference). Prefer getVaultStorageKeys(userId) for all reads/writes. */
|
||||||
|
export const VAULT_STORAGE_KEYS = {
|
||||||
|
INITIALIZED: 'sentinel_vault_initialized',
|
||||||
|
SHARE_DEVICE: 'sentinel_vault_s0',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns vault storage keys for the given user (user isolation).
|
||||||
|
* - Use for: reading S0, writing S0 after mnemonic, clearing on Reset Vault State.
|
||||||
|
* - userId null → guest namespace (_guest). userId set → per-user namespace (_u{userId}).
|
||||||
|
*/
|
||||||
|
export function getVaultStorageKeys(userId: number | string | null): {
|
||||||
|
INITIALIZED: string;
|
||||||
|
SHARE_DEVICE: string;
|
||||||
|
} {
|
||||||
|
const suffix = userId != null ? `_u${userId}` : '_guest';
|
||||||
|
return {
|
||||||
|
INITIALIZED: `${VAULT_KEY_PREFIX}_initialized${suffix}`,
|
||||||
|
SHARE_DEVICE: `${VAULT_KEY_PREFIX}_s0${suffix}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// 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',
|
||||||
|
email: 'captain@sentinel.local',
|
||||||
|
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,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI Roles configuration
|
||||||
|
*/
|
||||||
|
ROLES: [
|
||||||
|
{
|
||||||
|
id: 'reflective',
|
||||||
|
name: 'Reflective Assistant',
|
||||||
|
description: 'Helps you dive deep into your thoughts and feelings through meaningful reflection.',
|
||||||
|
systemPrompt: 'You are a helpful journal assistant. Help the user reflect on their thoughts and feelings.',
|
||||||
|
icon: 'journal-outline',
|
||||||
|
iconFamily: 'Ionicons',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'creative',
|
||||||
|
name: 'Creative Spark',
|
||||||
|
description: 'A partner for brainstorming, creative writing, and exploring new ideas.',
|
||||||
|
systemPrompt: 'You are a creative brainstorming partner. Help the user explore new ideas, write stories, or look at things from a fresh perspective.',
|
||||||
|
icon: 'bulb-outline',
|
||||||
|
iconFamily: 'Ionicons',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'planner',
|
||||||
|
name: 'Action Planner',
|
||||||
|
description: 'Focused on turning thoughts into actionable plans and organized goals.',
|
||||||
|
systemPrompt: 'You are a productivity coach. Help the user break down their thoughts into actionable steps and clear goals.',
|
||||||
|
icon: 'list-outline',
|
||||||
|
iconFamily: 'Ionicons',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'empathetic',
|
||||||
|
name: 'Empathetic Guide',
|
||||||
|
description: 'Provides a safe, non-judgmental space for emotional support and empathy.',
|
||||||
|
systemPrompt: 'You are a supportive and empathetic friend. Listen to the user\'s concerns and provide emotional support without judgment.',
|
||||||
|
icon: 'heart-outline',
|
||||||
|
iconFamily: 'Ionicons',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Export Type Definitions
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export type ApiEndpoint = typeof API_ENDPOINTS;
|
||||||
177
src/context/AuthContext.tsx
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
/**
|
||||||
|
* 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';
|
||||||
|
import { storageService } from '../services/storage.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 and session data
|
||||||
|
*/
|
||||||
|
const signOut = () => {
|
||||||
|
setUser(null);
|
||||||
|
setToken(null);
|
||||||
|
clearAuth();
|
||||||
|
//storageService.clearAllData();
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
6
src/hooks/index.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* React hooks for Sentinel
|
||||||
|
*/
|
||||||
|
|
||||||
|
export { useVaultAssets } from './useVaultAssets';
|
||||||
|
export type { CreateAssetResult, UseVaultAssetsReturn } from './useVaultAssets';
|
||||||
160
src/hooks/useVaultAssets.ts
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
/**
|
||||||
|
* useVaultAssets: Encapsulates /assets/get and /assets/create for VaultScreen.
|
||||||
|
* - Fetches assets when vault is unlocked and token exists.
|
||||||
|
* - Exposes createAsset with 401/network error handling and list refresh on success.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
|
import * as bip39 from 'bip39';
|
||||||
|
import { useAuth } from '../context/AuthContext';
|
||||||
|
import { assetsService } from '../services/assets.service';
|
||||||
|
import { createAssetPayload } from '../services/vault.service';
|
||||||
|
import {
|
||||||
|
initialVaultAssets,
|
||||||
|
mapApiAssetsToVaultAssets,
|
||||||
|
type ApiAsset,
|
||||||
|
} from '../utils/vaultAssets';
|
||||||
|
import type { VaultAsset } from '../types';
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// Types
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export interface CreateAssetResult {
|
||||||
|
success: boolean;
|
||||||
|
isUnauthorized?: boolean;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UseVaultAssetsReturn {
|
||||||
|
/** Current list (mock until API succeeds) */
|
||||||
|
assets: VaultAsset[];
|
||||||
|
/** Replace list (e.g. after external refresh) */
|
||||||
|
setAssets: React.Dispatch<React.SetStateAction<VaultAsset[]>>;
|
||||||
|
/** Refetch from GET /assets/get */
|
||||||
|
refreshAssets: () => Promise<void>;
|
||||||
|
/** Create asset via POST /assets/create; on success refreshes list */
|
||||||
|
createAsset: (params: { title: string; content: string }) => Promise<CreateAssetResult>;
|
||||||
|
/** True while create request is in flight */
|
||||||
|
isSealing: boolean;
|
||||||
|
/** Error message from last create failure (non-401) */
|
||||||
|
createError: string | null;
|
||||||
|
/** Clear createError */
|
||||||
|
clearCreateError: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// Hook
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vault assets list + create. Fetches on unlock when token exists; keeps mock on error.
|
||||||
|
*/
|
||||||
|
export function useVaultAssets(isUnlocked: boolean): UseVaultAssetsReturn {
|
||||||
|
const { token, signOut } = useAuth();
|
||||||
|
const [assets, setAssets] = useState<VaultAsset[]>(initialVaultAssets);
|
||||||
|
const [isSealing, setIsSealing] = useState(false);
|
||||||
|
const [createError, setCreateError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const refreshAssets = useCallback(async () => {
|
||||||
|
if (!token) return;
|
||||||
|
try {
|
||||||
|
const list = await assetsService.getMyAssets(token);
|
||||||
|
if (Array.isArray(list)) {
|
||||||
|
setAssets(mapApiAssetsToVaultAssets(list as ApiAsset[]));
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Keep current assets (mock or previous fetch)
|
||||||
|
}
|
||||||
|
}, [token]);
|
||||||
|
|
||||||
|
// Fetch list when unlocked and token exists
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isUnlocked || !token) return;
|
||||||
|
let cancelled = false;
|
||||||
|
assetsService
|
||||||
|
.getMyAssets(token)
|
||||||
|
.then((list) => {
|
||||||
|
if (!cancelled && Array.isArray(list)) {
|
||||||
|
setAssets(mapApiAssetsToVaultAssets(list as ApiAsset[]));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// Keep initial (mock) assets
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [isUnlocked, token]);
|
||||||
|
|
||||||
|
const createAsset = useCallback(
|
||||||
|
async ({
|
||||||
|
title,
|
||||||
|
content,
|
||||||
|
}: {
|
||||||
|
title: string;
|
||||||
|
content: string;
|
||||||
|
}): Promise<CreateAssetResult> => {
|
||||||
|
if (!token) {
|
||||||
|
return { success: false, error: 'Not logged in.' };
|
||||||
|
}
|
||||||
|
setIsSealing(true);
|
||||||
|
setCreateError(null);
|
||||||
|
try {
|
||||||
|
const wordList = bip39.wordlists.english;
|
||||||
|
const payload = await createAssetPayload(
|
||||||
|
title.trim(),
|
||||||
|
content.trim(),
|
||||||
|
wordList,
|
||||||
|
'note',
|
||||||
|
0
|
||||||
|
);
|
||||||
|
await assetsService.createAsset(
|
||||||
|
{
|
||||||
|
title: payload.title,
|
||||||
|
private_key_shard: payload.private_key_shard,
|
||||||
|
content_inner_encrypted: payload.content_inner_encrypted,
|
||||||
|
},
|
||||||
|
token
|
||||||
|
);
|
||||||
|
await refreshAssets();
|
||||||
|
return { success: true };
|
||||||
|
} catch (err: unknown) {
|
||||||
|
const status =
|
||||||
|
err && typeof err === 'object' && 'status' in err
|
||||||
|
? (err as { status?: number }).status
|
||||||
|
: undefined;
|
||||||
|
const rawMessage =
|
||||||
|
err instanceof Error ? err.message : String(err ?? 'Failed to create.');
|
||||||
|
const isUnauthorized =
|
||||||
|
status === 401 || /401|Unauthorized/i.test(rawMessage);
|
||||||
|
|
||||||
|
if (isUnauthorized) {
|
||||||
|
signOut();
|
||||||
|
return { success: false, isUnauthorized: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
const friendlyMessage = /failed to fetch|network error/i.test(rawMessage)
|
||||||
|
? 'Network error. Please check that the backend is running and reachable (see API_BASE_URL in config).'
|
||||||
|
: rawMessage;
|
||||||
|
setCreateError(friendlyMessage);
|
||||||
|
return { success: false, error: friendlyMessage };
|
||||||
|
} finally {
|
||||||
|
setIsSealing(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[token, refreshAssets, signOut]
|
||||||
|
);
|
||||||
|
|
||||||
|
const clearCreateError = useCallback(() => setCreateError(null), []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
assets,
|
||||||
|
setAssets,
|
||||||
|
refreshAssets,
|
||||||
|
createAsset,
|
||||||
|
isSealing,
|
||||||
|
createError,
|
||||||
|
clearCreateError,
|
||||||
|
};
|
||||||
|
}
|
||||||
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}
|
||||||
|
|||||||
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',
|
||||||
|
},
|
||||||
|
});
|
||||||
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',
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -7,17 +7,30 @@ import {
|
|||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
SafeAreaView,
|
SafeAreaView,
|
||||||
Animated,
|
Animated,
|
||||||
|
Modal,
|
||||||
} 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 { SystemStatus, KillSwitchLog } from '../types';
|
import { SystemStatus, KillSwitchLog } from '../types';
|
||||||
|
import VaultScreen from './VaultScreen';
|
||||||
|
|
||||||
|
// Animation timing constants
|
||||||
|
const ANIMATION_DURATION = {
|
||||||
|
pulse: 1200,
|
||||||
|
glow: 1500,
|
||||||
|
rotate: 30000,
|
||||||
|
heartbeatPress: 150,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// 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];
|
||||||
}> = {
|
}> = {
|
||||||
@@ -46,28 +59,14 @@ const statusConfig: Record<SystemStatus, {
|
|||||||
|
|
||||||
// Mock data
|
// Mock data
|
||||||
const initialLogs: KillSwitchLog[] = [
|
const initialLogs: KillSwitchLog[] = [
|
||||||
{
|
{ id: '1', action: 'HEARTBEAT_CONFIRMED', timestamp: new Date('2024-01-18T09:30:00') },
|
||||||
id: '1',
|
{ id: '2', action: 'SUBSCRIPTION_VERIFIED', timestamp: new Date('2024-01-17T00:00:00') },
|
||||||
action: 'HEARTBEAT_CONFIRMED',
|
{ id: '3', action: 'JOURNAL_ACTIVITY', timestamp: new Date('2024-01-16T15:42:00') },
|
||||||
timestamp: new Date('2024-01-18T09:30:00'),
|
{ id: '4', action: 'HEARTBEAT_CONFIRMED', timestamp: new Date('2024-01-15T11:20:00') },
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '2',
|
|
||||||
action: 'SUBSCRIPTION_VERIFIED',
|
|
||||||
timestamp: new Date('2024-01-17T00:00:00'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '3',
|
|
||||||
action: 'JOURNAL_ACTIVITY',
|
|
||||||
timestamp: new Date('2024-01-16T15:42:00'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: '4',
|
|
||||||
action: 'HEARTBEAT_CONFIRMED',
|
|
||||||
timestamp: new Date('2024-01-15T11:20:00'),
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export { VAULT_STORAGE_KEYS } from '../config';
|
||||||
|
|
||||||
export default function SentinelScreen() {
|
export default function SentinelScreen() {
|
||||||
const [status, setStatus] = useState<SystemStatus>('normal');
|
const [status, setStatus] = useState<SystemStatus>('normal');
|
||||||
const [lastSubscriptionCheck] = useState(new Date('2024-01-18T00:00:00'));
|
const [lastSubscriptionCheck] = useState(new Date('2024-01-18T00:00:00'));
|
||||||
@@ -76,102 +75,101 @@ 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);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Pulse animation
|
const pulseAnimation = Animated.loop(
|
||||||
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
|
const glowAnimation = Animated.loop(
|
||||||
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
|
const rotateAnimation = Animated.loop(
|
||||||
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();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
pulseAnimation.stop();
|
||||||
|
glowAnimation.stop();
|
||||||
|
rotateAnimation.stop();
|
||||||
|
};
|
||||||
|
}, [pulseAnim, glowAnim, rotateAnim]);
|
||||||
|
|
||||||
|
const openVault = () => setShowVault(true);
|
||||||
|
|
||||||
const handleHeartbeat = () => {
|
const handleHeartbeat = () => {
|
||||||
// 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
|
|
||||||
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
|
|
||||||
if (status === 'warning') {
|
if (status === 'warning') {
|
||||||
setStatus('normal');
|
setStatus('normal');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatDateTime = (date: Date) => {
|
const formatDateTime = (date: Date) =>
|
||||||
return date.toLocaleString('en-US', {
|
date.toLocaleString('en-US', {
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
month: '2-digit',
|
month: '2-digit',
|
||||||
day: '2-digit',
|
day: '2-digit',
|
||||||
hour: '2-digit',
|
hour: '2-digit',
|
||||||
minute: '2-digit',
|
minute: '2-digit',
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
|
||||||
const formatTimeAgo = (date: Date) => {
|
const formatTimeAgo = (date: Date) => {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const diff = now.getTime() - date.getTime();
|
const diff = now.getTime() - date.getTime();
|
||||||
const hours = Math.floor(diff / (1000 * 60 * 60));
|
const hours = Math.floor(diff / (1000 * 60 * 60));
|
||||||
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
|
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
|
||||||
|
if (hours > 24) return `${Math.floor(hours / 24)} days ago`;
|
||||||
if (hours > 24) {
|
if (hours > 0) return `${hours}h ${minutes}m ago`;
|
||||||
const days = Math.floor(hours / 24);
|
|
||||||
return `${days} days ago`;
|
|
||||||
}
|
|
||||||
if (hours > 0) {
|
|
||||||
return `${hours}h ${minutes}m ago`;
|
|
||||||
}
|
|
||||||
return `${minutes}m ago`;
|
return `${minutes}m ago`;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -188,7 +186,7 @@ export default function SentinelScreen() {
|
|||||||
style={styles.gradient}
|
style={styles.gradient}
|
||||||
>
|
>
|
||||||
<SafeAreaView style={styles.safeArea}>
|
<SafeAreaView style={styles.safeArea}>
|
||||||
<ScrollView
|
<ScrollView
|
||||||
style={styles.scrollView}
|
style={styles.scrollView}
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
contentContainerStyle={styles.scrollContent}
|
contentContainerStyle={styles.scrollContent}
|
||||||
@@ -204,14 +202,14 @@ export default function SentinelScreen() {
|
|||||||
|
|
||||||
{/* Status Display */}
|
{/* Status Display */}
|
||||||
<View style={styles.statusContainer}>
|
<View style={styles.statusContainer}>
|
||||||
<Animated.View
|
<Animated.View
|
||||||
style={[
|
style={[
|
||||||
styles.statusCircleOuter,
|
styles.statusCircleOuter,
|
||||||
{
|
{
|
||||||
transform: [{ scale: pulseAnim }],
|
transform: [{ scale: pulseAnim }],
|
||||||
opacity: glowAnim,
|
opacity: glowAnim,
|
||||||
backgroundColor: `${currentStatus.color}20`,
|
backgroundColor: `${currentStatus.color}20`,
|
||||||
}
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<Animated.View style={{ transform: [{ scale: pulseAnim }] }}>
|
<Animated.View style={{ transform: [{ scale: pulseAnim }] }}>
|
||||||
@@ -219,7 +217,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 }]}>
|
||||||
@@ -233,10 +231,10 @@ export default function SentinelScreen() {
|
|||||||
{/* Ship Wheel Watermark */}
|
{/* Ship Wheel Watermark */}
|
||||||
<View style={styles.wheelWatermark}>
|
<View style={styles.wheelWatermark}>
|
||||||
<Animated.View style={{ transform: [{ rotate: spin }] }}>
|
<Animated.View style={{ transform: [{ rotate: spin }] }}>
|
||||||
<MaterialCommunityIcons
|
<MaterialCommunityIcons
|
||||||
name="ship-wheel"
|
name="ship-wheel"
|
||||||
size={200}
|
size={200}
|
||||||
color={colors.sentinel.primary}
|
color={colors.sentinel.primary}
|
||||||
style={{ opacity: 0.03 }}
|
style={{ opacity: 0.03 }}
|
||||||
/>
|
/>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
@@ -249,25 +247,39 @@ export default function SentinelScreen() {
|
|||||||
<FontAwesome5 name="anchor" size={16} color={colors.sentinel.primary} />
|
<FontAwesome5 name="anchor" size={16} color={colors.sentinel.primary} />
|
||||||
</View>
|
</View>
|
||||||
<Text style={styles.metricLabel}>SUBSCRIPTION</Text>
|
<Text style={styles.metricLabel}>SUBSCRIPTION</Text>
|
||||||
<Text style={styles.metricValue}>
|
<Text style={styles.metricValue}>{formatTimeAgo(lastSubscriptionCheck)}</Text>
|
||||||
{formatTimeAgo(lastSubscriptionCheck)}
|
<Text style={styles.metricTime}>{formatDateTime(lastSubscriptionCheck)}</Text>
|
||||||
</Text>
|
|
||||||
<Text style={styles.metricTime}>
|
|
||||||
{formatDateTime(lastSubscriptionCheck)}
|
|
||||||
</Text>
|
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.metricCard}>
|
<View style={styles.metricCard}>
|
||||||
<View style={styles.metricIconContainer}>
|
<View style={styles.metricIconContainer}>
|
||||||
<Feather name="edit-3" size={16} color={colors.sentinel.primary} />
|
<Feather name="edit-3" size={16} color={colors.sentinel.primary} />
|
||||||
</View>
|
</View>
|
||||||
<Text style={styles.metricLabel}>LAST JOURNAL</Text>
|
<Text style={styles.metricLabel}>LAST JOURNAL</Text>
|
||||||
<Text style={styles.metricValue}>
|
<Text style={styles.metricValue}>{formatTimeAgo(lastFlowActivity)}</Text>
|
||||||
{formatTimeAgo(lastFlowActivity)}
|
<Text style={styles.metricTime}>{formatDateTime(lastFlowActivity)}</Text>
|
||||||
</Text>
|
</View>
|
||||||
<Text style={styles.metricTime}>
|
</View>
|
||||||
{formatDateTime(lastFlowActivity)}
|
|
||||||
|
{/* 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>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.vaultAccessButton}
|
||||||
|
onPress={openVault}
|
||||||
|
activeOpacity={0.8}
|
||||||
|
accessibilityLabel="Open Shadow Vault"
|
||||||
|
accessibilityRole="button"
|
||||||
|
>
|
||||||
|
<Text style={styles.vaultAccessButtonText}>Open</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Heartbeat Button */}
|
{/* Heartbeat Button */}
|
||||||
@@ -275,6 +287,8 @@ export default function SentinelScreen() {
|
|||||||
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]}
|
||||||
@@ -303,9 +317,7 @@ export default function SentinelScreen() {
|
|||||||
<View style={styles.logDot} />
|
<View style={styles.logDot} />
|
||||||
<View style={styles.logContent}>
|
<View style={styles.logContent}>
|
||||||
<Text style={styles.logAction}>{log.action}</Text>
|
<Text style={styles.logAction}>{log.action}</Text>
|
||||||
<Text style={styles.logTime}>
|
<Text style={styles.logTime}>{formatDateTime(log.timestamp)}</Text>
|
||||||
{formatDateTime(log.timestamp)}
|
|
||||||
</Text>
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
))}
|
))}
|
||||||
@@ -313,30 +325,40 @@ export default function SentinelScreen() {
|
|||||||
</ScrollView>
|
</ScrollView>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
</LinearGradient>
|
</LinearGradient>
|
||||||
|
|
||||||
|
{/* Vault Modal */}
|
||||||
|
<Modal
|
||||||
|
visible={showVault}
|
||||||
|
animationType="slide"
|
||||||
|
onRequestClose={() => setShowVault(false)}
|
||||||
|
>
|
||||||
|
<View style={styles.vaultModalContainer}>
|
||||||
|
{showVault ? <VaultScreen /> : null}
|
||||||
|
<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>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: { flex: 1 },
|
||||||
flex: 1,
|
gradient: { flex: 1 },
|
||||||
},
|
safeArea: { flex: 1 },
|
||||||
gradient: {
|
scrollView: { flex: 1 },
|
||||||
flex: 1,
|
|
||||||
},
|
|
||||||
safeArea: {
|
|
||||||
flex: 1,
|
|
||||||
},
|
|
||||||
scrollView: {
|
|
||||||
flex: 1,
|
|
||||||
},
|
|
||||||
scrollContent: {
|
scrollContent: {
|
||||||
padding: spacing.lg,
|
padding: spacing.lg,
|
||||||
paddingBottom: 120,
|
paddingBottom: 120,
|
||||||
},
|
},
|
||||||
header: {
|
header: { marginBottom: spacing.xl },
|
||||||
marginBottom: spacing.xl,
|
|
||||||
},
|
|
||||||
headerTitleRow: {
|
headerTitleRow: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
@@ -442,9 +464,7 @@ const styles = StyleSheet.create({
|
|||||||
marginBottom: spacing.xl,
|
marginBottom: spacing.xl,
|
||||||
...shadows.medium,
|
...shadows.medium,
|
||||||
},
|
},
|
||||||
heartbeatGradient: {
|
heartbeatGradient: { padding: spacing.lg },
|
||||||
padding: spacing.lg,
|
|
||||||
},
|
|
||||||
heartbeatContent: {
|
heartbeatContent: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
@@ -498,9 +518,7 @@ const styles = StyleSheet.create({
|
|||||||
marginTop: 6,
|
marginTop: 6,
|
||||||
marginRight: spacing.md,
|
marginRight: spacing.md,
|
||||||
},
|
},
|
||||||
logContent: {
|
logContent: { flex: 1 },
|
||||||
flex: 1,
|
|
||||||
},
|
|
||||||
logAction: {
|
logAction: {
|
||||||
fontSize: typography.fontSize.sm,
|
fontSize: typography.fontSize.sm,
|
||||||
color: colors.sentinel.text,
|
color: colors.sentinel.text,
|
||||||
@@ -513,4 +531,60 @@ const styles = StyleSheet.create({
|
|||||||
color: colors.sentinel.textSecondary,
|
color: colors.sentinel.textSecondary,
|
||||||
fontFamily: typography.fontFamily.mono,
|
fontFamily: typography.fontFamily.mono,
|
||||||
},
|
},
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
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',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
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;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
244
src/services/ai.service.ts
Normal file
@@ -0,0 +1,244 @@
|
|||||||
|
/**
|
||||||
|
* 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
|
||||||
|
* @param systemPrompt - Optional custom system prompt
|
||||||
|
* @returns AI response text
|
||||||
|
*/
|
||||||
|
async sendMessage(content: string, token?: string, systemPrompt?: string): Promise<string> {
|
||||||
|
const messages: AIMessage[] = [
|
||||||
|
{
|
||||||
|
role: 'system',
|
||||||
|
content: systemPrompt || 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;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
248
src/services/assets.service.ts
Normal file
@@ -0,0 +1,248 @@
|
|||||||
|
/**
|
||||||
|
* 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),
|
||||||
|
});
|
||||||
|
|
||||||
|
const responseStatus = response.status;
|
||||||
|
logApiDebug('Create Asset Response Status', responseStatus);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json().catch(() => ({}));
|
||||||
|
const detail = errorData.detail || 'Failed to create asset';
|
||||||
|
const message = responseStatus === 401 ? `Unauthorized (401): ${detail}` : detail;
|
||||||
|
const err = new Error(message) as Error & { status?: number };
|
||||||
|
err.status = responseStatus;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
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
@@ -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;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
31
src/services/index.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
/**
|
||||||
|
* 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';
|
||||||
|
export {
|
||||||
|
createVaultPayload,
|
||||||
|
createAssetPayload,
|
||||||
|
type CreateVaultPayloadResult,
|
||||||
|
type CreateAssetPayloadResult,
|
||||||
|
} from './vault.service';
|
||||||
120
src/services/storage.service.ts
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
/**
|
||||||
|
* Storage Service
|
||||||
|
*
|
||||||
|
* Handles local persistence of chat history and active conversations
|
||||||
|
* using AsyncStorage with user-specific isolation.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Constants
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
const STORAGE_KEYS = {
|
||||||
|
CHAT_HISTORY: '@sentinel:chat_history',
|
||||||
|
CURRENT_MESSAGES: '@sentinel:current_messages',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Service Implementation
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export const storageService = {
|
||||||
|
/**
|
||||||
|
* Get user-specific storage key
|
||||||
|
*/
|
||||||
|
getUserKey(baseKey: string, userId: string | number): string {
|
||||||
|
return `${baseKey}:user_${userId}`;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save the complete list of chat sessions to local storage for a specific user
|
||||||
|
*/
|
||||||
|
async saveChatHistory(history: any[], userId: string | number): Promise<void> {
|
||||||
|
try {
|
||||||
|
const jsonValue = JSON.stringify(history);
|
||||||
|
const key = this.getUserKey(STORAGE_KEYS.CHAT_HISTORY, userId);
|
||||||
|
await AsyncStorage.setItem(key, jsonValue);
|
||||||
|
console.log(`[Storage] Saved chat history for user ${userId}:`, history.length, 'sessions');
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error saving chat history:', e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load the list of chat sessions from local storage for a specific user
|
||||||
|
*/
|
||||||
|
async getChatHistory(userId: string | number): Promise<any[]> {
|
||||||
|
try {
|
||||||
|
const key = this.getUserKey(STORAGE_KEYS.CHAT_HISTORY, userId);
|
||||||
|
const jsonValue = await AsyncStorage.getItem(key);
|
||||||
|
const result = jsonValue != null ? JSON.parse(jsonValue) : [];
|
||||||
|
console.log(`[Storage] Loaded chat history for user ${userId}:`, result.length, 'sessions');
|
||||||
|
return result;
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error getting chat history:', e);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save the current active conversation messages for a specific role and user
|
||||||
|
*/
|
||||||
|
async saveCurrentChat(roleId: string, messages: any[], userId: string | number): Promise<void> {
|
||||||
|
try {
|
||||||
|
const jsonValue = JSON.stringify(messages);
|
||||||
|
const key = `${this.getUserKey(STORAGE_KEYS.CURRENT_MESSAGES, userId)}:${roleId}`;
|
||||||
|
await AsyncStorage.setItem(key, jsonValue);
|
||||||
|
console.log(`[Storage] Saved current chat for user ${userId}, role ${roleId}:`, messages.length, 'messages');
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Error saving current chat for role ${roleId}:`, e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load the current active conversation messages for a specific role and user
|
||||||
|
*/
|
||||||
|
async getCurrentChat(roleId: string, userId: string | number): Promise<any[]> {
|
||||||
|
try {
|
||||||
|
const key = `${this.getUserKey(STORAGE_KEYS.CURRENT_MESSAGES, userId)}:${roleId}`;
|
||||||
|
const jsonValue = await AsyncStorage.getItem(key);
|
||||||
|
const result = jsonValue != null ? JSON.parse(jsonValue) : [];
|
||||||
|
console.log(`[Storage] Loaded current chat for user ${userId}, role ${roleId}:`, result.length, 'messages');
|
||||||
|
return result;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Error getting current chat for role ${roleId}:`, e);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all stored chat data for a specific user
|
||||||
|
*/
|
||||||
|
async clearUserData(userId: string | number): Promise<void> {
|
||||||
|
try {
|
||||||
|
const keys = await AsyncStorage.getAllKeys();
|
||||||
|
const userPrefix = `:user_${userId}`;
|
||||||
|
const userKeys = keys.filter(key => key.includes(userPrefix));
|
||||||
|
await AsyncStorage.multiRemove(userKeys);
|
||||||
|
console.log(`[Storage] Cleared all data for user ${userId}:`, userKeys.length, 'keys removed');
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error clearing user data:', e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all stored chat data (all users)
|
||||||
|
*/
|
||||||
|
async clearAllData(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const keys = await AsyncStorage.getAllKeys();
|
||||||
|
const sentinelKeys = keys.filter(key => key.startsWith('@sentinel:'));
|
||||||
|
await AsyncStorage.multiRemove(sentinelKeys);
|
||||||
|
console.log('[Storage] Cleared all data:', sentinelKeys.length, 'keys removed');
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error clearing storage data:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
81
src/services/vault.service.ts
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
/**
|
||||||
|
* Vault Service: 为 /assets/create 生成 private_key_shard 与 content_inner_encrypted
|
||||||
|
*
|
||||||
|
* 流程(与后端 test_scenario / SentinelVault 一致):
|
||||||
|
* 1. 用 SSS 生成助记词并分片 → 选一个分片作为 private_key_shard(存后端,继承时返回)
|
||||||
|
* 2. 用助记词派生 AES 密钥,对明文做 AES-GCM 加密 → content_inner_encrypted(hex 字符串)
|
||||||
|
*
|
||||||
|
* 使用方式:在任意页面调用 createVaultPayload(plaintext, wordList),得到可直接传给 assetsService.createAsset 的字段。
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
generateVaultKeys,
|
||||||
|
serializeShare,
|
||||||
|
type SSSShare,
|
||||||
|
type VaultKeyData,
|
||||||
|
} from '../utils/sss';
|
||||||
|
import { deriveKey, encryptDataGCM, bytesToHex } from '../utils/vaultCrypto';
|
||||||
|
|
||||||
|
export interface CreateVaultPayloadResult {
|
||||||
|
/** 传给后端的 private_key_shard(存一个 SSS 分片的序列化字符串,如云端分片) */
|
||||||
|
private_key_shard: string;
|
||||||
|
/** 传给后端的 content_inner_encrypted(AES-GCM 密文的 hex) */
|
||||||
|
content_inner_encrypted: string;
|
||||||
|
/** 本次生成的助记词(用户需妥善保管,恢复时需任意 2 个分片) */
|
||||||
|
mnemonic: string[];
|
||||||
|
/** 三个分片:device / cloud / heir,可与后端返回的 server_shard 组合恢复助记词 */
|
||||||
|
shares: SSSShare[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateAssetPayloadResult {
|
||||||
|
title: string;
|
||||||
|
type: string;
|
||||||
|
private_key_shard: string;
|
||||||
|
content_inner_encrypted: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成金库:助记词 + SSS 分片 + 内层加密内容
|
||||||
|
* @param plaintext 要加密的明文(如遗产说明、账号密码等)
|
||||||
|
* @param wordList 助记词词表(与 sss 使用的词表一致)
|
||||||
|
* @param shareIndexForServer 哪个分片存后端,0=device, 1=cloud, 2=heir,默认 1(云端)
|
||||||
|
*/
|
||||||
|
export async function createVaultPayload(
|
||||||
|
plaintext: string,
|
||||||
|
wordList: readonly string[],
|
||||||
|
shareIndexForServer: 0 | 1 | 2 = 1
|
||||||
|
): Promise<CreateVaultPayloadResult> {
|
||||||
|
const { mnemonic, shares }: VaultKeyData = generateVaultKeys(wordList, 12);
|
||||||
|
const mnemonicPhrase = mnemonic.join(' ');
|
||||||
|
const key = await deriveKey(mnemonicPhrase);
|
||||||
|
const encrypted = await encryptDataGCM(key, plaintext);
|
||||||
|
const content_inner_encrypted = bytesToHex(encrypted);
|
||||||
|
const shareForServer = shares[shareIndexForServer];
|
||||||
|
const private_key_shard = serializeShare(shareForServer);
|
||||||
|
|
||||||
|
return {
|
||||||
|
private_key_shard,
|
||||||
|
content_inner_encrypted,
|
||||||
|
mnemonic,
|
||||||
|
shares,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成可直接用于 POST /assets/create 的请求体(含 title / type)
|
||||||
|
*/
|
||||||
|
export async function createAssetPayload(
|
||||||
|
title: string,
|
||||||
|
plaintext: string,
|
||||||
|
wordList: readonly string[],
|
||||||
|
assetType: string = 'note',
|
||||||
|
shareIndexForServer: 0 | 1 | 2 = 1
|
||||||
|
): Promise<CreateAssetPayloadResult> {
|
||||||
|
const vault = await createVaultPayload(plaintext, wordList, shareIndexForServer);
|
||||||
|
return {
|
||||||
|
title,
|
||||||
|
type: assetType,
|
||||||
|
private_key_shard: vault.private_key_shard,
|
||||||
|
content_inner_encrypted: vault.content_inner_encrypted,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -13,7 +13,7 @@ export interface FlowRecord {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Vault Types
|
// Vault Types
|
||||||
export type VaultAssetType =
|
export type VaultAssetType =
|
||||||
| 'game_account'
|
| 'game_account'
|
||||||
| 'private_key'
|
| 'private_key'
|
||||||
| 'document'
|
| 'document'
|
||||||
@@ -72,3 +72,33 @@ export interface ProtocolInfo {
|
|||||||
version: string;
|
version: string;
|
||||||
lastUpdated: Date;
|
lastUpdated: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Auth Types
|
||||||
|
export interface User {
|
||||||
|
id: number;
|
||||||
|
username: string;
|
||||||
|
email?: 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;
|
||||||
|
}
|
||||||
|
|||||||
6
src/utils/index.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* Utility functions for Sentinel
|
||||||
|
*/
|
||||||
|
|
||||||
|
export * from './sss';
|
||||||
|
export * from './vaultAssets';
|
||||||
268
src/utils/sss.ts
Normal file
@@ -0,0 +1,268 @@
|
|||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*
|
||||||
|
* Correspondence with crypto_core_demo (Python):
|
||||||
|
* - sp_trust_sharding.py: split_to_shares(), recover_from_shares()
|
||||||
|
* - Same algorithm: f(x) = secret + a*x (mod P), Lagrange interpolation
|
||||||
|
* - Difference: entropy conversion. Python uses BIP-39 (mnemonic.to_entropy);
|
||||||
|
* we use custom word list index-based encoding for compatibility with
|
||||||
|
* existing MNEMONIC_WORDS. SSS split/recover logic is identical.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
100
src/utils/vaultAssets.ts
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
/**
|
||||||
|
* Vault assets: API ↔ UI mapping and initial mock data.
|
||||||
|
* Used by useVaultAssets and VaultScreen for /assets/get and /assets/create flows.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { VaultAsset, VaultAssetType } from '../types';
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// Types
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/** Shape returned by GET /assets/get (backend AssetOut) */
|
||||||
|
export interface ApiAsset {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
type?: string;
|
||||||
|
created_at?: string;
|
||||||
|
updated_at?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// Constants
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export const VAULT_ASSET_TYPES: VaultAssetType[] = [
|
||||||
|
'game_account',
|
||||||
|
'private_key',
|
||||||
|
'document',
|
||||||
|
'photo',
|
||||||
|
'will',
|
||||||
|
'custom',
|
||||||
|
];
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// Mapping
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map backend API asset to VaultAsset for UI.
|
||||||
|
*/
|
||||||
|
export function mapApiAssetToVaultAsset(api: ApiAsset): VaultAsset {
|
||||||
|
const type: VaultAssetType =
|
||||||
|
api.type && VAULT_ASSET_TYPES.includes(api.type as VaultAssetType)
|
||||||
|
? (api.type as VaultAssetType)
|
||||||
|
: 'custom';
|
||||||
|
return {
|
||||||
|
id: String(api.id),
|
||||||
|
type,
|
||||||
|
label: api.title,
|
||||||
|
createdAt: api.created_at ? new Date(api.created_at) : new Date(),
|
||||||
|
updatedAt: api.updated_at ? new Date(api.updated_at) : new Date(),
|
||||||
|
isEncrypted: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map array of API assets to VaultAsset[].
|
||||||
|
*/
|
||||||
|
export function mapApiAssetsToVaultAssets(apiList: ApiAsset[]): VaultAsset[] {
|
||||||
|
return apiList.map(mapApiAssetToVaultAsset);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// Mock / initial data (fallback when API is unavailable)
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export const initialVaultAssets: VaultAsset[] = [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
type: 'private_key',
|
||||||
|
label: 'ETH Main Wallet Key',
|
||||||
|
createdAt: new Date('2024-01-10'),
|
||||||
|
updatedAt: new Date('2024-01-10'),
|
||||||
|
isEncrypted: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
type: 'game_account',
|
||||||
|
label: 'Steam Account Credentials',
|
||||||
|
createdAt: new Date('2024-01-08'),
|
||||||
|
updatedAt: new Date('2024-01-08'),
|
||||||
|
isEncrypted: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
type: 'document',
|
||||||
|
label: 'Insurance Policy Scan',
|
||||||
|
createdAt: new Date('2024-01-05'),
|
||||||
|
updatedAt: new Date('2024-01-05'),
|
||||||
|
isEncrypted: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '4',
|
||||||
|
type: 'will',
|
||||||
|
label: 'Testament Draft v2',
|
||||||
|
createdAt: new Date('2024-01-02'),
|
||||||
|
updatedAt: new Date('2024-01-15'),
|
||||||
|
isEncrypted: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
107
src/utils/vaultCrypto.ts
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
/**
|
||||||
|
* Vault crypto: PBKDF2 key derivation + AES-256-GCM encrypt/decrypt.
|
||||||
|
* Matches backend SentinelVault semantics (PBKDF2 from mnemonic, AES-GCM).
|
||||||
|
* Uses Web Crypto API (crypto.subtle). Requires secure context / React Native polyfill if needed.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const SALT = new TextEncoder().encode('Sentinel_Salt_2026');
|
||||||
|
const PBKDF2_ITERATIONS = 100000;
|
||||||
|
const AES_KEY_LEN = 256;
|
||||||
|
const GCM_IV_LEN = 16;
|
||||||
|
const GCM_TAG_LEN = 16;
|
||||||
|
|
||||||
|
function getCrypto(): Crypto {
|
||||||
|
if (typeof crypto !== 'undefined' && crypto.subtle) return crypto;
|
||||||
|
throw new Error('vaultCrypto: crypto.subtle not available');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Derive a 32-byte AES key from mnemonic phrase (space-separated words).
|
||||||
|
*/
|
||||||
|
export async function deriveKey(mnemonicPhrase: string, salt: Uint8Array = SALT): Promise<ArrayBuffer> {
|
||||||
|
const crypto = getCrypto();
|
||||||
|
const keyMaterial = await crypto.subtle.importKey(
|
||||||
|
'raw',
|
||||||
|
new TextEncoder().encode(mnemonicPhrase),
|
||||||
|
'PBKDF2',
|
||||||
|
false,
|
||||||
|
['deriveBits']
|
||||||
|
);
|
||||||
|
const saltBuf = salt.buffer.slice(salt.byteOffset, salt.byteOffset + salt.byteLength) as ArrayBuffer;
|
||||||
|
const bits = await crypto.subtle.deriveBits(
|
||||||
|
{
|
||||||
|
name: 'PBKDF2',
|
||||||
|
salt: saltBuf,
|
||||||
|
iterations: PBKDF2_ITERATIONS,
|
||||||
|
hash: 'SHA-256',
|
||||||
|
},
|
||||||
|
keyMaterial,
|
||||||
|
AES_KEY_LEN
|
||||||
|
);
|
||||||
|
return bits;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encrypt plaintext with AES-256-GCM. Returns nonce(16) + tag(16) + ciphertext (matches Python SentinelVault).
|
||||||
|
*/
|
||||||
|
export async function encryptDataGCM(key: ArrayBuffer, plaintext: string): Promise<Uint8Array> {
|
||||||
|
const crypto = getCrypto();
|
||||||
|
const iv = crypto.getRandomValues(new Uint8Array(GCM_IV_LEN));
|
||||||
|
const cryptoKey = await crypto.subtle.importKey(
|
||||||
|
'raw',
|
||||||
|
key,
|
||||||
|
{ name: 'AES-GCM' },
|
||||||
|
false,
|
||||||
|
['encrypt']
|
||||||
|
);
|
||||||
|
const encoded = new TextEncoder().encode(plaintext);
|
||||||
|
const ciphertextWithTag = await crypto.subtle.encrypt(
|
||||||
|
{ name: 'AES-GCM', iv, tagLength: GCM_TAG_LEN * 8 },
|
||||||
|
cryptoKey,
|
||||||
|
encoded
|
||||||
|
);
|
||||||
|
const out = new Uint8Array(iv.length + ciphertextWithTag.byteLength);
|
||||||
|
out.set(iv, 0);
|
||||||
|
out.set(new Uint8Array(ciphertextWithTag), iv.length);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrypt blob from encryptDataGCM (nonce(16) + ciphertext+tag).
|
||||||
|
*/
|
||||||
|
export async function decryptDataGCM(key: ArrayBuffer, blob: Uint8Array): Promise<string> {
|
||||||
|
const crypto = getCrypto();
|
||||||
|
const iv = blob.subarray(0, GCM_IV_LEN);
|
||||||
|
const ciphertextWithTag = blob.subarray(GCM_IV_LEN);
|
||||||
|
const ivBuf = iv.buffer.slice(iv.byteOffset, iv.byteOffset + iv.byteLength) as ArrayBuffer;
|
||||||
|
const ctBuf = ciphertextWithTag.buffer.slice(
|
||||||
|
ciphertextWithTag.byteOffset,
|
||||||
|
ciphertextWithTag.byteOffset + ciphertextWithTag.byteLength
|
||||||
|
) as ArrayBuffer;
|
||||||
|
const cryptoKey = await crypto.subtle.importKey(
|
||||||
|
'raw',
|
||||||
|
key,
|
||||||
|
{ name: 'AES-GCM' },
|
||||||
|
false,
|
||||||
|
['decrypt']
|
||||||
|
);
|
||||||
|
const dec = await crypto.subtle.decrypt(
|
||||||
|
{ name: 'AES-GCM', iv: ivBuf, tagLength: GCM_TAG_LEN * 8 },
|
||||||
|
cryptoKey,
|
||||||
|
ctBuf
|
||||||
|
);
|
||||||
|
return new TextDecoder().decode(dec);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function bytesToHex(bytes: Uint8Array): string {
|
||||||
|
return Array.from(bytes)
|
||||||
|
.map((b) => b.toString(16).padStart(2, '0'))
|
||||||
|
.join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hexToBytes(hex: string): Uint8Array {
|
||||||
|
const len = hex.length / 2;
|
||||||
|
const out = new Uint8Array(len);
|
||||||
|
for (let i = 0; i < len; i++) out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
||||||
|
return out;
|
||||||
|
}
|
||||||