diff --git a/src/screens/MeScreen.tsx b/src/screens/MeScreen.tsx index 2c0e463..6cf4c2b 100644 --- a/src/screens/MeScreen.tsx +++ b/src/screens/MeScreen.tsx @@ -14,9 +14,11 @@ import { import { LinearGradient } from 'expo-linear-gradient'; import { Ionicons, Feather, MaterialCommunityIcons, FontAwesome5 } from '@expo/vector-icons'; import { colors, typography, spacing, borderRadius, shadows } from '../theme/colors'; +import AsyncStorage from '@react-native-async-storage/async-storage'; import { useAuth } from '../context/AuthContext'; import { Heir, HeirStatus, PaymentStrategy } from '../types'; import HeritageScreen from './HeritageScreen'; +import { VAULT_STORAGE_KEYS } from './SentinelScreen'; // Mock heirs data const initialHeirs: Heir[] = [ @@ -308,6 +310,18 @@ export default function MeScreen() { ); }; + const handleResetVault = async () => { + try { + await AsyncStorage.multiRemove([ + VAULT_STORAGE_KEYS.INITIALIZED, + VAULT_STORAGE_KEYS.SHARE_DEVICE, + ]); + Alert.alert('Done', 'Vault state reset. Go to Sentinel → Open Shadow Vault to see first-time flow.'); + } catch (e) { + Alert.alert('Error', 'Failed to reset vault state.'); + } + }; + return ( View + + {__DEV__ && ( + + DEV ONLY + + + Reset Vault State + + Clear hasVaultInitialized & Share A. Test first-open flow. + + )} ([]); 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( @@ -222,7 +239,7 @@ export default function SentinelScreen() { }; }, [pulseAnim, glowAnim, rotateAnim]); - const openVaultWithMnemonic = () => { + const startFirstTimeSetup = () => { const words = generateMnemonic(); const shares = generateSSSShares(words); setMnemonicWords(words); @@ -231,12 +248,40 @@ export default function SentinelScreen() { setShowVault(false); setShowEmailForm(false); setEmailAddress(''); - - // Store Share A (device share) locally + setEmailRecipientType('self'); + if (shares[0]) { - AsyncStorage.setItem('sentinel_share_device', serializeShare(shares[0])).catch(() => { - // Best-effort local store; UI remains available - }); + 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(); } }; @@ -252,8 +297,7 @@ export default function SentinelScreen() { url: uri, message: 'Sentinel key backup', }); - setShowMnemonic(false); - setShowVault(true); + completeSetupAndEnterVault(); } catch (error) { Alert.alert('Screenshot failed', 'Please try again or use email backup.'); } finally { @@ -265,6 +309,10 @@ export default function SentinelScreen() { setShowEmailForm(true); }; + const handleCompleteBackupLocal = () => { + completeSetupAndEnterVault(); + }; + const handleSendEmail = async () => { const trimmed = emailAddress.trim(); if (!trimmed || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(trimmed)) { @@ -272,16 +320,21 @@ export default function SentinelScreen() { return; } - const subject = encodeURIComponent('Sentinel Vault Recovery Key'); - const body = encodeURIComponent(`Your 12-word mnemonic:\n${mnemonicWords.join(' ')}`); + 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); - setShowMnemonic(false); - setShowEmailForm(false); - setEmailAddress(''); - setShowVault(true); + completeSetupAndEnterVault(); } catch (error) { Alert.alert('Email failed', 'Unable to open email client.'); } @@ -450,7 +503,7 @@ export default function SentinelScreen() { + EMAIL BACKUP {showEmailForm ? ( + + setEmailRecipientType('self')} + > + + To Myself + + + setEmailRecipientType('heir')} + > + + To Heir + + + ) : null} + + COMPLETE BACKUP (LOCAL) + + + + {/* Biometric setup prompt after first-time backup (1.4) */} + ); } @@ -900,6 +997,13 @@ const styles = StyleSheet.create({ justifyContent: 'center', padding: spacing.lg, }, + mnemonicScroll: { + flex: 1, + }, + mnemonicScrollContent: { + flexGrow: 1, + justifyContent: 'center', + }, mnemonicCard: { borderRadius: borderRadius.xl, padding: spacing.lg, @@ -1013,6 +1117,47 @@ const styles = StyleSheet.create({ 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, }, diff --git a/src/utils/sss.ts b/src/utils/sss.ts index a0d3928..6ca1a3f 100644 --- a/src/utils/sss.ts +++ b/src/utils/sss.ts @@ -1,11 +1,16 @@ /** * Shamir's Secret Sharing (SSS) Implementation - * + * * This implements a (3,2) threshold scheme where: * - Secret is split into 3 shares * - Any 2 shares can recover the original secret - * - * Based on the Sentinel crypto_core_demo Python implementation. + * + * Correspondence with crypto_core_demo (Python): + * - sp_trust_sharding.py: split_to_shares(), recover_from_shares() + * - Same algorithm: f(x) = secret + a*x (mod P), Lagrange interpolation + * - Difference: entropy conversion. Python uses BIP-39 (mnemonic.to_entropy); + * we use custom word list index-based encoding for compatibility with + * existing MNEMONIC_WORDS. SSS split/recover logic is identical. */ // Use a large prime for the finite field