From 6822638d47b890d9e26c1aebdea623f73d9527b1 Mon Sep 17 00:00:00 2001 From: lusixing <32328454+lusixing@users.noreply.github.com> Date: Mon, 2 Feb 2026 19:40:49 -0800 Subject: [PATCH] complete_heir_functions --- src/hooks/useVaultAssets.ts | 57 +++++- src/screens/VaultScreen.tsx | 310 ++++++++++++++++++++++++++++++++- src/services/assets.service.ts | 6 +- src/types/index.ts | 2 + src/utils/vaultAssets.ts | 7 + 5 files changed, 373 insertions(+), 9 deletions(-) diff --git a/src/hooks/useVaultAssets.ts b/src/hooks/useVaultAssets.ts index 2c20c77..fe3c29b 100644 --- a/src/hooks/useVaultAssets.ts +++ b/src/hooks/useVaultAssets.ts @@ -40,6 +40,8 @@ export interface UseVaultAssetsReturn { 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) */ @@ -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 => { + 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, diff --git a/src/screens/VaultScreen.tsx b/src/screens/VaultScreen.tsx index dbbebe8..0264d64 100644 --- a/src/screens/VaultScreen.tsx +++ b/src/screens/VaultScreen.tsx @@ -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(null); const [backupContent, setBackupContent] = useState(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() { {detailMetaLabel} {detailMetaValue} + {selectedAsset?.heirEmail ? ( + + ASSIGNED HEIR + {selectedAsset.heirEmail} + + ) : null} @@ -1487,14 +1535,26 @@ export default function VaultScreen() { Export Cipher Pack - - + + Reset Sentinel Timer + + setShowAssignModal(true)} + activeOpacity={0.7} + > + + + {selectedAsset?.heirEmail ? 'Change Heir' : 'Assign Heir'} + + + setShowDeleteConfirm(true)} - activeOpacity={0.8} + activeOpacity={0.7} > Delete Treasure @@ -1593,6 +1653,110 @@ export default function VaultScreen() { + + {/* Assign Heir Modal */} + setShowAssignModal(false)} + > + + setShowAssignModal(false)} + /> + + + + + + + Designate Heir + + Enter the email address of the person who should inherit this treasure. + + + + + HEIR EMAIL ADDRESS + + + + + { + setShowAssignModal(false); + setHeirEmail(''); + }} + disabled={isAssigning} + > + Cancel + + + + {isAssigning ? ( + Assigning... + ) : ( + Confirm Heir + )} + + + + + + + + + {/* Assign Heir Error Modal */} + setShowAssignErrorModal(false)} + > + + setShowAssignErrorModal(false)} + /> + + + + + Assignment Failed + {assignErrorMessage} + setShowAssignErrorModal(false)} + > + Dismiss + + + + ); @@ -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', + }, }); diff --git a/src/services/assets.service.ts b/src/services/assets.service.ts index 964ce94..3f993bd 100644 --- a/src/services/assets.service.ts +++ b/src/services/assets.service.ts @@ -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); }); } diff --git a/src/types/index.ts b/src/types/index.ts index 8045de0..f334ca2 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -28,6 +28,8 @@ export interface VaultAsset { createdAt: Date; updatedAt: Date; isEncrypted: boolean; + heirEmail?: string; + rawData?: any; // For debug logging } // Sentinel Types diff --git a/src/utils/vaultAssets.ts b/src/utils/vaultAssets.ts index a8910d9..c9c061e 100644 --- a/src/utils/vaultAssets.ts +++ b/src/utils/vaultAssets.ts @@ -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, }; }