Merge branch 'Steven'

This commit is contained in:
Ada
2026-01-31 21:21:55 -08:00
7 changed files with 1023 additions and 645 deletions

View File

@@ -112,6 +112,7 @@ export const MOCK_CONFIG = {
USER: {
id: 999,
username: 'MockCaptain',
email: 'captain@sentinel.local',
public_key: 'mock_public_key',
is_admin: true,
guale: false,

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect, useRef } from 'react';
import React, { useState, useEffect } from 'react';
import {
View,
Text,
@@ -8,47 +8,12 @@ import {
SafeAreaView,
Animated,
Modal,
TextInput,
KeyboardAvoidingView,
Platform,
Share,
Alert,
Linking,
} from 'react-native';
import { LinearGradient } from 'expo-linear-gradient';
import { Ionicons, Feather, MaterialCommunityIcons, FontAwesome5 } from '@expo/vector-icons';
import { captureRef } from 'react-native-view-shot';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { colors, typography, spacing, borderRadius, shadows } from '../theme/colors';
import { SystemStatus, KillSwitchLog } from '../types';
import VaultScreen from './VaultScreen';
import BiometricModal from '../components/common/BiometricModal';
import {
SSSShare,
mnemonicToEntropy,
splitSecret,
formatShareCompact,
serializeShare,
verifyShares,
} from '../utils/sss';
// Vault storage keys (for testing: clear these to simulate first-open)
export const VAULT_STORAGE_KEYS = {
INITIALIZED: 'sentinel_vault_initialized',
SHARE_DEVICE: 'sentinel_share_device',
} as const;
// Nautical-themed mnemonic word list (unique words only)
const MNEMONIC_WORDS = [
'anchor', 'harbor', 'compass', 'lighthouse', 'current', 'ocean', 'tide', 'voyage',
'keel', 'stern', 'bow', 'mast', 'sail', 'port', 'starboard', 'reef',
'signal', 'beacon', 'chart', 'helm', 'gale', 'calm', 'cove', 'isle',
'horizon', 'sextant', 'sound', 'drift', 'wake', 'mariner', 'pilot', 'fathom',
'buoy', 'lantern', 'harpoon', 'lagoon', 'bay', 'strait', 'riptide', 'foam',
'coral', 'pearl', 'trident', 'ebb', 'flow', 'vault', 'cipher', 'shroud',
'salt', 'wave', 'grotto', 'storm', 'north', 'south', 'east', 'west',
'ember', 'cabin', 'ledger', 'torch', 'sanctum', 'oath', 'depths', 'captain',
] as const;
// Animation timing constants
const ANIMATION_DURATION = {
@@ -58,56 +23,13 @@ const ANIMATION_DURATION = {
heartbeatPress: 150,
} as const;
const generateMnemonic = (wordCount = 12) => {
const words: string[] = [];
for (let i = 0; i < wordCount; i += 1) {
const index = Math.floor(Math.random() * MNEMONIC_WORDS.length);
words.push(MNEMONIC_WORDS[index]);
}
return words;
};
/**
* Generate SSS shares from mnemonic words
* Uses Shamir's Secret Sharing (3,2) threshold scheme
*/
const generateSSSShares = (words: string[]): SSSShare[] => {
try {
// Convert mnemonic to entropy (big integer)
const entropy = mnemonicToEntropy(words, MNEMONIC_WORDS);
// Split entropy into 3 shares using SSS
const shares = splitSecret(entropy);
// Verify shares can recover the original (optional, for debugging)
if (__DEV__) {
const isValid = verifyShares(shares, entropy);
if (!isValid) {
console.warn('SSS verification failed!');
} else {
console.log('SSS shares verified successfully');
}
}
return shares;
} catch (error) {
console.error('Failed to generate SSS shares:', error);
// Fallback: return empty shares (should not happen in production)
return [
{ x: 1, y: BigInt(0), label: 'device' },
{ x: 2, y: BigInt(0), label: 'cloud' },
{ x: 3, y: BigInt(0), label: 'heir' },
];
}
};
// Icon names type for type safety
type StatusIconName = 'checkmark-circle' | 'warning' | 'alert-circle';
// Status configuration with nautical theme
const statusConfig: Record<SystemStatus, {
color: string;
label: string;
const statusConfig: Record<SystemStatus, {
color: string;
label: string;
icon: StatusIconName;
description: string;
gradientColors: [string, string];
@@ -137,26 +59,10 @@ const statusConfig: Record<SystemStatus, {
// Mock data
const initialLogs: KillSwitchLog[] = [
{
id: '1',
action: 'HEARTBEAT_CONFIRMED',
timestamp: new Date('2024-01-18T09:30:00'),
},
{
id: '2',
action: 'SUBSCRIPTION_VERIFIED',
timestamp: new Date('2024-01-17T00:00:00'),
},
{
id: '3',
action: 'JOURNAL_ACTIVITY',
timestamp: new Date('2024-01-16T15:42:00'),
},
{
id: '4',
action: 'HEARTBEAT_CONFIRMED',
timestamp: new Date('2024-01-15T11:20:00'),
},
{ id: '1', action: 'HEARTBEAT_CONFIRMED', timestamp: new Date('2024-01-18T09:30:00') },
{ id: '2', action: 'SUBSCRIPTION_VERIFIED', timestamp: new Date('2024-01-17T00:00:00') },
{ id: '3', action: 'JOURNAL_ACTIVITY', timestamp: new Date('2024-01-16T15:42:00') },
{ id: '4', action: 'HEARTBEAT_CONFIRMED', timestamp: new Date('2024-01-15T11:20:00') },
];
export default function SentinelScreen() {
@@ -168,26 +74,8 @@ export default function SentinelScreen() {
const [glowAnim] = useState(new Animated.Value(0.5));
const [rotateAnim] = useState(new Animated.Value(0));
const [showVault, setShowVault] = useState(false);
const [showMnemonic, setShowMnemonic] = useState(false);
const [mnemonicWords, setMnemonicWords] = useState<string[]>([]);
const [sssShares, setSssShares] = useState<SSSShare[]>([]);
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<boolean | null>(null);
const [showSetupBiometric, setShowSetupBiometric] = useState(false);
const mnemonicRef = useRef<View>(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(
Animated.sequence([
Animated.timing(pulseAnim, {
@@ -204,7 +92,6 @@ export default function SentinelScreen() {
);
pulseAnimation.start();
// Glow animation
const glowAnimation = Animated.loop(
Animated.sequence([
Animated.timing(glowAnim, {
@@ -221,7 +108,6 @@ export default function SentinelScreen() {
);
glowAnimation.start();
// Slow rotate for ship wheel
const rotateAnimation = Animated.loop(
Animated.timing(rotateAnim, {
toValue: 1,
@@ -231,7 +117,6 @@ export default function SentinelScreen() {
);
rotateAnimation.start();
// Cleanup animations on unmount to prevent memory leaks
return () => {
pulseAnimation.stop();
glowAnimation.stop();
@@ -239,109 +124,9 @@ export default function SentinelScreen() {
};
}, [pulseAnim, glowAnim, rotateAnim]);
const startFirstTimeSetup = () => {
const words = generateMnemonic();
const shares = generateSSSShares(words);
setMnemonicWords(words);
setSssShares(shares);
setShowMnemonic(true);
setShowVault(false);
setShowEmailForm(false);
setEmailAddress('');
setEmailRecipientType('self');
if (shares[0]) {
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();
}
};
const handleScreenshot = async () => {
try {
setIsCapturing(true);
const uri = await captureRef(mnemonicRef, {
format: 'png',
quality: 1,
result: 'tmpfile',
});
await Share.share({
url: uri,
message: 'Sentinel key backup',
});
completeSetupAndEnterVault();
} catch (error) {
Alert.alert('Screenshot failed', 'Please try again or use email backup.');
} finally {
setIsCapturing(false);
}
};
const handleEmailBackup = () => {
setShowEmailForm(true);
};
const handleCompleteBackupLocal = () => {
completeSetupAndEnterVault();
};
const handleSendEmail = async () => {
const trimmed = emailAddress.trim();
if (!trimmed || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(trimmed)) {
Alert.alert('Invalid email', 'Please enter a valid email address.');
return;
}
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);
completeSetupAndEnterVault();
} catch (error) {
Alert.alert('Email failed', 'Unable to open email client.');
}
};
const openVault = () => setShowVault(true);
const handleHeartbeat = () => {
// Animate pulse
Animated.sequence([
Animated.timing(pulseAnim, {
toValue: 1.15,
@@ -355,43 +140,34 @@ export default function SentinelScreen() {
}),
]).start();
// Add new log using functional update to avoid stale closure
const newLog: KillSwitchLog = {
id: Date.now().toString(),
action: 'HEARTBEAT_CONFIRMED',
timestamp: new Date(),
};
setLogs((prevLogs) => [newLog, ...prevLogs]);
// Reset status if warning
if (status === 'warning') {
setStatus('normal');
}
};
const formatDateTime = (date: Date) => {
return date.toLocaleString('en-US', {
const formatDateTime = (date: Date) =>
date.toLocaleString('en-US', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
});
};
const formatTimeAgo = (date: Date) => {
const now = new Date();
const diff = now.getTime() - date.getTime();
const hours = Math.floor(diff / (1000 * 60 * 60));
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
if (hours > 24) {
const days = Math.floor(hours / 24);
return `${days} days ago`;
}
if (hours > 0) {
return `${hours}h ${minutes}m ago`;
}
if (hours > 24) return `${Math.floor(hours / 24)} days ago`;
if (hours > 0) return `${hours}h ${minutes}m ago`;
return `${minutes}m ago`;
};
@@ -408,7 +184,7 @@ export default function SentinelScreen() {
style={styles.gradient}
>
<SafeAreaView style={styles.safeArea}>
<ScrollView
<ScrollView
style={styles.scrollView}
showsVerticalScrollIndicator={false}
contentContainerStyle={styles.scrollContent}
@@ -424,14 +200,14 @@ export default function SentinelScreen() {
{/* Status Display */}
<View style={styles.statusContainer}>
<Animated.View
<Animated.View
style={[
styles.statusCircleOuter,
{
{
transform: [{ scale: pulseAnim }],
opacity: glowAnim,
backgroundColor: `${currentStatus.color}20`,
}
},
]}
/>
<Animated.View style={{ transform: [{ scale: pulseAnim }] }}>
@@ -453,10 +229,10 @@ export default function SentinelScreen() {
{/* Ship Wheel Watermark */}
<View style={styles.wheelWatermark}>
<Animated.View style={{ transform: [{ rotate: spin }] }}>
<MaterialCommunityIcons
name="ship-wheel"
size={200}
color={colors.sentinel.primary}
<MaterialCommunityIcons
name="ship-wheel"
size={200}
color={colors.sentinel.primary}
style={{ opacity: 0.03 }}
/>
</Animated.View>
@@ -469,24 +245,16 @@ export default function SentinelScreen() {
<FontAwesome5 name="anchor" size={16} color={colors.sentinel.primary} />
</View>
<Text style={styles.metricLabel}>SUBSCRIPTION</Text>
<Text style={styles.metricValue}>
{formatTimeAgo(lastSubscriptionCheck)}
</Text>
<Text style={styles.metricTime}>
{formatDateTime(lastSubscriptionCheck)}
</Text>
<Text style={styles.metricValue}>{formatTimeAgo(lastSubscriptionCheck)}</Text>
<Text style={styles.metricTime}>{formatDateTime(lastSubscriptionCheck)}</Text>
</View>
<View style={styles.metricCard}>
<View style={styles.metricIconContainer}>
<Feather name="edit-3" size={16} color={colors.sentinel.primary} />
</View>
<Text style={styles.metricLabel}>LAST JOURNAL</Text>
<Text style={styles.metricValue}>
{formatTimeAgo(lastFlowActivity)}
</Text>
<Text style={styles.metricTime}>
{formatDateTime(lastFlowActivity)}
</Text>
<Text style={styles.metricValue}>{formatTimeAgo(lastFlowActivity)}</Text>
<Text style={styles.metricTime}>{formatDateTime(lastFlowActivity)}</Text>
</View>
</View>
@@ -503,7 +271,7 @@ export default function SentinelScreen() {
</View>
<TouchableOpacity
style={styles.vaultAccessButton}
onPress={handleOpenVault}
onPress={openVault}
activeOpacity={0.8}
accessibilityLabel="Open Shadow Vault"
accessibilityRole="button"
@@ -547,9 +315,7 @@ export default function SentinelScreen() {
<View style={styles.logDot} />
<View style={styles.logContent}>
<Text style={styles.logAction}>{log.action}</Text>
<Text style={styles.logTime}>
{formatDateTime(log.timestamp)}
</Text>
<Text style={styles.logTime}>{formatDateTime(log.timestamp)}</Text>
</View>
</View>
))}
@@ -565,7 +331,7 @@ export default function SentinelScreen() {
onRequestClose={() => setShowVault(false)}
>
<View style={styles.vaultModalContainer}>
<VaultScreen />
{showVault ? <VaultScreen /> : null}
<TouchableOpacity
style={styles.vaultCloseButton}
onPress={() => setShowVault(false)}
@@ -577,184 +343,20 @@ export default function SentinelScreen() {
</TouchableOpacity>
</View>
</Modal>
{/* Mnemonic Modal */}
<Modal
visible={showMnemonic}
animationType="fade"
transparent
onRequestClose={() => setShowMnemonic(false)}
>
<KeyboardAvoidingView
style={styles.mnemonicOverlay}
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
>
<ScrollView
style={styles.mnemonicScroll}
contentContainerStyle={styles.mnemonicScrollContent}
showsVerticalScrollIndicator={false}
keyboardShouldPersistTaps="handled"
>
<View ref={mnemonicRef} collapsable={false}>
<LinearGradient
colors={[colors.sentinel.cardBackground, colors.sentinel.backgroundGradientEnd]}
style={styles.mnemonicCard}
>
<TouchableOpacity
style={styles.mnemonicClose}
onPress={() => setShowMnemonic(false)}
activeOpacity={0.85}
accessibilityLabel="Close mnemonic modal"
accessibilityRole="button"
>
<Ionicons name="close" size={18} color={colors.sentinel.textSecondary} />
</TouchableOpacity>
<View style={styles.mnemonicHeader}>
<MaterialCommunityIcons name="key-variant" size={22} color={colors.sentinel.primary} />
<Text style={styles.mnemonicTitle}>12-Word Mnemonic</Text>
</View>
<Text style={styles.mnemonicSubtitle}>
Your seed is protected by SSS (3,2) threshold encryption. Any 2 shares can restore your vault.
</Text>
<View style={styles.mnemonicBlock}>
<Text style={styles.mnemonicBlockText}>
{mnemonicWords.join(' ')}
</Text>
</View>
<View style={styles.partGrid}>
<View style={[styles.partCard, styles.partCardStored]}>
<Text style={styles.partLabel}>SHARE A DEVICE</Text>
<Text style={styles.partValue}>
{sssShares[0] ? formatShareCompact(sssShares[0]) : '---'}
</Text>
<Text style={styles.partHint}>Stored on this device</Text>
</View>
<View style={styles.partCard}>
<Text style={styles.partLabel}>SHARE B CLOUD</Text>
<Text style={styles.partValue}>
{sssShares[1] ? formatShareCompact(sssShares[1]) : '---'}
</Text>
<Text style={styles.partHint}>To be synced to Sentinel</Text>
</View>
<View style={styles.partCard}>
<Text style={styles.partLabel}>SHARE C HEIR</Text>
<Text style={styles.partValue}>
{sssShares[2] ? formatShareCompact(sssShares[2]) : '---'}
</Text>
<Text style={styles.partHint}>For your heir (2-of-3 required)</Text>
</View>
</View>
<TouchableOpacity
style={[styles.mnemonicPrimaryButton, isCapturing && styles.mnemonicButtonDisabled]}
onPress={handleScreenshot}
activeOpacity={0.85}
disabled={isCapturing}
accessibilityLabel="Take screenshot backup of mnemonic"
accessibilityRole="button"
accessibilityState={{ disabled: isCapturing }}
>
<Text style={styles.mnemonicPrimaryText}>
{isCapturing ? 'CAPTURING...' : 'PHYSICAL BACKUP (SCREENSHOT)'}
</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.mnemonicSecondaryButton}
onPress={handleEmailBackup}
activeOpacity={0.85}
accessibilityLabel="Send backup via email"
accessibilityRole="button"
>
<Text style={styles.mnemonicSecondaryText}>EMAIL BACKUP</Text>
</TouchableOpacity>
{showEmailForm ? (
<View style={styles.emailForm}>
<View style={styles.emailTypeRow}>
<TouchableOpacity
style={[styles.emailTypeButton, emailRecipientType === 'self' && styles.emailTypeButtonActive]}
onPress={() => setEmailRecipientType('self')}
>
<Text style={[styles.emailTypeText, emailRecipientType === 'self' && styles.emailTypeTextActive]}>
To Myself
</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.emailTypeButton, emailRecipientType === 'heir' && styles.emailTypeButtonActive]}
onPress={() => setEmailRecipientType('heir')}
>
<Text style={[styles.emailTypeText, emailRecipientType === 'heir' && styles.emailTypeTextActive]}>
To Heir
</Text>
</TouchableOpacity>
</View>
<TextInput
style={styles.emailInput}
value={emailAddress}
onChangeText={setEmailAddress}
placeholder={emailRecipientType === 'heir' ? 'heir@email.com' : 'you@email.com'}
placeholderTextColor={colors.sentinel.textSecondary}
keyboardType="email-address"
autoCapitalize="none"
autoCorrect={false}
/>
<TouchableOpacity
style={styles.emailSendButton}
onPress={handleSendEmail}
activeOpacity={0.85}
accessibilityLabel="Send backup email"
accessibilityRole="button"
>
<Text style={styles.emailSendText}>SEND EMAIL</Text>
</TouchableOpacity>
</View>
) : null}
<TouchableOpacity
style={styles.mnemonicTertiaryButton}
onPress={handleCompleteBackupLocal}
activeOpacity={0.85}
accessibilityLabel="I've completed backup locally"
accessibilityRole="button"
>
<Text style={styles.mnemonicTertiaryText}>COMPLETE BACKUP (LOCAL)</Text>
</TouchableOpacity>
</LinearGradient>
</View>
</ScrollView>
</KeyboardAvoidingView>
</Modal>
{/* Biometric setup prompt after first-time backup (1.4) */}
<BiometricModal
visible={showSetupBiometric}
onSuccess={handleSetupBiometricSuccess}
onCancel={handleSetupBiometricSkip}
title="Quick Unlock"
message="Enable Face ID / Touch ID for faster access next time?"
isDark
/>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
gradient: {
flex: 1,
},
safeArea: {
flex: 1,
},
scrollView: {
flex: 1,
},
container: { flex: 1 },
gradient: { flex: 1 },
safeArea: { flex: 1 },
scrollView: { flex: 1 },
scrollContent: {
padding: spacing.lg,
paddingBottom: 120,
},
header: {
marginBottom: spacing.xl,
},
header: { marginBottom: spacing.xl },
headerTitleRow: {
flexDirection: 'row',
alignItems: 'center',
@@ -860,9 +462,7 @@ const styles = StyleSheet.create({
marginBottom: spacing.xl,
...shadows.medium,
},
heartbeatGradient: {
padding: spacing.lg,
},
heartbeatGradient: { padding: spacing.lg },
heartbeatContent: {
flexDirection: 'row',
alignItems: 'center',
@@ -916,9 +516,7 @@ const styles = StyleSheet.create({
marginTop: 6,
marginRight: spacing.md,
},
logContent: {
flex: 1,
},
logContent: { flex: 1 },
logAction: {
fontSize: typography.fontSize.sm,
color: colors.sentinel.text,
@@ -931,7 +529,6 @@ const styles = StyleSheet.create({
color: colors.sentinel.textSecondary,
fontFamily: typography.fontFamily.mono,
},
// Shadow Vault Access Card
vaultAccessCard: {
flexDirection: 'row',
alignItems: 'center',
@@ -951,9 +548,7 @@ const styles = StyleSheet.create({
justifyContent: 'center',
marginRight: spacing.md,
},
vaultAccessContent: {
flex: 1,
},
vaultAccessContent: { flex: 1 },
vaultAccessTitle: {
fontSize: typography.fontSize.base,
fontWeight: '600',
@@ -975,7 +570,6 @@ const styles = StyleSheet.create({
fontWeight: '700',
fontSize: typography.fontSize.sm,
},
// Vault Modal
vaultModalContainer: {
flex: 1,
backgroundColor: colors.vault.background,
@@ -991,196 +585,4 @@ const styles = StyleSheet.create({
alignItems: 'center',
justifyContent: 'center',
},
mnemonicOverlay: {
flex: 1,
backgroundColor: 'rgba(11, 20, 24, 0.72)',
justifyContent: 'center',
padding: spacing.lg,
},
mnemonicScroll: {
flex: 1,
},
mnemonicScrollContent: {
flexGrow: 1,
justifyContent: 'center',
},
mnemonicCard: {
borderRadius: borderRadius.xl,
padding: spacing.lg,
borderWidth: 1,
borderColor: colors.sentinel.cardBorder,
...shadows.glow,
},
mnemonicHeader: {
flexDirection: 'row',
alignItems: 'center',
gap: spacing.sm,
marginBottom: spacing.sm,
},
mnemonicClose: {
position: 'absolute',
top: spacing.sm,
right: spacing.sm,
width: 32,
height: 32,
borderRadius: 16,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: 'rgba(26, 58, 74, 0.35)',
},
mnemonicTitle: {
fontSize: typography.fontSize.lg,
fontWeight: '700',
color: colors.sentinel.text,
letterSpacing: typography.letterSpacing.wide,
},
mnemonicSubtitle: {
fontSize: typography.fontSize.sm,
color: colors.sentinel.textSecondary,
marginBottom: spacing.md,
},
mnemonicBlock: {
backgroundColor: colors.sentinel.cardBackground,
borderRadius: borderRadius.lg,
paddingVertical: spacing.md,
paddingHorizontal: spacing.md,
borderWidth: 1,
borderColor: colors.sentinel.cardBorder,
marginBottom: spacing.lg,
},
partGrid: {
gap: spacing.sm,
marginBottom: spacing.lg,
},
partCard: {
backgroundColor: colors.sentinel.cardBackground,
borderRadius: borderRadius.lg,
paddingVertical: spacing.sm,
paddingHorizontal: spacing.md,
borderWidth: 1,
borderColor: colors.sentinel.cardBorder,
},
partCardStored: {
borderColor: colors.sentinel.primary,
},
partLabel: {
fontSize: typography.fontSize.xs,
color: colors.sentinel.textSecondary,
letterSpacing: typography.letterSpacing.wide,
marginBottom: 4,
fontWeight: '600',
},
partValue: {
fontSize: typography.fontSize.md,
color: colors.sentinel.text,
fontFamily: typography.fontFamily.mono,
fontWeight: '700',
marginBottom: 2,
},
partHint: {
fontSize: typography.fontSize.xs,
color: colors.sentinel.textSecondary,
},
mnemonicBlockText: {
fontSize: typography.fontSize.sm,
color: colors.sentinel.text,
fontFamily: typography.fontFamily.mono,
fontWeight: '600',
lineHeight: 22,
textAlign: 'center',
},
mnemonicPrimaryButton: {
backgroundColor: colors.sentinel.primary,
paddingVertical: spacing.sm,
borderRadius: borderRadius.full,
alignItems: 'center',
marginBottom: spacing.sm,
},
mnemonicButtonDisabled: {
opacity: 0.6,
},
mnemonicPrimaryText: {
color: colors.nautical.cream,
fontWeight: '700',
letterSpacing: typography.letterSpacing.wide,
},
mnemonicSecondaryButton: {
backgroundColor: 'transparent',
paddingVertical: spacing.sm,
borderRadius: borderRadius.full,
alignItems: 'center',
borderWidth: 1,
borderColor: colors.sentinel.cardBorder,
},
mnemonicSecondaryText: {
color: colors.sentinel.text,
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,
},
emailInput: {
height: 44,
borderRadius: borderRadius.full,
borderWidth: 1,
borderColor: colors.sentinel.cardBorder,
paddingHorizontal: spacing.md,
color: colors.sentinel.text,
fontSize: typography.fontSize.sm,
backgroundColor: 'rgba(255, 255, 255, 0.02)',
marginBottom: spacing.sm,
},
emailSendButton: {
backgroundColor: colors.nautical.teal,
paddingVertical: spacing.sm,
borderRadius: borderRadius.full,
alignItems: 'center',
},
emailSendText: {
color: colors.nautical.cream,
fontWeight: '700',
letterSpacing: typography.letterSpacing.wide,
},
});

File diff suppressed because it is too large Load Diff

View File

@@ -77,6 +77,7 @@ export interface ProtocolInfo {
export interface User {
id: number;
username: string;
email?: string;
public_key: string;
is_admin: boolean;
guale: boolean;