The vault notification mode1
This commit is contained in:
5
App.tsx
5
App.tsx
@@ -6,6 +6,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { Buffer } from 'buffer';
|
||||||
import { StatusBar } from 'expo-status-bar';
|
import { StatusBar } from 'expo-status-bar';
|
||||||
import { NavigationContainer } from '@react-navigation/native';
|
import { NavigationContainer } from '@react-navigation/native';
|
||||||
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
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 { AuthProvider, useAuth } from './src/context/AuthContext';
|
||||||
import { colors } from './src/theme/colors';
|
import { colors } from './src/theme/colors';
|
||||||
|
|
||||||
|
if (typeof globalThis !== 'undefined' && !globalThis.Buffer) {
|
||||||
|
globalThis.Buffer = Buffer;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loading screen shown while restoring auth state
|
* Loading screen shown while restoring auth state
|
||||||
*/
|
*/
|
||||||
|
|||||||
55
package-lock.json
generated
55
package-lock.json
generated
@@ -14,6 +14,8 @@
|
|||||||
"@react-navigation/bottom-tabs": "^6.6.1",
|
"@react-navigation/bottom-tabs": "^6.6.1",
|
||||||
"@react-navigation/native": "^6.1.18",
|
"@react-navigation/native": "^6.1.18",
|
||||||
"@react-navigation/native-stack": "^6.11.0",
|
"@react-navigation/native-stack": "^6.11.0",
|
||||||
|
"bip39": "^3.1.0",
|
||||||
|
"buffer": "^6.0.3",
|
||||||
"expo": "~52.0.0",
|
"expo": "~52.0.0",
|
||||||
"expo-asset": "~11.0.5",
|
"expo-asset": "~11.0.5",
|
||||||
"expo-constants": "~17.0.8",
|
"expo-constants": "~17.0.8",
|
||||||
@@ -3180,6 +3182,18 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@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": {
|
"node_modules/@nodelib/fs.scandir": {
|
||||||
"version": "2.1.5",
|
"version": "2.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||||
@@ -4420,6 +4434,15 @@
|
|||||||
"node": ">=0.6"
|
"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": {
|
"node_modules/bplist-creator": {
|
||||||
"version": "0.0.7",
|
"version": "0.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.0.7.tgz",
|
||||||
@@ -4507,9 +4530,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/buffer": {
|
"node_modules/buffer": {
|
||||||
"version": "5.7.1",
|
"version": "6.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
|
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
||||||
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
|
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -4527,7 +4550,7 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"base64-js": "^1.3.1",
|
"base64-js": "^1.3.1",
|
||||||
"ieee754": "^1.1.13"
|
"ieee754": "^1.2.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/buffer-alloc": {
|
"node_modules/buffer-alloc": {
|
||||||
@@ -11035,6 +11058,30 @@
|
|||||||
"node": ">=10"
|
"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": {
|
"node_modules/whatwg-url-without-unicode/node_modules/webidl-conversions": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz",
|
||||||
|
|||||||
@@ -15,6 +15,8 @@
|
|||||||
"@react-navigation/bottom-tabs": "^6.6.1",
|
"@react-navigation/bottom-tabs": "^6.6.1",
|
||||||
"@react-navigation/native": "^6.1.18",
|
"@react-navigation/native": "^6.1.18",
|
||||||
"@react-navigation/native-stack": "^6.11.0",
|
"@react-navigation/native-stack": "^6.11.0",
|
||||||
|
"bip39": "^3.1.0",
|
||||||
|
"buffer": "^6.0.3",
|
||||||
"expo": "~52.0.0",
|
"expo": "~52.0.0",
|
||||||
"expo-asset": "~11.0.5",
|
"expo-asset": "~11.0.5",
|
||||||
"expo-constants": "~17.0.8",
|
"expo-constants": "~17.0.8",
|
||||||
|
|||||||
@@ -112,6 +112,7 @@ export const MOCK_CONFIG = {
|
|||||||
USER: {
|
USER: {
|
||||||
id: 999,
|
id: 999,
|
||||||
username: 'MockCaptain',
|
username: 'MockCaptain',
|
||||||
|
email: 'captain@sentinel.local',
|
||||||
public_key: 'mock_public_key',
|
public_key: 'mock_public_key',
|
||||||
is_admin: true,
|
is_admin: true,
|
||||||
guale: false,
|
guale: false,
|
||||||
|
|||||||
@@ -348,7 +348,7 @@ export default function SentinelScreen() {
|
|||||||
onRequestClose={() => setShowVault(false)}
|
onRequestClose={() => setShowVault(false)}
|
||||||
>
|
>
|
||||||
<View style={styles.vaultModalContainer}>
|
<View style={styles.vaultModalContainer}>
|
||||||
<VaultScreen />
|
{showVault ? <VaultScreen /> : null}
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.vaultCloseButton}
|
style={styles.vaultCloseButton}
|
||||||
onPress={() => setShowVault(false)}
|
onPress={() => setShowVault(false)}
|
||||||
|
|||||||
@@ -19,9 +19,11 @@ import { LinearGradient } from 'expo-linear-gradient';
|
|||||||
import { Ionicons, Feather, MaterialCommunityIcons, FontAwesome5 } from '@expo/vector-icons';
|
import { Ionicons, Feather, MaterialCommunityIcons, FontAwesome5 } from '@expo/vector-icons';
|
||||||
import { captureRef } from 'react-native-view-shot';
|
import { captureRef } from 'react-native-view-shot';
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
|
import * as bip39 from 'bip39';
|
||||||
import { colors, typography, spacing, borderRadius, shadows } from '../theme/colors';
|
import { colors, typography, spacing, borderRadius, shadows } from '../theme/colors';
|
||||||
import { VaultAsset, VaultAssetType, Heir } from '../types';
|
import { VaultAsset, VaultAssetType, Heir } from '../types';
|
||||||
import BiometricModal from '../components/common/BiometricModal';
|
import BiometricModal from '../components/common/BiometricModal';
|
||||||
|
import { useAuth } from '../context/AuthContext';
|
||||||
|
|
||||||
// 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 }> = {
|
||||||
@@ -40,17 +42,6 @@ const accountProviderOptions = [
|
|||||||
{ key: 'custom', label: 'Other', icon: 'shield-account', iconType: 'material' as const },
|
{ 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[] = [
|
const initialHeirs: Heir[] = [
|
||||||
{
|
{
|
||||||
id: '1',
|
id: '1',
|
||||||
@@ -84,14 +75,7 @@ const initialHeirs: Heir[] = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const generateMnemonic = (wordCount = 12) => {
|
const generateMnemonic = () => bip39.generateMnemonic(128).split(' ');
|
||||||
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 splitMnemonic = (words: string[]) => [
|
const splitMnemonic = (words: string[]) => [
|
||||||
words.slice(0, 4),
|
words.slice(0, 4),
|
||||||
@@ -104,15 +88,6 @@ type HeirAssignment = {
|
|||||||
heir: Heir;
|
heir: Heir;
|
||||||
};
|
};
|
||||||
|
|
||||||
const shuffle = <T,>(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
|
// Mock data
|
||||||
const initialAssets: VaultAsset[] = [
|
const initialAssets: VaultAsset[] = [
|
||||||
{
|
{
|
||||||
@@ -183,6 +158,7 @@ export default function VaultScreen() {
|
|||||||
const [showAddBiometric, setShowAddBiometric] = useState(false);
|
const [showAddBiometric, setShowAddBiometric] = useState(false);
|
||||||
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 [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);
|
||||||
@@ -190,12 +166,11 @@ export default function VaultScreen() {
|
|||||||
const [selectedHeir, setSelectedHeir] = useState<Heir | null>(null);
|
const [selectedHeir, setSelectedHeir] = useState<Heir | null>(null);
|
||||||
const [selectedHeirAsset, setSelectedHeirAsset] = useState<VaultAsset | null>(null);
|
const [selectedHeirAsset, setSelectedHeirAsset] = useState<VaultAsset | null>(null);
|
||||||
const [assignments, setAssignments] = useState<HeirAssignment[]>([]);
|
const [assignments, setAssignments] = useState<HeirAssignment[]>([]);
|
||||||
const [wordOptions, setWordOptions] = useState<string[]>([]);
|
const [replaceIndex, setReplaceIndex] = useState<number | null>(null);
|
||||||
const [selectedWords, setSelectedWords] = useState<string[]>([]);
|
const [replaceQuery, setReplaceQuery] = useState('');
|
||||||
const [progressIndex, setProgressIndex] = useState(0);
|
const [progressIndex, setProgressIndex] = useState(0);
|
||||||
const [progressAnim] = useState(new Animated.Value(0));
|
const [progressAnim] = useState(new Animated.Value(0));
|
||||||
const [showEmailForm, setShowEmailForm] = useState(false);
|
const { user } = useAuth();
|
||||||
const [emailAddress, setEmailAddress] = useState('');
|
|
||||||
const [isCapturing, setIsCapturing] = useState(false);
|
const [isCapturing, setIsCapturing] = useState(false);
|
||||||
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);
|
||||||
@@ -242,22 +217,17 @@ export default function VaultScreen() {
|
|||||||
setShowBiometric(false);
|
setShowBiometric(false);
|
||||||
const words = generateMnemonic();
|
const words = generateMnemonic();
|
||||||
const parts = splitMnemonic(words);
|
const parts = splitMnemonic(words);
|
||||||
const decoys = MNEMONIC_WORDS.filter((word) => !words.includes(word));
|
|
||||||
const options = shuffle([...words, ...decoys.slice(0, 12)]);
|
|
||||||
setMnemonicWords(words);
|
setMnemonicWords(words);
|
||||||
setMnemonicParts(parts);
|
setMnemonicParts(parts);
|
||||||
setWordOptions(options);
|
setReplaceIndex(null);
|
||||||
setSelectedWords([]);
|
setReplaceQuery('');
|
||||||
setMnemonicStep(1);
|
setMnemonicStep(1);
|
||||||
setHeirStep('decision');
|
setHeirStep('decision');
|
||||||
setSelectedHeir(null);
|
setSelectedHeir(null);
|
||||||
setSelectedHeirAsset(null);
|
setSelectedHeirAsset(null);
|
||||||
setAssignments([]);
|
|
||||||
setProgressIndex(0);
|
setProgressIndex(0);
|
||||||
progressAnim.setValue(0);
|
progressAnim.setValue(0);
|
||||||
setShowMnemonic(true);
|
setTimeout(() => setShowMnemonic(true), 200);
|
||||||
setShowEmailForm(false);
|
|
||||||
setEmailAddress('');
|
|
||||||
AsyncStorage.setItem('sentinel_mnemonic_part_local', parts[0].join(' ')).catch(() => {
|
AsyncStorage.setItem('sentinel_mnemonic_part_local', parts[0].join(' ')).catch(() => {
|
||||||
// Best-effort local store; UI remains available
|
// Best-effort local store; UI remains available
|
||||||
});
|
});
|
||||||
@@ -269,12 +239,20 @@ export default function VaultScreen() {
|
|||||||
const uri = await captureRef(mnemonicRef, {
|
const uri = await captureRef(mnemonicRef, {
|
||||||
format: 'png',
|
format: 'png',
|
||||||
quality: 1,
|
quality: 1,
|
||||||
result: 'tmpfile',
|
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({
|
await Share.share({
|
||||||
url: uri,
|
url: uri,
|
||||||
message: 'Sentinel key backup',
|
message: 'Sentinel key backup',
|
||||||
});
|
});
|
||||||
|
}
|
||||||
setMnemonicStep(4);
|
setMnemonicStep(4);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Alert.alert('Screenshot failed', 'Please try again or use email backup.');
|
Alert.alert('Screenshot failed', 'Please try again or use email backup.');
|
||||||
@@ -284,45 +262,19 @@ export default function VaultScreen() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleEmailBackup = () => {
|
const handleEmailBackup = () => {
|
||||||
setShowEmailForm(true);
|
// Proceed immediately; email delivery is handled separately.
|
||||||
};
|
|
||||||
|
|
||||||
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);
|
setMnemonicStep(4);
|
||||||
} catch (error) {
|
setShowMnemonic(true);
|
||||||
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(() => {
|
useEffect(() => {
|
||||||
@@ -384,11 +336,23 @@ export default function VaultScreen() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleHeirYes = () => {
|
const handleHeirYes = () => {
|
||||||
setHeirStep('asset');
|
setShowMnemonic(false);
|
||||||
|
setIsUnlocked(true);
|
||||||
|
setShowLegacyAssignCta(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleHeirNo = () => {
|
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) => {
|
const handleSelectHeirAsset = (asset: VaultAsset) => {
|
||||||
@@ -398,25 +362,6 @@ export default function VaultScreen() {
|
|||||||
|
|
||||||
const handleSelectHeir = (heir: Heir) => {
|
const handleSelectHeir = (heir: Heir) => {
|
||||||
setSelectedHeir(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');
|
setHeirStep('summary');
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -543,60 +488,7 @@ export default function VaultScreen() {
|
|||||||
&& addVerified
|
&& addVerified
|
||||||
&& (selectedType !== 'private_key' || rehearsalConfirmed);
|
&& (selectedType !== 'private_key' || rehearsalConfirmed);
|
||||||
|
|
||||||
// Lock screen
|
const mnemonicModal = (
|
||||||
if (!isUnlocked) {
|
|
||||||
return (
|
|
||||||
<View style={styles.lockContainer}>
|
|
||||||
<LinearGradient
|
|
||||||
colors={[colors.vault.backgroundGradientStart, colors.vault.backgroundGradientEnd]}
|
|
||||||
style={styles.lockGradient}
|
|
||||||
>
|
|
||||||
<SafeAreaView style={styles.lockSafeArea}>
|
|
||||||
<View style={styles.lockContent}>
|
|
||||||
<Animated.View style={[styles.lockIconContainer, { transform: [{ scale: pulseAnim }] }]}>
|
|
||||||
<LinearGradient
|
|
||||||
colors={[colors.nautical.teal, colors.nautical.deepTeal]}
|
|
||||||
style={styles.lockIconGradient}
|
|
||||||
>
|
|
||||||
<MaterialCommunityIcons name="treasure-chest" size={64} color={colors.vault.primary} />
|
|
||||||
</LinearGradient>
|
|
||||||
</Animated.View>
|
|
||||||
|
|
||||||
<Text style={styles.lockTitle}>THE DEEP VAULT</Text>
|
|
||||||
<Text style={styles.lockSubtitle}>Where treasures rest in silence</Text>
|
|
||||||
|
|
||||||
<View style={styles.waveContainer}>
|
|
||||||
<MaterialCommunityIcons name="waves" size={48} color={colors.vault.secondary} style={{ opacity: 0.3 }} />
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<TouchableOpacity
|
|
||||||
style={styles.unlockButton}
|
|
||||||
onPress={() => setShowBiometric(true)}
|
|
||||||
activeOpacity={0.8}
|
|
||||||
>
|
|
||||||
<LinearGradient
|
|
||||||
colors={[colors.vault.primary, colors.vault.secondary]}
|
|
||||||
style={styles.unlockButtonGradient}
|
|
||||||
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>
|
|
||||||
</LinearGradient>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
</SafeAreaView>
|
|
||||||
</LinearGradient>
|
|
||||||
|
|
||||||
<BiometricModal
|
|
||||||
visible={showBiometric}
|
|
||||||
onSuccess={handleUnlock}
|
|
||||||
onCancel={() => setShowBiometric(false)}
|
|
||||||
title="Enter the Vault"
|
|
||||||
message="Verify your identity to access your treasures"
|
|
||||||
isDark
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
visible={showMnemonic}
|
visible={showMnemonic}
|
||||||
animationType="fade"
|
animationType="fade"
|
||||||
@@ -626,39 +518,86 @@ export default function VaultScreen() {
|
|||||||
{mnemonicStep === 1 ? (
|
{mnemonicStep === 1 ? (
|
||||||
<>
|
<>
|
||||||
<Text style={styles.mnemonicSubtitle}>
|
<Text style={styles.mnemonicSubtitle}>
|
||||||
Select the 12 words in order.
|
Review your 12-word mnemonic. Tap any word to replace it.
|
||||||
</Text>
|
</Text>
|
||||||
<View style={styles.selectedRow}>
|
|
||||||
<Text style={styles.selectedLabel}>Selected {selectedWords.length}/12</Text>
|
|
||||||
<Text style={styles.selectedValue}>{selectedWords.join(' ')}</Text>
|
|
||||||
</View>
|
|
||||||
<View style={styles.wordGrid}>
|
<View style={styles.wordGrid}>
|
||||||
{wordOptions.map((word) => {
|
{mnemonicWords.map((word, index) => (
|
||||||
const isSelected = selectedWords.includes(word);
|
|
||||||
return (
|
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
key={word}
|
key={`${word}-${index}`}
|
||||||
style={[styles.wordChip, isSelected && styles.wordChipSelected]}
|
style={[
|
||||||
onPress={() => toggleWord(word)}
|
styles.wordChip,
|
||||||
|
replaceIndex === index && styles.wordChipSelected,
|
||||||
|
]}
|
||||||
|
onPress={() => setReplaceIndex(index)}
|
||||||
activeOpacity={0.8}
|
activeOpacity={0.8}
|
||||||
>
|
>
|
||||||
<Text style={[styles.wordChipText, isSelected && styles.wordChipTextSelected]}>
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.wordChipIndex,
|
||||||
|
replaceIndex === index && styles.wordChipTextSelected,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{index + 1}
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
styles.wordChipText,
|
||||||
|
replaceIndex === index && styles.wordChipTextSelected,
|
||||||
|
]}
|
||||||
|
>
|
||||||
{word}
|
{word}
|
||||||
</Text>
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
);
|
))}
|
||||||
})}
|
|
||||||
</View>
|
</View>
|
||||||
|
{replaceIndex !== null ? (
|
||||||
|
<View style={styles.replacePanel}>
|
||||||
|
<Text style={styles.replaceTitle}>
|
||||||
|
Replace word {replaceIndex + 1}
|
||||||
|
</Text>
|
||||||
|
<TextInput
|
||||||
|
style={styles.replaceInput}
|
||||||
|
value={replaceQuery}
|
||||||
|
onChangeText={setReplaceQuery}
|
||||||
|
placeholder="Search a word"
|
||||||
|
placeholderTextColor={colors.sentinel.textSecondary}
|
||||||
|
autoCapitalize="none"
|
||||||
|
autoCorrect={false}
|
||||||
|
/>
|
||||||
|
<ScrollView style={styles.replaceList} showsVerticalScrollIndicator={false}>
|
||||||
|
{(replaceQuery
|
||||||
|
? bip39.wordlists.english.filter((word) =>
|
||||||
|
word.startsWith(replaceQuery.toLowerCase())
|
||||||
|
)
|
||||||
|
: bip39.wordlists.english
|
||||||
|
)
|
||||||
|
.slice(0, 24)
|
||||||
|
.map((word) => (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={[
|
key={word}
|
||||||
styles.mnemonicPrimaryButton,
|
style={styles.replaceOption}
|
||||||
selectedWords.length !== 12 && styles.mnemonicButtonDisabled,
|
onPress={() => handleReplaceWord(word)}
|
||||||
]}
|
activeOpacity={0.8}
|
||||||
onPress={handleConfirmSelection}
|
|
||||||
activeOpacity={0.85}
|
|
||||||
disabled={selectedWords.length !== 12}
|
|
||||||
>
|
>
|
||||||
<Text style={styles.mnemonicPrimaryText}>CONFIRM SELECTION</Text>
|
<Text style={styles.replaceOptionText}>{word}</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
))}
|
||||||
|
</ScrollView>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.replaceCancel}
|
||||||
|
onPress={() => setReplaceIndex(null)}
|
||||||
|
activeOpacity={0.85}
|
||||||
|
>
|
||||||
|
<Text style={styles.replaceCancelText}>CANCEL</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.mnemonicPrimaryButton}
|
||||||
|
onPress={() => setMnemonicStep(2)}
|
||||||
|
activeOpacity={0.85}
|
||||||
|
>
|
||||||
|
<Text style={styles.mnemonicPrimaryText}>NEXT</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -717,27 +656,6 @@ export default function VaultScreen() {
|
|||||||
>
|
>
|
||||||
<Text style={styles.mnemonicSecondaryText}>EMAIL BACKUP</Text>
|
<Text style={styles.mnemonicSecondaryText}>EMAIL BACKUP</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
{showEmailForm ? (
|
|
||||||
<View style={styles.emailForm}>
|
|
||||||
<TextInput
|
|
||||||
style={styles.emailInput}
|
|
||||||
value={emailAddress}
|
|
||||||
onChangeText={setEmailAddress}
|
|
||||||
placeholder="you@email.com"
|
|
||||||
placeholderTextColor={colors.sentinel.textSecondary}
|
|
||||||
keyboardType="email-address"
|
|
||||||
autoCapitalize="none"
|
|
||||||
autoCorrect={false}
|
|
||||||
/>
|
|
||||||
<TouchableOpacity
|
|
||||||
style={styles.emailSendButton}
|
|
||||||
onPress={handleSendEmail}
|
|
||||||
activeOpacity={0.85}
|
|
||||||
>
|
|
||||||
<Text style={styles.emailSendText}>SEND EMAIL</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
) : null}
|
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
@@ -805,7 +723,6 @@ export default function VaultScreen() {
|
|||||||
<ScrollView style={styles.selectorList} showsVerticalScrollIndicator={false}>
|
<ScrollView style={styles.selectorList} showsVerticalScrollIndicator={false}>
|
||||||
{assets.map((asset) => {
|
{assets.map((asset) => {
|
||||||
const config = assetTypeConfig[asset.type];
|
const config = assetTypeConfig[asset.type];
|
||||||
const assignment = getAssignmentForAsset(asset.id);
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
key={asset.id}
|
key={asset.id}
|
||||||
@@ -818,21 +735,12 @@ export default function VaultScreen() {
|
|||||||
</View>
|
</View>
|
||||||
<View style={styles.selectorContent}>
|
<View style={styles.selectorContent}>
|
||||||
<Text style={styles.selectorTitle}>{asset.label}</Text>
|
<Text style={styles.selectorTitle}>{asset.label}</Text>
|
||||||
<Text style={styles.selectorSubtitle}>
|
<Text style={styles.selectorSubtitle}>{config.label}</Text>
|
||||||
{assignment ? `Assigned to ${assignment.heir.name}` : config.label}
|
|
||||||
</Text>
|
|
||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
<TouchableOpacity
|
|
||||||
style={styles.mnemonicPrimaryButton}
|
|
||||||
onPress={handleReviewAssignments}
|
|
||||||
activeOpacity={0.85}
|
|
||||||
>
|
|
||||||
<Text style={styles.mnemonicPrimaryText}>REVIEW & SUBMIT</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.mnemonicSecondaryButton}
|
style={styles.mnemonicSecondaryButton}
|
||||||
onPress={() => setHeirStep('decision')}
|
onPress={() => setHeirStep('decision')}
|
||||||
@@ -882,15 +790,13 @@ export default function VaultScreen() {
|
|||||||
Confirm assignment details.
|
Confirm assignment details.
|
||||||
</Text>
|
</Text>
|
||||||
<View style={styles.summaryCard}>
|
<View style={styles.summaryCard}>
|
||||||
{assignments.map((item) => (
|
<Text style={styles.summaryLabel}>Vault Item</Text>
|
||||||
<View key={item.asset.id} style={styles.summaryItem}>
|
<Text style={styles.summaryValue}>{selectedHeirAsset?.label}</Text>
|
||||||
<Text style={styles.summaryLabel}>Asset</Text>
|
<Text style={styles.summaryLabel}>Legacy Handler</Text>
|
||||||
<Text style={styles.summaryValue}>{item.asset.label}</Text>
|
<Text style={styles.summaryValue}>{selectedHeir?.name}</Text>
|
||||||
<Text style={styles.summaryLabel}>Heir</Text>
|
<Text style={styles.summaryValue}>{selectedHeir?.email}</Text>
|
||||||
<Text style={styles.summaryValue}>{item.heir.name}</Text>
|
<Text style={styles.summaryLabel}>Release Tier</Text>
|
||||||
<Text style={styles.summaryValue}>{item.heir.email}</Text>
|
<Text style={styles.summaryValue}>Tier {selectedHeir?.releaseLevel}</Text>
|
||||||
</View>
|
|
||||||
))}
|
|
||||||
</View>
|
</View>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.mnemonicPrimaryButton}
|
style={styles.mnemonicPrimaryButton}
|
||||||
@@ -901,34 +807,80 @@ export default function VaultScreen() {
|
|||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.mnemonicSecondaryButton}
|
style={styles.mnemonicSecondaryButton}
|
||||||
onPress={() => setHeirStep('asset')}
|
onPress={() => setHeirStep('heir')}
|
||||||
activeOpacity={0.85}
|
activeOpacity={0.85}
|
||||||
>
|
>
|
||||||
<Text style={styles.mnemonicSecondaryText}>EDIT ASSIGNMENTS</Text>
|
<Text style={styles.mnemonicSecondaryText}>EDIT</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
<View style={styles.stepDots}>
|
<View style={styles.stepDots}>
|
||||||
{[1, 2, 3, 4, 5].map((step) => (
|
<View style={[styles.stepDot, mnemonicStep === 1 && styles.stepDotActive]} />
|
||||||
<View
|
<View style={[styles.stepDot, mnemonicStep !== 1 && styles.stepDotActive]} />
|
||||||
key={step}
|
|
||||||
style={[
|
|
||||||
styles.stepDot,
|
|
||||||
mnemonicStep === step && styles.stepDotActive,
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</View>
|
</View>
|
||||||
</LinearGradient>
|
</LinearGradient>
|
||||||
</KeyboardAvoidingView>
|
</KeyboardAvoidingView>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Lock screen
|
||||||
|
const lockScreen = (
|
||||||
|
<View style={styles.lockContainer}>
|
||||||
|
<LinearGradient
|
||||||
|
colors={[colors.vault.backgroundGradientStart, colors.vault.backgroundGradientEnd]}
|
||||||
|
style={styles.lockGradient}
|
||||||
|
>
|
||||||
|
<SafeAreaView style={styles.lockSafeArea}>
|
||||||
|
<View style={styles.lockContent}>
|
||||||
|
<Animated.View style={[styles.lockIconContainer, { transform: [{ scale: pulseAnim }] }]}>
|
||||||
|
<LinearGradient
|
||||||
|
colors={[colors.nautical.teal, colors.nautical.deepTeal]}
|
||||||
|
style={styles.lockIconGradient}
|
||||||
|
>
|
||||||
|
<MaterialCommunityIcons name="treasure-chest" size={64} color={colors.vault.primary} />
|
||||||
|
</LinearGradient>
|
||||||
|
</Animated.View>
|
||||||
|
|
||||||
|
<Text style={styles.lockTitle}>THE DEEP VAULT</Text>
|
||||||
|
<Text style={styles.lockSubtitle}>Where treasures rest in silence</Text>
|
||||||
|
|
||||||
|
<View style={styles.waveContainer}>
|
||||||
|
<MaterialCommunityIcons name="waves" size={48} color={colors.vault.secondary} style={{ opacity: 0.3 }} />
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.unlockButton}
|
||||||
|
onPress={() => setShowBiometric(true)}
|
||||||
|
activeOpacity={0.8}
|
||||||
|
>
|
||||||
|
<LinearGradient
|
||||||
|
colors={[colors.vault.primary, colors.vault.secondary]}
|
||||||
|
style={styles.unlockButtonGradient}
|
||||||
|
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>
|
||||||
|
</LinearGradient>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</SafeAreaView>
|
||||||
|
</LinearGradient>
|
||||||
|
|
||||||
|
<BiometricModal
|
||||||
|
visible={showBiometric}
|
||||||
|
onSuccess={handleUnlock}
|
||||||
|
onCancel={() => setShowBiometric(false)}
|
||||||
|
title="Enter the Vault"
|
||||||
|
message="Verify your identity to access your treasures"
|
||||||
|
isDark
|
||||||
|
/>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
const vaultScreen = (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<LinearGradient
|
<LinearGradient
|
||||||
colors={[colors.vault.backgroundGradientStart, colors.vault.backgroundGradientEnd]}
|
colors={[colors.vault.backgroundGradientStart, colors.vault.backgroundGradientEnd]}
|
||||||
@@ -953,6 +905,24 @@ export default function VaultScreen() {
|
|||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
{showLegacyAssignCta && (
|
||||||
|
<View style={styles.legacyCtaCard}>
|
||||||
|
<View style={styles.legacyCtaInfo}>
|
||||||
|
<Text style={styles.legacyCtaTitle}>Legacy Assignment</Text>
|
||||||
|
<Text style={styles.legacyCtaText}>
|
||||||
|
Continue assigning a vault item to your legacy handler.
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.legacyCtaButton}
|
||||||
|
onPress={handleOpenLegacyAssign}
|
||||||
|
activeOpacity={0.85}
|
||||||
|
>
|
||||||
|
<Text style={styles.legacyCtaButtonText}>Continue</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Asset List */}
|
{/* Asset List */}
|
||||||
<ScrollView
|
<ScrollView
|
||||||
style={styles.assetList}
|
style={styles.assetList}
|
||||||
@@ -1424,9 +1394,19 @@ export default function VaultScreen() {
|
|||||||
</Modal>
|
</Modal>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.root}>
|
||||||
|
{isUnlocked ? vaultScreen : lockScreen}
|
||||||
|
{mnemonicModal}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
|
root: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
@@ -1541,6 +1521,42 @@ const styles = StyleSheet.create({
|
|||||||
fontSize: typography.fontSize.sm,
|
fontSize: typography.fontSize.sm,
|
||||||
color: colors.vault.textSecondary,
|
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: {
|
assetList: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
@@ -2200,28 +2216,6 @@ const styles = StyleSheet.create({
|
|||||||
lineHeight: 22,
|
lineHeight: 22,
|
||||||
textAlign: 'center',
|
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: {
|
progressContainer: {
|
||||||
marginTop: spacing.sm,
|
marginTop: spacing.sm,
|
||||||
marginBottom: spacing.md,
|
marginBottom: spacing.md,
|
||||||
@@ -2254,9 +2248,10 @@ const styles = StyleSheet.create({
|
|||||||
marginBottom: spacing.lg,
|
marginBottom: spacing.lg,
|
||||||
},
|
},
|
||||||
wordChip: {
|
wordChip: {
|
||||||
|
minWidth: '30%',
|
||||||
paddingHorizontal: spacing.sm,
|
paddingHorizontal: spacing.sm,
|
||||||
paddingVertical: spacing.xs,
|
paddingVertical: spacing.sm,
|
||||||
borderRadius: borderRadius.full,
|
borderRadius: borderRadius.lg,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: colors.sentinel.cardBorder,
|
borderColor: colors.sentinel.cardBorder,
|
||||||
backgroundColor: 'transparent',
|
backgroundColor: 'transparent',
|
||||||
@@ -2265,6 +2260,11 @@ const styles = StyleSheet.create({
|
|||||||
backgroundColor: colors.sentinel.primary,
|
backgroundColor: colors.sentinel.primary,
|
||||||
borderColor: colors.sentinel.primary,
|
borderColor: colors.sentinel.primary,
|
||||||
},
|
},
|
||||||
|
wordChipIndex: {
|
||||||
|
fontSize: typography.fontSize.xs,
|
||||||
|
color: colors.sentinel.textSecondary,
|
||||||
|
marginBottom: 2,
|
||||||
|
},
|
||||||
wordChipText: {
|
wordChipText: {
|
||||||
color: colors.sentinel.text,
|
color: colors.sentinel.text,
|
||||||
fontSize: typography.fontSize.xs,
|
fontSize: typography.fontSize.xs,
|
||||||
@@ -2273,6 +2273,62 @@ const styles = StyleSheet.create({
|
|||||||
wordChipTextSelected: {
|
wordChipTextSelected: {
|
||||||
color: colors.nautical.cream,
|
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: {
|
selectorList: {
|
||||||
maxHeight: 260,
|
maxHeight: 260,
|
||||||
marginBottom: spacing.md,
|
marginBottom: spacing.md,
|
||||||
@@ -2317,12 +2373,7 @@ const styles = StyleSheet.create({
|
|||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: colors.sentinel.cardBorder,
|
borderColor: colors.sentinel.cardBorder,
|
||||||
marginBottom: spacing.md,
|
marginBottom: spacing.md,
|
||||||
gap: spacing.sm,
|
gap: spacing.xs,
|
||||||
},
|
|
||||||
summaryItem: {
|
|
||||||
paddingBottom: spacing.sm,
|
|
||||||
borderBottomWidth: 1,
|
|
||||||
borderBottomColor: colors.sentinel.cardBorder,
|
|
||||||
},
|
},
|
||||||
summaryLabel: {
|
summaryLabel: {
|
||||||
fontSize: typography.fontSize.xs,
|
fontSize: typography.fontSize.xs,
|
||||||
@@ -2396,29 +2447,4 @@ const styles = StyleSheet.create({
|
|||||||
fontWeight: '700',
|
fontWeight: '700',
|
||||||
letterSpacing: typography.letterSpacing.wide,
|
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,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ export interface ProtocolInfo {
|
|||||||
export interface User {
|
export interface User {
|
||||||
id: number;
|
id: number;
|
||||||
username: string;
|
username: string;
|
||||||
|
email?: string;
|
||||||
public_key: string;
|
public_key: string;
|
||||||
is_admin: boolean;
|
is_admin: boolean;
|
||||||
guale: boolean;
|
guale: boolean;
|
||||||
|
|||||||
Reference in New Issue
Block a user