feat(vault): add get/create assets API in workflow

TODO: update vault.service.ts. Use MNEMONIC workflow to create real private_key_shard and content_inner_encrypted
This commit is contained in:
Ada
2026-02-01 09:19:45 -08:00
parent 536513ab3f
commit f6fa19d0b2
9 changed files with 628 additions and 78 deletions

View File

@@ -24,6 +24,7 @@ import { colors, typography, spacing, borderRadius, shadows } from '../theme/col
import { VaultAsset, VaultAssetType, Heir } from '../types';
import BiometricModal from '../components/common/BiometricModal';
import { useAuth } from '../context/AuthContext';
import { useVaultAssets } from '../hooks/useVaultAssets';
// Asset type configuration with nautical theme
const assetTypeConfig: Record<VaultAssetType, { icon: string; iconType: 'ionicons' | 'feather' | 'material' | 'fontawesome5'; label: string }> = {
@@ -89,40 +90,40 @@ type HeirAssignment = {
};
// Mock data
const initialAssets: VaultAsset[] = [
{
id: '1',
type: 'private_key',
label: 'ETH Main Wallet Key',
createdAt: new Date('2024-01-10'),
updatedAt: new Date('2024-01-10'),
isEncrypted: true,
},
{
id: '2',
type: 'game_account',
label: 'Steam Account Credentials',
createdAt: new Date('2024-01-08'),
updatedAt: new Date('2024-01-08'),
isEncrypted: true,
},
{
id: '3',
type: 'document',
label: 'Insurance Policy Scan',
createdAt: new Date('2024-01-05'),
updatedAt: new Date('2024-01-05'),
isEncrypted: true,
},
{
id: '4',
type: 'will',
label: 'Testament Draft v2',
createdAt: new Date('2024-01-02'),
updatedAt: new Date('2024-01-15'),
isEncrypted: true,
},
];
// const initialAssets: VaultAsset[] = [
// {
// id: '1',
// type: 'private_key',
// label: 'ETH Main Wallet Key',
// createdAt: new Date('2024-01-10'),
// updatedAt: new Date('2024-01-10'),
// isEncrypted: true,
// },
// {
// id: '2',
// type: 'game_account',
// label: 'Steam Account Credentials',
// createdAt: new Date('2024-01-08'),
// updatedAt: new Date('2024-01-08'),
// isEncrypted: true,
// },
// {
// id: '3',
// type: 'document',
// label: 'Insurance Policy Scan',
// createdAt: new Date('2024-01-05'),
// updatedAt: new Date('2024-01-05'),
// isEncrypted: true,
// },
// {
// id: '4',
// type: 'will',
// label: 'Testament Draft v2',
// createdAt: new Date('2024-01-02'),
// updatedAt: new Date('2024-01-15'),
// isEncrypted: true,
// },
// ];
const renderAssetTypeIcon = (config: typeof assetTypeConfig[VaultAssetType], size: number, color: string) => {
switch (config.iconType) {
@@ -140,7 +141,15 @@ const renderAssetTypeIcon = (config: typeof assetTypeConfig[VaultAssetType], siz
export default function VaultScreen() {
const [isUnlocked, setIsUnlocked] = useState(false);
const [showBiometric, setShowBiometric] = useState(false);
const [assets, setAssets] = useState<VaultAsset[]>(initialAssets);
const {
assets,
setAssets,
refreshAssets,
createAsset: createVaultAsset,
isSealing,
createError: addError,
clearCreateError: clearAddError,
} = useVaultAssets(isUnlocked);
const [showAddModal, setShowAddModal] = useState(false);
const [selectedType, setSelectedType] = useState<VaultAssetType>('custom');
const [newLabel, setNewLabel] = useState('');
@@ -170,8 +179,9 @@ export default function VaultScreen() {
const [replaceQuery, setReplaceQuery] = useState('');
const [progressIndex, setProgressIndex] = useState(0);
const [progressAnim] = useState(new Animated.Value(0));
const { user } = useAuth();
const { user, token } = useAuth();
const [isCapturing, setIsCapturing] = useState(false);
const [treasureContent, setTreasureContent] = useState('');
const mnemonicRef = useRef<View>(null);
const progressTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
@@ -375,29 +385,52 @@ export default function VaultScreen() {
setAccountProvider('bank');
};
const handleAddAsset = () => {
if (!newLabel.trim()) return;
const handleAddAsset = async () => {
if (!newLabel.trim() || !treasureContent.trim()) return;
if (!addVerified) return;
if (selectedType === 'private_key' && !rehearsalConfirmed) return;
if (!token) {
Alert.alert('Not logged in', 'Please sign in first to add a Treasure.');
return;
}
const newAsset: VaultAsset = {
id: Date.now().toString(),
type: selectedType,
label: newLabel,
createdAt: new Date(),
updatedAt: new Date(),
isEncrypted: true,
};
const result = await createVaultAsset({
title: newLabel.trim(),
content: treasureContent.trim(),
});
setAssets([newAsset, ...assets]);
setNewLabel('');
setSelectedType('custom');
setShowAddModal(false);
setAddVerified(false);
setRehearsalConfirmed(false);
setShowUploadSuccess(true);
setTimeout(() => setShowUploadSuccess(false), 2500);
if (result.success) {
setNewLabel('');
setTreasureContent('');
setSelectedType('custom');
setAddVerified(false);
setRehearsalConfirmed(false);
setShowAddModal(false);
clearAddError();
setShowUploadSuccess(true);
setTimeout(() => setShowUploadSuccess(false), 2500);
if (typeof Alert !== 'undefined' && Alert.alert) {
Alert.alert('Success', 'Treasure sealed and saved successfully.');
}
return;
}
if (result.isUnauthorized) {
setShowAddModal(false);
clearAddError();
if (typeof Alert !== 'undefined' && Alert.alert) {
Alert.alert(
'Unauthorized',
'Your session has expired or you are not logged in. Please sign in again.',
[{ text: 'OK' }]
);
}
return;
}
if (result.error && typeof Alert !== 'undefined' && Alert.alert) {
Alert.alert('Failed', result.error);
}
};
const formatDate = (date: Date) => {
@@ -485,7 +518,9 @@ export default function VaultScreen() {
? '12/24 Words'
: selectedConfig?.label || '--';
const canSeal = !!newLabel.trim()
&& !!treasureContent.trim()
&& addVerified
&& !isSealing
&& (selectedType !== 'private_key' || rehearsalConfirmed);
const mnemonicModal = (
@@ -499,10 +534,10 @@ export default function VaultScreen() {
style={styles.mnemonicOverlay}
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
>
<View ref={mnemonicRef} collapsable={false}>
<LinearGradient
colors={[colors.sentinel.cardBackground, colors.sentinel.backgroundGradientEnd]}
style={styles.mnemonicCard}
ref={mnemonicRef}
>
<TouchableOpacity
style={styles.mnemonicClose}
@@ -821,6 +856,7 @@ export default function VaultScreen() {
<View style={[styles.stepDot, mnemonicStep !== 1 && styles.stepDotActive]} />
</View>
</LinearGradient>
</View>
</KeyboardAvoidingView>
</Modal>
);
@@ -963,6 +999,7 @@ export default function VaultScreen() {
style={styles.addButton}
onPress={() => {
resetAddFlow();
clearAddError();
setShowAddModal(true);
}}
activeOpacity={0.9}
@@ -1005,7 +1042,7 @@ export default function VaultScreen() {
</View>
<View style={styles.stepRow}>
{['Type', 'Method', 'Verify'].map((label, index) => {
{['Title', 'Content', 'Verify'].map((label, index) => {
const stepIndex = index + 1;
const isActive = addStep === stepIndex;
const isDone = addStep > stepIndex;
@@ -1034,6 +1071,14 @@ export default function VaultScreen() {
{addStep === 1 && (
<>
<Text style={styles.modalLabel}>TREASURE TITLE</Text>
<TextInput
style={styles.input}
placeholder="e.g., Main wallet mnemonic"
placeholderTextColor={colors.nautical.sage}
value={newLabel}
onChangeText={setNewLabel}
/>
<Text style={styles.modalLabel}>TREASURE TYPE</Text>
<ScrollView
horizontal
@@ -1093,9 +1138,25 @@ export default function VaultScreen() {
);
})}
</View>
<Text style={styles.modalLabel}>CONTENT</Text>
<TextInput
style={[styles.input, styles.inputMultiline]}
placeholder="Enter content to seal (plaintext is encrypted locally before upload)"
placeholderTextColor={colors.nautical.sage}
value={treasureContent}
onChangeText={setTreasureContent}
multiline
numberOfLines={6}
textAlignVertical="top"
/>
<View style={styles.encryptionNote}>
<MaterialCommunityIcons name="anchor" size={16} color={colors.nautical.teal} />
<Text style={styles.encryptionNoteText}>
Data is encrypted on-device. Plaintext is shredded after sealing.
</Text>
</View>
</>
)}
{selectedType === 'game_account' && (
<>
<Text style={styles.modalLabel}>ACCOUNT PROVIDER</Text>
@@ -1135,24 +1196,16 @@ export default function VaultScreen() {
<Ionicons name="log-in-outline" size={18} color={colors.nautical.cream} />
<Text style={styles.loginButtonText}>Open App to Login</Text>
</TouchableOpacity>
<Text style={styles.modalLabel}>TREASURE NAME</Text>
<TextInput
style={styles.input}
placeholder="e.g., Main wallet mnemonic"
placeholderTextColor={colors.nautical.sage}
value={newLabel}
onChangeText={setNewLabel}
/>
</>
)}
<Text style={styles.modalLabel}>TREASURE NAME</Text>
<TextInput
style={styles.input}
placeholder="e.g., Main wallet mnemonic"
placeholderTextColor={colors.nautical.sage}
value={newLabel}
onChangeText={setNewLabel}
/>
<View style={styles.encryptionNote}>
<MaterialCommunityIcons name="anchor" size={16} color={colors.nautical.teal} />
<Text style={styles.encryptionNoteText}>
Data is encrypted on-device. Plaintext is shredded after sealing.
</Text>
</View>
</>
)}
@@ -1201,14 +1254,24 @@ export default function VaultScreen() {
</>
)}
{addError ? (
<View style={styles.addErrorBox}>
<Ionicons name="warning" size={18} color="#c2410c" />
<Text style={styles.addErrorText}>{addError}</Text>
</View>
) : null}
<View style={styles.modalButtons}>
<TouchableOpacity
style={styles.cancelButton}
onPress={() => {
if (addStep === 1) {
setShowAddModal(false);
setTreasureContent('');
clearAddError();
} else {
setAddStep(addStep - 1);
clearAddError();
}
}}
>
@@ -1248,7 +1311,7 @@ export default function VaultScreen() {
end={{ x: 1, y: 0 }}
>
<MaterialCommunityIcons name="lock" size={18} color="#fff" />
<Text style={styles.confirmButtonText}>Seal Treasure</Text>
<Text style={styles.confirmButtonText}>{isSealing ? 'Sealing...' : 'Seal Treasure'}</Text>
</LinearGradient>
</TouchableOpacity>
)}
@@ -1344,7 +1407,7 @@ export default function VaultScreen() {
<Text style={styles.actionText}>Export Cipher Pack</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.actionRow} activeOpacity={0.8}>
<MaterialCommunityIcons name="timer-reset" size={18} color={colors.vault.primary} />
<MaterialCommunityIcons name="refresh" size={18} color={colors.vault.primary} />
<Text style={styles.actionText}>Reset Sentinel Timer</Text>
</TouchableOpacity>
</View>
@@ -1835,6 +1898,10 @@ const styles = StyleSheet.create({
borderWidth: 1,
borderColor: colors.nautical.lightMint,
},
inputMultiline: {
minHeight: 120,
paddingTop: spacing.base,
},
encryptionNote: {
flexDirection: 'row',
alignItems: 'center',
@@ -1850,6 +1917,23 @@ const styles = StyleSheet.create({
color: colors.nautical.teal,
lineHeight: typography.fontSize.sm * 1.4,
},
addErrorBox: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: 'rgba(194, 65, 12, 0.12)',
borderRadius: borderRadius.lg,
padding: spacing.md,
marginBottom: spacing.md,
gap: spacing.sm,
borderWidth: 1,
borderColor: 'rgba(194, 65, 12, 0.3)',
},
addErrorText: {
flex: 1,
fontSize: typography.fontSize.sm,
color: '#c2410c',
lineHeight: typography.fontSize.sm * 1.4,
},
modalButtons: {
flexDirection: 'row',
gap: spacing.md,