From 56bb72aab8c37181175a92d999ceee23c1bf6478 Mon Sep 17 00:00:00 2001 From: Steven Du Date: Fri, 30 Jan 2026 22:42:20 -0400 Subject: [PATCH] The vault notification mode1 --- src/screens/SentinelScreen.tsx | 343 +------------ src/screens/VaultScreen.tsx | 898 ++++++++++++++++++++++++++++++++- 2 files changed, 900 insertions(+), 341 deletions(-) diff --git a/src/screens/SentinelScreen.tsx b/src/screens/SentinelScreen.tsx index 795d3c7..0db7dfd 100644 --- a/src/screens/SentinelScreen.tsx +++ b/src/screens/SentinelScreen.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useRef } from 'react'; +import React, { useState, useEffect } from 'react'; import { View, Text, @@ -8,47 +8,13 @@ import { SafeAreaView, Animated, Modal, - TextInput, - KeyboardAvoidingView, - Platform, - Share, - Alert, - Linking, } 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 { colors, typography, spacing, borderRadius, shadows } from '../theme/colors'; import { SystemStatus, KillSwitchLog } from '../types'; import VaultScreen from './VaultScreen'; -const MNEMONIC_WORDS = [ - 'anchor', 'harbor', 'compass', 'lighthouse', 'current', 'ocean', 'tide', 'voyage', - 'keel', 'stern', 'bow', 'mast', 'sail', 'port', 'starboard', 'reef', - 'signal', 'beacon', 'chart', 'helm', 'gale', 'calm', 'cove', 'isle', - 'horizon', 'sextant', 'sound', 'drift', 'wake', 'mariner', 'pilot', 'fathom', - 'buoy', 'lantern', 'harpoon', 'lagoon', 'bay', 'strait', 'riptide', 'foam', - 'coral', 'pearl', 'trident', 'ebb', 'flow', 'vault', 'cipher', 'shroud', - 'salt', 'wave', 'grotto', 'lagoon', 'storm', 'north', 'south', 'east', - 'west', 'ember', 'cabin', 'signal', 'ledger', 'torch', 'sanctum', 'oath', -]; - -const generateMnemonic = (wordCount = 12) => { - const words: string[] = []; - for (let i = 0; i < wordCount; i += 1) { - const index = Math.floor(Math.random() * MNEMONIC_WORDS.length); - words.push(MNEMONIC_WORDS[index]); - } - return words; -}; - -const splitMnemonic = (words: string[]) => [ - words.slice(0, 4), - words.slice(4, 8), - words.slice(8, 12), -]; - // Status configuration with nautical theme const statusConfig: Record([]); - const [mnemonicParts, setMnemonicParts] = useState([]); - const [showEmailForm, setShowEmailForm] = useState(false); - const [emailAddress, setEmailAddress] = useState(''); - const [isCapturing, setIsCapturing] = useState(false); - const mnemonicRef = useRef(null); useEffect(() => { // Pulse animation @@ -164,65 +123,8 @@ export default function SentinelScreen() { ).start(); }, []); - const openVaultWithMnemonic = () => { - const words = generateMnemonic(); - const parts = splitMnemonic(words); - setMnemonicWords(words); - setMnemonicParts(parts); - setShowMnemonic(true); - setShowVault(false); - setShowEmailForm(false); - setEmailAddress(''); - AsyncStorage.setItem('sentinel_mnemonic_part_local', parts[0].join(' ')).catch(() => { - // Best-effort local store; UI remains available - }); - }; - - const handleScreenshot = async () => { - try { - setIsCapturing(true); - const uri = await captureRef(mnemonicRef, { - format: 'png', - quality: 1, - result: 'tmpfile', - }); - await Share.share({ - url: uri, - message: 'Sentinel key backup', - }); - setShowMnemonic(false); - setShowVault(true); - } catch (error) { - Alert.alert('Screenshot failed', 'Please try again or use email backup.'); - } finally { - setIsCapturing(false); - } - }; - - const handleEmailBackup = () => { - setShowEmailForm(true); - }; - - const handleSendEmail = async () => { - const trimmed = emailAddress.trim(); - if (!trimmed || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(trimmed)) { - Alert.alert('Invalid email', 'Please enter a valid email address.'); - return; - } - - const subject = encodeURIComponent('Sentinel Vault Recovery Key'); - const body = encodeURIComponent(`Your 12-word mnemonic:\n${mnemonicWords.join(' ')}`); - const mailtoUrl = `mailto:${trimmed}?subject=${subject}&body=${body}`; - - try { - await Linking.openURL(mailtoUrl); - setShowMnemonic(false); - setShowEmailForm(false); - setEmailAddress(''); - setShowVault(true); - } catch (error) { - Alert.alert('Email failed', 'Unable to open email client.'); - } + const openVault = () => { + setShowVault(true); }; const handleHeartbeat = () => { @@ -388,7 +290,7 @@ export default function SentinelScreen() { Open @@ -457,99 +359,6 @@ export default function SentinelScreen() { - {/* Mnemonic Modal */} - setShowMnemonic(false)} - > - - - setShowMnemonic(false)} - activeOpacity={0.85} - > - - - - - 12-Word Mnemonic - - - Your mnemonic is split into 3 parts (4/4/4). Part 1 is stored locally. - - - - {mnemonicWords.join(' ')} - - - - - PART 1 • LOCAL - {mnemonicParts[0]?.join(' ')} - Stored on this device - - - PART 2 • CLOUD NODE - {mnemonicParts[1]?.join(' ')} - To be synced - - - PART 3 • HEIR - {mnemonicParts[2]?.join(' ')} - To be assigned - - - - - {isCapturing ? 'CAPTURING...' : 'PHYSICAL BACKUP (SCREENSHOT)'} - - - - EMAIL BACKUP - - {showEmailForm ? ( - - - - SEND EMAIL - - - ) : null} - - - ); } @@ -810,148 +619,4 @@ const styles = StyleSheet.create({ alignItems: 'center', justifyContent: 'center', }, - mnemonicOverlay: { - flex: 1, - backgroundColor: 'rgba(11, 20, 24, 0.72)', - justifyContent: 'center', - padding: spacing.lg, - }, - mnemonicCard: { - borderRadius: borderRadius.xl, - padding: spacing.lg, - borderWidth: 1, - borderColor: colors.sentinel.cardBorder, - ...shadows.glow, - }, - mnemonicHeader: { - flexDirection: 'row', - alignItems: 'center', - gap: spacing.sm, - marginBottom: spacing.sm, - }, - mnemonicClose: { - position: 'absolute', - top: spacing.sm, - right: spacing.sm, - width: 32, - height: 32, - borderRadius: 16, - alignItems: 'center', - justifyContent: 'center', - backgroundColor: 'rgba(26, 58, 74, 0.35)', - }, - mnemonicTitle: { - fontSize: typography.fontSize.lg, - fontWeight: '700', - color: colors.sentinel.text, - letterSpacing: typography.letterSpacing.wide, - }, - mnemonicSubtitle: { - fontSize: typography.fontSize.sm, - color: colors.sentinel.textSecondary, - marginBottom: spacing.md, - }, - mnemonicBlock: { - backgroundColor: colors.sentinel.cardBackground, - borderRadius: borderRadius.lg, - paddingVertical: spacing.md, - paddingHorizontal: spacing.md, - borderWidth: 1, - borderColor: colors.sentinel.cardBorder, - marginBottom: spacing.lg, - }, - partGrid: { - gap: spacing.sm, - marginBottom: spacing.lg, - }, - partCard: { - backgroundColor: colors.sentinel.cardBackground, - borderRadius: borderRadius.lg, - paddingVertical: spacing.sm, - paddingHorizontal: spacing.md, - borderWidth: 1, - borderColor: colors.sentinel.cardBorder, - }, - partCardStored: { - borderColor: colors.sentinel.primary, - }, - partLabel: { - fontSize: typography.fontSize.xs, - color: colors.sentinel.textSecondary, - letterSpacing: typography.letterSpacing.wide, - marginBottom: 4, - fontWeight: '600', - }, - partValue: { - fontSize: typography.fontSize.md, - color: colors.sentinel.text, - fontFamily: typography.fontFamily.mono, - fontWeight: '700', - marginBottom: 2, - }, - partHint: { - fontSize: typography.fontSize.xs, - color: colors.sentinel.textSecondary, - }, - mnemonicBlockText: { - fontSize: typography.fontSize.sm, - color: colors.sentinel.text, - fontFamily: typography.fontFamily.mono, - fontWeight: '600', - lineHeight: 22, - textAlign: 'center', - }, - mnemonicPrimaryButton: { - backgroundColor: colors.sentinel.primary, - paddingVertical: spacing.sm, - borderRadius: borderRadius.full, - alignItems: 'center', - marginBottom: spacing.sm, - }, - mnemonicButtonDisabled: { - opacity: 0.6, - }, - mnemonicPrimaryText: { - color: colors.nautical.cream, - fontWeight: '700', - letterSpacing: typography.letterSpacing.wide, - }, - mnemonicSecondaryButton: { - backgroundColor: 'transparent', - paddingVertical: spacing.sm, - borderRadius: borderRadius.full, - alignItems: 'center', - borderWidth: 1, - borderColor: colors.sentinel.cardBorder, - }, - mnemonicSecondaryText: { - color: colors.sentinel.text, - fontWeight: '700', - letterSpacing: typography.letterSpacing.wide, - }, - emailForm: { - marginTop: spacing.sm, - }, - emailInput: { - height: 44, - borderRadius: borderRadius.full, - borderWidth: 1, - borderColor: colors.sentinel.cardBorder, - paddingHorizontal: spacing.md, - color: colors.sentinel.text, - fontSize: typography.fontSize.sm, - backgroundColor: 'rgba(255, 255, 255, 0.02)', - marginBottom: spacing.sm, - }, - emailSendButton: { - backgroundColor: colors.nautical.teal, - paddingVertical: spacing.sm, - borderRadius: borderRadius.full, - alignItems: 'center', - }, - emailSendText: { - color: colors.nautical.cream, - fontWeight: '700', - letterSpacing: typography.letterSpacing.wide, - }, }); diff --git a/src/screens/VaultScreen.tsx b/src/screens/VaultScreen.tsx index 25daf08..4077e2e 100644 --- a/src/screens/VaultScreen.tsx +++ b/src/screens/VaultScreen.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useRef } from 'react'; import { View, Text, @@ -7,15 +7,20 @@ import { 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 { colors, typography, spacing, borderRadius, shadows } from '../theme/colors'; -import { VaultAsset, VaultAssetType } from '../types'; +import { VaultAsset, VaultAssetType, Heir } from '../types'; import BiometricModal from '../components/common/BiometricModal'; // Asset type configuration with nautical theme @@ -35,6 +40,79 @@ const accountProviderOptions = [ { key: 'custom', label: 'Other', icon: 'shield-account', iconType: 'material' as const }, ]; +const MNEMONIC_WORDS = [ + 'anchor', 'harbor', 'compass', 'lighthouse', 'current', 'ocean', 'tide', 'voyage', + 'keel', 'stern', 'bow', 'mast', 'sail', 'port', 'starboard', 'reef', + 'signal', 'beacon', 'chart', 'helm', 'gale', 'calm', 'cove', 'isle', + 'horizon', 'sextant', 'sound', 'drift', 'wake', 'mariner', 'pilot', 'fathom', + 'buoy', 'lantern', 'harpoon', 'lagoon', 'bay', 'strait', 'riptide', 'foam', + 'coral', 'pearl', 'trident', 'ebb', 'flow', 'vault', 'cipher', 'shroud', + 'salt', 'wave', 'grotto', 'lagoon', 'storm', 'north', 'south', 'east', + 'west', 'ember', 'cabin', 'signal', 'ledger', 'torch', 'sanctum', 'oath', +]; + +const initialHeirs: Heir[] = [ + { + id: '1', + name: 'John Smith', + email: 'john.smith@email.com', + phone: '+1 415 555 0132', + status: 'confirmed', + releaseLevel: 3, + releaseOrder: 1, + paymentStrategy: 'prepaid', + }, + { + id: '2', + name: 'Jane Doe', + email: 'jane.doe@email.com', + phone: '+1 212 555 0184', + status: 'confirmed', + releaseLevel: 2, + releaseOrder: 2, + paymentStrategy: 'pay_on_access', + }, + { + id: '3', + name: 'Alice Johnson', + email: 'alice.j@email.com', + phone: '+1 646 555 0149', + status: 'invited', + releaseLevel: 1, + releaseOrder: 3, + paymentStrategy: 'pay_on_access', + }, +]; + +const generateMnemonic = (wordCount = 12) => { + const words: string[] = []; + for (let i = 0; i < wordCount; i += 1) { + const index = Math.floor(Math.random() * MNEMONIC_WORDS.length); + words.push(MNEMONIC_WORDS[index]); + } + return words; +}; + +const splitMnemonic = (words: string[]) => [ + words.slice(0, 4), + words.slice(4, 8), + words.slice(8, 12), +]; + +type HeirAssignment = { + asset: VaultAsset; + heir: Heir; +}; + +const shuffle = (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 const initialAssets: VaultAsset[] = [ { @@ -104,6 +182,23 @@ export default function VaultScreen() { 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 [mnemonicWords, setMnemonicWords] = useState([]); + const [mnemonicParts, setMnemonicParts] = useState([]); + const [mnemonicStep, setMnemonicStep] = useState<1 | 2 | 3 | 4 | 5>(1); + const [heirStep, setHeirStep] = useState<'decision' | 'asset' | 'heir' | 'summary'>('decision'); + const [selectedHeir, setSelectedHeir] = useState(null); + const [selectedHeirAsset, setSelectedHeirAsset] = useState(null); + const [assignments, setAssignments] = useState([]); + const [wordOptions, setWordOptions] = useState([]); + const [selectedWords, setSelectedWords] = useState([]); + const [progressIndex, setProgressIndex] = useState(0); + const [progressAnim] = useState(new Animated.Value(0)); + const [showEmailForm, setShowEmailForm] = useState(false); + const [emailAddress, setEmailAddress] = useState(''); + const [isCapturing, setIsCapturing] = useState(false); + const mnemonicRef = useRef(null); + const progressTimerRef = useRef | null>(null); useEffect(() => { if (!isUnlocked) { @@ -145,9 +240,186 @@ export default function VaultScreen() { const handleUnlock = () => { setShowBiometric(false); + const words = generateMnemonic(); + const parts = splitMnemonic(words); + const decoys = MNEMONIC_WORDS.filter((word) => !words.includes(word)); + const options = shuffle([...words, ...decoys.slice(0, 12)]); + setMnemonicWords(words); + setMnemonicParts(parts); + setWordOptions(options); + setSelectedWords([]); + setMnemonicStep(1); + setHeirStep('decision'); + setSelectedHeir(null); + setSelectedHeirAsset(null); + setAssignments([]); + setProgressIndex(0); + progressAnim.setValue(0); + setShowMnemonic(true); + setShowEmailForm(false); + setEmailAddress(''); + AsyncStorage.setItem('sentinel_mnemonic_part_local', parts[0].join(' ')).catch(() => { + // Best-effort local store; UI remains available + }); + }; + + const handleScreenshot = async () => { + try { + setIsCapturing(true); + const uri = await captureRef(mnemonicRef, { + format: 'png', + quality: 1, + result: 'tmpfile', + }); + await Share.share({ + url: uri, + message: 'Sentinel key backup', + }); + setMnemonicStep(4); + } catch (error) { + Alert.alert('Screenshot failed', 'Please try again or use email backup.'); + } finally { + setIsCapturing(false); + } + }; + + const handleEmailBackup = () => { + setShowEmailForm(true); + }; + + const toggleWord = (word: string) => { + if (selectedWords.includes(word)) { + setSelectedWords(selectedWords.filter((item) => item !== word)); + return; + } + if (selectedWords.length >= 12) return; + setSelectedWords([...selectedWords, word]); + }; + + const handleConfirmSelection = () => { + if (selectedWords.length !== 12) { + Alert.alert('Select 12 words', 'Please select all 12 words in order.'); + return; + } + setMnemonicStep(2); + }; + + const handleSendEmail = async () => { + const trimmed = emailAddress.trim(); + if (!trimmed || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(trimmed)) { + Alert.alert('Invalid email', 'Please enter a valid email address.'); + return; + } + + const subject = encodeURIComponent('Sentinel Vault Recovery Key'); + const body = encodeURIComponent(`Your 12-word mnemonic:\n${mnemonicWords.join(' ')}`); + const mailtoUrl = `mailto:${trimmed}?subject=${subject}&body=${body}`; + + try { + await Linking.openURL(mailtoUrl); + setShowEmailForm(false); + setEmailAddress(''); + setMnemonicStep(4); + } catch (error) { + Alert.alert('Email failed', 'Unable to open email client.'); + } + }; + + useEffect(() => { + if (mnemonicStep !== 4) { + if (progressTimerRef.current) { + clearInterval(progressTimerRef.current); + progressTimerRef.current = null; + } + return; + } + + const messagesCount = 5; + let current = 0; + setProgressIndex(current); + progressAnim.setValue(0); + + progressTimerRef.current = setInterval(() => { + current += 1; + if (current >= messagesCount) { + if (progressTimerRef.current) { + clearInterval(progressTimerRef.current); + progressTimerRef.current = null; + } + setMnemonicStep(5); + return; + } + setProgressIndex(current); + Animated.timing(progressAnim, { + toValue: current / (messagesCount - 1), + duration: 700, + useNativeDriver: false, + }).start(); + }, 1100); + + Animated.timing(progressAnim, { + toValue: 1 / (messagesCount - 1), + duration: 700, + useNativeDriver: false, + }).start(); + + return () => { + if (progressTimerRef.current) { + clearInterval(progressTimerRef.current); + progressTimerRef.current = null; + } + }; + }, [mnemonicStep, progressAnim]); + + const handleHeirDecision = (share: boolean) => { + // Placeholder for future heir flow + setShowMnemonic(false); setIsUnlocked(true); }; + const handleSubmitAssignment = () => { + // Placeholder for submitting assignment to API + setShowMnemonic(false); + setIsUnlocked(true); + }; + + const handleHeirYes = () => { + setHeirStep('asset'); + }; + + const handleHeirNo = () => { + handleHeirDecision(false); + }; + + const handleSelectHeirAsset = (asset: VaultAsset) => { + setSelectedHeirAsset(asset); + setHeirStep('heir'); + }; + + const handleSelectHeir = (heir: Heir) => { + setSelectedHeir(heir); + if (selectedHeirAsset) { + setAssignments((prev) => { + const remaining = prev.filter((item) => item.asset.id !== selectedHeirAsset.id); + return [...remaining, { asset: selectedHeirAsset, heir }]; + }); + } + setSelectedHeir(null); + setSelectedHeirAsset(null); + setHeirStep('asset'); + }; + + const getAssignmentForAsset = (assetId: string) => + assignments.find((item) => item.asset.id === assetId); + + const handleReviewAssignments = () => { + if (assignments.length === 0) { + Alert.alert('No assignments', 'Please assign at least one asset.'); + return; + } + setHeirStep('summary'); + }; + const resetAddFlow = () => { setAddStep(1); setAddMethod('text'); @@ -324,6 +596,334 @@ export default function VaultScreen() { message="Verify your identity to access your treasures" isDark /> + + setShowMnemonic(false)} + > + + + setShowMnemonic(false)} + activeOpacity={0.85} + > + + + + + Mnemonic Setup + + {mnemonicStep === 1 ? ( + <> + + Select the 12 words in order. + + + Selected {selectedWords.length}/12 + {selectedWords.join(' ')} + + + {wordOptions.map((word) => { + const isSelected = selectedWords.includes(word); + return ( + toggleWord(word)} + activeOpacity={0.8} + > + + {word} + + + ); + })} + + + CONFIRM SELECTION + + + ) : null} + + {mnemonicStep === 2 ? ( + <> + + Confirm your 12-word mnemonic. + + + + {mnemonicWords.join(' ')} + + + setMnemonicStep(3)} + activeOpacity={0.85} + > + CONFIRM + + setMnemonicStep(1)} + activeOpacity={0.85} + > + EDIT SELECTION + + + ) : null} + + {mnemonicStep === 3 ? ( + <> + + Back up your mnemonic before entering the Vault. + + + + {mnemonicWords.join(' ')} + + + + + {isCapturing ? 'CAPTURING...' : 'PHYSICAL BACKUP (SCREENSHOT)'} + + + + EMAIL BACKUP + + {showEmailForm ? ( + + + + SEND EMAIL + + + ) : null} + + ) : null} + + {mnemonicStep === 4 ? ( + <> + + Finalizing your vault protection. + + + + + + + + + {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'} + + + + ) : null} + + {mnemonicStep === 5 ? ( + <> + {heirStep === 'decision' ? ( + <> + + Share Part Three with your legacy handler? + + + YES, SEND + + + NOT NOW + + + ) : null} + + {heirStep === 'asset' ? ( + <> + + Select the vault item to assign. + + + {assets.map((asset) => { + const config = assetTypeConfig[asset.type]; + const assignment = getAssignmentForAsset(asset.id); + return ( + handleSelectHeirAsset(asset)} + activeOpacity={0.8} + > + + {renderAssetTypeIcon(config, 18, colors.vault.primary)} + + + {asset.label} + + {assignment ? `Assigned to ${assignment.heir.name}` : config.label} + + + + ); + })} + + + REVIEW & SUBMIT + + setHeirStep('decision')} + activeOpacity={0.85} + > + BACK + + + ) : null} + + {heirStep === 'heir' ? ( + <> + + Choose a legacy handler. + + + {initialHeirs.map((heir) => ( + handleSelectHeir(heir)} + activeOpacity={0.8} + > + + + + + {heir.name} + {heir.email} + + + ))} + + setHeirStep('asset')} + activeOpacity={0.85} + > + BACK + + + ) : null} + + {heirStep === 'summary' ? ( + <> + + Confirm assignment details. + + + {assignments.map((item) => ( + + Asset + {item.asset.label} + Heir + {item.heir.name} + {item.heir.email} + + ))} + + + SUBMIT + + setHeirStep('asset')} + activeOpacity={0.85} + > + EDIT ASSIGNMENTS + + + ) : null} + + ) : null} + + {[1, 2, 3, 4, 5].map((step) => ( + + ))} + + + + ); } @@ -1527,4 +2127,298 @@ const styles = StyleSheet.create({ 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', + }, + selectedRow: { + backgroundColor: colors.sentinel.cardBackground, + borderRadius: borderRadius.lg, + paddingVertical: spacing.sm, + paddingHorizontal: spacing.md, + borderWidth: 1, + borderColor: colors.sentinel.cardBorder, + marginBottom: spacing.md, + }, + selectedLabel: { + fontSize: typography.fontSize.xs, + color: colors.sentinel.textSecondary, + letterSpacing: typography.letterSpacing.wide, + marginBottom: 4, + fontWeight: '600', + }, + selectedValue: { + fontSize: typography.fontSize.xs, + color: colors.sentinel.text, + fontFamily: typography.fontFamily.mono, + lineHeight: 18, + }, + progressContainer: { + marginTop: spacing.sm, + marginBottom: spacing.md, + }, + progressTrack: { + height: 6, + borderRadius: 999, + backgroundColor: colors.sentinel.cardBorder, + overflow: 'hidden', + }, + progressFill: { + height: '100%', + backgroundColor: colors.sentinel.primary, + }, + progressSteps: { + minHeight: 44, + justifyContent: 'center', + marginBottom: spacing.md, + }, + progressText: { + fontSize: typography.fontSize.sm, + color: colors.sentinel.text, + textAlign: 'center', + lineHeight: typography.fontSize.sm * 1.4, + }, + wordGrid: { + flexDirection: 'row', + flexWrap: 'wrap', + gap: spacing.sm, + marginBottom: spacing.lg, + }, + wordChip: { + paddingHorizontal: spacing.sm, + paddingVertical: spacing.xs, + borderRadius: borderRadius.full, + borderWidth: 1, + borderColor: colors.sentinel.cardBorder, + backgroundColor: 'transparent', + }, + wordChipSelected: { + backgroundColor: colors.sentinel.primary, + borderColor: colors.sentinel.primary, + }, + wordChipText: { + color: colors.sentinel.text, + fontSize: typography.fontSize.xs, + fontWeight: '600', + }, + wordChipTextSelected: { + color: colors.nautical.cream, + }, + selectorList: { + maxHeight: 260, + marginBottom: spacing.md, + }, + selectorRow: { + flexDirection: 'row', + alignItems: 'center', + gap: spacing.sm, + paddingVertical: spacing.sm, + paddingHorizontal: spacing.sm, + borderRadius: borderRadius.lg, + borderWidth: 1, + borderColor: colors.sentinel.cardBorder, + backgroundColor: colors.sentinel.cardBackground, + marginBottom: spacing.sm, + }, + selectorIcon: { + width: 32, + height: 32, + borderRadius: 16, + alignItems: 'center', + justifyContent: 'center', + backgroundColor: `${colors.sentinel.primary}1A`, + }, + selectorContent: { + flex: 1, + }, + selectorTitle: { + fontSize: typography.fontSize.sm, + color: colors.sentinel.text, + fontWeight: '600', + }, + selectorSubtitle: { + fontSize: typography.fontSize.xs, + color: colors.sentinel.textSecondary, + marginTop: 2, + }, + summaryCard: { + backgroundColor: colors.sentinel.cardBackground, + borderRadius: borderRadius.lg, + padding: spacing.md, + borderWidth: 1, + borderColor: colors.sentinel.cardBorder, + marginBottom: spacing.md, + gap: spacing.sm, + }, + summaryItem: { + paddingBottom: spacing.sm, + borderBottomWidth: 1, + borderBottomColor: colors.sentinel.cardBorder, + }, + summaryLabel: { + fontSize: typography.fontSize.xs, + color: colors.sentinel.textSecondary, + letterSpacing: typography.letterSpacing.wide, + marginTop: spacing.xs, + }, + summaryValue: { + fontSize: typography.fontSize.sm, + color: colors.sentinel.text, + fontWeight: '600', + }, + partGrid: { + gap: spacing.sm, + marginBottom: spacing.lg, + }, + partCard: { + backgroundColor: colors.sentinel.cardBackground, + borderRadius: borderRadius.lg, + paddingVertical: spacing.sm, + paddingHorizontal: spacing.md, + borderWidth: 1, + borderColor: colors.sentinel.cardBorder, + }, + partCardStored: { + borderColor: colors.sentinel.primary, + }, + partLabel: { + fontSize: typography.fontSize.xs, + color: colors.sentinel.textSecondary, + letterSpacing: typography.letterSpacing.wide, + marginBottom: 4, + fontWeight: '600', + }, + partValue: { + fontSize: typography.fontSize.md, + color: colors.sentinel.text, + fontFamily: typography.fontFamily.mono, + fontWeight: '700', + marginBottom: 2, + }, + partHint: { + fontSize: typography.fontSize.xs, + color: colors.sentinel.textSecondary, + }, + mnemonicPrimaryButton: { + backgroundColor: colors.sentinel.primary, + paddingVertical: spacing.sm, + borderRadius: borderRadius.full, + alignItems: 'center', + marginBottom: spacing.sm, + }, + mnemonicButtonDisabled: { + opacity: 0.6, + }, + mnemonicPrimaryText: { + color: colors.nautical.cream, + fontWeight: '700', + letterSpacing: typography.letterSpacing.wide, + }, + mnemonicSecondaryButton: { + backgroundColor: 'transparent', + paddingVertical: spacing.sm, + borderRadius: borderRadius.full, + alignItems: 'center', + borderWidth: 1, + borderColor: colors.sentinel.cardBorder, + }, + mnemonicSecondaryText: { + color: colors.sentinel.text, + fontWeight: '700', + letterSpacing: typography.letterSpacing.wide, + }, + emailForm: { + marginTop: spacing.sm, + }, + emailInput: { + height: 44, + borderRadius: borderRadius.full, + borderWidth: 1, + borderColor: colors.sentinel.cardBorder, + paddingHorizontal: spacing.md, + color: colors.sentinel.text, + fontSize: typography.fontSize.sm, + backgroundColor: 'rgba(255, 255, 255, 0.02)', + marginBottom: spacing.sm, + }, + emailSendButton: { + backgroundColor: colors.nautical.teal, + paddingVertical: spacing.sm, + borderRadius: borderRadius.full, + alignItems: 'center', + }, + emailSendText: { + color: colors.nautical.cream, + fontWeight: '700', + letterSpacing: typography.letterSpacing.wide, + }, });