diff --git a/App.tsx b/App.tsx index 2c913dc..cecce47 100644 --- a/App.tsx +++ b/App.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; +import { Buffer } from 'buffer'; import { StatusBar } from 'expo-status-bar'; import { NavigationContainer } from '@react-navigation/native'; 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 { colors } from './src/theme/colors'; +if (typeof globalThis !== 'undefined' && !globalThis.Buffer) { + globalThis.Buffer = Buffer; +} + /** * Loading screen shown while restoring auth state */ diff --git a/package-lock.json b/package-lock.json index 889b6d2..20b06ee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,8 @@ "@react-navigation/bottom-tabs": "^6.6.1", "@react-navigation/native": "^6.1.18", "@react-navigation/native-stack": "^6.11.0", + "bip39": "^3.1.0", + "buffer": "^6.0.3", "expo": "~52.0.0", "expo-asset": "~11.0.5", "expo-constants": "~17.0.8", @@ -3209,6 +3211,18 @@ "@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": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -4447,6 +4461,15 @@ "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": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.0.7.tgz", @@ -4533,9 +4556,9 @@ } }, "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", "funding": [ { "type": "github", @@ -4553,7 +4576,7 @@ "license": "MIT", "dependencies": { "base64-js": "^1.3.1", - "ieee754": "^1.1.13" + "ieee754": "^1.2.1" } }, "node_modules/buffer-alloc": { @@ -11057,6 +11080,30 @@ "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": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz", diff --git a/package.json b/package.json index e8c548d..d6cdd73 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,8 @@ "@react-navigation/bottom-tabs": "^6.6.1", "@react-navigation/native": "^6.1.18", "@react-navigation/native-stack": "^6.11.0", + "bip39": "^3.1.0", + "buffer": "^6.0.3", "expo": "~52.0.0", "expo-asset": "~11.0.5", "expo-constants": "~17.0.8", diff --git a/src/config/index.ts b/src/config/index.ts index 5fb2bb7..224076a 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -112,6 +112,7 @@ export const MOCK_CONFIG = { USER: { id: 999, username: 'MockCaptain', + email: 'captain@sentinel.local', public_key: 'mock_public_key', is_admin: true, guale: false, diff --git a/src/screens/SentinelScreen.tsx b/src/screens/SentinelScreen.tsx index 3136e9b..8b9ffc5 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,12 @@ 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'; -import BiometricModal from '../components/common/BiometricModal'; -import { - SSSShare, - mnemonicToEntropy, - splitSecret, - formatShareCompact, - serializeShare, - verifyShares, -} from '../utils/sss'; - -// Vault storage keys (for testing: clear these to simulate first-open) -export const VAULT_STORAGE_KEYS = { - INITIALIZED: 'sentinel_vault_initialized', - SHARE_DEVICE: 'sentinel_share_device', -} as const; - -// Nautical-themed mnemonic word list (unique words only) -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', 'storm', 'north', 'south', 'east', 'west', - 'ember', 'cabin', 'ledger', 'torch', 'sanctum', 'oath', 'depths', 'captain', -] as const; // Animation timing constants const ANIMATION_DURATION = { @@ -58,56 +23,13 @@ const ANIMATION_DURATION = { heartbeatPress: 150, } as const; -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; -}; - -/** - * Generate SSS shares from mnemonic words - * Uses Shamir's Secret Sharing (3,2) threshold scheme - */ -const generateSSSShares = (words: string[]): SSSShare[] => { - try { - // Convert mnemonic to entropy (big integer) - const entropy = mnemonicToEntropy(words, MNEMONIC_WORDS); - - // Split entropy into 3 shares using SSS - const shares = splitSecret(entropy); - - // Verify shares can recover the original (optional, for debugging) - if (__DEV__) { - const isValid = verifyShares(shares, entropy); - if (!isValid) { - console.warn('SSS verification failed!'); - } else { - console.log('SSS shares verified successfully'); - } - } - - return shares; - } catch (error) { - console.error('Failed to generate SSS shares:', error); - // Fallback: return empty shares (should not happen in production) - return [ - { x: 1, y: BigInt(0), label: 'device' }, - { x: 2, y: BigInt(0), label: 'cloud' }, - { x: 3, y: BigInt(0), label: 'heir' }, - ]; - } -}; - // Icon names type for type safety type StatusIconName = 'checkmark-circle' | 'warning' | 'alert-circle'; // Status configuration with nautical theme -const statusConfig: Record([]); - const [sssShares, setSssShares] = useState([]); - const [showEmailForm, setShowEmailForm] = useState(false); - const [emailAddress, setEmailAddress] = useState(''); - const [emailRecipientType, setEmailRecipientType] = useState<'self' | 'heir'>('self'); - const [isCapturing, setIsCapturing] = useState(false); - const [hasVaultInitialized, setHasVaultInitialized] = useState(null); - const [showSetupBiometric, setShowSetupBiometric] = useState(false); - const mnemonicRef = useRef(null); - - // Load vault init status on mount (1.1) - useEffect(() => { - AsyncStorage.getItem(VAULT_STORAGE_KEYS.INITIALIZED) - .then((val) => setHasVaultInitialized(val === 'true')) - .catch(() => setHasVaultInitialized(false)); - }, []); useEffect(() => { - // Pulse animation const pulseAnimation = Animated.loop( Animated.sequence([ Animated.timing(pulseAnim, { @@ -204,7 +92,6 @@ export default function SentinelScreen() { ); pulseAnimation.start(); - // Glow animation const glowAnimation = Animated.loop( Animated.sequence([ Animated.timing(glowAnim, { @@ -221,7 +108,6 @@ export default function SentinelScreen() { ); glowAnimation.start(); - // Slow rotate for ship wheel const rotateAnimation = Animated.loop( Animated.timing(rotateAnim, { toValue: 1, @@ -231,7 +117,6 @@ export default function SentinelScreen() { ); rotateAnimation.start(); - // Cleanup animations on unmount to prevent memory leaks return () => { pulseAnimation.stop(); glowAnimation.stop(); @@ -239,109 +124,9 @@ export default function SentinelScreen() { }; }, [pulseAnim, glowAnim, rotateAnim]); - const startFirstTimeSetup = () => { - const words = generateMnemonic(); - const shares = generateSSSShares(words); - setMnemonicWords(words); - setSssShares(shares); - setShowMnemonic(true); - setShowVault(false); - setShowEmailForm(false); - setEmailAddress(''); - setEmailRecipientType('self'); - - if (shares[0]) { - AsyncStorage.setItem(VAULT_STORAGE_KEYS.SHARE_DEVICE, serializeShare(shares[0])).catch(() => {}); - } - }; - - const completeSetupAndEnterVault = () => { - setShowMnemonic(false); - setShowEmailForm(false); - setEmailAddress(''); - setShowSetupBiometric(true); - }; - - const handleSetupBiometricSuccess = () => { - setShowSetupBiometric(false); - AsyncStorage.setItem(VAULT_STORAGE_KEYS.INITIALIZED, 'true').catch(() => {}); - setHasVaultInitialized(true); - setShowVault(true); - }; - - const handleSetupBiometricSkip = () => { - setShowSetupBiometric(false); - AsyncStorage.setItem(VAULT_STORAGE_KEYS.INITIALIZED, 'true').catch(() => {}); - setHasVaultInitialized(true); - setShowVault(true); - }; - - const handleOpenVault = () => { - if (hasVaultInitialized === true) { - setShowVault(true); - setShowMnemonic(false); - } else { - startFirstTimeSetup(); - } - }; - - 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', - }); - completeSetupAndEnterVault(); - } catch (error) { - Alert.alert('Screenshot failed', 'Please try again or use email backup.'); - } finally { - setIsCapturing(false); - } - }; - - const handleEmailBackup = () => { - setShowEmailForm(true); - }; - - const handleCompleteBackupLocal = () => { - completeSetupAndEnterVault(); - }; - - 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( - emailRecipientType === 'heir' - ? 'Sentinel Vault - Share C (Heir)' - : 'Sentinel Vault Recovery Key' - ); - const body = encodeURIComponent( - emailRecipientType === 'heir' - ? `Share C (for heir, 2-of-3 required to recover):\n${sssShares[2] ? serializeShare(sssShares[2]) : ''}\n\nKeep this secure. Combined with Share B from Sentinel cloud, this can restore the vault.` - : `Your 12-word mnemonic (backup for yourself):\n${mnemonicWords.join(' ')}` - ); - const mailtoUrl = `mailto:${trimmed}?subject=${subject}&body=${body}`; - - try { - await Linking.openURL(mailtoUrl); - completeSetupAndEnterVault(); - } catch (error) { - Alert.alert('Email failed', 'Unable to open email client.'); - } - }; + const openVault = () => setShowVault(true); const handleHeartbeat = () => { - // Animate pulse Animated.sequence([ Animated.timing(pulseAnim, { toValue: 1.15, @@ -355,43 +140,34 @@ export default function SentinelScreen() { }), ]).start(); - // Add new log using functional update to avoid stale closure const newLog: KillSwitchLog = { id: Date.now().toString(), action: 'HEARTBEAT_CONFIRMED', timestamp: new Date(), }; setLogs((prevLogs) => [newLog, ...prevLogs]); - - // Reset status if warning + if (status === 'warning') { setStatus('normal'); } }; - const formatDateTime = (date: Date) => { - return date.toLocaleString('en-US', { + const formatDateTime = (date: Date) => + date.toLocaleString('en-US', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', }); - }; const formatTimeAgo = (date: Date) => { const now = new Date(); const diff = now.getTime() - date.getTime(); const hours = Math.floor(diff / (1000 * 60 * 60)); const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)); - - if (hours > 24) { - const days = Math.floor(hours / 24); - return `${days} days ago`; - } - if (hours > 0) { - return `${hours}h ${minutes}m ago`; - } + if (hours > 24) return `${Math.floor(hours / 24)} days ago`; + if (hours > 0) return `${hours}h ${minutes}m ago`; return `${minutes}m ago`; }; @@ -408,7 +184,7 @@ export default function SentinelScreen() { style={styles.gradient} > - - @@ -453,10 +229,10 @@ export default function SentinelScreen() { {/* Ship Wheel Watermark */} - @@ -469,24 +245,16 @@ export default function SentinelScreen() { SUBSCRIPTION - - {formatTimeAgo(lastSubscriptionCheck)} - - - {formatDateTime(lastSubscriptionCheck)} - + {formatTimeAgo(lastSubscriptionCheck)} + {formatDateTime(lastSubscriptionCheck)} LAST JOURNAL - - {formatTimeAgo(lastFlowActivity)} - - - {formatDateTime(lastFlowActivity)} - + {formatTimeAgo(lastFlowActivity)} + {formatDateTime(lastFlowActivity)} @@ -503,7 +271,7 @@ export default function SentinelScreen() { {log.action} - - {formatDateTime(log.timestamp)} - + {formatDateTime(log.timestamp)} ))} @@ -565,7 +331,7 @@ export default function SentinelScreen() { onRequestClose={() => setShowVault(false)} > - + {showVault ? : null} setShowVault(false)} @@ -577,184 +343,20 @@ export default function SentinelScreen() { - - {/* Mnemonic Modal */} - setShowMnemonic(false)} - > - - - - - setShowMnemonic(false)} - activeOpacity={0.85} - accessibilityLabel="Close mnemonic modal" - accessibilityRole="button" - > - - - - - 12-Word Mnemonic - - - Your seed is protected by SSS (3,2) threshold encryption. Any 2 shares can restore your vault. - - - - {mnemonicWords.join(' ')} - - - - - SHARE A • DEVICE - - {sssShares[0] ? formatShareCompact(sssShares[0]) : '---'} - - Stored on this device - - - SHARE B • CLOUD - - {sssShares[1] ? formatShareCompact(sssShares[1]) : '---'} - - To be synced to Sentinel - - - SHARE C • HEIR - - {sssShares[2] ? formatShareCompact(sssShares[2]) : '---'} - - For your heir (2-of-3 required) - - - - - {isCapturing ? 'CAPTURING...' : 'PHYSICAL BACKUP (SCREENSHOT)'} - - - - EMAIL BACKUP - - {showEmailForm ? ( - - - setEmailRecipientType('self')} - > - - To Myself - - - setEmailRecipientType('heir')} - > - - To Heir - - - - - - SEND EMAIL - - - ) : null} - - COMPLETE BACKUP (LOCAL) - - - - - - - - {/* Biometric setup prompt after first-time backup (1.4) */} - ); } const styles = StyleSheet.create({ - container: { - flex: 1, - }, - gradient: { - flex: 1, - }, - safeArea: { - flex: 1, - }, - scrollView: { - flex: 1, - }, + container: { flex: 1 }, + gradient: { flex: 1 }, + safeArea: { flex: 1 }, + scrollView: { flex: 1 }, scrollContent: { padding: spacing.lg, paddingBottom: 120, }, - header: { - marginBottom: spacing.xl, - }, + header: { marginBottom: spacing.xl }, headerTitleRow: { flexDirection: 'row', alignItems: 'center', @@ -860,9 +462,7 @@ const styles = StyleSheet.create({ marginBottom: spacing.xl, ...shadows.medium, }, - heartbeatGradient: { - padding: spacing.lg, - }, + heartbeatGradient: { padding: spacing.lg }, heartbeatContent: { flexDirection: 'row', alignItems: 'center', @@ -916,9 +516,7 @@ const styles = StyleSheet.create({ marginTop: 6, marginRight: spacing.md, }, - logContent: { - flex: 1, - }, + logContent: { flex: 1 }, logAction: { fontSize: typography.fontSize.sm, color: colors.sentinel.text, @@ -931,7 +529,6 @@ const styles = StyleSheet.create({ color: colors.sentinel.textSecondary, fontFamily: typography.fontFamily.mono, }, - // Shadow Vault Access Card vaultAccessCard: { flexDirection: 'row', alignItems: 'center', @@ -951,9 +548,7 @@ const styles = StyleSheet.create({ justifyContent: 'center', marginRight: spacing.md, }, - vaultAccessContent: { - flex: 1, - }, + vaultAccessContent: { flex: 1 }, vaultAccessTitle: { fontSize: typography.fontSize.base, fontWeight: '600', @@ -975,7 +570,6 @@ const styles = StyleSheet.create({ fontWeight: '700', fontSize: typography.fontSize.sm, }, - // Vault Modal vaultModalContainer: { flex: 1, backgroundColor: colors.vault.background, @@ -991,196 +585,4 @@ const styles = StyleSheet.create({ alignItems: 'center', justifyContent: 'center', }, - mnemonicOverlay: { - flex: 1, - backgroundColor: 'rgba(11, 20, 24, 0.72)', - justifyContent: 'center', - padding: spacing.lg, - }, - mnemonicScroll: { - flex: 1, - }, - mnemonicScrollContent: { - flexGrow: 1, - justifyContent: 'center', - }, - 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, - }, - mnemonicTertiaryButton: { - backgroundColor: 'transparent', - paddingVertical: spacing.sm, - borderRadius: borderRadius.full, - alignItems: 'center', - marginTop: spacing.sm, - borderWidth: 1, - borderColor: colors.sentinel.cardBorder, - borderStyle: 'dashed', - }, - mnemonicTertiaryText: { - color: colors.sentinel.textSecondary, - fontWeight: '600', - letterSpacing: typography.letterSpacing.wide, - fontSize: typography.fontSize.sm, - }, - emailTypeRow: { - flexDirection: 'row', - gap: spacing.sm, - marginBottom: spacing.sm, - }, - emailTypeButton: { - flex: 1, - paddingVertical: spacing.sm, - borderRadius: borderRadius.lg, - alignItems: 'center', - borderWidth: 1, - borderColor: colors.sentinel.cardBorder, - }, - emailTypeButtonActive: { - borderColor: colors.sentinel.primary, - backgroundColor: `${colors.sentinel.primary}15`, - }, - emailTypeText: { - fontSize: typography.fontSize.sm, - color: colors.sentinel.textSecondary, - fontWeight: '600', - }, - emailTypeTextActive: { - color: colors.sentinel.primary, - }, - 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..33bc48d 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,16 +7,23 @@ 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 * as bip39 from 'bip39'; 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'; +import { useAuth } from '../context/AuthContext'; // Asset type configuration with nautical theme const assetTypeConfig: Record = { @@ -35,6 +42,52 @@ const accountProviderOptions = [ { 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[] = [ { @@ -104,6 +157,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 [showLegacyAssignCta, setShowLegacyAssignCta] = 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 [replaceIndex, setReplaceIndex] = useState(null); + const [replaceQuery, setReplaceQuery] = useState(''); + const [progressIndex, setProgressIndex] = useState(0); + const [progressAnim] = useState(new Animated.Value(0)); + const { user } = useAuth(); + const [isCapturing, setIsCapturing] = useState(false); + const mnemonicRef = useRef(null); + const progressTimerRef = useRef | null>(null); useEffect(() => { if (!isUnlocked) { @@ -145,9 +215,156 @@ export default function VaultScreen() { 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'); @@ -271,9 +488,345 @@ export default function VaultScreen() { && addVerified && (selectedType !== 'private_key' || rehearsalConfirmed); + const mnemonicModal = ( + setShowMnemonic(false)} + > + + + setShowMnemonic(false)} + activeOpacity={0.85} + > + + + + + Mnemonic Setup + + {mnemonicStep === 1 ? ( + <> + + Review your 12-word mnemonic. Tap any word to replace it. + + + {mnemonicWords.map((word, index) => ( + setReplaceIndex(index)} + activeOpacity={0.8} + > + + {index + 1} + + + {word} + + + ))} + + {replaceIndex !== null ? ( + + + Replace word {replaceIndex + 1} + + + + {(replaceQuery + ? bip39.wordlists.english.filter((word) => + word.startsWith(replaceQuery.toLowerCase()) + ) + : bip39.wordlists.english + ) + .slice(0, 24) + .map((word) => ( + handleReplaceWord(word)} + activeOpacity={0.8} + > + {word} + + ))} + + setReplaceIndex(null)} + activeOpacity={0.85} + > + CANCEL + + + ) : null} + setMnemonicStep(2)} + activeOpacity={0.85} + > + NEXT + + + ) : 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 + + + ) : 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]; + return ( + handleSelectHeirAsset(asset)} + activeOpacity={0.8} + > + + {renderAssetTypeIcon(config, 18, colors.vault.primary)} + + + {asset.label} + {config.label} + + + ); + })} + + 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. + + + Vault Item + {selectedHeirAsset?.label} + Legacy Handler + {selectedHeir?.name} + {selectedHeir?.email} + Release Tier + Tier {selectedHeir?.releaseLevel} + + + SUBMIT + + setHeirStep('heir')} + activeOpacity={0.85} + > + EDIT + + + ) : null} + + ) : null} + + + + + + + + ); + // Lock screen - if (!isUnlocked) { - return ( + const lockScreen = ( ); - } - return ( + const vaultScreen = ( + {showLegacyAssignCta && ( + + + Legacy Assignment + + Continue assigning a vault item to your legacy handler. + + + + Continue + + + )} + {/* Asset List */} ); + + return ( + + {isUnlocked ? vaultScreen : lockScreen} + {mnemonicModal} + + ); } const styles = StyleSheet.create({ + root: { + flex: 1, + }, container: { flex: 1, }, @@ -941,6 +1521,42 @@ const styles = StyleSheet.create({ 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, }, @@ -1527,4 +2143,308 @@ 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', + }, + 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, + }, }); diff --git a/src/types/index.ts b/src/types/index.ts index 5d46372..8045de0 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -77,6 +77,7 @@ export interface ProtocolInfo { export interface User { id: number; username: string; + email?: string; public_key: string; is_admin: boolean; guale: boolean;