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