3100 lines
99 KiB
TypeScript
3100 lines
99 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';
|
|
import { getVaultStorageKeys, DEBUG_MODE } from '../config';
|
|
import { mnemonicToEntropy, splitSecret, serializeShare } from '../utils/sss';
|
|
import { storageService } from '../services/storage.service';
|
|
import { SentinelVault } from '@/utils/crypto_core';
|
|
|
|
// 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;
|
|
};
|
|
|
|
|
|
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,
|
|
deleteAsset: deleteVaultAsset,
|
|
assignAsset: assignVaultAsset,
|
|
isSealing,
|
|
createError: addError,
|
|
clearCreateError: clearAddError,
|
|
} = useVaultAssets(isUnlocked);
|
|
const [showAddModal, setShowAddModal] = useState(false);
|
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
|
const [isDeleting, setIsDeleting] = 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 [showAssignModal, setShowAssignModal] = useState(false);
|
|
const [showAssignErrorModal, setShowAssignErrorModal] = useState(false);
|
|
const [assignErrorMessage, setAssignErrorMessage] = useState('');
|
|
const [isAssigning, setIsAssigning] = useState(false);
|
|
const [heirEmail, setHeirEmail] = useState('');
|
|
const [showLegacyAssignCta, setShowLegacyAssignCta] = useState(false);
|
|
const [hasS0, setHasS0] = useState<boolean | null>(null);
|
|
const [backupContent, setBackupContent] = useState<string | null>(null);
|
|
const [isFetchingBackup, setIsFetchingBackup] = 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 vaultKeys = React.useMemo(() => getVaultStorageKeys(user?.id ?? null), [user?.id]);
|
|
const [isCapturing, setIsCapturing] = useState(false);
|
|
const [treasureContent, setTreasureContent] = useState('');
|
|
const mnemonicRef = useRef<View>(null);
|
|
const progressTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
|
|
// Detect S0 (TEE/SE) for current user: if present, later open shows biometric only; if not, mnemonic flow
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
AsyncStorage.getItem(vaultKeys.SHARE_DEVICE)
|
|
.then((v) => {
|
|
if (!cancelled) setHasS0(!!v);
|
|
})
|
|
.catch(() => {
|
|
if (!cancelled) setHasS0(false);
|
|
});
|
|
return () => { cancelled = true; };
|
|
}, [vaultKeys.SHARE_DEVICE]);
|
|
|
|
// Only when S0 exists and vault not unlocked: show biometric after short delay.
|
|
// When hasS0 is false or null, never show biometric — go straight to mnemonic flow.
|
|
useEffect(() => {
|
|
if (hasS0 !== true) {
|
|
setShowBiometric(false);
|
|
return;
|
|
}
|
|
if (isUnlocked) return;
|
|
const timer = setTimeout(() => setShowBiometric(true), 100);
|
|
return () => clearTimeout(timer);
|
|
}, [isUnlocked, hasS0]);
|
|
|
|
useEffect(() => {
|
|
if (isUnlocked) {
|
|
Animated.timing(fadeAnim, {
|
|
toValue: 1,
|
|
duration: 200,
|
|
useNativeDriver: true,
|
|
}).start();
|
|
}
|
|
}, [isUnlocked]);
|
|
|
|
useEffect(() => {
|
|
if (!isUnlocked) {
|
|
Animated.loop(
|
|
Animated.sequence([
|
|
Animated.timing(pulseAnim, {
|
|
toValue: 1.05,
|
|
duration: 500,
|
|
useNativeDriver: true,
|
|
}),
|
|
Animated.timing(pulseAnim, {
|
|
toValue: 1,
|
|
duration: 500,
|
|
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(vaultKeys.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
|
|
finishMnemonicThenShowBiometric();
|
|
};
|
|
|
|
const handleSubmitAssignment = () => {
|
|
// Placeholder for submitting assignment to API
|
|
finishMnemonicThenShowBiometric();
|
|
};
|
|
|
|
const handleHeirYes = () => {
|
|
finishMnemonicThenShowBiometric();
|
|
setShowLegacyAssignCta(true);
|
|
};
|
|
|
|
const handleHeirNo = () => {
|
|
finishMnemonicThenShowBiometric();
|
|
setShowLegacyAssignCta(true);
|
|
};
|
|
|
|
/** After mnemonic flow: persist S0 (simulated TEE/SE via AsyncStorage), then show biometric; unlock on success */
|
|
const finishMnemonicThenShowBiometric = async () => {
|
|
try {
|
|
const wordList = bip39.wordlists.english;
|
|
const entropy = mnemonicToEntropy(mnemonicWords, wordList);
|
|
const shares = splitSecret(entropy);
|
|
const s0 = shares[0]; // device share (S0)
|
|
const s1 = shares[1]; // server share (S1)
|
|
const s2 = shares[2]; // heir share (S2)
|
|
// S0 is stored in AsyncStorage under user-scoped key — app-level storage, not hardware TEE/SE
|
|
const vault = new SentinelVault()
|
|
|
|
const aes_key = await vault.deriveKey(mnemonicWords.join(' '))
|
|
|
|
await AsyncStorage.setItem(vaultKeys.SHARE_DEVICE, serializeShare(s0));
|
|
await AsyncStorage.setItem(vaultKeys.SHARE_SERVER, serializeShare(s1));
|
|
await AsyncStorage.setItem(vaultKeys.INITIALIZED, '1');
|
|
await AsyncStorage.setItem(vaultKeys.AES_KEY, aes_key.toString('hex'));
|
|
await AsyncStorage.setItem(vaultKeys.SHARE_HEIR, serializeShare(s2));
|
|
|
|
setHasS0(true);
|
|
setShowMnemonic(false);
|
|
setShowBiometric(true);
|
|
} catch (e) {
|
|
if (__DEV__) console.warn('finishMnemonicThenShowBiometric', e);
|
|
setShowMnemonic(false);
|
|
setShowBiometric(true);
|
|
}
|
|
};
|
|
|
|
const handleBiometricSuccess = () => {
|
|
setShowBiometric(false);
|
|
setIsUnlocked(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);
|
|
|
|
if (DEBUG_MODE) {
|
|
console.log('[DEBUG] Vault Asset Details:', JSON.stringify(asset.rawData, null, 2));
|
|
}
|
|
};
|
|
|
|
const handleCloseDetail = () => {
|
|
setShowDetail(false);
|
|
setSelectedAsset(null);
|
|
setShowKeyPreview(false);
|
|
setShowGuardedBiometric(false);
|
|
setBackupContent(null);
|
|
};
|
|
|
|
const handleFetchBackup = async () => {
|
|
if (!selectedAsset || !user?.id) return;
|
|
|
|
setIsFetchingBackup(true);
|
|
try {
|
|
const content = await storageService.getAssetBackup(Number(selectedAsset.id), user.id);
|
|
if (content) {
|
|
setBackupContent(content);
|
|
} else {
|
|
if (typeof Alert !== 'undefined' && Alert.alert) {
|
|
Alert.alert('No Backup Found', 'No local plaintext backup found for this treasure.');
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Fetch backup error:', error);
|
|
if (typeof Alert !== 'undefined' && Alert.alert) {
|
|
Alert.alert('Error', 'Failed to retrieve local backup.');
|
|
}
|
|
} finally {
|
|
setIsFetchingBackup(false);
|
|
}
|
|
};
|
|
|
|
const handleDeleteAsset = async () => {
|
|
if (!selectedAsset || isDeleting) return;
|
|
|
|
setIsDeleting(true);
|
|
try {
|
|
const result = await deleteVaultAsset(Number(selectedAsset.id));
|
|
if (result.success) {
|
|
setShowDeleteConfirm(false);
|
|
handleCloseDetail();
|
|
if (typeof Alert !== 'undefined' && Alert.alert) {
|
|
Alert.alert('Success', 'Treasure removed from the vault.');
|
|
}
|
|
} else if (result.isUnauthorized) {
|
|
setShowDeleteConfirm(false);
|
|
handleCloseDetail();
|
|
if (typeof Alert !== 'undefined' && Alert.alert) {
|
|
Alert.alert('Unauthorized', 'Your session has expired. Please sign in again.');
|
|
}
|
|
} else if (result.error && typeof Alert !== 'undefined' && Alert.alert) {
|
|
Alert.alert('Failed', result.error);
|
|
}
|
|
} catch (error) {
|
|
console.error('Delete error:', error);
|
|
if (typeof Alert !== 'undefined' && Alert.alert) {
|
|
Alert.alert('Error', 'An unexpected error occurred during deletion.');
|
|
}
|
|
} finally {
|
|
setIsDeleting(false);
|
|
}
|
|
};
|
|
|
|
const handleAssignHeir = async () => {
|
|
if (!selectedAsset || isAssigning) return;
|
|
if (!heirEmail.trim() || !heirEmail.includes('@')) {
|
|
Alert.alert('Invalid Email', 'Please enter a valid email address.');
|
|
return;
|
|
}
|
|
|
|
setIsAssigning(true);
|
|
try {
|
|
const result = await assignVaultAsset(Number(selectedAsset.id), heirEmail.trim());
|
|
if (result.success) {
|
|
setShowAssignModal(false);
|
|
setHeirEmail('');
|
|
Alert.alert('Success', `Asset assigned to ${heirEmail.trim()}`);
|
|
} else if (result.isUnauthorized) {
|
|
setShowAssignModal(false);
|
|
if (typeof Alert !== 'undefined' && Alert.alert) {
|
|
Alert.alert('Unauthorized', 'Your session has expired. Please sign in again.');
|
|
}
|
|
} else if (result.error) {
|
|
setAssignErrorMessage(result.error);
|
|
setShowAssignErrorModal(true);
|
|
}
|
|
} catch (error) {
|
|
console.error('Assign error:', error);
|
|
setAssignErrorMessage('An unexpected error occurred during assignment.');
|
|
setShowAssignErrorModal(true);
|
|
} finally {
|
|
setIsAssigning(false);
|
|
}
|
|
};
|
|
|
|
const handleGuardedAccess = () => {
|
|
setShowGuardedBiometric(true);
|
|
};
|
|
|
|
const handleGuardedSuccess = () => {
|
|
setShowGuardedBiometric(false);
|
|
setShowKeyPreview(true);
|
|
handleFetchBackup();
|
|
};
|
|
|
|
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={hasS0 ? () => setShowBiometric(true) : handleUnlock}
|
|
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={hasS0 ? 'finger-print' : 'key'} size={20} color={colors.vault.background} />
|
|
<Text style={styles.unlockButtonText}>
|
|
{hasS0 === true ? "Captain's Verification" : hasS0 === false ? 'Enter Vault' : 'Loading…'}
|
|
</Text>
|
|
</LinearGradient>
|
|
</TouchableOpacity>
|
|
</View>
|
|
</SafeAreaView>
|
|
</LinearGradient>
|
|
|
|
<BiometricModal
|
|
visible={hasS0 === true && showBiometric}
|
|
onSuccess={handleBiometricSuccess}
|
|
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>
|
|
{selectedAsset?.heirEmail ? (
|
|
<View style={[styles.metaCard, { width: '100%' }]}>
|
|
<Text style={styles.metaLabel}>ASSIGNED HEIR</Text>
|
|
<Text style={styles.metaValue}>{selectedAsset.heirEmail}</Text>
|
|
</View>
|
|
) : null}
|
|
</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.7}>
|
|
<Ionicons name="notifications-outline" size={18} color={colors.vault.text} />
|
|
<Text style={styles.actionText}>Reset Sentinel Timer</Text>
|
|
</TouchableOpacity>
|
|
|
|
<TouchableOpacity
|
|
style={[styles.actionRow, styles.assignActionRow]}
|
|
onPress={() => setShowAssignModal(true)}
|
|
activeOpacity={0.7}
|
|
>
|
|
<Ionicons name="person-add-outline" size={18} color={colors.vault.text} />
|
|
<Text style={styles.actionText}>
|
|
{selectedAsset?.heirEmail ? 'Change Heir' : 'Assign Heir'}
|
|
</Text>
|
|
</TouchableOpacity>
|
|
|
|
<TouchableOpacity
|
|
style={[styles.actionRow, styles.deleteActionRow]}
|
|
onPress={() => setShowDeleteConfirm(true)}
|
|
activeOpacity={0.7}
|
|
>
|
|
<Feather name="trash-2" size={18} color={colors.vault.warning} />
|
|
<Text style={[styles.actionText, styles.deleteActionText]}>Delete Treasure</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}>LOCAL PLAINTEXT BACKUP</Text>
|
|
<Text style={styles.previewValue}>
|
|
{isFetchingBackup ? 'Fetching content...' : (backupContent || 'No local backup found for this treasure')}
|
|
</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>
|
|
|
|
{/* Delete Confirmation Modal */}
|
|
<Modal
|
|
visible={showDeleteConfirm}
|
|
animationType="fade"
|
|
transparent
|
|
onRequestClose={() => setShowDeleteConfirm(false)}
|
|
>
|
|
<View style={styles.modalOverlay}>
|
|
<TouchableOpacity
|
|
style={styles.modalOverlayDismiss}
|
|
activeOpacity={1}
|
|
onPress={() => setShowDeleteConfirm(false)}
|
|
/>
|
|
<View style={styles.deleteConfirmContent}>
|
|
<View style={styles.deleteIconContainer}>
|
|
<Feather name="alert-triangle" size={32} color={colors.vault.warning} />
|
|
</View>
|
|
<Text style={styles.deleteTitle}>Remove Treasure?</Text>
|
|
<Text style={styles.deleteMessage}>
|
|
This action cannot be undone. The treasure will be permanently shredded from the deep vault.
|
|
</Text>
|
|
<View style={styles.deleteButtons}>
|
|
<TouchableOpacity
|
|
style={styles.deleteCancelButton}
|
|
onPress={() => setShowDeleteConfirm(false)}
|
|
disabled={isDeleting}
|
|
>
|
|
<Text style={styles.deleteCancelText}>Cancel</Text>
|
|
</TouchableOpacity>
|
|
<TouchableOpacity
|
|
style={styles.deleteConfirmButton}
|
|
onPress={handleDeleteAsset}
|
|
disabled={isDeleting}
|
|
>
|
|
{isDeleting ? (
|
|
<Text style={styles.deleteConfirmText}>Shredding...</Text>
|
|
) : (
|
|
<Text style={styles.deleteConfirmText}>Confirm Delete</Text>
|
|
)}
|
|
</TouchableOpacity>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
</Modal>
|
|
|
|
{/* Assign Heir Modal */}
|
|
<Modal
|
|
visible={showAssignModal}
|
|
animationType="slide"
|
|
transparent
|
|
onRequestClose={() => setShowAssignModal(false)}
|
|
>
|
|
<View style={styles.modalOverlay}>
|
|
<TouchableOpacity
|
|
style={styles.modalOverlayDismiss}
|
|
activeOpacity={1}
|
|
onPress={() => setShowAssignModal(false)}
|
|
/>
|
|
<KeyboardAvoidingView
|
|
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
|
style={styles.assignModalContainer}
|
|
>
|
|
<View style={styles.assignModalContent}>
|
|
<View style={styles.assignModalHeader}>
|
|
<View style={styles.assignIconGlow}>
|
|
<Ionicons name="person-add" size={32} color={colors.vault.primary} />
|
|
</View>
|
|
<Text style={styles.assignTitle}>Designate Heir</Text>
|
|
<Text style={styles.assignSubtitle}>
|
|
Enter the email address of the person who should inherit this treasure.
|
|
</Text>
|
|
</View>
|
|
|
|
<View style={styles.assignInputContainer}>
|
|
<Text style={styles.inputLabel}>HEIR EMAIL ADDRESS</Text>
|
|
<TextInput
|
|
style={styles.assignInput}
|
|
value={heirEmail}
|
|
onChangeText={setHeirEmail}
|
|
placeholder="name@example.com"
|
|
placeholderTextColor={colors.vault.textSecondary}
|
|
keyboardType="email-address"
|
|
autoCapitalize="none"
|
|
autoCorrect={false}
|
|
/>
|
|
</View>
|
|
|
|
<View style={styles.assignButtons}>
|
|
<TouchableOpacity
|
|
style={styles.assignCancelButton}
|
|
onPress={() => {
|
|
setShowAssignModal(false);
|
|
setHeirEmail('');
|
|
}}
|
|
disabled={isAssigning}
|
|
>
|
|
<Text style={styles.assignCancelText}>Cancel</Text>
|
|
</TouchableOpacity>
|
|
<TouchableOpacity
|
|
style={styles.assignConfirmButton}
|
|
onPress={handleAssignHeir}
|
|
disabled={isAssigning || !heirEmail.trim()}
|
|
>
|
|
<LinearGradient
|
|
colors={[colors.vault.primary, colors.vault.secondary]}
|
|
style={styles.assignConfirmGradient}
|
|
>
|
|
{isAssigning ? (
|
|
<Text style={styles.assignConfirmText}>Assigning...</Text>
|
|
) : (
|
|
<Text style={styles.assignConfirmText}>Confirm Heir</Text>
|
|
)}
|
|
</LinearGradient>
|
|
</TouchableOpacity>
|
|
</View>
|
|
</View>
|
|
</KeyboardAvoidingView>
|
|
</View>
|
|
</Modal>
|
|
|
|
{/* Assign Heir Error Modal */}
|
|
<Modal
|
|
visible={showAssignErrorModal}
|
|
animationType="fade"
|
|
transparent
|
|
onRequestClose={() => setShowAssignErrorModal(false)}
|
|
>
|
|
<View style={styles.modalOverlay}>
|
|
<TouchableOpacity
|
|
style={styles.modalOverlayDismiss}
|
|
activeOpacity={1}
|
|
onPress={() => setShowAssignErrorModal(false)}
|
|
/>
|
|
<View style={styles.errorModalContent}>
|
|
<View style={styles.errorIconContainer}>
|
|
<MaterialCommunityIcons name="alert-circle-outline" size={32} color={colors.vault.warning} />
|
|
</View>
|
|
<Text style={styles.errorTitle}>Assignment Failed</Text>
|
|
<Text style={styles.errorMessage}>{assignErrorMessage}</Text>
|
|
<TouchableOpacity
|
|
style={styles.errorCloseButton}
|
|
onPress={() => setShowAssignErrorModal(false)}
|
|
>
|
|
<Text style={styles.errorCloseButtonText}>Dismiss</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
</View>
|
|
</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',
|
|
},
|
|
modalOverlayDismiss: {
|
|
...StyleSheet.absoluteFillObject,
|
|
},
|
|
deleteConfirmContent: {
|
|
backgroundColor: colors.vault.cardBackground,
|
|
marginHorizontal: spacing.lg,
|
|
borderRadius: borderRadius.xl,
|
|
padding: spacing.xl,
|
|
borderWidth: 1,
|
|
borderColor: colors.vault.cardBorder,
|
|
alignItems: 'center',
|
|
marginBottom: spacing.xxl + spacing.lg,
|
|
},
|
|
deleteIconContainer: {
|
|
width: 64,
|
|
height: 64,
|
|
borderRadius: 32,
|
|
backgroundColor: `${colors.vault.warning}20`,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
marginBottom: spacing.lg,
|
|
},
|
|
deleteTitle: {
|
|
fontSize: typography.fontSize.lg,
|
|
color: colors.vault.text,
|
|
fontWeight: '700',
|
|
marginBottom: spacing.sm,
|
|
fontFamily: typography.fontFamily.serif,
|
|
},
|
|
deleteMessage: {
|
|
fontSize: typography.fontSize.sm,
|
|
color: colors.vault.textSecondary,
|
|
textAlign: 'center',
|
|
lineHeight: typography.fontSize.sm * 1.5,
|
|
marginBottom: spacing.xl,
|
|
},
|
|
deleteButtons: {
|
|
flexDirection: 'row',
|
|
gap: spacing.md,
|
|
width: '100%',
|
|
},
|
|
deleteCancelButton: {
|
|
flex: 1,
|
|
paddingVertical: spacing.md,
|
|
borderRadius: borderRadius.lg,
|
|
backgroundColor: 'rgba(255, 255, 255, 0.08)',
|
|
alignItems: 'center',
|
|
borderWidth: 1,
|
|
borderColor: 'rgba(255, 255, 255, 0.1)',
|
|
},
|
|
deleteCancelText: {
|
|
color: colors.vault.textSecondary,
|
|
fontSize: typography.fontSize.base,
|
|
fontWeight: '600',
|
|
},
|
|
deleteConfirmButton: {
|
|
flex: 2,
|
|
paddingVertical: spacing.md,
|
|
borderRadius: borderRadius.lg,
|
|
backgroundColor: colors.vault.warning,
|
|
alignItems: 'center',
|
|
},
|
|
deleteConfirmText: {
|
|
color: colors.vault.text,
|
|
fontSize: typography.fontSize.base,
|
|
fontWeight: '700',
|
|
},
|
|
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: {
|
|
fontWeight: '700',
|
|
letterSpacing: typography.letterSpacing.wide,
|
|
},
|
|
deleteActionRow: {
|
|
backgroundColor: 'rgba(229, 115, 115, 0.08)',
|
|
borderColor: 'rgba(229, 115, 115, 0.2)',
|
|
marginTop: spacing.sm,
|
|
},
|
|
deleteActionText: {
|
|
color: colors.vault.warning,
|
|
},
|
|
confirmModalOverlay: {
|
|
flex: 1,
|
|
backgroundColor: 'rgba(11, 20, 24, 0.85)',
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
padding: spacing.xl,
|
|
},
|
|
confirmModalContent: {
|
|
width: '100%',
|
|
backgroundColor: colors.vault.cardBackground,
|
|
borderRadius: borderRadius.xl,
|
|
padding: spacing.lg,
|
|
borderWidth: 1,
|
|
borderColor: colors.vault.cardBorder,
|
|
gap: spacing.md,
|
|
},
|
|
confirmModalTitle: {
|
|
fontSize: typography.fontSize.lg,
|
|
color: colors.vault.text,
|
|
fontWeight: '700',
|
|
textAlign: 'center',
|
|
},
|
|
confirmModalText: {
|
|
fontSize: typography.fontSize.base,
|
|
color: colors.vault.textSecondary,
|
|
textAlign: 'center',
|
|
lineHeight: 22,
|
|
},
|
|
confirmModalButtons: {
|
|
flexDirection: 'row',
|
|
gap: spacing.md,
|
|
marginTop: spacing.sm,
|
|
},
|
|
confirmModalButton: {
|
|
flex: 1,
|
|
paddingVertical: spacing.md,
|
|
borderRadius: borderRadius.lg,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
},
|
|
confirmCancelButton: {
|
|
backgroundColor: 'rgba(255, 255, 255, 0.05)',
|
|
borderWidth: 1,
|
|
borderColor: 'rgba(255, 255, 255, 0.1)',
|
|
},
|
|
confirmDeleteButton: {
|
|
backgroundColor: colors.vault.warning,
|
|
},
|
|
confirmCancelText: {
|
|
color: colors.vault.textSecondary,
|
|
},
|
|
assignActionRow: {
|
|
// Optional specific styling
|
|
},
|
|
assignModalContainer: {
|
|
backgroundColor: colors.vault.cardBackground,
|
|
marginHorizontal: spacing.lg,
|
|
borderRadius: borderRadius.xl,
|
|
borderWidth: 1,
|
|
borderColor: colors.vault.cardBorder,
|
|
overflow: 'hidden',
|
|
marginBottom: spacing.xxl,
|
|
},
|
|
assignModalContent: {
|
|
padding: spacing.xl,
|
|
},
|
|
assignModalHeader: {
|
|
alignItems: 'center',
|
|
marginBottom: spacing.xl,
|
|
},
|
|
assignIconGlow: {
|
|
width: 64,
|
|
height: 64,
|
|
borderRadius: 32,
|
|
backgroundColor: `${colors.vault.primary}15`,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
marginBottom: spacing.md,
|
|
},
|
|
assignTitle: {
|
|
fontSize: typography.fontSize.lg,
|
|
color: colors.vault.text,
|
|
fontWeight: '700',
|
|
marginBottom: spacing.xs,
|
|
fontFamily: typography.fontFamily.serif,
|
|
},
|
|
assignSubtitle: {
|
|
fontSize: typography.fontSize.sm,
|
|
color: colors.vault.textSecondary,
|
|
textAlign: 'center',
|
|
lineHeight: typography.fontSize.sm * 1.5,
|
|
},
|
|
assignInputContainer: {
|
|
marginBottom: spacing.xl,
|
|
},
|
|
inputLabel: {
|
|
fontSize: typography.fontSize.xs,
|
|
color: colors.vault.textSecondary,
|
|
fontWeight: '700',
|
|
letterSpacing: 1,
|
|
marginBottom: spacing.sm,
|
|
},
|
|
assignInput: {
|
|
backgroundColor: 'rgba(255, 255, 255, 0.05)',
|
|
borderRadius: borderRadius.lg,
|
|
padding: spacing.md,
|
|
fontSize: typography.fontSize.base,
|
|
color: colors.vault.text,
|
|
borderWidth: 1,
|
|
borderColor: 'rgba(255, 255, 255, 0.1)',
|
|
},
|
|
assignButtons: {
|
|
flexDirection: 'row',
|
|
gap: spacing.md,
|
|
},
|
|
assignCancelButton: {
|
|
flex: 1,
|
|
paddingVertical: spacing.md,
|
|
borderRadius: borderRadius.lg,
|
|
backgroundColor: 'rgba(255, 255, 255, 0.08)',
|
|
alignItems: 'center',
|
|
borderWidth: 1,
|
|
borderColor: 'rgba(255, 255, 255, 0.1)',
|
|
},
|
|
assignCancelText: {
|
|
color: colors.vault.textSecondary,
|
|
fontSize: typography.fontSize.base,
|
|
fontWeight: '600',
|
|
},
|
|
assignConfirmButton: {
|
|
flex: 2,
|
|
borderRadius: borderRadius.lg,
|
|
overflow: 'hidden',
|
|
},
|
|
assignConfirmGradient: {
|
|
paddingVertical: spacing.md,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
},
|
|
assignConfirmText: {
|
|
color: colors.vault.text,
|
|
fontSize: typography.fontSize.base,
|
|
fontWeight: '700',
|
|
},
|
|
errorModalContent: {
|
|
backgroundColor: colors.vault.cardBackground,
|
|
marginHorizontal: spacing.lg,
|
|
borderRadius: borderRadius.xl,
|
|
padding: spacing.xl,
|
|
borderWidth: 1,
|
|
borderColor: colors.vault.cardBorder,
|
|
alignItems: 'center',
|
|
marginBottom: spacing.xxl,
|
|
},
|
|
errorIconContainer: {
|
|
width: 64,
|
|
height: 64,
|
|
borderRadius: 32,
|
|
backgroundColor: `${colors.vault.warning}15`,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
marginBottom: spacing.lg,
|
|
},
|
|
errorTitle: {
|
|
fontSize: typography.fontSize.lg,
|
|
color: colors.vault.text,
|
|
fontWeight: '700',
|
|
marginBottom: spacing.sm,
|
|
fontFamily: typography.fontFamily.serif,
|
|
},
|
|
errorMessage: {
|
|
fontSize: typography.fontSize.sm,
|
|
color: colors.vault.textSecondary,
|
|
textAlign: 'center',
|
|
lineHeight: typography.fontSize.sm * 1.5,
|
|
marginBottom: spacing.xl,
|
|
},
|
|
errorCloseButton: {
|
|
width: '100%',
|
|
paddingVertical: spacing.md,
|
|
borderRadius: borderRadius.lg,
|
|
backgroundColor: colors.vault.warning,
|
|
alignItems: 'center',
|
|
},
|
|
errorCloseButtonText: {
|
|
color: colors.vault.text,
|
|
fontSize: typography.fontSize.base,
|
|
fontWeight: '700',
|
|
},
|
|
});
|