Files
frontend/src/screens/VaultScreen.tsx
Ada f6fa19d0b2 feat(vault): add get/create assets API in workflow
TODO: update vault.service.ts. Use MNEMONIC workflow to create real private_key_shard and content_inner_encrypted
2026-02-01 09:19:45 -08:00

2535 lines
80 KiB
TypeScript

import React, { useState, useEffect, useRef } from 'react';
import {
View,
Text,
StyleSheet,
ScrollView,
TouchableOpacity,
Modal,
TextInput,
KeyboardAvoidingView,
Platform,
SafeAreaView,
Animated,
Linking,
Alert,
Share,
} 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 * as bip39 from 'bip39';
import { colors, typography, spacing, borderRadius, shadows } from '../theme/colors';
import { VaultAsset, VaultAssetType, Heir } from '../types';
import BiometricModal from '../components/common/BiometricModal';
import { useAuth } from '../context/AuthContext';
import { useVaultAssets } from '../hooks/useVaultAssets';
// Asset type configuration with nautical theme
const assetTypeConfig: Record<VaultAssetType, { icon: string; iconType: 'ionicons' | 'feather' | 'material' | 'fontawesome5'; label: string }> = {
game_account: { icon: 'account-key', iconType: 'material', label: 'Account Login' },
private_key: { icon: 'key', iconType: 'fontawesome5', label: 'Secret Key' },
document: { icon: 'scroll', iconType: 'fontawesome5', label: 'Document' },
photo: { icon: 'image', iconType: 'ionicons', label: 'Sealed Photo' },
will: { icon: 'file-signature', iconType: 'fontawesome5', label: 'Testament' },
custom: { icon: 'gem', iconType: 'fontawesome5', label: 'Treasure' },
};
const accountProviderOptions = [
{ key: 'bank', label: 'Bank', icon: 'bank', iconType: 'material' as const },
{ key: 'steam', label: 'Steam', icon: 'steam', iconType: 'fontawesome5' as const },
{ key: 'facebook', label: 'Facebook', icon: 'facebook-f', iconType: 'fontawesome5' as const },
{ key: 'custom', label: 'Other', icon: 'shield-account', iconType: 'material' as const },
];
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 = () => bip39.generateMnemonic(128).split(' ');
const splitMnemonic = (words: string[]) => [
words.slice(0, 4),
words.slice(4, 8),
words.slice(8, 12),
];
type HeirAssignment = {
asset: VaultAsset;
heir: Heir;
};
// Mock data
// const initialAssets: VaultAsset[] = [
// {
// id: '1',
// type: 'private_key',
// label: 'ETH Main Wallet Key',
// createdAt: new Date('2024-01-10'),
// updatedAt: new Date('2024-01-10'),
// isEncrypted: true,
// },
// {
// id: '2',
// type: 'game_account',
// label: 'Steam Account Credentials',
// createdAt: new Date('2024-01-08'),
// updatedAt: new Date('2024-01-08'),
// isEncrypted: true,
// },
// {
// id: '3',
// type: 'document',
// label: 'Insurance Policy Scan',
// createdAt: new Date('2024-01-05'),
// updatedAt: new Date('2024-01-05'),
// isEncrypted: true,
// },
// {
// id: '4',
// type: 'will',
// label: 'Testament Draft v2',
// createdAt: new Date('2024-01-02'),
// updatedAt: new Date('2024-01-15'),
// isEncrypted: true,
// },
// ];
const renderAssetTypeIcon = (config: typeof assetTypeConfig[VaultAssetType], size: number, color: string) => {
switch (config.iconType) {
case 'ionicons':
return <Ionicons name={config.icon as any} size={size} color={color} />;
case 'feather':
return <Feather name={config.icon as any} size={size} color={color} />;
case 'material':
return <MaterialCommunityIcons name={config.icon as any} size={size} color={color} />;
case 'fontawesome5':
return <FontAwesome5 name={config.icon as any} size={size} color={color} />;
}
};
export default function VaultScreen() {
const [isUnlocked, setIsUnlocked] = useState(false);
const [showBiometric, setShowBiometric] = useState(false);
const {
assets,
setAssets,
refreshAssets,
createAsset: createVaultAsset,
isSealing,
createError: addError,
clearCreateError: clearAddError,
} = useVaultAssets(isUnlocked);
const [showAddModal, setShowAddModal] = useState(false);
const [selectedType, setSelectedType] = useState<VaultAssetType>('custom');
const [newLabel, setNewLabel] = useState('');
const [showUploadSuccess, setShowUploadSuccess] = useState(false);
const [fadeAnim] = useState(new Animated.Value(0));
const [pulseAnim] = useState(new Animated.Value(1));
const [selectedAsset, setSelectedAsset] = useState<VaultAsset | null>(null);
const [showDetail, setShowDetail] = useState(false);
const [showGuardedBiometric, setShowGuardedBiometric] = useState(false);
const [showKeyPreview, setShowKeyPreview] = useState(false);
const [addStep, setAddStep] = useState(1);
const [addMethod, setAddMethod] = useState<'text' | 'file' | 'scan'>('text');
const [addVerified, setAddVerified] = useState(false);
const [rehearsalConfirmed, setRehearsalConfirmed] = useState(false);
const [showAddBiometric, setShowAddBiometric] = useState(false);
const [accountProvider, setAccountProvider] = useState<'bank' | 'steam' | 'facebook' | 'custom'>('bank');
const [showMnemonic, setShowMnemonic] = useState(false);
const [showLegacyAssignCta, setShowLegacyAssignCta] = 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 [replaceIndex, setReplaceIndex] = useState<number | null>(null);
const [replaceQuery, setReplaceQuery] = useState('');
const [progressIndex, setProgressIndex] = useState(0);
const [progressAnim] = useState(new Animated.Value(0));
const { user, token } = useAuth();
const [isCapturing, setIsCapturing] = useState(false);
const [treasureContent, setTreasureContent] = useState('');
const mnemonicRef = useRef<View>(null);
const progressTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
useEffect(() => {
if (!isUnlocked) {
const timer = setTimeout(() => {
setShowBiometric(true);
}, 500);
return () => clearTimeout(timer);
}
}, []);
useEffect(() => {
if (isUnlocked) {
Animated.timing(fadeAnim, {
toValue: 1,
duration: 600,
useNativeDriver: true,
}).start();
}
}, [isUnlocked]);
useEffect(() => {
if (!isUnlocked) {
Animated.loop(
Animated.sequence([
Animated.timing(pulseAnim, {
toValue: 1.05,
duration: 1500,
useNativeDriver: true,
}),
Animated.timing(pulseAnim, {
toValue: 1,
duration: 1500,
useNativeDriver: true,
}),
])
).start();
}
}, [isUnlocked]);
const handleUnlock = () => {
setShowBiometric(false);
const words = generateMnemonic();
const parts = splitMnemonic(words);
setMnemonicWords(words);
setMnemonicParts(parts);
setReplaceIndex(null);
setReplaceQuery('');
setMnemonicStep(1);
setHeirStep('decision');
setSelectedHeir(null);
setSelectedHeirAsset(null);
setProgressIndex(0);
progressAnim.setValue(0);
setTimeout(() => setShowMnemonic(true), 200);
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: Platform.OS === 'web' ? 'data-uri' : 'tmpfile',
});
if (Platform.OS === 'web') {
try {
await Linking.openURL(uri);
} catch {
// Ignore if the browser blocks data-uri navigation.
}
} else {
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 = () => {
// Proceed immediately; email delivery is handled separately.
setMnemonicStep(4);
setShowMnemonic(true);
};
const handleReplaceWord = (word: string) => {
if (replaceIndex === null) return;
const nextWords = [...mnemonicWords];
nextWords[replaceIndex] = word;
setMnemonicWords(nextWords);
setMnemonicParts(splitMnemonic(nextWords));
setReplaceIndex(null);
setReplaceQuery('');
};
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);
};
const handleSubmitAssignment = () => {
// Placeholder for submitting assignment to API
setShowMnemonic(false);
setIsUnlocked(true);
};
const handleHeirYes = () => {
setShowMnemonic(false);
setIsUnlocked(true);
setShowLegacyAssignCta(true);
};
const handleHeirNo = () => {
setShowMnemonic(false);
setIsUnlocked(true);
setShowLegacyAssignCta(true);
};
const handleOpenLegacyAssign = () => {
setSelectedHeir(null);
setSelectedHeirAsset(null);
setHeirStep('asset');
setMnemonicStep(5);
setShowMnemonic(true);
};
const handleSelectHeirAsset = (asset: VaultAsset) => {
setSelectedHeirAsset(asset);
setHeirStep('heir');
};
const handleSelectHeir = (heir: Heir) => {
setSelectedHeir(heir);
setHeirStep('summary');
};
const resetAddFlow = () => {
setAddStep(1);
setAddMethod('text');
setAddVerified(false);
setRehearsalConfirmed(false);
setSelectedType('custom');
setNewLabel('');
setAccountProvider('bank');
};
const handleAddAsset = async () => {
if (!newLabel.trim() || !treasureContent.trim()) return;
if (!addVerified) return;
if (selectedType === 'private_key' && !rehearsalConfirmed) return;
if (!token) {
Alert.alert('Not logged in', 'Please sign in first to add a Treasure.');
return;
}
const result = await createVaultAsset({
title: newLabel.trim(),
content: treasureContent.trim(),
});
if (result.success) {
setNewLabel('');
setTreasureContent('');
setSelectedType('custom');
setAddVerified(false);
setRehearsalConfirmed(false);
setShowAddModal(false);
clearAddError();
setShowUploadSuccess(true);
setTimeout(() => setShowUploadSuccess(false), 2500);
if (typeof Alert !== 'undefined' && Alert.alert) {
Alert.alert('Success', 'Treasure sealed and saved successfully.');
}
return;
}
if (result.isUnauthorized) {
setShowAddModal(false);
clearAddError();
if (typeof Alert !== 'undefined' && Alert.alert) {
Alert.alert(
'Unauthorized',
'Your session has expired or you are not logged in. Please sign in again.',
[{ text: 'OK' }]
);
}
return;
}
if (result.error && typeof Alert !== 'undefined' && Alert.alert) {
Alert.alert('Failed', result.error);
}
};
const formatDate = (date: Date) => {
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
});
};
const handleOpenDetail = (asset: VaultAsset) => {
setSelectedAsset(asset);
setShowDetail(true);
setShowKeyPreview(false);
};
const handleCloseDetail = () => {
setShowDetail(false);
setSelectedAsset(null);
setShowKeyPreview(false);
setShowGuardedBiometric(false);
};
const handleGuardedAccess = () => {
setShowGuardedBiometric(true);
};
const handleGuardedSuccess = () => {
setShowGuardedBiometric(false);
setShowKeyPreview(true);
};
const handleAddVerification = () => {
setShowAddBiometric(true);
};
const handleAddVerificationSuccess = () => {
setShowAddBiometric(false);
setAddVerified(true);
};
const openProviderLogin = async () => {
if (accountProvider === 'bank') {
Alert.alert(
'Bank App Needed',
'Provide the bank app deep link scheme to enable one-tap login.'
);
return;
}
const providerLinks = {
steam: {
app: 'steam://openurl/https://store.steampowered.com/login/',
web: 'https://store.steampowered.com/login/',
},
facebook: {
app: 'fb://facewebmodal/f?href=https://www.facebook.com/login',
web: 'https://www.facebook.com/login',
},
custom: {
app: '',
web: '',
},
} as const;
const target = providerLinks[accountProvider];
if (!target?.app) {
return;
}
const canOpen = await Linking.canOpenURL(target.app);
if (canOpen) {
await Linking.openURL(target.app);
} else if (target.web) {
await Linking.openURL(target.web);
}
};
const selectedConfig = selectedAsset ? assetTypeConfig[selectedAsset.type] : null;
const detailHeading = selectedAsset?.type === 'private_key'
? 'Key Material'
: selectedConfig?.label || 'Vault Asset';
const detailMetaLabel = selectedAsset?.type === 'private_key' ? 'KEY MATERIAL' : 'ASSET CLASS';
const detailMetaValue = selectedAsset?.type === 'private_key'
? '12/24 Words'
: selectedConfig?.label || '--';
const canSeal = !!newLabel.trim()
&& !!treasureContent.trim()
&& addVerified
&& !isSealing
&& (selectedType !== 'private_key' || rehearsalConfirmed);
const mnemonicModal = (
<Modal
visible={showMnemonic}
animationType="fade"
transparent
onRequestClose={() => setShowMnemonic(false)}
>
<KeyboardAvoidingView
style={styles.mnemonicOverlay}
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
>
<View ref={mnemonicRef} collapsable={false}>
<LinearGradient
colors={[colors.sentinel.cardBackground, colors.sentinel.backgroundGradientEnd]}
style={styles.mnemonicCard}
>
<TouchableOpacity
style={styles.mnemonicClose}
onPress={() => setShowMnemonic(false)}
activeOpacity={0.85}
>
<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}>
Review your 12-word mnemonic. Tap any word to replace it.
</Text>
<View style={styles.wordGrid}>
{mnemonicWords.map((word, index) => (
<TouchableOpacity
key={`${word}-${index}`}
style={[
styles.wordChip,
replaceIndex === index && styles.wordChipSelected,
]}
onPress={() => setReplaceIndex(index)}
activeOpacity={0.8}
>
<Text
style={[
styles.wordChipIndex,
replaceIndex === index && styles.wordChipTextSelected,
]}
>
{index + 1}
</Text>
<Text
style={[
styles.wordChipText,
replaceIndex === index && styles.wordChipTextSelected,
]}
>
{word}
</Text>
</TouchableOpacity>
))}
</View>
{replaceIndex !== null ? (
<View style={styles.replacePanel}>
<Text style={styles.replaceTitle}>
Replace word {replaceIndex + 1}
</Text>
<TextInput
style={styles.replaceInput}
value={replaceQuery}
onChangeText={setReplaceQuery}
placeholder="Search a word"
placeholderTextColor={colors.sentinel.textSecondary}
autoCapitalize="none"
autoCorrect={false}
/>
<ScrollView style={styles.replaceList} showsVerticalScrollIndicator={false}>
{(replaceQuery
? bip39.wordlists.english.filter((word) =>
word.startsWith(replaceQuery.toLowerCase())
)
: bip39.wordlists.english
)
.slice(0, 24)
.map((word) => (
<TouchableOpacity
key={word}
style={styles.replaceOption}
onPress={() => handleReplaceWord(word)}
activeOpacity={0.8}
>
<Text style={styles.replaceOptionText}>{word}</Text>
</TouchableOpacity>
))}
</ScrollView>
<TouchableOpacity
style={styles.replaceCancel}
onPress={() => setReplaceIndex(null)}
activeOpacity={0.85}
>
<Text style={styles.replaceCancelText}>CANCEL</Text>
</TouchableOpacity>
</View>
) : null}
<TouchableOpacity
style={styles.mnemonicPrimaryButton}
onPress={() => setMnemonicStep(2)}
activeOpacity={0.85}
>
<Text style={styles.mnemonicPrimaryText}>NEXT</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>
</>
) : 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];
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}>{config.label}</Text>
</View>
</TouchableOpacity>
);
})}
</ScrollView>
<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}>
<Text style={styles.summaryLabel}>Vault Item</Text>
<Text style={styles.summaryValue}>{selectedHeirAsset?.label}</Text>
<Text style={styles.summaryLabel}>Legacy Handler</Text>
<Text style={styles.summaryValue}>{selectedHeir?.name}</Text>
<Text style={styles.summaryValue}>{selectedHeir?.email}</Text>
<Text style={styles.summaryLabel}>Release Tier</Text>
<Text style={styles.summaryValue}>Tier {selectedHeir?.releaseLevel}</Text>
</View>
<TouchableOpacity
style={styles.mnemonicPrimaryButton}
onPress={handleSubmitAssignment}
activeOpacity={0.85}
>
<Text style={styles.mnemonicPrimaryText}>SUBMIT</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.mnemonicSecondaryButton}
onPress={() => setHeirStep('heir')}
activeOpacity={0.85}
>
<Text style={styles.mnemonicSecondaryText}>EDIT</Text>
</TouchableOpacity>
</>
) : null}
</>
) : null}
<View style={styles.stepDots}>
<View style={[styles.stepDot, mnemonicStep === 1 && styles.stepDotActive]} />
<View style={[styles.stepDot, mnemonicStep !== 1 && styles.stepDotActive]} />
</View>
</LinearGradient>
</View>
</KeyboardAvoidingView>
</Modal>
);
// Lock screen
const lockScreen = (
<View style={styles.lockContainer}>
<LinearGradient
colors={[colors.vault.backgroundGradientStart, colors.vault.backgroundGradientEnd]}
style={styles.lockGradient}
>
<SafeAreaView style={styles.lockSafeArea}>
<View style={styles.lockContent}>
<Animated.View style={[styles.lockIconContainer, { transform: [{ scale: pulseAnim }] }]}>
<LinearGradient
colors={[colors.nautical.teal, colors.nautical.deepTeal]}
style={styles.lockIconGradient}
>
<MaterialCommunityIcons name="treasure-chest" size={64} color={colors.vault.primary} />
</LinearGradient>
</Animated.View>
<Text style={styles.lockTitle}>THE DEEP VAULT</Text>
<Text style={styles.lockSubtitle}>Where treasures rest in silence</Text>
<View style={styles.waveContainer}>
<MaterialCommunityIcons name="waves" size={48} color={colors.vault.secondary} style={{ opacity: 0.3 }} />
</View>
<TouchableOpacity
style={styles.unlockButton}
onPress={() => setShowBiometric(true)}
activeOpacity={0.8}
>
<LinearGradient
colors={[colors.vault.primary, colors.vault.secondary]}
style={styles.unlockButtonGradient}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
>
<Ionicons name="finger-print" size={20} color={colors.vault.background} />
<Text style={styles.unlockButtonText}>Captain's Verification</Text>
</LinearGradient>
</TouchableOpacity>
</View>
</SafeAreaView>
</LinearGradient>
<BiometricModal
visible={showBiometric}
onSuccess={handleUnlock}
onCancel={() => setShowBiometric(false)}
title="Enter the Vault"
message="Verify your identity to access your treasures"
isDark
/>
</View>
);
const vaultScreen = (
<View style={styles.container}>
<LinearGradient
colors={[colors.vault.backgroundGradientStart, colors.vault.backgroundGradientEnd]}
style={styles.gradient}
>
<SafeAreaView style={styles.safeArea}>
<Animated.View style={[styles.content, { opacity: fadeAnim }]}>
{/* Header */}
<View style={styles.header}>
<View style={styles.headerTop}>
<View style={styles.headerTitleRow}>
<MaterialCommunityIcons name="treasure-chest" size={26} color={colors.vault.primary} />
<Text style={styles.title}>THE VAULT</Text>
</View>
<View style={styles.securityBadge}>
<Ionicons name="shield-checkmark" size={14} color={colors.vault.success} />
<Text style={styles.securityText}>SEALED</Text>
</View>
</View>
<Text style={styles.subtitle}>
{assets.length} treasures · Encrypted at rest
</Text>
</View>
{showLegacyAssignCta && (
<View style={styles.legacyCtaCard}>
<View style={styles.legacyCtaInfo}>
<Text style={styles.legacyCtaTitle}>Legacy Assignment</Text>
<Text style={styles.legacyCtaText}>
Continue assigning a vault item to your legacy handler.
</Text>
</View>
<TouchableOpacity
style={styles.legacyCtaButton}
onPress={handleOpenLegacyAssign}
activeOpacity={0.85}
>
<Text style={styles.legacyCtaButtonText}>Continue</Text>
</TouchableOpacity>
</View>
)}
{/* Asset List */}
<ScrollView
style={styles.assetList}
showsVerticalScrollIndicator={false}
contentContainerStyle={styles.assetListContent}
>
{assets.map((asset) => {
const config = assetTypeConfig[asset.type];
return (
<TouchableOpacity
key={asset.id}
style={styles.assetCard}
activeOpacity={0.7}
onPress={() => handleOpenDetail(asset)}
>
<View style={styles.assetIconContainer}>
{renderAssetTypeIcon(config, 22, colors.vault.primary)}
</View>
<View style={styles.assetInfo}>
<Text style={styles.assetType}>{config.label}</Text>
<Text style={styles.assetLabel}>{asset.label}</Text>
<View style={styles.assetMetaRow}>
<Feather name="clock" size={10} color={colors.vault.textSecondary} />
<Text style={styles.assetMeta}>Sealed {formatDate(asset.createdAt)}</Text>
</View>
</View>
<View style={styles.encryptedBadge}>
<MaterialCommunityIcons name="lock" size={16} color="#fff" />
</View>
</TouchableOpacity>
);
})}
<View style={{ height: 100 }} />
</ScrollView>
{/* Add Button */}
<TouchableOpacity
style={styles.addButton}
onPress={() => {
resetAddFlow();
clearAddError();
setShowAddModal(true);
}}
activeOpacity={0.9}
>
<LinearGradient
colors={[colors.vault.primary, colors.vault.secondary]}
style={styles.addButtonGradient}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
>
<FontAwesome5 name="plus" size={16} color={colors.vault.background} />
<Text style={styles.addButtonText}>Add Treasure</Text>
</LinearGradient>
</TouchableOpacity>
{/* Upload Success Toast */}
{showUploadSuccess && (
<Animated.View style={styles.successToast}>
<Ionicons name="checkmark-circle" size={20} color="#fff" />
<Text style={styles.successText}>Treasure sealed successfully</Text>
</Animated.View>
)}
</Animated.View>
</SafeAreaView>
</LinearGradient>
{/* Add Modal */}
<Modal
visible={showAddModal}
animationType="slide"
transparent
onRequestClose={() => setShowAddModal(false)}
>
<View style={styles.modalOverlay}>
<View style={styles.modalContent}>
<View style={styles.modalHandle} />
<View style={styles.modalHeader}>
<FontAwesome5 name="gem" size={22} color={colors.nautical.teal} />
<Text style={styles.modalTitle}>Add New Treasure</Text>
</View>
<View style={styles.stepRow}>
{['Title', 'Content', 'Verify'].map((label, index) => {
const stepIndex = index + 1;
const isActive = addStep === stepIndex;
const isDone = addStep > stepIndex;
return (
<View key={label} style={styles.stepItem}>
<View style={[
styles.stepCircle,
isActive && styles.stepCircleActive,
isDone && styles.stepCircleDone,
]}>
<Text style={[
styles.stepNumber,
isActive && styles.stepNumberActive,
isDone && styles.stepNumberDone,
]}>
{stepIndex}
</Text>
</View>
<Text style={[styles.stepLabel, (isActive || isDone) && styles.stepLabelActive]}>
{label}
</Text>
</View>
);
})}
</View>
{addStep === 1 && (
<>
<Text style={styles.modalLabel}>TREASURE TITLE</Text>
<TextInput
style={styles.input}
placeholder="e.g., Main wallet mnemonic"
placeholderTextColor={colors.nautical.sage}
value={newLabel}
onChangeText={setNewLabel}
/>
<Text style={styles.modalLabel}>TREASURE TYPE</Text>
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
style={styles.typeScroll}
contentContainerStyle={styles.typeScrollContent}
>
{(Object.keys(assetTypeConfig) as VaultAssetType[]).map((type) => {
const config = assetTypeConfig[type];
const isSelected = selectedType === type;
return (
<TouchableOpacity
key={type}
style={[styles.typeButton, isSelected && styles.typeButtonActive]}
onPress={() => setSelectedType(type)}
>
<View style={[styles.typeIconContainer, isSelected && styles.typeIconContainerActive]}>
{renderAssetTypeIcon(config, 22, isSelected ? colors.nautical.teal : colors.nautical.sage)}
</View>
<Text style={[styles.typeButtonLabel, isSelected && styles.typeButtonLabelActive]}>
{config.label}
</Text>
</TouchableOpacity>
);
})}
</ScrollView>
</>
)}
{addStep === 2 && (
<>
{selectedType !== 'game_account' && (
<>
<Text style={styles.modalLabel}>CAPTURE METHOD</Text>
<View style={styles.methodRow}>
{[
{ key: 'text', label: 'Text', icon: 'text' },
{ key: 'file', label: 'File', icon: 'file-tray' },
{ key: 'scan', label: 'Scan', icon: 'camera' },
].map((item) => {
const isActive = addMethod === item.key;
return (
<TouchableOpacity
key={item.key}
style={[styles.methodButton, isActive && styles.methodButtonActive]}
onPress={() => setAddMethod(item.key as typeof addMethod)}
>
<Ionicons
name={item.icon as any}
size={18}
color={isActive ? colors.nautical.teal : colors.nautical.sage}
/>
<Text style={[styles.methodLabel, isActive && styles.methodLabelActive]}>
{item.label}
</Text>
</TouchableOpacity>
);
})}
</View>
<Text style={styles.modalLabel}>CONTENT</Text>
<TextInput
style={[styles.input, styles.inputMultiline]}
placeholder="Enter content to seal (plaintext is encrypted locally before upload)"
placeholderTextColor={colors.nautical.sage}
value={treasureContent}
onChangeText={setTreasureContent}
multiline
numberOfLines={6}
textAlignVertical="top"
/>
<View style={styles.encryptionNote}>
<MaterialCommunityIcons name="anchor" size={16} color={colors.nautical.teal} />
<Text style={styles.encryptionNoteText}>
Data is encrypted on-device. Plaintext is shredded after sealing.
</Text>
</View>
</>
)}
{selectedType === 'game_account' && (
<>
<Text style={styles.modalLabel}>ACCOUNT PROVIDER</Text>
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
style={styles.typeScroll}
contentContainerStyle={styles.typeScrollContent}
>
{accountProviderOptions.map((option) => {
const isSelected = accountProvider === option.key;
return (
<TouchableOpacity
key={option.key}
style={[styles.typeButton, isSelected && styles.typeButtonActive]}
onPress={() => setAccountProvider(option.key as typeof accountProvider)}
>
<View style={[styles.typeIconContainer, isSelected && styles.typeIconContainerActive]}>
{renderAssetTypeIcon(
{ icon: option.icon, iconType: option.iconType, label: option.label },
22,
isSelected ? colors.nautical.teal : colors.nautical.sage
)}
</View>
<Text style={[styles.typeButtonLabel, isSelected && styles.typeButtonLabelActive]}>
{option.label}
</Text>
</TouchableOpacity>
);
})}
</ScrollView>
<TouchableOpacity
style={styles.loginButton}
onPress={openProviderLogin}
activeOpacity={0.85}
>
<Ionicons name="log-in-outline" size={18} color={colors.nautical.cream} />
<Text style={styles.loginButtonText}>Open App to Login</Text>
</TouchableOpacity>
<Text style={styles.modalLabel}>TREASURE NAME</Text>
<TextInput
style={styles.input}
placeholder="e.g., Main wallet mnemonic"
placeholderTextColor={colors.nautical.sage}
value={newLabel}
onChangeText={setNewLabel}
/>
</>
)}
</>
)}
{addStep === 3 && (
<>
<Text style={styles.modalLabel}>IDENTITY VERIFICATION</Text>
<View style={styles.verifyCard}>
<View style={styles.verifyRow}>
<Ionicons name="finger-print" size={18} color={colors.nautical.teal} />
<Text style={styles.verifyText}>Biometric required before sealing.</Text>
</View>
<TouchableOpacity
style={[styles.verifyButton, addVerified && styles.verifyButtonDone]}
onPress={handleAddVerification}
activeOpacity={0.85}
>
<Text style={styles.verifyButtonText}>
{addVerified ? 'Verified' : 'Verify Now'}
</Text>
</TouchableOpacity>
</View>
{selectedType === 'private_key' && (
<TouchableOpacity
style={styles.rehearsalRow}
onPress={() => setRehearsalConfirmed(!rehearsalConfirmed)}
activeOpacity={0.8}
>
<Ionicons
name={rehearsalConfirmed ? 'checkbox' : 'square-outline'}
size={20}
color={rehearsalConfirmed ? colors.nautical.teal : colors.nautical.sage}
/>
<Text style={styles.rehearsalText}>
I rehearsed the mnemonic once (required).
</Text>
</TouchableOpacity>
)}
<View style={styles.encryptionNote}>
<MaterialCommunityIcons name="lock" size={16} color={colors.nautical.teal} />
<Text style={styles.encryptionNoteText}>
Only a masked shard can be revealed, and access is time-limited.
</Text>
</View>
</>
)}
{addError ? (
<View style={styles.addErrorBox}>
<Ionicons name="warning" size={18} color="#c2410c" />
<Text style={styles.addErrorText}>{addError}</Text>
</View>
) : null}
<View style={styles.modalButtons}>
<TouchableOpacity
style={styles.cancelButton}
onPress={() => {
if (addStep === 1) {
setShowAddModal(false);
setTreasureContent('');
clearAddError();
} else {
setAddStep(addStep - 1);
clearAddError();
}
}}
>
<Text style={styles.cancelButtonText}>
{addStep === 1 ? 'Cancel' : 'Back'}
</Text>
</TouchableOpacity>
{addStep < 3 ? (
<TouchableOpacity
style={styles.confirmButton}
onPress={() => setAddStep(addStep + 1)}
>
<LinearGradient
colors={[colors.nautical.teal, colors.nautical.seafoam]}
style={styles.confirmButtonGradient}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
>
<Text style={styles.confirmButtonText}>Continue</Text>
</LinearGradient>
</TouchableOpacity>
) : (
<TouchableOpacity
style={styles.confirmButton}
onPress={handleAddAsset}
activeOpacity={canSeal ? 0.9 : 1}
disabled={!canSeal}
>
<LinearGradient
colors={[colors.nautical.teal, colors.nautical.seafoam]}
style={[
styles.confirmButtonGradient,
!canSeal && styles.confirmButtonGradientDisabled,
]}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
>
<MaterialCommunityIcons name="lock" size={18} color="#fff" />
<Text style={styles.confirmButtonText}>{isSealing ? 'Sealing...' : 'Seal Treasure'}</Text>
</LinearGradient>
</TouchableOpacity>
)}
</View>
</View>
</View>
</Modal>
<BiometricModal
visible={showAddBiometric}
onSuccess={handleAddVerificationSuccess}
onCancel={() => setShowAddBiometric(false)}
title="Confirm Identity"
message="Verify before sealing this treasure"
isDark
/>
{/* Detail Modal */}
<Modal
visible={showDetail}
animationType="slide"
onRequestClose={handleCloseDetail}
>
<View style={styles.detailContainer}>
<LinearGradient
colors={[colors.vault.backgroundGradientStart, colors.vault.backgroundGradientEnd]}
style={styles.detailGradient}
>
<SafeAreaView style={styles.detailSafeArea}>
<View style={styles.detailHeader}>
<TouchableOpacity
style={styles.detailBackButton}
onPress={handleCloseDetail}
activeOpacity={0.8}
>
<Ionicons name="chevron-back" size={20} color={colors.vault.text} />
</TouchableOpacity>
<Text style={styles.detailTitle}>{detailHeading}</Text>
{selectedAsset?.type === 'private_key' && (
<View style={styles.riskBadge}>
<Text style={styles.riskBadgeText}>HIGH-RISK</Text>
</View>
)}
</View>
<ScrollView
style={styles.detailScroll}
contentContainerStyle={styles.detailContent}
showsVerticalScrollIndicator={false}
>
<View style={styles.detailHeroCard}>
<View style={styles.detailHeroIcon}>
<MaterialCommunityIcons name="key-variant" size={28} color={colors.vault.primary} />
</View>
<View style={styles.detailHeroInfo}>
<Text style={styles.detailHeroLabel}>SEALED ASSET</Text>
<Text style={styles.detailHeroTitle}>{selectedAsset?.label}</Text>
<Text style={styles.detailHeroSubtitle}>Encrypted at rest · No plaintext stored</Text>
</View>
</View>
<View style={styles.metaGrid}>
<View style={styles.metaCard}>
<Text style={styles.metaLabel}>CREATED</Text>
<Text style={styles.metaValue}>
{selectedAsset ? formatDate(selectedAsset.createdAt) : '--'}
</Text>
</View>
<View style={styles.metaCard}>
<Text style={styles.metaLabel}>LAST VERIFIED</Text>
<Text style={styles.metaValue}>
{selectedAsset ? formatDate(selectedAsset.updatedAt) : '--'}
</Text>
</View>
<View style={styles.metaCard}>
<Text style={styles.metaLabel}>STORAGE</Text>
<Text style={styles.metaValue}>Cipher Pack</Text>
</View>
<View style={styles.metaCard}>
<Text style={styles.metaLabel}>{detailMetaLabel}</Text>
<Text style={styles.metaValue}>{detailMetaValue}</Text>
</View>
</View>
<View style={styles.actionGroup}>
<Text style={styles.sectionTitle}>CONTROLLED ACTIONS</Text>
<TouchableOpacity style={styles.actionRow} activeOpacity={0.8}>
<Ionicons name="finger-print" size={18} color={colors.vault.primary} />
<Text style={styles.actionText}>View Encrypted Digest</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.actionRow} activeOpacity={0.8}>
<MaterialCommunityIcons name="file-lock" size={18} color={colors.vault.primary} />
<Text style={styles.actionText}>Export Cipher Pack</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.actionRow} activeOpacity={0.8}>
<MaterialCommunityIcons name="refresh" size={18} color={colors.vault.primary} />
<Text style={styles.actionText}>Reset Sentinel Timer</Text>
</TouchableOpacity>
</View>
<View style={styles.guardCard}>
<View style={styles.guardHeader}>
<MaterialCommunityIcons name="shield-lock" size={18} color={colors.vault.warning} />
<Text style={styles.guardTitle}>Guarded Reveal</Text>
</View>
<Text style={styles.guardText}>
Plaintext access requires biometric verification and a memory rehearsal step.
</Text>
<TouchableOpacity
style={styles.guardButton}
onPress={handleGuardedAccess}
activeOpacity={0.85}
>
<Text style={styles.guardButtonText}>Begin Verification</Text>
</TouchableOpacity>
{showKeyPreview && (
<View style={styles.previewCard}>
<Text style={styles.previewLabel}>MNEMONIC SHARD (MASKED)</Text>
<Text style={styles.previewValue}>ocean-anchored-ember-veil</Text>
</View>
)}
</View>
<View style={styles.warningCard}>
<Ionicons name="warning" size={18} color={colors.vault.warning} />
<Text style={styles.warningText}>
Any reveal attempt is time-limited and logged. Screenshots are blocked by policy.
</Text>
</View>
</ScrollView>
</SafeAreaView>
</LinearGradient>
</View>
<BiometricModal
visible={showGuardedBiometric}
onSuccess={handleGuardedSuccess}
onCancel={() => setShowGuardedBiometric(false)}
title="Verify Access"
message="Confirm to reveal a masked shard of the mnemonic"
isDark
/>
</Modal>
</View>
);
return (
<View style={styles.root}>
{isUnlocked ? vaultScreen : lockScreen}
{mnemonicModal}
</View>
);
}
const styles = StyleSheet.create({
root: {
flex: 1,
},
container: {
flex: 1,
},
gradient: {
flex: 1,
},
safeArea: {
flex: 1,
},
content: {
flex: 1,
},
lockContainer: {
flex: 1,
},
lockGradient: {
flex: 1,
},
lockSafeArea: {
flex: 1,
},
lockContent: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
paddingHorizontal: spacing.xl,
},
lockIconContainer: {
marginBottom: spacing.lg,
},
lockIconGradient: {
width: 130,
height: 130,
borderRadius: 65,
justifyContent: 'center',
alignItems: 'center',
...shadows.glow,
},
lockTitle: {
fontSize: typography.fontSize.xxl,
fontWeight: '700',
color: colors.vault.text,
letterSpacing: typography.letterSpacing.widest,
marginBottom: spacing.sm,
fontFamily: typography.fontFamily.serif,
},
lockSubtitle: {
fontSize: typography.fontSize.base,
color: colors.vault.textSecondary,
marginBottom: spacing.xl,
textAlign: 'center',
fontStyle: 'italic',
},
waveContainer: {
marginBottom: spacing.xl,
},
unlockButton: {
borderRadius: borderRadius.lg,
overflow: 'hidden',
},
unlockButtonGradient: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: spacing.md,
paddingHorizontal: spacing.xl,
gap: spacing.sm,
},
unlockButtonText: {
fontSize: typography.fontSize.base,
color: colors.vault.background,
fontWeight: '600',
},
header: {
paddingHorizontal: spacing.lg,
paddingTop: spacing.lg,
paddingBottom: spacing.md,
},
headerTop: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: spacing.xs,
},
headerTitleRow: {
flexDirection: 'row',
alignItems: 'center',
gap: spacing.sm,
},
title: {
fontSize: typography.fontSize.xl,
fontWeight: '700',
color: colors.vault.text,
letterSpacing: typography.letterSpacing.wider,
fontFamily: typography.fontFamily.serif,
},
securityBadge: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: `${colors.vault.success}20`,
paddingHorizontal: spacing.sm,
paddingVertical: spacing.xs,
borderRadius: borderRadius.full,
gap: spacing.xs,
},
securityText: {
fontSize: typography.fontSize.xs,
color: colors.vault.success,
fontWeight: '700',
letterSpacing: 1,
},
subtitle: {
fontSize: typography.fontSize.sm,
color: colors.vault.textSecondary,
},
legacyCtaCard: {
marginHorizontal: spacing.lg,
marginBottom: spacing.sm,
padding: spacing.md,
borderRadius: borderRadius.lg,
backgroundColor: colors.vault.cardBackground,
borderWidth: 1,
borderColor: colors.vault.cardBorder,
flexDirection: 'row',
alignItems: 'center',
gap: spacing.md,
},
legacyCtaInfo: {
flex: 1,
},
legacyCtaTitle: {
color: colors.vault.text,
fontSize: typography.fontSize.base,
fontWeight: '700',
},
legacyCtaText: {
color: colors.vault.textSecondary,
fontSize: typography.fontSize.sm,
marginTop: spacing.xs,
},
legacyCtaButton: {
paddingHorizontal: spacing.md,
paddingVertical: spacing.sm,
borderRadius: borderRadius.full,
backgroundColor: colors.vault.primary,
},
legacyCtaButtonText: {
color: colors.vault.background,
fontWeight: '700',
fontSize: typography.fontSize.sm,
},
assetList: {
flex: 1,
},
assetListContent: {
padding: spacing.lg,
paddingTop: spacing.sm,
},
assetCard: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.vault.cardBackground,
borderRadius: borderRadius.xl,
padding: spacing.base,
marginBottom: spacing.md,
borderWidth: 1,
borderColor: colors.vault.cardBorder,
},
assetIconContainer: {
width: 52,
height: 52,
borderRadius: 26,
backgroundColor: `${colors.vault.primary}15`,
justifyContent: 'center',
alignItems: 'center',
marginRight: spacing.base,
},
assetInfo: {
flex: 1,
},
assetType: {
fontSize: typography.fontSize.xs,
color: colors.vault.textSecondary,
textTransform: 'uppercase',
letterSpacing: 1,
marginBottom: 2,
fontWeight: '600',
},
assetLabel: {
fontSize: typography.fontSize.base,
color: colors.vault.text,
fontWeight: '600',
marginBottom: 4,
},
assetMetaRow: {
flexDirection: 'row',
alignItems: 'center',
gap: spacing.xs,
},
assetMeta: {
fontSize: typography.fontSize.xs,
color: colors.vault.textSecondary,
},
encryptedBadge: {
width: 36,
height: 36,
borderRadius: 18,
backgroundColor: colors.vault.success,
justifyContent: 'center',
alignItems: 'center',
},
addButton: {
position: 'absolute',
bottom: 100,
left: spacing.lg,
right: spacing.lg,
borderRadius: borderRadius.lg,
overflow: 'hidden',
},
addButtonGradient: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingVertical: spacing.md,
gap: spacing.sm,
},
addButtonText: {
fontSize: typography.fontSize.base,
color: colors.vault.background,
fontWeight: '700',
},
successToast: {
position: 'absolute',
bottom: 170,
left: spacing.lg,
right: spacing.lg,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: colors.vault.success,
paddingVertical: spacing.md,
borderRadius: borderRadius.lg,
gap: spacing.sm,
},
successText: {
fontSize: typography.fontSize.base,
color: '#fff',
fontWeight: '600',
},
modalOverlay: {
flex: 1,
backgroundColor: 'rgba(26, 58, 74, 0.8)',
justifyContent: 'flex-end',
},
modalContent: {
backgroundColor: colors.nautical.cream,
borderTopLeftRadius: borderRadius.xxl,
borderTopRightRadius: borderRadius.xxl,
padding: spacing.lg,
paddingBottom: spacing.xxl,
},
modalHandle: {
width: 40,
height: 4,
backgroundColor: colors.nautical.lightMint,
borderRadius: 2,
alignSelf: 'center',
marginBottom: spacing.lg,
},
modalHeader: {
flexDirection: 'row',
alignItems: 'center',
gap: spacing.sm,
marginBottom: spacing.lg,
},
modalTitle: {
fontSize: typography.fontSize.lg,
fontWeight: '600',
color: colors.nautical.navy,
},
stepRow: {
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: spacing.lg,
},
stepItem: {
alignItems: 'center',
flex: 1,
},
stepCircle: {
width: 28,
height: 28,
borderRadius: 14,
borderWidth: 1,
borderColor: colors.nautical.lightMint,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: colors.nautical.paleAqua,
},
stepCircleActive: {
borderColor: colors.nautical.teal,
backgroundColor: colors.nautical.lightMint,
},
stepCircleDone: {
borderColor: colors.nautical.teal,
backgroundColor: colors.nautical.teal,
},
stepNumber: {
fontSize: typography.fontSize.xs,
color: colors.nautical.sage,
fontWeight: '600',
},
stepNumberActive: {
color: colors.nautical.teal,
},
stepNumberDone: {
color: colors.nautical.cream,
},
stepLabel: {
fontSize: typography.fontSize.xs,
color: colors.nautical.sage,
marginTop: spacing.xs,
},
stepLabelActive: {
color: colors.nautical.teal,
fontWeight: '600',
},
modalLabel: {
fontSize: typography.fontSize.xs,
color: colors.nautical.sage,
marginBottom: spacing.sm,
letterSpacing: typography.letterSpacing.wider,
fontWeight: '600',
},
methodRow: {
flexDirection: 'row',
gap: spacing.sm,
marginBottom: spacing.lg,
},
methodButton: {
flex: 1,
paddingVertical: spacing.sm,
borderRadius: borderRadius.lg,
alignItems: 'center',
gap: spacing.xs,
backgroundColor: colors.nautical.paleAqua,
borderWidth: 1,
borderColor: colors.nautical.lightMint,
},
methodButtonActive: {
borderColor: colors.nautical.teal,
backgroundColor: colors.nautical.lightMint,
},
methodLabel: {
fontSize: typography.fontSize.sm,
color: colors.nautical.sage,
fontWeight: '600',
},
methodLabelActive: {
color: colors.nautical.teal,
},
loginButton: {
marginTop: spacing.sm,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
gap: spacing.sm,
backgroundColor: colors.nautical.teal,
paddingVertical: spacing.sm,
borderRadius: borderRadius.full,
},
loginButtonText: {
color: colors.nautical.cream,
fontSize: typography.fontSize.sm,
fontWeight: '700',
letterSpacing: 0.6,
},
typeScroll: {
marginBottom: spacing.lg,
},
typeScrollContent: {
gap: spacing.sm,
},
typeButton: {
alignItems: 'center',
paddingVertical: spacing.sm,
paddingHorizontal: spacing.base,
borderRadius: borderRadius.lg,
backgroundColor: colors.nautical.paleAqua,
borderWidth: 2,
borderColor: 'transparent',
minWidth: 100,
},
typeButtonActive: {
borderColor: colors.nautical.teal,
backgroundColor: colors.nautical.lightMint,
},
typeIconContainer: {
width: 44,
height: 44,
borderRadius: 22,
backgroundColor: colors.nautical.cream,
justifyContent: 'center',
alignItems: 'center',
marginBottom: spacing.xs,
},
typeIconContainerActive: {
backgroundColor: `${colors.nautical.teal}15`,
},
typeButtonLabel: {
fontSize: typography.fontSize.xs,
color: colors.nautical.sage,
textAlign: 'center',
fontWeight: '500',
},
typeButtonLabelActive: {
color: colors.nautical.teal,
fontWeight: '600',
},
input: {
backgroundColor: colors.nautical.paleAqua,
borderRadius: borderRadius.lg,
padding: spacing.base,
fontSize: typography.fontSize.base,
color: colors.nautical.navy,
marginBottom: spacing.md,
borderWidth: 1,
borderColor: colors.nautical.lightMint,
},
inputMultiline: {
minHeight: 120,
paddingTop: spacing.base,
},
encryptionNote: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.nautical.lightMint,
borderRadius: borderRadius.lg,
padding: spacing.md,
marginBottom: spacing.lg,
gap: spacing.sm,
},
encryptionNoteText: {
flex: 1,
fontSize: typography.fontSize.sm,
color: colors.nautical.teal,
lineHeight: typography.fontSize.sm * 1.4,
},
addErrorBox: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: 'rgba(194, 65, 12, 0.12)',
borderRadius: borderRadius.lg,
padding: spacing.md,
marginBottom: spacing.md,
gap: spacing.sm,
borderWidth: 1,
borderColor: 'rgba(194, 65, 12, 0.3)',
},
addErrorText: {
flex: 1,
fontSize: typography.fontSize.sm,
color: '#c2410c',
lineHeight: typography.fontSize.sm * 1.4,
},
modalButtons: {
flexDirection: 'row',
gap: spacing.md,
},
cancelButton: {
flex: 1,
paddingVertical: spacing.md,
borderRadius: borderRadius.lg,
backgroundColor: colors.nautical.paleAqua,
alignItems: 'center',
borderWidth: 1,
borderColor: colors.nautical.lightMint,
},
cancelButtonText: {
fontSize: typography.fontSize.base,
color: colors.nautical.sage,
fontWeight: '600',
},
confirmButton: {
flex: 1,
borderRadius: borderRadius.lg,
overflow: 'hidden',
},
confirmButtonGradient: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
paddingVertical: spacing.md,
gap: spacing.sm,
},
confirmButtonGradientDisabled: {
opacity: 0.5,
},
confirmButtonText: {
fontSize: typography.fontSize.base,
color: '#fff',
fontWeight: '600',
},
verifyCard: {
backgroundColor: colors.nautical.paleAqua,
borderRadius: borderRadius.lg,
padding: spacing.base,
marginBottom: spacing.md,
borderWidth: 1,
borderColor: colors.nautical.lightMint,
gap: spacing.sm,
},
verifyRow: {
flexDirection: 'row',
alignItems: 'center',
gap: spacing.sm,
},
verifyText: {
color: colors.nautical.navy,
fontSize: typography.fontSize.sm,
},
verifyButton: {
alignSelf: 'flex-start',
paddingHorizontal: spacing.md,
paddingVertical: spacing.sm,
borderRadius: borderRadius.full,
backgroundColor: colors.nautical.teal,
},
verifyButtonDone: {
backgroundColor: colors.nautical.sage,
},
verifyButtonText: {
color: colors.nautical.cream,
fontWeight: '700',
},
rehearsalRow: {
flexDirection: 'row',
alignItems: 'center',
gap: spacing.sm,
marginBottom: spacing.md,
},
rehearsalText: {
color: colors.nautical.navy,
fontSize: typography.fontSize.sm,
flex: 1,
},
detailContainer: {
flex: 1,
backgroundColor: colors.vault.background,
},
detailGradient: {
flex: 1,
},
detailSafeArea: {
flex: 1,
},
detailHeader: {
paddingHorizontal: spacing.lg,
paddingTop: spacing.lg,
paddingBottom: spacing.md,
flexDirection: 'row',
alignItems: 'center',
gap: spacing.sm,
},
detailBackButton: {
width: 36,
height: 36,
borderRadius: 18,
backgroundColor: 'rgba(255, 255, 255, 0.08)',
alignItems: 'center',
justifyContent: 'center',
},
detailTitle: {
flex: 1,
fontSize: typography.fontSize.lg,
color: colors.vault.text,
fontWeight: '700',
letterSpacing: typography.letterSpacing.wide,
},
riskBadge: {
backgroundColor: `${colors.vault.warning}25`,
borderRadius: borderRadius.full,
paddingHorizontal: spacing.sm,
paddingVertical: 4,
},
riskBadgeText: {
fontSize: typography.fontSize.xs,
color: colors.vault.warning,
fontWeight: '700',
letterSpacing: 1,
},
detailScroll: {
flex: 1,
},
detailContent: {
padding: spacing.lg,
paddingTop: spacing.sm,
gap: spacing.lg,
},
detailHeroCard: {
backgroundColor: colors.vault.cardBackground,
borderRadius: borderRadius.xl,
padding: spacing.lg,
borderWidth: 1,
borderColor: colors.vault.cardBorder,
flexDirection: 'row',
gap: spacing.base,
alignItems: 'center',
},
detailHeroIcon: {
width: 54,
height: 54,
borderRadius: 27,
backgroundColor: 'rgba(184, 224, 229, 0.15)',
alignItems: 'center',
justifyContent: 'center',
},
detailHeroInfo: {
flex: 1,
},
detailHeroLabel: {
fontSize: typography.fontSize.xs,
color: colors.vault.textSecondary,
letterSpacing: 1,
fontWeight: '600',
marginBottom: 4,
},
detailHeroTitle: {
fontSize: typography.fontSize.lg,
color: colors.vault.text,
fontWeight: '700',
marginBottom: 4,
},
detailHeroSubtitle: {
fontSize: typography.fontSize.sm,
color: colors.vault.textSecondary,
},
metaGrid: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: spacing.md,
},
metaCard: {
width: '48%',
backgroundColor: 'rgba(255, 255, 255, 0.05)',
borderRadius: borderRadius.lg,
padding: spacing.base,
borderWidth: 1,
borderColor: 'rgba(255, 255, 255, 0.08)',
},
metaLabel: {
fontSize: typography.fontSize.xs,
color: colors.vault.textSecondary,
letterSpacing: 1,
marginBottom: spacing.xs,
fontWeight: '600',
},
metaValue: {
fontSize: typography.fontSize.sm,
color: colors.vault.text,
fontWeight: '600',
},
actionGroup: {
gap: spacing.sm,
},
sectionTitle: {
fontSize: typography.fontSize.xs,
color: colors.vault.textSecondary,
letterSpacing: 1,
fontWeight: '700',
},
actionRow: {
flexDirection: 'row',
alignItems: 'center',
gap: spacing.sm,
paddingVertical: spacing.sm,
paddingHorizontal: spacing.md,
borderRadius: borderRadius.lg,
backgroundColor: 'rgba(255, 255, 255, 0.06)',
borderWidth: 1,
borderColor: 'rgba(255, 255, 255, 0.08)',
},
actionText: {
color: colors.vault.text,
fontSize: typography.fontSize.base,
fontWeight: '600',
},
guardCard: {
backgroundColor: 'rgba(229, 115, 115, 0.12)',
borderRadius: borderRadius.lg,
padding: spacing.base,
gap: spacing.sm,
borderWidth: 1,
borderColor: 'rgba(229, 115, 115, 0.3)',
},
guardHeader: {
flexDirection: 'row',
alignItems: 'center',
gap: spacing.sm,
},
guardTitle: {
fontSize: typography.fontSize.base,
color: colors.vault.warning,
fontWeight: '700',
},
guardText: {
fontSize: typography.fontSize.sm,
color: colors.vault.textSecondary,
lineHeight: typography.fontSize.sm * 1.5,
},
guardButton: {
alignSelf: 'flex-start',
backgroundColor: colors.vault.warning,
paddingHorizontal: spacing.md,
paddingVertical: spacing.sm,
borderRadius: borderRadius.full,
},
guardButtonText: {
color: colors.vault.text,
fontWeight: '700',
letterSpacing: 0.6,
},
previewCard: {
backgroundColor: colors.vault.cardBackground,
borderRadius: borderRadius.lg,
padding: spacing.base,
borderWidth: 1,
borderColor: colors.vault.cardBorder,
},
previewLabel: {
fontSize: typography.fontSize.xs,
color: colors.vault.textSecondary,
letterSpacing: 1,
marginBottom: spacing.xs,
fontWeight: '600',
},
previewValue: {
fontFamily: typography.fontFamily.mono,
color: colors.vault.text,
fontSize: typography.fontSize.base,
letterSpacing: 0.6,
},
warningCard: {
flexDirection: 'row',
alignItems: 'flex-start',
gap: spacing.sm,
backgroundColor: 'rgba(26, 58, 74, 0.6)',
borderRadius: borderRadius.lg,
padding: spacing.base,
borderWidth: 1,
borderColor: 'rgba(229, 115, 115, 0.35)',
},
warningText: {
flex: 1,
color: colors.vault.textSecondary,
fontSize: typography.fontSize.sm,
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',
},
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: {
minWidth: '30%',
paddingHorizontal: spacing.sm,
paddingVertical: spacing.sm,
borderRadius: borderRadius.lg,
borderWidth: 1,
borderColor: colors.sentinel.cardBorder,
backgroundColor: 'transparent',
},
wordChipSelected: {
backgroundColor: colors.sentinel.primary,
borderColor: colors.sentinel.primary,
},
wordChipIndex: {
fontSize: typography.fontSize.xs,
color: colors.sentinel.textSecondary,
marginBottom: 2,
},
wordChipText: {
color: colors.sentinel.text,
fontSize: typography.fontSize.xs,
fontWeight: '600',
},
wordChipTextSelected: {
color: colors.nautical.cream,
},
replacePanel: {
backgroundColor: colors.sentinel.cardBackground,
borderRadius: borderRadius.lg,
padding: spacing.md,
borderWidth: 1,
borderColor: colors.sentinel.cardBorder,
marginBottom: spacing.lg,
},
replaceTitle: {
fontSize: typography.fontSize.sm,
color: colors.sentinel.text,
fontWeight: '600',
marginBottom: spacing.sm,
},
replaceInput: {
height: 40,
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,
},
replaceList: {
maxHeight: 160,
marginBottom: spacing.sm,
},
replaceOption: {
paddingVertical: spacing.xs,
paddingHorizontal: spacing.sm,
borderRadius: borderRadius.full,
borderWidth: 1,
borderColor: colors.sentinel.cardBorder,
marginBottom: spacing.xs,
},
replaceOptionText: {
fontSize: typography.fontSize.xs,
color: colors.sentinel.text,
fontWeight: '600',
},
replaceCancel: {
alignSelf: 'flex-end',
paddingHorizontal: spacing.md,
paddingVertical: spacing.xs,
borderRadius: borderRadius.full,
borderWidth: 1,
borderColor: colors.sentinel.cardBorder,
},
replaceCancelText: {
fontSize: typography.fontSize.xs,
color: colors.sentinel.textSecondary,
fontWeight: '600',
letterSpacing: typography.letterSpacing.wide,
},
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.xs,
},
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,
},
});