complete_heir_functions

This commit is contained in:
lusixing
2026-02-02 19:40:49 -08:00
parent 5c1172a912
commit 6822638d47
5 changed files with 373 additions and 9 deletions

View File

@@ -40,6 +40,8 @@ export interface UseVaultAssetsReturn {
createAsset: (params: { title: string; content: string }) => Promise<CreateAssetResult>;
/** Delete asset via POST /assets/delete; on success refreshes list */
deleteAsset: (assetId: number) => Promise<CreateAssetResult>;
/** Assign asset to heir via POST /assets/assign */
assignAsset: (assetId: number, heirEmail: string) => Promise<CreateAssetResult>;
/** True while create request is in flight */
isSealing: boolean;
/** Error message from last create failure (non-401) */
@@ -68,10 +70,14 @@ export function useVaultAssets(isUnlocked: boolean): UseVaultAssetsReturn {
if (Array.isArray(list)) {
setAssets(mapApiAssetsToVaultAssets(list as ApiAsset[]));
}
} catch {
} catch (err: unknown) {
const rawMessage = err instanceof Error ? err.message : String(err ?? '');
if (/Could not validate credentials/i.test(rawMessage)) {
signOut();
}
// Keep current assets (mock or previous fetch)
}
}, [token]);
}, [token, signOut]);
// Fetch list when unlocked and token exists
useEffect(() => {
@@ -84,7 +90,13 @@ export function useVaultAssets(isUnlocked: boolean): UseVaultAssetsReturn {
setAssets(mapApiAssetsToVaultAssets(list as ApiAsset[]));
}
})
.catch(() => {
.catch((err) => {
if (!cancelled) {
const rawMessage = err instanceof Error ? err.message : String(err ?? '');
if (/Could not validate credentials/i.test(rawMessage)) {
signOut();
}
}
// Keep initial (mock) assets
});
return () => {
@@ -212,6 +224,44 @@ export function useVaultAssets(isUnlocked: boolean): UseVaultAssetsReturn {
[token, refreshAssets, signOut]
);
const assignAsset = useCallback(
async (assetId: number, heirEmail: string): Promise<CreateAssetResult> => {
if (!token) {
return { success: false, error: 'Not logged in.' };
}
setIsSealing(true);
setCreateError(null);
try {
await assetsService.assignAsset({ asset_id: assetId, heir_email: heirEmail }, token);
await refreshAssets();
return { success: true };
} catch (err: unknown) {
const status =
err && typeof err === 'object' && 'status' in err
? (err as { status?: number }).status
: undefined;
const rawMessage =
err instanceof Error ? err.message : String(err ?? 'Failed to assign.');
const isUnauthorized =
status === 401 || /401|Unauthorized/i.test(rawMessage);
if (isUnauthorized) {
signOut();
return { success: false, isUnauthorized: true };
}
const friendlyMessage = /failed to fetch|network error/i.test(rawMessage)
? 'Network error. Please check that the backend is running and reachable.'
: rawMessage;
setCreateError(friendlyMessage);
return { success: false, error: friendlyMessage };
} finally {
setIsSealing(false);
}
},
[token, signOut]
);
const clearCreateError = useCallback(() => setCreateError(null), []);
return {
@@ -220,6 +270,7 @@ export function useVaultAssets(isUnlocked: boolean): UseVaultAssetsReturn {
refreshAssets,
createAsset,
deleteAsset,
assignAsset,
isSealing,
createError,
clearCreateError,

View File

@@ -25,7 +25,7 @@ import { VaultAsset, VaultAssetType, Heir } from '../types';
import BiometricModal from '../components/common/BiometricModal';
import { useAuth } from '../context/AuthContext';
import { useVaultAssets } from '../hooks/useVaultAssets';
import { getVaultStorageKeys } from '../config';
import { getVaultStorageKeys, DEBUG_MODE } from '../config';
import { mnemonicToEntropy, splitSecret, serializeShare } from '../utils/sss';
import { storageService } from '../services/storage.service';
import { SentinelVault } from '@/utils/crypto_core';
@@ -116,6 +116,7 @@ export default function VaultScreen() {
refreshAssets,
createAsset: createVaultAsset,
deleteAsset: deleteVaultAsset,
assignAsset: assignVaultAsset,
isSealing,
createError: addError,
clearCreateError: clearAddError,
@@ -139,6 +140,11 @@ export default function VaultScreen() {
const [showAddBiometric, setShowAddBiometric] = useState(false);
const [accountProvider, setAccountProvider] = useState<'bank' | 'steam' | 'facebook' | 'custom'>('bank');
const [showMnemonic, setShowMnemonic] = useState(false);
const [showAssignModal, setShowAssignModal] = useState(false);
const [showAssignErrorModal, setShowAssignErrorModal] = useState(false);
const [assignErrorMessage, setAssignErrorMessage] = useState('');
const [isAssigning, setIsAssigning] = useState(false);
const [heirEmail, setHeirEmail] = useState('');
const [showLegacyAssignCta, setShowLegacyAssignCta] = useState(false);
const [hasS0, setHasS0] = useState<boolean | null>(null);
const [backupContent, setBackupContent] = useState<string | null>(null);
@@ -468,6 +474,10 @@ export default function VaultScreen() {
setSelectedAsset(asset);
setShowDetail(true);
setShowKeyPreview(false);
if (DEBUG_MODE) {
console.log('[DEBUG] Vault Asset Details:', JSON.stringify(asset.rawData, null, 2));
}
};
const handleCloseDetail = () => {
@@ -532,6 +542,38 @@ export default function VaultScreen() {
}
};
const handleAssignHeir = async () => {
if (!selectedAsset || isAssigning) return;
if (!heirEmail.trim() || !heirEmail.includes('@')) {
Alert.alert('Invalid Email', 'Please enter a valid email address.');
return;
}
setIsAssigning(true);
try {
const result = await assignVaultAsset(Number(selectedAsset.id), heirEmail.trim());
if (result.success) {
setShowAssignModal(false);
setHeirEmail('');
Alert.alert('Success', `Asset assigned to ${heirEmail.trim()}`);
} else if (result.isUnauthorized) {
setShowAssignModal(false);
if (typeof Alert !== 'undefined' && Alert.alert) {
Alert.alert('Unauthorized', 'Your session has expired. Please sign in again.');
}
} else if (result.error) {
setAssignErrorMessage(result.error);
setShowAssignErrorModal(true);
}
} catch (error) {
console.error('Assign error:', error);
setAssignErrorMessage('An unexpected error occurred during assignment.');
setShowAssignErrorModal(true);
} finally {
setIsAssigning(false);
}
};
const handleGuardedAccess = () => {
setShowGuardedBiometric(true);
};
@@ -1475,6 +1517,12 @@ export default function VaultScreen() {
<Text style={styles.metaLabel}>{detailMetaLabel}</Text>
<Text style={styles.metaValue}>{detailMetaValue}</Text>
</View>
{selectedAsset?.heirEmail ? (
<View style={[styles.metaCard, { width: '100%' }]}>
<Text style={styles.metaLabel}>ASSIGNED HEIR</Text>
<Text style={styles.metaValue}>{selectedAsset.heirEmail}</Text>
</View>
) : null}
</View>
<View style={styles.actionGroup}>
@@ -1487,14 +1535,26 @@ export default function VaultScreen() {
<MaterialCommunityIcons name="file-lock" size={18} color={colors.vault.primary} />
<Text style={styles.actionText}>Export Cipher Pack</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.actionRow} activeOpacity={0.8}>
<MaterialCommunityIcons name="refresh" size={18} color={colors.vault.primary} />
<TouchableOpacity style={styles.actionRow} activeOpacity={0.7}>
<Ionicons name="notifications-outline" size={18} color={colors.vault.text} />
<Text style={styles.actionText}>Reset Sentinel Timer</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.actionRow, styles.assignActionRow]}
onPress={() => setShowAssignModal(true)}
activeOpacity={0.7}
>
<Ionicons name="person-add-outline" size={18} color={colors.vault.text} />
<Text style={styles.actionText}>
{selectedAsset?.heirEmail ? 'Change Heir' : 'Assign Heir'}
</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.actionRow, styles.deleteActionRow]}
onPress={() => setShowDeleteConfirm(true)}
activeOpacity={0.8}
activeOpacity={0.7}
>
<Feather name="trash-2" size={18} color={colors.vault.warning} />
<Text style={[styles.actionText, styles.deleteActionText]}>Delete Treasure</Text>
@@ -1593,6 +1653,110 @@ export default function VaultScreen() {
</View>
</View>
</Modal>
{/* Assign Heir Modal */}
<Modal
visible={showAssignModal}
animationType="slide"
transparent
onRequestClose={() => setShowAssignModal(false)}
>
<View style={styles.modalOverlay}>
<TouchableOpacity
style={styles.modalOverlayDismiss}
activeOpacity={1}
onPress={() => setShowAssignModal(false)}
/>
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
style={styles.assignModalContainer}
>
<View style={styles.assignModalContent}>
<View style={styles.assignModalHeader}>
<View style={styles.assignIconGlow}>
<Ionicons name="person-add" size={32} color={colors.vault.primary} />
</View>
<Text style={styles.assignTitle}>Designate Heir</Text>
<Text style={styles.assignSubtitle}>
Enter the email address of the person who should inherit this treasure.
</Text>
</View>
<View style={styles.assignInputContainer}>
<Text style={styles.inputLabel}>HEIR EMAIL ADDRESS</Text>
<TextInput
style={styles.assignInput}
value={heirEmail}
onChangeText={setHeirEmail}
placeholder="name@example.com"
placeholderTextColor={colors.vault.textSecondary}
keyboardType="email-address"
autoCapitalize="none"
autoCorrect={false}
/>
</View>
<View style={styles.assignButtons}>
<TouchableOpacity
style={styles.assignCancelButton}
onPress={() => {
setShowAssignModal(false);
setHeirEmail('');
}}
disabled={isAssigning}
>
<Text style={styles.assignCancelText}>Cancel</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.assignConfirmButton}
onPress={handleAssignHeir}
disabled={isAssigning || !heirEmail.trim()}
>
<LinearGradient
colors={[colors.vault.primary, colors.vault.secondary]}
style={styles.assignConfirmGradient}
>
{isAssigning ? (
<Text style={styles.assignConfirmText}>Assigning...</Text>
) : (
<Text style={styles.assignConfirmText}>Confirm Heir</Text>
)}
</LinearGradient>
</TouchableOpacity>
</View>
</View>
</KeyboardAvoidingView>
</View>
</Modal>
{/* Assign Heir Error Modal */}
<Modal
visible={showAssignErrorModal}
animationType="fade"
transparent
onRequestClose={() => setShowAssignErrorModal(false)}
>
<View style={styles.modalOverlay}>
<TouchableOpacity
style={styles.modalOverlayDismiss}
activeOpacity={1}
onPress={() => setShowAssignErrorModal(false)}
/>
<View style={styles.errorModalContent}>
<View style={styles.errorIconContainer}>
<MaterialCommunityIcons name="alert-circle-outline" size={32} color={colors.vault.warning} />
</View>
<Text style={styles.errorTitle}>Assignment Failed</Text>
<Text style={styles.errorMessage}>{assignErrorMessage}</Text>
<TouchableOpacity
style={styles.errorCloseButton}
onPress={() => setShowAssignErrorModal(false)}
>
<Text style={styles.errorCloseButtonText}>Dismiss</Text>
</TouchableOpacity>
</View>
</View>
</Modal>
</View>
);
@@ -2794,4 +2958,142 @@ const styles = StyleSheet.create({
confirmCancelText: {
color: colors.vault.textSecondary,
},
assignActionRow: {
// Optional specific styling
},
assignModalContainer: {
backgroundColor: colors.vault.cardBackground,
marginHorizontal: spacing.lg,
borderRadius: borderRadius.xl,
borderWidth: 1,
borderColor: colors.vault.cardBorder,
overflow: 'hidden',
marginBottom: spacing.xxl,
},
assignModalContent: {
padding: spacing.xl,
},
assignModalHeader: {
alignItems: 'center',
marginBottom: spacing.xl,
},
assignIconGlow: {
width: 64,
height: 64,
borderRadius: 32,
backgroundColor: `${colors.vault.primary}15`,
alignItems: 'center',
justifyContent: 'center',
marginBottom: spacing.md,
},
assignTitle: {
fontSize: typography.fontSize.lg,
color: colors.vault.text,
fontWeight: '700',
marginBottom: spacing.xs,
fontFamily: typography.fontFamily.serif,
},
assignSubtitle: {
fontSize: typography.fontSize.sm,
color: colors.vault.textSecondary,
textAlign: 'center',
lineHeight: typography.fontSize.sm * 1.5,
},
assignInputContainer: {
marginBottom: spacing.xl,
},
inputLabel: {
fontSize: typography.fontSize.xs,
color: colors.vault.textSecondary,
fontWeight: '700',
letterSpacing: 1,
marginBottom: spacing.sm,
},
assignInput: {
backgroundColor: 'rgba(255, 255, 255, 0.05)',
borderRadius: borderRadius.lg,
padding: spacing.md,
fontSize: typography.fontSize.base,
color: colors.vault.text,
borderWidth: 1,
borderColor: 'rgba(255, 255, 255, 0.1)',
},
assignButtons: {
flexDirection: 'row',
gap: spacing.md,
},
assignCancelButton: {
flex: 1,
paddingVertical: spacing.md,
borderRadius: borderRadius.lg,
backgroundColor: 'rgba(255, 255, 255, 0.08)',
alignItems: 'center',
borderWidth: 1,
borderColor: 'rgba(255, 255, 255, 0.1)',
},
assignCancelText: {
color: colors.vault.textSecondary,
fontSize: typography.fontSize.base,
fontWeight: '600',
},
assignConfirmButton: {
flex: 2,
borderRadius: borderRadius.lg,
overflow: 'hidden',
},
assignConfirmGradient: {
paddingVertical: spacing.md,
alignItems: 'center',
justifyContent: 'center',
},
assignConfirmText: {
color: colors.vault.text,
fontSize: typography.fontSize.base,
fontWeight: '700',
},
errorModalContent: {
backgroundColor: colors.vault.cardBackground,
marginHorizontal: spacing.lg,
borderRadius: borderRadius.xl,
padding: spacing.xl,
borderWidth: 1,
borderColor: colors.vault.cardBorder,
alignItems: 'center',
marginBottom: spacing.xxl,
},
errorIconContainer: {
width: 64,
height: 64,
borderRadius: 32,
backgroundColor: `${colors.vault.warning}15`,
alignItems: 'center',
justifyContent: 'center',
marginBottom: spacing.lg,
},
errorTitle: {
fontSize: typography.fontSize.lg,
color: colors.vault.text,
fontWeight: '700',
marginBottom: spacing.sm,
fontFamily: typography.fontFamily.serif,
},
errorMessage: {
fontSize: typography.fontSize.sm,
color: colors.vault.textSecondary,
textAlign: 'center',
lineHeight: typography.fontSize.sm * 1.5,
marginBottom: spacing.xl,
},
errorCloseButton: {
width: '100%',
paddingVertical: spacing.md,
borderRadius: borderRadius.lg,
backgroundColor: colors.vault.warning,
alignItems: 'center',
},
errorCloseButtonText: {
color: colors.vault.text,
fontSize: typography.fontSize.base,
fontWeight: '700',
},
});

View File

@@ -23,6 +23,7 @@ export interface Asset {
author_id: number;
private_key_shard: string;
content_outer_encrypted: string;
heir_email?: string;
}
export interface AssetCreate {
@@ -45,7 +46,7 @@ export interface AssetClaimResponse {
export interface AssetAssign {
asset_id: number;
heir_name: string;
heir_email: string;
}
// =============================================================================
@@ -59,6 +60,7 @@ const MOCK_ASSETS: Asset[] = [
author_id: MOCK_CONFIG.USER.id,
private_key_shard: 'mock_shard_1',
content_outer_encrypted: 'mock_encrypted_content_1',
heir_email: 'heir@example.com',
},
{
id: 2,
@@ -217,7 +219,7 @@ export const assetsService = {
logApiDebug('Assign Asset', 'Using mock mode');
return new Promise((resolve) => {
setTimeout(() => {
resolve({ message: `Asset assigned to ${assignment.heir_name}` });
resolve({ message: `Asset assigned to ${assignment.heir_email}` });
}, MOCK_CONFIG.RESPONSE_DELAY);
});
}

View File

@@ -28,6 +28,8 @@ export interface VaultAsset {
createdAt: Date;
updatedAt: Date;
isEncrypted: boolean;
heirEmail?: string;
rawData?: any; // For debug logging
}
// Sentinel Types

View File

@@ -14,8 +14,13 @@ export interface ApiAsset {
id: number;
title: string;
type?: string;
author_id?: number;
private_key_shard?: string;
content_outer_encrypted?: string;
created_at?: string;
updated_at?: string;
heir_id?: number;
heir_email?: string;
}
// -----------------------------------------------------------------------------
@@ -52,6 +57,8 @@ export function mapApiAssetToVaultAsset(api: ApiAsset): VaultAsset {
createdAt: api.created_at ? new Date(api.created_at) : new Date(),
updatedAt: api.updated_at ? new Date(api.updated_at) : new Date(),
isEncrypted: true,
heirEmail: api.heir_email,
rawData: api,
};
}