diff --git a/apps/browser-extension/src/utils/SqliteClient.ts b/apps/browser-extension/src/utils/SqliteClient.ts index e06f7fe15..cc6ec9165 100644 --- a/apps/browser-extension/src/utils/SqliteClient.ts +++ b/apps/browser-extension/src/utils/SqliteClient.ts @@ -927,23 +927,7 @@ export class SqliteClient { for (const attachment of attachments) { const isExistingAttachment = originalAttachmentIds.includes(attachment.Id); - if (isExistingAttachment) { - // Update existing attachment - const updateQuery = ` - UPDATE Attachments - SET Filename = ?, - Blob = ?, - UpdatedAt = ?, - IsDeleted = ? - WHERE Id = ?`; - this.executeUpdate(updateQuery, [ - attachment.Filename, - attachment.Blob as Uint8Array, - currentDateTime, - attachment.IsDeleted ? 1 : 0, - attachment.Id - ]); - } else { + if (!isExistingAttachment) { // Insert new attachment const insertQuery = ` INSERT INTO Attachments (Id, Filename, Blob, CredentialId, CreatedAt, UpdatedAt, IsDeleted) diff --git a/apps/mobile-app/app/(tabs)/credentials/add-edit.tsx b/apps/mobile-app/app/(tabs)/credentials/add-edit.tsx index 0b38cf656..a3fa871e6 100644 --- a/apps/mobile-app/app/(tabs)/credentials/add-edit.tsx +++ b/apps/mobile-app/app/(tabs)/credentials/add-edit.tsx @@ -9,7 +9,7 @@ import { StyleSheet, View, TouchableOpacity, Alert, Keyboard, KeyboardAvoidingVi import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view'; import Toast from 'react-native-toast-message'; -import { CreateIdentityGenerator, IdentityHelperUtils, IdentityGenerator } from '@/utils/dist/shared/identity-generator'; +import { CreateIdentityGenerator, IdentityGenerator, IdentityHelperUtils } from '@/utils/dist/shared/identity-generator'; import type { Attachment, Credential } from '@/utils/dist/shared/models/vault'; import type { FaviconExtractModel } from '@/utils/dist/shared/models/webapi'; import { CreatePasswordGenerator, PasswordGenerator } from '@/utils/dist/shared/password-generator'; @@ -145,7 +145,7 @@ export default function AddEditCredentialScreen() : React.ReactNode { /** * Initialize the identity and password generators with settings from user's vault. - * @returns {identityGenerator: IdentityGenerator, passwordGenerator: PasswordGenerator} + * @returns {identityGenerator: IIdentityGenerator, passwordGenerator: PasswordGenerator} */ const initializeGenerators = useCallback(async () : Promise<{ identityGenerator: IdentityGenerator, passwordGenerator: PasswordGenerator }> => { // Get default identity language from database @@ -724,7 +724,6 @@ export default function AddEditCredentialScreen() : React.ReactNode { diff --git a/apps/mobile-app/components/credentials/details/AttachmentSection.tsx b/apps/mobile-app/components/credentials/details/AttachmentSection.tsx index d9253132e..519125064 100644 --- a/apps/mobile-app/components/credentials/details/AttachmentSection.tsx +++ b/apps/mobile-app/components/credentials/details/AttachmentSection.tsx @@ -1,9 +1,10 @@ +import { Buffer } from 'buffer'; + import { Ionicons } from '@expo/vector-icons'; import * as FileSystem from 'expo-file-system'; -import * as Sharing from 'expo-sharing'; import React, { useState, useEffect, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; -import { View, StyleSheet, TouchableOpacity, Alert } from 'react-native'; +import { View, StyleSheet, TouchableOpacity, Alert, Share } from 'react-native'; import type { Credential, Attachment } from '@/utils/dist/shared/models/vault'; import emitter from '@/utils/EventEmitter'; @@ -32,42 +33,38 @@ export const AttachmentSection: React.FC = ({ credential */ const downloadAttachment = async (attachment: Attachment): Promise => { try { - // Convert Uint8Array or number[] to Uint8Array - const byteArray = attachment.Blob instanceof Uint8Array - ? attachment.Blob - : new Uint8Array(attachment.Blob); + const tempFile = `${FileSystem.cacheDirectory}${attachment.Filename}`; - const fileUri = `${FileSystem.documentDirectory}${attachment.Filename}`; + // Step 1: Create a Blob + if (typeof attachment.Blob === 'string') { + // If attachment.Blob is already a base64 string + const base64Data = attachment.Blob; - // Convert byte array to base64 - let binary = ''; - const bytes = new Uint8Array(byteArray); - const len = bytes.byteLength; - for (let i = 0; i < len; i++) { - binary += String.fromCharCode(bytes[i]); + await FileSystem.writeAsStringAsync(tempFile, base64Data, { + encoding: FileSystem.EncodingType.Base64, + }); + } + else { + // Convert attachment.Blob to base64 + const base64Data = Buffer.from(attachment.Blob as unknown as string, 'base64'); + await FileSystem.writeAsStringAsync(tempFile, base64Data.toString(), { + encoding: FileSystem.EncodingType.Base64, + }); } - const base64 = btoa(binary); - // Write file to local storage - await FileSystem.writeAsStringAsync(fileUri, base64, { - encoding: FileSystem.EncodingType.Base64, + // Step 2: Share using Sharing API (better handles mime types) + await Share.share({ + url: tempFile, + title: attachment.Filename, }); - // Share the file - if (await Sharing.isAvailableAsync()) { - await Sharing.shareAsync(fileUri, { - mimeType: 'application/octet-stream', - dialogTitle: attachment.Filename, - }); - } else { - Alert.alert('Download Complete', `File saved to: ${fileUri}`); - } + // Optional cleanup + await FileSystem.deleteAsync(tempFile); } catch (error) { console.error('Error downloading attachment:', error); Alert.alert('Error', 'Failed to download attachment'); } }; - /** * Load the attachments. */ @@ -87,7 +84,6 @@ export const AttachmentSection: React.FC = ({ credential useEffect((): (() => void) => { loadAttachments(); - // Add listener for credential changes to reload attachments const credentialChangedSub = emitter.addListener('credentialChanged', async (changedId: string) => { if (changedId === credential.Id) { await loadAttachments(); @@ -163,4 +159,4 @@ export const AttachmentSection: React.FC = ({ credential ))} ); -}; \ No newline at end of file +}; diff --git a/apps/mobile-app/components/credentials/details/AttachmentUploader.tsx b/apps/mobile-app/components/credentials/details/AttachmentUploader.tsx index 0eee95fa8..1669c0d0b 100644 --- a/apps/mobile-app/components/credentials/details/AttachmentUploader.tsx +++ b/apps/mobile-app/components/credentials/details/AttachmentUploader.tsx @@ -1,5 +1,6 @@ import { Ionicons } from '@expo/vector-icons'; import * as DocumentPicker from 'expo-document-picker'; +import * as FileSystem from 'expo-file-system'; import React, { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { View, StyleSheet, TouchableOpacity, Alert } from 'react-native'; @@ -34,7 +35,7 @@ export const AttachmentUploader: React.FC = ({ try { const result = await DocumentPicker.getDocumentAsync({ multiple: true, - copyToCacheDirectory: false, + copyToCacheDirectory: true, }); if (result.canceled) { @@ -45,22 +46,59 @@ export const AttachmentUploader: React.FC = ({ for (const file of result.assets) { if (file.uri) { - // Read file as bytes - const response = await fetch(file.uri); - const arrayBuffer = await response.arrayBuffer(); - const byteArray = new Uint8Array(arrayBuffer); + try { + let fileUri = file.uri; - const attachment: Attachment = { - Id: crypto.randomUUID(), - Filename: file.name, - Blob: byteArray, - CredentialId: '', // Will be set when saving credential - CreatedAt: new Date().toISOString(), - UpdatedAt: new Date().toISOString(), - IsDeleted: false, - }; + /* + * If the URI is a content:// URI and copyToCacheDirectory didn't work, + * try to copy it to a readable location + */ + if (fileUri.startsWith('content://')) { + const tempUri = `${FileSystem.cacheDirectory}${file.name}`; + await FileSystem.copyAsync({ + from: fileUri, + to: tempUri, + }); + fileUri = tempUri; + } - newAttachments.push(attachment); + // Read file as base64 string using FileSystem + const base64Data = await FileSystem.readAsStringAsync(fileUri, { + encoding: FileSystem.EncodingType.Base64, + }); + + // Convert base64 to Uint8Array + const binaryString = atob(base64Data); + const byteArray = new Uint8Array(binaryString.length); + for (let i = 0; i < binaryString.length; i++) { + byteArray[i] = binaryString.charCodeAt(i); + } + + const attachment: Attachment = { + Id: crypto.randomUUID(), + Filename: file.name, + Blob: byteArray, + CredentialId: '', // Will be set when saving credential + CreatedAt: new Date().toISOString(), + UpdatedAt: new Date().toISOString(), + IsDeleted: false, + }; + + newAttachments.push(attachment); + + // Clean up temporary file if we created one + if (fileUri !== file.uri) { + try { + await FileSystem.deleteAsync(fileUri); + } catch (cleanupError) { + console.warn('Failed to cleanup temporary file:', cleanupError); + } + } + } catch (fileError) { + console.error('Error reading file:', fileError); + setStatusMessage(`Error reading file ${file.name}.`); + setTimeout(() => setStatusMessage(''), 3000); + } } } diff --git a/apps/mobile-app/ios/Podfile.lock b/apps/mobile-app/ios/Podfile.lock index 893805a7b..bb12db38f 100644 --- a/apps/mobile-app/ios/Podfile.lock +++ b/apps/mobile-app/ios/Podfile.lock @@ -222,7 +222,7 @@ PODS: - ExpoModulesCore - ExpoClipboard (7.0.1): - ExpoModulesCore - - ExpoDocumentPicker (13.1.6): + - ExpoDocumentPicker (13.0.3): - ExpoModulesCore - ExpoFileSystem (18.0.12): - ExpoModulesCore @@ -2533,7 +2533,7 @@ SPEC CHECKSUMS: ExpoAsset: 48386d40d53a8c1738929b3ed509bcad595b5516 ExpoBlur: 392c1207f71d0ecf22371621c1fbd44ba84d9742 ExpoClipboard: 44fd1c8959ee8f6175d059dc011b154c9709a969 - ExpoDocumentPicker: b263a279685b6640b8c8bc70d71c83067aeaae55 + ExpoDocumentPicker: 6d3d499cf15b692688a804f42927d0f35de5ebaa ExpoFileSystem: 42d363d3b96f9afab980dcef60d5657a4443c655 ExpoFont: f354e926f8feae5e831ec8087f36652b44a0b188 ExpoHaptics: 8d199b2f33245ea85289ff6c954c7ee7c00a5b5d diff --git a/apps/mobile-app/package-lock.json b/apps/mobile-app/package-lock.json index 113834976..38eb51af4 100644 --- a/apps/mobile-app/package-lock.json +++ b/apps/mobile-app/package-lock.json @@ -19,7 +19,7 @@ "expo-clipboard": "~7.0.1", "expo-constants": "~17.0.8", "expo-dev-client": "~5.0.19", - "expo-document-picker": "^13.1.6", + "expo-document-picker": "~13.0.3", "expo-file-system": "~18.0.12", "expo-font": "~13.0.4", "expo-haptics": "~14.0.1", @@ -11150,9 +11150,9 @@ } }, "node_modules/expo-document-picker": { - "version": "13.1.6", - "resolved": "https://registry.npmjs.org/expo-document-picker/-/expo-document-picker-13.1.6.tgz", - "integrity": "sha512-8FTQPDOkyCvFN/i4xyqzH7ELW4AsB6B3XBZQjn1FEdqpozo6rpNJRr7sWFU/93WrLgA9FJEKpKbyr6XxczK6BA==", + "version": "13.0.3", + "resolved": "https://registry.npmjs.org/expo-document-picker/-/expo-document-picker-13.0.3.tgz", + "integrity": "sha512-348xcsiA/YhgWm1SuJNNdb5cUDpRJYCyIk8MhOU2MEDxbVRR+Q1TiUBTCIMVqaWHcxsFQzP56Wwv9n24qjeILg==", "license": "MIT", "peerDependencies": { "expo": "*" diff --git a/apps/mobile-app/package.json b/apps/mobile-app/package.json index 015fb6291..40705baa2 100644 --- a/apps/mobile-app/package.json +++ b/apps/mobile-app/package.json @@ -40,7 +40,7 @@ "expo-clipboard": "~7.0.1", "expo-constants": "~17.0.8", "expo-dev-client": "~5.0.19", - "expo-document-picker": "^13.1.6", + "expo-document-picker": "~13.0.3", "expo-file-system": "~18.0.12", "expo-font": "~13.0.4", "expo-haptics": "~14.0.1", diff --git a/apps/mobile-app/utils/SqliteClient.tsx b/apps/mobile-app/utils/SqliteClient.tsx index 08e954be7..a26c383d6 100644 --- a/apps/mobile-app/utils/SqliteClient.tsx +++ b/apps/mobile-app/utils/SqliteClient.tsx @@ -978,21 +978,7 @@ class SqliteClient { for (const attachment of attachments) { const isExistingAttachment = originalAttachmentIds.includes(attachment.Id); - if (isExistingAttachment) { - // Update existing attachment - const updateQuery = ` - UPDATE Attachments - SET Filename = ?, - Blob = ?, - UpdatedAt = ? - WHERE Id = ?`; - await this.executeUpdate(updateQuery, [ - attachment.Filename, - attachment.Blob as Uint8Array, - currentDateTime, - attachment.Id - ]); - } else { + if (!isExistingAttachment) { // Insert new attachment const insertQuery = ` INSERT INTO Attachments (Id, Filename, Blob, CredentialId, CreatedAt, UpdatedAt, IsDeleted)