diff --git a/src/config/index.ts b/src/config/index.ts index 224076a..2eec012 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -27,7 +27,7 @@ export const DEBUG_MODE = true; /** * Base URL for the backend API server */ -export const API_BASE_URL = 'http://192.168.56.103:8000'; +export const API_BASE_URL = 'http://localhost:8000'; /** * API request timeout in milliseconds @@ -64,6 +64,20 @@ export const API_ENDPOINTS = { }, } as const; +/** + * Vault storage key names (AsyncStorage keys only — not user-editable "initial values"). + * - These are constants: the key names used to read/write/remove vault state in AsyncStorage. + * - The actual stored values (S0 data, '1') are set by the app; do not change these key strings + * unless you are migrating storage (changing them would make existing data unfindable). + * - Placed in config so VaultScreen and MeScreen (and others) use the same keys in one place. + * - INITIALIZED: app sets to '1' after first mnemonic flow; SHARE_DEVICE: app stores serialized S0. + * - "Reset Vault State" = remove both keys; next vault open sees no S0 and shows mnemonic flow. + */ +export const VAULT_STORAGE_KEYS = { + INITIALIZED: 'sentinel_vault_initialized', + SHARE_DEVICE: 'sentinel_vault_s0', +} as const; + // ============================================================================= // Helper Functions // ============================================================================= diff --git a/src/screens/MeScreen.tsx b/src/screens/MeScreen.tsx index 5d5847b..24e360b 100644 --- a/src/screens/MeScreen.tsx +++ b/src/screens/MeScreen.tsx @@ -18,7 +18,7 @@ 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'; +import { VAULT_STORAGE_KEYS } from '../config'; // Mock heirs data const initialHeirs: Heir[] = [ @@ -248,6 +248,7 @@ export default function MeScreen() { }); const [sanctumArchive, setSanctumArchive] = useState<'off' | 'standard' | 'strict'>('standard'); const [sanctumRehearsal, setSanctumRehearsal] = useState<'monthly' | 'quarterly'>('quarterly'); + const [resetVaultFeedback, setResetVaultFeedback] = useState<{ status: 'idle' | 'success' | 'error'; message: string }>({ status: 'idle', message: '' }); const [triggerDisconnectDays, setTriggerDisconnectDays] = useState(30); const [triggerGraceDays, setTriggerGraceDays] = useState(15); const [triggerSource, setTriggerSource] = useState<'dual' | 'subscription' | 'activity'>('dual'); @@ -308,17 +309,29 @@ export default function MeScreen() { }; const handleResetVault = async () => { + setResetVaultFeedback({ status: 'idle', message: '' }); 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.'); + setResetVaultFeedback({ + status: 'success', + message: 'Vault state has been reset. Next time you open Shadow Vault you will see the mnemonic flow again.', + }); } catch (e) { - Alert.alert('Error', 'Failed to reset vault state.'); + setResetVaultFeedback({ + status: 'error', + message: 'Failed to reset vault state. Please try again.', + }); } }; + const handleCloseSanctumModal = () => { + setResetVaultFeedback({ status: 'idle', message: '' }); + setShowSanctumModal(false); + }; + return ( setShowSanctumModal(false)} + onRequestClose={handleCloseSanctumModal} > @@ -908,7 +921,31 @@ export default function MeScreen() { Reset Vault State - Clear hasVaultInitialized & Share A. Test first-open flow. + Clear S0 (SHARE_DEVICE) from storage. Next vault open uses mnemonic flow. + {resetVaultFeedback.status !== 'idle' && ( + + + + {resetVaultFeedback.status === 'success' ? 'Success' : 'Error'} + {' — '} + {resetVaultFeedback.message} + + + )} )} @@ -916,7 +953,7 @@ export default function MeScreen() { setShowSanctumModal(false)} + onPress={handleCloseSanctumModal} > Save @@ -924,7 +961,7 @@ export default function MeScreen() { setShowSanctumModal(false)} + onPress={handleCloseSanctumModal} > Close @@ -1910,6 +1947,34 @@ const styles = StyleSheet.create({ fontSize: typography.fontSize.sm, color: colors.nautical.coral, }, + resetVaultFeedback: { + flexDirection: 'row', + alignItems: 'center', + gap: spacing.sm, + borderRadius: borderRadius.lg, + padding: spacing.base, + marginTop: spacing.md, + }, + resetVaultFeedbackSuccess: { + backgroundColor: 'rgba(107, 191, 138, 0.2)', + borderWidth: 1, + borderColor: 'rgba(107, 191, 138, 0.5)', + }, + resetVaultFeedbackError: { + backgroundColor: 'rgba(229, 115, 115, 0.2)', + borderWidth: 1, + borderColor: 'rgba(229, 115, 115, 0.5)', + }, + resetVaultFeedbackText: { + flex: 1, + fontSize: typography.fontSize.sm, + }, + resetVaultFeedbackTextSuccess: { + color: '#2E7D5E', + }, + resetVaultFeedbackTextError: { + color: colors.nautical.coral, + }, confirmPulseButton: { flexDirection: 'row', alignItems: 'center', diff --git a/src/screens/SentinelScreen.tsx b/src/screens/SentinelScreen.tsx index 8b9ffc5..3ad34b2 100644 --- a/src/screens/SentinelScreen.tsx +++ b/src/screens/SentinelScreen.tsx @@ -65,6 +65,8 @@ const initialLogs: KillSwitchLog[] = [ { id: '4', action: 'HEARTBEAT_CONFIRMED', timestamp: new Date('2024-01-15T11:20:00') }, ]; +export { VAULT_STORAGE_KEYS } from '../config'; + export default function SentinelScreen() { const [status, setStatus] = useState('normal'); const [lastSubscriptionCheck] = useState(new Date('2024-01-18T00:00:00')); diff --git a/src/screens/VaultScreen.tsx b/src/screens/VaultScreen.tsx index 0f629f1..d7d9576 100644 --- a/src/screens/VaultScreen.tsx +++ b/src/screens/VaultScreen.tsx @@ -25,6 +25,8 @@ import { VaultAsset, VaultAssetType, Heir } from '../types'; import BiometricModal from '../components/common/BiometricModal'; import { useAuth } from '../context/AuthContext'; import { useVaultAssets } from '../hooks/useVaultAssets'; +import { VAULT_STORAGE_KEYS } from '../config'; +import { mnemonicToEntropy, splitSecret, serializeShare } from '../utils/sss'; // Asset type configuration with nautical theme const assetTypeConfig: Record = { @@ -168,6 +170,7 @@ export default function VaultScreen() { const [accountProvider, setAccountProvider] = useState<'bank' | 'steam' | 'facebook' | 'custom'>('bank'); const [showMnemonic, setShowMnemonic] = useState(false); const [showLegacyAssignCta, setShowLegacyAssignCta] = useState(false); + const [hasS0, setHasS0] = useState(null); const [mnemonicWords, setMnemonicWords] = useState([]); const [mnemonicParts, setMnemonicParts] = useState([]); const [mnemonicStep, setMnemonicStep] = useState<1 | 2 | 3 | 4 | 5>(1); @@ -185,15 +188,31 @@ export default function VaultScreen() { const mnemonicRef = useRef(null); const progressTimerRef = useRef | null>(null); + // Detect S0 (TEE/SE): if present, later open shows biometric only; if not, mnemonic flow useEffect(() => { - if (!isUnlocked) { - const timer = setTimeout(() => { - setShowBiometric(true); - }, 500); - return () => clearTimeout(timer); - } + let cancelled = false; + AsyncStorage.getItem(VAULT_STORAGE_KEYS.SHARE_DEVICE) + .then((v) => { + if (!cancelled) setHasS0(!!v); + }) + .catch(() => { + if (!cancelled) setHasS0(false); + }); + return () => { cancelled = true; }; }, []); + // Only when S0 exists and vault not unlocked: show biometric after short delay. + // When hasS0 is false or null, never show biometric — go straight to mnemonic flow. + useEffect(() => { + if (hasS0 !== true) { + setShowBiometric(false); + return; + } + if (isUnlocked) return; + const timer = setTimeout(() => setShowBiometric(true), 500); + return () => clearTimeout(timer); + }, [isUnlocked, hasS0]); + useEffect(() => { if (isUnlocked) { Animated.timing(fadeAnim, { @@ -335,28 +354,49 @@ export default function VaultScreen() { const handleHeirDecision = (share: boolean) => { // Placeholder for future heir flow - setShowMnemonic(false); - setIsUnlocked(true); + finishMnemonicThenShowBiometric(); }; const handleSubmitAssignment = () => { // Placeholder for submitting assignment to API - setShowMnemonic(false); - setIsUnlocked(true); + finishMnemonicThenShowBiometric(); }; const handleHeirYes = () => { - setShowMnemonic(false); - setIsUnlocked(true); + finishMnemonicThenShowBiometric(); setShowLegacyAssignCta(true); }; const handleHeirNo = () => { - setShowMnemonic(false); - setIsUnlocked(true); + finishMnemonicThenShowBiometric(); setShowLegacyAssignCta(true); }; + /** After mnemonic flow: persist S0 (simulated TEE/SE via AsyncStorage), then show biometric; unlock on success */ + const finishMnemonicThenShowBiometric = async () => { + try { + const wordList = bip39.wordlists.english; + const entropy = mnemonicToEntropy(mnemonicWords, wordList); + const shares = splitSecret(entropy); + const s0 = shares[0]; // device share (S0) + // S0 is stored in AsyncStorage under SHARE_DEVICE — app-level storage, not hardware TEE/SE + await AsyncStorage.setItem(VAULT_STORAGE_KEYS.SHARE_DEVICE, serializeShare(s0)); + await AsyncStorage.setItem(VAULT_STORAGE_KEYS.INITIALIZED, '1'); + setHasS0(true); + setShowMnemonic(false); + setShowBiometric(true); + } catch (e) { + if (__DEV__) console.warn('finishMnemonicThenShowBiometric', e); + setShowMnemonic(false); + setShowBiometric(true); + } + }; + + const handleBiometricSuccess = () => { + setShowBiometric(false); + setIsUnlocked(true); + }; + const handleOpenLegacyAssign = () => { setSelectedHeir(null); setSelectedHeirAsset(null); @@ -888,7 +928,7 @@ export default function VaultScreen() { setShowBiometric(true)} + onPress={hasS0 ? () => setShowBiometric(true) : handleUnlock} activeOpacity={0.8} > - - Captain's Verification + + + {hasS0 === true ? "Captain's Verification" : hasS0 === false ? 'Enter Vault' : 'Loading…'} + @@ -906,8 +948,8 @@ export default function VaultScreen() { setShowBiometric(false)} title="Enter the Vault" message="Verify your identity to access your treasures"