feat(vault): show mnemonic flow will show at first time ; or user reset vault state;
This commit is contained in:
@@ -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
|
||||
// =============================================================================
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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'));
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user