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)