The vault notification mode1

This commit is contained in:
2026-01-30 22:42:20 -04:00
parent 749ed2f05a
commit 56bb72aab8
2 changed files with 900 additions and 341 deletions

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect, useRef } from 'react'; import React, { useState, useEffect } from 'react';
import { import {
View, View,
Text, Text,
@@ -8,47 +8,13 @@ import {
SafeAreaView, SafeAreaView,
Animated, Animated,
Modal, Modal,
TextInput,
KeyboardAvoidingView,
Platform,
Share,
Alert,
Linking,
} from 'react-native'; } from 'react-native';
import { LinearGradient } from 'expo-linear-gradient'; import { LinearGradient } from 'expo-linear-gradient';
import { Ionicons, Feather, MaterialCommunityIcons, FontAwesome5 } from '@expo/vector-icons'; import { Ionicons, Feather, MaterialCommunityIcons, FontAwesome5 } from '@expo/vector-icons';
import { captureRef } from 'react-native-view-shot';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { colors, typography, spacing, borderRadius, shadows } from '../theme/colors'; import { colors, typography, spacing, borderRadius, shadows } from '../theme/colors';
import { SystemStatus, KillSwitchLog } from '../types'; import { SystemStatus, KillSwitchLog } from '../types';
import VaultScreen from './VaultScreen'; import 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 // Status configuration with nautical theme
const statusConfig: Record<SystemStatus, { const statusConfig: Record<SystemStatus, {
color: string; color: string;
@@ -113,13 +79,6 @@ export default function SentinelScreen() {
const [glowAnim] = useState(new Animated.Value(0.5)); const [glowAnim] = useState(new Animated.Value(0.5));
const [rotateAnim] = useState(new Animated.Value(0)); const [rotateAnim] = useState(new Animated.Value(0));
const [showVault, setShowVault] = useState(false); const [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(() => { useEffect(() => {
// Pulse animation // Pulse animation
@@ -164,65 +123,8 @@ export default function SentinelScreen() {
).start(); ).start();
}, []); }, []);
const openVaultWithMnemonic = () => { const openVault = () => {
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); setShowVault(true);
} catch (error) {
Alert.alert('Screenshot failed', 'Please try again or use email backup.');
} finally {
setIsCapturing(false);
}
};
const handleEmailBackup = () => {
setShowEmailForm(true);
};
const handleSendEmail = async () => {
const trimmed = emailAddress.trim();
if (!trimmed || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(trimmed)) {
Alert.alert('Invalid email', 'Please enter a valid email address.');
return;
}
const subject = encodeURIComponent('Sentinel Vault Recovery Key');
const body = encodeURIComponent(`Your 12-word mnemonic:\n${mnemonicWords.join(' ')}`);
const mailtoUrl = `mailto:${trimmed}?subject=${subject}&body=${body}`;
try {
await Linking.openURL(mailtoUrl);
setShowMnemonic(false);
setShowEmailForm(false);
setEmailAddress('');
setShowVault(true);
} catch (error) {
Alert.alert('Email failed', 'Unable to open email client.');
}
}; };
const handleHeartbeat = () => { const handleHeartbeat = () => {
@@ -388,7 +290,7 @@ export default function SentinelScreen() {
</View> </View>
<TouchableOpacity <TouchableOpacity
style={styles.vaultAccessButton} style={styles.vaultAccessButton}
onPress={openVaultWithMnemonic} onPress={openVault}
activeOpacity={0.8} activeOpacity={0.8}
> >
<Text style={styles.vaultAccessButtonText}>Open</Text> <Text style={styles.vaultAccessButtonText}>Open</Text>
@@ -457,99 +359,6 @@ export default function SentinelScreen() {
</View> </View>
</Modal> </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> </View>
); );
} }
@@ -810,148 +619,4 @@ const styles = StyleSheet.create({
alignItems: 'center', alignItems: 'center',
justifyContent: '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,
},
}); });

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect, useRef } from 'react';
import { import {
View, View,
Text, Text,
@@ -7,15 +7,20 @@ import {
TouchableOpacity, TouchableOpacity,
Modal, Modal,
TextInput, TextInput,
KeyboardAvoidingView,
Platform,
SafeAreaView, SafeAreaView,
Animated, Animated,
Linking, Linking,
Alert, Alert,
Share,
} from 'react-native'; } from 'react-native';
import { LinearGradient } from 'expo-linear-gradient'; import { LinearGradient } from 'expo-linear-gradient';
import { Ionicons, Feather, MaterialCommunityIcons, FontAwesome5 } from '@expo/vector-icons'; import { Ionicons, Feather, MaterialCommunityIcons, FontAwesome5 } from '@expo/vector-icons';
import { captureRef } from 'react-native-view-shot';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { colors, typography, spacing, borderRadius, shadows } from '../theme/colors'; import { colors, typography, spacing, borderRadius, shadows } from '../theme/colors';
import { VaultAsset, VaultAssetType } from '../types'; import { VaultAsset, VaultAssetType, Heir } from '../types';
import BiometricModal from '../components/common/BiometricModal'; import BiometricModal from '../components/common/BiometricModal';
// Asset type configuration with nautical theme // Asset type configuration with nautical theme
@@ -35,6 +40,79 @@ const accountProviderOptions = [
{ key: 'custom', label: 'Other', icon: 'shield-account', iconType: 'material' as const }, { key: 'custom', label: 'Other', icon: 'shield-account', iconType: 'material' as const },
]; ];
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 initialHeirs: Heir[] = [
{
id: '1',
name: 'John Smith',
email: 'john.smith@email.com',
phone: '+1 415 555 0132',
status: 'confirmed',
releaseLevel: 3,
releaseOrder: 1,
paymentStrategy: 'prepaid',
},
{
id: '2',
name: 'Jane Doe',
email: 'jane.doe@email.com',
phone: '+1 212 555 0184',
status: 'confirmed',
releaseLevel: 2,
releaseOrder: 2,
paymentStrategy: 'pay_on_access',
},
{
id: '3',
name: 'Alice Johnson',
email: 'alice.j@email.com',
phone: '+1 646 555 0149',
status: 'invited',
releaseLevel: 1,
releaseOrder: 3,
paymentStrategy: 'pay_on_access',
},
];
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),
];
type HeirAssignment = {
asset: VaultAsset;
heir: Heir;
};
const shuffle = <T,>(items: T[]) => {
const next = [...items];
for (let i = next.length - 1; i > 0; i -= 1) {
const j = Math.floor(Math.random() * (i + 1));
[next[i], next[j]] = [next[j], next[i]];
}
return next;
};
// Mock data // Mock data
const initialAssets: VaultAsset[] = [ const initialAssets: VaultAsset[] = [
{ {
@@ -104,6 +182,23 @@ export default function VaultScreen() {
const [rehearsalConfirmed, setRehearsalConfirmed] = useState(false); const [rehearsalConfirmed, setRehearsalConfirmed] = useState(false);
const [showAddBiometric, setShowAddBiometric] = useState(false); const [showAddBiometric, setShowAddBiometric] = useState(false);
const [accountProvider, setAccountProvider] = useState<'bank' | 'steam' | 'facebook' | 'custom'>('bank'); const [accountProvider, setAccountProvider] = useState<'bank' | 'steam' | 'facebook' | 'custom'>('bank');
const [showMnemonic, setShowMnemonic] = useState(false);
const [mnemonicWords, setMnemonicWords] = useState<string[]>([]);
const [mnemonicParts, setMnemonicParts] = useState<string[][]>([]);
const [mnemonicStep, setMnemonicStep] = useState<1 | 2 | 3 | 4 | 5>(1);
const [heirStep, setHeirStep] = useState<'decision' | 'asset' | 'heir' | 'summary'>('decision');
const [selectedHeir, setSelectedHeir] = useState<Heir | null>(null);
const [selectedHeirAsset, setSelectedHeirAsset] = useState<VaultAsset | null>(null);
const [assignments, setAssignments] = useState<HeirAssignment[]>([]);
const [wordOptions, setWordOptions] = useState<string[]>([]);
const [selectedWords, setSelectedWords] = useState<string[]>([]);
const [progressIndex, setProgressIndex] = useState(0);
const [progressAnim] = useState(new Animated.Value(0));
const [showEmailForm, setShowEmailForm] = useState(false);
const [emailAddress, setEmailAddress] = useState('');
const [isCapturing, setIsCapturing] = useState(false);
const mnemonicRef = useRef<View>(null);
const progressTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
useEffect(() => { useEffect(() => {
if (!isUnlocked) { if (!isUnlocked) {
@@ -145,9 +240,186 @@ export default function VaultScreen() {
const handleUnlock = () => { const handleUnlock = () => {
setShowBiometric(false); setShowBiometric(false);
const words = generateMnemonic();
const parts = splitMnemonic(words);
const decoys = MNEMONIC_WORDS.filter((word) => !words.includes(word));
const options = shuffle([...words, ...decoys.slice(0, 12)]);
setMnemonicWords(words);
setMnemonicParts(parts);
setWordOptions(options);
setSelectedWords([]);
setMnemonicStep(1);
setHeirStep('decision');
setSelectedHeir(null);
setSelectedHeirAsset(null);
setAssignments([]);
setProgressIndex(0);
progressAnim.setValue(0);
setShowMnemonic(true);
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',
});
setMnemonicStep(4);
} catch (error) {
Alert.alert('Screenshot failed', 'Please try again or use email backup.');
} finally {
setIsCapturing(false);
}
};
const handleEmailBackup = () => {
setShowEmailForm(true);
};
const toggleWord = (word: string) => {
if (selectedWords.includes(word)) {
setSelectedWords(selectedWords.filter((item) => item !== word));
return;
}
if (selectedWords.length >= 12) return;
setSelectedWords([...selectedWords, word]);
};
const handleConfirmSelection = () => {
if (selectedWords.length !== 12) {
Alert.alert('Select 12 words', 'Please select all 12 words in order.');
return;
}
setMnemonicStep(2);
};
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);
setShowEmailForm(false);
setEmailAddress('');
setMnemonicStep(4);
} catch (error) {
Alert.alert('Email failed', 'Unable to open email client.');
}
};
useEffect(() => {
if (mnemonicStep !== 4) {
if (progressTimerRef.current) {
clearInterval(progressTimerRef.current);
progressTimerRef.current = null;
}
return;
}
const messagesCount = 5;
let current = 0;
setProgressIndex(current);
progressAnim.setValue(0);
progressTimerRef.current = setInterval(() => {
current += 1;
if (current >= messagesCount) {
if (progressTimerRef.current) {
clearInterval(progressTimerRef.current);
progressTimerRef.current = null;
}
setMnemonicStep(5);
return;
}
setProgressIndex(current);
Animated.timing(progressAnim, {
toValue: current / (messagesCount - 1),
duration: 700,
useNativeDriver: false,
}).start();
}, 1100);
Animated.timing(progressAnim, {
toValue: 1 / (messagesCount - 1),
duration: 700,
useNativeDriver: false,
}).start();
return () => {
if (progressTimerRef.current) {
clearInterval(progressTimerRef.current);
progressTimerRef.current = null;
}
};
}, [mnemonicStep, progressAnim]);
const handleHeirDecision = (share: boolean) => {
// Placeholder for future heir flow
setShowMnemonic(false);
setIsUnlocked(true); setIsUnlocked(true);
}; };
const handleSubmitAssignment = () => {
// Placeholder for submitting assignment to API
setShowMnemonic(false);
setIsUnlocked(true);
};
const handleHeirYes = () => {
setHeirStep('asset');
};
const handleHeirNo = () => {
handleHeirDecision(false);
};
const handleSelectHeirAsset = (asset: VaultAsset) => {
setSelectedHeirAsset(asset);
setHeirStep('heir');
};
const handleSelectHeir = (heir: Heir) => {
setSelectedHeir(heir);
if (selectedHeirAsset) {
setAssignments((prev) => {
const remaining = prev.filter((item) => item.asset.id !== selectedHeirAsset.id);
return [...remaining, { asset: selectedHeirAsset, heir }];
});
}
setSelectedHeir(null);
setSelectedHeirAsset(null);
setHeirStep('asset');
};
const getAssignmentForAsset = (assetId: string) =>
assignments.find((item) => item.asset.id === assetId);
const handleReviewAssignments = () => {
if (assignments.length === 0) {
Alert.alert('No assignments', 'Please assign at least one asset.');
return;
}
setHeirStep('summary');
};
const resetAddFlow = () => { const resetAddFlow = () => {
setAddStep(1); setAddStep(1);
setAddMethod('text'); setAddMethod('text');
@@ -324,6 +596,334 @@ export default function VaultScreen() {
message="Verify your identity to access your treasures" message="Verify your identity to access your treasures"
isDark isDark
/> />
<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}>Mnemonic Setup</Text>
</View>
{mnemonicStep === 1 ? (
<>
<Text style={styles.mnemonicSubtitle}>
Select the 12 words in order.
</Text>
<View style={styles.selectedRow}>
<Text style={styles.selectedLabel}>Selected {selectedWords.length}/12</Text>
<Text style={styles.selectedValue}>{selectedWords.join(' ')}</Text>
</View>
<View style={styles.wordGrid}>
{wordOptions.map((word) => {
const isSelected = selectedWords.includes(word);
return (
<TouchableOpacity
key={word}
style={[styles.wordChip, isSelected && styles.wordChipSelected]}
onPress={() => toggleWord(word)}
activeOpacity={0.8}
>
<Text style={[styles.wordChipText, isSelected && styles.wordChipTextSelected]}>
{word}
</Text>
</TouchableOpacity>
);
})}
</View>
<TouchableOpacity
style={[
styles.mnemonicPrimaryButton,
selectedWords.length !== 12 && styles.mnemonicButtonDisabled,
]}
onPress={handleConfirmSelection}
activeOpacity={0.85}
disabled={selectedWords.length !== 12}
>
<Text style={styles.mnemonicPrimaryText}>CONFIRM SELECTION</Text>
</TouchableOpacity>
</>
) : null}
{mnemonicStep === 2 ? (
<>
<Text style={styles.mnemonicSubtitle}>
Confirm your 12-word mnemonic.
</Text>
<View style={styles.mnemonicBlock}>
<Text style={styles.mnemonicBlockText}>
{mnemonicWords.join(' ')}
</Text>
</View>
<TouchableOpacity
style={styles.mnemonicPrimaryButton}
onPress={() => setMnemonicStep(3)}
activeOpacity={0.85}
>
<Text style={styles.mnemonicPrimaryText}>CONFIRM</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.mnemonicSecondaryButton}
onPress={() => setMnemonicStep(1)}
activeOpacity={0.85}
>
<Text style={styles.mnemonicSecondaryText}>EDIT SELECTION</Text>
</TouchableOpacity>
</>
) : null}
{mnemonicStep === 3 ? (
<>
<Text style={styles.mnemonicSubtitle}>
Back up your mnemonic before entering the Vault.
</Text>
<View style={styles.mnemonicBlock}>
<Text style={styles.mnemonicBlockText}>
{mnemonicWords.join(' ')}
</Text>
</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}
</>
) : null}
{mnemonicStep === 4 ? (
<>
<Text style={styles.mnemonicSubtitle}>
Finalizing your vault protection.
</Text>
<View style={styles.progressContainer}>
<View style={styles.progressTrack}>
<Animated.View
style={[
styles.progressFill,
{
width: progressAnim.interpolate({
inputRange: [0, 1],
outputRange: ['0%', '100%'],
}),
},
]}
/>
</View>
</View>
<View style={styles.progressSteps}>
<Text style={styles.progressText}>
{progressIndex === 0 && '1. Your key is being processed'}
{progressIndex === 1 && '2. Your key has been split'}
{progressIndex === 2 && '3. Part one stored on this device'}
{progressIndex === 3 && '4. Part two uploaded to the cloud'}
{progressIndex >= 4 && '5. Part three inquiry initiated'}
</Text>
</View>
</>
) : null}
{mnemonicStep === 5 ? (
<>
{heirStep === 'decision' ? (
<>
<Text style={styles.mnemonicSubtitle}>
Share Part Three with your legacy handler?
</Text>
<TouchableOpacity
style={styles.mnemonicPrimaryButton}
onPress={handleHeirYes}
activeOpacity={0.85}
>
<Text style={styles.mnemonicPrimaryText}>YES, SEND</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.mnemonicSecondaryButton}
onPress={handleHeirNo}
activeOpacity={0.85}
>
<Text style={styles.mnemonicSecondaryText}>NOT NOW</Text>
</TouchableOpacity>
</>
) : null}
{heirStep === 'asset' ? (
<>
<Text style={styles.mnemonicSubtitle}>
Select the vault item to assign.
</Text>
<ScrollView style={styles.selectorList} showsVerticalScrollIndicator={false}>
{assets.map((asset) => {
const config = assetTypeConfig[asset.type];
const assignment = getAssignmentForAsset(asset.id);
return (
<TouchableOpacity
key={asset.id}
style={styles.selectorRow}
onPress={() => handleSelectHeirAsset(asset)}
activeOpacity={0.8}
>
<View style={styles.selectorIcon}>
{renderAssetTypeIcon(config, 18, colors.vault.primary)}
</View>
<View style={styles.selectorContent}>
<Text style={styles.selectorTitle}>{asset.label}</Text>
<Text style={styles.selectorSubtitle}>
{assignment ? `Assigned to ${assignment.heir.name}` : config.label}
</Text>
</View>
</TouchableOpacity>
);
})}
</ScrollView>
<TouchableOpacity
style={styles.mnemonicPrimaryButton}
onPress={handleReviewAssignments}
activeOpacity={0.85}
>
<Text style={styles.mnemonicPrimaryText}>REVIEW & SUBMIT</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.mnemonicSecondaryButton}
onPress={() => setHeirStep('decision')}
activeOpacity={0.85}
>
<Text style={styles.mnemonicSecondaryText}>BACK</Text>
</TouchableOpacity>
</>
) : null}
{heirStep === 'heir' ? (
<>
<Text style={styles.mnemonicSubtitle}>
Choose a legacy handler.
</Text>
<ScrollView style={styles.selectorList} showsVerticalScrollIndicator={false}>
{initialHeirs.map((heir) => (
<TouchableOpacity
key={heir.id}
style={styles.selectorRow}
onPress={() => handleSelectHeir(heir)}
activeOpacity={0.8}
>
<View style={styles.selectorIcon}>
<Ionicons name="person-circle" size={20} color={colors.sentinel.primary} />
</View>
<View style={styles.selectorContent}>
<Text style={styles.selectorTitle}>{heir.name}</Text>
<Text style={styles.selectorSubtitle}>{heir.email}</Text>
</View>
</TouchableOpacity>
))}
</ScrollView>
<TouchableOpacity
style={styles.mnemonicSecondaryButton}
onPress={() => setHeirStep('asset')}
activeOpacity={0.85}
>
<Text style={styles.mnemonicSecondaryText}>BACK</Text>
</TouchableOpacity>
</>
) : null}
{heirStep === 'summary' ? (
<>
<Text style={styles.mnemonicSubtitle}>
Confirm assignment details.
</Text>
<View style={styles.summaryCard}>
{assignments.map((item) => (
<View key={item.asset.id} style={styles.summaryItem}>
<Text style={styles.summaryLabel}>Asset</Text>
<Text style={styles.summaryValue}>{item.asset.label}</Text>
<Text style={styles.summaryLabel}>Heir</Text>
<Text style={styles.summaryValue}>{item.heir.name}</Text>
<Text style={styles.summaryValue}>{item.heir.email}</Text>
</View>
))}
</View>
<TouchableOpacity
style={styles.mnemonicPrimaryButton}
onPress={handleSubmitAssignment}
activeOpacity={0.85}
>
<Text style={styles.mnemonicPrimaryText}>SUBMIT</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.mnemonicSecondaryButton}
onPress={() => setHeirStep('asset')}
activeOpacity={0.85}
>
<Text style={styles.mnemonicSecondaryText}>EDIT ASSIGNMENTS</Text>
</TouchableOpacity>
</>
) : null}
</>
) : null}
<View style={styles.stepDots}>
{[1, 2, 3, 4, 5].map((step) => (
<View
key={step}
style={[
styles.stepDot,
mnemonicStep === step && styles.stepDotActive,
]}
/>
))}
</View>
</LinearGradient>
</KeyboardAvoidingView>
</Modal>
</View> </View>
); );
} }
@@ -1527,4 +2127,298 @@ const styles = StyleSheet.create({
fontSize: typography.fontSize.sm, fontSize: typography.fontSize.sm,
lineHeight: typography.fontSize.sm * 1.5, lineHeight: typography.fontSize.sm * 1.5,
}, },
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,
},
stepDots: {
flexDirection: 'row',
gap: spacing.sm,
marginTop: spacing.sm,
justifyContent: 'center',
},
stepDot: {
width: 8,
height: 8,
borderRadius: 4,
backgroundColor: colors.sentinel.cardBorder,
},
stepDotActive: {
backgroundColor: colors.sentinel.primary,
},
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,
},
mnemonicBlockText: {
fontSize: typography.fontSize.sm,
color: colors.sentinel.text,
fontFamily: typography.fontFamily.mono,
fontWeight: '600',
lineHeight: 22,
textAlign: 'center',
},
selectedRow: {
backgroundColor: colors.sentinel.cardBackground,
borderRadius: borderRadius.lg,
paddingVertical: spacing.sm,
paddingHorizontal: spacing.md,
borderWidth: 1,
borderColor: colors.sentinel.cardBorder,
marginBottom: spacing.md,
},
selectedLabel: {
fontSize: typography.fontSize.xs,
color: colors.sentinel.textSecondary,
letterSpacing: typography.letterSpacing.wide,
marginBottom: 4,
fontWeight: '600',
},
selectedValue: {
fontSize: typography.fontSize.xs,
color: colors.sentinel.text,
fontFamily: typography.fontFamily.mono,
lineHeight: 18,
},
progressContainer: {
marginTop: spacing.sm,
marginBottom: spacing.md,
},
progressTrack: {
height: 6,
borderRadius: 999,
backgroundColor: colors.sentinel.cardBorder,
overflow: 'hidden',
},
progressFill: {
height: '100%',
backgroundColor: colors.sentinel.primary,
},
progressSteps: {
minHeight: 44,
justifyContent: 'center',
marginBottom: spacing.md,
},
progressText: {
fontSize: typography.fontSize.sm,
color: colors.sentinel.text,
textAlign: 'center',
lineHeight: typography.fontSize.sm * 1.4,
},
wordGrid: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: spacing.sm,
marginBottom: spacing.lg,
},
wordChip: {
paddingHorizontal: spacing.sm,
paddingVertical: spacing.xs,
borderRadius: borderRadius.full,
borderWidth: 1,
borderColor: colors.sentinel.cardBorder,
backgroundColor: 'transparent',
},
wordChipSelected: {
backgroundColor: colors.sentinel.primary,
borderColor: colors.sentinel.primary,
},
wordChipText: {
color: colors.sentinel.text,
fontSize: typography.fontSize.xs,
fontWeight: '600',
},
wordChipTextSelected: {
color: colors.nautical.cream,
},
selectorList: {
maxHeight: 260,
marginBottom: spacing.md,
},
selectorRow: {
flexDirection: 'row',
alignItems: 'center',
gap: spacing.sm,
paddingVertical: spacing.sm,
paddingHorizontal: spacing.sm,
borderRadius: borderRadius.lg,
borderWidth: 1,
borderColor: colors.sentinel.cardBorder,
backgroundColor: colors.sentinel.cardBackground,
marginBottom: spacing.sm,
},
selectorIcon: {
width: 32,
height: 32,
borderRadius: 16,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: `${colors.sentinel.primary}1A`,
},
selectorContent: {
flex: 1,
},
selectorTitle: {
fontSize: typography.fontSize.sm,
color: colors.sentinel.text,
fontWeight: '600',
},
selectorSubtitle: {
fontSize: typography.fontSize.xs,
color: colors.sentinel.textSecondary,
marginTop: 2,
},
summaryCard: {
backgroundColor: colors.sentinel.cardBackground,
borderRadius: borderRadius.lg,
padding: spacing.md,
borderWidth: 1,
borderColor: colors.sentinel.cardBorder,
marginBottom: spacing.md,
gap: spacing.sm,
},
summaryItem: {
paddingBottom: spacing.sm,
borderBottomWidth: 1,
borderBottomColor: colors.sentinel.cardBorder,
},
summaryLabel: {
fontSize: typography.fontSize.xs,
color: colors.sentinel.textSecondary,
letterSpacing: typography.letterSpacing.wide,
marginTop: spacing.xs,
},
summaryValue: {
fontSize: typography.fontSize.sm,
color: colors.sentinel.text,
fontWeight: '600',
},
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,
},
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,
},
}); });