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 {
|
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,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user