Compare commits

..

15 Commits

Author SHA1 Message Date
0e6a9c4f89 adjust grammar for recording receipt 2025-06-25 20:51:57 -06:00
Jose Olarte III
ca22161f12 Fix: entity-type identifier validation
- Ensure claims contain only correct and necessary giver and recipient identifiers, as per Endorser.ch documentation
2025-06-20 20:37:14 +08:00
Jose Olarte III
d3b80fbe47 Feature: giver-recipient validation
- Ensures person-to-person gifting won't allow the same entity as giver and recipient
- Disable user item selection if it would create conflict
- Error messaging fallback
2025-06-20 18:38:35 +08:00
Jose Olarte III
0342c872f4 Fix: added context for ContactGiftingView 2025-06-20 15:50:57 +08:00
Jose Olarte III
a7e65b3b49 Giver-recipient controls
- Dialog now shows separate cards for giver and recipient
- Ability to change giver and/or recipient
- Project giver/recipient is locked in ProjectView (context reinforcement)
2025-06-19 21:16:56 +08:00
Jose Olarte III
eb7605991c Fixed more gifting use cases 2025-06-18 19:58:10 +08:00
fa21660fd1 fix spelling 2025-06-15 12:43:22 -06:00
Jose Olarte III
df1c1f0186 Fix: pass project info
In GiftingDialog, project information is passed along if:
- Selecting "Show All" to go to ContactGiftingView
- Selecting "Photos and Other Options" to go to GiftedDetailsView
2025-06-13 20:52:26 +08:00
Jose Olarte III
3daf1c8a5c Feature: Project Gifting
- Gifting dialog: added ability to pick a project to benefit from
- Project view: modified dialog calls in Project view to toggle between giving to and benefiting from a project
- Project view: removed redundant person selection
- Project view: benefiting from a project locks the project selection in dialog to enforce context.
2025-06-12 20:50:27 +08:00
Jose Olarte III
7eefee1ea5 Fix: Conditional show-all link
- Only show "Show All" when user has contacts
2025-06-12 14:34:00 +08:00
Jose Olarte III
140c36a416 Merge branch 'master' into gifting-ui-2025-05 2025-06-11 19:10:59 +08:00
Jose Olarte III
988244b7ae Added check for "Unnamed" giver
Pass string "Unnamed" to select unnamed giver and skip contact selection step of dialog.
2025-05-16 20:22:09 +08:00
Jose Olarte III
4b355a5448 WIP: two-step dialog + functionality
- Dialog is now presented as two distinct steps
- Gifting functionality reinstated
- Minor UI tweaks
- IN PROGRESS: ensuring calls to dialog from other parts of the app remain functional
2025-05-14 21:47:12 +08:00
Jose Olarte III
b511f9cd24 WIP: adjustments to bring closer to original mockups 2025-05-13 21:16:39 +08:00
Jose Olarte III
579cecbe6e WIP: gifting UI revamp
Started to transform the gifting dialog into the two-step setup as per previous mockups
2025-05-12 21:22:05 +08:00
21 changed files with 1552 additions and 6189 deletions

1
.gitignore vendored
View File

@@ -21,7 +21,6 @@ npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
android/app/src/main/res/
# Editor directories and files
.idea

View File

@@ -1,533 +0,0 @@
# TimeSafari Contact Backup System
## Overview
The TimeSafari application implements a comprehensive contact backup and listing system that works across multiple platforms (Web, iOS, Android, Desktop). This document breaks down how contacts are saved, exported, and listed as backups.
## Architecture Components
### 1. Database Layer
#### Contact Data Structure
```typescript
interface Contact {
did: string; // Decentralized Identifier (primary key)
contactMethods?: ContactMethod[]; // Array of contact methods (EMAIL, SMS, etc.)
name?: string; // Display name
nextPubKeyHashB64?: string; // Base64 hash of next public key
notes?: string; // User notes
profileImageUrl?: string; // Profile image URL
publicKeyBase64?: string; // Base64 encoded public key
seesMe?: boolean; // Visibility setting
registered?: boolean; // Registration status
}
interface ContactMethod {
label: string; // Display label
type: string; // Type (EMAIL, SMS, WHATSAPP, etc.)
value: string; // Contact value
}
```
#### Database Schema
```sql
CREATE TABLE contacts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
did TEXT NOT NULL, -- Decentralized Identifier
name TEXT, -- Display name
contactMethods TEXT, -- JSON string of contact methods
nextPubKeyHashB64 TEXT, -- Next public key hash
notes TEXT, -- User notes
profileImageUrl TEXT, -- Profile image URL
publicKeyBase64 TEXT, -- Public key
seesMe BOOLEAN, -- Visibility flag
registered BOOLEAN -- Registration status
);
CREATE INDEX idx_contacts_did ON contacts(did);
CREATE INDEX idx_contacts_name ON contacts(name);
```
### 2. Contact Saving Operations
#### A. Adding New Contacts
**1. QR Code Scanning (`ContactQRScanFullView.vue`)**
```typescript
async addNewContact(contact: Contact) {
// Check for existing contact
const existingContacts = await platformService.dbQuery(
"SELECT * FROM contacts WHERE did = ?", [contact.did]
);
if (existingContact) {
// Handle duplicate
return;
}
// Convert contactMethods to JSON string for storage
contact.contactMethods = JSON.stringify(
parseJsonField(contact.contactMethods, [])
);
// Insert into database
const { sql, params } = databaseUtil.generateInsertStatement(
contact as unknown as Record<string, unknown>, "contacts"
);
await platformService.dbExec(sql, params);
}
```
**2. Manual Contact Addition (`ContactsView.vue`)**
```typescript
private async addContact(newContact: Contact) {
// Validate DID format
if (!isDid(newContact.did)) {
throw new Error("Invalid DID format");
}
// Generate and execute INSERT statement
const { sql, params } = databaseUtil.generateInsertStatement(
newContact as unknown as Record<string, unknown>, "contacts"
);
await platformService.dbExec(sql, params);
}
```
**3. Contact Import (`ContactImportView.vue`)**
```typescript
async importContacts() {
for (const contact of selectedContacts) {
const contactToStore = contactToDbRecord(contact);
if (existingContact) {
// Update existing contact
const { sql, params } = databaseUtil.generateUpdateStatement(
contactToStore, "contacts", "did = ?", [contact.did]
);
await platformService.dbExec(sql, params);
} else {
// Add new contact
const { sql, params } = databaseUtil.generateInsertStatement(
contactToStore, "contacts"
);
await platformService.dbExec(sql, params);
}
}
}
```
#### B. Updating Existing Contacts
**Contact Editing (`ContactEditView.vue`)**
```typescript
async saveEdit() {
// Normalize contact methods
const contactMethods = this.contactMethods.map(method => ({
...method,
type: method.type.toUpperCase()
}));
// Update database
const contactMethodsString = JSON.stringify(contactMethods);
await platformService.dbExec(
"UPDATE contacts SET name = ?, notes = ?, contactMethods = ? WHERE did = ?",
[this.contactName, this.contactNotes, contactMethodsString, this.contact?.did]
);
}
```
### 3. Contact Export/Backup System
#### A. Export Process (`DataExportSection.vue`)
#### 1. Data Retrieval
```typescript
async exportDatabase() {
// Query all contacts from database
const result = await platformService.dbQuery("SELECT * FROM contacts");
const allContacts = databaseUtil.mapQueryResultToValues(result) as Contact[];
// Convert to export format
const exportData = contactsToExportJson(allContacts);
const jsonStr = JSON.stringify(exportData, null, 2);
}
```
#### 2. Export Format Conversion (`libs/util.ts`)
```typescript
export const contactsToExportJson = (contacts: Contact[]): DatabaseExport => {
const rows = contacts.map((contact) => ({
did: contact.did,
name: contact.name || null,
contactMethods: contact.contactMethods
? JSON.stringify(parseJsonField(contact.contactMethods, []))
: null,
nextPubKeyHashB64: contact.nextPubKeyHashB64 || null,
notes: contact.notes || null,
profileImageUrl: contact.profileImageUrl || null,
publicKeyBase64: contact.publicKeyBase64 || null,
seesMe: contact.seesMe || false,
registered: contact.registered || false,
}));
return {
data: {
data: [{ tableName: "contacts", rows }]
}
};
};
```
#### 3. File Generation
```typescript
// Create timestamped filename
const timestamp = getTimestampForFilename();
const fileName = `${AppString.APP_NAME_NO_SPACES}-backup-contacts-${timestamp}.json`;
// Create blob and save
const blob = new Blob([jsonStr], { type: "application/json" });
```
#### B. Platform-Specific File Saving
##### 1. Web Platform (`WebPlatformService.ts`)**
```typescript
// Uses browser download API
const downloadUrl = URL.createObjectURL(blob);
const downloadAnchor = this.$refs.downloadLink as HTMLAnchorElement;
downloadAnchor.href = downloadUrl;
downloadAnchor.download = fileName;
downloadAnchor.click();
```
##### 2. Mobile Platforms (`CapacitorPlatformService.ts`)
```typescript
async writeAndShareFile(fileName: string, content: string, options = {}) {
let fileUri: string;
if (options.allowLocationSelection) {
// User chooses location
fileUri = await this.saveWithUserChoice(fileName, content, options.mimeType);
} else if (options.saveToPrivateStorage) {
// Save to app-private storage
const result = await Filesystem.writeFile({
path: fileName,
data: content,
directory: Directory.Data,
encoding: Encoding.UTF8,
recursive: true,
});
fileUri = result.uri;
} else {
// Save to user-accessible location (Downloads/Documents)
fileUri = await this.saveToDownloads(fileName, content);
}
// Share the file
return await this.shareFile(fileUri, fileName);
}
```
##### 3. Desktop Platforms (`ElectronPlatformService.ts`, `PyWebViewPlatformService.ts`)
```typescript
// Not implemented - returns empty results
async listBackupFiles(): Promise<Array<{name: string, uri: string, size?: number, type: 'contacts' | 'seed' | 'other', path?: string}>> {
return [];
}
```
### 4. Backup File Listing System
#### A. File Discovery (`CapacitorPlatformService.ts`)
##### 1. Enhanced File Discovery
```typescript
async listUserAccessibleFilesEnhanced(): Promise<Array<{name: string, uri: string, size?: number, path?: string}>> {
const allFiles: Array<{name: string, uri: string, size?: number, path?: string}> = [];
if (this.getCapabilities().isIOS) {
// iOS: Documents directory
const result = await Filesystem.readdir({
path: ".",
directory: Directory.Documents,
});
const files = result.files.map((file) => ({
name: typeof file === "string" ? file : file.name,
uri: `file://${file.uri || file}`,
size: typeof file === "string" ? undefined : file.size,
path: "Documents"
}));
allFiles.push(...files);
} else {
// Android: Multiple locations
const commonPaths = ["Download", "Documents", "Backups", "TimeSafari", "Data"];
for (const path of commonPaths) {
try {
const result = await Filesystem.readdir({
path: path,
directory: Directory.ExternalStorage,
});
// Filter for TimeSafari-related files
const relevantFiles = result.files
.filter(file => {
const fileName = typeof file === "string" ? file : file.name;
const name = fileName.toLowerCase();
return name.includes('timesafari') ||
name.includes('backup') ||
name.includes('contacts') ||
name.endsWith('.json');
})
.map((file) => ({
name: typeof file === "string" ? file : file.name,
uri: `file://${file.uri || file}`,
size: typeof file === "string" ? undefined : file.size,
path: path
}));
allFiles.push(...relevantFiles);
} catch (error) {
// Silently skip inaccessible directories
}
}
}
return allFiles;
}
```
**2. Backup File Filtering**
```typescript
async listBackupFiles(): Promise<Array<{name: string, uri: string, size?: number, type: 'contacts' | 'seed' | 'other', path?: string}>> {
const allFiles = await this.listUserAccessibleFilesEnhanced();
const backupFiles = allFiles
.filter(file => {
const name = file.name.toLowerCase();
// Exclude directory-access notification files
if (name.startsWith('timesafari-directory-access-') && name.endsWith('.txt')) {
return false;
}
// Check backup criteria
const isJson = name.endsWith('.json');
const hasTimeSafari = name.includes('timesafari');
const hasBackup = name.includes('backup');
const hasContacts = name.includes('contacts');
const hasSeed = name.includes('seed');
const hasExport = name.includes('export');
const hasData = name.includes('data');
return isJson || hasTimeSafari || hasBackup || hasContacts || hasSeed || hasExport || hasData;
})
.map(file => {
const name = file.name.toLowerCase();
let type: 'contacts' | 'seed' | 'other' = 'other';
// Categorize files
if (name.includes('contacts') || (name.includes('timesafari') && name.includes('backup'))) {
type = 'contacts';
} else if (name.includes('seed') || name.includes('mnemonic') || name.includes('private')) {
type = 'seed';
} else if (name.endsWith('.json')) {
type = 'other';
}
return { ...file, type };
});
return backupFiles;
}
```
#### B. UI Components (`BackupFilesList.vue`)
**1. File Display**
```typescript
@Component
export default class BackupFilesList extends Vue {
backupFiles: Array<{name: string, uri: string, size?: number, type: 'contacts' | 'seed' | 'other', path?: string}> = [];
selectedType: 'all' | 'contacts' | 'seed' | 'other' = 'all';
isLoading = false;
async refreshFiles() {
this.isLoading = true;
try {
this.backupFiles = await this.platformService.listBackupFiles();
// Log file type distribution
const typeCounts = {
contacts: this.backupFiles.filter(f => f.type === 'contacts').length,
seed: this.backupFiles.filter(f => f.type === 'seed').length,
other: this.backupFiles.filter(f => f.type === 'other').length,
total: this.backupFiles.length
};
} catch (error) {
// Handle error
} finally {
this.isLoading = false;
}
}
}
```
**2. File Operations**
```typescript
async openFile(fileUri: string, fileName: string) {
const result = await this.platformService.openFile(fileUri, fileName);
if (!result.success) {
throw new Error(result.error || "Failed to open file");
}
}
async openBackupDirectory() {
const result = await this.platformService.openBackupDirectory();
if (!result.success) {
throw new Error(result.error || "Failed to open backup directory");
}
}
```
### 5. Platform-Specific Storage Locations
#### A. iOS Platform
- **Primary Location**: Documents folder (accessible via Files app)
- **Persistence**: Survives app installations
- **Access**: Through iOS Files app
- **File Format**: JSON with timestamped filenames
#### B. Android Platform
- **Primary Locations**:
- `Download/TimeSafari/` (external storage)
- `TimeSafari/` (external storage)
- User-chosen locations via file picker
- **Persistence**: Survives app installations
- **Access**: Through file managers
- **File Format**: JSON with timestamped filenames
#### C. Web Platform
- **Primary Location**: Browser downloads folder
- **Persistence**: Depends on browser settings
- **Access**: Through browser download manager
- **File Format**: JSON with timestamped filenames
#### D. Desktop Platforms (Electron/PyWebView)
- **Status**: Not implemented
- **Fallback**: Returns empty arrays for file operations
### 6. File Naming Convention
#### A. Contact Backup Files
```
TimeSafari-backup-contacts-YYYY-MM-DD-HH-MM-SS.json
```
#### B. File Content Structure
```json
{
"data": {
"data": [
{
"tableName": "contacts",
"rows": [
{
"did": "did:ethr:0x...",
"name": "Contact Name",
"contactMethods": "[{\"type\":\"EMAIL\",\"value\":\"email@example.com\"}]",
"notes": "User notes",
"profileImageUrl": "https://...",
"publicKeyBase64": "base64...",
"seesMe": true,
"registered": false
}
]
}
]
}
}
```
### 7. Error Handling and Logging
#### A. Comprehensive Logging
```typescript
logger.log("[CapacitorPlatformService] File write successful:", {
uri: fileUri,
saved,
timestamp: new Date().toISOString(),
});
logger.log("[BackupFilesList] Refreshed backup files:", {
count: this.backupFiles.length,
files: this.backupFiles.map(f => ({
name: f.name,
type: f.type,
path: f.path,
size: f.size
})),
platform: this.platformCapabilities.isIOS ? "iOS" : "Android",
timestamp: new Date().toISOString(),
});
```
#### B. Error Recovery
```typescript
try {
// File operations
} catch (error) {
logger.error("[CapacitorPlatformService] Failed to list backup files:", error);
return [];
}
```
### 8. Security Considerations
#### A. Data Privacy
- Contact data is stored locally on device
- No cloud synchronization of contact data
- User controls visibility settings per contact
- Backup files contain only user-authorized data
#### B. File Access
- Platform-specific permission handling
- User choice for file locations
- Secure storage options for sensitive data
- Proper error handling for access failures
### 9. Performance Optimizations
#### A. Database Operations
- Indexed queries on `did` and `name` fields
- Batch operations for multiple contacts
- Efficient JSON serialization/deserialization
- Connection pooling and reuse
#### B. File Operations
- Asynchronous file I/O
- Efficient file discovery algorithms
- Caching of file lists
- Background refresh operations
## Summary
The TimeSafari contact backup system provides:
1. **Robust Data Storage**: SQLite-based contact storage with proper indexing
2. **Cross-Platform Compatibility**: Works on web, iOS, Android, and desktop
3. **Flexible Export Options**: Multiple file formats and storage locations
4. **Intelligent File Discovery**: Finds backup files regardless of user-chosen locations
5. **User-Friendly Interface**: Clear categorization and easy file management
6. **Comprehensive Logging**: Detailed tracking for debugging and monitoring
7. **Security-First Design**: Privacy-preserving with user-controlled data access
The system ensures that users can reliably backup and restore their contact data across different platforms while maintaining data integrity and user privacy.

565
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "timesafari",
"version": "0.5.1",
"version": "0.4.8",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "timesafari",
"version": "0.5.1",
"version": "0.4.8",
"dependencies": {
"@capacitor-community/sqlite": "6.0.2",
"@capacitor-mlkit/barcode-scanning": "^6.0.0",
@@ -3835,9 +3835,9 @@
}
},
"node_modules/@electron/asar/node_modules/brace-expansion": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4524,9 +4524,9 @@
}
},
"node_modules/@eslint/eslintrc/node_modules/brace-expansion": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -6686,9 +6686,9 @@
}
},
"node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -6983,29 +6983,6 @@
"integrity": "sha512-meL9DERHj+fFVWoOX9fXqfcYcSpUfSYJPcFvDPKrxitICbwAoWR+Ut4j5NO9zAT917HUHLQmqzQbAsGNHlDcxQ==",
"license": "Apache-2.0 OR MIT"
},
"node_modules/@isaacs/balanced-match": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
"integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": "20 || >=22"
}
},
"node_modules/@isaacs/brace-expansion": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz",
"integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@isaacs/balanced-match": "^4.0.1"
},
"engines": {
"node": "20 || >=22"
}
},
"node_modules/@isaacs/cliui": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
@@ -7862,9 +7839,9 @@
}
},
"node_modules/@react-native/assets-registry": {
"version": "0.80.0",
"resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.80.0.tgz",
"integrity": "sha512-MlScsKAz99zoYghe5Rf5mUqsqz2rMB02640NxtPtBMSHNdGxxRlWu/pp1bFexDa1DYJwyIjnLgt3Z/Y90ikHfw==",
"version": "0.79.3",
"resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.79.3.tgz",
"integrity": "sha512-Vy8DQXCJ21YSAiHxrNBz35VqVlZPpRYm50xRTWRf660JwHuJkFQG8cUkrLzm7AUriqUXxwpkQHcY+b0ibw9ejQ==",
"license": "MIT",
"optional": true,
"peer": true,
@@ -7970,20 +7947,20 @@
}
},
"node_modules/@react-native/community-cli-plugin": {
"version": "0.80.0",
"resolved": "https://registry.npmjs.org/@react-native/community-cli-plugin/-/community-cli-plugin-0.80.0.tgz",
"integrity": "sha512-uadfVvzZfz5tGpqwslL12i+rELK9m6cLhtqICX0JQvS7Bu12PJwrozhKzEzIYwN9i3wl2dWrKDUr08izt7S9Iw==",
"version": "0.79.3",
"resolved": "https://registry.npmjs.org/@react-native/community-cli-plugin/-/community-cli-plugin-0.79.3.tgz",
"integrity": "sha512-N/+p4HQqN4yK6IRzn7OgMvUIcrmEWkecglk1q5nj+AzNpfIOzB+mqR20SYmnPfeXF+mZzYCzRANb3KiM+WsSDA==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@react-native/dev-middleware": "0.80.0",
"@react-native/dev-middleware": "0.79.3",
"chalk": "^4.0.0",
"debug": "^4.4.0",
"debug": "^2.2.0",
"invariant": "^2.2.4",
"metro": "^0.82.2",
"metro-config": "^0.82.2",
"metro-core": "^0.82.2",
"metro": "^0.82.0",
"metro-config": "^0.82.0",
"metro-core": "^0.82.0",
"semver": "^7.1.3"
},
"engines": {
@@ -7998,97 +7975,25 @@
}
}
},
"node_modules/@react-native/community-cli-plugin/node_modules/@react-native/debugger-frontend": {
"version": "0.80.0",
"resolved": "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.80.0.tgz",
"integrity": "sha512-lpu9Z3xtKUaKFvEcm5HSgo1KGfkDa/W3oZHn22Zy0WQ9MiOu2/ar1txgd1wjkoNiK/NethKcRdCN7mqnc6y2mA==",
"license": "BSD-3-Clause",
"optional": true,
"peer": true,
"engines": {
"node": ">=18"
}
},
"node_modules/@react-native/community-cli-plugin/node_modules/@react-native/dev-middleware": {
"version": "0.80.0",
"resolved": "https://registry.npmjs.org/@react-native/dev-middleware/-/dev-middleware-0.80.0.tgz",
"integrity": "sha512-lLyTnJ687A5jF3fn8yR/undlCis3FG+N/apQ+Q0Lcl+GV6FsZs0U5H28YmL6lZtjOj4TLek6uGPMPmZasHx7cQ==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@isaacs/ttlcache": "^1.4.1",
"@react-native/debugger-frontend": "0.80.0",
"chrome-launcher": "^0.15.2",
"chromium-edge-launcher": "^0.2.0",
"connect": "^3.6.5",
"debug": "^4.4.0",
"invariant": "^2.2.4",
"nullthrows": "^1.1.1",
"open": "^7.0.3",
"serve-static": "^1.16.2",
"ws": "^6.2.3"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@react-native/community-cli-plugin/node_modules/debug": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
"ms": "2.0.0"
}
},
"node_modules/@react-native/community-cli-plugin/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"license": "MIT",
"optional": true,
"peer": true
},
"node_modules/@react-native/community-cli-plugin/node_modules/open": {
"version": "7.4.2",
"resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz",
"integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"is-docker": "^2.0.0",
"is-wsl": "^2.1.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@react-native/community-cli-plugin/node_modules/ws": {
"version": "6.2.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-6.2.3.tgz",
"integrity": "sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"async-limiter": "~1.0.0"
}
},
"node_modules/@react-native/debugger-frontend": {
"version": "0.79.3",
"resolved": "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.79.3.tgz",
@@ -8173,9 +8078,9 @@
}
},
"node_modules/@react-native/gradle-plugin": {
"version": "0.80.0",
"resolved": "https://registry.npmjs.org/@react-native/gradle-plugin/-/gradle-plugin-0.80.0.tgz",
"integrity": "sha512-drmS68rabSMOuDD+YsAY2luNT8br82ycodSDORDqAg7yWQcieHMp4ZUOcdOi5iW+JCqobablT/b6qxcrBg+RaA==",
"version": "0.79.3",
"resolved": "https://registry.npmjs.org/@react-native/gradle-plugin/-/gradle-plugin-0.79.3.tgz",
"integrity": "sha512-imfpZLhNBc9UFSzb/MOy2tNcIBHqVmexh/qdzw83F75BmUtLb/Gs1L2V5gw+WI1r7RqDILbWk7gXB8zUllwd+g==",
"license": "MIT",
"optional": true,
"peer": true,
@@ -8184,9 +8089,9 @@
}
},
"node_modules/@react-native/js-polyfills": {
"version": "0.80.0",
"resolved": "https://registry.npmjs.org/@react-native/js-polyfills/-/js-polyfills-0.80.0.tgz",
"integrity": "sha512-dMX7IcBuwghySTgIeK8q03tYz/epg5ScGmJEfBQAciuhzMDMV1LBR/9wwdgD73EXM/133yC5A+TlHb3KQil4Ew==",
"version": "0.79.3",
"resolved": "https://registry.npmjs.org/@react-native/js-polyfills/-/js-polyfills-0.79.3.tgz",
"integrity": "sha512-PEBtg6Kox6KahjCAch0UrqCAmHiNLEbp2SblUEoFAQnov4DSxBN9safh+QSVaCiMAwLjvNfXrJyygZz60Dqz3Q==",
"license": "MIT",
"optional": true,
"peer": true,
@@ -8203,9 +8108,9 @@
"peer": true
},
"node_modules/@react-native/virtualized-lists": {
"version": "0.80.0",
"resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.80.0.tgz",
"integrity": "sha512-d9zZdPS/ZRexVAkxo1eRp85U7XnnEpXA1ZpSomRKxBuStYKky1YohfEX5YD5MhphemKK24tT7JR4UhaLlmeX8Q==",
"version": "0.79.3",
"resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.79.3.tgz",
"integrity": "sha512-/0rRozkn+iIHya2vnnvprDgT7QkfI54FLrACAN3BLP7MRlfOIGOrZsXpRLndnLBVnjNzkcre84i1RecjoXnwIA==",
"license": "MIT",
"optional": true,
"peer": true,
@@ -8312,9 +8217,9 @@
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.43.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.43.0.tgz",
"integrity": "sha512-Krjy9awJl6rKbruhQDgivNbD1WuLb8xAclM4IR4cN5pHGAs2oIMMQJEiC3IC/9TZJ+QZkmZhlMO/6MBGxPidpw==",
"version": "4.42.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.42.0.tgz",
"integrity": "sha512-gldmAyS9hpj+H6LpRNlcjQWbuKUtb94lodB9uCz71Jm+7BxK1VIOo7y62tZZwxhA7j1ylv/yQz080L5WkS+LoQ==",
"cpu": [
"arm"
],
@@ -8326,9 +8231,9 @@
]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.43.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.43.0.tgz",
"integrity": "sha512-ss4YJwRt5I63454Rpj+mXCXicakdFmKnUNxr1dLK+5rv5FJgAxnN7s31a5VchRYxCFWdmnDWKd0wbAdTr0J5EA==",
"version": "4.42.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.42.0.tgz",
"integrity": "sha512-bpRipfTgmGFdCZDFLRvIkSNO1/3RGS74aWkJJTFJBH7h3MRV4UijkaEUeOMbi9wxtxYmtAbVcnMtHTPBhLEkaw==",
"cpu": [
"arm64"
],
@@ -8366,9 +8271,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.43.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.43.0.tgz",
"integrity": "sha512-SV+U5sSo0yujrjzBF7/YidieK2iF6E7MdF6EbYxNz94lA+R0wKl3SiixGyG/9Klab6uNBIqsN7j4Y/Fya7wAjQ==",
"version": "4.42.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.42.0.tgz",
"integrity": "sha512-fJcN4uSGPWdpVmvLuMtALUFwCHgb2XiQjuECkHT3lWLZhSQ3MBQ9pq+WoWeJq2PrNxr9rPM1Qx+IjyGj8/c6zQ==",
"cpu": [
"arm64"
],
@@ -8380,9 +8285,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.43.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.43.0.tgz",
"integrity": "sha512-J7uCsiV13L/VOeHJBo5SjasKiGxJ0g+nQTrBkAsmQBIdil3KhPnSE9GnRon4ejX1XDdsmK/l30IYLiAaQEO0Cg==",
"version": "4.42.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.42.0.tgz",
"integrity": "sha512-CziHfyzpp8hJpCVE/ZdTizw58gr+m7Y2Xq5VOuCSrZR++th2xWAz4Nqk52MoIIrV3JHtVBhbBsJcAxs6NammOQ==",
"cpu": [
"x64"
],
@@ -8394,9 +8299,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.43.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.43.0.tgz",
"integrity": "sha512-gTJ/JnnjCMc15uwB10TTATBEhK9meBIY+gXP4s0sHD1zHOaIh4Dmy1X9wup18IiY9tTNk5gJc4yx9ctj/fjrIw==",
"version": "4.42.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.42.0.tgz",
"integrity": "sha512-UsQD5fyLWm2Fe5CDM7VPYAo+UC7+2Px4Y+N3AcPh/LdZu23YcuGPegQly++XEVaC8XUTFVPscl5y5Cl1twEI4A==",
"cpu": [
"arm"
],
@@ -8408,9 +8313,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.43.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.43.0.tgz",
"integrity": "sha512-ZJ3gZynL1LDSIvRfz0qXtTNs56n5DI2Mq+WACWZ7yGHFUEirHBRt7fyIk0NsCKhmRhn7WAcjgSkSVVxKlPNFFw==",
"version": "4.42.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.42.0.tgz",
"integrity": "sha512-/i8NIrlgc/+4n1lnoWl1zgH7Uo0XK5xK3EDqVTf38KvyYgCU/Rm04+o1VvvzJZnVS5/cWSd07owkzcVasgfIkQ==",
"cpu": [
"arm"
],
@@ -8448,9 +8353,9 @@
]
},
"node_modules/@rollup/rollup-linux-loongarch64-gnu": {
"version": "4.43.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.43.0.tgz",
"integrity": "sha512-HPGDIH0/ZzAZjvtlXj6g+KDQ9ZMHfSP553za7o2Odegb/BEfwJcR0Sw0RLNpQ9nC6Gy8s+3mSS9xjZ0n3rhcYg==",
"version": "4.42.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.42.0.tgz",
"integrity": "sha512-O8AplvIeavK5ABmZlKBq9/STdZlnQo7Sle0LLhVA7QT+CiGpNVe197/t8Aph9bhJqbDVGCHpY2i7QyfEDDStDg==",
"cpu": [
"loong64"
],
@@ -8462,9 +8367,9 @@
]
},
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
"version": "4.43.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.43.0.tgz",
"integrity": "sha512-gEmwbOws4U4GLAJDhhtSPWPXUzDfMRedT3hFMyRAvM9Mrnj+dJIFIeL7otsv2WF3D7GrV0GIewW0y28dOYWkmw==",
"version": "4.42.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.42.0.tgz",
"integrity": "sha512-6Qb66tbKVN7VyQrekhEzbHRxXXFFD8QKiFAwX5v9Xt6FiJ3BnCVBuyBxa2fkFGqxOCSGGYNejxd8ht+q5SnmtA==",
"cpu": [
"ppc64"
],
@@ -8476,9 +8381,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.43.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.43.0.tgz",
"integrity": "sha512-XXKvo2e+wFtXZF/9xoWohHg+MuRnvO29TI5Hqe9xwN5uN8NKUYy7tXUG3EZAlfchufNCTHNGjEx7uN78KsBo0g==",
"version": "4.42.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.42.0.tgz",
"integrity": "sha512-KQETDSEBamQFvg/d8jajtRwLNBlGc3aKpaGiP/LvEbnmVUKlFta1vqJqTrvPtsYsfbE/DLg5CC9zyXRX3fnBiA==",
"cpu": [
"riscv64"
],
@@ -8490,9 +8395,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-musl": {
"version": "4.43.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.43.0.tgz",
"integrity": "sha512-ruf3hPWhjw6uDFsOAzmbNIvlXFXlBQ4nk57Sec8E8rUxs/AI4HD6xmiiasOOx/3QxS2f5eQMKTAwk7KHwpzr/Q==",
"version": "4.42.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.42.0.tgz",
"integrity": "sha512-qMvnyjcU37sCo/tuC+JqeDKSuukGAd+pVlRl/oyDbkvPJ3awk6G6ua7tyum02O3lI+fio+eM5wsVd66X0jQtxw==",
"cpu": [
"riscv64"
],
@@ -8504,9 +8409,9 @@
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.43.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.43.0.tgz",
"integrity": "sha512-QmNIAqDiEMEvFV15rsSnjoSmO0+eJLoKRD9EAa9rrYNwO/XRCtOGM3A5A0X+wmG+XRrw9Fxdsw+LnyYiZWWcVw==",
"version": "4.42.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.42.0.tgz",
"integrity": "sha512-I2Y1ZUgTgU2RLddUHXTIgyrdOwljjkmcZ/VilvaEumtS3Fkuhbw4p4hgHc39Ypwvo2o7sBFNl2MquNvGCa55Iw==",
"cpu": [
"s390x"
],
@@ -8557,9 +8462,9 @@
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.43.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.43.0.tgz",
"integrity": "sha512-fYCTEyzf8d+7diCw8b+asvWDCLMjsCEA8alvtAutqJOJp/wL5hs1rWSqJ1vkjgW0L2NB4bsYJrpKkiIPRR9dvw==",
"version": "4.42.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.42.0.tgz",
"integrity": "sha512-F+5J9pelstXKwRSDq92J0TEBXn2nfUrQGg+HK1+Tk7VOL09e0gBqUHugZv7SW4MGrYj41oNCUe3IKCDGVlis2g==",
"cpu": [
"ia32"
],
@@ -8969,9 +8874,9 @@
}
},
"node_modules/@stencil/core": {
"version": "4.35.0",
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.35.0.tgz",
"integrity": "sha512-x0IFtj7IJStK+ZqIkhReWbiC0UMjMJnNXV8OXG+DCLDExZaVaxL3MLuq6BJBBcQ1MHZduTHDv3Iz0Zshoj3zjQ==",
"version": "4.33.1",
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.33.1.tgz",
"integrity": "sha512-12k9xhAJBkpg598it+NRmaYIdEe6TSnsL/v6/KRXDcUyTK11VYwZQej2eHnMWtqot+znJ+GNTqb5YbiXi+5Low==",
"license": "MIT",
"bin": {
"stencil": "bin/stencil"
@@ -11279,13 +11184,13 @@
}
},
"node_modules/app-builder-lib/node_modules/minimatch": {
"version": "10.0.3",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz",
"integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==",
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz",
"integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==",
"dev": true,
"license": "ISC",
"dependencies": {
"@isaacs/brace-expansion": "^5.0.0"
"brace-expansion": "^2.0.1"
},
"engines": {
"node": "20 || >=22"
@@ -12294,9 +12199,9 @@
}
},
"node_modules/brace-expansion": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
@@ -12743,9 +12648,9 @@
}
},
"node_modules/cacache/node_modules/rimraf/node_modules/brace-expansion": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -12963,9 +12868,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001723",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001723.tgz",
"integrity": "sha512-1R/elMjtehrFejxwmexeXAtae5UO9iSyFn6G/I806CYC/BLyyBk1EPhrKBkWhy6wM6Xnm47dSJQec+tLJ39WHw==",
"version": "1.0.30001721",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001721.tgz",
"integrity": "sha512-cOuvmUVtKrtEaoKiO0rSc29jcjwMwX5tOHDy4MgVFEWiUXj4uBMJkwI8MDySkgXidpMiHUcviogAvFi4pA2hDQ==",
"devOptional": true,
"funding": [
{
@@ -15026,9 +14931,9 @@
}
},
"node_modules/dir-compare/node_modules/brace-expansion": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -15442,9 +15347,9 @@
}
},
"node_modules/electron-to-chromium": {
"version": "1.5.167",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.167.tgz",
"integrity": "sha512-LxcRvnYO5ez2bMOFpbuuVuAI5QNeY1ncVytE/KXaL6ZNfzX1yPlAO0nSOyIHx2fVAuUprMqPs/TdVhUFZy7SIQ==",
"version": "1.5.166",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.166.tgz",
"integrity": "sha512-QPWqHL0BglzPYyJJ1zSSmwFFL6MFXhbACOCcsCdUMCkzPdS9/OIBVxg516X/Ado2qwAq8k0nJJ7phQPCqiaFAw==",
"devOptional": true,
"license": "ISC"
},
@@ -15960,9 +15865,9 @@
}
},
"node_modules/eslint/node_modules/brace-expansion": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -16174,9 +16079,9 @@
}
},
"node_modules/ethers": {
"version": "6.14.4",
"resolved": "https://registry.npmjs.org/ethers/-/ethers-6.14.4.tgz",
"integrity": "sha512-Jm/dzRs2Z9iBrT6e9TvGxyb5YVKAPLlpna7hjxH7KH/++DSh2T/JVmQUv7iHI5E55hDbp/gEVvstWYXVxXFzsA==",
"version": "6.14.3",
"resolved": "https://registry.npmjs.org/ethers/-/ethers-6.14.3.tgz",
"integrity": "sha512-qq7ft/oCJohoTcsNPFaXSQUm457MA5iWqkf1Mb11ujONdg7jBI6sAOrHaTi3j0CBqIGFSCeR/RMc+qwRRub7IA==",
"funding": [
{
"type": "individual",
@@ -16273,21 +16178,21 @@
"license": "MIT"
},
"node_modules/ethr-did": {
"version": "3.0.38",
"resolved": "https://registry.npmjs.org/ethr-did/-/ethr-did-3.0.38.tgz",
"integrity": "sha512-gUxtErXVOQUJf+bmnxRdSJdlU9aFbQSBNaJCYGt+PLqw6l4qqInTfMRiWpwe/brhRtdjE+64tnayOVk8ataeQA==",
"version": "3.0.37",
"resolved": "https://registry.npmjs.org/ethr-did/-/ethr-did-3.0.37.tgz",
"integrity": "sha512-L9UUhAS8B1T7jTRdKLwAt514lx2UrJebJK7uc6UU4AJ9RhY8Vcfwc93Ux82jREE7yvvqDPXsVNH+lS3aw18a9A==",
"license": "Apache-2.0",
"dependencies": {
"did-jwt": "^8.0.0",
"did-resolver": "^4.1.0",
"ethers": "^6.8.1",
"ethr-did-resolver": "11.0.4"
"ethr-did-resolver": "11.0.3"
}
},
"node_modules/ethr-did-resolver": {
"version": "11.0.4",
"resolved": "https://registry.npmjs.org/ethr-did-resolver/-/ethr-did-resolver-11.0.4.tgz",
"integrity": "sha512-EJ/dL2QsFzvhBJd0nlPFjma3bxpQOWyp2TytQZyAeqi6SfZ4ALCB0VaA4dSeT4T8ZtI2pzs/sD7t/7A0584J6Q==",
"version": "11.0.3",
"resolved": "https://registry.npmjs.org/ethr-did-resolver/-/ethr-did-resolver-11.0.3.tgz",
"integrity": "sha512-lQ1T/SZfgR6Kp05/GSIXnMELxQ5H6M6OCTH4wBTVSAgHzbJiDNVIYWzg/c+NniIM88B0ViAi4CaiCHaiUlvPQg==",
"license": "Apache-2.0",
"dependencies": {
"did-resolver": "^4.1.0",
@@ -17712,9 +17617,9 @@
}
},
"node_modules/glob/node_modules/brace-expansion": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"devOptional": true,
"license": "MIT",
"dependencies": {
@@ -19222,9 +19127,9 @@
}
},
"node_modules/jake/node_modules/brace-expansion": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -24149,9 +24054,9 @@
}
},
"node_modules/postcss": {
"version": "8.5.5",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.5.tgz",
"integrity": "sha512-d/jtm+rdNT8tpXuHY5MMtcbJFBkhXE6593XVR9UoGCH8jSFGci7jGvMGH5RYd5PBJW+00NZQt6gf7CbagJCrhg==",
"version": "8.5.4",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.4.tgz",
"integrity": "sha512-QSa9EBe+uwlGTFmHsPKokv3B/oEMQZxfqW0QqNCyhpa6mB1afzulwn8hihglqAb2pOw+BJgNlmXQ8la2VeHB7w==",
"funding": [
{
"type": "opencollective",
@@ -25049,43 +24954,44 @@
"peer": true
},
"node_modules/react-native": {
"version": "0.80.0",
"resolved": "https://registry.npmjs.org/react-native/-/react-native-0.80.0.tgz",
"integrity": "sha512-b9K1ygb2MWCBtKAodKmE3UsbUuC29Pt4CrJMR0ocTA8k+8HJQTPleBPDNKL4/p0P01QO9aL/gZUddoxHempLow==",
"version": "0.79.3",
"resolved": "https://registry.npmjs.org/react-native/-/react-native-0.79.3.tgz",
"integrity": "sha512-EzH1+9gzdyEo9zdP6u7Sh3Jtf5EOMwzy+TK65JysdlgAzfEVfq4mNeXcAZ6SmD+CW6M7ARJbvXLyTD0l2S5rpg==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@jest/create-cache-key-function": "^29.7.0",
"@react-native/assets-registry": "0.80.0",
"@react-native/codegen": "0.80.0",
"@react-native/community-cli-plugin": "0.80.0",
"@react-native/gradle-plugin": "0.80.0",
"@react-native/js-polyfills": "0.80.0",
"@react-native/normalize-colors": "0.80.0",
"@react-native/virtualized-lists": "0.80.0",
"@react-native/assets-registry": "0.79.3",
"@react-native/codegen": "0.79.3",
"@react-native/community-cli-plugin": "0.79.3",
"@react-native/gradle-plugin": "0.79.3",
"@react-native/js-polyfills": "0.79.3",
"@react-native/normalize-colors": "0.79.3",
"@react-native/virtualized-lists": "0.79.3",
"abort-controller": "^3.0.0",
"anser": "^1.4.9",
"ansi-regex": "^5.0.0",
"babel-jest": "^29.7.0",
"babel-plugin-syntax-hermes-parser": "0.28.1",
"babel-plugin-syntax-hermes-parser": "0.25.1",
"base64-js": "^1.5.1",
"chalk": "^4.0.0",
"commander": "^12.0.0",
"event-target-shim": "^5.0.1",
"flow-enums-runtime": "^0.0.6",
"glob": "^7.1.1",
"invariant": "^2.2.4",
"jest-environment-node": "^29.7.0",
"memoize-one": "^5.0.0",
"metro-runtime": "^0.82.2",
"metro-source-map": "^0.82.2",
"metro-runtime": "^0.82.0",
"metro-source-map": "^0.82.0",
"nullthrows": "^1.1.1",
"pretty-format": "^29.7.0",
"promise": "^8.3.0",
"react-devtools-core": "^6.1.1",
"react-refresh": "^0.14.0",
"regenerator-runtime": "^0.13.2",
"scheduler": "0.26.0",
"scheduler": "0.25.0",
"semver": "^7.1.3",
"stacktrace-parser": "^0.1.10",
"whatwg-fetch": "^3.0.0",
@@ -25099,8 +25005,8 @@
"node": ">=18"
},
"peerDependencies": {
"@types/react": "^19.1.0",
"react": "^19.1.0"
"@types/react": "^19.0.0",
"react": "^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
@@ -25133,46 +25039,14 @@
"react-native": "*"
}
},
"node_modules/react-native/node_modules/@react-native/codegen": {
"version": "0.80.0",
"resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.80.0.tgz",
"integrity": "sha512-X9TsPgytoUkNrQjzAZh4dXa4AuouvYT0NzYyvnjw1ry4LESCZtKba+eY4x3+M30WPR52zjgu+UFL//14BSdCCA==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"glob": "^7.1.1",
"hermes-parser": "0.28.1",
"invariant": "^2.2.4",
"nullthrows": "^1.1.1",
"yargs": "^17.6.2"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@babel/core": "*"
}
},
"node_modules/react-native/node_modules/@react-native/normalize-colors": {
"version": "0.80.0",
"resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.80.0.tgz",
"integrity": "sha512-bJZDSopadjJxMDvysc634eTfLL4w7cAx5diPe14Ez5l+xcKjvpfofS/1Ja14DlgdMJhxGd03MTXlrxoWust3zg==",
"version": "0.79.3",
"resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.79.3.tgz",
"integrity": "sha512-T75NIQPRFCj6DFMxtcVMJTZR+3vHXaUMSd15t+CkJpc5LnyX91GVaPxpRSAdjFh7m3Yppl5MpdjV/fntImheYQ==",
"license": "MIT",
"optional": true,
"peer": true
},
"node_modules/react-native/node_modules/babel-plugin-syntax-hermes-parser": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/babel-plugin-syntax-hermes-parser/-/babel-plugin-syntax-hermes-parser-0.28.1.tgz",
"integrity": "sha512-meT17DOuUElMNsL5LZN56d+KBp22hb0EfxWfuPUeoSi54e40v1W4C2V36P75FpsH9fVEfDKpw5Nnkahc8haSsQ==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"hermes-parser": "0.28.1"
}
},
"node_modules/react-native/node_modules/commander": {
"version": "12.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
@@ -25184,25 +25058,6 @@
"node": ">=18"
}
},
"node_modules/react-native/node_modules/hermes-estree": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.28.1.tgz",
"integrity": "sha512-w3nxl/RGM7LBae0v8LH2o36+8VqwOZGv9rX1wyoWT6YaKZLqpJZ0YQ5P0LVr3tuRpf7vCx0iIG4i/VmBJejxTQ==",
"license": "MIT",
"optional": true,
"peer": true
},
"node_modules/react-native/node_modules/hermes-parser": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.28.1.tgz",
"integrity": "sha512-nf8o+hE8g7UJWParnccljHumE9Vlq8F7MqIdeahl+4x0tvCUJYRrT0L7h0MMg/X9YJmkNwsfbaNNrzPtFXOscg==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"hermes-estree": "0.28.1"
}
},
"node_modules/react-native/node_modules/ws": {
"version": "6.2.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-6.2.3.tgz",
@@ -25659,9 +25514,9 @@
}
},
"node_modules/replace/node_modules/brace-expansion": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -26105,15 +25960,15 @@
}
},
"node_modules/rimraf/node_modules/glob": {
"version": "11.0.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz",
"integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==",
"version": "11.0.2",
"resolved": "https://registry.npmjs.org/glob/-/glob-11.0.2.tgz",
"integrity": "sha512-YT7U7Vye+t5fZ/QMkBFrTJ7ZQxInIUjwyAjVj84CYXqgBdv30MFUPGnBR6sQaVq6Is15wYJUsnzTuWaGRBhBAQ==",
"dev": true,
"license": "ISC",
"dependencies": {
"foreground-child": "^3.3.1",
"jackspeak": "^4.1.1",
"minimatch": "^10.0.3",
"foreground-child": "^3.1.0",
"jackspeak": "^4.0.1",
"minimatch": "^10.0.0",
"minipass": "^7.1.2",
"package-json-from-dist": "^1.0.0",
"path-scurry": "^2.0.0"
@@ -26155,13 +26010,13 @@
}
},
"node_modules/rimraf/node_modules/minimatch": {
"version": "10.0.3",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz",
"integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==",
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz",
"integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==",
"dev": true,
"license": "ISC",
"dependencies": {
"@isaacs/brace-expansion": "^5.0.0"
"brace-expansion": "^2.0.1"
},
"engines": {
"node": "20 || >=22"
@@ -26239,9 +26094,9 @@
}
},
"node_modules/rollup": {
"version": "4.43.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.43.0.tgz",
"integrity": "sha512-wdN2Kd3Twh8MAEOEJZsuxuLKCsBEo4PVNLK6tQWAn10VhsVewQLzcucMgLolRlhFybGxfclbPeEYBaP6RvUFGg==",
"version": "4.42.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.42.0.tgz",
"integrity": "sha512-LW+Vse3BJPyGJGAJt1j8pWDKPd73QM8cRXYK1IxOBgL2AGLu7Xd2YOW0M2sLUBCkF5MshXXtMApyEAEzMVMsnw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -26255,33 +26110,33 @@
"npm": ">=8.0.0"
},
"optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.43.0",
"@rollup/rollup-android-arm64": "4.43.0",
"@rollup/rollup-darwin-arm64": "4.43.0",
"@rollup/rollup-darwin-x64": "4.43.0",
"@rollup/rollup-freebsd-arm64": "4.43.0",
"@rollup/rollup-freebsd-x64": "4.43.0",
"@rollup/rollup-linux-arm-gnueabihf": "4.43.0",
"@rollup/rollup-linux-arm-musleabihf": "4.43.0",
"@rollup/rollup-linux-arm64-gnu": "4.43.0",
"@rollup/rollup-linux-arm64-musl": "4.43.0",
"@rollup/rollup-linux-loongarch64-gnu": "4.43.0",
"@rollup/rollup-linux-powerpc64le-gnu": "4.43.0",
"@rollup/rollup-linux-riscv64-gnu": "4.43.0",
"@rollup/rollup-linux-riscv64-musl": "4.43.0",
"@rollup/rollup-linux-s390x-gnu": "4.43.0",
"@rollup/rollup-linux-x64-gnu": "4.43.0",
"@rollup/rollup-linux-x64-musl": "4.43.0",
"@rollup/rollup-win32-arm64-msvc": "4.43.0",
"@rollup/rollup-win32-ia32-msvc": "4.43.0",
"@rollup/rollup-win32-x64-msvc": "4.43.0",
"@rollup/rollup-android-arm-eabi": "4.42.0",
"@rollup/rollup-android-arm64": "4.42.0",
"@rollup/rollup-darwin-arm64": "4.42.0",
"@rollup/rollup-darwin-x64": "4.42.0",
"@rollup/rollup-freebsd-arm64": "4.42.0",
"@rollup/rollup-freebsd-x64": "4.42.0",
"@rollup/rollup-linux-arm-gnueabihf": "4.42.0",
"@rollup/rollup-linux-arm-musleabihf": "4.42.0",
"@rollup/rollup-linux-arm64-gnu": "4.42.0",
"@rollup/rollup-linux-arm64-musl": "4.42.0",
"@rollup/rollup-linux-loongarch64-gnu": "4.42.0",
"@rollup/rollup-linux-powerpc64le-gnu": "4.42.0",
"@rollup/rollup-linux-riscv64-gnu": "4.42.0",
"@rollup/rollup-linux-riscv64-musl": "4.42.0",
"@rollup/rollup-linux-s390x-gnu": "4.42.0",
"@rollup/rollup-linux-x64-gnu": "4.42.0",
"@rollup/rollup-linux-x64-musl": "4.42.0",
"@rollup/rollup-win32-arm64-msvc": "4.42.0",
"@rollup/rollup-win32-ia32-msvc": "4.42.0",
"@rollup/rollup-win32-x64-msvc": "4.42.0",
"fsevents": "~2.3.2"
}
},
"node_modules/rollup/node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.43.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.43.0.tgz",
"integrity": "sha512-eKoL8ykZ7zz8MjgBenEF2OoTNFAPFz1/lyJ5UmmFSz5jW+7XbH1+MAgCVHy72aG59rbuQLcJeiMrP8qP5d/N0A==",
"version": "4.42.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.42.0.tgz",
"integrity": "sha512-JxHtA081izPBVCHLKnl6GEA0w3920mlJPLh89NojpU2GsBSB6ypu4erFg/Wx1qbpUbepn0jY4dVWMGZM8gplgA==",
"cpu": [
"arm64"
],
@@ -26293,9 +26148,9 @@
]
},
"node_modules/rollup/node_modules/@rollup/rollup-darwin-x64": {
"version": "4.43.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.43.0.tgz",
"integrity": "sha512-SYwXJgaBYW33Wi/q4ubN+ldWC4DzQY62S4Ll2dgfr/dbPoF50dlQwEaEHSKrQdSjC6oIe1WgzosoaNoHCdNuMg==",
"version": "4.42.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.42.0.tgz",
"integrity": "sha512-rv5UZaWVIJTDMyQ3dCEK+m0SAn6G7H3PRc2AZmExvbDvtaDc+qXkei0knQWcI3+c9tEs7iL/4I4pTQoPbNL2SA==",
"cpu": [
"x64"
],
@@ -26307,9 +26162,9 @@
]
},
"node_modules/rollup/node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.43.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.43.0.tgz",
"integrity": "sha512-8FnkipasmOOSSlfucGYEu58U8cxEdhziKjPD2FIa0ONVMxvl/hmONtX/7y4vGjdUhjcTHlKlDhw3H9t98fPvyA==",
"version": "4.42.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.42.0.tgz",
"integrity": "sha512-eoujJFOvoIBjZEi9hJnXAbWg+Vo1Ov8n/0IKZZcPZ7JhBzxh2A+2NFyeMZIRkY9iwBvSjloKgcvnjTbGKHE44Q==",
"cpu": [
"arm64"
],
@@ -26321,9 +26176,9 @@
]
},
"node_modules/rollup/node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.43.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.43.0.tgz",
"integrity": "sha512-KPPyAdlcIZ6S9C3S2cndXDkV0Bb1OSMsX0Eelr2Bay4EsF9yi9u9uzc9RniK3mcUGCLhWY9oLr6er80P5DE6XA==",
"version": "4.42.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.42.0.tgz",
"integrity": "sha512-/3NrcOWFSR7RQUQIuZQChLND36aTU9IYE4j+TB40VU78S+RA0IiqHR30oSh6P1S9f9/wVOenHQnacs/Byb824g==",
"cpu": [
"arm64"
],
@@ -26335,9 +26190,9 @@
]
},
"node_modules/rollup/node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.43.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.43.0.tgz",
"integrity": "sha512-jAHr/S0iiBtFyzjhOkAics/2SrXE092qyqEg96e90L3t9Op8OTzS6+IX0Fy5wCt2+KqeHAkti+eitV0wvblEoQ==",
"version": "4.42.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.42.0.tgz",
"integrity": "sha512-Gfm6cV6mj3hCUY8TqWa63DB8Mx3NADoFwiJrMpoZ1uESbK8FQV3LXkhfry+8bOniq9pqY1OdsjFWNsSbfjPugw==",
"cpu": [
"x64"
],
@@ -26349,9 +26204,9 @@
]
},
"node_modules/rollup/node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.43.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.43.0.tgz",
"integrity": "sha512-3yATWgdeXyuHtBhrLt98w+5fKurdqvs8B53LaoKD7P7H7FKOONLsBVMNl9ghPQZQuYcceV5CDyPfyfGpMWD9mQ==",
"version": "4.42.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.42.0.tgz",
"integrity": "sha512-g86PF8YZ9GRqkdi0VoGlcDUb4rYtQKyTD1IVtxxN4Hpe7YqLBShA7oHMKU6oKTCi3uxwW4VkIGnOaH/El8de3w==",
"cpu": [
"x64"
],
@@ -26363,9 +26218,9 @@
]
},
"node_modules/rollup/node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.43.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.43.0.tgz",
"integrity": "sha512-wVzXp2qDSCOpcBCT5WRWLmpJRIzv23valvcTwMHEobkjippNf+C3ys/+wf07poPkeNix0paTNemB2XrHr2TnGw==",
"version": "4.42.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.42.0.tgz",
"integrity": "sha512-+axkdyDGSp6hjyzQ5m1pgcvQScfHnMCcsXkx8pTgy/6qBmWVhtRVlgxjWwDp67wEXXUr0x+vD6tp5W4x6V7u1A==",
"cpu": [
"arm64"
],
@@ -26377,9 +26232,9 @@
]
},
"node_modules/rollup/node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.43.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.43.0.tgz",
"integrity": "sha512-SnGhLiE5rlK0ofq8kzuDkM0g7FN1s5VYY+YSMTibP7CqShxCQvqtNxTARS4xX4PFJfHjG0ZQYX9iGzI3FQh5Aw==",
"version": "4.42.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.42.0.tgz",
"integrity": "sha512-LpHiJRwkaVz/LqjHjK8LCi8osq7elmpwujwbXKNW88bM8eeGxavJIKKjkjpMHAh/2xfnrt1ZSnhTv41WYUHYmA==",
"cpu": [
"x64"
],
@@ -26555,9 +26410,9 @@
"license": "ISC"
},
"node_modules/scheduler": {
"version": "0.26.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz",
"integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==",
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz",
"integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==",
"license": "MIT",
"optional": true,
"peer": true
@@ -26569,9 +26424,9 @@
"license": "MIT"
},
"node_modules/sdp": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/sdp/-/sdp-3.2.1.tgz",
"integrity": "sha512-lwsAIzOPlH8/7IIjjz3K0zYBk7aBVVcvjMwt3M4fLxpjMYyy7i3I97SLHebgn4YBjirkzfp3RvRDWSKsh/+WFw==",
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/sdp/-/sdp-3.2.0.tgz",
"integrity": "sha512-d7wDPgDV3DDiqulJjKiV2865wKsJ34YI+NDREbm+FySq6WuKOikwyNQcm+doLAZ1O6ltdO0SeKle2xMpN3Brgw==",
"license": "MIT"
},
"node_modules/secp256k1": {
@@ -28640,9 +28495,9 @@
}
},
"node_modules/test-exclude/node_modules/brace-expansion": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"license": "MIT",
"optional": true,
"peer": true,
@@ -31190,9 +31045,9 @@
}
},
"node_modules/zod": {
"version": "3.25.64",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.64.tgz",
"integrity": "sha512-hbP9FpSZf7pkS7hRVUrOjhwKJNyampPgtXKc3AN6DsWtoHsg2Sb4SQaS4Tcay380zSwd2VPo9G9180emBACp5g==",
"version": "3.25.58",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.58.tgz",
"integrity": "sha512-DVLmMQzSZwNYzQoMaM3MQWnxr2eq+AtM9Hx3w1/Yl0pH8sLTSjN4jGP7w6f7uand6Hw44tsnSu1hz1AOA6qI2Q==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"

View File

@@ -1,894 +0,0 @@
/** * Backup Files List Component * * Displays a list of backup files saved by
the app and provides options to: * - View backup files by type (contacts, seed,
other) * - Open individual files in the device's file viewer * - Access the
backup directory in the device's file explorer * * @component * @displayName
BackupFilesList * @example * ```vue *
<BackupFilesList />
* ``` */
<template>
<div class="backup-files-list">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-semibold">Backup Files</h3>
<div class="flex gap-2">
<button
v-if="platformCapabilities.hasFileSystem"
class="text-sm bg-blue-500 hover:bg-blue-600 text-white px-3 py-1 rounded"
:disabled="isLoading"
@click="refreshFiles()"
>
<font-awesome
icon="refresh"
class="fa-fw"
:class="{ 'animate-spin': isLoading }"
/>
Refresh
</button>
<button
v-if="platformCapabilities.hasFileSystem"
class="text-sm bg-green-500 hover:bg-green-600 text-white px-3 py-1 rounded"
:disabled="isLoading"
@click="openBackupDirectory()"
>
<font-awesome icon="folder-open" class="fa-fw" />
Open Directory
</button>
<button
v-if="platformCapabilities.hasFileSystem && isDevelopment"
class="text-sm bg-yellow-500 hover:bg-yellow-600 text-white px-3 py-1 rounded"
:disabled="isLoading"
title="Debug file discovery (development only)"
@click="debugFileDiscovery()"
>
<font-awesome icon="bug" class="fa-fw" />
Debug
</button>
<button
:disabled="isLoading"
class="px-3 py-1 bg-green-500 text-white rounded text-sm hover:bg-green-600 disabled:opacity-50"
@click="createTestBackup"
>
Create Test Backup
</button>
<button
:disabled="isLoading"
class="px-3 py-1 bg-purple-500 text-white rounded text-sm hover:bg-purple-600 disabled:opacity-50"
@click="testDirectoryContexts"
>
Test Contexts
</button>
</div>
</div>
<div v-if="isLoading" class="text-center py-4">
<font-awesome icon="spinner" class="animate-spin fa-2x" />
<p class="mt-2">Loading backup files...</p>
</div>
<div
v-else-if="backupFiles.length === 0"
class="text-center py-4 text-gray-500"
>
<font-awesome icon="folder-open" class="fa-2x mb-2" />
<p>No backup files found</p>
<p class="text-sm mt-1">
Create backups using the export functions above
</p>
<div
class="mt-3 p-3 bg-blue-50 border border-blue-200 rounded-lg text-left"
>
<p class="text-sm font-medium text-blue-800 mb-2">
💡 How to create backup files:
</p>
<ul class="text-xs text-blue-700 space-y-1">
<li>
Use the "Export Contacts" button above to create contact backups
</li>
<li> Use the "Export Seed" button to backup your recovery phrase</li>
<li>
Backup files are saved to persistent storage that survives app
installations
</li>
<li
v-if="platformCapabilities.isMobile && !platformCapabilities.isIOS"
class="text-orange-700"
>
On Android: Files are saved to Downloads/TimeSafari or app data
directory
</li>
<li v-if="platformCapabilities.isIOS" class="text-orange-700">
On iOS: Files are saved to Documents folder (accessible via Files
app)
</li>
</ul>
</div>
</div>
<div v-else class="space-y-2">
<!-- File Type Filter -->
<div class="flex gap-2 mb-3">
<button
v-for="type in ['all', 'contacts', 'seed', 'other'] as const"
:key="type"
:class="[
'text-sm px-3 py-1 rounded border',
selectedType === type
? 'bg-blue-500 text-white border-blue-500'
: 'bg-white text-gray-700 border-gray-300 hover:bg-gray-50',
]"
@click="selectedType = type"
>
{{
type === "all"
? "All"
: type.charAt(0).toUpperCase() + type.slice(1)
}}
<span class="ml-1 text-xs"> ({{ getFileCountByType(type) }}) </span>
</button>
</div>
<!-- Files List -->
<div class="flex items-center gap-2 mb-2">
<span v-for="(crumb, idx) in breadcrumbs" :key="idx">
<span
v-if="idx < breadcrumbs.length - 1"
class="text-blue-600 cursor-pointer underline"
@click="goToBreadcrumb(idx)"
>
{{ crumb }}
</span>
<span v-else class="font-bold">{{ crumb }}</span>
<span v-if="idx < breadcrumbs.length - 1"> / </span>
</span>
</div>
<div v-if="currentPath.length > 1" class="mb-2">
<button class="text-xs text-blue-500 underline" @click="goUp">
Up
</button>
</div>
<div class="mb-2">
<label class="inline-flex items-center">
<input
v-model="debugShowAll"
type="checkbox"
class="mr-2"
@change="loadDirectory"
/>
<span class="text-xs">Debug: Show all entries as files</span>
</label>
<span v-if="debugShowAll" class="text-xs text-red-600 ml-2"
>[Debug mode: forcibly treating all entries as files]</span
>
</div>
<div class="space-y-2 max-h-64 overflow-y-auto">
<div
v-for="entry in folders"
:key="'folder-' + entry.path"
class="flex items-center justify-between p-3 bg-white border border-gray-200 rounded-lg hover:bg-gray-50 cursor-pointer"
@click="openFolder(entry)"
>
<div class="flex items-center gap-2">
<font-awesome icon="folder" class="fa-fw text-yellow-500" />
<span class="font-medium">{{ entry.name }}</span>
<span
class="text-xs bg-gray-200 text-gray-700 px-2 py-0.5 rounded-full ml-2"
>Folder</span
>
</div>
</div>
<div
v-for="entry in files"
:key="'file-' + entry.path"
class="flex items-center justify-between p-3 bg-white border border-gray-200 rounded-lg hover:bg-gray-50"
>
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2">
<font-awesome icon="file-alt" class="fa-fw text-gray-500" />
<span class="font-medium truncate">{{ entry.name }}</span>
</div>
<div class="text-sm text-gray-500 mt-1">
<span v-if="entry.size">{{ formatFileSize(entry.size) }}</span>
<span v-else>Size unknown</span>
<span
v-if="entry.path && !platformCapabilities.isIOS"
class="ml-2 text-xs text-blue-600"
>📁 {{ entry.path }}</span
>
</div>
</div>
<div class="flex gap-2 ml-3">
<button
class="text-blue-500 hover:text-blue-700 p-1"
title="Open file"
@click="openFile(entry.uri, entry.name)"
>
<font-awesome icon="external-link-alt" class="fa-fw" />
</button>
</div>
</div>
</div>
<!-- Summary -->
<div class="text-sm text-gray-500 mt-3 pt-3 border-t">
Showing {{ filteredFiles.length }} of {{ backupFiles.length }} backup
files
</div>
<div class="text-sm text-gray-600 mb-2">
<p>
📁 Backup files are saved to persistent storage that survives app
installations:
</p>
<ul class="list-disc list-inside ml-2 mt-1 text-xs">
<li v-if="platformCapabilities.isIOS">
iOS: Documents folder (accessible via Files app)
</li>
<li
v-if="platformCapabilities.isMobile && !platformCapabilities.isIOS"
>
Android: Downloads/TimeSafari or external storage (accessible via
file managers)
</li>
<li v-if="!platformCapabilities.isMobile">
Desktop: User's download directory
</li>
</ul>
</div>
</div>
</div>
</template>
<script lang="ts">
import { Component, Vue, Watch } from "vue-facing-decorator";
import { NotificationIface } from "../constants/app";
import { logger } from "../utils/logger";
import { PlatformServiceFactory } from "../services/PlatformServiceFactory";
import {
PlatformService,
PlatformCapabilities,
} from "../services/PlatformService";
/**
* @vue-component
* Backup Files List Component
* Displays and manages backup files with platform-specific functionality
*/
@Component
export default class BackupFilesList extends Vue {
/**
* Notification function injected by Vue
* Used to show success/error messages to the user
*/
$notify!: (notification: NotificationIface, timeout?: number) => void;
/**
* Platform service instance for platform-specific operations
*/
private platformService: PlatformService =
PlatformServiceFactory.getInstance();
/**
* Platform capabilities for the current platform
*/
private get platformCapabilities(): PlatformCapabilities {
return this.platformService.getCapabilities();
}
/**
* List of backup files found on the device
*/
backupFiles: Array<{
name: string;
uri: string;
size?: number;
type: "contacts" | "seed" | "other";
path?: string;
}> = [];
/**
* Currently selected file type filter
*/
selectedType: "all" | "contacts" | "seed" | "other" = "all";
/**
* Loading state for file operations
*/
isLoading = false;
/**
* Interval for periodic refresh (5 minutes)
*/
private refreshInterval: number | null = null;
/**
* Current path for folder navigation (array for breadcrumbs)
*/
currentPath: string[] = [];
/**
* List of files/folders in the current directory
*/
directoryEntries: Array<{
name: string;
uri: string;
size?: number;
path: string;
type: "file" | "folder";
}> = [];
/**
* Temporary debug mode to show all entries as files
*/
debugShowAll = false;
/**
* Checks and requests storage permissions if needed.
* Returns true if permission is granted, false otherwise.
*/
private async ensureStoragePermission(): Promise<boolean> {
logger.log(
"[BackupFilesList] ensureStoragePermission called. platformCapabilities:",
this.platformCapabilities,
);
if (!this.platformCapabilities.hasFileSystem) return true;
// Only relevant for native platforms (Android/iOS)
const platformService = this.platformService as any;
if (typeof platformService.checkStoragePermissions === "function") {
try {
await platformService.checkStoragePermissions();
logger.log("[BackupFilesList] Storage permission granted.");
return true;
} catch (error) {
logger.error("[BackupFilesList] Storage permission denied:", error);
// Get specific guidance for the platform
let guidance =
"This app needs permission to access your files to list and restore backups.";
if (
typeof platformService.getStoragePermissionGuidance === "function"
) {
try {
guidance = await platformService.getStoragePermissionGuidance();
} catch (guidanceError) {
logger.warn(
"[BackupFilesList] Could not get permission guidance:",
guidanceError,
);
}
}
this.$notify(
{
group: "alert",
type: "warning",
title: "Storage Permission Required",
text: guidance,
},
10000, // Show for 10 seconds to give user time to read
);
return false;
}
}
return true;
}
/**
* Lifecycle hook to load backup files when component is mounted
*/
async mounted() {
logger.log(
"[BackupFilesList] mounted hook called. platformCapabilities:",
this.platformCapabilities,
);
if (this.platformCapabilities.hasFileSystem) {
// Check/request permission before loading
const hasPermission = await this.ensureStoragePermission();
if (hasPermission) {
// Set default root path
if (this.platformCapabilities.isIOS) {
this.currentPath = ["."];
} else {
this.currentPath = ["Download", "TimeSafari"];
}
await this.loadDirectory();
this.refreshInterval = window.setInterval(
() => {
this.loadDirectory();
},
5 * 60 * 1000,
);
}
}
}
/**
* Lifecycle hook to clean up resources when component is unmounted
*/
beforeUnmount() {
if (this.refreshInterval) {
clearInterval(this.refreshInterval);
this.refreshInterval = null;
}
}
/**
* Computed property for filtered files based on selected type
* Note: The 'All' tab count is sometimes too small. Logging for debugging.
*/
get filteredFiles() {
if (this.selectedType === "all") {
logger.log("[BackupFilesList] filteredFiles (All):", this.backupFiles);
return this.backupFiles;
}
const filtered = this.backupFiles.filter(
(file) => file.type === this.selectedType,
);
logger.log(
`[BackupFilesList] filteredFiles (${this.selectedType}):`,
filtered,
);
return filtered;
}
/**
* Computed property to check if we're in development mode
*/
get isDevelopment(): boolean {
return import.meta.env.DEV;
}
/**
* Load the current directory entries
*/
async loadDirectory() {
if (!this.platformCapabilities.hasFileSystem) return;
this.isLoading = true;
try {
const path =
this.currentPath.join("/") ||
(this.platformCapabilities.isIOS ? "." : "Download/TimeSafari");
this.directoryEntries = await (
this.platformService as PlatformService
).listFilesInDirectory(path, this.debugShowAll);
logger.log("[BackupFilesList] Loaded directory:", {
path,
entries: this.directoryEntries,
});
} catch (error) {
logger.error("[BackupFilesList] Failed to load directory:", error);
this.directoryEntries = [];
} finally {
this.isLoading = false;
}
}
/**
* Navigate into a folder
*/
async openFolder(entry: { name: string; path: string }) {
this.currentPath.push(entry.name);
await this.loadDirectory();
}
/**
* Navigate to a breadcrumb
*/
async goToBreadcrumb(index: number) {
this.currentPath = this.currentPath.slice(0, index + 1);
await this.loadDirectory();
}
/**
* Go up one directory
*/
async goUp() {
if (this.currentPath.length > 1) {
this.currentPath.pop();
await this.loadDirectory();
}
}
/**
* Computed property for breadcrumbs
*/
get breadcrumbs() {
return this.currentPath;
}
/**
* Computed property for showing files and folders
*/
get folders() {
return this.directoryEntries.filter((e) => e.type === "folder");
}
get files() {
return this.directoryEntries.filter((e) => e.type === "file");
}
/**
* Refreshes the list of backup files from the device
*/
async refreshFiles() {
logger.log("[BackupFilesList] refreshFiles called.");
if (!this.platformCapabilities.hasFileSystem) {
return;
}
// Check/request permission before refreshing
const hasPermission = await this.ensureStoragePermission();
if (!hasPermission) {
this.backupFiles = [];
this.isLoading = false;
return;
}
this.isLoading = true;
try {
this.backupFiles = await this.platformService.listBackupFiles();
logger.log("[BackupFilesList] Refreshed backup files:", {
count: this.backupFiles.length,
files: this.backupFiles.map((f) => ({
name: f.name,
type: f.type,
path: f.path,
size: f.size,
})),
platform: this.platformCapabilities.isIOS ? "iOS" : "Android",
timestamp: new Date().toISOString(),
});
// Debug: Log file type distribution
const typeCounts = {
contacts: this.backupFiles.filter((f) => f.type === "contacts").length,
seed: this.backupFiles.filter((f) => f.type === "seed").length,
other: this.backupFiles.filter((f) => f.type === "other").length,
total: this.backupFiles.length,
};
logger.log("[BackupFilesList] File type distribution:", typeCounts);
// Log the full backupFiles array for debugging the 'All' tab count
logger.log(
"[BackupFilesList] backupFiles array for All tab:",
this.backupFiles,
);
} catch (error) {
logger.error("[BackupFilesList] Failed to refresh backup files:", error);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Loading Files",
text: "Failed to load backup files from your device.",
},
5000,
);
} finally {
this.isLoading = false;
}
}
/**
* Creates a test backup file for debugging purposes
*/
async createTestBackup() {
try {
this.isLoading = true;
logger.log("[BackupFilesList] Creating test backup file");
const result = await this.platformService.createTestBackupFile();
if (result.success) {
logger.log("[BackupFilesList] Test backup file created successfully:", {
fileName: result.fileName,
uri: result.uri,
timestamp: new Date().toISOString(),
});
this.$notify(
{
group: "alert",
type: "success",
title: "Test Backup Created",
text: `Test backup file "${result.fileName}" created successfully. Refresh the list to see it.`,
},
5000,
);
// Refresh the file list to show the new test file
await this.refreshFiles();
} else {
throw new Error(result.error || "Failed to create test backup file");
}
} catch (error) {
logger.error(
"[BackupFilesList] Failed to create test backup file:",
error,
);
this.$notify(
{
group: "alert",
type: "danger",
title: "Test Backup Failed",
text: "Failed to create test backup file. Check the console for details.",
},
5000,
);
} finally {
this.isLoading = false;
}
}
/**
* Tests different directory contexts to debug file visibility issues
*/
async testDirectoryContexts() {
try {
this.isLoading = true;
logger.log("[BackupFilesList] Testing directory contexts");
const debugOutput = await this.platformService.testDirectoryContexts();
logger.log(
"[BackupFilesList] Directory context test results:",
debugOutput,
);
// Show the debug output in a notification or alert
this.$notify(
{
group: "alert",
type: "info",
title: "Directory Context Test",
text: "Directory context test completed. Check the console for detailed results.",
},
5000,
);
// Also log the full output to console for easy access
logger.log("=== Directory Context Test Results ===");
logger.log(debugOutput);
logger.log("=== End Test Results ===");
} catch (error) {
logger.error(
"[BackupFilesList] Failed to test directory contexts:",
error,
);
this.$notify(
{
group: "alert",
type: "danger",
title: "Context Test Failed",
text: "Failed to test directory contexts. Check the console for details.",
},
5000,
);
} finally {
this.isLoading = false;
}
}
/**
* Refreshes the file list after a backup is created
* This method can be called from parent components
*/
async refreshAfterSave() {
logger.log("[BackupFilesList] refreshAfterSave called");
await this.refreshFiles();
}
/**
* Opens a specific file in the device's file viewer
* @param fileUri - URI of the file to open
* @param fileName - Name of the file for display
*/
async openFile(fileUri: string, fileName: string) {
try {
const result = await this.platformService.openFile(fileUri, fileName);
if (result.success) {
logger.log("[BackupFilesList] File opened successfully:", {
fileName,
timestamp: new Date().toISOString(),
});
} else {
throw new Error(result.error || "Failed to open file");
}
} catch (error) {
logger.error("[BackupFilesList] Failed to open file:", error);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Opening File",
text: `Failed to open ${fileName}. ${error instanceof Error ? error.message : String(error)}`,
},
5000,
);
}
}
/**
* Opens the backup directory in the device's file explorer
*/
async openBackupDirectory() {
try {
const result = await this.platformService.openBackupDirectory();
if (result.success) {
logger.log("[BackupFilesList] Backup directory opened successfully:", {
timestamp: new Date().toISOString(),
});
} else {
throw new Error(result.error || "Failed to open backup directory");
}
} catch (error) {
logger.error("[BackupFilesList] Failed to open backup directory:", error);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error Opening Directory",
text: `Failed to open backup directory. ${error instanceof Error ? error.message : String(error)}`,
},
5000,
);
}
}
/**
* Gets the count of files for a specific type
* Note: The 'All' tab count is sometimes too small. Logging for debugging.
*/
getFileCountByType(type: "all" | "contacts" | "seed" | "other"): number {
let count;
if (type === "all") {
count = this.backupFiles.length;
logger.log(
"[BackupFilesList] getFileCountByType (All):",
count,
this.backupFiles,
);
return count;
}
count = this.backupFiles.filter((file) => file.type === type).length;
logger.log(`[BackupFilesList] getFileCountByType (${type}):`, count);
return count;
}
/**
* Gets the appropriate icon for a file type
* @param type - File type
* @returns FontAwesome icon name
*/
getFileIcon(type: "contacts" | "seed" | "other"): string {
switch (type) {
case "contacts":
return "address-book";
case "seed":
return "key";
default:
return "file-alt";
}
}
/**
* Gets the appropriate icon color for a file type
* @param type - File type
* @returns CSS color class
*/
getFileIconColor(type: "contacts" | "seed" | "other"): string {
switch (type) {
case "contacts":
return "text-blue-500";
case "seed":
return "text-orange-500";
default:
return "text-gray-500";
}
}
/**
* Gets the appropriate badge color for a file type
* @param type - File type
* @returns CSS color class
*/
getTypeBadgeColor(type: "contacts" | "seed" | "other"): string {
switch (type) {
case "contacts":
return "bg-blue-100 text-blue-800";
case "seed":
return "bg-orange-100 text-orange-800";
default:
return "bg-gray-100 text-gray-800";
}
}
/**
* Formats file size in human-readable format
* @param bytes - File size in bytes
* @returns Formatted file size string
*/
formatFileSize(bytes: number): string {
if (bytes === 0) return "0 Bytes";
const k = 1024;
const sizes = ["Bytes", "KB", "MB", "GB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
}
/**
* Debug method to test file discovery
* Can be called from browser console for troubleshooting
*/
public async debugFileDiscovery() {
try {
logger.log("[BackupFilesList] Starting debug file discovery...");
// Test the platform service's test methods
const platformService = PlatformServiceFactory.getInstance();
// Test listing all user files
const allFilesResult = await platformService.testListUserFiles();
logger.log(
"[BackupFilesList] All user files test result:",
allFilesResult,
);
// Test listing backup files specifically
const backupFilesResult = await platformService.testBackupFiles();
logger.log(
"[BackupFilesList] Backup files test result:",
backupFilesResult,
);
// Note: testListAllBackupFiles method is not part of the PlatformService interface
// It exists only in CapacitorPlatformService implementation
// If needed, this could be added to the interface or called via type assertion
// Test debug listing all files without filtering (if available)
if ("debugListAllFiles" in platformService) {
const debugAllFiles = await (
platformService as any
).debugListAllFiles();
logger.log("[BackupFilesList] Debug all files (no filtering):", {
count: debugAllFiles.length,
files: debugAllFiles.map((f: any) => ({
name: f.name,
path: f.path,
size: f.size,
})),
});
}
// Test comprehensive step-by-step debug (if available)
if ("debugFileDiscoveryStepByStep" in platformService) {
const stepByStepDebug = await (
platformService as any
).debugFileDiscoveryStepByStep();
logger.log(
"[BackupFilesList] Step-by-step debug output:",
stepByStepDebug,
);
}
return {
allFiles: allFilesResult,
backupFiles: backupFilesResult,
currentBackupFiles: this.backupFiles,
debugAllFiles:
"debugListAllFiles" in platformService
? await (platformService as any).debugListAllFiles()
: null,
};
} catch (error) {
logger.error("[BackupFilesList] Debug file discovery failed:", error);
throw error;
}
}
@Watch("platformCapabilities.hasFileSystem", { immediate: true })
async onFileSystemCapabilityChanged(newVal: boolean) {
if (newVal) {
await this.refreshFiles();
}
}
}
</script>

View File

@@ -1,8 +1,7 @@
/** * Data Export Section Component * * Provides UI and functionality for
exporting user data and backing up identifier seeds. * Includes buttons for seed
backup and database export, with platform-specific download instructions. * Also
displays a list of backup files with options to open them in the device's file
explorer. * * @component * @displayName DataExportSection * @example * ```vue *
backup and database export, with platform-specific download instructions. * *
@component * @displayName DataExportSection * @example * ```vue *
<DataExportSection :active-did="currentDid" />
* ``` */
@@ -44,27 +43,18 @@ explorer. * * @component * @displayName DataExportSection * @example * ```vue *
v-if="platformCapabilities.isIOS"
class="list-disc list-outside ml-4"
>
On iOS: Files are saved to Documents folder (accessible via Files app)
and persist between app installations.
On iOS: You will be prompted to choose a location to save your backup
file.
</li>
<li
v-if="platformCapabilities.isMobile && !platformCapabilities.isIOS"
class="list-disc list-outside ml-4"
>
On Android: Files are saved to Downloads/TimeSafari or external
storage (accessible via file managers) and persist between app
installations.
On Android: You will be prompted to choose a location to save your
backup file.
</li>
</ul>
</div>
<!-- Backup Files List -->
<div
v-if="platformCapabilities.hasFileSystem"
class="mt-6 pt-6 border-t border-gray-300"
>
<BackupFilesList ref="backupFilesList" />
</div>
</div>
</template>
@@ -75,21 +65,20 @@ import { AppString, NotificationIface } from "../constants/app";
import { Contact } from "../db/tables/contacts";
import * as databaseUtil from "../db/databaseUtil";
import { logger, getTimestampForFilename } from "../utils/logger";
import { logger } from "../utils/logger";
import { PlatformServiceFactory } from "../services/PlatformServiceFactory";
import {
PlatformService,
PlatformCapabilities,
} from "../services/PlatformService";
import { contactsToExportJson } from "../libs/util";
import BackupFilesList from "./BackupFilesList.vue";
/**
* @vue-component
* Data Export Section Component
* Handles database export and seed backup functionality with platform-specific behavior
*/
@Component({ components: { BackupFilesList } })
@Component
export default class DataExportSection extends Vue {
/**
* Notification function injected by Vue
@@ -162,9 +151,7 @@ export default class DataExportSection extends Vue {
const jsonStr = JSON.stringify(exportData, null, 2);
const blob = new Blob([jsonStr], { type: "application/json" });
// Create timestamped filename
const timestamp = getTimestampForFilename();
const fileName = `${AppString.APP_NAME_NO_SPACES}-backup-contacts-${timestamp}.json`;
const fileName = `${AppString.APP_NAME_NO_SPACES}-backup-contacts.json`;
if (this.platformCapabilities.hasFileDownload) {
// Web platform: Use download link
@@ -175,21 +162,8 @@ export default class DataExportSection extends Vue {
downloadAnchor.click();
setTimeout(() => URL.revokeObjectURL(this.downloadUrl), 1000);
} else if (this.platformCapabilities.hasFileSystem) {
// Native platform: Write to user-accessible location and share
const result = await this.platformService.writeAndShareFile(
fileName,
jsonStr,
{
allowLocationSelection: true,
showLocationSelectionDialog: true,
mimeType: "application/json",
},
);
// Handle the result
if (!result.saved) {
throw new Error(result.error || "Failed to save file");
}
// Native platform: Write to app directory
await this.platformService.writeAndShareFile(fileName, jsonStr);
} else {
throw new Error("This platform does not support file downloads.");
}
@@ -201,19 +175,10 @@ export default class DataExportSection extends Vue {
title: "Export Successful",
text: this.platformCapabilities.hasFileDownload
? "See your downloads directory for the backup."
: "Backup saved to persistent storage that survives app installations. Use the share dialog to access your file and choose where to save it permanently.",
: "The backup file has been saved.",
},
5000,
3000,
);
// Refresh the backup files list
const backupFilesList = this.$refs.backupFilesList as any;
if (
backupFilesList &&
typeof backupFilesList.refreshAfterSave === "function"
) {
await backupFilesList.refreshAfterSave();
}
} catch (error) {
logger.error("Export Error:", error);
this.$notify(
@@ -247,18 +212,5 @@ export default class DataExportSection extends Vue {
hidden: !this.downloadUrl || !this.platformCapabilities.hasFileDownload,
};
}
async mounted() {
// Ensure permissions are requested and refresh backup files list on mount
if (this.platformCapabilities.hasFileSystem) {
const backupFilesList = this.$refs.backupFilesList as any;
if (
backupFilesList &&
typeof backupFilesList.refreshFiles === "function"
) {
await backupFilesList.refreshFiles();
}
}
}
}
</script>

View File

@@ -1,99 +1,477 @@
<template>
<div v-if="visible" class="dialog-overlay">
<div class="dialog">
<h1 class="text-xl font-bold text-center mb-4">
{{ customTitle }}
</h1>
<input
v-model="description"
type="text"
class="block w-full rounded border border-slate-400 mb-2 px-3 py-2"
:placeholder="prompt || 'What was given?'"
/>
<div class="flex flex-row justify-center">
<span
class="rounded-l border border-r-0 border-slate-400 bg-slate-200 text-center text-blue-500 px-2 py-2 w-20"
@click="changeUnitCode()"
<!-- Step 1: Giver -->
<div v-show="currentStep === 1" id="sectionGiftedGiver">
<label class="block font-bold mb-4">
{{
stepType === "recipient"
? "Choose who received the gift:"
: showProjects
? "Choose a project benefitted from:"
: "Choose a person received from:"
}}
</label>
<!-- Unified Quick-pick grid for People and Projects -->
<ul
:class="
shouldShowProjects
? 'grid grid-cols-3 md:grid-cols-4 gap-x-2 gap-y-4 text-center mb-4'
: 'grid grid-cols-4 sm:grid-cols-5 md:grid-cols-6 gap-x-2 gap-y-4 text-center mb-4'
"
>
{{ libsUtil.UNIT_SHORT[unitCode] || unitCode }}
</span>
<div
class="border border-r-0 border-slate-400 bg-slate-200 px-4 py-2"
@click="amountInput === '0' ? null : decrement()"
>
<font-awesome icon="chevron-left" />
</div>
<input
id="inputGivenAmount"
v-model="amountInput"
type="number"
class="border border-r-0 border-slate-400 px-2 py-2 text-center w-20"
/>
<div
class="rounded-r border border-slate-400 bg-slate-200 px-4 py-2"
@click="increment()"
>
<font-awesome icon="chevron-right" />
</div>
</div>
<div class="mt-4 flex justify-center">
<span>
<router-link
:to="{
name: 'gifted-details',
query: {
amountInput,
description,
giverDid: giver?.did,
giverName: giver?.name,
offerId,
fulfillsProjectId: toProjectId,
providerProjectId: fromProjectId,
recipientDid: receiver?.did,
recipientName: receiver?.name,
unitCode,
},
}"
class="text-blue-500"
>
Photo & more options ...
</router-link>
</span>
</div>
<p class="text-center mb-2 mt-6 italic">
Sign & Send to publish to the world
<font-awesome
icon="circle-info"
class="pl-2 text-blue-500 cursor-pointer"
@click="explainData()"
/>
</p>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
<template v-if="shouldShowProjects">
<!-- show projects -->
<li
v-for="project in projects.slice(0, 7)"
:key="project.handleId"
class="cursor-pointer"
@click="
stepType === 'recipient'
? selectRecipientProject(project)
: selectProject(project)
"
>
<div class="relative w-fit mx-auto">
<ProjectIcon
:entity-id="project.handleId"
:icon-size="48"
:image-url="project.image"
class="!size-[3rem] mx-auto border border-slate-300 bg-white overflow-hidden rounded-full mb-1"
/>
</div>
<h3
class="text-xs font-medium text-ellipsis whitespace-nowrap overflow-hidden"
>
{{ project.name }}
</h3>
<div class="text-xs text-slate-500 truncate">
<font-awesome icon="user" class="fa-fw text-slate-400" />
{{
didInfo(project.issuerDid, activeDid, allMyDids, allContacts)
}}
</div>
</li>
<li
v-if="projects.length === 0"
class="text-xs text-slate-500 italic col-span-full"
>
(No projects found.)
</li>
<li v-if="projects.length > 0">
<router-link :to="{ name: 'discover' }" class="cursor-pointer">
<font-awesome
icon="circle-right"
class="text-blue-500 text-5xl mb-1"
/>
<h3
class="text-xs text-slate-500 font-medium italic text-ellipsis whitespace-nowrap overflow-hidden"
>
Show All
</h3>
</router-link>
</li>
</template>
<template v-else>
<!-- show people (contacts) -->
<li
v-if="
stepType === 'recipient' ||
(stepType === 'giver' && isFromProjectView)
"
:class="{
'cursor-pointer': !wouldCreateConflict(activeDid),
'cursor-not-allowed opacity-50': wouldCreateConflict(activeDid)
}"
@click="
!wouldCreateConflict(activeDid) &&
(stepType === 'recipient'
? selectRecipient({ did: activeDid, name: 'You' })
: selectGiver({ did: activeDid, name: 'You' }))
"
>
<font-awesome
:class="{
'text-blue-500 text-5xl mb-1': !wouldCreateConflict(activeDid),
'text-slate-400 text-5xl mb-1': wouldCreateConflict(activeDid)
}"
icon="hand"
/>
<h3
:class="{
'text-xs text-blue-500 font-medium text-ellipsis whitespace-nowrap overflow-hidden': !wouldCreateConflict(activeDid),
'text-xs text-slate-400 font-medium text-ellipsis whitespace-nowrap overflow-hidden': wouldCreateConflict(activeDid)
}"
>
You
</h3>
</li>
<li
class="cursor-pointer"
@click="
stepType === 'recipient' ? selectRecipient() : selectGiver()
"
>
<font-awesome
icon="circle-question"
class="text-slate-400 text-5xl mb-1"
/>
<h3
class="text-xs text-slate-500 font-medium italic text-ellipsis whitespace-nowrap overflow-hidden"
>
Unnamed
</h3>
</li>
<li
v-if="allContacts.length === 0"
class="text-xs text-slate-500 italic col-span-full"
>
(Add friends to see more people worthy of recognition.)
</li>
<li
v-for="contact in allContacts.slice(0, 10)"
:key="contact.did"
:class="{
'cursor-pointer': !wouldCreateConflict(contact.did),
'cursor-not-allowed opacity-50': wouldCreateConflict(contact.did)
}"
@click="
!wouldCreateConflict(contact.did) &&
(stepType === 'recipient'
? selectRecipient(contact)
: selectGiver(contact))
"
>
<div class="relative w-fit mx-auto">
<EntityIcon
:contact="contact"
class="!size-[3rem] mx-auto border border-slate-300 bg-white overflow-hidden rounded-full mb-1"
/>
<div
class="rounded-full bg-slate-400 absolute bottom-0 right-0 p-1 translate-x-1/3"
>
<font-awesome
icon="clock"
class="block text-white text-xs w-[1em]"
/>
</div>
</div>
<h3
:class="{
'text-xs font-medium text-ellipsis whitespace-nowrap overflow-hidden': !wouldCreateConflict(contact.did),
'text-xs font-medium text-ellipsis whitespace-nowrap overflow-hidden text-slate-400': wouldCreateConflict(contact.did)
}"
>
{{ contact.name || contact.did }}
</h3>
</li>
<li v-if="allContacts.length > 0" class="cursor-pointer">
<router-link
:to="{
name: 'contact-gift',
query: {
stepType: stepType,
giverEntityType: giverEntityType,
recipientEntityType: recipientEntityType,
...(stepType === 'giver'
? {
recipientProjectId: toProjectId,
recipientProjectName: receiver?.name,
recipientProjectImage: receiver?.image,
recipientProjectHandleId: receiver?.handleId,
recipientDid: receiver?.did,
}
: {
giverProjectId: fromProjectId,
giverProjectName: giver?.name,
giverProjectImage: giver?.image,
giverProjectHandleId: giver?.handleId,
giverDid: giver?.did,
}),
fromProjectId: fromProjectId,
toProjectId: toProjectId,
showProjects: (showProjects || false).toString(),
isFromProjectView: (isFromProjectView || false).toString(),
},
}"
>
<font-awesome
icon="circle-right"
class="text-blue-500 text-5xl mb-1"
/>
<h3
class="text-xs text-slate-500 font-medium italic text-ellipsis whitespace-nowrap overflow-hidden"
>
Show All
</h3>
</router-link>
</li>
</template>
</ul>
<button
class="block w-full text-center text-lg font-bold uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md"
@click="confirm"
>
Sign &amp; Send
</button>
<button
class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md"
class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-lg"
@click="cancel"
>
Cancel
</button>
</div>
<!-- Step 2: Gift -->
<div v-show="currentStep === 2" id="sectionGiftedGift">
<div class="grid grid-cols-2 gap-2 mb-4">
<!-- Giver Button -->
<button
v-if="
(giverEntityType === 'person' || giverEntityType === 'project') &&
!(isFromProjectView && giverEntityType === 'project')
"
class="flex-1 flex items-center gap-2 bg-slate-100 border border-slate-300 rounded-md p-2"
@click="goBackToStep1('giver')"
>
<div>
<template v-if="giverEntityType === 'project'">
<ProjectIcon
v-if="giver?.handleId"
:entity-id="giver.handleId"
:icon-size="32"
:image-url="giver.image"
class="rounded-full bg-white overflow-hidden !size-[2rem] object-cover"
/>
</template>
<template v-else>
<EntityIcon
v-if="giver?.did"
:contact="giver"
class="rounded-full bg-white overflow-hidden !size-[2rem] object-cover"
/>
<font-awesome
v-else
icon="circle-question"
class="text-slate-400 text-3xl"
/>
</template>
</div>
<div class="text-start min-w-0">
<p class="text-xs text-slate-500 leading-1 -mb-1 uppercase">
{{
giverEntityType === "project"
? "Benefited from:"
: "Received from:"
}}
</p>
<h3 class="font-semibold truncate">
{{ giver?.name || "Unnamed" }}
</h3>
</div>
<p class="ms-auto text-sm text-blue-500 pe-1">
<font-awesome icon="pen" title="Change" />
</p>
</button>
<div
v-else
class="flex-1 flex items-center gap-2 bg-slate-100 border border-slate-300 rounded-md p-2"
>
<div>
<template v-if="giverEntityType === 'project'">
<ProjectIcon
v-if="giver?.handleId"
:entity-id="giver.handleId"
:icon-size="32"
:image-url="giver.image"
class="rounded-full bg-white overflow-hidden !size-[2rem] object-cover"
/>
</template>
<template v-else>
<EntityIcon
v-if="giver?.did"
:contact="giver"
class="rounded-full bg-white overflow-hidden !size-[2rem] object-cover"
/>
<font-awesome
v-else
icon="circle-question"
class="text-slate-400 text-3xl"
/>
</template>
</div>
<div class="text-start min-w-0">
<p class="text-xs text-slate-500 leading-1 -mb-1 uppercase">
{{
giverEntityType === "project"
? "Benefited from:"
: "Received from:"
}}
</p>
<h3 class="font-semibold truncate">
{{ giver?.name || "Unnamed" }}
</h3>
</div>
<p class="ms-auto text-sm text-slate-400 pe-1">
<font-awesome icon="lock" title="Can't be changed" />
</p>
</div>
<!-- Recipient Button -->
<button
v-if="recipientEntityType === 'person'"
class="flex-1 flex items-center gap-2 bg-slate-100 border border-slate-300 rounded-md p-2"
@click="goBackToStep1('recipient')"
>
<div>
<EntityIcon
v-if="receiver?.did"
:contact="receiver"
class="rounded-full bg-white overflow-hidden !size-[2rem] object-cover"
/>
<font-awesome
v-else
icon="circle-question"
class="text-slate-400 text-3xl"
/>
</div>
<div class="text-start min-w-0">
<p class="text-xs text-slate-500 leading-1 -mb-1 uppercase">
Given to:
</p>
<h3 class="font-semibold truncate">
{{ receiver?.name || "Unnamed" }}
</h3>
</div>
<p class="ms-auto text-sm text-blue-500 pe-1">
<font-awesome icon="pen" title="Change" />
</p>
</button>
<div
v-else-if="recipientEntityType === 'project'"
class="flex-1 flex items-center gap-2 bg-slate-100 border border-slate-300 rounded-md p-2"
>
<div>
<ProjectIcon
v-if="receiver?.handleId"
:entity-id="receiver.handleId"
:icon-size="32"
:image-url="receiver.image"
class="rounded-full bg-white overflow-hidden !size-[2rem] object-cover"
/>
</div>
<div class="text-start min-w-0">
<p class="text-xs text-slate-500 leading-1 -mb-1 uppercase">
Given to project:
</p>
<h3 class="font-semibold truncate">
{{ receiver?.name || "Unnamed" }}
</h3>
</div>
<p class="ms-auto text-sm text-slate-400 pe-1">
<font-awesome icon="lock" title="Can't be changed" />
</p>
</div>
</div>
<input
v-model="description"
type="text"
class="block w-full rounded border border-slate-400 px-3 py-2 mb-4 placeholder:italic"
:placeholder="prompt || 'What was given?'"
/>
<div class="flex mb-4">
<button
class="rounded-s border border-e-0 border-slate-400 bg-slate-200 px-4 py-2"
@click="amountInput === '0' ? null : decrement()"
>
<font-awesome icon="chevron-left" />
</button>
<input
id="inputGivenAmount"
v-model="amountInput"
type="number"
class="flex-1 border border-e-0 border-slate-400 px-2 py-2 text-center w-[1px]"
/>
<button
class="rounded-e border border-slate-400 bg-slate-200 px-4 py-2"
@click="increment()"
>
<font-awesome icon="chevron-right" />
</button>
<select
v-model="unitCode"
class="flex-1 rounded border border-slate-400 ms-2 px-3 py-2"
>
<option value="HUR">Hours</option>
<option value="USD">US $</option>
<option value="BTC">BTC</option>
<option value="BX">BX</option>
<option value="ETH">ETH</option>
</select>
</div>
<router-link
:to="{
name: 'gifted-details',
query: giftedDetailsQuery,
}"
class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-lg mb-4"
>
Photo &amp; more options&hellip;
</router-link>
<p class="text-center text-sm mb-4">
<b class="font-medium">Sign &amp; Send</b> to publish to the world
<font-awesome
icon="circle-info"
class="fa-fw text-blue-500 text-base cursor-pointer"
@click="explainData()"
/>
</p>
<!-- Conflict warning -->
<div v-if="hasPersonConflict" class="mb-4 p-3 bg-red-50 border border-red-200 rounded-md">
<p class="text-red-700 text-sm text-center">
<font-awesome icon="exclamation-triangle" class="fa-fw mr-1" />
Cannot record: Same person selected as both giver and recipient
</p>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
<button
:disabled="hasPersonConflict"
:class="{
'block w-full text-center text-md uppercase font-bold bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-lg': !hasPersonConflict,
'block w-full text-center text-md uppercase font-bold bg-gradient-to-b from-slate-300 to-slate-500 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-slate-400 px-1.5 py-2 rounded-lg cursor-not-allowed': hasPersonConflict
}"
@click="confirm"
>
Sign &amp; Send
</button>
<button
class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-lg"
@click="cancel"
>
Cancel
</button>
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import { Vue, Component, Prop } from "vue-facing-decorator";
import { Vue, Component, Prop, Watch } from "vue-facing-decorator";
import { NotificationIface, USE_DEXIE_DB } from "../constants/app";
import {
createAndSubmitGive,
didInfo,
serverMessageForUser,
getHeaders,
} from "../libs/endorserServer";
import * as libsUtil from "../libs/util";
import { db, retrieveSettingsForActiveAccount } from "../db/index";
@@ -102,13 +480,38 @@ import * as databaseUtil from "../db/databaseUtil";
import { retrieveAccountDids } from "../libs/util";
import { logger } from "../utils/logger";
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
import EntityIcon from "../components/EntityIcon.vue";
import ProjectIcon from "../components/ProjectIcon.vue";
import { PlanData } from "../interfaces/records";
@Component
@Component({
components: {
EntityIcon,
ProjectIcon,
},
})
export default class GiftedDialog extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
@Prop() fromProjectId = "";
@Prop() toProjectId = "";
@Prop({ default: false }) showProjects = false;
@Prop() isFromProjectView = false;
@Watch("showProjects")
onShowProjectsChange() {
this.updateEntityTypes();
}
@Watch("fromProjectId")
onFromProjectIdChange() {
this.updateEntityTypes();
}
@Watch("toProjectId")
onToProjectIdChange() {
this.updateEntityTypes();
}
activeDid = "";
allContacts: Array<Contact> = [];
@@ -125,9 +528,84 @@ export default class GiftedDialog extends Vue {
receiver?: libsUtil.GiverReceiverInputInfo;
unitCode = "HUR";
visible = false;
currentStep = 1;
libsUtil = libsUtil;
projects: PlanData[] = [];
didInfo = didInfo;
// Computed property to help debug template logic
get shouldShowProjects() {
const result =
(this.stepType === "giver" && this.giverEntityType === "project") ||
(this.stepType === "recipient" && this.recipientEntityType === "project");
return result;
}
// Computed property to check if current selection would create a conflict
get hasPersonConflict() {
// Only check for conflicts when both entities are persons
if (this.giverEntityType !== "person" || this.recipientEntityType !== "person") {
return false;
}
// Check if giver and recipient are the same person
if (this.giver?.did && this.receiver?.did && this.giver.did === this.receiver.did) {
return true;
}
return false;
}
// Computed property to check if a contact would create a conflict when selected
wouldCreateConflict(contactDid: string) {
// Only check for conflicts when both entities are persons
if (this.giverEntityType !== "person" || this.recipientEntityType !== "person") {
return false;
}
if (this.stepType === "giver") {
// If selecting as giver, check if it conflicts with current recipient
return this.receiver?.did === contactDid;
} else if (this.stepType === "recipient") {
// If selecting as recipient, check if it conflicts with current giver
return this.giver?.did === contactDid;
}
return false;
}
stepType = "giver";
giverEntityType = "person" as "person" | "project";
recipientEntityType = "person" as "person" | "project";
updateEntityTypes() {
// Reset and set entity types based on current context
this.giverEntityType = "person";
this.recipientEntityType = "person";
// Determine entity types based on current context
if (this.showProjects) {
// HomeView "Project" button or ProjectViewView "Given by This"
this.giverEntityType = "project";
this.recipientEntityType = "person";
} else if (this.fromProjectId) {
// ProjectViewView "Given by This" button (project is giver)
this.giverEntityType = "project";
this.recipientEntityType = "person";
} else if (this.toProjectId) {
// ProjectViewView "Given to This" button (project is recipient)
this.giverEntityType = "person";
this.recipientEntityType = "project";
} else {
// HomeView "Person" button
this.giverEntityType = "person";
this.recipientEntityType = "person";
}
}
async open(
giver?: libsUtil.GiverReceiverInputInfo,
receiver?: libsUtil.GiverReceiverInputInfo,
@@ -140,10 +618,14 @@ export default class GiftedDialog extends Vue {
this.giver = giver;
this.prompt = prompt || "";
this.receiver = receiver;
// if we show "given to user" selection, default checkbox to true
this.amountInput = "0";
this.callbackOnSuccess = callbackOnSuccess;
this.offerId = offerId || "";
this.currentStep = giver ? 2 : 1;
this.stepType = "giver";
// Update entity types based on current props
this.updateEntityTypes();
try {
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
@@ -174,7 +656,16 @@ export default class GiftedDialog extends Vue {
this.allContacts,
);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if (
this.giverEntityType === "project" ||
this.recipientEntityType === "project"
) {
await this.loadProjects();
} else {
// Clear projects array when not needed
this.projects = [];
}
} catch (err: any) {
logger.error("Error retrieving settings from database:", err);
this.$notify(
@@ -224,6 +715,7 @@ export default class GiftedDialog extends Vue {
this.amountInput = "0";
this.prompt = "";
this.unitCode = "HUR";
this.currentStep = 1;
}
async confirm() {
@@ -265,6 +757,20 @@ export default class GiftedDialog extends Vue {
);
return;
}
// Check for person conflict
if (this.hasPersonConflict) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "You cannot select the same person as both giver and recipient.",
},
3000,
);
return;
}
this.close();
this.$notify(
@@ -304,20 +810,46 @@ export default class GiftedDialog extends Vue {
unitCode: string = "HUR",
) {
try {
// Determine the correct parameters based on entity types
let fromDid: string | undefined;
let toDid: string | undefined;
let fulfillsProjectHandleId: string | undefined;
let providerPlanHandleId: string | undefined;
if (this.giverEntityType === "project" && this.recipientEntityType === "person") {
// Project-to-person gift
fromDid = undefined; // No person giver
toDid = recipientDid as string; // Person recipient
fulfillsProjectHandleId = undefined; // No project recipient
providerPlanHandleId = this.giver?.handleId; // Project giver
} else if (this.giverEntityType === "person" && this.recipientEntityType === "project") {
// Person-to-project gift
fromDid = giverDid as string; // Person giver
toDid = undefined; // No person recipient
fulfillsProjectHandleId = this.toProjectId; // Project recipient
providerPlanHandleId = undefined; // No project giver
} else {
// Person-to-person gift
fromDid = giverDid as string;
toDid = recipientDid as string;
fulfillsProjectHandleId = undefined;
providerPlanHandleId = undefined;
}
const result = await createAndSubmitGive(
this.axios,
this.apiServer,
this.activeDid,
giverDid as string,
recipientDid as string,
fromDid,
toDid,
description,
amount,
unitCode,
this.toProjectId,
fulfillsProjectHandleId,
this.offerId,
false,
undefined,
this.fromProjectId,
providerPlanHandleId,
);
if (!result.success) {
@@ -391,6 +923,114 @@ export default class GiftedDialog extends Vue {
-1,
);
}
selectGiver(contact?: Contact) {
if (contact) {
this.giver = {
did: contact.did,
name: contact.name || contact.did,
};
} else {
this.giver = {
did: "",
name: "Unnamed",
};
}
this.currentStep = 2;
}
goBackToStep1(step: string) {
this.stepType = step;
this.currentStep = 1;
}
async loadProjects() {
try {
const response = await fetch(this.apiServer + "/api/v2/report/plans", {
method: "GET",
headers: await getHeaders(this.activeDid),
});
if (response.status !== 200) {
throw new Error("Failed to load projects");
}
const results = await response.json();
if (results.data) {
this.projects = results.data;
}
} catch (error) {
logger.error("Error loading projects:", error);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "Failed to load projects",
},
3000,
);
}
}
selectProject(project: PlanData) {
this.giver = {
did: project.handleId,
name: project.name,
image: project.image,
handleId: project.handleId,
};
this.receiver = {
did: this.activeDid,
name: "You",
};
this.currentStep = 2;
}
selectRecipient(contact?: Contact) {
if (contact) {
this.receiver = {
did: contact.did,
name: contact.name || contact.did,
};
} else {
this.receiver = {
did: "",
name: "Unnamed",
};
}
this.currentStep = 2;
}
selectRecipientProject(project: PlanData) {
this.receiver = {
did: project.handleId,
name: project.name,
image: project.image,
handleId: project.handleId,
};
this.currentStep = 2;
}
// Computed property for the query parameters
get giftedDetailsQuery() {
return {
amountInput: this.amountInput,
description: this.description,
giverDid: this.giverEntityType === "person" ? this.giver?.did : undefined,
giverName: this.giver?.name,
offerId: this.offerId,
fulfillsProjectId: this.giverEntityType === "person" && this.recipientEntityType === "project"
? this.toProjectId
: undefined,
providerProjectId: this.giverEntityType === "project" && this.recipientEntityType === "person"
? this.giver?.handleId
: this.fromProjectId,
recipientDid: this.receiver?.did,
recipientName: this.receiver?.name,
unitCode: this.unitCode,
};
}
}
</script>

View File

@@ -268,7 +268,7 @@ import {
} from "../constants/app";
import { retrieveSettingsForActiveAccount } from "../db/index";
import { accessToken } from "../libs/crypto";
import { logger, getTimestampForFilename } from "../utils/logger";
import { logger } from "../utils/logger";
import { PlatformServiceFactory } from "../services/PlatformServiceFactory";
import * as databaseUtil from "../db/databaseUtil";
@@ -576,7 +576,7 @@ export default class ImageMethodDialog extends Vue {
(blob) => {
if (blob) {
this.blob = blob;
this.fileName = `photo-${getTimestampForFilename()}.jpg`;
this.fileName = `photo_${Date.now()}.jpg`;
this.showRetry = true;
this.stopCameraPreview();
}

View File

@@ -127,7 +127,7 @@ import {
import * as databaseUtil from "../db/databaseUtil";
import { retrieveSettingsForActiveAccount } from "../db/index";
import { accessToken } from "../libs/crypto";
import { logger, getTimestampForFilename } from "../utils/logger";
import { logger } from "../utils/logger";
import { PlatformServiceFactory } from "../services/PlatformServiceFactory";
@Component({ components: { VuePictureCropper } })
@@ -393,7 +393,7 @@ export default class PhotoDialog extends Vue {
(blob) => {
if (blob) {
this.blob = blob;
this.fileName = `photo-${getTimestampForFilename()}.jpg`;
this.fileName = `photo_${Date.now()}.jpg`;
this.stopCameraPreview();
}
},

View File

@@ -12,14 +12,6 @@ import type { PluginListenerHandle } from "@capacitor/core";
* Supports 'backButton' and 'appUrlOpen' events from Capacitor
*/
interface AppInterface {
/**
* Force exit the app. This should only be used in conjunction with the `backButton` handler for Android to
* exit the app when navigation is complete.
*
* @returns Promise that resolves when the app has been exited
*/
exitApp(): Promise<void>;
/**
* Add listener for back button events
* @param eventName - Must be 'backButton'
@@ -46,19 +38,8 @@ interface AppInterface {
/**
* App wrapper for Capacitor functionality
* Provides type-safe event listeners for back button and URL open events
* and app exit functionality
*/
export const App: AppInterface = {
/**
* Force exit the app. This should only be used in conjunction with the `backButton` handler for Android to
* exit the app when navigation is complete.
*
* @returns Promise that resolves when the app has been exited
*/
exitApp(): Promise<void> {
return CapacitorApp.exitApp();
},
addListener(
eventName: "backButton" | "appUrlOpen",
listenerFunc: BackButtonListener | ((data: AppLaunchUrl) => void),

View File

@@ -29,6 +29,7 @@ import {
faCircleCheck,
faCircleInfo,
faCircleQuestion,
faCircleRight,
faCircleUser,
faClock,
faCoins,
@@ -60,6 +61,7 @@ import {
faLightbulb,
faLink,
faLocationDot,
faLock,
faLongArrowAltLeft,
faLongArrowAltRight,
faMagnifyingGlass,
@@ -79,6 +81,7 @@ import {
faSquareCaretDown,
faSquareCaretUp,
faSquarePlus,
faThumbtack,
faTrashCan,
faTriangleExclamation,
faUser,
@@ -111,6 +114,7 @@ library.add(
faCircleCheck,
faCircleInfo,
faCircleQuestion,
faCircleRight,
faCircleUser,
faClock,
faCoins,
@@ -142,6 +146,7 @@ library.add(
faLightbulb,
faLink,
faLocationDot,
faLock,
faLongArrowAltLeft,
faLongArrowAltRight,
faMagnifyingGlass,
@@ -161,6 +166,7 @@ library.add(
faSquareCaretDown,
faSquareCaretUp,
faSquarePlus,
faThumbtack,
faTrashCan,
faTriangleExclamation,
faUser,

View File

@@ -49,6 +49,8 @@ import { insertDidSpecificSettings, parseJsonField } from "../db/databaseUtil";
export interface GiverReceiverInputInfo {
did?: string;
name?: string;
image?: string;
handleId?: string;
}
export enum OnboardPage {

View File

@@ -65,21 +65,9 @@ export interface PlatformService {
* Writes content to a file at the specified path and shares it.
* @param fileName - The filename of the file to write
* @param content - The content to write to the file
* @param options - Optional parameters for file saving behavior
* @returns Promise that resolves to save/share result
* @returns Promise that resolves when the write is complete
*/
writeAndShareFile(
fileName: string,
content: string,
options?: {
allowLocationSelection?: boolean;
saveToDownloads?: boolean;
saveToPrivateStorage?: boolean;
mimeType?: string;
showShareDialog?: boolean;
showLocationSelectionDialog?: boolean;
},
): Promise<{ saved: boolean; uri?: string; shared: boolean; error?: string }>;
writeAndShareFile(fileName: string, content: string): Promise<void>;
/**
* Deletes a file at the specified path.
@@ -95,48 +83,6 @@ export interface PlatformService {
*/
listFiles(directory: string): Promise<string[]>;
/**
* Tests the file sharing functionality by creating and sharing a test file.
* @returns Promise resolving to a test result message
*/
testFileSharing(): Promise<string>;
/**
* Tests saving a file without showing the share dialog.
* @returns Promise resolving to a test result message
*/
testFileSaveOnly(): Promise<string>;
/**
* Tests the location selection functionality using the file picker.
* @returns Promise resolving to a test result message
*/
testLocationSelection(): Promise<string>;
/**
* Tests location selection without showing the dialog (restores original behavior).
* @returns Promise resolving to a test result message
*/
testLocationSelectionSilent(): Promise<string>;
/**
* Tests listing user-accessible files saved by the app.
* @returns Promise resolving to a test result message
*/
testListUserFiles(): Promise<string>;
/**
* Tests listing backup files specifically saved by the app.
* @returns Promise resolving to a test result message
*/
testBackupFiles(): Promise<string>;
/**
* Tests opening the backup directory in the device's file explorer.
* @returns Promise resolving to a test result message
*/
testOpenBackupDirectory(): Promise<string>;
// Camera operations
/**
* Activates the device camera to take a picture.
@@ -184,92 +130,4 @@ export interface PlatformService {
sql: string,
params?: unknown[],
): Promise<{ changes: number; lastId?: number }>;
/**
* Lists user-accessible files saved by the app.
* Returns files from Downloads (Android) or Documents (iOS) directories.
* @returns Promise resolving to array of file information
*/
listUserAccessibleFiles(): Promise<
Array<{ name: string; uri: string; size?: number }>
>;
/**
* Lists backup files specifically saved by the app.
* Filters for files that appear to be TimeSafari backups.
* @returns Promise resolving to array of backup file information
*/
listBackupFiles(): Promise<
Array<{
name: string;
uri: string;
size?: number;
type: "contacts" | "seed" | "other";
path?: string;
}>
>;
/**
* Opens a file in the device's default file viewer/app.
* Uses the native share dialog to provide options for opening the file.
* @param fileUri - URI of the file to open
* @param fileName - Name of the file (for display purposes)
* @returns Promise resolving to success status
*/
openFile(
fileUri: string,
fileName: string,
): Promise<{ success: boolean; error?: string }>;
/**
* Opens the directory containing backup files in the device's file explorer.
* Uses the native share dialog to provide options for accessing the directory.
* @returns Promise resolving to success status
*/
openBackupDirectory(): Promise<{ success: boolean; error?: string }>;
/**
* Creates a test backup file to verify file writing and reading functionality.
* This is useful for debugging file visibility issues.
* @returns Promise resolving to success status and file information
*/
createTestBackupFile(): Promise<{
success: boolean;
fileName?: string;
uri?: string;
error?: string;
}>;
/**
* Tests different directory contexts to see what files are available.
* This helps debug file visibility issues across different storage contexts.
* @returns Promise resolving to debug information about file discovery across contexts
*/
testDirectoryContexts(): Promise<string>;
/**
* Lists files and folders in a specific directory for directory browsing
* @param path - The directory path to list
* @param debugShowAll - Debug flag to treat all entries as files
* @returns Promise resolving to array of directory entries
*/
listFilesInDirectory(
path: string,
debugShowAll?: boolean,
): Promise<
Array<{
name: string;
uri: string;
size?: number;
path: string;
type: "file" | "folder";
}>
>;
/**
* Debug method to check what's actually in the TimeSafari directory
* This helps identify if the directory exists but is empty or has permission issues
* @returns Promise resolving to debug information about the TimeSafari directory
*/
debugTimeSafariDirectory(): Promise<string>;
}

File diff suppressed because it is too large Load Diff

View File

@@ -205,7 +205,6 @@ export class ElectronPlatformService implements PlatformService {
isIOS: false,
hasFileDownload: false, // Not implemented yet
needsFileHandlingInstructions: false,
isNativeApp: true,
};
}
@@ -235,32 +234,11 @@ export class ElectronPlatformService implements PlatformService {
* Writes content to a file and opens the system share dialog.
* @param _fileName - Name of the file to create
* @param _content - Content to write to the file
* @param _options - Options for file saving behavior
* @throws Error with "Not implemented" message
* @todo Implement using Electron's dialog and file system APIs
*/
async writeAndShareFile(
_fileName: string,
_content: string,
_options?: {
allowLocationSelection?: boolean;
saveToDownloads?: boolean;
saveToPrivateStorage?: boolean;
mimeType?: string;
showShareDialog?: boolean;
showLocationSelectionDialog?: boolean;
},
): Promise<{
saved: boolean;
uri?: string;
shared: boolean;
error?: string;
}> {
return {
saved: false,
shared: false,
error: "Not implemented in Electron platform",
};
async writeAndShareFile(_fileName: string, _content: string): Promise<void> {
throw new Error("Not implemented");
}
/**
@@ -306,17 +284,6 @@ export class ElectronPlatformService implements PlatformService {
throw new Error("Not implemented");
}
/**
* Should rotate the camera between front and back cameras.
* @returns Promise that resolves when the camera is rotated
* @throws Error with "Not implemented" message
* @todo Implement camera rotation using Electron's media APIs
*/
async rotateCamera(): Promise<void> {
logger.error("rotateCamera not implemented in Electron platform");
throw new Error("Not implemented");
}
/**
* Should handle deep link URLs for the desktop application.
* @param _url - The deep link URL to handle
@@ -378,173 +345,4 @@ export class ElectronPlatformService implements PlatformService {
);
}
}
/**
* Tests the file sharing functionality.
* @returns Promise resolving to a test result message
*/
async testFileSharing(): Promise<string> {
return "File sharing not available in Electron platform - not implemented";
}
/**
* Tests saving a file without showing the share dialog.
* @returns Promise resolving to a test result message
*/
async testFileSaveOnly(): Promise<string> {
return "File save only not available in Electron platform - not implemented";
}
/**
* Tests the location selection functionality using the file picker.
* @returns Promise resolving to a test result message
*/
async testLocationSelection(): Promise<string> {
return "Location selection not available in Electron platform - not implemented";
}
/**
* Tests location selection without showing the dialog (restores original behavior).
* @returns Promise resolving to a test result message
*/
async testLocationSelectionSilent(): Promise<string> {
return "Location selection not available in Electron platform - not implemented";
}
/**
* Tests listing user-accessible files saved by the app.
* @returns Promise resolving to a test result message
*/
async testListUserFiles(): Promise<string> {
return "File listing not available in Electron platform - not implemented";
}
/**
* Tests listing backup files specifically saved by the app.
* @returns Promise resolving to a test result message
*/
async testBackupFiles(): Promise<string> {
return "Backup file listing not available in Electron platform - not implemented";
}
/**
* Tests opening the backup directory in the device's file explorer.
* @returns Promise resolving to a test result message
*/
async testOpenBackupDirectory(): Promise<string> {
return "Directory access not available in Electron platform - not implemented";
}
/**
* Lists user-accessible files saved by the app.
* Not implemented in Electron platform.
* @returns Promise resolving to empty array
*/
async listUserAccessibleFiles(): Promise<
Array<{ name: string; uri: string; size?: number }>
> {
return [];
}
/**
* Lists backup files specifically saved by the app.
* Not implemented for Electron platform.
* @returns Promise resolving to empty array
*/
async listBackupFiles(): Promise<
Array<{
name: string;
uri: string;
size?: number;
type: "contacts" | "seed" | "other";
path?: string;
}>
> {
return [];
}
/**
* Opens a file in the device's default file viewer/app.
* Not implemented in Electron platform.
* @param _fileUri - URI of the file to open
* @param _fileName - Name of the file (for display purposes)
* @returns Promise resolving to error status
*/
async openFile(
_fileUri: string,
_fileName: string,
): Promise<{ success: boolean; error?: string }> {
return {
success: false,
error: "File opening not implemented in Electron platform",
};
}
/**
* Opens the directory containing backup files in the device's file explorer.
* Not implemented in Electron platform.
* @returns Promise resolving to error status
*/
async openBackupDirectory(): Promise<{ success: boolean; error?: string }> {
return {
success: false,
error: "Directory access not implemented in Electron platform",
};
}
/**
* Lists files and folders in a specific directory for directory browsing.
* Not implemented for Electron platform.
* @returns Promise resolving to empty array
*/
async listFilesInDirectory(
_path: string,
_debugShowAll?: boolean,
): Promise<
Array<{
name: string;
uri: string;
size?: number;
path: string;
type: "file" | "folder";
}>
> {
return [];
}
/**
* Debug method to check what's actually in the TimeSafari directory.
* Not implemented for Electron platform.
* @returns Promise resolving to debug information
*/
async debugTimeSafariDirectory(): Promise<string> {
return "Electron platform does not support file system access for debugging TimeSafari directory.";
}
/**
* Creates a test backup file to verify file writing and reading functionality.
* Not implemented for Electron platform.
* @returns Promise resolving to error status
*/
async createTestBackupFile(): Promise<{
success: boolean;
fileName?: string;
uri?: string;
error?: string;
}> {
return {
success: false,
error:
"Electron platform does not support file system access for creating test backup files.",
};
}
/**
* Test method to try different directory contexts and see what files are available.
* Not implemented for Electron platform.
* @returns Promise resolving to debug information
*/
async testDirectoryContexts(): Promise<string> {
return "Electron platform does not support file system access for testing directory contexts.";
}
}

View File

@@ -32,7 +32,6 @@ export class PyWebViewPlatformService implements PlatformService {
isIOS: false,
hasFileDownload: false, // Not implemented yet
needsFileHandlingInstructions: false,
isNativeApp: true,
};
}
@@ -123,211 +122,14 @@ export class PyWebViewPlatformService implements PlatformService {
}
/**
* Writes content to a file at the specified path and shares it.
* Not implemented in PyWebView platform.
* @param _fileName - The filename of the file to write
* @param _content - The content to write to the file
* @param _options - Optional parameters for file saving behavior
* @returns Promise that resolves to save/share result
* Should write and share a file using the Python backend.
* @param _fileName - Name of the file to write and share
* @param _content - Content to write to the file
* @throws Error with "Not implemented" message
* @todo Implement file writing and sharing through pywebview's Python-JavaScript bridge
*/
async writeAndShareFile(
_fileName: string,
_content: string,
_options?: {
allowLocationSelection?: boolean;
saveToDownloads?: boolean;
saveToPrivateStorage?: boolean;
mimeType?: string;
showShareDialog?: boolean;
showLocationSelectionDialog?: boolean;
},
): Promise<{
saved: boolean;
uri?: string;
shared: boolean;
error?: string;
}> {
return {
saved: false,
shared: false,
error: "File sharing not implemented in PyWebView platform",
};
}
/**
* Lists user-accessible files saved by the app.
* Not implemented in PyWebView platform.
* @returns Promise resolving to empty array
*/
async listUserAccessibleFiles(): Promise<
Array<{ name: string; uri: string; size?: number }>
> {
return [];
}
/**
* Lists backup files specifically saved by the app.
* Not implemented for PyWebView platform.
* @returns Promise resolving to empty array
*/
async listBackupFiles(): Promise<
Array<{
name: string;
uri: string;
size?: number;
type: "contacts" | "seed" | "other";
path?: string;
}>
> {
return [];
}
/**
* Opens a file in the device's default file viewer/app.
* Not implemented in PyWebView platform.
* @param _fileUri - URI of the file to open
* @param _fileName - Name of the file (for display purposes)
* @returns Promise resolving to error status
*/
async openFile(
_fileUri: string,
_fileName: string,
): Promise<{ success: boolean; error?: string }> {
return {
success: false,
error: "File opening not implemented in PyWebView platform",
};
}
/**
* Opens the directory containing backup files in the device's file explorer.
* Not implemented in PyWebView platform.
* @returns Promise resolving to error status
*/
async openBackupDirectory(): Promise<{ success: boolean; error?: string }> {
return {
success: false,
error: "Directory access not implemented in PyWebView platform",
};
}
/**
* Tests listing user-accessible files saved by the app.
* @returns Promise resolving to a test result message
*/
async testListUserFiles(): Promise<string> {
return "File listing not available in PyWebView platform - not implemented";
}
/**
* Tests listing backup files specifically saved by the app.
* @returns Promise resolving to a test result message
*/
async testBackupFiles(): Promise<string> {
return "Backup file listing not available in PyWebView platform - not implemented";
}
/**
* Tests opening the backup directory in the device's file explorer.
* @returns Promise resolving to a test result message
*/
async testOpenBackupDirectory(): Promise<string> {
return "Directory access not available in PyWebView platform - not implemented";
}
/**
* Tests the file sharing functionality.
* @returns Promise resolving to a test result message
*/
async testFileSharing(): Promise<string> {
return "File sharing not available in PyWebView platform - not implemented";
}
/**
* Tests saving a file without showing the share dialog.
* @returns Promise resolving to a test result message
*/
async testFileSaveOnly(): Promise<string> {
return "File saving not available in PyWebView platform - not implemented";
}
/**
* Tests the location selection functionality.
* @returns Promise resolving to a test result message
*/
async testLocationSelection(): Promise<string> {
return "Location selection not available in PyWebView platform - not implemented";
}
/**
* Tests location selection without showing the dialog.
* @returns Promise resolving to a test result message
*/
async testLocationSelectionSilent(): Promise<string> {
return "Silent location selection not available in PyWebView platform - not implemented";
}
/**
* Rotates the camera between front and back.
* Not implemented in PyWebView platform.
*/
async rotateCamera(): Promise<void> {
// Not implemented
}
/**
* Lists files and folders in a specific directory for directory browsing.
* Not implemented for PyWebView platform.
* @returns Promise resolving to empty array
*/
async listFilesInDirectory(
_path: string,
_debugShowAll?: boolean,
): Promise<
Array<{
name: string;
uri: string;
size?: number;
path: string;
type: "file" | "folder";
}>
> {
return [];
}
/**
* Debug method to check what's actually in the TimeSafari directory.
* Not implemented for PyWebView platform.
* @returns Promise resolving to debug information
*/
async debugTimeSafariDirectory(): Promise<string> {
return "PyWebView platform does not support file system access for debugging TimeSafari directory.";
}
/**
* Creates a test backup file to verify file writing and reading functionality.
* Not implemented for PyWebView platform.
* @returns Promise resolving to error status
*/
async createTestBackupFile(): Promise<{
success: boolean;
fileName?: string;
uri?: string;
error?: string;
}> {
return {
success: false,
error:
"PyWebView platform does not support file system access for creating test backup files.",
};
}
/**
* Test method to try different directory contexts and see what files are available.
* Not implemented for PyWebView platform.
* @returns Promise resolving to debug information
*/
async testDirectoryContexts(): Promise<string> {
return "PyWebView platform does not support file system access for testing directory contexts.";
async writeAndShareFile(_fileName: string, _content: string): Promise<void> {
logger.error("writeAndShareFile not implemented in PyWebView platform");
throw new Error("Not implemented");
}
}

View File

@@ -3,7 +3,7 @@ import {
PlatformService,
PlatformCapabilities,
} from "../PlatformService";
import { logger, getTimestampForFilename } from "../../utils/logger";
import { logger } from "../../utils/logger";
import { QueryExecResult } from "@/interfaces/database";
import databaseService from "../AbsurdSqlDatabaseService";
@@ -29,14 +29,10 @@ export class WebPlatformService implements PlatformService {
return {
hasFileSystem: false,
hasCamera: true, // Through file input with capture
isMobile:
/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
navigator.userAgent,
),
isMobile: /iPhone|iPad|iPod|Android/i.test(navigator.userAgent),
isIOS: /iPad|iPhone|iPod/.test(navigator.userAgent),
hasFileDownload: true,
needsFileHandlingInstructions: false,
isNativeApp: false,
};
}
@@ -194,7 +190,7 @@ export class WebPlatformService implements PlatformService {
if (blob) {
resolve({
blob,
fileName: `photo-${getTimestampForFilename()}.jpg`,
fileName: `photo_${Date.now()}.jpg`,
});
} else {
reject(new Error("Failed to capture image from webcam"));
@@ -360,31 +356,10 @@ export class WebPlatformService implements PlatformService {
* Not supported in web platform.
* @param _fileName - Unused fileName parameter
* @param _content - Unused content parameter
* @param _options - Unused options parameter
* @returns Promise that resolves to a failure result
* @throws Error indicating file system access is not available
*/
async writeAndShareFile(
_fileName: string,
_content: string,
_options?: {
allowLocationSelection?: boolean;
saveToDownloads?: boolean;
saveToPrivateStorage?: boolean;
mimeType?: string;
showShareDialog?: boolean;
showLocationSelectionDialog?: boolean;
},
): Promise<{
saved: boolean;
uri?: string;
shared: boolean;
error?: string;
}> {
return {
saved: false,
shared: false,
error: "File system access not available in web platform",
};
async writeAndShareFile(_fileName: string, _content: string): Promise<void> {
throw new Error("File system access not available in web platform");
}
/**
@@ -415,183 +390,4 @@ export class WebPlatformService implements PlatformService {
.query(sql, params)
.then((result: QueryExecResult[]) => result[0]?.values[0]);
}
/**
* Tests the file sharing functionality.
* @returns Promise resolving to a test result message
*/
async testFileSharing(): Promise<string> {
return "File sharing not available in web platform - use download instead";
}
/**
* Tests saving a file without showing the share dialog.
* @returns Promise resolving to a test result message
*/
async testFileSaveOnly(): Promise<string> {
return "File saving not available in web platform - use download instead";
}
/**
* Tests the location selection functionality using the file picker.
* @returns Promise resolving to a test result message
*/
async testLocationSelection(): Promise<string> {
return "Location selection not available in web platform - use download instead";
}
/**
* Tests location selection without showing the dialog (restores original behavior).
* @returns Promise resolving to a test result message
*/
async testLocationSelectionSilent(): Promise<string> {
return "Location selection not available in web platform - use download instead";
}
/**
* Tests listing user-accessible files saved by the app.
* @returns Promise resolving to a test result message
*/
async testListUserFiles(): Promise<string> {
return "File listing not available in web platform - files are downloaded directly";
}
/**
* Tests listing backup files specifically saved by the app.
* @returns Promise resolving to a test result message
*/
async testBackupFiles(): Promise<string> {
return "Backup file listing not available in web platform - files are downloaded directly";
}
/**
* Tests opening the backup directory in the device's file explorer.
* @returns Promise resolving to a test result message
*/
async testOpenBackupDirectory(): Promise<string> {
return "Directory access not available in web platform - files are downloaded directly";
}
/**
* Lists user-accessible files saved by the app.
* Not supported in web platform.
* @returns Promise resolving to empty array
*/
async listUserAccessibleFiles(): Promise<
Array<{ name: string; uri: string; size?: number }>
> {
return [];
}
/**
* Lists backup files specifically saved by the app.
* Not supported in web platform.
* @returns Promise resolving to empty array
*/
async listBackupFiles(): Promise<
Array<{
name: string;
uri: string;
size?: number;
type: "contacts" | "seed" | "other";
path?: string;
}>
> {
return [];
}
/**
* Opens a file in the device's default file viewer/app.
* Not supported in web platform.
* @param _fileUri - URI of the file to open
* @param _fileName - Name of the file (for display purposes)
* @returns Promise resolving to error status
*/
async openFile(
_fileUri: string,
_fileName: string,
): Promise<{ success: boolean; error?: string }> {
return {
success: false,
error: "File opening not available in web platform",
};
}
/**
* Opens the directory containing backup files in the device's file explorer.
* Not supported in web platform.
* @returns Promise resolving to error status
*/
async openBackupDirectory(): Promise<{ success: boolean; error?: string }> {
return {
success: false,
error: "Directory access not available in web platform",
};
}
/**
* Rotates the camera between front and back cameras.
* Not supported in web platform.
* @returns Promise that resolves immediately
*/
async rotateCamera(): Promise<void> {
// Not supported in web platform
return Promise.resolve();
}
/**
* Lists files and folders in a specific directory for directory browsing.
* Not supported in web platform.
* @returns Promise resolving to empty array
*/
async listFilesInDirectory(
_path: string,
_debugShowAll?: boolean,
): Promise<
Array<{
name: string;
uri: string;
size?: number;
path: string;
type: "file" | "folder";
}>
> {
return [];
}
/**
* Debug method to check what's actually in the TimeSafari directory.
* Not supported in web platform.
* @returns Promise resolving to debug information
*/
async debugTimeSafariDirectory(): Promise<string> {
return "Web platform does not support file system access for debugging TimeSafari directory.";
}
/**
* Creates a test backup file to verify file writing and reading functionality.
* Not supported in web platform.
* @returns Promise resolving to error status
*/
async createTestBackupFile(): Promise<{
success: boolean;
fileName?: string;
uri?: string;
error?: string;
}> {
return {
success: false,
error:
"Web platform does not support file system access for creating test backup files.",
};
}
/**
* Test method to try different directory contexts and see what files are available.
* Not supported in web platform.
* @returns Promise resolving to debug information
*/
async testDirectoryContexts(): Promise<string> {
return "Web platform does not support file system access for testing directory contexts.";
}
}

View File

@@ -79,22 +79,3 @@ if (typeof module !== "undefined" && module.exports) {
// Add default export for ESM
export default { logger };
/**
* Formats current timestamp for use in filenames.
* Returns ISO string with colons and periods replaced with hyphens, truncated to seconds.
* Format: 2024-01-15T14-30-45
* @returns Formatted timestamp string safe for filenames
*/
export function getTimestampForFilename(): string {
return new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
}
/**
* Formats current timestamp for use in filenames with date only.
* Format: 2024-01-15
* @returns Date-only timestamp string safe for filenames
*/
export function getDateForFilename(): string {
return new Date().toISOString().slice(0, 10);
}

View File

@@ -11,7 +11,7 @@
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
><font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
</router-link>
Given by...
{{ stepType === "giver" ? "Given by..." : "Given to..." }}
</h1>
</div>
@@ -31,7 +31,7 @@
<button
type="button"
class="block w-full text-center text-sm uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-3 py-1.5 rounded-md"
@click="openDialog()"
@click="openDialog('Unnamed')"
>
<font-awesome icon="gift" class="fa-fw"></font-awesome>
</button>
@@ -65,7 +65,13 @@
</li>
</ul>
<GiftedDialog ref="customDialog" :to-project-id="projectId" />
<GiftedDialog
ref="customDialog"
:from-project-id="fromProjectId"
:to-project-id="toProjectId"
:show-projects="showProjects"
:is-from-project-view="isFromProjectView"
/>
</section>
</template>
@@ -97,6 +103,24 @@ export default class ContactGiftingView extends Vue {
description = "";
projectId = "";
prompt = "";
recipientProjectName = "";
recipientProjectImage = "";
recipientProjectHandleId = "";
// New context parameters
stepType = "giver";
giverEntityType = "person" as "person" | "project";
recipientEntityType = "person" as "person" | "project";
giverProjectId = "";
giverProjectName = "";
giverProjectImage = "";
giverProjectHandleId = "";
giverDid = "";
recipientDid = "";
fromProjectId = "";
toProjectId = "";
showProjects = false;
isFromProjectView = false;
async created() {
try {
@@ -124,9 +148,41 @@ export default class ContactGiftingView extends Vue {
);
}
this.projectId = (this.$route.query["projectId"] as string) || "";
this.projectId =
(this.$route.query["recipientProjectId"] as string) || "";
this.recipientProjectName =
(this.$route.query["recipientProjectName"] as string) || "";
this.recipientProjectImage =
(this.$route.query["recipientProjectImage"] as string) || "";
this.recipientProjectHandleId =
(this.$route.query["recipientProjectHandleId"] as string) || "";
this.prompt = (this.$route.query["prompt"] as string) ?? this.prompt;
// Read new context parameters
this.stepType = (this.$route.query["stepType"] as string) || "giver";
this.giverEntityType =
(this.$route.query["giverEntityType"] as "person" | "project") ||
"person";
this.recipientEntityType =
(this.$route.query["recipientEntityType"] as "person" | "project") ||
"person";
this.giverProjectId =
(this.$route.query["giverProjectId"] as string) || "";
this.giverProjectName =
(this.$route.query["giverProjectName"] as string) || "";
this.giverProjectImage =
(this.$route.query["giverProjectImage"] as string) || "";
this.giverProjectHandleId =
(this.$route.query["giverProjectHandleId"] as string) || "";
this.giverDid = (this.$route.query["giverDid"] as string) || "";
this.recipientDid = (this.$route.query["recipientDid"] as string) || "";
this.fromProjectId = (this.$route.query["fromProjectId"] as string) || "";
this.toProjectId = (this.$route.query["toProjectId"] as string) || "";
this.showProjects =
(this.$route.query["showProjects"] as string) === "true";
this.isFromProjectView =
(this.$route.query["isFromProjectView"] as string) === "true";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) {
logger.error("Error retrieving settings & contacts:", err);
@@ -144,17 +200,108 @@ export default class ContactGiftingView extends Vue {
}
}
openDialog(giver?: GiverReceiverInputInfo) {
const recipient = this.projectId
? undefined
: { did: this.activeDid, name: "you" };
(this.$refs.customDialog as GiftedDialog).open(
giver,
recipient,
undefined,
"Given by " + (giver?.name || "someone not named"),
this.prompt,
);
openDialog(contact?: GiverReceiverInputInfo | "Unnamed") {
if (contact === "Unnamed") {
// Special case: Pass undefined to trigger Step 1, but with "Unnamed" pre-selected
let recipient: GiverReceiverInputInfo;
let giver: GiverReceiverInputInfo | undefined;
if (this.stepType === "giver") {
// We're selecting a giver, so recipient is either a project or the current user
if (this.recipientEntityType === "project") {
recipient = {
did: this.recipientProjectHandleId,
name: this.recipientProjectName,
image: this.recipientProjectImage,
handleId: this.recipientProjectHandleId,
};
} else {
recipient = { did: this.activeDid, name: "You" };
}
giver = undefined; // Will be set to "Unnamed" in GiftedDialog
} else {
// We're selecting a recipient, so recipient is "Unnamed" and giver is preserved from context
recipient = { did: "", name: "Unnamed" };
// Preserve the existing giver from the context
if (this.giverEntityType === "project") {
giver = {
did: this.giverProjectHandleId,
name: this.giverProjectName,
image: this.giverProjectImage,
handleId: this.giverProjectHandleId,
};
} else if (this.giverDid) {
giver = {
did: this.giverDid,
name: this.giverProjectName || "Someone",
};
} else {
giver = { did: this.activeDid, name: "You" };
}
}
(this.$refs.customDialog as GiftedDialog).open(
giver,
recipient,
undefined,
this.stepType === "giver" ? "Given by Unnamed" : "Given to Unnamed",
this.prompt,
);
// Immediately select "Unnamed" and move to Step 2
(this.$refs.customDialog as GiftedDialog).selectGiver();
} else {
// Regular case: contact is a GiverReceiverInputInfo
let giver: GiverReceiverInputInfo;
let recipient: GiverReceiverInputInfo;
if (this.stepType === "giver") {
// We're selecting a giver, so the contact becomes the giver
giver = contact as GiverReceiverInputInfo; // Safe because we know contact is not "Unnamed" or undefined
// Recipient is either a project or the current user
if (this.recipientEntityType === "project") {
recipient = {
did: this.recipientProjectHandleId,
name: this.recipientProjectName,
image: this.recipientProjectImage,
handleId: this.recipientProjectHandleId,
};
} else {
recipient = { did: this.activeDid, name: "You" };
}
} else {
// We're selecting a recipient, so the contact becomes the recipient
recipient = contact as GiverReceiverInputInfo; // Safe because we know contact is not "Unnamed" or undefined
// Preserve the existing giver from the context
if (this.giverEntityType === "project") {
giver = {
did: this.giverProjectHandleId,
name: this.giverProjectName,
image: this.giverProjectImage,
handleId: this.giverProjectHandleId,
};
} else if (this.giverDid) {
giver = {
did: this.giverDid,
name: this.giverProjectName || "Someone",
};
} else {
giver = { did: this.activeDid, name: "You" };
}
}
(this.$refs.customDialog as GiftedDialog).open(
giver,
recipient,
undefined,
this.stepType === "giver"
? "Given by " + (contact?.name || "someone not named")
: "Given to " + (contact?.name || "someone not named"),
this.prompt,
);
}
}
}
</script>

View File

@@ -117,101 +117,73 @@ Raymer * @version 1.0.0 */
</div>
<div v-else id="sectionRecordSomethingGiven">
<!-- !isCreatingIdentifier && isRegistered -->
<!-- Record Quick-Action -->
<div class="mb-6">
<div class="flex gap-2 items-center mb-2">
<h2 class="text-xl font-bold">Record something given by:</h2>
<button
class="block ms-auto text-center text-white bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-2 rounded-full"
@click="openGiftedPrompts()"
>
<font-awesome
icon="lightbulb"
class="block text-center w-[1em]"
/>
</button>
</div>
<!-- show the actions for recognizing a give -->
<div class="flex">
<h2 class="text-xl font-bold">What have you seen someone do?</h2>
<button
class="ml-2 block text-xs text-center bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1 rounded-md"
@click="openGiftedPrompts()"
>
<font-awesome icon="lightbulb" class="fa-fw" />
</button>
<div class="grid grid-cols-2 gap-2">
<button
type="button"
class="text-center text-base uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-3 py-2 rounded-lg"
@click="openDialogPerson()"
>
<font-awesome icon="user" />
Person
</button>
<button
type="button"
class="text-center text-base uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-3 py-2 rounded-lg"
@click="openProjectDialog()"
>
<font-awesome icon="folder-open" />
Project
</button>
</div>
</div>
<ul
class="grid grid-cols-4 sm:grid-cols-5 md:grid-cols-6 gap-x-3 gap-y-5 text-center mt-4"
>
<li @click="openDialog()">
<img
src="../assets/blank-square.svg"
class="mx-auto border border-blue-500 rounded-md mb-1 cursor-pointer"
/>
<h3
class="text-xs text-blue-500 italic font-medium text-ellipsis whitespace-nowrap overflow-hidden cursor-pointer"
>
Unnamed/Unknown
</h3>
</li>
<li v-if="allContacts.length === 0" class="text-sm">
(Add friends to see more people worthy of recognition.)
</li>
<li
v-for="contact in allContacts.slice(0, 6)"
:key="contact.did"
@click="openDialog(contact)"
>
<EntityIcon
:contact="contact"
:icon-size="64"
class="mx-auto border border-blue-500 rounded-md mb-1 cursor-pointer"
/>
<h3
class="text-xs text-blue-500 font-medium text-ellipsis whitespace-nowrap overflow-hidden cursor-pointer"
>
{{ contact.name || contact.did }}
</h3>
</li>
<li>
<router-link
v-if="allContacts.length >= 6"
:to="{ name: 'contact-gift' }"
class="flex align-bottom text-xs text-blue-500 mt-12 cursor-pointer"
>
... or someone else...
</router-link>
</li>
</ul>
</div>
</div>
</div>
</div>
<GiftedDialog ref="customDialog" />
<GiftedDialog ref="customDialog" :show-projects="showProjectsDialog" />
<GiftedPrompts ref="giftedPrompts" />
<FeedFilters ref="feedFilters" />
<div class="relative">
<button
v-if="isRegistered"
class="absolute right-6 bottom-0 transform translate-y-1/2 text-center text-4xl leading-none bg-green-600 text-white w-14 py-2.5 rounded-full"
@click="openDialog()"
>
<font-awesome icon="plus" class="fa-fw" />
</button>
</div>
<!-- Results List -->
<div class="mt-4 mb-4">
<div class="flex items-center mb-4">
<h2 class="text-xl font-bold flex items-center gap-4">
Latest Activity
<button
v-if="resultsAreFiltered()"
class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] px-3 py-1.5 rounded-md text-xs text-white"
@click="openFeedFilters()"
>
<font-awesome icon="filter" class="fa-fw" />
</button>
<button
v-else
class="bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] px-3 py-1.5 rounded-md text-xs text-white"
@click="openFeedFilters()"
>
<font-awesome icon="filter" class="fa-fw" />
</button>
</h2>
<div class="flex gap-2 items-center mb-3">
<h2 class="text-xl font-bold">Latest Activity</h2>
<button
v-if="resultsAreFiltered()"
class="block ms-auto text-center text-white bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-2 rounded-full"
@click="openFeedFilters()"
>
<font-awesome
icon="filter"
class="block text-center w-[1em] translate-y-[0.05em]"
/>
</button>
<button
v-else
class="block ms-auto text-center text-white bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-2 rounded-full"
@click="openFeedFilters()"
>
<font-awesome
icon="filter"
class="block text-center w-[1em] translate-y-[0.05em]"
/>
</button>
</div>
<div
@@ -474,6 +446,7 @@ export default class HomeView extends Vue {
selectedImageData: Blob | null = null;
isImageViewerOpen = false;
imageCache: Map<string, Blob | null> = new Map();
showProjectsDialog = false;
/**
* Initializes the component on mount
@@ -1637,17 +1610,33 @@ export default class HomeView extends Vue {
* @param giver Optional contact info for giver
* @param description Optional gift description
*/
openDialog(giver?: GiverReceiverInputInfo, description?: string) {
(this.$refs.customDialog as GiftedDialog).open(
giver,
{
did: this.activeDid,
name: "you",
} as GiverReceiverInputInfo,
undefined,
"Given by " + (giver?.name || "someone not named"),
description,
);
openDialog(giver?: GiverReceiverInputInfo | "Unnamed", description?: string) {
if (giver === "Unnamed") {
// Special case: Pass undefined to trigger Step 1, but with "Unnamed" pre-selected
(this.$refs.customDialog as GiftedDialog).open(
undefined,
{
did: this.activeDid,
name: "You",
} as GiverReceiverInputInfo,
undefined,
"Given by Unnamed",
description,
);
// Immediately select "Unnamed" and move to Step 2
(this.$refs.customDialog as GiftedDialog).selectGiver();
} else {
(this.$refs.customDialog as GiftedDialog).open(
giver,
{
did: this.activeDid,
name: "You",
} as GiverReceiverInputInfo,
undefined,
"Given by " + (giver?.name || "someone not named"),
description,
);
}
}
/**
@@ -1881,5 +1870,18 @@ export default class HomeView extends Vue {
this.$router.push({ name: "contact-qr" });
}
}
openDialogPerson(
giver?: GiverReceiverInputInfo | "Unnamed",
description?: string,
) {
this.showProjectsDialog = false;
this.openDialog(giver, description);
}
openProjectDialog() {
this.showProjectsDialog = true;
(this.$refs.customDialog as any).open();
}
}
</script>

View File

@@ -196,63 +196,11 @@
</div>
</div>
<div v-if="activeDid && isRegistered">
<div class="text-center">
<p class="mt-2 mt-4 text-center">Record a contribution from:</p>
</div>
<ul
class="grid grid-cols-4 sm:grid-cols-5 md:grid-cols-6 gap-x-3 gap-y-5 text-center mb-5 mt-2"
>
<li @click="openGiftDialogToProject({ name: 'you', did: activeDid })">
<font-awesome
icon="hand"
class="fa-fw text-blue-500 text-5xl cursor-pointer"
/>
<h3
class="mt-5 text-xs text-blue-500 font-medium text-ellipsis whitespace-nowrap overflow-hidden cursor-pointer"
>
You
</h3>
</li>
<li @click="openGiftDialogToProject()">
<img
src="../assets/blank-square.svg"
class="mx-auto border border-blue-300 rounded-md mb-1 cursor-pointer"
/>
<h3
class="text-xs text-blue-500 italic font-medium text-ellipsis whitespace-nowrap overflow-hidden cursor-pointer"
>
Unnamed/Unknown
</h3>
</li>
<li
v-for="contact in allContacts.slice(0, 5)"
:key="contact.did"
@click="openGiftDialogToProject(contact)"
>
<EntityIcon
:contact="contact"
:icon-size="64"
class="mx-auto border border-blue-300 rounded-md mb-1 cursor-pointer"
/>
<h3
class="text-xs text-blue-500 font-medium text-ellipsis whitespace-nowrap overflow-hidden cursor-pointer"
>
{{ contact.name || "(no name)" }}
</h3>
</li>
<li>
<span
v-if="allContacts.length >= 5"
class="flex align-bottom text-xs text-blue-500 mt-12 cursor-pointer"
@click="onClickAllContactsGifting()"
>
... or someone else...
</span>
</li>
</ul>
</div>
<GiftedDialog ref="giveDialogToThis" :to-project-id="projectId" />
<GiftedDialog
ref="giveDialogToThis"
:to-project-id="projectId"
:is-from-project-view="true"
/>
<!-- Offers & Gifts to & from this -->
<div class="grid items-start grid-cols-1 sm:grid-cols-3 gap-4 mt-4">
@@ -518,7 +466,12 @@
</button>
</div>
</div>
<GiftedDialog ref="giveDialogFromThis" :from-project-id="projectId" />
<GiftedDialog
ref="giveDialogFromThis"
:from-project-id="projectId"
:show-projects="true"
:is-from-project-view="true"
/>
<h3 class="text-lg font-bold mb-3 mt-4">
Benefitted From This Project
@@ -1229,21 +1182,53 @@ export default class ProjectViewView extends Vue {
);
}
openGiftDialogToProject(contact?: libsUtil.GiverReceiverInputInfo) {
(this.$refs.giveDialogToThis as GiftedDialog).open(
contact,
undefined,
undefined,
(contact?.name || "Someone not named") + ` gave to this project`,
);
openGiftDialogToProject(
contact?: libsUtil.GiverReceiverInputInfo | "Unnamed",
) {
if (contact === "Unnamed") {
// Special case: Pass undefined to trigger Step 1, but with "Unnamed" pre-selected
(this.$refs.giveDialogToThis as GiftedDialog).open(
undefined,
undefined,
undefined,
"Given by Unnamed to this project",
);
// Immediately select "Unnamed" and move to Step 2
(this.$refs.giveDialogToThis as GiftedDialog).selectGiver();
} else {
// Open straight to Step 2 with current user as giver and current project as recipient
(this.$refs.giveDialogToThis as GiftedDialog).open(
{
did: this.activeDid,
name: "You",
},
{
did: this.issuer,
name: this.name,
handleId: this.projectId,
image: this.imageUrl,
},
undefined,
`Given to ${this.name}`,
);
}
}
openGiftDialogFromProject() {
// Set the project as giver and the current user as recipient
(this.$refs.giveDialogFromThis as GiftedDialog).open(
undefined,
{
did: undefined,
name: this.name,
handleId: this.projectId,
image: this.imageUrl,
},
{ did: this.activeDid, name: "You" },
undefined,
`This project gave to you`,
`${this.name} gave to you`,
undefined,
undefined,
true,
);
}

View File

@@ -215,65 +215,6 @@
</div>
</div>
<div class="mt-8">
<h2 class="text-xl font-bold mb-4">File Sharing Test</h2>
Test the new file sharing functionality that saves to user-accessible
locations.
<div>
<button
class="font-bold capitalize bg-slate-500 text-white px-3 py-2 rounded-md mr-2"
@click="testFileSharing()"
>
Test File Sharing
</button>
<button
class="font-bold capitalize bg-slate-500 text-white px-3 py-2 rounded-md mr-2"
@click="testFileSaveOnly()"
>
Test Save Only (No Share Dialog)
</button>
<button
class="font-bold capitalize bg-slate-500 text-white px-3 py-2 rounded-md mr-2"
@click="testLocationSelection()"
>
Test Location Selection
</button>
<button
class="font-bold capitalize bg-slate-500 text-white px-3 py-2 rounded-md mr-2"
@click="testLocationSelectionSilent()"
>
Test Silent Location Selection
</button>
<button
class="font-bold capitalize bg-slate-500 text-white px-3 py-2 rounded-md mr-2"
@click="testListUserFiles()"
>
List User Files
</button>
<button
class="font-bold capitalize bg-slate-500 text-white px-3 py-2 rounded-md mr-2"
@click="testBackupFiles()"
>
Test Backup Files
</button>
<button
class="font-bold capitalize bg-slate-500 text-white px-3 py-2 rounded-md mr-2"
@click="testOpenBackupDirectory()"
>
Test Open Directory
</button>
<button
class="font-bold capitalize bg-slate-500 text-white px-3 py-2 rounded-md mr-2"
@click="testFileDiscoveryDebug()"
>
Debug File Discovery
</button>
<div v-if="fileSharingResult" class="mt-2 p-2 bg-gray-100 rounded">
<strong>Result:</strong> {{ fileSharingResult }}
</div>
</div>
</div>
<div class="mt-8">
<h2 class="text-xl font-bold mb-4">Image Sharing</h2>
Populates the "shared-photo" view as if they used "share_target".
@@ -446,9 +387,6 @@ export default class Help extends Vue {
sqlQuery = "";
sqlResult: unknown = null;
// for file sharing test
fileSharingResult = "";
cryptoLib = cryptoLib;
async mounted() {
@@ -682,174 +620,5 @@ export default class Help extends Vue {
);
}
}
public async testFileSharing() {
const platformService = PlatformServiceFactory.getInstance();
try {
const result = await platformService.testFileSharing();
this.fileSharingResult = result;
logger.log("File Sharing Test Result:", this.fileSharingResult);
} catch (error) {
logger.error("File Sharing Test Error:", error);
this.$notify(
{
group: "alert",
type: "danger",
title: "File Sharing Error",
text: error instanceof Error ? error.message : String(error),
},
5000,
);
}
}
public async testFileSaveOnly() {
const platformService = PlatformServiceFactory.getInstance();
try {
const result = await platformService.testFileSaveOnly();
this.fileSharingResult = result;
logger.log("File Save Only Test Result:", this.fileSharingResult);
} catch (error) {
logger.error("File Save Only Test Error:", error);
this.$notify(
{
group: "alert",
type: "danger",
title: "File Save Only Error",
text: error instanceof Error ? error.message : String(error),
},
5000,
);
}
}
public async testLocationSelection() {
const platformService = PlatformServiceFactory.getInstance();
try {
const result = await platformService.testLocationSelection();
this.fileSharingResult = result;
logger.log("Location Selection Test Result:", this.fileSharingResult);
} catch (error) {
logger.error("Location Selection Test Error:", error);
this.$notify(
{
group: "alert",
type: "danger",
title: "Location Selection Error",
text: error instanceof Error ? error.message : String(error),
},
5000,
);
}
}
public async testLocationSelectionSilent() {
const platformService = PlatformServiceFactory.getInstance();
try {
const result = await platformService.testLocationSelectionSilent();
this.fileSharingResult = result;
logger.log(
"Silent Location Selection Test Result:",
this.fileSharingResult,
);
} catch (error) {
logger.error("Silent Location Selection Test Error:", error);
this.$notify(
{
group: "alert",
type: "danger",
title: "Silent Location Selection Error",
text: error instanceof Error ? error.message : String(error),
},
5000,
);
}
}
public async testListUserFiles() {
const platformService = PlatformServiceFactory.getInstance();
try {
const result = await platformService.testListUserFiles();
this.fileSharingResult = result;
logger.log("List User Files Test Result:", this.fileSharingResult);
} catch (error) {
logger.error("List User Files Test Error:", error);
this.$notify(
{
group: "alert",
type: "danger",
title: "List User Files Error",
text: error instanceof Error ? error.message : String(error),
},
5000,
);
}
}
public async testBackupFiles() {
const platformService = PlatformServiceFactory.getInstance();
try {
const result = await platformService.testBackupFiles();
this.fileSharingResult = result;
logger.log("Backup Files Test Result:", this.fileSharingResult);
} catch (error) {
logger.error("Backup Files Test Error:", error);
this.$notify(
{
group: "alert",
type: "danger",
title: "Backup Files Error",
text: error instanceof Error ? error.message : String(error),
},
5000,
);
}
}
public async testOpenBackupDirectory() {
const platformService = PlatformServiceFactory.getInstance();
try {
const result = await platformService.testOpenBackupDirectory();
this.fileSharingResult = result;
logger.log("Open Backup Directory Test Result:", this.fileSharingResult);
} catch (error) {
logger.error("Open Backup Directory Test Error:", error);
this.$notify(
{
group: "alert",
type: "danger",
title: "Open Backup Directory Error",
text: error instanceof Error ? error.message : String(error),
},
5000,
);
}
}
public async testFileDiscoveryDebug() {
const platformService = PlatformServiceFactory.getInstance();
try {
if ("debugFileDiscoveryStepByStep" in platformService) {
const result = await (
platformService as any
).debugFileDiscoveryStepByStep();
this.fileSharingResult = result;
logger.log("File Discovery Debug Test Result:", this.fileSharingResult);
} else {
this.fileSharingResult = "Debug method not available on this platform";
}
} catch (error) {
logger.error("File Discovery Debug Test Error:", error);
this.$notify(
{
group: "alert",
type: "danger",
title: "File Discovery Debug Error",
text: error instanceof Error ? error.message : String(error),
},
5000,
);
}
}
}
</script>