Merge branch 'Steven'
This commit is contained in:
5
App.tsx
5
App.tsx
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Buffer } from 'buffer';
|
||||
import { StatusBar } from 'expo-status-bar';
|
||||
import { NavigationContainer } from '@react-navigation/native';
|
||||
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
||||
@@ -15,6 +16,10 @@ 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
|
||||
*/
|
||||
|
||||
55
package-lock.json
generated
55
package-lock.json
generated
@@ -14,6 +14,8 @@
|
||||
"@react-navigation/bottom-tabs": "^6.6.1",
|
||||
"@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-asset": "~11.0.5",
|
||||
"expo-constants": "~17.0.8",
|
||||
@@ -3209,6 +3211,18 @@
|
||||
"@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": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
@@ -4447,6 +4461,15 @@
|
||||
"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": {
|
||||
"version": "0.0.7",
|
||||
"resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.0.7.tgz",
|
||||
@@ -4533,9 +4556,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/buffer": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
|
||||
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
||||
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -4553,7 +4576,7 @@
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.1",
|
||||
"ieee754": "^1.1.13"
|
||||
"ieee754": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer-alloc": {
|
||||
@@ -11057,6 +11080,30 @@
|
||||
"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": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz",
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
"@react-navigation/bottom-tabs": "^6.6.1",
|
||||
"@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-asset": "~11.0.5",
|
||||
"expo-constants": "~17.0.8",
|
||||
|
||||
@@ -112,6 +112,7 @@ export const MOCK_CONFIG = {
|
||||
USER: {
|
||||
id: 999,
|
||||
username: 'MockCaptain',
|
||||
email: 'captain@sentinel.local',
|
||||
public_key: 'mock_public_key',
|
||||
is_admin: true,
|
||||
guale: false,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
@@ -8,47 +8,12 @@ import {
|
||||
SafeAreaView,
|
||||
Animated,
|
||||
Modal,
|
||||
TextInput,
|
||||
KeyboardAvoidingView,
|
||||
Platform,
|
||||
Share,
|
||||
Alert,
|
||||
Linking,
|
||||
} from 'react-native';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import { Ionicons, Feather, MaterialCommunityIcons, FontAwesome5 } from '@expo/vector-icons';
|
||||
import { captureRef } from 'react-native-view-shot';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import { colors, typography, spacing, borderRadius, shadows } from '../theme/colors';
|
||||
import { SystemStatus, KillSwitchLog } from '../types';
|
||||
import VaultScreen from './VaultScreen';
|
||||
import BiometricModal from '../components/common/BiometricModal';
|
||||
import {
|
||||
SSSShare,
|
||||
mnemonicToEntropy,
|
||||
splitSecret,
|
||||
formatShareCompact,
|
||||
serializeShare,
|
||||
verifyShares,
|
||||
} from '../utils/sss';
|
||||
|
||||
// Vault storage keys (for testing: clear these to simulate first-open)
|
||||
export const VAULT_STORAGE_KEYS = {
|
||||
INITIALIZED: 'sentinel_vault_initialized',
|
||||
SHARE_DEVICE: 'sentinel_share_device',
|
||||
} as const;
|
||||
|
||||
// Nautical-themed mnemonic word list (unique words only)
|
||||
const MNEMONIC_WORDS = [
|
||||
'anchor', 'harbor', 'compass', 'lighthouse', 'current', 'ocean', 'tide', 'voyage',
|
||||
'keel', 'stern', 'bow', 'mast', 'sail', 'port', 'starboard', 'reef',
|
||||
'signal', 'beacon', 'chart', 'helm', 'gale', 'calm', 'cove', 'isle',
|
||||
'horizon', 'sextant', 'sound', 'drift', 'wake', 'mariner', 'pilot', 'fathom',
|
||||
'buoy', 'lantern', 'harpoon', 'lagoon', 'bay', 'strait', 'riptide', 'foam',
|
||||
'coral', 'pearl', 'trident', 'ebb', 'flow', 'vault', 'cipher', 'shroud',
|
||||
'salt', 'wave', 'grotto', 'storm', 'north', 'south', 'east', 'west',
|
||||
'ember', 'cabin', 'ledger', 'torch', 'sanctum', 'oath', 'depths', 'captain',
|
||||
] as const;
|
||||
|
||||
// Animation timing constants
|
||||
const ANIMATION_DURATION = {
|
||||
@@ -58,56 +23,13 @@ const ANIMATION_DURATION = {
|
||||
heartbeatPress: 150,
|
||||
} as const;
|
||||
|
||||
const generateMnemonic = (wordCount = 12) => {
|
||||
const words: string[] = [];
|
||||
for (let i = 0; i < wordCount; i += 1) {
|
||||
const index = Math.floor(Math.random() * MNEMONIC_WORDS.length);
|
||||
words.push(MNEMONIC_WORDS[index]);
|
||||
}
|
||||
return words;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate SSS shares from mnemonic words
|
||||
* Uses Shamir's Secret Sharing (3,2) threshold scheme
|
||||
*/
|
||||
const generateSSSShares = (words: string[]): SSSShare[] => {
|
||||
try {
|
||||
// Convert mnemonic to entropy (big integer)
|
||||
const entropy = mnemonicToEntropy(words, MNEMONIC_WORDS);
|
||||
|
||||
// Split entropy into 3 shares using SSS
|
||||
const shares = splitSecret(entropy);
|
||||
|
||||
// Verify shares can recover the original (optional, for debugging)
|
||||
if (__DEV__) {
|
||||
const isValid = verifyShares(shares, entropy);
|
||||
if (!isValid) {
|
||||
console.warn('SSS verification failed!');
|
||||
} else {
|
||||
console.log('SSS shares verified successfully');
|
||||
}
|
||||
}
|
||||
|
||||
return shares;
|
||||
} catch (error) {
|
||||
console.error('Failed to generate SSS shares:', error);
|
||||
// Fallback: return empty shares (should not happen in production)
|
||||
return [
|
||||
{ x: 1, y: BigInt(0), label: 'device' },
|
||||
{ x: 2, y: BigInt(0), label: 'cloud' },
|
||||
{ x: 3, y: BigInt(0), label: 'heir' },
|
||||
];
|
||||
}
|
||||
};
|
||||
|
||||
// Icon names type for type safety
|
||||
type StatusIconName = 'checkmark-circle' | 'warning' | 'alert-circle';
|
||||
|
||||
// Status configuration with nautical theme
|
||||
const statusConfig: Record<SystemStatus, {
|
||||
color: string;
|
||||
label: string;
|
||||
const statusConfig: Record<SystemStatus, {
|
||||
color: string;
|
||||
label: string;
|
||||
icon: StatusIconName;
|
||||
description: string;
|
||||
gradientColors: [string, string];
|
||||
@@ -137,26 +59,10 @@ const statusConfig: Record<SystemStatus, {
|
||||
|
||||
// Mock data
|
||||
const initialLogs: KillSwitchLog[] = [
|
||||
{
|
||||
id: '1',
|
||||
action: 'HEARTBEAT_CONFIRMED',
|
||||
timestamp: new Date('2024-01-18T09:30:00'),
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
action: 'SUBSCRIPTION_VERIFIED',
|
||||
timestamp: new Date('2024-01-17T00:00:00'),
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
action: 'JOURNAL_ACTIVITY',
|
||||
timestamp: new Date('2024-01-16T15:42:00'),
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
action: 'HEARTBEAT_CONFIRMED',
|
||||
timestamp: new Date('2024-01-15T11:20:00'),
|
||||
},
|
||||
{ id: '1', action: 'HEARTBEAT_CONFIRMED', timestamp: new Date('2024-01-18T09:30:00') },
|
||||
{ id: '2', action: 'SUBSCRIPTION_VERIFIED', timestamp: new Date('2024-01-17T00:00:00') },
|
||||
{ id: '3', action: 'JOURNAL_ACTIVITY', timestamp: new Date('2024-01-16T15:42:00') },
|
||||
{ id: '4', action: 'HEARTBEAT_CONFIRMED', timestamp: new Date('2024-01-15T11:20:00') },
|
||||
];
|
||||
|
||||
export default function SentinelScreen() {
|
||||
@@ -168,26 +74,8 @@ export default function SentinelScreen() {
|
||||
const [glowAnim] = useState(new Animated.Value(0.5));
|
||||
const [rotateAnim] = useState(new Animated.Value(0));
|
||||
const [showVault, setShowVault] = useState(false);
|
||||
const [showMnemonic, setShowMnemonic] = useState(false);
|
||||
const [mnemonicWords, setMnemonicWords] = useState<string[]>([]);
|
||||
const [sssShares, setSssShares] = useState<SSSShare[]>([]);
|
||||
const [showEmailForm, setShowEmailForm] = useState(false);
|
||||
const [emailAddress, setEmailAddress] = useState('');
|
||||
const [emailRecipientType, setEmailRecipientType] = useState<'self' | 'heir'>('self');
|
||||
const [isCapturing, setIsCapturing] = useState(false);
|
||||
const [hasVaultInitialized, setHasVaultInitialized] = useState<boolean | null>(null);
|
||||
const [showSetupBiometric, setShowSetupBiometric] = useState(false);
|
||||
const mnemonicRef = useRef<View>(null);
|
||||
|
||||
// Load vault init status on mount (1.1)
|
||||
useEffect(() => {
|
||||
AsyncStorage.getItem(VAULT_STORAGE_KEYS.INITIALIZED)
|
||||
.then((val) => setHasVaultInitialized(val === 'true'))
|
||||
.catch(() => setHasVaultInitialized(false));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// Pulse animation
|
||||
const pulseAnimation = Animated.loop(
|
||||
Animated.sequence([
|
||||
Animated.timing(pulseAnim, {
|
||||
@@ -204,7 +92,6 @@ export default function SentinelScreen() {
|
||||
);
|
||||
pulseAnimation.start();
|
||||
|
||||
// Glow animation
|
||||
const glowAnimation = Animated.loop(
|
||||
Animated.sequence([
|
||||
Animated.timing(glowAnim, {
|
||||
@@ -221,7 +108,6 @@ export default function SentinelScreen() {
|
||||
);
|
||||
glowAnimation.start();
|
||||
|
||||
// Slow rotate for ship wheel
|
||||
const rotateAnimation = Animated.loop(
|
||||
Animated.timing(rotateAnim, {
|
||||
toValue: 1,
|
||||
@@ -231,7 +117,6 @@ export default function SentinelScreen() {
|
||||
);
|
||||
rotateAnimation.start();
|
||||
|
||||
// Cleanup animations on unmount to prevent memory leaks
|
||||
return () => {
|
||||
pulseAnimation.stop();
|
||||
glowAnimation.stop();
|
||||
@@ -239,109 +124,9 @@ export default function SentinelScreen() {
|
||||
};
|
||||
}, [pulseAnim, glowAnim, rotateAnim]);
|
||||
|
||||
const startFirstTimeSetup = () => {
|
||||
const words = generateMnemonic();
|
||||
const shares = generateSSSShares(words);
|
||||
setMnemonicWords(words);
|
||||
setSssShares(shares);
|
||||
setShowMnemonic(true);
|
||||
setShowVault(false);
|
||||
setShowEmailForm(false);
|
||||
setEmailAddress('');
|
||||
setEmailRecipientType('self');
|
||||
|
||||
if (shares[0]) {
|
||||
AsyncStorage.setItem(VAULT_STORAGE_KEYS.SHARE_DEVICE, serializeShare(shares[0])).catch(() => {});
|
||||
}
|
||||
};
|
||||
|
||||
const completeSetupAndEnterVault = () => {
|
||||
setShowMnemonic(false);
|
||||
setShowEmailForm(false);
|
||||
setEmailAddress('');
|
||||
setShowSetupBiometric(true);
|
||||
};
|
||||
|
||||
const handleSetupBiometricSuccess = () => {
|
||||
setShowSetupBiometric(false);
|
||||
AsyncStorage.setItem(VAULT_STORAGE_KEYS.INITIALIZED, 'true').catch(() => {});
|
||||
setHasVaultInitialized(true);
|
||||
setShowVault(true);
|
||||
};
|
||||
|
||||
const handleSetupBiometricSkip = () => {
|
||||
setShowSetupBiometric(false);
|
||||
AsyncStorage.setItem(VAULT_STORAGE_KEYS.INITIALIZED, 'true').catch(() => {});
|
||||
setHasVaultInitialized(true);
|
||||
setShowVault(true);
|
||||
};
|
||||
|
||||
const handleOpenVault = () => {
|
||||
if (hasVaultInitialized === true) {
|
||||
setShowVault(true);
|
||||
setShowMnemonic(false);
|
||||
} else {
|
||||
startFirstTimeSetup();
|
||||
}
|
||||
};
|
||||
|
||||
const handleScreenshot = async () => {
|
||||
try {
|
||||
setIsCapturing(true);
|
||||
const uri = await captureRef(mnemonicRef, {
|
||||
format: 'png',
|
||||
quality: 1,
|
||||
result: 'tmpfile',
|
||||
});
|
||||
await Share.share({
|
||||
url: uri,
|
||||
message: 'Sentinel key backup',
|
||||
});
|
||||
completeSetupAndEnterVault();
|
||||
} catch (error) {
|
||||
Alert.alert('Screenshot failed', 'Please try again or use email backup.');
|
||||
} finally {
|
||||
setIsCapturing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEmailBackup = () => {
|
||||
setShowEmailForm(true);
|
||||
};
|
||||
|
||||
const handleCompleteBackupLocal = () => {
|
||||
completeSetupAndEnterVault();
|
||||
};
|
||||
|
||||
const handleSendEmail = async () => {
|
||||
const trimmed = emailAddress.trim();
|
||||
if (!trimmed || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(trimmed)) {
|
||||
Alert.alert('Invalid email', 'Please enter a valid email address.');
|
||||
return;
|
||||
}
|
||||
|
||||
const subject = encodeURIComponent(
|
||||
emailRecipientType === 'heir'
|
||||
? 'Sentinel Vault - Share C (Heir)'
|
||||
: 'Sentinel Vault Recovery Key'
|
||||
);
|
||||
const body = encodeURIComponent(
|
||||
emailRecipientType === 'heir'
|
||||
? `Share C (for heir, 2-of-3 required to recover):\n${sssShares[2] ? serializeShare(sssShares[2]) : ''}\n\nKeep this secure. Combined with Share B from Sentinel cloud, this can restore the vault.`
|
||||
: `Your 12-word mnemonic (backup for yourself):\n${mnemonicWords.join(' ')}`
|
||||
);
|
||||
const mailtoUrl = `mailto:${trimmed}?subject=${subject}&body=${body}`;
|
||||
|
||||
try {
|
||||
await Linking.openURL(mailtoUrl);
|
||||
completeSetupAndEnterVault();
|
||||
} catch (error) {
|
||||
Alert.alert('Email failed', 'Unable to open email client.');
|
||||
}
|
||||
};
|
||||
const openVault = () => setShowVault(true);
|
||||
|
||||
const handleHeartbeat = () => {
|
||||
// Animate pulse
|
||||
Animated.sequence([
|
||||
Animated.timing(pulseAnim, {
|
||||
toValue: 1.15,
|
||||
@@ -355,43 +140,34 @@ export default function SentinelScreen() {
|
||||
}),
|
||||
]).start();
|
||||
|
||||
// Add new log using functional update to avoid stale closure
|
||||
const newLog: KillSwitchLog = {
|
||||
id: Date.now().toString(),
|
||||
action: 'HEARTBEAT_CONFIRMED',
|
||||
timestamp: new Date(),
|
||||
};
|
||||
setLogs((prevLogs) => [newLog, ...prevLogs]);
|
||||
|
||||
// Reset status if warning
|
||||
|
||||
if (status === 'warning') {
|
||||
setStatus('normal');
|
||||
}
|
||||
};
|
||||
|
||||
const formatDateTime = (date: Date) => {
|
||||
return date.toLocaleString('en-US', {
|
||||
const formatDateTime = (date: Date) =>
|
||||
date.toLocaleString('en-US', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
};
|
||||
|
||||
const formatTimeAgo = (date: Date) => {
|
||||
const now = new Date();
|
||||
const diff = now.getTime() - date.getTime();
|
||||
const hours = Math.floor(diff / (1000 * 60 * 60));
|
||||
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
|
||||
|
||||
if (hours > 24) {
|
||||
const days = Math.floor(hours / 24);
|
||||
return `${days} days ago`;
|
||||
}
|
||||
if (hours > 0) {
|
||||
return `${hours}h ${minutes}m ago`;
|
||||
}
|
||||
if (hours > 24) return `${Math.floor(hours / 24)} days ago`;
|
||||
if (hours > 0) return `${hours}h ${minutes}m ago`;
|
||||
return `${minutes}m ago`;
|
||||
};
|
||||
|
||||
@@ -408,7 +184,7 @@ export default function SentinelScreen() {
|
||||
style={styles.gradient}
|
||||
>
|
||||
<SafeAreaView style={styles.safeArea}>
|
||||
<ScrollView
|
||||
<ScrollView
|
||||
style={styles.scrollView}
|
||||
showsVerticalScrollIndicator={false}
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
@@ -424,14 +200,14 @@ export default function SentinelScreen() {
|
||||
|
||||
{/* Status Display */}
|
||||
<View style={styles.statusContainer}>
|
||||
<Animated.View
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.statusCircleOuter,
|
||||
{
|
||||
{
|
||||
transform: [{ scale: pulseAnim }],
|
||||
opacity: glowAnim,
|
||||
backgroundColor: `${currentStatus.color}20`,
|
||||
}
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Animated.View style={{ transform: [{ scale: pulseAnim }] }}>
|
||||
@@ -453,10 +229,10 @@ export default function SentinelScreen() {
|
||||
{/* Ship Wheel Watermark */}
|
||||
<View style={styles.wheelWatermark}>
|
||||
<Animated.View style={{ transform: [{ rotate: spin }] }}>
|
||||
<MaterialCommunityIcons
|
||||
name="ship-wheel"
|
||||
size={200}
|
||||
color={colors.sentinel.primary}
|
||||
<MaterialCommunityIcons
|
||||
name="ship-wheel"
|
||||
size={200}
|
||||
color={colors.sentinel.primary}
|
||||
style={{ opacity: 0.03 }}
|
||||
/>
|
||||
</Animated.View>
|
||||
@@ -469,24 +245,16 @@ export default function SentinelScreen() {
|
||||
<FontAwesome5 name="anchor" size={16} color={colors.sentinel.primary} />
|
||||
</View>
|
||||
<Text style={styles.metricLabel}>SUBSCRIPTION</Text>
|
||||
<Text style={styles.metricValue}>
|
||||
{formatTimeAgo(lastSubscriptionCheck)}
|
||||
</Text>
|
||||
<Text style={styles.metricTime}>
|
||||
{formatDateTime(lastSubscriptionCheck)}
|
||||
</Text>
|
||||
<Text style={styles.metricValue}>{formatTimeAgo(lastSubscriptionCheck)}</Text>
|
||||
<Text style={styles.metricTime}>{formatDateTime(lastSubscriptionCheck)}</Text>
|
||||
</View>
|
||||
<View style={styles.metricCard}>
|
||||
<View style={styles.metricIconContainer}>
|
||||
<Feather name="edit-3" size={16} color={colors.sentinel.primary} />
|
||||
</View>
|
||||
<Text style={styles.metricLabel}>LAST JOURNAL</Text>
|
||||
<Text style={styles.metricValue}>
|
||||
{formatTimeAgo(lastFlowActivity)}
|
||||
</Text>
|
||||
<Text style={styles.metricTime}>
|
||||
{formatDateTime(lastFlowActivity)}
|
||||
</Text>
|
||||
<Text style={styles.metricValue}>{formatTimeAgo(lastFlowActivity)}</Text>
|
||||
<Text style={styles.metricTime}>{formatDateTime(lastFlowActivity)}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -503,7 +271,7 @@ export default function SentinelScreen() {
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
style={styles.vaultAccessButton}
|
||||
onPress={handleOpenVault}
|
||||
onPress={openVault}
|
||||
activeOpacity={0.8}
|
||||
accessibilityLabel="Open Shadow Vault"
|
||||
accessibilityRole="button"
|
||||
@@ -547,9 +315,7 @@ export default function SentinelScreen() {
|
||||
<View style={styles.logDot} />
|
||||
<View style={styles.logContent}>
|
||||
<Text style={styles.logAction}>{log.action}</Text>
|
||||
<Text style={styles.logTime}>
|
||||
{formatDateTime(log.timestamp)}
|
||||
</Text>
|
||||
<Text style={styles.logTime}>{formatDateTime(log.timestamp)}</Text>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
@@ -565,7 +331,7 @@ export default function SentinelScreen() {
|
||||
onRequestClose={() => setShowVault(false)}
|
||||
>
|
||||
<View style={styles.vaultModalContainer}>
|
||||
<VaultScreen />
|
||||
{showVault ? <VaultScreen /> : null}
|
||||
<TouchableOpacity
|
||||
style={styles.vaultCloseButton}
|
||||
onPress={() => setShowVault(false)}
|
||||
@@ -577,184 +343,20 @@ export default function SentinelScreen() {
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</Modal>
|
||||
|
||||
{/* Mnemonic Modal */}
|
||||
<Modal
|
||||
visible={showMnemonic}
|
||||
animationType="fade"
|
||||
transparent
|
||||
onRequestClose={() => setShowMnemonic(false)}
|
||||
>
|
||||
<KeyboardAvoidingView
|
||||
style={styles.mnemonicOverlay}
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
||||
>
|
||||
<ScrollView
|
||||
style={styles.mnemonicScroll}
|
||||
contentContainerStyle={styles.mnemonicScrollContent}
|
||||
showsVerticalScrollIndicator={false}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
>
|
||||
<View ref={mnemonicRef} collapsable={false}>
|
||||
<LinearGradient
|
||||
colors={[colors.sentinel.cardBackground, colors.sentinel.backgroundGradientEnd]}
|
||||
style={styles.mnemonicCard}
|
||||
>
|
||||
<TouchableOpacity
|
||||
style={styles.mnemonicClose}
|
||||
onPress={() => setShowMnemonic(false)}
|
||||
activeOpacity={0.85}
|
||||
accessibilityLabel="Close mnemonic modal"
|
||||
accessibilityRole="button"
|
||||
>
|
||||
<Ionicons name="close" size={18} color={colors.sentinel.textSecondary} />
|
||||
</TouchableOpacity>
|
||||
<View style={styles.mnemonicHeader}>
|
||||
<MaterialCommunityIcons name="key-variant" size={22} color={colors.sentinel.primary} />
|
||||
<Text style={styles.mnemonicTitle}>12-Word Mnemonic</Text>
|
||||
</View>
|
||||
<Text style={styles.mnemonicSubtitle}>
|
||||
Your seed is protected by SSS (3,2) threshold encryption. Any 2 shares can restore your vault.
|
||||
</Text>
|
||||
<View style={styles.mnemonicBlock}>
|
||||
<Text style={styles.mnemonicBlockText}>
|
||||
{mnemonicWords.join(' ')}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.partGrid}>
|
||||
<View style={[styles.partCard, styles.partCardStored]}>
|
||||
<Text style={styles.partLabel}>SHARE A • DEVICE</Text>
|
||||
<Text style={styles.partValue}>
|
||||
{sssShares[0] ? formatShareCompact(sssShares[0]) : '---'}
|
||||
</Text>
|
||||
<Text style={styles.partHint}>Stored on this device</Text>
|
||||
</View>
|
||||
<View style={styles.partCard}>
|
||||
<Text style={styles.partLabel}>SHARE B • CLOUD</Text>
|
||||
<Text style={styles.partValue}>
|
||||
{sssShares[1] ? formatShareCompact(sssShares[1]) : '---'}
|
||||
</Text>
|
||||
<Text style={styles.partHint}>To be synced to Sentinel</Text>
|
||||
</View>
|
||||
<View style={styles.partCard}>
|
||||
<Text style={styles.partLabel}>SHARE C • HEIR</Text>
|
||||
<Text style={styles.partValue}>
|
||||
{sssShares[2] ? formatShareCompact(sssShares[2]) : '---'}
|
||||
</Text>
|
||||
<Text style={styles.partHint}>For your heir (2-of-3 required)</Text>
|
||||
</View>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
style={[styles.mnemonicPrimaryButton, isCapturing && styles.mnemonicButtonDisabled]}
|
||||
onPress={handleScreenshot}
|
||||
activeOpacity={0.85}
|
||||
disabled={isCapturing}
|
||||
accessibilityLabel="Take screenshot backup of mnemonic"
|
||||
accessibilityRole="button"
|
||||
accessibilityState={{ disabled: isCapturing }}
|
||||
>
|
||||
<Text style={styles.mnemonicPrimaryText}>
|
||||
{isCapturing ? 'CAPTURING...' : 'PHYSICAL BACKUP (SCREENSHOT)'}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={styles.mnemonicSecondaryButton}
|
||||
onPress={handleEmailBackup}
|
||||
activeOpacity={0.85}
|
||||
accessibilityLabel="Send backup via email"
|
||||
accessibilityRole="button"
|
||||
>
|
||||
<Text style={styles.mnemonicSecondaryText}>EMAIL BACKUP</Text>
|
||||
</TouchableOpacity>
|
||||
{showEmailForm ? (
|
||||
<View style={styles.emailForm}>
|
||||
<View style={styles.emailTypeRow}>
|
||||
<TouchableOpacity
|
||||
style={[styles.emailTypeButton, emailRecipientType === 'self' && styles.emailTypeButtonActive]}
|
||||
onPress={() => setEmailRecipientType('self')}
|
||||
>
|
||||
<Text style={[styles.emailTypeText, emailRecipientType === 'self' && styles.emailTypeTextActive]}>
|
||||
To Myself
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={[styles.emailTypeButton, emailRecipientType === 'heir' && styles.emailTypeButtonActive]}
|
||||
onPress={() => setEmailRecipientType('heir')}
|
||||
>
|
||||
<Text style={[styles.emailTypeText, emailRecipientType === 'heir' && styles.emailTypeTextActive]}>
|
||||
To Heir
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<TextInput
|
||||
style={styles.emailInput}
|
||||
value={emailAddress}
|
||||
onChangeText={setEmailAddress}
|
||||
placeholder={emailRecipientType === 'heir' ? 'heir@email.com' : 'you@email.com'}
|
||||
placeholderTextColor={colors.sentinel.textSecondary}
|
||||
keyboardType="email-address"
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
/>
|
||||
<TouchableOpacity
|
||||
style={styles.emailSendButton}
|
||||
onPress={handleSendEmail}
|
||||
activeOpacity={0.85}
|
||||
accessibilityLabel="Send backup email"
|
||||
accessibilityRole="button"
|
||||
>
|
||||
<Text style={styles.emailSendText}>SEND EMAIL</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
) : null}
|
||||
<TouchableOpacity
|
||||
style={styles.mnemonicTertiaryButton}
|
||||
onPress={handleCompleteBackupLocal}
|
||||
activeOpacity={0.85}
|
||||
accessibilityLabel="I've completed backup locally"
|
||||
accessibilityRole="button"
|
||||
>
|
||||
<Text style={styles.mnemonicTertiaryText}>COMPLETE BACKUP (LOCAL)</Text>
|
||||
</TouchableOpacity>
|
||||
</LinearGradient>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</KeyboardAvoidingView>
|
||||
</Modal>
|
||||
|
||||
{/* Biometric setup prompt after first-time backup (1.4) */}
|
||||
<BiometricModal
|
||||
visible={showSetupBiometric}
|
||||
onSuccess={handleSetupBiometricSuccess}
|
||||
onCancel={handleSetupBiometricSkip}
|
||||
title="Quick Unlock"
|
||||
message="Enable Face ID / Touch ID for faster access next time?"
|
||||
isDark
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
gradient: {
|
||||
flex: 1,
|
||||
},
|
||||
safeArea: {
|
||||
flex: 1,
|
||||
},
|
||||
scrollView: {
|
||||
flex: 1,
|
||||
},
|
||||
container: { flex: 1 },
|
||||
gradient: { flex: 1 },
|
||||
safeArea: { flex: 1 },
|
||||
scrollView: { flex: 1 },
|
||||
scrollContent: {
|
||||
padding: spacing.lg,
|
||||
paddingBottom: 120,
|
||||
},
|
||||
header: {
|
||||
marginBottom: spacing.xl,
|
||||
},
|
||||
header: { marginBottom: spacing.xl },
|
||||
headerTitleRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
@@ -860,9 +462,7 @@ const styles = StyleSheet.create({
|
||||
marginBottom: spacing.xl,
|
||||
...shadows.medium,
|
||||
},
|
||||
heartbeatGradient: {
|
||||
padding: spacing.lg,
|
||||
},
|
||||
heartbeatGradient: { padding: spacing.lg },
|
||||
heartbeatContent: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
@@ -916,9 +516,7 @@ const styles = StyleSheet.create({
|
||||
marginTop: 6,
|
||||
marginRight: spacing.md,
|
||||
},
|
||||
logContent: {
|
||||
flex: 1,
|
||||
},
|
||||
logContent: { flex: 1 },
|
||||
logAction: {
|
||||
fontSize: typography.fontSize.sm,
|
||||
color: colors.sentinel.text,
|
||||
@@ -931,7 +529,6 @@ const styles = StyleSheet.create({
|
||||
color: colors.sentinel.textSecondary,
|
||||
fontFamily: typography.fontFamily.mono,
|
||||
},
|
||||
// Shadow Vault Access Card
|
||||
vaultAccessCard: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
@@ -951,9 +548,7 @@ const styles = StyleSheet.create({
|
||||
justifyContent: 'center',
|
||||
marginRight: spacing.md,
|
||||
},
|
||||
vaultAccessContent: {
|
||||
flex: 1,
|
||||
},
|
||||
vaultAccessContent: { flex: 1 },
|
||||
vaultAccessTitle: {
|
||||
fontSize: typography.fontSize.base,
|
||||
fontWeight: '600',
|
||||
@@ -975,7 +570,6 @@ const styles = StyleSheet.create({
|
||||
fontWeight: '700',
|
||||
fontSize: typography.fontSize.sm,
|
||||
},
|
||||
// Vault Modal
|
||||
vaultModalContainer: {
|
||||
flex: 1,
|
||||
backgroundColor: colors.vault.background,
|
||||
@@ -991,196 +585,4 @@ const styles = StyleSheet.create({
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
mnemonicOverlay: {
|
||||
flex: 1,
|
||||
backgroundColor: 'rgba(11, 20, 24, 0.72)',
|
||||
justifyContent: 'center',
|
||||
padding: spacing.lg,
|
||||
},
|
||||
mnemonicScroll: {
|
||||
flex: 1,
|
||||
},
|
||||
mnemonicScrollContent: {
|
||||
flexGrow: 1,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
mnemonicCard: {
|
||||
borderRadius: borderRadius.xl,
|
||||
padding: spacing.lg,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.sentinel.cardBorder,
|
||||
...shadows.glow,
|
||||
},
|
||||
mnemonicHeader: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: spacing.sm,
|
||||
marginBottom: spacing.sm,
|
||||
},
|
||||
mnemonicClose: {
|
||||
position: 'absolute',
|
||||
top: spacing.sm,
|
||||
right: spacing.sm,
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 16,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: 'rgba(26, 58, 74, 0.35)',
|
||||
},
|
||||
mnemonicTitle: {
|
||||
fontSize: typography.fontSize.lg,
|
||||
fontWeight: '700',
|
||||
color: colors.sentinel.text,
|
||||
letterSpacing: typography.letterSpacing.wide,
|
||||
},
|
||||
mnemonicSubtitle: {
|
||||
fontSize: typography.fontSize.sm,
|
||||
color: colors.sentinel.textSecondary,
|
||||
marginBottom: spacing.md,
|
||||
},
|
||||
mnemonicBlock: {
|
||||
backgroundColor: colors.sentinel.cardBackground,
|
||||
borderRadius: borderRadius.lg,
|
||||
paddingVertical: spacing.md,
|
||||
paddingHorizontal: spacing.md,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.sentinel.cardBorder,
|
||||
marginBottom: spacing.lg,
|
||||
},
|
||||
partGrid: {
|
||||
gap: spacing.sm,
|
||||
marginBottom: spacing.lg,
|
||||
},
|
||||
partCard: {
|
||||
backgroundColor: colors.sentinel.cardBackground,
|
||||
borderRadius: borderRadius.lg,
|
||||
paddingVertical: spacing.sm,
|
||||
paddingHorizontal: spacing.md,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.sentinel.cardBorder,
|
||||
},
|
||||
partCardStored: {
|
||||
borderColor: colors.sentinel.primary,
|
||||
},
|
||||
partLabel: {
|
||||
fontSize: typography.fontSize.xs,
|
||||
color: colors.sentinel.textSecondary,
|
||||
letterSpacing: typography.letterSpacing.wide,
|
||||
marginBottom: 4,
|
||||
fontWeight: '600',
|
||||
},
|
||||
partValue: {
|
||||
fontSize: typography.fontSize.md,
|
||||
color: colors.sentinel.text,
|
||||
fontFamily: typography.fontFamily.mono,
|
||||
fontWeight: '700',
|
||||
marginBottom: 2,
|
||||
},
|
||||
partHint: {
|
||||
fontSize: typography.fontSize.xs,
|
||||
color: colors.sentinel.textSecondary,
|
||||
},
|
||||
mnemonicBlockText: {
|
||||
fontSize: typography.fontSize.sm,
|
||||
color: colors.sentinel.text,
|
||||
fontFamily: typography.fontFamily.mono,
|
||||
fontWeight: '600',
|
||||
lineHeight: 22,
|
||||
textAlign: 'center',
|
||||
},
|
||||
mnemonicPrimaryButton: {
|
||||
backgroundColor: colors.sentinel.primary,
|
||||
paddingVertical: spacing.sm,
|
||||
borderRadius: borderRadius.full,
|
||||
alignItems: 'center',
|
||||
marginBottom: spacing.sm,
|
||||
},
|
||||
mnemonicButtonDisabled: {
|
||||
opacity: 0.6,
|
||||
},
|
||||
mnemonicPrimaryText: {
|
||||
color: colors.nautical.cream,
|
||||
fontWeight: '700',
|
||||
letterSpacing: typography.letterSpacing.wide,
|
||||
},
|
||||
mnemonicSecondaryButton: {
|
||||
backgroundColor: 'transparent',
|
||||
paddingVertical: spacing.sm,
|
||||
borderRadius: borderRadius.full,
|
||||
alignItems: 'center',
|
||||
borderWidth: 1,
|
||||
borderColor: colors.sentinel.cardBorder,
|
||||
},
|
||||
mnemonicSecondaryText: {
|
||||
color: colors.sentinel.text,
|
||||
fontWeight: '700',
|
||||
letterSpacing: typography.letterSpacing.wide,
|
||||
},
|
||||
mnemonicTertiaryButton: {
|
||||
backgroundColor: 'transparent',
|
||||
paddingVertical: spacing.sm,
|
||||
borderRadius: borderRadius.full,
|
||||
alignItems: 'center',
|
||||
marginTop: spacing.sm,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.sentinel.cardBorder,
|
||||
borderStyle: 'dashed',
|
||||
},
|
||||
mnemonicTertiaryText: {
|
||||
color: colors.sentinel.textSecondary,
|
||||
fontWeight: '600',
|
||||
letterSpacing: typography.letterSpacing.wide,
|
||||
fontSize: typography.fontSize.sm,
|
||||
},
|
||||
emailTypeRow: {
|
||||
flexDirection: 'row',
|
||||
gap: spacing.sm,
|
||||
marginBottom: spacing.sm,
|
||||
},
|
||||
emailTypeButton: {
|
||||
flex: 1,
|
||||
paddingVertical: spacing.sm,
|
||||
borderRadius: borderRadius.lg,
|
||||
alignItems: 'center',
|
||||
borderWidth: 1,
|
||||
borderColor: colors.sentinel.cardBorder,
|
||||
},
|
||||
emailTypeButtonActive: {
|
||||
borderColor: colors.sentinel.primary,
|
||||
backgroundColor: `${colors.sentinel.primary}15`,
|
||||
},
|
||||
emailTypeText: {
|
||||
fontSize: typography.fontSize.sm,
|
||||
color: colors.sentinel.textSecondary,
|
||||
fontWeight: '600',
|
||||
},
|
||||
emailTypeTextActive: {
|
||||
color: colors.sentinel.primary,
|
||||
},
|
||||
emailForm: {
|
||||
marginTop: spacing.sm,
|
||||
},
|
||||
emailInput: {
|
||||
height: 44,
|
||||
borderRadius: borderRadius.full,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.sentinel.cardBorder,
|
||||
paddingHorizontal: spacing.md,
|
||||
color: colors.sentinel.text,
|
||||
fontSize: typography.fontSize.sm,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.02)',
|
||||
marginBottom: spacing.sm,
|
||||
},
|
||||
emailSendButton: {
|
||||
backgroundColor: colors.nautical.teal,
|
||||
paddingVertical: spacing.sm,
|
||||
borderRadius: borderRadius.full,
|
||||
alignItems: 'center',
|
||||
},
|
||||
emailSendText: {
|
||||
color: colors.nautical.cream,
|
||||
fontWeight: '700',
|
||||
letterSpacing: typography.letterSpacing.wide,
|
||||
},
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -77,6 +77,7 @@ export interface ProtocolInfo {
|
||||
export interface User {
|
||||
id: number;
|
||||
username: string;
|
||||
email?: string;
|
||||
public_key: string;
|
||||
is_admin: boolean;
|
||||
guale: boolean;
|
||||
|
||||
Reference in New Issue
Block a user