feat(vault): show mnemonic flow will show at first time ; or user reset vault state;

This commit is contained in:
Ada
2026-02-01 11:02:14 -08:00
parent 7b8511f080
commit 8e6c621f7b
4 changed files with 150 additions and 27 deletions

View File

@@ -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
// =============================================================================

View File

@@ -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 (
<View style={styles.container}>
<LinearGradient
@@ -760,7 +773,7 @@ export default function MeScreen() {
visible={showSanctumModal}
animationType="fade"
transparent
onRequestClose={() => setShowSanctumModal(false)}
onRequestClose={handleCloseSanctumModal}
>
<View style={styles.spiritOverlay}>
<View style={styles.spiritModal}>
@@ -908,7 +921,31 @@ export default function MeScreen() {
<Ionicons name="refresh" size={16} color={colors.nautical.coral} />
<Text style={styles.devResetText}>Reset Vault State</Text>
</TouchableOpacity>
<Text style={styles.sanctumHint}>Clear hasVaultInitialized & Share A. Test first-open flow.</Text>
<Text style={styles.sanctumHint}>Clear S0 (SHARE_DEVICE) from storage. Next vault open uses mnemonic flow.</Text>
{resetVaultFeedback.status !== 'idle' && (
<View
style={[
styles.resetVaultFeedback,
resetVaultFeedback.status === 'success' ? styles.resetVaultFeedbackSuccess : styles.resetVaultFeedbackError,
]}
>
<Ionicons
name={resetVaultFeedback.status === 'success' ? 'checkmark-circle' : 'alert-circle'}
size={20}
color={resetVaultFeedback.status === 'success' ? colors.sentinel?.statusNormal ?? '#6BBF8A' : colors.nautical.coral}
/>
<Text
style={[
styles.resetVaultFeedbackText,
resetVaultFeedback.status === 'success' ? styles.resetVaultFeedbackTextSuccess : styles.resetVaultFeedbackTextError,
]}
>
{resetVaultFeedback.status === 'success' ? 'Success' : 'Error'}
{' — '}
{resetVaultFeedback.message}
</Text>
</View>
)}
</View>
)}
</ScrollView>
@@ -916,7 +953,7 @@ export default function MeScreen() {
<TouchableOpacity
style={styles.confirmPulseButton}
activeOpacity={0.85}
onPress={() => setShowSanctumModal(false)}
onPress={handleCloseSanctumModal}
>
<Ionicons name="checkmark-circle" size={18} color={colors.nautical.teal} />
<Text style={styles.confirmPulseText}>Save</Text>
@@ -924,7 +961,7 @@ export default function MeScreen() {
<TouchableOpacity
style={styles.confirmPulseButton}
activeOpacity={0.85}
onPress={() => setShowSanctumModal(false)}
onPress={handleCloseSanctumModal}
>
<Ionicons name="close-circle" size={18} color={colors.nautical.teal} />
<Text style={styles.confirmPulseText}>Close</Text>
@@ -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',

View File

@@ -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<SystemStatus>('normal');
const [lastSubscriptionCheck] = useState(new Date('2024-01-18T00:00:00'));

View File

@@ -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<VaultAssetType, { icon: string; iconType: 'ionicons' | 'feather' | 'material' | 'fontawesome5'; label: string }> = {
@@ -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<boolean | null>(null);
const [mnemonicWords, setMnemonicWords] = useState<string[]>([]);
const [mnemonicParts, setMnemonicParts] = useState<string[][]>([]);
const [mnemonicStep, setMnemonicStep] = useState<1 | 2 | 3 | 4 | 5>(1);
@@ -185,15 +188,31 @@ export default function VaultScreen() {
const mnemonicRef = useRef<View>(null);
const progressTimerRef = useRef<ReturnType<typeof setInterval> | 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() {
<TouchableOpacity
style={styles.unlockButton}
onPress={() => setShowBiometric(true)}
onPress={hasS0 ? () => setShowBiometric(true) : handleUnlock}
activeOpacity={0.8}
>
<LinearGradient
@@ -897,8 +937,10 @@ export default function VaultScreen() {
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>
<Ionicons name={hasS0 ? 'finger-print' : 'key'} size={20} color={colors.vault.background} />
<Text style={styles.unlockButtonText}>
{hasS0 === true ? "Captain's Verification" : hasS0 === false ? 'Enter Vault' : 'Loading…'}
</Text>
</LinearGradient>
</TouchableOpacity>
</View>
@@ -906,8 +948,8 @@ export default function VaultScreen() {
</LinearGradient>
<BiometricModal
visible={showBiometric}
onSuccess={handleUnlock}
visible={hasS0 === true && showBiometric}
onSuccess={handleBiometricSuccess}
onCancel={() => setShowBiometric(false)}
title="Enter the Vault"
message="Verify your identity to access your treasures"