diff --git a/App.tsx b/App.tsx index 2c913dc..cecce47 100644 --- a/App.tsx +++ b/App.tsx @@ -6,6 +6,7 @@ */ import React from 'react'; +import { Buffer } from 'buffer'; import { StatusBar } from 'expo-status-bar'; import { NavigationContainer } from '@react-navigation/native'; import { GestureHandlerRootView } from 'react-native-gesture-handler'; @@ -15,6 +16,10 @@ import AuthNavigator from './src/navigation/AuthNavigator'; import { AuthProvider, useAuth } from './src/context/AuthContext'; import { colors } from './src/theme/colors'; +if (typeof globalThis !== 'undefined' && !globalThis.Buffer) { + globalThis.Buffer = Buffer; +} + /** * Loading screen shown while restoring auth state */ diff --git a/package-lock.json b/package-lock.json index 9e4cc83..194d6e8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,8 @@ "@react-navigation/bottom-tabs": "^6.6.1", "@react-navigation/native": "^6.1.18", "@react-navigation/native-stack": "^6.11.0", + "bip39": "^3.1.0", + "buffer": "^6.0.3", "expo": "~52.0.0", "expo-asset": "~11.0.5", "expo-constants": "~17.0.8", @@ -3180,6 +3182,18 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -4420,6 +4434,15 @@ "node": ">=0.6" } }, + "node_modules/bip39": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bip39/-/bip39-3.1.0.tgz", + "integrity": "sha512-c9kiwdk45Do5GL0vJMe7tS95VjCii65mYAH7DfWl3uW8AVzXKQVUm64i3hzVybBDMp9r7j9iNxR85+ul8MdN/A==", + "license": "ISC", + "dependencies": { + "@noble/hashes": "^1.2.0" + } + }, "node_modules/bplist-creator": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.0.7.tgz", @@ -4507,9 +4530,9 @@ } }, "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", "funding": [ { "type": "github", @@ -4527,7 +4550,7 @@ "license": "MIT", "dependencies": { "base64-js": "^1.3.1", - "ieee754": "^1.1.13" + "ieee754": "^1.2.1" } }, "node_modules/buffer-alloc": { @@ -11035,6 +11058,30 @@ "node": ">=10" } }, + "node_modules/whatwg-url-without-unicode/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "node_modules/whatwg-url-without-unicode/node_modules/webidl-conversions": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz", diff --git a/package.json b/package.json index e8c548d..d6cdd73 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,8 @@ "@react-navigation/bottom-tabs": "^6.6.1", "@react-navigation/native": "^6.1.18", "@react-navigation/native-stack": "^6.11.0", + "bip39": "^3.1.0", + "buffer": "^6.0.3", "expo": "~52.0.0", "expo-asset": "~11.0.5", "expo-constants": "~17.0.8", diff --git a/src/config/index.ts b/src/config/index.ts index d892ac0..890562c 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -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, diff --git a/src/screens/SentinelScreen.tsx b/src/screens/SentinelScreen.tsx index 0db7dfd..58862d5 100644 --- a/src/screens/SentinelScreen.tsx +++ b/src/screens/SentinelScreen.tsx @@ -348,7 +348,7 @@ export default function SentinelScreen() { onRequestClose={() => setShowVault(false)} > - + {showVault ? : null} setShowVault(false)} diff --git a/src/screens/VaultScreen.tsx b/src/screens/VaultScreen.tsx index 4077e2e..33bc48d 100644 --- a/src/screens/VaultScreen.tsx +++ b/src/screens/VaultScreen.tsx @@ -19,9 +19,11 @@ 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 * as bip39 from 'bip39'; import { colors, typography, spacing, borderRadius, shadows } from '../theme/colors'; import { VaultAsset, VaultAssetType, Heir } from '../types'; import BiometricModal from '../components/common/BiometricModal'; +import { useAuth } from '../context/AuthContext'; // Asset type configuration with nautical theme const assetTypeConfig: Record = { @@ -40,17 +42,6 @@ const accountProviderOptions = [ { key: 'custom', label: 'Other', icon: 'shield-account', iconType: 'material' as const }, ]; -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', 'lagoon', 'storm', 'north', 'south', 'east', - 'west', 'ember', 'cabin', 'signal', 'ledger', 'torch', 'sanctum', 'oath', -]; - const initialHeirs: Heir[] = [ { id: '1', @@ -84,14 +75,7 @@ const initialHeirs: Heir[] = [ }, ]; -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; -}; +const generateMnemonic = () => bip39.generateMnemonic(128).split(' '); const splitMnemonic = (words: string[]) => [ words.slice(0, 4), @@ -104,15 +88,6 @@ type HeirAssignment = { heir: Heir; }; -const shuffle = (items: T[]) => { - const next = [...items]; - for (let i = next.length - 1; i > 0; i -= 1) { - const j = Math.floor(Math.random() * (i + 1)); - [next[i], next[j]] = [next[j], next[i]]; - } - return next; -}; - // Mock data const initialAssets: VaultAsset[] = [ { @@ -183,6 +158,7 @@ export default function VaultScreen() { const [showAddBiometric, setShowAddBiometric] = useState(false); const [accountProvider, setAccountProvider] = useState<'bank' | 'steam' | 'facebook' | 'custom'>('bank'); const [showMnemonic, setShowMnemonic] = useState(false); + const [showLegacyAssignCta, setShowLegacyAssignCta] = useState(false); const [mnemonicWords, setMnemonicWords] = useState([]); const [mnemonicParts, setMnemonicParts] = useState([]); const [mnemonicStep, setMnemonicStep] = useState<1 | 2 | 3 | 4 | 5>(1); @@ -190,12 +166,11 @@ export default function VaultScreen() { const [selectedHeir, setSelectedHeir] = useState(null); const [selectedHeirAsset, setSelectedHeirAsset] = useState(null); const [assignments, setAssignments] = useState([]); - const [wordOptions, setWordOptions] = useState([]); - const [selectedWords, setSelectedWords] = useState([]); + const [replaceIndex, setReplaceIndex] = useState(null); + const [replaceQuery, setReplaceQuery] = useState(''); const [progressIndex, setProgressIndex] = useState(0); const [progressAnim] = useState(new Animated.Value(0)); - const [showEmailForm, setShowEmailForm] = useState(false); - const [emailAddress, setEmailAddress] = useState(''); + const { user } = useAuth(); const [isCapturing, setIsCapturing] = useState(false); const mnemonicRef = useRef(null); const progressTimerRef = useRef | null>(null); @@ -242,22 +217,17 @@ export default function VaultScreen() { setShowBiometric(false); const words = generateMnemonic(); const parts = splitMnemonic(words); - const decoys = MNEMONIC_WORDS.filter((word) => !words.includes(word)); - const options = shuffle([...words, ...decoys.slice(0, 12)]); setMnemonicWords(words); setMnemonicParts(parts); - setWordOptions(options); - setSelectedWords([]); + setReplaceIndex(null); + setReplaceQuery(''); setMnemonicStep(1); setHeirStep('decision'); setSelectedHeir(null); setSelectedHeirAsset(null); - setAssignments([]); setProgressIndex(0); progressAnim.setValue(0); - setShowMnemonic(true); - setShowEmailForm(false); - setEmailAddress(''); + setTimeout(() => setShowMnemonic(true), 200); AsyncStorage.setItem('sentinel_mnemonic_part_local', parts[0].join(' ')).catch(() => { // Best-effort local store; UI remains available }); @@ -269,12 +239,20 @@ export default function VaultScreen() { const uri = await captureRef(mnemonicRef, { format: 'png', quality: 1, - result: 'tmpfile', - }); - await Share.share({ - url: uri, - message: 'Sentinel key backup', + result: Platform.OS === 'web' ? 'data-uri' : 'tmpfile', }); + if (Platform.OS === 'web') { + try { + await Linking.openURL(uri); + } catch { + // Ignore if the browser blocks data-uri navigation. + } + } else { + await Share.share({ + url: uri, + message: 'Sentinel key backup', + }); + } setMnemonicStep(4); } catch (error) { Alert.alert('Screenshot failed', 'Please try again or use email backup.'); @@ -284,45 +262,19 @@ export default function VaultScreen() { }; const handleEmailBackup = () => { - setShowEmailForm(true); + // Proceed immediately; email delivery is handled separately. + setMnemonicStep(4); + setShowMnemonic(true); }; - const toggleWord = (word: string) => { - if (selectedWords.includes(word)) { - setSelectedWords(selectedWords.filter((item) => item !== word)); - return; - } - if (selectedWords.length >= 12) return; - setSelectedWords([...selectedWords, word]); - }; - - const handleConfirmSelection = () => { - if (selectedWords.length !== 12) { - Alert.alert('Select 12 words', 'Please select all 12 words in order.'); - return; - } - setMnemonicStep(2); - }; - - 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('Sentinel Vault Recovery Key'); - const body = encodeURIComponent(`Your 12-word mnemonic:\n${mnemonicWords.join(' ')}`); - const mailtoUrl = `mailto:${trimmed}?subject=${subject}&body=${body}`; - - try { - await Linking.openURL(mailtoUrl); - setShowEmailForm(false); - setEmailAddress(''); - setMnemonicStep(4); - } catch (error) { - Alert.alert('Email failed', 'Unable to open email client.'); - } + const handleReplaceWord = (word: string) => { + if (replaceIndex === null) return; + const nextWords = [...mnemonicWords]; + nextWords[replaceIndex] = word; + setMnemonicWords(nextWords); + setMnemonicParts(splitMnemonic(nextWords)); + setReplaceIndex(null); + setReplaceQuery(''); }; useEffect(() => { @@ -384,11 +336,23 @@ export default function VaultScreen() { }; const handleHeirYes = () => { - setHeirStep('asset'); + setShowMnemonic(false); + setIsUnlocked(true); + setShowLegacyAssignCta(true); }; const handleHeirNo = () => { - handleHeirDecision(false); + setShowMnemonic(false); + setIsUnlocked(true); + setShowLegacyAssignCta(true); + }; + + const handleOpenLegacyAssign = () => { + setSelectedHeir(null); + setSelectedHeirAsset(null); + setHeirStep('asset'); + setMnemonicStep(5); + setShowMnemonic(true); }; const handleSelectHeirAsset = (asset: VaultAsset) => { @@ -398,25 +362,6 @@ export default function VaultScreen() { const handleSelectHeir = (heir: Heir) => { setSelectedHeir(heir); - if (selectedHeirAsset) { - setAssignments((prev) => { - const remaining = prev.filter((item) => item.asset.id !== selectedHeirAsset.id); - return [...remaining, { asset: selectedHeirAsset, heir }]; - }); - } - setSelectedHeir(null); - setSelectedHeirAsset(null); - setHeirStep('asset'); - }; - - const getAssignmentForAsset = (assetId: string) => - assignments.find((item) => item.asset.id === assetId); - - const handleReviewAssignments = () => { - if (assignments.length === 0) { - Alert.alert('No assignments', 'Please assign at least one asset.'); - return; - } setHeirStep('summary'); }; @@ -543,9 +488,345 @@ export default function VaultScreen() { && addVerified && (selectedType !== 'private_key' || rehearsalConfirmed); + const mnemonicModal = ( + setShowMnemonic(false)} + > + + + setShowMnemonic(false)} + activeOpacity={0.85} + > + + + + + Mnemonic Setup + + {mnemonicStep === 1 ? ( + <> + + Review your 12-word mnemonic. Tap any word to replace it. + + + {mnemonicWords.map((word, index) => ( + setReplaceIndex(index)} + activeOpacity={0.8} + > + + {index + 1} + + + {word} + + + ))} + + {replaceIndex !== null ? ( + + + Replace word {replaceIndex + 1} + + + + {(replaceQuery + ? bip39.wordlists.english.filter((word) => + word.startsWith(replaceQuery.toLowerCase()) + ) + : bip39.wordlists.english + ) + .slice(0, 24) + .map((word) => ( + handleReplaceWord(word)} + activeOpacity={0.8} + > + {word} + + ))} + + setReplaceIndex(null)} + activeOpacity={0.85} + > + CANCEL + + + ) : null} + setMnemonicStep(2)} + activeOpacity={0.85} + > + NEXT + + + ) : null} + + {mnemonicStep === 2 ? ( + <> + + Confirm your 12-word mnemonic. + + + + {mnemonicWords.join(' ')} + + + setMnemonicStep(3)} + activeOpacity={0.85} + > + CONFIRM + + setMnemonicStep(1)} + activeOpacity={0.85} + > + EDIT SELECTION + + + ) : null} + + {mnemonicStep === 3 ? ( + <> + + Back up your mnemonic before entering the Vault. + + + + {mnemonicWords.join(' ')} + + + + + {isCapturing ? 'CAPTURING...' : 'PHYSICAL BACKUP (SCREENSHOT)'} + + + + EMAIL BACKUP + + + ) : null} + + {mnemonicStep === 4 ? ( + <> + + Finalizing your vault protection. + + + + + + + + + {progressIndex === 0 && '1. Your key is being processed'} + {progressIndex === 1 && '2. Your key has been split'} + {progressIndex === 2 && '3. Part one stored on this device'} + {progressIndex === 3 && '4. Part two uploaded to the cloud'} + {progressIndex >= 4 && '5. Part three inquiry initiated'} + + + + ) : null} + + {mnemonicStep === 5 ? ( + <> + {heirStep === 'decision' ? ( + <> + + Share Part Three with your legacy handler? + + + YES, SEND + + + NOT NOW + + + ) : null} + + {heirStep === 'asset' ? ( + <> + + Select the vault item to assign. + + + {assets.map((asset) => { + const config = assetTypeConfig[asset.type]; + return ( + handleSelectHeirAsset(asset)} + activeOpacity={0.8} + > + + {renderAssetTypeIcon(config, 18, colors.vault.primary)} + + + {asset.label} + {config.label} + + + ); + })} + + setHeirStep('decision')} + activeOpacity={0.85} + > + BACK + + + ) : null} + + {heirStep === 'heir' ? ( + <> + + Choose a legacy handler. + + + {initialHeirs.map((heir) => ( + handleSelectHeir(heir)} + activeOpacity={0.8} + > + + + + + {heir.name} + {heir.email} + + + ))} + + setHeirStep('asset')} + activeOpacity={0.85} + > + BACK + + + ) : null} + + {heirStep === 'summary' ? ( + <> + + Confirm assignment details. + + + Vault Item + {selectedHeirAsset?.label} + Legacy Handler + {selectedHeir?.name} + {selectedHeir?.email} + Release Tier + Tier {selectedHeir?.releaseLevel} + + + SUBMIT + + setHeirStep('heir')} + activeOpacity={0.85} + > + EDIT + + + ) : null} + + ) : null} + + + + + + + + ); + // Lock screen - if (!isUnlocked) { - return ( + const lockScreen = ( - - setShowMnemonic(false)} - > - - - setShowMnemonic(false)} - activeOpacity={0.85} - > - - - - - Mnemonic Setup - - {mnemonicStep === 1 ? ( - <> - - Select the 12 words in order. - - - Selected {selectedWords.length}/12 - {selectedWords.join(' ')} - - - {wordOptions.map((word) => { - const isSelected = selectedWords.includes(word); - return ( - toggleWord(word)} - activeOpacity={0.8} - > - - {word} - - - ); - })} - - - CONFIRM SELECTION - - - ) : null} - - {mnemonicStep === 2 ? ( - <> - - Confirm your 12-word mnemonic. - - - - {mnemonicWords.join(' ')} - - - setMnemonicStep(3)} - activeOpacity={0.85} - > - CONFIRM - - setMnemonicStep(1)} - activeOpacity={0.85} - > - EDIT SELECTION - - - ) : null} - - {mnemonicStep === 3 ? ( - <> - - Back up your mnemonic before entering the Vault. - - - - {mnemonicWords.join(' ')} - - - - - {isCapturing ? 'CAPTURING...' : 'PHYSICAL BACKUP (SCREENSHOT)'} - - - - EMAIL BACKUP - - {showEmailForm ? ( - - - - SEND EMAIL - - - ) : null} - - ) : null} - - {mnemonicStep === 4 ? ( - <> - - Finalizing your vault protection. - - - - - - - - - {progressIndex === 0 && '1. Your key is being processed'} - {progressIndex === 1 && '2. Your key has been split'} - {progressIndex === 2 && '3. Part one stored on this device'} - {progressIndex === 3 && '4. Part two uploaded to the cloud'} - {progressIndex >= 4 && '5. Part three inquiry initiated'} - - - - ) : null} - - {mnemonicStep === 5 ? ( - <> - {heirStep === 'decision' ? ( - <> - - Share Part Three with your legacy handler? - - - YES, SEND - - - NOT NOW - - - ) : null} - - {heirStep === 'asset' ? ( - <> - - Select the vault item to assign. - - - {assets.map((asset) => { - const config = assetTypeConfig[asset.type]; - const assignment = getAssignmentForAsset(asset.id); - return ( - handleSelectHeirAsset(asset)} - activeOpacity={0.8} - > - - {renderAssetTypeIcon(config, 18, colors.vault.primary)} - - - {asset.label} - - {assignment ? `Assigned to ${assignment.heir.name}` : config.label} - - - - ); - })} - - - REVIEW & SUBMIT - - setHeirStep('decision')} - activeOpacity={0.85} - > - BACK - - - ) : null} - - {heirStep === 'heir' ? ( - <> - - Choose a legacy handler. - - - {initialHeirs.map((heir) => ( - handleSelectHeir(heir)} - activeOpacity={0.8} - > - - - - - {heir.name} - {heir.email} - - - ))} - - setHeirStep('asset')} - activeOpacity={0.85} - > - BACK - - - ) : null} - - {heirStep === 'summary' ? ( - <> - - Confirm assignment details. - - - {assignments.map((item) => ( - - Asset - {item.asset.label} - Heir - {item.heir.name} - {item.heir.email} - - ))} - - - SUBMIT - - setHeirStep('asset')} - activeOpacity={0.85} - > - EDIT ASSIGNMENTS - - - ) : null} - - ) : null} - - {[1, 2, 3, 4, 5].map((step) => ( - - ))} - - - - ); - } - return ( + const vaultScreen = ( + {showLegacyAssignCta && ( + + + Legacy Assignment + + Continue assigning a vault item to your legacy handler. + + + + Continue + + + )} + {/* Asset List */} ); + + return ( + + {isUnlocked ? vaultScreen : lockScreen} + {mnemonicModal} + + ); } const styles = StyleSheet.create({ + root: { + flex: 1, + }, container: { flex: 1, }, @@ -1541,6 +1521,42 @@ const styles = StyleSheet.create({ fontSize: typography.fontSize.sm, color: colors.vault.textSecondary, }, + legacyCtaCard: { + marginHorizontal: spacing.lg, + marginBottom: spacing.sm, + padding: spacing.md, + borderRadius: borderRadius.lg, + backgroundColor: colors.vault.cardBackground, + borderWidth: 1, + borderColor: colors.vault.cardBorder, + flexDirection: 'row', + alignItems: 'center', + gap: spacing.md, + }, + legacyCtaInfo: { + flex: 1, + }, + legacyCtaTitle: { + color: colors.vault.text, + fontSize: typography.fontSize.base, + fontWeight: '700', + }, + legacyCtaText: { + color: colors.vault.textSecondary, + fontSize: typography.fontSize.sm, + marginTop: spacing.xs, + }, + legacyCtaButton: { + paddingHorizontal: spacing.md, + paddingVertical: spacing.sm, + borderRadius: borderRadius.full, + backgroundColor: colors.vault.primary, + }, + legacyCtaButtonText: { + color: colors.vault.background, + fontWeight: '700', + fontSize: typography.fontSize.sm, + }, assetList: { flex: 1, }, @@ -2200,28 +2216,6 @@ const styles = StyleSheet.create({ lineHeight: 22, textAlign: 'center', }, - selectedRow: { - backgroundColor: colors.sentinel.cardBackground, - borderRadius: borderRadius.lg, - paddingVertical: spacing.sm, - paddingHorizontal: spacing.md, - borderWidth: 1, - borderColor: colors.sentinel.cardBorder, - marginBottom: spacing.md, - }, - selectedLabel: { - fontSize: typography.fontSize.xs, - color: colors.sentinel.textSecondary, - letterSpacing: typography.letterSpacing.wide, - marginBottom: 4, - fontWeight: '600', - }, - selectedValue: { - fontSize: typography.fontSize.xs, - color: colors.sentinel.text, - fontFamily: typography.fontFamily.mono, - lineHeight: 18, - }, progressContainer: { marginTop: spacing.sm, marginBottom: spacing.md, @@ -2254,9 +2248,10 @@ const styles = StyleSheet.create({ marginBottom: spacing.lg, }, wordChip: { + minWidth: '30%', paddingHorizontal: spacing.sm, - paddingVertical: spacing.xs, - borderRadius: borderRadius.full, + paddingVertical: spacing.sm, + borderRadius: borderRadius.lg, borderWidth: 1, borderColor: colors.sentinel.cardBorder, backgroundColor: 'transparent', @@ -2265,6 +2260,11 @@ const styles = StyleSheet.create({ backgroundColor: colors.sentinel.primary, borderColor: colors.sentinel.primary, }, + wordChipIndex: { + fontSize: typography.fontSize.xs, + color: colors.sentinel.textSecondary, + marginBottom: 2, + }, wordChipText: { color: colors.sentinel.text, fontSize: typography.fontSize.xs, @@ -2273,6 +2273,62 @@ const styles = StyleSheet.create({ wordChipTextSelected: { color: colors.nautical.cream, }, + replacePanel: { + backgroundColor: colors.sentinel.cardBackground, + borderRadius: borderRadius.lg, + padding: spacing.md, + borderWidth: 1, + borderColor: colors.sentinel.cardBorder, + marginBottom: spacing.lg, + }, + replaceTitle: { + fontSize: typography.fontSize.sm, + color: colors.sentinel.text, + fontWeight: '600', + marginBottom: spacing.sm, + }, + replaceInput: { + height: 40, + 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, + }, + replaceList: { + maxHeight: 160, + marginBottom: spacing.sm, + }, + replaceOption: { + paddingVertical: spacing.xs, + paddingHorizontal: spacing.sm, + borderRadius: borderRadius.full, + borderWidth: 1, + borderColor: colors.sentinel.cardBorder, + marginBottom: spacing.xs, + }, + replaceOptionText: { + fontSize: typography.fontSize.xs, + color: colors.sentinel.text, + fontWeight: '600', + }, + replaceCancel: { + alignSelf: 'flex-end', + paddingHorizontal: spacing.md, + paddingVertical: spacing.xs, + borderRadius: borderRadius.full, + borderWidth: 1, + borderColor: colors.sentinel.cardBorder, + }, + replaceCancelText: { + fontSize: typography.fontSize.xs, + color: colors.sentinel.textSecondary, + fontWeight: '600', + letterSpacing: typography.letterSpacing.wide, + }, selectorList: { maxHeight: 260, marginBottom: spacing.md, @@ -2317,12 +2373,7 @@ const styles = StyleSheet.create({ borderWidth: 1, borderColor: colors.sentinel.cardBorder, marginBottom: spacing.md, - gap: spacing.sm, - }, - summaryItem: { - paddingBottom: spacing.sm, - borderBottomWidth: 1, - borderBottomColor: colors.sentinel.cardBorder, + gap: spacing.xs, }, summaryLabel: { fontSize: typography.fontSize.xs, @@ -2396,29 +2447,4 @@ const styles = StyleSheet.create({ fontWeight: '700', letterSpacing: typography.letterSpacing.wide, }, - 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, - }, }); diff --git a/src/types/index.ts b/src/types/index.ts index 5d46372..8045de0 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -77,6 +77,7 @@ export interface ProtocolInfo { export interface User { id: number; username: string; + email?: string; public_key: string; is_admin: boolean; guale: boolean;