/** * useVaultAssets: Encapsulates /assets/get and /assets/create for VaultScreen. * - Fetches assets when vault is unlocked and token exists. * - Exposes createAsset with 401/network error handling and list refresh on success. */ import { useState, useEffect, useCallback } from 'react'; import * as bip39 from 'bip39'; import AsyncStorage from '@react-native-async-storage/async-storage'; import { useAuth } from '../context/AuthContext'; import { assetsService } from '../services/assets.service'; import { getVaultStorageKeys, DEBUG_MODE } from '../config'; import { SentinelVault } from '../utils/crypto_core'; import { storageService } from '../services/storage.service'; import { initialVaultAssets, mapApiAssetsToVaultAssets, type ApiAsset, } from '../utils/vaultAssets'; import type { VaultAsset } from '../types'; // ----------------------------------------------------------------------------- // Types // ----------------------------------------------------------------------------- export interface CreateAssetResult { success: boolean; isUnauthorized?: boolean; error?: string; } export interface UseVaultAssetsReturn { /** Current list (mock until API succeeds) */ assets: VaultAsset[]; /** Replace list (e.g. after external refresh) */ setAssets: React.Dispatch>; /** Refetch from GET /assets/get */ refreshAssets: () => Promise; /** Create asset via POST /assets/create; on success refreshes list */ createAsset: (params: { title: string; content: string }) => Promise; /** Delete asset via POST /assets/delete; on success refreshes list */ deleteAsset: (assetId: number) => Promise; /** Assign asset to heir via POST /assets/assign */ assignAsset: (assetId: number, heirEmail: string) => Promise; /** True while create request is in flight */ isSealing: boolean; /** Error message from last create failure (non-401) */ createError: string | null; /** Clear createError */ clearCreateError: () => void; } // ----------------------------------------------------------------------------- // Hook // ----------------------------------------------------------------------------- /** * Vault assets list + create. Fetches on unlock when token exists; keeps mock on error. */ export function useVaultAssets(isUnlocked: boolean): UseVaultAssetsReturn { const { user, token, signOut } = useAuth(); const [assets, setAssets] = useState(initialVaultAssets); const [isSealing, setIsSealing] = useState(false); const [createError, setCreateError] = useState(null); const refreshAssets = useCallback(async () => { if (!token) return; try { const list = await assetsService.getMyAssets(token); if (Array.isArray(list)) { setAssets(mapApiAssetsToVaultAssets(list as ApiAsset[])); } } 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, signOut]); // Fetch list when unlocked and token exists useEffect(() => { if (!isUnlocked || !token) return; let cancelled = false; assetsService .getMyAssets(token) .then((list) => { if (!cancelled && Array.isArray(list)) { setAssets(mapApiAssetsToVaultAssets(list as ApiAsset[])); } }) .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 () => { cancelled = true; }; }, [isUnlocked, token]); const createAsset = useCallback( async ({ title, content, }: { title: string; content: string; }): Promise => { if (!token) { return { success: false, error: 'Not logged in.' }; } setIsSealing(true); setCreateError(null); try { const vaultKeys = getVaultStorageKeys(user?.id ?? null); const [s1Str, aesKeyHex, s0Str, s2Str] = await Promise.all([ AsyncStorage.getItem(vaultKeys.SHARE_SERVER), AsyncStorage.getItem(vaultKeys.AES_KEY), AsyncStorage.getItem(vaultKeys.SHARE_DEVICE), AsyncStorage.getItem(vaultKeys.SHARE_HEIR), ]); if (!s1Str || !aesKeyHex) { throw new Error('Vault keys missing. Please re-unlock your vault.'); } const vault = new SentinelVault(); const aesKey = Buffer.from(aesKeyHex, 'hex'); const encryptedBuffer = vault.encryptData(aesKey, content.trim()); const content_inner_encrypted = encryptedBuffer.toString('hex'); if (DEBUG_MODE) { console.log('[DEBUG] Crypto Data during Asset Creation:'); console.log(' s0 (Device):', s0Str); console.log(' s1 (Server):', s1Str); console.log(' s2 (Heir): ', s2Str); console.log(' AES Key: ', aesKeyHex); console.log(' Encrypted: ', content_inner_encrypted); } const createdAsset = await assetsService.createAsset( { title: title.trim(), private_key_shard: s1Str, content_inner_encrypted, }, token ); // Backup plaintext content locally if (createdAsset && createdAsset.id && user?.id) { await storageService.saveAssetBackup(createdAsset.id, content, user.id); } 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 create.'); 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 (see API_BASE_URL in config).' : rawMessage; setCreateError(friendlyMessage); return { success: false, error: friendlyMessage }; } finally { setIsSealing(false); } }, [token, user, refreshAssets, signOut] ); const deleteAsset = useCallback( async (assetId: number): Promise => { if (!token) { return { success: false, error: 'Not logged in.' }; } setIsSealing(true); setCreateError(null); try { await assetsService.deleteAsset(assetId, 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 delete.'); 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, refreshAssets, signOut] ); const assignAsset = useCallback( async (assetId: number, heirEmail: string): Promise => { 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 { assets, setAssets, refreshAssets, createAsset, deleteAsset, assignAsset, isSealing, createError, clearCreateError, }; }