diff --git a/src/config/index.ts b/src/config/index.ts index 9bec176..9d93b62 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -51,6 +51,7 @@ export const API_ENDPOINTS = { CREATE: '/assets/create', CLAIM: '/assets/claim', ASSIGN: '/assets/assign', + DELETE: '/assets/delete', }, // AI Services diff --git a/src/hooks/useVaultAssets.ts b/src/hooks/useVaultAssets.ts index ffaffeb..2c20c77 100644 --- a/src/hooks/useVaultAssets.ts +++ b/src/hooks/useVaultAssets.ts @@ -11,6 +11,7 @@ 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, @@ -37,6 +38,8 @@ export interface UseVaultAssetsReturn { 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; /** True while create request is in flight */ isSealing: boolean; /** Error message from last create failure (non-401) */ @@ -129,7 +132,7 @@ export function useVaultAssets(isUnlocked: boolean): UseVaultAssetsReturn { console.log(' Encrypted: ', content_inner_encrypted); } - await assetsService.createAsset( + const createdAsset = await assetsService.createAsset( { title: title.trim(), private_key_shard: s1Str, @@ -137,6 +140,11 @@ export function useVaultAssets(isUnlocked: boolean): UseVaultAssetsReturn { }, 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) { @@ -166,6 +174,44 @@ export function useVaultAssets(isUnlocked: boolean): UseVaultAssetsReturn { [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 clearCreateError = useCallback(() => setCreateError(null), []); return { @@ -173,6 +219,7 @@ export function useVaultAssets(isUnlocked: boolean): UseVaultAssetsReturn { setAssets, refreshAssets, createAsset, + deleteAsset, isSealing, createError, clearCreateError, diff --git a/src/screens/FlowScreen.tsx b/src/screens/FlowScreen.tsx index a45214f..3332abb 100644 --- a/src/screens/FlowScreen.tsx +++ b/src/screens/FlowScreen.tsx @@ -542,12 +542,17 @@ export default function FlowScreen() { const encryptedSummary = vault.encryptData(aesKey, generatedSummary).toString('hex'); // Create asset in backend - await assetsService.createAsset({ + const createdAsset = await assetsService.createAsset({ title: `Chat Summary - ${new Date().toLocaleDateString()}`, private_key_shard: shareServer, content_inner_encrypted: encryptedSummary, }, token); + // Backup plaintext content locally + if (createdAsset && createdAsset.id && user?.id) { + await storageService.saveAssetBackup(createdAsset.id, generatedSummary, user.id); + } + setSaveResult({ success: true, message: 'Summary encrypted and saved to your vault successfully.' }); setShowSaveResultModal(true); } catch (error) { diff --git a/src/screens/VaultScreen.tsx b/src/screens/VaultScreen.tsx index 339619a..dbbebe8 100644 --- a/src/screens/VaultScreen.tsx +++ b/src/screens/VaultScreen.tsx @@ -27,6 +27,7 @@ import { useAuth } from '../context/AuthContext'; import { useVaultAssets } from '../hooks/useVaultAssets'; import { getVaultStorageKeys } from '../config'; import { mnemonicToEntropy, splitSecret, serializeShare } from '../utils/sss'; +import { storageService } from '../services/storage.service'; import { SentinelVault } from '@/utils/crypto_core'; // Asset type configuration with nautical theme @@ -92,41 +93,6 @@ type HeirAssignment = { heir: Heir; }; -// 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 renderAssetTypeIcon = (config: typeof assetTypeConfig[VaultAssetType], size: number, color: string) => { switch (config.iconType) { @@ -149,11 +115,14 @@ export default function VaultScreen() { setAssets, refreshAssets, createAsset: createVaultAsset, + deleteAsset: deleteVaultAsset, isSealing, createError: addError, clearCreateError: clearAddError, } = useVaultAssets(isUnlocked); const [showAddModal, setShowAddModal] = useState(false); + const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); + const [isDeleting, setIsDeleting] = useState(false); const [selectedType, setSelectedType] = useState('custom'); const [newLabel, setNewLabel] = useState(''); const [showUploadSuccess, setShowUploadSuccess] = useState(false); @@ -172,6 +141,8 @@ export default function VaultScreen() { const [showMnemonic, setShowMnemonic] = useState(false); const [showLegacyAssignCta, setShowLegacyAssignCta] = useState(false); const [hasS0, setHasS0] = useState(null); + const [backupContent, setBackupContent] = useState(null); + const [isFetchingBackup, setIsFetchingBackup] = useState(false); const [mnemonicWords, setMnemonicWords] = useState([]); const [mnemonicParts, setMnemonicParts] = useState([]); const [mnemonicStep, setMnemonicStep] = useState<1 | 2 | 3 | 4 | 5>(1); @@ -504,6 +475,61 @@ export default function VaultScreen() { setSelectedAsset(null); setShowKeyPreview(false); setShowGuardedBiometric(false); + setBackupContent(null); + }; + + const handleFetchBackup = async () => { + if (!selectedAsset || !user?.id) return; + + setIsFetchingBackup(true); + try { + const content = await storageService.getAssetBackup(Number(selectedAsset.id), user.id); + if (content) { + setBackupContent(content); + } else { + if (typeof Alert !== 'undefined' && Alert.alert) { + Alert.alert('No Backup Found', 'No local plaintext backup found for this treasure.'); + } + } + } catch (error) { + console.error('Fetch backup error:', error); + if (typeof Alert !== 'undefined' && Alert.alert) { + Alert.alert('Error', 'Failed to retrieve local backup.'); + } + } finally { + setIsFetchingBackup(false); + } + }; + + const handleDeleteAsset = async () => { + if (!selectedAsset || isDeleting) return; + + setIsDeleting(true); + try { + const result = await deleteVaultAsset(Number(selectedAsset.id)); + if (result.success) { + setShowDeleteConfirm(false); + handleCloseDetail(); + if (typeof Alert !== 'undefined' && Alert.alert) { + Alert.alert('Success', 'Treasure removed from the vault.'); + } + } else if (result.isUnauthorized) { + setShowDeleteConfirm(false); + handleCloseDetail(); + if (typeof Alert !== 'undefined' && Alert.alert) { + Alert.alert('Unauthorized', 'Your session has expired. Please sign in again.'); + } + } else if (result.error && typeof Alert !== 'undefined' && Alert.alert) { + Alert.alert('Failed', result.error); + } + } catch (error) { + console.error('Delete error:', error); + if (typeof Alert !== 'undefined' && Alert.alert) { + Alert.alert('Error', 'An unexpected error occurred during deletion.'); + } + } finally { + setIsDeleting(false); + } }; const handleGuardedAccess = () => { @@ -513,6 +539,7 @@ export default function VaultScreen() { const handleGuardedSuccess = () => { setShowGuardedBiometric(false); setShowKeyPreview(true); + handleFetchBackup(); }; const handleAddVerification = () => { @@ -1464,6 +1491,14 @@ export default function VaultScreen() { Reset Sentinel Timer + setShowDeleteConfirm(true)} + activeOpacity={0.8} + > + + Delete Treasure + @@ -1474,6 +1509,7 @@ export default function VaultScreen() { Plaintext access requires biometric verification and a memory rehearsal step. + Begin Verification + {showKeyPreview && ( - MNEMONIC SHARD (MASKED) - ocean-anchored-ember-veil + LOCAL PLAINTEXT BACKUP + + {isFetchingBackup ? 'Fetching content...' : (backupContent || 'No local backup found for this treasure')} + )} @@ -1509,6 +1548,51 @@ export default function VaultScreen() { isDark /> + + {/* Delete Confirmation Modal */} + setShowDeleteConfirm(false)} + > + + setShowDeleteConfirm(false)} + /> + + + + + Remove Treasure? + + This action cannot be undone. The treasure will be permanently shredded from the deep vault. + + + setShowDeleteConfirm(false)} + disabled={isDeleting} + > + Cancel + + + {isDeleting ? ( + Shredding... + ) : ( + Confirm Delete + )} + + + + + ); @@ -1777,6 +1861,73 @@ const styles = StyleSheet.create({ backgroundColor: 'rgba(26, 58, 74, 0.8)', justifyContent: 'flex-end', }, + modalOverlayDismiss: { + ...StyleSheet.absoluteFillObject, + }, + deleteConfirmContent: { + backgroundColor: colors.vault.cardBackground, + marginHorizontal: spacing.lg, + borderRadius: borderRadius.xl, + padding: spacing.xl, + borderWidth: 1, + borderColor: colors.vault.cardBorder, + alignItems: 'center', + marginBottom: spacing.xxl + spacing.lg, + }, + deleteIconContainer: { + width: 64, + height: 64, + borderRadius: 32, + backgroundColor: `${colors.vault.warning}20`, + alignItems: 'center', + justifyContent: 'center', + marginBottom: spacing.lg, + }, + deleteTitle: { + fontSize: typography.fontSize.lg, + color: colors.vault.text, + fontWeight: '700', + marginBottom: spacing.sm, + fontFamily: typography.fontFamily.serif, + }, + deleteMessage: { + fontSize: typography.fontSize.sm, + color: colors.vault.textSecondary, + textAlign: 'center', + lineHeight: typography.fontSize.sm * 1.5, + marginBottom: spacing.xl, + }, + deleteButtons: { + flexDirection: 'row', + gap: spacing.md, + width: '100%', + }, + deleteCancelButton: { + 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)', + }, + deleteCancelText: { + color: colors.vault.textSecondary, + fontSize: typography.fontSize.base, + fontWeight: '600', + }, + deleteConfirmButton: { + flex: 2, + paddingVertical: spacing.md, + borderRadius: borderRadius.lg, + backgroundColor: colors.vault.warning, + alignItems: 'center', + }, + deleteConfirmText: { + color: colors.vault.text, + fontSize: typography.fontSize.base, + fontWeight: '700', + }, modalContent: { backgroundColor: colors.nautical.cream, borderTopLeftRadius: borderRadius.xxl, @@ -2581,8 +2732,66 @@ const styles = StyleSheet.create({ borderColor: colors.sentinel.cardBorder, }, mnemonicSecondaryText: { - color: colors.sentinel.text, fontWeight: '700', letterSpacing: typography.letterSpacing.wide, }, + deleteActionRow: { + backgroundColor: 'rgba(229, 115, 115, 0.08)', + borderColor: 'rgba(229, 115, 115, 0.2)', + marginTop: spacing.sm, + }, + deleteActionText: { + color: colors.vault.warning, + }, + confirmModalOverlay: { + flex: 1, + backgroundColor: 'rgba(11, 20, 24, 0.85)', + justifyContent: 'center', + alignItems: 'center', + padding: spacing.xl, + }, + confirmModalContent: { + width: '100%', + backgroundColor: colors.vault.cardBackground, + borderRadius: borderRadius.xl, + padding: spacing.lg, + borderWidth: 1, + borderColor: colors.vault.cardBorder, + gap: spacing.md, + }, + confirmModalTitle: { + fontSize: typography.fontSize.lg, + color: colors.vault.text, + fontWeight: '700', + textAlign: 'center', + }, + confirmModalText: { + fontSize: typography.fontSize.base, + color: colors.vault.textSecondary, + textAlign: 'center', + lineHeight: 22, + }, + confirmModalButtons: { + flexDirection: 'row', + gap: spacing.md, + marginTop: spacing.sm, + }, + confirmModalButton: { + flex: 1, + paddingVertical: spacing.md, + borderRadius: borderRadius.lg, + alignItems: 'center', + justifyContent: 'center', + }, + confirmCancelButton: { + backgroundColor: 'rgba(255, 255, 255, 0.05)', + borderWidth: 1, + borderColor: 'rgba(255, 255, 255, 0.1)', + }, + confirmDeleteButton: { + backgroundColor: colors.vault.warning, + }, + confirmCancelText: { + color: colors.vault.textSecondary, + }, }); diff --git a/src/services/assets.service.ts b/src/services/assets.service.ts index c0e3197..964ce94 100644 --- a/src/services/assets.service.ts +++ b/src/services/assets.service.ts @@ -245,4 +245,44 @@ export const assetsService = { throw error; } }, + + /** + * Delete an asset + * @param assetId - ID of the asset to delete + * @param token - JWT token for authentication + * @returns Success message + */ + async deleteAsset(assetId: number, token: string): Promise<{ message: string }> { + if (NO_BACKEND_MODE) { + logApiDebug('Delete Asset', `Using mock mode for ID: ${assetId}`); + return new Promise((resolve) => { + setTimeout(() => { + resolve({ message: 'Asset deleted successfully' }); + }, MOCK_CONFIG.RESPONSE_DELAY); + }); + } + + const url = buildApiUrl(API_ENDPOINTS.ASSETS.DELETE); + logApiDebug('Delete Asset URL', url); + + try { + const response = await fetch(url, { + method: 'POST', + headers: getApiHeaders(token), + body: JSON.stringify({ asset_id: assetId }), + }); + + logApiDebug('Delete Asset Response Status', response.status); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw new Error(errorData.detail || 'Failed to delete asset'); + } + + return await response.json(); + } catch (error) { + console.error('Delete asset error:', error); + throw error; + } + }, }; diff --git a/src/services/storage.service.ts b/src/services/storage.service.ts index 1048ce3..b4e7dd4 100644 --- a/src/services/storage.service.ts +++ b/src/services/storage.service.ts @@ -14,6 +14,7 @@ import AsyncStorage from '@react-native-async-storage/async-storage'; const STORAGE_KEYS = { CHAT_HISTORY: '@sentinel:chat_history', CURRENT_MESSAGES: '@sentinel:current_messages', + ASSET_BACKUP: '@sentinel:asset_backup', } as const; // ============================================================================= @@ -115,6 +116,32 @@ export const storageService = { } catch (e) { console.error('Error clearing storage data:', e); } + }, + + /** + * Save the plaintext backup of an asset locally + */ + async saveAssetBackup(assetId: number, content: string, userId: string | number): Promise { + try { + const key = `${this.getUserKey(STORAGE_KEYS.ASSET_BACKUP, userId)}:${assetId}`; + await AsyncStorage.setItem(key, content); + console.log(`[Storage] Saved asset backup for user ${userId}, asset ${assetId}`); + } catch (e) { + console.error(`Error saving asset backup for asset ${assetId}:`, e); + } + }, + + /** + * Retrieve the plaintext backup of an asset locally + */ + async getAssetBackup(assetId: number, userId: string | number): Promise { + try { + const key = `${this.getUserKey(STORAGE_KEYS.ASSET_BACKUP, userId)}:${assetId}`; + return await AsyncStorage.getItem(key); + } catch (e) { + console.error(`Error getting asset backup for asset ${assetId}:`, e); + return null; + } } }; diff --git a/src/theme/colors.ts b/src/theme/colors.ts index 2567d78..480b310 100644 --- a/src/theme/colors.ts +++ b/src/theme/colors.ts @@ -5,7 +5,7 @@ export const colors = { // Base colors white: '#FFFFFF', black: '#1A2F3A', - + // Nautical palette nautical: { deepTeal: '#1B4D5C', @@ -21,7 +21,7 @@ export const colors = { navy: '#1A3A4A', sage: '#8CA5A5', }, - + // Flow - Captain's Journal flow: { background: '#E8F6F8', @@ -38,7 +38,7 @@ export const colors = { archivedText: '#7A9A9A', highlight: '#B8E0E5', }, - + // Vault - Ship's Vault vault: { background: '#1B4D5C', @@ -54,7 +54,7 @@ export const colors = { warning: '#E57373', success: '#6BBF8A', }, - + // Sentinel - Lighthouse Watch sentinel: { background: '#1A3A4A', @@ -70,7 +70,7 @@ export const colors = { statusWarning: '#E5B873', statusCritical: '#E57373', }, - + // Heritage - Legacy Fleet heritage: { background: '#E8F6F8', @@ -86,7 +86,7 @@ export const colors = { confirmed: '#6BBF8A', pending: '#E5B873', }, - + // Me - Captain's Quarters me: { background: '#E8F6F8', diff --git a/src/utils/vaultAssets.ts b/src/utils/vaultAssets.ts index 9b8c3e2..a8910d9 100644 --- a/src/utils/vaultAssets.ts +++ b/src/utils/vaultAssets.ts @@ -31,6 +31,8 @@ export const VAULT_ASSET_TYPES: VaultAssetType[] = [ 'custom', ]; +export const initialVaultAssets: VaultAsset[] = []; + // ----------------------------------------------------------------------------- // Mapping // ----------------------------------------------------------------------------- @@ -60,41 +62,3 @@ export function mapApiAssetsToVaultAssets(apiList: ApiAsset[]): VaultAsset[] { return apiList.map(mapApiAssetToVaultAsset); } -// ----------------------------------------------------------------------------- -// Mock / initial data (fallback when API is unavailable) -// ----------------------------------------------------------------------------- - -export const initialVaultAssets: 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, - }, -];