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
|
* 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
|
* API request timeout in milliseconds
|
||||||
@@ -64,6 +64,20 @@ export const API_ENDPOINTS = {
|
|||||||
},
|
},
|
||||||
} as const;
|
} 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
|
// Helper Functions
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import AsyncStorage from '@react-native-async-storage/async-storage';
|
|||||||
import { useAuth } from '../context/AuthContext';
|
import { useAuth } from '../context/AuthContext';
|
||||||
import { Heir, HeirStatus, PaymentStrategy } from '../types';
|
import { Heir, HeirStatus, PaymentStrategy } from '../types';
|
||||||
import HeritageScreen from './HeritageScreen';
|
import HeritageScreen from './HeritageScreen';
|
||||||
import { VAULT_STORAGE_KEYS } from './SentinelScreen';
|
import { VAULT_STORAGE_KEYS } from '../config';
|
||||||
|
|
||||||
// Mock heirs data
|
// Mock heirs data
|
||||||
const initialHeirs: Heir[] = [
|
const initialHeirs: Heir[] = [
|
||||||
@@ -248,6 +248,7 @@ export default function MeScreen() {
|
|||||||
});
|
});
|
||||||
const [sanctumArchive, setSanctumArchive] = useState<'off' | 'standard' | 'strict'>('standard');
|
const [sanctumArchive, setSanctumArchive] = useState<'off' | 'standard' | 'strict'>('standard');
|
||||||
const [sanctumRehearsal, setSanctumRehearsal] = useState<'monthly' | 'quarterly'>('quarterly');
|
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 [triggerDisconnectDays, setTriggerDisconnectDays] = useState(30);
|
||||||
const [triggerGraceDays, setTriggerGraceDays] = useState(15);
|
const [triggerGraceDays, setTriggerGraceDays] = useState(15);
|
||||||
const [triggerSource, setTriggerSource] = useState<'dual' | 'subscription' | 'activity'>('dual');
|
const [triggerSource, setTriggerSource] = useState<'dual' | 'subscription' | 'activity'>('dual');
|
||||||
@@ -308,17 +309,29 @@ export default function MeScreen() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleResetVault = async () => {
|
const handleResetVault = async () => {
|
||||||
|
setResetVaultFeedback({ status: 'idle', message: '' });
|
||||||
try {
|
try {
|
||||||
await AsyncStorage.multiRemove([
|
await AsyncStorage.multiRemove([
|
||||||
VAULT_STORAGE_KEYS.INITIALIZED,
|
VAULT_STORAGE_KEYS.INITIALIZED,
|
||||||
VAULT_STORAGE_KEYS.SHARE_DEVICE,
|
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) {
|
} 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 (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<LinearGradient
|
<LinearGradient
|
||||||
@@ -760,7 +773,7 @@ export default function MeScreen() {
|
|||||||
visible={showSanctumModal}
|
visible={showSanctumModal}
|
||||||
animationType="fade"
|
animationType="fade"
|
||||||
transparent
|
transparent
|
||||||
onRequestClose={() => setShowSanctumModal(false)}
|
onRequestClose={handleCloseSanctumModal}
|
||||||
>
|
>
|
||||||
<View style={styles.spiritOverlay}>
|
<View style={styles.spiritOverlay}>
|
||||||
<View style={styles.spiritModal}>
|
<View style={styles.spiritModal}>
|
||||||
@@ -908,7 +921,31 @@ export default function MeScreen() {
|
|||||||
<Ionicons name="refresh" size={16} color={colors.nautical.coral} />
|
<Ionicons name="refresh" size={16} color={colors.nautical.coral} />
|
||||||
<Text style={styles.devResetText}>Reset Vault State</Text>
|
<Text style={styles.devResetText}>Reset Vault State</Text>
|
||||||
</TouchableOpacity>
|
</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>
|
</View>
|
||||||
)}
|
)}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
@@ -916,7 +953,7 @@ export default function MeScreen() {
|
|||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.confirmPulseButton}
|
style={styles.confirmPulseButton}
|
||||||
activeOpacity={0.85}
|
activeOpacity={0.85}
|
||||||
onPress={() => setShowSanctumModal(false)}
|
onPress={handleCloseSanctumModal}
|
||||||
>
|
>
|
||||||
<Ionicons name="checkmark-circle" size={18} color={colors.nautical.teal} />
|
<Ionicons name="checkmark-circle" size={18} color={colors.nautical.teal} />
|
||||||
<Text style={styles.confirmPulseText}>Save</Text>
|
<Text style={styles.confirmPulseText}>Save</Text>
|
||||||
@@ -924,7 +961,7 @@ export default function MeScreen() {
|
|||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.confirmPulseButton}
|
style={styles.confirmPulseButton}
|
||||||
activeOpacity={0.85}
|
activeOpacity={0.85}
|
||||||
onPress={() => setShowSanctumModal(false)}
|
onPress={handleCloseSanctumModal}
|
||||||
>
|
>
|
||||||
<Ionicons name="close-circle" size={18} color={colors.nautical.teal} />
|
<Ionicons name="close-circle" size={18} color={colors.nautical.teal} />
|
||||||
<Text style={styles.confirmPulseText}>Close</Text>
|
<Text style={styles.confirmPulseText}>Close</Text>
|
||||||
@@ -1910,6 +1947,34 @@ const styles = StyleSheet.create({
|
|||||||
fontSize: typography.fontSize.sm,
|
fontSize: typography.fontSize.sm,
|
||||||
color: colors.nautical.coral,
|
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: {
|
confirmPulseButton: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
|||||||
@@ -65,6 +65,8 @@ const initialLogs: KillSwitchLog[] = [
|
|||||||
{ id: '4', action: 'HEARTBEAT_CONFIRMED', timestamp: new Date('2024-01-15T11:20:00') },
|
{ id: '4', action: 'HEARTBEAT_CONFIRMED', timestamp: new Date('2024-01-15T11:20:00') },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export { VAULT_STORAGE_KEYS } from '../config';
|
||||||
|
|
||||||
export default function SentinelScreen() {
|
export default function SentinelScreen() {
|
||||||
const [status, setStatus] = useState<SystemStatus>('normal');
|
const [status, setStatus] = useState<SystemStatus>('normal');
|
||||||
const [lastSubscriptionCheck] = useState(new Date('2024-01-18T00:00:00'));
|
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 BiometricModal from '../components/common/BiometricModal';
|
||||||
import { useAuth } from '../context/AuthContext';
|
import { useAuth } from '../context/AuthContext';
|
||||||
import { useVaultAssets } from '../hooks/useVaultAssets';
|
import { useVaultAssets } from '../hooks/useVaultAssets';
|
||||||
|
import { VAULT_STORAGE_KEYS } from '../config';
|
||||||
|
import { mnemonicToEntropy, splitSecret, serializeShare } from '../utils/sss';
|
||||||
|
|
||||||
// Asset type configuration with nautical theme
|
// Asset type configuration with nautical theme
|
||||||
const assetTypeConfig: Record<VaultAssetType, { icon: string; iconType: 'ionicons' | 'feather' | 'material' | 'fontawesome5'; label: string }> = {
|
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 [accountProvider, setAccountProvider] = useState<'bank' | 'steam' | 'facebook' | 'custom'>('bank');
|
||||||
const [showMnemonic, setShowMnemonic] = useState(false);
|
const [showMnemonic, setShowMnemonic] = useState(false);
|
||||||
const [showLegacyAssignCta, setShowLegacyAssignCta] = useState(false);
|
const [showLegacyAssignCta, setShowLegacyAssignCta] = useState(false);
|
||||||
|
const [hasS0, setHasS0] = useState<boolean | null>(null);
|
||||||
const [mnemonicWords, setMnemonicWords] = useState<string[]>([]);
|
const [mnemonicWords, setMnemonicWords] = useState<string[]>([]);
|
||||||
const [mnemonicParts, setMnemonicParts] = useState<string[][]>([]);
|
const [mnemonicParts, setMnemonicParts] = useState<string[][]>([]);
|
||||||
const [mnemonicStep, setMnemonicStep] = useState<1 | 2 | 3 | 4 | 5>(1);
|
const [mnemonicStep, setMnemonicStep] = useState<1 | 2 | 3 | 4 | 5>(1);
|
||||||
@@ -185,15 +188,31 @@ export default function VaultScreen() {
|
|||||||
const mnemonicRef = useRef<View>(null);
|
const mnemonicRef = useRef<View>(null);
|
||||||
const progressTimerRef = useRef<ReturnType<typeof setInterval> | null>(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(() => {
|
useEffect(() => {
|
||||||
if (!isUnlocked) {
|
let cancelled = false;
|
||||||
const timer = setTimeout(() => {
|
AsyncStorage.getItem(VAULT_STORAGE_KEYS.SHARE_DEVICE)
|
||||||
setShowBiometric(true);
|
.then((v) => {
|
||||||
}, 500);
|
if (!cancelled) setHasS0(!!v);
|
||||||
return () => clearTimeout(timer);
|
})
|
||||||
}
|
.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(() => {
|
useEffect(() => {
|
||||||
if (isUnlocked) {
|
if (isUnlocked) {
|
||||||
Animated.timing(fadeAnim, {
|
Animated.timing(fadeAnim, {
|
||||||
@@ -335,28 +354,49 @@ export default function VaultScreen() {
|
|||||||
|
|
||||||
const handleHeirDecision = (share: boolean) => {
|
const handleHeirDecision = (share: boolean) => {
|
||||||
// Placeholder for future heir flow
|
// Placeholder for future heir flow
|
||||||
setShowMnemonic(false);
|
finishMnemonicThenShowBiometric();
|
||||||
setIsUnlocked(true);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmitAssignment = () => {
|
const handleSubmitAssignment = () => {
|
||||||
// Placeholder for submitting assignment to API
|
// Placeholder for submitting assignment to API
|
||||||
setShowMnemonic(false);
|
finishMnemonicThenShowBiometric();
|
||||||
setIsUnlocked(true);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleHeirYes = () => {
|
const handleHeirYes = () => {
|
||||||
setShowMnemonic(false);
|
finishMnemonicThenShowBiometric();
|
||||||
setIsUnlocked(true);
|
|
||||||
setShowLegacyAssignCta(true);
|
setShowLegacyAssignCta(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleHeirNo = () => {
|
const handleHeirNo = () => {
|
||||||
setShowMnemonic(false);
|
finishMnemonicThenShowBiometric();
|
||||||
setIsUnlocked(true);
|
|
||||||
setShowLegacyAssignCta(true);
|
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 = () => {
|
const handleOpenLegacyAssign = () => {
|
||||||
setSelectedHeir(null);
|
setSelectedHeir(null);
|
||||||
setSelectedHeirAsset(null);
|
setSelectedHeirAsset(null);
|
||||||
@@ -888,7 +928,7 @@ export default function VaultScreen() {
|
|||||||
|
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.unlockButton}
|
style={styles.unlockButton}
|
||||||
onPress={() => setShowBiometric(true)}
|
onPress={hasS0 ? () => setShowBiometric(true) : handleUnlock}
|
||||||
activeOpacity={0.8}
|
activeOpacity={0.8}
|
||||||
>
|
>
|
||||||
<LinearGradient
|
<LinearGradient
|
||||||
@@ -897,8 +937,10 @@ export default function VaultScreen() {
|
|||||||
start={{ x: 0, y: 0 }}
|
start={{ x: 0, y: 0 }}
|
||||||
end={{ x: 1, y: 0 }}
|
end={{ x: 1, y: 0 }}
|
||||||
>
|
>
|
||||||
<Ionicons name="finger-print" size={20} color={colors.vault.background} />
|
<Ionicons name={hasS0 ? 'finger-print' : 'key'} size={20} color={colors.vault.background} />
|
||||||
<Text style={styles.unlockButtonText}>Captain's Verification</Text>
|
<Text style={styles.unlockButtonText}>
|
||||||
|
{hasS0 === true ? "Captain's Verification" : hasS0 === false ? 'Enter Vault' : 'Loading…'}
|
||||||
|
</Text>
|
||||||
</LinearGradient>
|
</LinearGradient>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
@@ -906,8 +948,8 @@ export default function VaultScreen() {
|
|||||||
</LinearGradient>
|
</LinearGradient>
|
||||||
|
|
||||||
<BiometricModal
|
<BiometricModal
|
||||||
visible={showBiometric}
|
visible={hasS0 === true && showBiometric}
|
||||||
onSuccess={handleUnlock}
|
onSuccess={handleBiometricSuccess}
|
||||||
onCancel={() => setShowBiometric(false)}
|
onCancel={() => setShowBiometric(false)}
|
||||||
title="Enter the Vault"
|
title="Enter the Vault"
|
||||||
message="Verify your identity to access your treasures"
|
message="Verify your identity to access your treasures"
|
||||||
|
|||||||
Reference in New Issue
Block a user