complete_heir_functions
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -28,6 +28,8 @@ export interface VaultAsset {
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
isEncrypted: boolean;
|
||||
heirEmail?: string;
|
||||
rawData?: any; // For debug logging
|
||||
}
|
||||
|
||||
// Sentinel Types
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user