The vault notification mode1
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
@@ -8,47 +8,13 @@ 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';
|
||||
|
||||
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', 'lagoon', 'storm', 'north', 'south', 'east',
|
||||
'west', 'ember', 'cabin', 'signal', 'ledger', 'torch', 'sanctum', 'oath',
|
||||
];
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
const splitMnemonic = (words: string[]) => [
|
||||
words.slice(0, 4),
|
||||
words.slice(4, 8),
|
||||
words.slice(8, 12),
|
||||
];
|
||||
|
||||
// Status configuration with nautical theme
|
||||
const statusConfig: Record<SystemStatus, {
|
||||
color: string;
|
||||
@@ -113,13 +79,6 @@ 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 [mnemonicParts, setMnemonicParts] = useState<string[][]>([]);
|
||||
const [showEmailForm, setShowEmailForm] = useState(false);
|
||||
const [emailAddress, setEmailAddress] = useState('');
|
||||
const [isCapturing, setIsCapturing] = useState(false);
|
||||
const mnemonicRef = useRef<View>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// Pulse animation
|
||||
@@ -164,65 +123,8 @@ export default function SentinelScreen() {
|
||||
).start();
|
||||
}, []);
|
||||
|
||||
const openVaultWithMnemonic = () => {
|
||||
const words = generateMnemonic();
|
||||
const parts = splitMnemonic(words);
|
||||
setMnemonicWords(words);
|
||||
setMnemonicParts(parts);
|
||||
setShowMnemonic(true);
|
||||
setShowVault(false);
|
||||
setShowEmailForm(false);
|
||||
setEmailAddress('');
|
||||
AsyncStorage.setItem('sentinel_mnemonic_part_local', parts[0].join(' ')).catch(() => {
|
||||
// Best-effort local store; UI remains available
|
||||
});
|
||||
};
|
||||
|
||||
const handleScreenshot = async () => {
|
||||
try {
|
||||
setIsCapturing(true);
|
||||
const uri = await captureRef(mnemonicRef, {
|
||||
format: 'png',
|
||||
quality: 1,
|
||||
result: 'tmpfile',
|
||||
});
|
||||
await Share.share({
|
||||
url: uri,
|
||||
message: 'Sentinel key backup',
|
||||
});
|
||||
setShowMnemonic(false);
|
||||
setShowVault(true);
|
||||
} catch (error) {
|
||||
Alert.alert('Screenshot failed', 'Please try again or use email backup.');
|
||||
} finally {
|
||||
setIsCapturing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEmailBackup = () => {
|
||||
setShowEmailForm(true);
|
||||
};
|
||||
|
||||
const handleSendEmail = async () => {
|
||||
const trimmed = emailAddress.trim();
|
||||
if (!trimmed || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(trimmed)) {
|
||||
Alert.alert('Invalid email', 'Please enter a valid email address.');
|
||||
return;
|
||||
}
|
||||
|
||||
const subject = encodeURIComponent('Sentinel Vault Recovery Key');
|
||||
const body = encodeURIComponent(`Your 12-word mnemonic:\n${mnemonicWords.join(' ')}`);
|
||||
const mailtoUrl = `mailto:${trimmed}?subject=${subject}&body=${body}`;
|
||||
|
||||
try {
|
||||
await Linking.openURL(mailtoUrl);
|
||||
setShowMnemonic(false);
|
||||
setShowEmailForm(false);
|
||||
setEmailAddress('');
|
||||
setShowVault(true);
|
||||
} catch (error) {
|
||||
Alert.alert('Email failed', 'Unable to open email client.');
|
||||
}
|
||||
const openVault = () => {
|
||||
setShowVault(true);
|
||||
};
|
||||
|
||||
const handleHeartbeat = () => {
|
||||
@@ -388,7 +290,7 @@ export default function SentinelScreen() {
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
style={styles.vaultAccessButton}
|
||||
onPress={openVaultWithMnemonic}
|
||||
onPress={openVault}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<Text style={styles.vaultAccessButtonText}>Open</Text>
|
||||
@@ -457,99 +359,6 @@ export default function SentinelScreen() {
|
||||
</View>
|
||||
</Modal>
|
||||
|
||||
{/* Mnemonic Modal */}
|
||||
<Modal
|
||||
visible={showMnemonic}
|
||||
animationType="fade"
|
||||
transparent
|
||||
onRequestClose={() => setShowMnemonic(false)}
|
||||
>
|
||||
<KeyboardAvoidingView
|
||||
style={styles.mnemonicOverlay}
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
||||
>
|
||||
<LinearGradient
|
||||
colors={[colors.sentinel.cardBackground, colors.sentinel.backgroundGradientEnd]}
|
||||
style={styles.mnemonicCard}
|
||||
ref={mnemonicRef}
|
||||
>
|
||||
<TouchableOpacity
|
||||
style={styles.mnemonicClose}
|
||||
onPress={() => setShowMnemonic(false)}
|
||||
activeOpacity={0.85}
|
||||
>
|
||||
<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 mnemonic is split into 3 parts (4/4/4). Part 1 is stored locally.
|
||||
</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}>PART 1 • LOCAL</Text>
|
||||
<Text style={styles.partValue}>{mnemonicParts[0]?.join(' ')}</Text>
|
||||
<Text style={styles.partHint}>Stored on this device</Text>
|
||||
</View>
|
||||
<View style={styles.partCard}>
|
||||
<Text style={styles.partLabel}>PART 2 • CLOUD NODE</Text>
|
||||
<Text style={styles.partValue}>{mnemonicParts[1]?.join(' ')}</Text>
|
||||
<Text style={styles.partHint}>To be synced</Text>
|
||||
</View>
|
||||
<View style={styles.partCard}>
|
||||
<Text style={styles.partLabel}>PART 3 • HEIR</Text>
|
||||
<Text style={styles.partValue}>{mnemonicParts[2]?.join(' ')}</Text>
|
||||
<Text style={styles.partHint}>To be assigned</Text>
|
||||
</View>
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
style={[styles.mnemonicPrimaryButton, isCapturing && styles.mnemonicButtonDisabled]}
|
||||
onPress={handleScreenshot}
|
||||
activeOpacity={0.85}
|
||||
disabled={isCapturing}
|
||||
>
|
||||
<Text style={styles.mnemonicPrimaryText}>
|
||||
{isCapturing ? 'CAPTURING...' : 'PHYSICAL BACKUP (SCREENSHOT)'}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={styles.mnemonicSecondaryButton}
|
||||
onPress={handleEmailBackup}
|
||||
activeOpacity={0.85}
|
||||
>
|
||||
<Text style={styles.mnemonicSecondaryText}>EMAIL BACKUP</Text>
|
||||
</TouchableOpacity>
|
||||
{showEmailForm ? (
|
||||
<View style={styles.emailForm}>
|
||||
<TextInput
|
||||
style={styles.emailInput}
|
||||
value={emailAddress}
|
||||
onChangeText={setEmailAddress}
|
||||
placeholder="you@email.com"
|
||||
placeholderTextColor={colors.sentinel.textSecondary}
|
||||
keyboardType="email-address"
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
/>
|
||||
<TouchableOpacity
|
||||
style={styles.emailSendButton}
|
||||
onPress={handleSendEmail}
|
||||
activeOpacity={0.85}
|
||||
>
|
||||
<Text style={styles.emailSendText}>SEND EMAIL</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
) : null}
|
||||
</LinearGradient>
|
||||
</KeyboardAvoidingView>
|
||||
</Modal>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -810,148 +619,4 @@ const styles = StyleSheet.create({
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
mnemonicOverlay: {
|
||||
flex: 1,
|
||||
backgroundColor: 'rgba(11, 20, 24, 0.72)',
|
||||
justifyContent: 'center',
|
||||
padding: spacing.lg,
|
||||
},
|
||||
mnemonicCard: {
|
||||
borderRadius: borderRadius.xl,
|
||||
padding: spacing.lg,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.sentinel.cardBorder,
|
||||
...shadows.glow,
|
||||
},
|
||||
mnemonicHeader: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: spacing.sm,
|
||||
marginBottom: spacing.sm,
|
||||
},
|
||||
mnemonicClose: {
|
||||
position: 'absolute',
|
||||
top: spacing.sm,
|
||||
right: spacing.sm,
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 16,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: 'rgba(26, 58, 74, 0.35)',
|
||||
},
|
||||
mnemonicTitle: {
|
||||
fontSize: typography.fontSize.lg,
|
||||
fontWeight: '700',
|
||||
color: colors.sentinel.text,
|
||||
letterSpacing: typography.letterSpacing.wide,
|
||||
},
|
||||
mnemonicSubtitle: {
|
||||
fontSize: typography.fontSize.sm,
|
||||
color: colors.sentinel.textSecondary,
|
||||
marginBottom: spacing.md,
|
||||
},
|
||||
mnemonicBlock: {
|
||||
backgroundColor: colors.sentinel.cardBackground,
|
||||
borderRadius: borderRadius.lg,
|
||||
paddingVertical: spacing.md,
|
||||
paddingHorizontal: spacing.md,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.sentinel.cardBorder,
|
||||
marginBottom: spacing.lg,
|
||||
},
|
||||
partGrid: {
|
||||
gap: spacing.sm,
|
||||
marginBottom: spacing.lg,
|
||||
},
|
||||
partCard: {
|
||||
backgroundColor: colors.sentinel.cardBackground,
|
||||
borderRadius: borderRadius.lg,
|
||||
paddingVertical: spacing.sm,
|
||||
paddingHorizontal: spacing.md,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.sentinel.cardBorder,
|
||||
},
|
||||
partCardStored: {
|
||||
borderColor: colors.sentinel.primary,
|
||||
},
|
||||
partLabel: {
|
||||
fontSize: typography.fontSize.xs,
|
||||
color: colors.sentinel.textSecondary,
|
||||
letterSpacing: typography.letterSpacing.wide,
|
||||
marginBottom: 4,
|
||||
fontWeight: '600',
|
||||
},
|
||||
partValue: {
|
||||
fontSize: typography.fontSize.md,
|
||||
color: colors.sentinel.text,
|
||||
fontFamily: typography.fontFamily.mono,
|
||||
fontWeight: '700',
|
||||
marginBottom: 2,
|
||||
},
|
||||
partHint: {
|
||||
fontSize: typography.fontSize.xs,
|
||||
color: colors.sentinel.textSecondary,
|
||||
},
|
||||
mnemonicBlockText: {
|
||||
fontSize: typography.fontSize.sm,
|
||||
color: colors.sentinel.text,
|
||||
fontFamily: typography.fontFamily.mono,
|
||||
fontWeight: '600',
|
||||
lineHeight: 22,
|
||||
textAlign: 'center',
|
||||
},
|
||||
mnemonicPrimaryButton: {
|
||||
backgroundColor: colors.sentinel.primary,
|
||||
paddingVertical: spacing.sm,
|
||||
borderRadius: borderRadius.full,
|
||||
alignItems: 'center',
|
||||
marginBottom: spacing.sm,
|
||||
},
|
||||
mnemonicButtonDisabled: {
|
||||
opacity: 0.6,
|
||||
},
|
||||
mnemonicPrimaryText: {
|
||||
color: colors.nautical.cream,
|
||||
fontWeight: '700',
|
||||
letterSpacing: typography.letterSpacing.wide,
|
||||
},
|
||||
mnemonicSecondaryButton: {
|
||||
backgroundColor: 'transparent',
|
||||
paddingVertical: spacing.sm,
|
||||
borderRadius: borderRadius.full,
|
||||
alignItems: 'center',
|
||||
borderWidth: 1,
|
||||
borderColor: colors.sentinel.cardBorder,
|
||||
},
|
||||
mnemonicSecondaryText: {
|
||||
color: colors.sentinel.text,
|
||||
fontWeight: '700',
|
||||
letterSpacing: typography.letterSpacing.wide,
|
||||
},
|
||||
emailForm: {
|
||||
marginTop: spacing.sm,
|
||||
},
|
||||
emailInput: {
|
||||
height: 44,
|
||||
borderRadius: borderRadius.full,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.sentinel.cardBorder,
|
||||
paddingHorizontal: spacing.md,
|
||||
color: colors.sentinel.text,
|
||||
fontSize: typography.fontSize.sm,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.02)',
|
||||
marginBottom: spacing.sm,
|
||||
},
|
||||
emailSendButton: {
|
||||
backgroundColor: colors.nautical.teal,
|
||||
paddingVertical: spacing.sm,
|
||||
borderRadius: borderRadius.full,
|
||||
alignItems: 'center',
|
||||
},
|
||||
emailSendText: {
|
||||
color: colors.nautical.cream,
|
||||
fontWeight: '700',
|
||||
letterSpacing: typography.letterSpacing.wide,
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user