TODO: update vault.service.ts. Use MNEMONIC workflow to create real private_key_shard and content_inner_encrypted
2535 lines
80 KiB
TypeScript
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,
|
|
},
|
|
});
|