The vault notification mode1

This commit is contained in:
2026-01-31 22:58:51 -04:00
parent 56bb72aab8
commit d64a6557a8
7 changed files with 577 additions and 495 deletions

View File

@@ -6,6 +6,7 @@
*/ */
import React from 'react'; import React from 'react';
import { Buffer } from 'buffer';
import { StatusBar } from 'expo-status-bar'; import { StatusBar } from 'expo-status-bar';
import { NavigationContainer } from '@react-navigation/native'; import { NavigationContainer } from '@react-navigation/native';
import { GestureHandlerRootView } from 'react-native-gesture-handler'; import { GestureHandlerRootView } from 'react-native-gesture-handler';
@@ -15,6 +16,10 @@ import AuthNavigator from './src/navigation/AuthNavigator';
import { AuthProvider, useAuth } from './src/context/AuthContext'; import { AuthProvider, useAuth } from './src/context/AuthContext';
import { colors } from './src/theme/colors'; import { colors } from './src/theme/colors';
if (typeof globalThis !== 'undefined' && !globalThis.Buffer) {
globalThis.Buffer = Buffer;
}
/** /**
* Loading screen shown while restoring auth state * Loading screen shown while restoring auth state
*/ */

55
package-lock.json generated
View File

@@ -14,6 +14,8 @@
"@react-navigation/bottom-tabs": "^6.6.1", "@react-navigation/bottom-tabs": "^6.6.1",
"@react-navigation/native": "^6.1.18", "@react-navigation/native": "^6.1.18",
"@react-navigation/native-stack": "^6.11.0", "@react-navigation/native-stack": "^6.11.0",
"bip39": "^3.1.0",
"buffer": "^6.0.3",
"expo": "~52.0.0", "expo": "~52.0.0",
"expo-asset": "~11.0.5", "expo-asset": "~11.0.5",
"expo-constants": "~17.0.8", "expo-constants": "~17.0.8",
@@ -3180,6 +3182,18 @@
"@jridgewell/sourcemap-codec": "^1.4.14" "@jridgewell/sourcemap-codec": "^1.4.14"
} }
}, },
"node_modules/@noble/hashes": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
"integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
"license": "MIT",
"engines": {
"node": "^14.21.3 || >=16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@nodelib/fs.scandir": { "node_modules/@nodelib/fs.scandir": {
"version": "2.1.5", "version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -4420,6 +4434,15 @@
"node": ">=0.6" "node": ">=0.6"
} }
}, },
"node_modules/bip39": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/bip39/-/bip39-3.1.0.tgz",
"integrity": "sha512-c9kiwdk45Do5GL0vJMe7tS95VjCii65mYAH7DfWl3uW8AVzXKQVUm64i3hzVybBDMp9r7j9iNxR85+ul8MdN/A==",
"license": "ISC",
"dependencies": {
"@noble/hashes": "^1.2.0"
}
},
"node_modules/bplist-creator": { "node_modules/bplist-creator": {
"version": "0.0.7", "version": "0.0.7",
"resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.0.7.tgz", "resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.0.7.tgz",
@@ -4507,9 +4530,9 @@
} }
}, },
"node_modules/buffer": { "node_modules/buffer": {
"version": "5.7.1", "version": "6.0.3",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
"funding": [ "funding": [
{ {
"type": "github", "type": "github",
@@ -4527,7 +4550,7 @@
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"base64-js": "^1.3.1", "base64-js": "^1.3.1",
"ieee754": "^1.1.13" "ieee754": "^1.2.1"
} }
}, },
"node_modules/buffer-alloc": { "node_modules/buffer-alloc": {
@@ -11035,6 +11058,30 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/whatwg-url-without-unicode/node_modules/buffer": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"dependencies": {
"base64-js": "^1.3.1",
"ieee754": "^1.1.13"
}
},
"node_modules/whatwg-url-without-unicode/node_modules/webidl-conversions": { "node_modules/whatwg-url-without-unicode/node_modules/webidl-conversions": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz",

View File

@@ -15,6 +15,8 @@
"@react-navigation/bottom-tabs": "^6.6.1", "@react-navigation/bottom-tabs": "^6.6.1",
"@react-navigation/native": "^6.1.18", "@react-navigation/native": "^6.1.18",
"@react-navigation/native-stack": "^6.11.0", "@react-navigation/native-stack": "^6.11.0",
"bip39": "^3.1.0",
"buffer": "^6.0.3",
"expo": "~52.0.0", "expo": "~52.0.0",
"expo-asset": "~11.0.5", "expo-asset": "~11.0.5",
"expo-constants": "~17.0.8", "expo-constants": "~17.0.8",

View File

@@ -112,6 +112,7 @@ export const MOCK_CONFIG = {
USER: { USER: {
id: 999, id: 999,
username: 'MockCaptain', username: 'MockCaptain',
email: 'captain@sentinel.local',
public_key: 'mock_public_key', public_key: 'mock_public_key',
is_admin: true, is_admin: true,
guale: false, guale: false,

View File

@@ -348,7 +348,7 @@ export default function SentinelScreen() {
onRequestClose={() => setShowVault(false)} onRequestClose={() => setShowVault(false)}
> >
<View style={styles.vaultModalContainer}> <View style={styles.vaultModalContainer}>
<VaultScreen /> {showVault ? <VaultScreen /> : null}
<TouchableOpacity <TouchableOpacity
style={styles.vaultCloseButton} style={styles.vaultCloseButton}
onPress={() => setShowVault(false)} onPress={() => setShowVault(false)}

View File

@@ -19,9 +19,11 @@ import { LinearGradient } from 'expo-linear-gradient';
import { Ionicons, Feather, MaterialCommunityIcons, FontAwesome5 } from '@expo/vector-icons'; import { Ionicons, Feather, MaterialCommunityIcons, FontAwesome5 } from '@expo/vector-icons';
import { captureRef } from 'react-native-view-shot'; import { captureRef } from 'react-native-view-shot';
import AsyncStorage from '@react-native-async-storage/async-storage'; import AsyncStorage from '@react-native-async-storage/async-storage';
import * as bip39 from 'bip39';
import { colors, typography, spacing, borderRadius, shadows } from '../theme/colors'; import { colors, typography, spacing, borderRadius, shadows } from '../theme/colors';
import { VaultAsset, VaultAssetType, Heir } from '../types'; import { VaultAsset, VaultAssetType, Heir } from '../types';
import BiometricModal from '../components/common/BiometricModal'; import BiometricModal from '../components/common/BiometricModal';
import { useAuth } from '../context/AuthContext';
// Asset type configuration with nautical theme // Asset type configuration with nautical theme
const assetTypeConfig: Record<VaultAssetType, { icon: string; iconType: 'ionicons' | 'feather' | 'material' | 'fontawesome5'; label: string }> = { const assetTypeConfig: Record<VaultAssetType, { icon: string; iconType: 'ionicons' | 'feather' | 'material' | 'fontawesome5'; label: string }> = {
@@ -40,17 +42,6 @@ const accountProviderOptions = [
{ key: 'custom', label: 'Other', icon: 'shield-account', iconType: 'material' as const }, { key: 'custom', label: 'Other', icon: 'shield-account', iconType: 'material' as const },
]; ];
const MNEMONIC_WORDS = [
'anchor', 'harbor', 'compass', 'lighthouse', 'current', 'ocean', 'tide', 'voyage',
'keel', 'stern', 'bow', 'mast', 'sail', 'port', 'starboard', 'reef',
'signal', 'beacon', 'chart', 'helm', 'gale', 'calm', 'cove', 'isle',
'horizon', 'sextant', 'sound', 'drift', 'wake', 'mariner', 'pilot', 'fathom',
'buoy', 'lantern', 'harpoon', 'lagoon', 'bay', 'strait', 'riptide', 'foam',
'coral', 'pearl', 'trident', 'ebb', 'flow', 'vault', 'cipher', 'shroud',
'salt', 'wave', 'grotto', 'lagoon', 'storm', 'north', 'south', 'east',
'west', 'ember', 'cabin', 'signal', 'ledger', 'torch', 'sanctum', 'oath',
];
const initialHeirs: Heir[] = [ const initialHeirs: Heir[] = [
{ {
id: '1', id: '1',
@@ -84,14 +75,7 @@ const initialHeirs: Heir[] = [
}, },
]; ];
const generateMnemonic = (wordCount = 12) => { const generateMnemonic = () => bip39.generateMnemonic(128).split(' ');
const words: string[] = [];
for (let i = 0; i < wordCount; i += 1) {
const index = Math.floor(Math.random() * MNEMONIC_WORDS.length);
words.push(MNEMONIC_WORDS[index]);
}
return words;
};
const splitMnemonic = (words: string[]) => [ const splitMnemonic = (words: string[]) => [
words.slice(0, 4), words.slice(0, 4),
@@ -104,15 +88,6 @@ type HeirAssignment = {
heir: Heir; heir: Heir;
}; };
const shuffle = <T,>(items: T[]) => {
const next = [...items];
for (let i = next.length - 1; i > 0; i -= 1) {
const j = Math.floor(Math.random() * (i + 1));
[next[i], next[j]] = [next[j], next[i]];
}
return next;
};
// Mock data // Mock data
const initialAssets: VaultAsset[] = [ const initialAssets: VaultAsset[] = [
{ {
@@ -183,6 +158,7 @@ export default function VaultScreen() {
const [showAddBiometric, setShowAddBiometric] = useState(false); const [showAddBiometric, setShowAddBiometric] = useState(false);
const [accountProvider, setAccountProvider] = useState<'bank' | 'steam' | 'facebook' | 'custom'>('bank'); const [accountProvider, setAccountProvider] = useState<'bank' | 'steam' | 'facebook' | 'custom'>('bank');
const [showMnemonic, setShowMnemonic] = useState(false); const [showMnemonic, setShowMnemonic] = useState(false);
const [showLegacyAssignCta, setShowLegacyAssignCta] = useState(false);
const [mnemonicWords, setMnemonicWords] = useState<string[]>([]); const [mnemonicWords, setMnemonicWords] = useState<string[]>([]);
const [mnemonicParts, setMnemonicParts] = useState<string[][]>([]); const [mnemonicParts, setMnemonicParts] = useState<string[][]>([]);
const [mnemonicStep, setMnemonicStep] = useState<1 | 2 | 3 | 4 | 5>(1); const [mnemonicStep, setMnemonicStep] = useState<1 | 2 | 3 | 4 | 5>(1);
@@ -190,12 +166,11 @@ export default function VaultScreen() {
const [selectedHeir, setSelectedHeir] = useState<Heir | null>(null); const [selectedHeir, setSelectedHeir] = useState<Heir | null>(null);
const [selectedHeirAsset, setSelectedHeirAsset] = useState<VaultAsset | null>(null); const [selectedHeirAsset, setSelectedHeirAsset] = useState<VaultAsset | null>(null);
const [assignments, setAssignments] = useState<HeirAssignment[]>([]); const [assignments, setAssignments] = useState<HeirAssignment[]>([]);
const [wordOptions, setWordOptions] = useState<string[]>([]); const [replaceIndex, setReplaceIndex] = useState<number | null>(null);
const [selectedWords, setSelectedWords] = useState<string[]>([]); const [replaceQuery, setReplaceQuery] = useState('');
const [progressIndex, setProgressIndex] = useState(0); const [progressIndex, setProgressIndex] = useState(0);
const [progressAnim] = useState(new Animated.Value(0)); const [progressAnim] = useState(new Animated.Value(0));
const [showEmailForm, setShowEmailForm] = useState(false); const { user } = useAuth();
const [emailAddress, setEmailAddress] = useState('');
const [isCapturing, setIsCapturing] = useState(false); const [isCapturing, setIsCapturing] = useState(false);
const mnemonicRef = useRef<View>(null); const mnemonicRef = useRef<View>(null);
const progressTimerRef = useRef<ReturnType<typeof setInterval> | null>(null); const progressTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
@@ -242,22 +217,17 @@ export default function VaultScreen() {
setShowBiometric(false); setShowBiometric(false);
const words = generateMnemonic(); const words = generateMnemonic();
const parts = splitMnemonic(words); const parts = splitMnemonic(words);
const decoys = MNEMONIC_WORDS.filter((word) => !words.includes(word));
const options = shuffle([...words, ...decoys.slice(0, 12)]);
setMnemonicWords(words); setMnemonicWords(words);
setMnemonicParts(parts); setMnemonicParts(parts);
setWordOptions(options); setReplaceIndex(null);
setSelectedWords([]); setReplaceQuery('');
setMnemonicStep(1); setMnemonicStep(1);
setHeirStep('decision'); setHeirStep('decision');
setSelectedHeir(null); setSelectedHeir(null);
setSelectedHeirAsset(null); setSelectedHeirAsset(null);
setAssignments([]);
setProgressIndex(0); setProgressIndex(0);
progressAnim.setValue(0); progressAnim.setValue(0);
setShowMnemonic(true); setTimeout(() => setShowMnemonic(true), 200);
setShowEmailForm(false);
setEmailAddress('');
AsyncStorage.setItem('sentinel_mnemonic_part_local', parts[0].join(' ')).catch(() => { AsyncStorage.setItem('sentinel_mnemonic_part_local', parts[0].join(' ')).catch(() => {
// Best-effort local store; UI remains available // Best-effort local store; UI remains available
}); });
@@ -269,12 +239,20 @@ export default function VaultScreen() {
const uri = await captureRef(mnemonicRef, { const uri = await captureRef(mnemonicRef, {
format: 'png', format: 'png',
quality: 1, quality: 1,
result: 'tmpfile', 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({ await Share.share({
url: uri, url: uri,
message: 'Sentinel key backup', message: 'Sentinel key backup',
}); });
}
setMnemonicStep(4); setMnemonicStep(4);
} catch (error) { } catch (error) {
Alert.alert('Screenshot failed', 'Please try again or use email backup.'); Alert.alert('Screenshot failed', 'Please try again or use email backup.');
@@ -284,45 +262,19 @@ export default function VaultScreen() {
}; };
const handleEmailBackup = () => { const handleEmailBackup = () => {
setShowEmailForm(true); // Proceed immediately; email delivery is handled separately.
};
const toggleWord = (word: string) => {
if (selectedWords.includes(word)) {
setSelectedWords(selectedWords.filter((item) => item !== word));
return;
}
if (selectedWords.length >= 12) return;
setSelectedWords([...selectedWords, word]);
};
const handleConfirmSelection = () => {
if (selectedWords.length !== 12) {
Alert.alert('Select 12 words', 'Please select all 12 words in order.');
return;
}
setMnemonicStep(2);
};
const handleSendEmail = async () => {
const trimmed = emailAddress.trim();
if (!trimmed || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(trimmed)) {
Alert.alert('Invalid email', 'Please enter a valid email address.');
return;
}
const subject = encodeURIComponent('Sentinel Vault Recovery Key');
const body = encodeURIComponent(`Your 12-word mnemonic:\n${mnemonicWords.join(' ')}`);
const mailtoUrl = `mailto:${trimmed}?subject=${subject}&body=${body}`;
try {
await Linking.openURL(mailtoUrl);
setShowEmailForm(false);
setEmailAddress('');
setMnemonicStep(4); setMnemonicStep(4);
} catch (error) { setShowMnemonic(true);
Alert.alert('Email failed', 'Unable to open email client.'); };
}
const handleReplaceWord = (word: string) => {
if (replaceIndex === null) return;
const nextWords = [...mnemonicWords];
nextWords[replaceIndex] = word;
setMnemonicWords(nextWords);
setMnemonicParts(splitMnemonic(nextWords));
setReplaceIndex(null);
setReplaceQuery('');
}; };
useEffect(() => { useEffect(() => {
@@ -384,11 +336,23 @@ export default function VaultScreen() {
}; };
const handleHeirYes = () => { const handleHeirYes = () => {
setHeirStep('asset'); setShowMnemonic(false);
setIsUnlocked(true);
setShowLegacyAssignCta(true);
}; };
const handleHeirNo = () => { const handleHeirNo = () => {
handleHeirDecision(false); setShowMnemonic(false);
setIsUnlocked(true);
setShowLegacyAssignCta(true);
};
const handleOpenLegacyAssign = () => {
setSelectedHeir(null);
setSelectedHeirAsset(null);
setHeirStep('asset');
setMnemonicStep(5);
setShowMnemonic(true);
}; };
const handleSelectHeirAsset = (asset: VaultAsset) => { const handleSelectHeirAsset = (asset: VaultAsset) => {
@@ -398,25 +362,6 @@ export default function VaultScreen() {
const handleSelectHeir = (heir: Heir) => { const handleSelectHeir = (heir: Heir) => {
setSelectedHeir(heir); setSelectedHeir(heir);
if (selectedHeirAsset) {
setAssignments((prev) => {
const remaining = prev.filter((item) => item.asset.id !== selectedHeirAsset.id);
return [...remaining, { asset: selectedHeirAsset, heir }];
});
}
setSelectedHeir(null);
setSelectedHeirAsset(null);
setHeirStep('asset');
};
const getAssignmentForAsset = (assetId: string) =>
assignments.find((item) => item.asset.id === assetId);
const handleReviewAssignments = () => {
if (assignments.length === 0) {
Alert.alert('No assignments', 'Please assign at least one asset.');
return;
}
setHeirStep('summary'); setHeirStep('summary');
}; };
@@ -543,60 +488,7 @@ export default function VaultScreen() {
&& addVerified && addVerified
&& (selectedType !== 'private_key' || rehearsalConfirmed); && (selectedType !== 'private_key' || rehearsalConfirmed);
// Lock screen const mnemonicModal = (
if (!isUnlocked) {
return (
<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
/>
<Modal <Modal
visible={showMnemonic} visible={showMnemonic}
animationType="fade" animationType="fade"
@@ -626,39 +518,86 @@ export default function VaultScreen() {
{mnemonicStep === 1 ? ( {mnemonicStep === 1 ? (
<> <>
<Text style={styles.mnemonicSubtitle}> <Text style={styles.mnemonicSubtitle}>
Select the 12 words in order. Review your 12-word mnemonic. Tap any word to replace it.
</Text> </Text>
<View style={styles.selectedRow}>
<Text style={styles.selectedLabel}>Selected {selectedWords.length}/12</Text>
<Text style={styles.selectedValue}>{selectedWords.join(' ')}</Text>
</View>
<View style={styles.wordGrid}> <View style={styles.wordGrid}>
{wordOptions.map((word) => { {mnemonicWords.map((word, index) => (
const isSelected = selectedWords.includes(word);
return (
<TouchableOpacity <TouchableOpacity
key={word} key={`${word}-${index}`}
style={[styles.wordChip, isSelected && styles.wordChipSelected]} style={[
onPress={() => toggleWord(word)} styles.wordChip,
replaceIndex === index && styles.wordChipSelected,
]}
onPress={() => setReplaceIndex(index)}
activeOpacity={0.8} activeOpacity={0.8}
> >
<Text style={[styles.wordChipText, isSelected && styles.wordChipTextSelected]}> <Text
style={[
styles.wordChipIndex,
replaceIndex === index && styles.wordChipTextSelected,
]}
>
{index + 1}
</Text>
<Text
style={[
styles.wordChipText,
replaceIndex === index && styles.wordChipTextSelected,
]}
>
{word} {word}
</Text> </Text>
</TouchableOpacity> </TouchableOpacity>
); ))}
})}
</View> </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 <TouchableOpacity
style={[ key={word}
styles.mnemonicPrimaryButton, style={styles.replaceOption}
selectedWords.length !== 12 && styles.mnemonicButtonDisabled, onPress={() => handleReplaceWord(word)}
]} activeOpacity={0.8}
onPress={handleConfirmSelection}
activeOpacity={0.85}
disabled={selectedWords.length !== 12}
> >
<Text style={styles.mnemonicPrimaryText}>CONFIRM SELECTION</Text> <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> </TouchableOpacity>
</> </>
) : null} ) : null}
@@ -717,27 +656,6 @@ export default function VaultScreen() {
> >
<Text style={styles.mnemonicSecondaryText}>EMAIL BACKUP</Text> <Text style={styles.mnemonicSecondaryText}>EMAIL BACKUP</Text>
</TouchableOpacity> </TouchableOpacity>
{showEmailForm ? (
<View style={styles.emailForm}>
<TextInput
style={styles.emailInput}
value={emailAddress}
onChangeText={setEmailAddress}
placeholder="you@email.com"
placeholderTextColor={colors.sentinel.textSecondary}
keyboardType="email-address"
autoCapitalize="none"
autoCorrect={false}
/>
<TouchableOpacity
style={styles.emailSendButton}
onPress={handleSendEmail}
activeOpacity={0.85}
>
<Text style={styles.emailSendText}>SEND EMAIL</Text>
</TouchableOpacity>
</View>
) : null}
</> </>
) : null} ) : null}
@@ -805,7 +723,6 @@ export default function VaultScreen() {
<ScrollView style={styles.selectorList} showsVerticalScrollIndicator={false}> <ScrollView style={styles.selectorList} showsVerticalScrollIndicator={false}>
{assets.map((asset) => { {assets.map((asset) => {
const config = assetTypeConfig[asset.type]; const config = assetTypeConfig[asset.type];
const assignment = getAssignmentForAsset(asset.id);
return ( return (
<TouchableOpacity <TouchableOpacity
key={asset.id} key={asset.id}
@@ -818,21 +735,12 @@ export default function VaultScreen() {
</View> </View>
<View style={styles.selectorContent}> <View style={styles.selectorContent}>
<Text style={styles.selectorTitle}>{asset.label}</Text> <Text style={styles.selectorTitle}>{asset.label}</Text>
<Text style={styles.selectorSubtitle}> <Text style={styles.selectorSubtitle}>{config.label}</Text>
{assignment ? `Assigned to ${assignment.heir.name}` : config.label}
</Text>
</View> </View>
</TouchableOpacity> </TouchableOpacity>
); );
})} })}
</ScrollView> </ScrollView>
<TouchableOpacity
style={styles.mnemonicPrimaryButton}
onPress={handleReviewAssignments}
activeOpacity={0.85}
>
<Text style={styles.mnemonicPrimaryText}>REVIEW & SUBMIT</Text>
</TouchableOpacity>
<TouchableOpacity <TouchableOpacity
style={styles.mnemonicSecondaryButton} style={styles.mnemonicSecondaryButton}
onPress={() => setHeirStep('decision')} onPress={() => setHeirStep('decision')}
@@ -882,15 +790,13 @@ export default function VaultScreen() {
Confirm assignment details. Confirm assignment details.
</Text> </Text>
<View style={styles.summaryCard}> <View style={styles.summaryCard}>
{assignments.map((item) => ( <Text style={styles.summaryLabel}>Vault Item</Text>
<View key={item.asset.id} style={styles.summaryItem}> <Text style={styles.summaryValue}>{selectedHeirAsset?.label}</Text>
<Text style={styles.summaryLabel}>Asset</Text> <Text style={styles.summaryLabel}>Legacy Handler</Text>
<Text style={styles.summaryValue}>{item.asset.label}</Text> <Text style={styles.summaryValue}>{selectedHeir?.name}</Text>
<Text style={styles.summaryLabel}>Heir</Text> <Text style={styles.summaryValue}>{selectedHeir?.email}</Text>
<Text style={styles.summaryValue}>{item.heir.name}</Text> <Text style={styles.summaryLabel}>Release Tier</Text>
<Text style={styles.summaryValue}>{item.heir.email}</Text> <Text style={styles.summaryValue}>Tier {selectedHeir?.releaseLevel}</Text>
</View>
))}
</View> </View>
<TouchableOpacity <TouchableOpacity
style={styles.mnemonicPrimaryButton} style={styles.mnemonicPrimaryButton}
@@ -901,34 +807,80 @@ export default function VaultScreen() {
</TouchableOpacity> </TouchableOpacity>
<TouchableOpacity <TouchableOpacity
style={styles.mnemonicSecondaryButton} style={styles.mnemonicSecondaryButton}
onPress={() => setHeirStep('asset')} onPress={() => setHeirStep('heir')}
activeOpacity={0.85} activeOpacity={0.85}
> >
<Text style={styles.mnemonicSecondaryText}>EDIT ASSIGNMENTS</Text> <Text style={styles.mnemonicSecondaryText}>EDIT</Text>
</TouchableOpacity> </TouchableOpacity>
</> </>
) : null} ) : null}
</> </>
) : null} ) : null}
<View style={styles.stepDots}> <View style={styles.stepDots}>
{[1, 2, 3, 4, 5].map((step) => ( <View style={[styles.stepDot, mnemonicStep === 1 && styles.stepDotActive]} />
<View <View style={[styles.stepDot, mnemonicStep !== 1 && styles.stepDotActive]} />
key={step}
style={[
styles.stepDot,
mnemonicStep === step && styles.stepDotActive,
]}
/>
))}
</View> </View>
</LinearGradient> </LinearGradient>
</KeyboardAvoidingView> </KeyboardAvoidingView>
</Modal> </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> </View>
); );
}
return ( const vaultScreen = (
<View style={styles.container}> <View style={styles.container}>
<LinearGradient <LinearGradient
colors={[colors.vault.backgroundGradientStart, colors.vault.backgroundGradientEnd]} colors={[colors.vault.backgroundGradientStart, colors.vault.backgroundGradientEnd]}
@@ -953,6 +905,24 @@ export default function VaultScreen() {
</Text> </Text>
</View> </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 */} {/* Asset List */}
<ScrollView <ScrollView
style={styles.assetList} style={styles.assetList}
@@ -1424,9 +1394,19 @@ export default function VaultScreen() {
</Modal> </Modal>
</View> </View>
); );
return (
<View style={styles.root}>
{isUnlocked ? vaultScreen : lockScreen}
{mnemonicModal}
</View>
);
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
root: {
flex: 1,
},
container: { container: {
flex: 1, flex: 1,
}, },
@@ -1541,6 +1521,42 @@ const styles = StyleSheet.create({
fontSize: typography.fontSize.sm, fontSize: typography.fontSize.sm,
color: colors.vault.textSecondary, 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: { assetList: {
flex: 1, flex: 1,
}, },
@@ -2200,28 +2216,6 @@ const styles = StyleSheet.create({
lineHeight: 22, lineHeight: 22,
textAlign: 'center', textAlign: 'center',
}, },
selectedRow: {
backgroundColor: colors.sentinel.cardBackground,
borderRadius: borderRadius.lg,
paddingVertical: spacing.sm,
paddingHorizontal: spacing.md,
borderWidth: 1,
borderColor: colors.sentinel.cardBorder,
marginBottom: spacing.md,
},
selectedLabel: {
fontSize: typography.fontSize.xs,
color: colors.sentinel.textSecondary,
letterSpacing: typography.letterSpacing.wide,
marginBottom: 4,
fontWeight: '600',
},
selectedValue: {
fontSize: typography.fontSize.xs,
color: colors.sentinel.text,
fontFamily: typography.fontFamily.mono,
lineHeight: 18,
},
progressContainer: { progressContainer: {
marginTop: spacing.sm, marginTop: spacing.sm,
marginBottom: spacing.md, marginBottom: spacing.md,
@@ -2254,9 +2248,10 @@ const styles = StyleSheet.create({
marginBottom: spacing.lg, marginBottom: spacing.lg,
}, },
wordChip: { wordChip: {
minWidth: '30%',
paddingHorizontal: spacing.sm, paddingHorizontal: spacing.sm,
paddingVertical: spacing.xs, paddingVertical: spacing.sm,
borderRadius: borderRadius.full, borderRadius: borderRadius.lg,
borderWidth: 1, borderWidth: 1,
borderColor: colors.sentinel.cardBorder, borderColor: colors.sentinel.cardBorder,
backgroundColor: 'transparent', backgroundColor: 'transparent',
@@ -2265,6 +2260,11 @@ const styles = StyleSheet.create({
backgroundColor: colors.sentinel.primary, backgroundColor: colors.sentinel.primary,
borderColor: colors.sentinel.primary, borderColor: colors.sentinel.primary,
}, },
wordChipIndex: {
fontSize: typography.fontSize.xs,
color: colors.sentinel.textSecondary,
marginBottom: 2,
},
wordChipText: { wordChipText: {
color: colors.sentinel.text, color: colors.sentinel.text,
fontSize: typography.fontSize.xs, fontSize: typography.fontSize.xs,
@@ -2273,6 +2273,62 @@ const styles = StyleSheet.create({
wordChipTextSelected: { wordChipTextSelected: {
color: colors.nautical.cream, 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: { selectorList: {
maxHeight: 260, maxHeight: 260,
marginBottom: spacing.md, marginBottom: spacing.md,
@@ -2317,12 +2373,7 @@ const styles = StyleSheet.create({
borderWidth: 1, borderWidth: 1,
borderColor: colors.sentinel.cardBorder, borderColor: colors.sentinel.cardBorder,
marginBottom: spacing.md, marginBottom: spacing.md,
gap: spacing.sm, gap: spacing.xs,
},
summaryItem: {
paddingBottom: spacing.sm,
borderBottomWidth: 1,
borderBottomColor: colors.sentinel.cardBorder,
}, },
summaryLabel: { summaryLabel: {
fontSize: typography.fontSize.xs, fontSize: typography.fontSize.xs,
@@ -2396,29 +2447,4 @@ const styles = StyleSheet.create({
fontWeight: '700', fontWeight: '700',
letterSpacing: typography.letterSpacing.wide, letterSpacing: typography.letterSpacing.wide,
}, },
emailForm: {
marginTop: spacing.sm,
},
emailInput: {
height: 44,
borderRadius: borderRadius.full,
borderWidth: 1,
borderColor: colors.sentinel.cardBorder,
paddingHorizontal: spacing.md,
color: colors.sentinel.text,
fontSize: typography.fontSize.sm,
backgroundColor: 'rgba(255, 255, 255, 0.02)',
marginBottom: spacing.sm,
},
emailSendButton: {
backgroundColor: colors.nautical.teal,
paddingVertical: spacing.sm,
borderRadius: borderRadius.full,
alignItems: 'center',
},
emailSendText: {
color: colors.nautical.cream,
fontWeight: '700',
letterSpacing: typography.letterSpacing.wide,
},
}); });

View File

@@ -77,6 +77,7 @@ export interface ProtocolInfo {
export interface User { export interface User {
id: number; id: number;
username: string; username: string;
email?: string;
public_key: string; public_key: string;
is_admin: boolean; is_admin: boolean;
guale: boolean; guale: boolean;