diff --git a/.gitignore b/.gitignore
index 937ac99f..b92c212a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -55,3 +55,4 @@ build_logs/
icons
+android/app/src/main/res/
\ No newline at end of file
diff --git a/BUILDING.md b/BUILDING.md
index ddc6e6d2..04531f01 100644
--- a/BUILDING.md
+++ b/BUILDING.md
@@ -41,6 +41,7 @@ Install dependencies:
1. Run the production build:
```bash
+ rm -rf dist
npm run build:web
```
@@ -62,16 +63,18 @@ Install dependencies:
* Update the ClickUp tasks & CHANGELOG.md & the version in package.json, run `npm install`.
+* Run a build to make sure package-lock version is updated, linting works, etc: `npm install && npm run build`
+
* Commit everything (since the commit hash is used the app).
-* Put the commit hash in the changelog (which will help you remember to bump the version later).
+* Put the commit hash in the changelog (which will help you remember to bump the version in the step later).
-* Tag with the new version, [online](https://gitea.anomalistdesign.com/trent_larson/crowd-funder-for-time-pwa/releases) or `git tag 0.3.55 && git push origin 0.3.55`.
+* Tag with the new version, [online](https://gitea.anomalistdesign.com/trent_larson/crowd-funder-for-time-pwa/releases) or `git tag 1.0.2 && git push origin 1.0.2`.
* For test, build the app (because test server is not yet set up to build):
```bash
-TIME_SAFARI_APP_TITLE="TimeSafari_Test" VITE_APP_SERVER=https://test.timesafari.app VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HWE8FWHQ1YGP7GFZYYPS272F VITE_DEFAULT_ENDORSER_API_SERVER=https://test-api.endorser.ch VITE_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app VITE_DEFAULT_PARTNER_API_SERVER=https://test-partner-api.endorser.ch VITE_DEFAULT_PUSH_SERVER=https://test.timesafari.app VITE_PASSKEYS_ENABLED=true npm run build
+TIME_SAFARI_APP_TITLE="TimeSafari_Test" VITE_APP_SERVER=https://test.timesafari.app VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HWE8FWHQ1YGP7GFZYYPS272F VITE_DEFAULT_ENDORSER_API_SERVER=https://test-api.endorser.ch VITE_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app VITE_DEFAULT_PARTNER_API_SERVER=https://test-partner-api.endorser.ch VITE_DEFAULT_PUSH_SERVER=https://test.timesafari.app VITE_PASSKEYS_ENABLED=true npm run build:web
```
... and transfer to the test server:
@@ -90,13 +93,13 @@ TIME_SAFARI_APP_TITLE="TimeSafari_Test" VITE_APP_SERVER=https://test.timesafari.
* `pkgx +npm sh`
- * `cd crowd-funder-for-time-pwa && git checkout master && git pull && git checkout 0.3.55 && npm install && npm run build && cd -`
+ * `cd crowd-funder-for-time-pwa && git checkout master && git pull && git checkout 1.0.2 && npm install && npm run build:web && cd -`
- (The plain `npm run build` uses the .env.production file.)
+ (The plain `npm run build:web` uses the .env.production file.)
-* Back up the time-safari/dist folder & deploy: `mv time-safari/dist time-safari-dist-prev.0 && mv crowd-funder-for-time-pwa/dist time-safari/`
+* Back up the time-safari/dist folder & deploy: `mv time-safari/dist time-safari-dist-prev-2 && mv crowd-funder-for-time-pwa/dist time-safari/`
-* Record the new hash in the changelog. Edit package.json to increment version & add "-beta", `npm install`, and commit. Also record what version is on production.
+* Record the new hash in the changelog. Edit package.json to increment version & add "-beta", `npm install`, commit, and push. Also record what version is on production.
## Docker Deployment
@@ -321,11 +324,11 @@ Prerequisites: macOS with Xcode installed
#### Each Release
-0. First time (or if XCode dependencies change):
+0. First time (or if dependencies change):
- `pkgx +rubygems.org sh`
- - ... and you may have to fix these, especially with pkgx
+ - ... and you may have to fix these, especially with pkgx:
```bash
gem_path=$(which gem)
@@ -334,23 +337,12 @@ Prerequisites: macOS with Xcode installed
export GEM_PATH=$shortened_path
```
- ```bash
- cd ios/App
- pod install
- ```
-
-1. Build the web assets:
+1. Build the web assets & update ios:
```bash
rm -rf dist
npm run build:web
npm run build:capacitor
- ```
-
-
-2. Update iOS project with latest build:
-
- ```bash
npx cap sync ios
```
@@ -367,16 +359,12 @@ Prerequisites: macOS with Xcode installed
npx capacitor-assets generate --ios
```
-4. Bump the version to match Android:
+4. Bump the version to match Android & package.json:
```
- cd ios/App
- xcrun agvtool new-version 25
+ cd ios/App && xcrun agvtool new-version 35 && perl -p -i -e "s/MARKETING_VERSION = .*;/MARKETING_VERSION = 1.0.2;/g" App.xcodeproj/project.pbxproj && cd -
# Unfortunately this edits Info.plist directly.
#xcrun agvtool new-marketing-version 0.4.5
- cat App.xcodeproj/project.pbxproj | sed "s/MARKETING_VERSION = .*;/MARKETING_VERSION = 0.5.1;/g" > temp
- mv temp App.xcodeproj/project.pbxproj
- cd -
```
5. Open the project in Xcode:
@@ -427,7 +415,7 @@ Prerequisites: Android Studio with Java SDK installed
npx capacitor-assets generate --android
```
-4. Bump version to match iOS: android/app/build.gradle
+4. Bump version to match iOS & package.json: android/app/build.gradle
5. Open the project in Android Studio:
@@ -444,7 +432,6 @@ Prerequisites: Android Studio with Java SDK installed
./gradlew clean
./gradlew build -Dlint.baselines.continue=true
cd -
- npx cap run android
```
... or, to create the `aab` file, `bundle` instead of `build`:
@@ -478,7 +465,7 @@ At play.google.com/console:
- Note that if you add testers, you have to go to "Publishing Overview" and send those changes or your (closed) testers won't see it.
-## First-time Android Configuration for deep links
+## Android Configuration for deep links
You must add the following intent filter to the `android/app/src/main/AndroidManifest.xml` file:
@@ -489,4 +476,6 @@ You must add the following intent filter to the `android/app/src/main/AndroidMan
- ```
\ No newline at end of file
+ ```
+
+... though when we tried that most recently it failed to 'build' the APK with: http(s) scheme and host attribute are missing, but are required for Android App Links [AppLinkUrlError]
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 71657a57..f723b1ab 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,28 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+## [1.0.2] - 2025.06.20 - 276e0a741bc327de3380c4e508cccb7fee58c06d
+### Added
+- Version on feed title
+
+
+## [1.0.1] - 2025.06.20
+### Added
+- Allow a user to block someone else's content from view
+
+
+## [1.0.0] - 2025.06.20 - 5aa693de6337e5dbb278bfddc6bd39094bc14f73
+### Added
+- Web-oriented migration from IndexedDB to SQLite
+
+
+## [0.5.8]
+### Added
+- /deep-link/ path for URLs that are shared with people
+### Changed
+- External links now go to /deep-link/...
+- Feed visuals now have arrow imagery from giver to receiver
+
## [0.4.7]
### Fixed
diff --git a/README.md b/README.md
index ec499174..d673c279 100644
--- a/README.md
+++ b/README.md
@@ -3,6 +3,32 @@
[Time Safari](https://timesafari.org/) allows people to ease into collaboration: start with expressions of gratitude
and expand to crowd-fund with time & money, then record and see the impact of contributions.
+## Database Migration Status
+
+**Current Status**: The application is undergoing a migration from Dexie (IndexedDB) to SQLite using absurd-sql. This migration is in **Phase 2** with a well-defined migration fence in place.
+
+### Migration Progress
+- ✅ **SQLite Database Service**: Fully implemented with absurd-sql
+- ✅ **Platform Service Layer**: Unified database interface across platforms
+- ✅ **Settings Migration**: Core user settings transferred
+- ✅ **Account Migration**: Identity and key management
+- 🔄 **Contact Migration**: User contact data (via import interface)
+- 📋 **Code Cleanup**: Remove unused Dexie imports
+
+### Migration Fence
+The migration is controlled by a **migration fence** that separates legacy Dexie code from the new SQLite implementation. See [Migration Fence Definition](doc/migration-fence-definition.md) for complete details.
+
+**Key Points**:
+- Legacy Dexie database is disabled by default (`USE_DEXIE_DB = false`)
+- All database operations go through `PlatformService`
+- Migration tools provide controlled access to both databases
+- Clear separation between legacy and new code
+
+### Migration Documentation
+- [Migration Guide](doc/migration-to-wa-sqlite.md) - Complete migration process
+- [Migration Fence Definition](doc/migration-fence-definition.md) - Fence boundaries and rules
+- [Database Migration Guide](doc/database-migration-guide.md) - User-facing migration tools
+
## Roadmap
See [project.task.yaml](project.task.yaml) for current priorities.
@@ -21,16 +47,10 @@ npm run dev
See [BUILDING.md](BUILDING.md) for more details.
-
-
-
## Tests
See [TESTING.md](test-playwright/TESTING.md) for detailed test instructions.
-
-
-
## Icons
Application icons are in the `assets` directory, processed by the `capacitor-assets` command.
@@ -66,6 +86,21 @@ Key principles:
- Common interfaces are shared through `common.ts`
- Type definitions are generated from Zod schemas where possible
+### Database Architecture
+
+The application uses a platform-agnostic database layer:
+
+* `src/services/PlatformService.ts` - Database interface definition
+* `src/services/PlatformServiceFactory.ts` - Platform-specific service factory
+* `src/services/AbsurdSqlDatabaseService.ts` - SQLite implementation
+* `src/db/` - Legacy Dexie database (migration in progress)
+
+**Development Guidelines**:
+- Always use `PlatformService` for database operations
+- Never import Dexie directly in application code
+- Test with `USE_DEXIE_DB = false` for new features
+- Use migration tools for data transfer between systems
+
### Kudos
Gifts make the world go 'round!
diff --git a/android/app/build.gradle b/android/app/build.gradle
index f105fb52..0843dce8 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -31,8 +31,8 @@ android {
applicationId "app.timesafari.app"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
- versionCode 26
- versionName "0.5.1"
+ versionCode 35
+ versionName "1.0.2"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
diff --git a/doc/DEEP_LINKS.md b/doc/DEEP_LINKS.md
index a68a5ed1..a6bf9f6b 100644
--- a/doc/DEEP_LINKS.md
+++ b/doc/DEEP_LINKS.md
@@ -100,6 +100,7 @@ try {
- `src/interfaces/deepLinks.ts`: Type definitions and validation schemas
- `src/services/deepLinks.ts`: Deep link processing service
- `src/main.capacitor.ts`: Capacitor integration
+- `src/views/DeepLinkRedirectView.vue`: Page to handle links to both mobile and web
## Type Safety Examples
diff --git a/doc/database-migration-guide.md b/doc/database-migration-guide.md
new file mode 100644
index 00000000..fe17287e
--- /dev/null
+++ b/doc/database-migration-guide.md
@@ -0,0 +1,295 @@
+# Database Migration Guide
+
+## Overview
+
+The Database Migration feature allows you to compare and migrate data between Dexie (IndexedDB) and SQLite databases in the TimeSafari application. This is particularly useful during the transition from the old Dexie-based storage system to the new SQLite-based system.
+
+## Features
+
+### 1. Database Comparison
+
+- Compare data between Dexie and SQLite databases
+- View detailed differences in contacts and settings
+- Identify added, modified, and missing records
+- Export comparison results for analysis
+
+### 2. Data Migration
+
+- Migrate contacts from Dexie to SQLite
+- Migrate settings from Dexie to SQLite
+- Option to overwrite existing records or skip them
+- Comprehensive error handling and reporting
+
+### 3. User Interface
+
+- Modern, responsive UI built with Tailwind CSS
+- Real-time loading states and progress indicators
+- Clear success and error messaging
+- Export functionality for comparison data
+
+## Prerequisites
+
+### Enable Dexie Database
+
+Before using the migration features, you must enable the Dexie database by setting:
+
+```typescript
+// In constants/app.ts
+export const USE_DEXIE_DB = true;
+```
+
+**Note**: This should only be enabled temporarily during migration. Remember to set it back to `false` after migration is complete.
+
+## Accessing the Migration Interface
+
+1. Navigate to the **Account** page in the TimeSafari app
+2. Scroll down to find the **Database Migration** link
+3. Click the link to open the migration interface
+
+## Using the Migration Interface
+
+### Step 1: Compare Databases
+
+1. Click the **"Compare Databases"** button
+2. The system will retrieve data from both Dexie and SQLite databases
+3. Review the comparison results showing:
+ - Summary counts for each database
+ - Detailed differences (added, modified, missing records)
+ - Specific records that need attention
+
+### Step 2: Review Differences
+
+The comparison results are displayed in several sections:
+
+#### Summary Cards
+
+- **Dexie Contacts**: Number of contacts in Dexie database
+- **SQLite Contacts**: Number of contacts in SQLite database
+- **Dexie Settings**: Number of settings in Dexie database
+- **SQLite Settings**: Number of settings in SQLite database
+
+#### Contact Differences
+
+- **Added**: Contacts in Dexie but not in SQLite
+- **Modified**: Contacts that differ between databases
+- **Missing**: Contacts in SQLite but not in Dexie
+
+#### Settings Differences
+
+- **Added**: Settings in Dexie but not in SQLite
+- **Modified**: Settings that differ between databases
+- **Missing**: Settings in SQLite but not in Dexie
+
+### Step 3: Configure Migration Options
+
+Before migrating data, configure the migration options:
+
+- **Overwrite existing records**: When enabled, existing records in SQLite will be updated with data from Dexie. When disabled, existing records will be skipped.
+
+### Step 4: Migrate Data
+
+#### Migrate Contacts
+
+1. Click the **"Migrate Contacts"** button
+2. The system will transfer contacts from Dexie to SQLite
+3. Review the migration results showing:
+ - Number of contacts successfully migrated
+ - Any warnings or errors encountered
+
+#### Migrate Settings
+
+1. Click the **"Migrate Settings"** button
+2. The system will transfer settings from Dexie to SQLite
+3. Review the migration results showing:
+ - Number of settings successfully migrated
+ - Any warnings or errors encountered
+
+### Step 5: Export Comparison (Optional)
+
+1. Click the **"Export Comparison"** button
+2. A JSON file will be downloaded containing the complete comparison data
+3. This file can be used for analysis or backup purposes
+
+## Migration Process Details
+
+### Contact Migration
+
+The contact migration process:
+
+1. **Retrieves** all contacts from Dexie database
+2. **Checks** for existing contacts in SQLite by DID
+3. **Inserts** new contacts or **updates** existing ones (if overwrite is enabled)
+4. **Handles** complex fields like `contactMethods` (JSON arrays)
+5. **Reports** success/failure for each contact
+
+### Settings Migration
+
+The settings migration process:
+
+1. **Retrieves** all settings from Dexie database
+2. **Focuses** on key user-facing settings:
+ - `firstName`
+ - `isRegistered`
+ - `profileImageUrl`
+ - `showShortcutBvc`
+ - `searchBoxes`
+3. **Preserves** other settings in SQLite
+4. **Reports** success/failure for each setting
+
+## Error Handling
+
+### Common Issues
+
+#### Dexie Database Not Enabled
+
+**Error**: "Dexie database is not enabled"
+**Solution**: Set `USE_DEXIE_DB = true` in `constants/app.ts`
+
+#### Database Connection Issues
+
+**Error**: "Failed to retrieve Dexie contacts"
+**Solution**: Check that the Dexie database is properly initialized and accessible
+
+#### SQLite Query Errors
+
+**Error**: "Failed to retrieve SQLite contacts"
+**Solution**: Verify that the SQLite database is properly set up and the platform service is working
+
+#### Migration Failures
+
+**Error**: "Migration failed: [specific error]"
+**Solution**: Review the error details and check data integrity in both databases
+
+### Error Recovery
+
+1. **Review** the error messages carefully
+2. **Check** the browser console for additional details
+3. **Verify** database connectivity and permissions
+4. **Retry** the operation if appropriate
+5. **Export** comparison data for manual review if needed
+
+## Best Practices
+
+### Before Migration
+
+1. **Backup** your data if possible
+2. **Test** the migration on a small dataset first
+3. **Verify** that both databases are accessible
+4. **Review** the comparison results before migrating
+
+### During Migration
+
+1. **Don't** interrupt the migration process
+2. **Monitor** the progress and error messages
+3. **Note** any warnings or skipped records
+4. **Export** comparison data for reference
+
+### After Migration
+
+1. **Verify** that data was migrated correctly
+2. **Test** the application functionality
+3. **Disable** Dexie database (`USE_DEXIE_DB = false`)
+4. **Clean up** any temporary files or exports
+
+## Technical Details
+
+### Database Schema
+
+The migration handles the following data structures:
+
+#### Contacts Table
+
+```typescript
+interface Contact {
+ did: string; // Decentralized Identifier
+ name: string; // Contact name
+ contactMethods: ContactMethod[]; // Array of contact methods
+ nextPubKeyHashB64: string; // Next public key hash
+ notes: string; // Contact notes
+ profileImageUrl: string; // Profile image URL
+ publicKeyBase64: string; // Public key in base64
+ seesMe: boolean; // Visibility flag
+ registered: boolean; // Registration status
+}
+```
+
+#### Settings Table
+
+```typescript
+interface Settings {
+ id: number; // Settings ID
+ accountDid: string; // Account DID
+ activeDid: string; // Active DID
+ firstName: string; // User's first name
+ isRegistered: boolean; // Registration status
+ profileImageUrl: string; // Profile image URL
+ showShortcutBvc: boolean; // UI preference
+ searchBoxes: any[]; // Search configuration
+ // ... other fields
+}
+```
+
+### Migration Logic
+
+The migration service uses sophisticated comparison logic:
+
+1. **Primary Key Matching**: Uses DID for contacts, ID for settings
+2. **Deep Comparison**: Compares all fields including complex objects
+3. **JSON Handling**: Properly handles JSON fields like `contactMethods` and `searchBoxes`
+4. **Conflict Resolution**: Provides options for handling existing records
+
+### Performance Considerations
+
+- **Batch Processing**: Processes records one by one for reliability
+- **Error Isolation**: Individual record failures don't stop the entire migration
+- **Memory Management**: Handles large datasets efficiently
+- **Progress Reporting**: Provides real-time feedback during migration
+
+## Troubleshooting
+
+### Migration Stuck
+
+If the migration appears to be stuck:
+
+1. **Check** the browser console for errors
+2. **Refresh** the page and try again
+3. **Verify** database connectivity
+4. **Check** for large datasets that might take time
+
+### Incomplete Migration
+
+If migration doesn't complete:
+
+1. **Review** error messages
+2. **Check** data integrity in both databases
+3. **Export** comparison data for manual review
+4. **Consider** migrating in smaller batches
+
+### Data Inconsistencies
+
+If you notice data inconsistencies:
+
+1. **Export** comparison data
+2. **Review** the differences carefully
+3. **Manually** verify critical records
+4. **Consider** selective migration of specific records
+
+## Support
+
+For issues with the Database Migration feature:
+
+1. **Check** this documentation first
+2. **Review** the browser console for error details
+3. **Export** comparison data for analysis
+4. **Contact** the development team with specific error details
+
+## Security Considerations
+
+- **Data Privacy**: Migration data is processed locally and not sent to external servers
+- **Access Control**: Only users with access to the account can perform migration
+- **Data Integrity**: Migration preserves data integrity and handles conflicts gracefully
+- **Audit Trail**: Export functionality provides an audit trail of migration operations
+
+---
+
+**Note**: This migration tool is designed for the transition period between database systems. Once migration is complete and verified, the Dexie database should be disabled to avoid confusion and potential data conflicts.
diff --git a/doc/dexie-to-sqlite-mapping.md b/doc/dexie-to-sqlite-mapping.md
index 893b4670..e598cdcf 100644
--- a/doc/dexie-to-sqlite-mapping.md
+++ b/doc/dexie-to-sqlite-mapping.md
@@ -3,6 +3,7 @@
## Schema Mapping
### Current Dexie Schema
+
```typescript
// Current Dexie schema
const db = new Dexie('TimeSafariDB');
@@ -15,6 +16,7 @@ db.version(1).stores({
```
### New SQLite Schema
+
```sql
-- New SQLite schema
CREATE TABLE accounts (
@@ -50,6 +52,7 @@ CREATE INDEX idx_settings_updated_at ON settings(updated_at);
### 1. Account Operations
#### Get Account by DID
+
```typescript
// Dexie
const account = await db.accounts.get(did);
@@ -62,6 +65,7 @@ const account = result[0]?.values[0];
```
#### Get All Accounts
+
```typescript
// Dexie
const accounts = await db.accounts.toArray();
@@ -74,6 +78,7 @@ const accounts = result[0]?.values || [];
```
#### Add Account
+
```typescript
// Dexie
await db.accounts.add({
@@ -91,6 +96,7 @@ await db.run(`
```
#### Update Account
+
```typescript
// Dexie
await db.accounts.update(did, {
@@ -100,7 +106,7 @@ await db.accounts.update(did, {
// absurd-sql
await db.run(`
- UPDATE accounts
+ UPDATE accounts
SET public_key_hex = ?, updated_at = ?
WHERE did = ?
`, [publicKeyHex, Date.now(), did]);
@@ -109,6 +115,7 @@ await db.run(`
### 2. Settings Operations
#### Get Setting
+
```typescript
// Dexie
const setting = await db.settings.get(key);
@@ -121,6 +128,7 @@ const setting = result[0]?.values[0];
```
#### Set Setting
+
```typescript
// Dexie
await db.settings.put({
@@ -142,6 +150,7 @@ await db.run(`
### 3. Contact Operations
#### Get Contacts by Account
+
```typescript
// Dexie
const contacts = await db.contacts
@@ -151,7 +160,7 @@ const contacts = await db.contacts
// absurd-sql
const result = await db.exec(`
- SELECT * FROM contacts
+ SELECT * FROM contacts
WHERE did = ?
ORDER BY created_at DESC
`, [accountDid]);
@@ -159,6 +168,7 @@ const contacts = result[0]?.values || [];
```
#### Add Contact
+
```typescript
// Dexie
await db.contacts.add({
@@ -179,6 +189,7 @@ await db.run(`
## Transaction Mapping
### Batch Operations
+
```typescript
// Dexie
await db.transaction('rw', [db.accounts, db.contacts], async () => {
@@ -210,10 +221,11 @@ try {
## Migration Helper Functions
### 1. Data Export (Dexie to JSON)
+
```typescript
async function exportDexieData(): Promise {
const db = new Dexie('TimeSafariDB');
-
+
return {
accounts: await db.accounts.toArray(),
settings: await db.settings.toArray(),
@@ -228,6 +240,7 @@ async function exportDexieData(): Promise {
```
### 2. Data Import (JSON to absurd-sql)
+
```typescript
async function importToAbsurdSql(data: MigrationData): Promise {
await db.exec('BEGIN TRANSACTION;');
@@ -239,7 +252,7 @@ async function importToAbsurdSql(data: MigrationData): Promise {
VALUES (?, ?, ?, ?)
`, [account.did, account.publicKeyHex, account.createdAt, account.updatedAt]);
}
-
+
// Import settings
for (const setting of data.settings) {
await db.run(`
@@ -247,7 +260,7 @@ async function importToAbsurdSql(data: MigrationData): Promise {
VALUES (?, ?, ?)
`, [setting.key, setting.value, setting.updatedAt]);
}
-
+
// Import contacts
for (const contact of data.contacts) {
await db.run(`
@@ -264,6 +277,7 @@ async function importToAbsurdSql(data: MigrationData): Promise {
```
### 3. Verification
+
```typescript
async function verifyMigration(dexieData: MigrationData): Promise {
// Verify account count
@@ -272,21 +286,21 @@ async function verifyMigration(dexieData: MigrationData): Promise {
if (accountCount !== dexieData.accounts.length) {
return false;
}
-
+
// Verify settings count
const settingsResult = await db.exec('SELECT COUNT(*) as count FROM settings');
const settingsCount = settingsResult[0].values[0][0];
if (settingsCount !== dexieData.settings.length) {
return false;
}
-
+
// Verify contacts count
const contactsResult = await db.exec('SELECT COUNT(*) as count FROM contacts');
const contactsCount = contactsResult[0].values[0][0];
if (contactsCount !== dexieData.contacts.length) {
return false;
}
-
+
// Verify data integrity
for (const account of dexieData.accounts) {
const result = await db.exec(
@@ -294,12 +308,12 @@ async function verifyMigration(dexieData: MigrationData): Promise {
[account.did]
);
const migratedAccount = result[0]?.values[0];
- if (!migratedAccount ||
+ if (!migratedAccount ||
migratedAccount[1] !== account.publicKeyHex) { // public_key_hex is second column
return false;
}
}
-
+
return true;
}
```
@@ -307,18 +321,21 @@ async function verifyMigration(dexieData: MigrationData): Promise {
## Performance Considerations
### 1. Indexing
+
- Dexie automatically creates indexes based on the schema
- absurd-sql requires explicit index creation
- Added indexes for frequently queried fields
- Use `PRAGMA journal_mode=MEMORY;` for better performance
### 2. Batch Operations
+
- Dexie has built-in bulk operations
- absurd-sql uses transactions for batch operations
- Consider chunking large datasets
- Use prepared statements for repeated queries
### 3. Query Optimization
+
- Dexie uses IndexedDB's native indexing
- absurd-sql requires explicit query optimization
- Use prepared statements for repeated queries
@@ -327,6 +344,7 @@ async function verifyMigration(dexieData: MigrationData): Promise {
## Error Handling
### 1. Common Errors
+
```typescript
// Dexie errors
try {
@@ -351,6 +369,7 @@ try {
```
### 2. Transaction Recovery
+
```typescript
// Dexie transaction
try {
@@ -396,4 +415,4 @@ try {
- Remove Dexie database
- Clear IndexedDB storage
- Update application code
- - Remove old dependencies
\ No newline at end of file
+ - Remove old dependencies
diff --git a/doc/migration-fence-definition.md b/doc/migration-fence-definition.md
new file mode 100644
index 00000000..6c68e6d0
--- /dev/null
+++ b/doc/migration-fence-definition.md
@@ -0,0 +1,272 @@
+# Migration Fence Definition: Dexie to SQLite
+
+## Overview
+
+This document defines the **migration fence** - the boundary between the legacy Dexie (IndexedDB) storage system and the new SQLite-based storage system in TimeSafari. The fence ensures controlled migration while maintaining data integrity and application stability.
+
+## Current Migration Status
+
+### ✅ Completed Components
+- **SQLite Database Service**: Fully implemented with absurd-sql
+- **Platform Service Layer**: Unified database interface across platforms
+- **Migration Tools**: Data comparison and transfer utilities
+- **Schema Migration**: Complete table structure migration
+- **Data Export/Import**: Backup and restore functionality
+
+### 🔄 Active Migration Components
+- **Settings Migration**: Core user settings transferred
+- **Account Migration**: Identity and key management
+- **Contact Migration**: User contact data (via import interface)
+
+### ❌ Legacy Components (Fence Boundary)
+- **Dexie Database**: Legacy IndexedDB storage (disabled by default)
+- **Dexie-Specific Code**: Direct database access patterns
+- **Legacy Migration Paths**: Old data transfer methods
+
+## Migration Fence Definition
+
+### 1. Configuration Boundary
+
+```typescript
+// src/constants/app.ts
+export const USE_DEXIE_DB = false; // FENCE: Controls legacy database access
+```
+
+**Fence Rule**: When `USE_DEXIE_DB = false`:
+- All new data operations use SQLite
+- Legacy Dexie database is not initialized
+- Migration tools are the only path to legacy data
+
+**Fence Rule**: When `USE_DEXIE_DB = true`:
+- Legacy database is available for migration
+- Dual-write operations may be enabled
+- Migration tools can access both databases
+
+### 2. Service Layer Boundary
+
+```typescript
+// src/services/PlatformServiceFactory.ts
+export class PlatformServiceFactory {
+ public static getInstance(): PlatformService {
+ // FENCE: All database operations go through platform service
+ // No direct Dexie access outside migration tools
+ }
+}
+```
+
+**Fence Rule**: All database operations must use:
+- `PlatformService.dbQuery()` for read operations
+- `PlatformService.dbExec()` for write operations
+- No direct `db.` or `accountsDBPromise` access in application code
+
+### 3. Data Access Patterns
+
+#### ✅ Allowed (Inside Fence)
+```typescript
+// Use platform service for all database operations
+const platformService = PlatformServiceFactory.getInstance();
+const contacts = await platformService.dbQuery(
+ "SELECT * FROM contacts WHERE did = ?",
+ [accountDid]
+);
+```
+
+#### ❌ Forbidden (Outside Fence)
+```typescript
+// Direct Dexie access (legacy pattern)
+const contacts = await db.contacts.where('did').equals(accountDid).toArray();
+
+// Direct database reference
+const result = await accountsDBPromise;
+```
+
+### 4. Migration Tool Boundary
+
+```typescript
+// src/services/indexedDBMigrationService.ts
+// FENCE: Only migration tools can access both databases
+export async function compareDatabases(): Promise {
+ // This is the ONLY place where both databases are accessed
+}
+```
+
+**Fence Rule**: Migration tools are the exclusive interface between:
+- Legacy Dexie database
+- New SQLite database
+- Data comparison and transfer operations
+
+## Migration Fence Guidelines
+
+### 1. Code Development Rules
+
+#### New Feature Development
+- **Always** use `PlatformService` for database operations
+- **Never** import or reference Dexie directly
+- **Always** test with `USE_DEXIE_DB = false`
+
+#### Legacy Code Maintenance
+- **Only** modify Dexie code for migration purposes
+- **Always** add migration tests for schema changes
+- **Never** add new Dexie-specific features
+
+### 2. Data Integrity Rules
+
+#### Migration Safety
+- **Always** create backups before migration
+- **Always** verify data integrity after migration
+- **Never** delete legacy data until verified
+
+#### Rollback Strategy
+- **Always** maintain ability to rollback to Dexie
+- **Always** preserve migration logs
+- **Never** assume migration is irreversible
+
+### 3. Testing Requirements
+
+#### Migration Testing
+```typescript
+// Required test pattern for migration
+describe('Database Migration', () => {
+ it('should migrate data without loss', async () => {
+ // 1. Enable Dexie
+ // 2. Create test data
+ // 3. Run migration
+ // 4. Verify data integrity
+ // 5. Disable Dexie
+ });
+});
+```
+
+#### Application Testing
+```typescript
+// Required test pattern for application features
+describe('Feature with Database', () => {
+ it('should work with SQLite only', async () => {
+ // Test with USE_DEXIE_DB = false
+ // Verify all operations use PlatformService
+ });
+});
+```
+
+## Migration Fence Enforcement
+
+### 1. Static Analysis
+
+#### ESLint Rules
+```json
+{
+ "rules": {
+ "no-restricted-imports": [
+ "error",
+ {
+ "patterns": [
+ {
+ "group": ["../db/index"],
+ "message": "Use PlatformService instead of direct Dexie access"
+ }
+ ]
+ }
+ ]
+ }
+}
+```
+
+#### TypeScript Rules
+```json
+{
+ "compilerOptions": {
+ "strict": true,
+ "noImplicitAny": true
+ }
+}
+```
+
+### 2. Runtime Checks
+
+#### Development Mode Validation
+```typescript
+// Development-only fence validation
+if (import.meta.env.DEV && USE_DEXIE_DB) {
+ console.warn('⚠️ Dexie is enabled - migration mode active');
+}
+```
+
+#### Production Safety
+```typescript
+// Production fence enforcement
+if (import.meta.env.PROD && USE_DEXIE_DB) {
+ throw new Error('Dexie cannot be enabled in production');
+}
+```
+
+## Migration Fence Timeline
+
+### Phase 1: Fence Establishment ✅
+- [x] Define migration fence boundaries
+- [x] Implement PlatformService layer
+- [x] Create migration tools
+- [x] Set `USE_DEXIE_DB = false` by default
+
+### Phase 2: Data Migration 🔄
+- [x] Migrate core settings
+- [x] Migrate account data
+- [ ] Complete contact migration
+- [ ] Verify all data integrity
+
+### Phase 3: Code Cleanup 📋
+- [ ] Remove unused Dexie imports
+- [ ] Clean up legacy database code
+- [ ] Update all documentation
+- [ ] Remove migration tools
+
+### Phase 4: Fence Removal 🎯
+- [ ] Remove `USE_DEXIE_DB` constant
+- [ ] Remove Dexie dependencies
+- [ ] Remove migration service
+- [ ] Finalize SQLite-only architecture
+
+## Security Considerations
+
+### 1. Data Protection
+- **Encryption**: Maintain encryption standards across migration
+- **Access Control**: Preserve user privacy during migration
+- **Audit Trail**: Log all migration operations
+
+### 2. Error Handling
+- **Graceful Degradation**: Handle migration failures gracefully
+- **User Communication**: Clear messaging about migration status
+- **Recovery Options**: Provide rollback mechanisms
+
+## Performance Considerations
+
+### 1. Migration Performance
+- **Batch Operations**: Use transactions for bulk data transfer
+- **Progress Indicators**: Show migration progress to users
+- **Background Processing**: Non-blocking migration operations
+
+### 2. Application Performance
+- **Query Optimization**: Optimize SQLite queries for performance
+- **Indexing Strategy**: Maintain proper database indexes
+- **Memory Management**: Efficient memory usage during migration
+
+## Documentation Requirements
+
+### 1. Code Documentation
+- **Migration Fence Comments**: Document fence boundaries in code
+- **API Documentation**: Update all database API documentation
+- **Migration Guides**: Comprehensive migration documentation
+
+### 2. User Documentation
+- **Migration Instructions**: Clear user migration steps
+- **Troubleshooting**: Common migration issues and solutions
+- **Rollback Instructions**: How to revert if needed
+
+## Conclusion
+
+The migration fence provides a controlled boundary between legacy and new database systems, ensuring:
+- **Data Integrity**: No data loss during migration
+- **Application Stability**: Consistent behavior across platforms
+- **Development Clarity**: Clear guidelines for code development
+- **Migration Safety**: Controlled and reversible migration process
+
+This fence will remain in place until all data is successfully migrated and verified, at which point the legacy system can be safely removed.
\ No newline at end of file
diff --git a/doc/migration-security-checklist.md b/doc/migration-security-checklist.md
new file mode 100644
index 00000000..da219b69
--- /dev/null
+++ b/doc/migration-security-checklist.md
@@ -0,0 +1,355 @@
+# Database Migration Security Audit Checklist
+
+## Overview
+
+This document provides a comprehensive security audit checklist for the Dexie to SQLite migration in TimeSafari. The checklist ensures that data protection, privacy, and security are maintained throughout the migration process.
+
+## Pre-Migration Security Assessment
+
+### 1. Data Classification and Sensitivity
+
+- [ ] **Data Inventory**
+ - [ ] Identify all sensitive data types (DIDs, private keys, personal information)
+ - [ ] Document data retention requirements
+ - [ ] Map data relationships and dependencies
+ - [ ] Assess data sensitivity levels (public, internal, confidential, restricted)
+
+- [ ] **Encryption Assessment**
+ - [ ] Verify current encryption methods for sensitive data
+ - [ ] Document encryption keys and their management
+ - [ ] Assess encryption strength and compliance
+ - [ ] Plan encryption migration strategy
+
+### 2. Access Control Review
+
+- [ ] **User Access Rights**
+ - [ ] Audit current user permissions and roles
+ - [ ] Document access control mechanisms
+ - [ ] Verify principle of least privilege
+ - [ ] Plan access control migration
+
+- [ ] **System Access**
+ - [ ] Review database access patterns
+ - [ ] Document authentication mechanisms
+ - [ ] Assess session management
+ - [ ] Plan authentication migration
+
+### 3. Compliance Requirements
+
+- [ ] **Regulatory Compliance**
+ - [ ] Identify applicable regulations (GDPR, CCPA, etc.)
+ - [ ] Document data processing requirements
+ - [ ] Assess privacy impact
+ - [ ] Plan compliance verification
+
+- [ ] **Industry Standards**
+ - [ ] Review security standards compliance
+ - [ ] Document security controls
+ - [ ] Assess audit requirements
+ - [ ] Plan standards compliance
+
+## Migration Security Controls
+
+### 1. Data Protection During Migration
+
+- [ ] **Encryption in Transit**
+ - [ ] Verify all data transfers are encrypted
+ - [ ] Use secure communication protocols (TLS 1.3+)
+ - [ ] Implement secure API endpoints
+ - [ ] Monitor encryption status
+
+- [ ] **Encryption at Rest**
+ - [ ] Maintain encryption for stored data
+ - [ ] Verify encryption key management
+ - [ ] Test encryption/decryption processes
+ - [ ] Document encryption procedures
+
+### 2. Access Control During Migration
+
+- [ ] **Authentication**
+ - [ ] Maintain user authentication during migration
+ - [ ] Verify session management
+ - [ ] Implement secure token handling
+ - [ ] Monitor authentication events
+
+- [ ] **Authorization**
+ - [ ] Preserve user permissions during migration
+ - [ ] Verify role-based access control
+ - [ ] Implement audit logging
+ - [ ] Monitor access patterns
+
+### 3. Data Integrity
+
+- [ ] **Data Validation**
+ - [ ] Implement input validation for all data
+ - [ ] Verify data format consistency
+ - [ ] Test data transformation processes
+ - [ ] Document validation rules
+
+- [ ] **Data Verification**
+ - [ ] Implement checksums for data integrity
+ - [ ] Verify data completeness after migration
+ - [ ] Test data consistency checks
+ - [ ] Document verification procedures
+
+## Migration Process Security
+
+### 1. Backup Security
+
+- [ ] **Backup Creation**
+ - [ ] Create encrypted backups before migration
+ - [ ] Verify backup integrity
+ - [ ] Store backups securely
+ - [ ] Test backup restoration
+
+- [ ] **Backup Access**
+ - [ ] Limit backup access to authorized personnel
+ - [ ] Implement backup access logging
+ - [ ] Verify backup encryption
+ - [ ] Document backup procedures
+
+### 2. Migration Tool Security
+
+- [ ] **Tool Authentication**
+ - [ ] Implement secure authentication for migration tools
+ - [ ] Verify tool access controls
+ - [ ] Monitor tool usage
+ - [ ] Document tool security
+
+- [ ] **Tool Validation**
+ - [ ] Verify migration tool integrity
+ - [ ] Test tool security features
+ - [ ] Validate tool outputs
+ - [ ] Document tool validation
+
+### 3. Error Handling
+
+- [ ] **Error Security**
+ - [ ] Implement secure error handling
+ - [ ] Avoid information disclosure in errors
+ - [ ] Log security-relevant errors
+ - [ ] Document error procedures
+
+- [ ] **Recovery Security**
+ - [ ] Implement secure recovery procedures
+ - [ ] Verify recovery data protection
+ - [ ] Test recovery processes
+ - [ ] Document recovery security
+
+## Post-Migration Security
+
+### 1. Data Verification
+
+- [ ] **Data Completeness**
+ - [ ] Verify all data was migrated successfully
+ - [ ] Check for data corruption
+ - [ ] Validate data relationships
+ - [ ] Document verification results
+
+- [ ] **Data Accuracy**
+ - [ ] Verify data accuracy after migration
+ - [ ] Test data consistency
+ - [ ] Validate data integrity
+ - [ ] Document accuracy checks
+
+### 2. Access Control Verification
+
+- [ ] **User Access**
+ - [ ] Verify user access rights after migration
+ - [ ] Test authentication mechanisms
+ - [ ] Validate authorization rules
+ - [ ] Document access verification
+
+- [ ] **System Access**
+ - [ ] Verify system access controls
+ - [ ] Test API security
+ - [ ] Validate session management
+ - [ ] Document system security
+
+### 3. Security Testing
+
+- [ ] **Penetration Testing**
+ - [ ] Conduct security penetration testing
+ - [ ] Test for common vulnerabilities
+ - [ ] Verify security controls
+ - [ ] Document test results
+
+- [ ] **Vulnerability Assessment**
+ - [ ] Scan for security vulnerabilities
+ - [ ] Assess security posture
+ - [ ] Identify security gaps
+ - [ ] Document assessment results
+
+## Monitoring and Logging
+
+### 1. Security Monitoring
+
+- [ ] **Access Monitoring**
+ - [ ] Monitor database access patterns
+ - [ ] Track user authentication events
+ - [ ] Monitor system access
+ - [ ] Document monitoring procedures
+
+- [ ] **Data Monitoring**
+ - [ ] Monitor data access patterns
+ - [ ] Track data modification events
+ - [ ] Monitor data integrity
+ - [ ] Document data monitoring
+
+### 2. Security Logging
+
+- [ ] **Audit Logging**
+ - [ ] Implement comprehensive audit logging
+ - [ ] Log all security-relevant events
+ - [ ] Secure log storage and access
+ - [ ] Document logging procedures
+
+- [ ] **Log Analysis**
+ - [ ] Implement log analysis tools
+ - [ ] Monitor for security incidents
+ - [ ] Analyze security trends
+ - [ ] Document analysis procedures
+
+## Incident Response
+
+### 1. Security Incident Planning
+
+- [ ] **Incident Response Plan**
+ - [ ] Develop security incident response plan
+ - [ ] Define incident response procedures
+ - [ ] Train incident response team
+ - [ ] Document response procedures
+
+- [ ] **Incident Detection**
+ - [ ] Implement incident detection mechanisms
+ - [ ] Monitor for security incidents
+ - [ ] Establish incident reporting procedures
+ - [ ] Document detection procedures
+
+### 2. Recovery Procedures
+
+- [ ] **Data Recovery**
+ - [ ] Develop data recovery procedures
+ - [ ] Test recovery processes
+ - [ ] Verify recovery data integrity
+ - [ ] Document recovery procedures
+
+- [ ] **System Recovery**
+ - [ ] Develop system recovery procedures
+ - [ ] Test system recovery
+ - [ ] Verify system security after recovery
+ - [ ] Document recovery procedures
+
+## Compliance Verification
+
+### 1. Regulatory Compliance
+
+- [ ] **Privacy Compliance**
+ - [ ] Verify GDPR compliance
+ - [ ] Check CCPA compliance
+ - [ ] Assess other privacy regulations
+ - [ ] Document compliance status
+
+- [ ] **Security Compliance**
+ - [ ] Verify security standard compliance
+ - [ ] Check industry requirements
+ - [ ] Assess security certifications
+ - [ ] Document compliance status
+
+### 2. Audit Requirements
+
+- [ ] **Audit Trail**
+ - [ ] Maintain comprehensive audit trail
+ - [ ] Verify audit log integrity
+ - [ ] Test audit log accessibility
+ - [ ] Document audit procedures
+
+- [ ] **Audit Reporting**
+ - [ ] Generate audit reports
+ - [ ] Verify report accuracy
+ - [ ] Distribute reports securely
+ - [ ] Document reporting procedures
+
+## Documentation and Training
+
+### 1. Security Documentation
+
+- [ ] **Security Procedures**
+ - [ ] Document security procedures
+ - [ ] Update security policies
+ - [ ] Create security guidelines
+ - [ ] Maintain documentation
+
+- [ ] **Security Training**
+ - [ ] Develop security training materials
+ - [ ] Train staff on security procedures
+ - [ ] Verify training effectiveness
+ - [ ] Document training procedures
+
+### 2. Ongoing Security
+
+- [ ] **Security Maintenance**
+ - [ ] Establish security maintenance procedures
+ - [ ] Schedule security updates
+ - [ ] Monitor security trends
+ - [ ] Document maintenance procedures
+
+- [ ] **Security Review**
+ - [ ] Conduct regular security reviews
+ - [ ] Update security controls
+ - [ ] Assess security effectiveness
+ - [ ] Document review procedures
+
+## Risk Assessment
+
+### 1. Risk Identification
+
+- [ ] **Security Risks**
+ - [ ] Identify potential security risks
+ - [ ] Assess risk likelihood and impact
+ - [ ] Prioritize security risks
+ - [ ] Document risk assessment
+
+- [ ] **Mitigation Strategies**
+ - [ ] Develop risk mitigation strategies
+ - [ ] Implement risk controls
+ - [ ] Monitor risk status
+ - [ ] Document mitigation procedures
+
+### 2. Risk Monitoring
+
+- [ ] **Risk Tracking**
+ - [ ] Track identified risks
+ - [ ] Monitor risk status
+ - [ ] Update risk assessments
+ - [ ] Document risk tracking
+
+- [ ] **Risk Reporting**
+ - [ ] Generate risk reports
+ - [ ] Distribute risk information
+ - [ ] Update risk documentation
+ - [ ] Document reporting procedures
+
+## Conclusion
+
+This security audit checklist ensures that the database migration maintains the highest standards of data protection, privacy, and security. Regular review and updates of this checklist are essential to maintain security throughout the migration process and beyond.
+
+### Security Checklist Summary
+
+- [ ] **Pre-Migration Assessment**: Complete
+- [ ] **Migration Controls**: Complete
+- [ ] **Process Security**: Complete
+- [ ] **Post-Migration Verification**: Complete
+- [ ] **Monitoring and Logging**: Complete
+- [ ] **Incident Response**: Complete
+- [ ] **Compliance Verification**: Complete
+- [ ] **Documentation and Training**: Complete
+- [ ] **Risk Assessment**: Complete
+
+**Overall Security Status**: [ ] Secure [ ] Needs Attention [ ] Critical Issues
+
+**Next Review Date**: _______________
+
+**Reviewed By**: _______________
+
+**Approved By**: _______________
\ No newline at end of file
diff --git a/doc/migration-to-wa-sqlite.md b/doc/migration-to-wa-sqlite.md
index 627c112d..2249b0df 100644
--- a/doc/migration-to-wa-sqlite.md
+++ b/doc/migration-to-wa-sqlite.md
@@ -4,610 +4,223 @@
This document outlines the migration process from Dexie.js to absurd-sql for the TimeSafari app's storage implementation. The migration aims to provide a consistent SQLite-based storage solution across all platforms while maintaining data integrity and ensuring a smooth transition for users.
+**Current Status**: The migration is in **Phase 2** with a well-defined migration fence in place. Core settings and account data have been migrated, with contact migration in progress. **ActiveDid migration has been implemented** to ensure user identity continuity.
+
## Migration Goals
1. **Data Integrity**
- Preserve all existing data
- Maintain data relationships
- Ensure data consistency
+ - **Preserve user's active identity**
2. **Performance**
- Improve query performance
- Reduce storage overhead
- - Optimize for platform-specific features
-
-3. **Security**
- - Maintain or improve encryption
- - Preserve access controls
- - Enhance data protection
-
-4. **User Experience**
- - Zero data loss
- - Minimal downtime
- - Automatic migration where possible
-
-## Prerequisites
-
-1. **Backup Requirements**
- ```typescript
- interface MigrationBackup {
- timestamp: number;
- accounts: Account[];
- settings: Setting[];
- contacts: Contact[];
- metadata: {
- version: string;
- platform: string;
- dexieVersion: string;
- };
- }
- ```
-
-2. **Dependencies**
- ```json
- {
- "@jlongster/sql.js": "^1.8.0",
- "absurd-sql": "^1.8.0"
- }
- ```
-
-3. **Storage Requirements**
- - Sufficient IndexedDB quota
- - Available disk space for SQLite
- - Backup storage space
-
-4. **Platform Support**
- - Web: Modern browser with IndexedDB support
- - iOS: iOS 13+ with SQLite support
- - Android: Android 5+ with SQLite support
- - Electron: Latest version with SQLite support
+ - Optimize for platform-specific capabilities
-## Migration Process
+3. **User Experience**
+ - Seamless transition with no data loss
+ - Maintain user's active identity and preferences
+ - Preserve application state
+
+## Migration Architecture
+
+### Migration Fence
+The migration fence is defined by the `USE_DEXIE_DB` constant in `src/constants/app.ts`:
+- `USE_DEXIE_DB = false` (default): Uses SQLite database
+- `USE_DEXIE_DB = true`: Uses Dexie database (for migration purposes)
+
+### Migration Order
+The migration follows a specific order to maintain data integrity:
+
+1. **Accounts** (foundational - contains DIDs)
+2. **Settings** (references accountDid, activeDid)
+3. **ActiveDid** (depends on accounts and settings) ⭐ **NEW**
+4. **Contacts** (independent, but migrated after accounts for consistency)
+
+## ActiveDid Migration ⭐ **NEW FEATURE**
-### 1. Preparation
+### Problem Solved
+Previously, the `activeDid` setting was not migrated from Dexie to SQLite, causing users to lose their active identity after migration.
+### Solution Implemented
+The migration now includes a dedicated step for migrating the `activeDid`:
+
+1. **Detection**: Identifies the `activeDid` from Dexie master settings
+2. **Validation**: Verifies the `activeDid` exists in SQLite accounts
+3. **Migration**: Updates SQLite master settings with the `activeDid`
+4. **Error Handling**: Graceful handling of missing accounts
+
+### Implementation Details
+
+#### New Function: `migrateActiveDid()`
```typescript
-// src/services/storage/migration/MigrationService.ts
-import initSqlJs from '@jlongster/sql.js';
-import { SQLiteFS } from 'absurd-sql';
-import IndexedDBBackend from 'absurd-sql/dist/indexeddb-backend';
-
-export class MigrationService {
- private static instance: MigrationService;
- private backup: MigrationBackup | null = null;
- private sql: any = null;
- private db: any = null;
-
- async prepare(): Promise {
- try {
- // 1. Check prerequisites
- await this.checkPrerequisites();
-
- // 2. Create backup
- this.backup = await this.createBackup();
-
- // 3. Verify backup integrity
- await this.verifyBackup();
-
- // 4. Initialize absurd-sql
- await this.initializeAbsurdSql();
- } catch (error) {
- throw new StorageError(
- 'Migration preparation failed',
- StorageErrorCodes.MIGRATION_FAILED,
- error
- );
- }
- }
-
- private async initializeAbsurdSql(): Promise {
- // Initialize SQL.js
- this.sql = await initSqlJs({
- locateFile: (file: string) => {
- return new URL(`/node_modules/@jlongster/sql.js/dist/${file}`, import.meta.url).href;
- }
- });
-
- // Setup SQLiteFS with IndexedDB backend
- const sqlFS = new SQLiteFS(this.sql.FS, new IndexedDBBackend());
- this.sql.register_for_idb(sqlFS);
-
- // Create and mount filesystem
- this.sql.FS.mkdir('/sql');
- this.sql.FS.mount(sqlFS, {}, '/sql');
-
- // Open database
- const path = '/sql/db.sqlite';
- if (typeof SharedArrayBuffer === 'undefined') {
- let stream = this.sql.FS.open(path, 'a+');
- await stream.node.contents.readIfFallback();
- this.sql.FS.close(stream);
- }
-
- this.db = new this.sql.Database(path, { filename: true });
- if (!this.db) {
- throw new StorageError(
- 'Database initialization failed',
- StorageErrorCodes.INITIALIZATION_FAILED
- );
- }
-
- // Configure database
- await this.db.exec(`PRAGMA journal_mode=MEMORY;`);
- }
-
- private async checkPrerequisites(): Promise {
- // Check IndexedDB availability
- if (!window.indexedDB) {
- throw new StorageError(
- 'IndexedDB not available',
- StorageErrorCodes.INITIALIZATION_FAILED
- );
- }
-
- // Check storage quota
- const quota = await navigator.storage.estimate();
- if (quota.quota && quota.usage && quota.usage > quota.quota * 0.9) {
- throw new StorageError(
- 'Insufficient storage space',
- StorageErrorCodes.STORAGE_FULL
- );
- }
-
- // Check platform support
- const capabilities = await PlatformDetection.getCapabilities();
- if (!capabilities.hasFileSystem) {
- throw new StorageError(
- 'Platform does not support required features',
- StorageErrorCodes.INITIALIZATION_FAILED
- );
- }
- }
-
- private async createBackup(): Promise {
- const dexieDB = new Dexie('TimeSafariDB');
-
- return {
- timestamp: Date.now(),
- accounts: await dexieDB.accounts.toArray(),
- settings: await dexieDB.settings.toArray(),
- contacts: await dexieDB.contacts.toArray(),
- metadata: {
- version: '1.0.0',
- platform: await PlatformDetection.getPlatform(),
- dexieVersion: Dexie.version
- }
- };
- }
+export async function migrateActiveDid(): Promise {
+ // 1. Get Dexie settings to find the activeDid
+ const dexieSettings = await getDexieSettings();
+ const masterSettings = dexieSettings.find(setting => !setting.accountDid);
+
+ // 2. Verify the activeDid exists in SQLite accounts
+ const accountExists = await platformService.dbQuery(
+ "SELECT did FROM accounts WHERE did = ?",
+ [dexieActiveDid],
+ );
+
+ // 3. Update SQLite master settings
+ await updateDefaultSettings({ activeDid: dexieActiveDid });
}
```
-### 2. Data Migration
+#### Enhanced `migrateSettings()` Function
+The settings migration now includes activeDid handling:
+- Extracts `activeDid` from Dexie master settings
+- Validates account existence in SQLite
+- Updates SQLite master settings with the `activeDid`
+#### Updated `migrateAll()` Function
+The complete migration now includes a dedicated step for activeDid:
```typescript
-// src/services/storage/migration/DataMigration.ts
-export class DataMigration {
- async migrate(backup: MigrationBackup): Promise {
- try {
- // 1. Create new database schema
- await this.createSchema();
-
- // 2. Migrate accounts
- await this.migrateAccounts(backup.accounts);
-
- // 3. Migrate settings
- await this.migrateSettings(backup.settings);
-
- // 4. Migrate contacts
- await this.migrateContacts(backup.contacts);
-
- // 5. Verify migration
- await this.verifyMigration(backup);
- } catch (error) {
- // 6. Handle failure
- await this.handleMigrationFailure(error, backup);
- }
- }
-
- private async migrateAccounts(accounts: Account[]): Promise {
- // Use transaction for atomicity
- await this.db.exec('BEGIN TRANSACTION;');
- try {
- for (const account of accounts) {
- await this.db.run(`
- INSERT INTO accounts (did, public_key_hex, created_at, updated_at)
- VALUES (?, ?, ?, ?)
- `, [
- account.did,
- account.publicKeyHex,
- account.createdAt,
- account.updatedAt
- ]);
- }
- await this.db.exec('COMMIT;');
- } catch (error) {
- await this.db.exec('ROLLBACK;');
- throw error;
- }
- }
-
- private async verifyMigration(backup: MigrationBackup): Promise {
- // Verify account count
- const result = await this.db.exec('SELECT COUNT(*) as count FROM accounts');
- const accountCount = result[0].values[0][0];
-
- if (accountCount !== backup.accounts.length) {
- throw new StorageError(
- 'Account count mismatch',
- StorageErrorCodes.VERIFICATION_FAILED
- );
- }
-
- // Verify data integrity
- await this.verifyDataIntegrity(backup);
- }
-}
+// Step 3: Migrate ActiveDid (depends on accounts and settings)
+logger.info("[MigrationService] Step 3: Migrating activeDid...");
+const activeDidResult = await migrateActiveDid();
```
-### 3. Rollback Strategy
+### Benefits
+- ✅ **User Identity Preservation**: Users maintain their active identity
+- ✅ **Seamless Experience**: No need to manually select identity after migration
+- ✅ **Data Consistency**: Ensures all identity-related settings are preserved
+- ✅ **Error Resilience**: Graceful handling of edge cases
+
+## Migration Process
+### Phase 1: Preparation ✅
+- [x] Enable Dexie database access
+- [x] Implement data comparison tools
+- [x] Create migration service structure
+
+### Phase 2: Core Migration ✅
+- [x] Account migration with `importFromMnemonic`
+- [x] Settings migration (excluding activeDid)
+- [x] **ActiveDid migration** ⭐ **COMPLETED**
+- [x] Contact migration framework
+
+### Phase 3: Validation and Cleanup 🔄
+- [ ] Comprehensive data validation
+- [ ] Performance testing
+- [ ] User acceptance testing
+- [ ] Dexie removal
+
+## Usage
+
+### Manual Migration
```typescript
-// src/services/storage/migration/RollbackService.ts
-export class RollbackService {
- async rollback(backup: MigrationBackup): Promise {
- try {
- // 1. Stop all database operations
- await this.stopDatabaseOperations();
-
- // 2. Restore from backup
- await this.restoreFromBackup(backup);
-
- // 3. Verify restoration
- await this.verifyRestoration(backup);
-
- // 4. Clean up absurd-sql
- await this.cleanupAbsurdSql();
- } catch (error) {
- throw new StorageError(
- 'Rollback failed',
- StorageErrorCodes.ROLLBACK_FAILED,
- error
- );
- }
- }
-
- private async restoreFromBackup(backup: MigrationBackup): Promise {
- const dexieDB = new Dexie('TimeSafariDB');
-
- // Restore accounts
- await dexieDB.accounts.bulkPut(backup.accounts);
-
- // Restore settings
- await dexieDB.settings.bulkPut(backup.settings);
-
- // Restore contacts
- await dexieDB.contacts.bulkPut(backup.contacts);
- }
-}
+import { migrateAll, migrateActiveDid } from '../services/indexedDBMigrationService';
+
+// Complete migration
+const result = await migrateAll();
+
+// Or migrate just the activeDid
+const activeDidResult = await migrateActiveDid();
```
-## Migration UI
-
-```vue
-
-
-
-
Database Migration
-
-
-
-
{{ progress }}%
-
-
-
{{ statusMessage }}
-
-
- {{ error }}
-
-
-
-
-
-
+## Error Handling
-
+# Run migration
+npm run migrate
+
+# Verify results
+npm run test:migration
+```
+
+### ActiveDid Testing
+```typescript
+// Test activeDid migration specifically
+const result = await migrateActiveDid();
+expect(result.success).toBe(true);
+expect(result.warnings).toContain('Successfully migrated activeDid');
```
-## Testing Strategy
-
-1. **Unit Tests**
- ```typescript
- // src/services/storage/migration/__tests__/MigrationService.spec.ts
- describe('MigrationService', () => {
- it('should initialize absurd-sql correctly', async () => {
- const service = MigrationService.getInstance();
- await service.initializeAbsurdSql();
-
- expect(service.isInitialized()).toBe(true);
- expect(service.getDatabase()).toBeDefined();
- });
-
- it('should create valid backup', async () => {
- const service = MigrationService.getInstance();
- const backup = await service.createBackup();
-
- expect(backup).toBeDefined();
- expect(backup.accounts).toBeInstanceOf(Array);
- expect(backup.settings).toBeInstanceOf(Array);
- expect(backup.contacts).toBeInstanceOf(Array);
- });
-
- it('should migrate data correctly', async () => {
- const service = MigrationService.getInstance();
- const backup = await service.createBackup();
-
- await service.migrate(backup);
-
- // Verify migration
- const accounts = await service.getMigratedAccounts();
- expect(accounts).toHaveLength(backup.accounts.length);
- });
-
- it('should handle rollback correctly', async () => {
- const service = MigrationService.getInstance();
- const backup = await service.createBackup();
-
- // Simulate failed migration
- await service.migrate(backup);
- await service.simulateFailure();
-
- // Perform rollback
- await service.rollback(backup);
-
- // Verify rollback
- const accounts = await service.getOriginalAccounts();
- expect(accounts).toHaveLength(backup.accounts.length);
- });
- });
- ```
-
-2. **Integration Tests**
- ```typescript
- // src/services/storage/migration/__tests__/integration/Migration.spec.ts
- describe('Migration Integration', () => {
- it('should handle concurrent access during migration', async () => {
- const service = MigrationService.getInstance();
-
- // Start migration
- const migrationPromise = service.migrate();
-
- // Simulate concurrent access
- const accessPromises = Array(5).fill(null).map(() =>
- service.getAccount('did:test:123')
- );
-
- // Wait for all operations
- const [migrationResult, ...accessResults] = await Promise.allSettled([
- migrationPromise,
- ...accessPromises
- ]);
-
- // Verify results
- expect(migrationResult.status).toBe('fulfilled');
- expect(accessResults.some(r => r.status === 'rejected')).toBe(true);
- });
-
- it('should maintain data integrity during platform transition', async () => {
- const service = MigrationService.getInstance();
-
- // Simulate platform change
- await service.simulatePlatformChange();
-
- // Verify data
- const accounts = await service.getAllAccounts();
- const settings = await service.getAllSettings();
- const contacts = await service.getAllContacts();
-
- expect(accounts).toBeDefined();
- expect(settings).toBeDefined();
- expect(contacts).toBeDefined();
- });
- });
- ```
-
-## Success Criteria
+## Troubleshooting
-1. **Data Integrity**
- - [ ] All accounts migrated successfully
- - [ ] All settings preserved
- - [ ] All contacts transferred
- - [ ] No data corruption
+### Common Issues
-2. **Performance**
- - [ ] Migration completes within acceptable time
- - [ ] No significant performance degradation
- - [ ] Efficient storage usage
- - [ ] Smooth user experience
-
-3. **Security**
- - [ ] Encrypted data remains secure
- - [ ] Access controls maintained
- - [ ] No sensitive data exposure
- - [ ] Secure backup process
-
-4. **User Experience**
- - [ ] Clear migration progress
- - [ ] Informative error messages
- - [ ] Automatic recovery from failures
- - [ ] No data loss
-
-## Rollback Plan
-
-1. **Automatic Rollback**
- - Triggered by migration failure
- - Restores from verified backup
- - Maintains data consistency
- - Logs rollback reason
-
-2. **Manual Rollback**
- - Available through settings
- - Requires user confirmation
- - Preserves backup data
- - Provides rollback status
-
-3. **Emergency Recovery**
- - Manual backup restoration
- - Database repair tools
- - Data recovery procedures
- - Support contact information
-
-## Post-Migration
-
-1. **Verification**
- - Data integrity checks
- - Performance monitoring
- - Error rate tracking
- - User feedback collection
-
-2. **Cleanup**
- - Remove old database
- - Clear migration artifacts
- - Update application state
- - Archive backup data
-
-3. **Monitoring**
- - Track migration success rate
- - Monitor performance metrics
- - Collect error reports
- - Gather user feedback
-
-## Support
-
-For assistance with migration:
-1. Check the troubleshooting guide
-2. Review error logs
-3. Contact support team
-4. Submit issue report
-
-## Timeline
-
-1. **Preparation Phase** (1 week)
- - Backup system implementation
- - Migration service development
- - Testing framework setup
-
-2. **Testing Phase** (2 weeks)
- - Unit testing
- - Integration testing
- - Performance testing
- - Security testing
-
-3. **Deployment Phase** (1 week)
- - Staged rollout
- - Monitoring
- - Support preparation
- - Documentation updates
-
-4. **Post-Deployment** (2 weeks)
- - Monitoring
- - Bug fixes
- - Performance optimization
- - User feedback collection
\ No newline at end of file
+1. **ActiveDid Not Found**
+ - Ensure accounts were migrated before activeDid migration
+ - Check that the Dexie activeDid exists in SQLite accounts
+
+2. **Migration Failures**
+ - Verify Dexie database is accessible
+ - Check SQLite database permissions
+ - Review migration logs for specific errors
+
+3. **Data Inconsistencies**
+ - Use `compareDatabases()` to identify differences
+ - Re-run migration if necessary
+ - Check for duplicate or conflicting records
+
+### Debugging
+```typescript
+// Enable detailed logging
+logger.setLevel('debug');
+
+// Check migration status
+const comparison = await compareDatabases();
+console.log('Settings differences:', comparison.differences.settings);
+```
+
+## Future Enhancements
+
+### Planned Improvements
+1. **Batch Processing**: Optimize for large datasets
+2. **Incremental Migration**: Support partial migrations
+3. **Rollback Capability**: Ability to revert migration
+4. **Progress Tracking**: Real-time migration progress
+
+### Performance Optimizations
+1. **Parallel Processing**: Migrate independent data concurrently
+2. **Memory Management**: Optimize for large datasets
+3. **Transaction Batching**: Reduce database round trips
+
+## Conclusion
+
+The Dexie to SQLite migration provides a robust, secure, and user-friendly transition path. The addition of activeDid migration ensures that users maintain their identity continuity throughout the migration process, significantly improving the user experience.
+
+The migration fence architecture allows for controlled, reversible migration while maintaining application stability and data integrity.
\ No newline at end of file
diff --git a/ios/App/App.xcodeproj/project.pbxproj b/ios/App/App.xcodeproj/project.pbxproj
index 17665bc7..01ed14fe 100644
--- a/ios/App/App.xcodeproj/project.pbxproj
+++ b/ios/App/App.xcodeproj/project.pbxproj
@@ -403,7 +403,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 26;
+ CURRENT_PROJECT_VERSION = 35;
DEVELOPMENT_TEAM = GM3FS5JQPH;
ENABLE_APP_SANDBOX = NO;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
@@ -413,7 +413,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
- MARKETING_VERSION = 0.5.1;
+ MARKETING_VERSION = 1.0.2;
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -430,7 +430,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 26;
+ CURRENT_PROJECT_VERSION = 35;
DEVELOPMENT_TEAM = GM3FS5JQPH;
ENABLE_APP_SANDBOX = NO;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
@@ -440,7 +440,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
- MARKETING_VERSION = 0.5.1;
+ MARKETING_VERSION = 1.0.2;
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
diff --git a/ios/App/App/Info.plist b/ios/App/App/Info.plist
index b0b1f21a..14ca68c1 100644
--- a/ios/App/App/Info.plist
+++ b/ios/App/App/Info.plist
@@ -49,5 +49,16 @@
UIViewControllerBasedStatusBarAppearance
+ CFBundleURLTypes
+
+
+ CFBundleURLName
+ app.timesafari
+ CFBundleURLSchemes
+
+ timesafari
+
+
+
diff --git a/package-lock.json b/package-lock.json
index 32c3ce63..a0f90964 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "timesafari",
- "version": "0.4.8",
+ "version": "1.0.3-beta",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "timesafari",
- "version": "0.4.8",
+ "version": "1.0.3-beta",
"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.11",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
- "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4524,9 +4524,9 @@
}
},
"node_modules/@eslint/eslintrc/node_modules/brace-expansion": {
- "version": "1.1.11",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
- "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -5147,9 +5147,9 @@
}
},
"node_modules/@expo/cli": {
- "version": "0.24.14",
- "resolved": "https://registry.npmjs.org/@expo/cli/-/cli-0.24.14.tgz",
- "integrity": "sha512-o+QYyfIBhSRTgaywKTLJhm2Fg5PrSeUVCXS+uQySamgoMjLNhHa8QwE64mW/FmJr5hZLiqUEQxb60FK4JcyqXg==",
+ "version": "0.24.15",
+ "resolved": "https://registry.npmjs.org/@expo/cli/-/cli-0.24.15.tgz",
+ "integrity": "sha512-RDZS30OSnbXkSPnBXdyPL29KbltjOmegE23bZZDiGV23WOReWcPgRc5U7Fd8eLPhtRjHBKlBpNJMTed5Ntr/uw==",
"license": "MIT",
"optional": true,
"peer": true,
@@ -5158,20 +5158,20 @@
"@babel/runtime": "^7.20.0",
"@expo/code-signing-certificates": "^0.0.5",
"@expo/config": "~11.0.10",
- "@expo/config-plugins": "~10.0.2",
+ "@expo/config-plugins": "~10.0.3",
"@expo/devcert": "^1.1.2",
"@expo/env": "~1.0.5",
"@expo/image-utils": "^0.7.4",
"@expo/json-file": "^9.1.4",
- "@expo/metro-config": "~0.20.14",
+ "@expo/metro-config": "~0.20.15",
"@expo/osascript": "^2.2.4",
"@expo/package-manager": "^1.8.4",
"@expo/plist": "^0.3.4",
- "@expo/prebuild-config": "^9.0.6",
+ "@expo/prebuild-config": "^9.0.7",
"@expo/spawn-async": "^1.7.2",
"@expo/ws-tunnel": "^1.0.1",
"@expo/xcpretty": "^4.3.0",
- "@react-native/dev-middleware": "0.79.3",
+ "@react-native/dev-middleware": "0.79.4",
"@urql/core": "^5.0.6",
"@urql/exchange-retry": "^1.3.0",
"accepts": "^1.3.8",
@@ -5602,20 +5602,20 @@
}
},
"node_modules/@expo/config-plugins": {
- "version": "10.0.2",
- "resolved": "https://registry.npmjs.org/@expo/config-plugins/-/config-plugins-10.0.2.tgz",
- "integrity": "sha512-TzUn3pPdpwCS0yYaSlZOClgDmCX8N4I2lfgitX5oStqmvpPtB+vqtdyqsVM02fQ2tlJIAqwBW+NHaHqqy8Jv7g==",
+ "version": "10.0.3",
+ "resolved": "https://registry.npmjs.org/@expo/config-plugins/-/config-plugins-10.0.3.tgz",
+ "integrity": "sha512-fjCckkde67pSDf48x7wRuPsgQVIqlDwN7NlOk9/DFgQ1hCH0L5pGqoSmikA1vtAyiA83MOTpkGl3F3wyATyUog==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
- "@expo/config-types": "^53.0.3",
+ "@expo/config-types": "^53.0.4",
"@expo/json-file": "~9.1.4",
"@expo/plist": "^0.3.4",
"@expo/sdk-runtime-versions": "^1.0.0",
"chalk": "^4.1.2",
"debug": "^4.3.5",
- "getenv": "^1.0.0",
+ "getenv": "^2.0.0",
"glob": "^10.4.2",
"resolve-from": "^5.0.0",
"semver": "^7.5.4",
@@ -5644,17 +5644,6 @@
}
}
},
- "node_modules/@expo/config-plugins/node_modules/getenv": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/getenv/-/getenv-1.0.0.tgz",
- "integrity": "sha512-7yetJWqbS9sbn0vIfliPsFgoXMKn/YMF+Wuiog97x+urnSRRRZ7xB+uVkwGKzRgq9CDFfMQnE9ruL5DHv9c6Xg==",
- "license": "MIT",
- "optional": true,
- "peer": true,
- "engines": {
- "node": ">=6"
- }
- },
"node_modules/@expo/config-plugins/node_modules/glob": {
"version": "10.4.5",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
@@ -5956,9 +5945,9 @@
}
},
"node_modules/@expo/fingerprint": {
- "version": "0.13.0",
- "resolved": "https://registry.npmjs.org/@expo/fingerprint/-/fingerprint-0.13.0.tgz",
- "integrity": "sha512-3IwpH0p3uO8jrJSLOUNDzJVh7VEBod0emnCBq0hD72sy6ICmzauM6Xf4he+2Tip7fzImCJRd63GaehV+CCtpvA==",
+ "version": "0.13.1",
+ "resolved": "https://registry.npmjs.org/@expo/fingerprint/-/fingerprint-0.13.1.tgz",
+ "integrity": "sha512-MgZ5uIvvwAnjWeQoj4D3RnBXjD1GNOpCvhp2jtZWdQ8yEokhDEJGoHjsMT8/NCB5m2fqP5sv2V5nPzC7CN1YjQ==",
"license": "MIT",
"optional": true,
"peer": true,
@@ -5969,6 +5958,7 @@
"debug": "^4.3.4",
"find-up": "^5.0.0",
"getenv": "^2.0.0",
+ "glob": "^10.4.2",
"ignore": "^5.3.1",
"minimatch": "^9.0.0",
"p-limit": "^3.1.0",
@@ -5979,6 +5969,56 @@
"fingerprint": "bin/cli.js"
}
},
+ "node_modules/@expo/fingerprint/node_modules/glob": {
+ "version": "10.4.5",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
+ "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
+ "license": "ISC",
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "foreground-child": "^3.1.0",
+ "jackspeak": "^3.1.2",
+ "minimatch": "^9.0.4",
+ "minipass": "^7.1.2",
+ "package-json-from-dist": "^1.0.0",
+ "path-scurry": "^1.11.1"
+ },
+ "bin": {
+ "glob": "dist/esm/bin.mjs"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@expo/fingerprint/node_modules/minimatch": {
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+ "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+ "license": "ISC",
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@expo/fingerprint/node_modules/minipass": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
+ "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
+ "license": "ISC",
+ "optional": true,
+ "peer": true,
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
"node_modules/@expo/fingerprint/node_modules/resolve-from": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",
@@ -6055,9 +6095,9 @@
}
},
"node_modules/@expo/metro-config": {
- "version": "0.20.14",
- "resolved": "https://registry.npmjs.org/@expo/metro-config/-/metro-config-0.20.14.tgz",
- "integrity": "sha512-tYDDubuZycK+NX00XN7BMu73kBur/evOPcKfxc+UBeFfgN2EifOITtdwSUDdRsbtJ2OnXwMY1HfRUG3Lq3l4cw==",
+ "version": "0.20.15",
+ "resolved": "https://registry.npmjs.org/@expo/metro-config/-/metro-config-0.20.15.tgz",
+ "integrity": "sha512-m8i58IQ7I8iOdVRfOhFmhPMHuhgeTVfQp1+mxW7URqPZaeVbuDVktPqOiNoHraKBoGPLKMUSsD+qdUuJVL3wMg==",
"license": "MIT",
"optional": true,
"peer": true,
@@ -6066,7 +6106,7 @@
"@babel/generator": "^7.20.5",
"@babel/parser": "^7.20.0",
"@babel/types": "^7.20.0",
- "@expo/config": "~11.0.9",
+ "@expo/config": "~11.0.10",
"@expo/env": "~1.0.5",
"@expo/json-file": "~9.1.4",
"@expo/spawn-async": "^1.7.2",
@@ -6074,7 +6114,7 @@
"debug": "^4.3.2",
"dotenv": "~16.4.5",
"dotenv-expand": "~11.0.6",
- "getenv": "^1.0.0",
+ "getenv": "^2.0.0",
"glob": "^10.4.2",
"jsc-safe-url": "^0.2.4",
"lightningcss": "~1.27.0",
@@ -6097,17 +6137,6 @@
"url": "https://dotenvx.com"
}
},
- "node_modules/@expo/metro-config/node_modules/getenv": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/getenv/-/getenv-1.0.0.tgz",
- "integrity": "sha512-7yetJWqbS9sbn0vIfliPsFgoXMKn/YMF+Wuiog97x+urnSRRRZ7xB+uVkwGKzRgq9CDFfMQnE9ruL5DHv9c6Xg==",
- "license": "MIT",
- "optional": true,
- "peer": true,
- "engines": {
- "node": ">=6"
- }
- },
"node_modules/@expo/metro-config/node_modules/glob": {
"version": "10.4.5",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
@@ -6468,19 +6497,19 @@
}
},
"node_modules/@expo/prebuild-config": {
- "version": "9.0.6",
- "resolved": "https://registry.npmjs.org/@expo/prebuild-config/-/prebuild-config-9.0.6.tgz",
- "integrity": "sha512-HDTdlMkTQZ95rd6EpvuLM+xkZV03yGLc38FqI37qKFLJtUN1WnYVaWsuXKoljd1OrVEVsHe6CfqKwaPZ52D56Q==",
+ "version": "9.0.7",
+ "resolved": "https://registry.npmjs.org/@expo/prebuild-config/-/prebuild-config-9.0.7.tgz",
+ "integrity": "sha512-1w5MBp6NdF51gPGp0HsCZt0QC82hZWo37wI9HfxhdQF/sN/92Mh4t30vaY7gjHe71T5QNyab00oxZH/wP0MDgQ==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
- "@expo/config": "~11.0.9",
- "@expo/config-plugins": "~10.0.2",
+ "@expo/config": "~11.0.10",
+ "@expo/config-plugins": "~10.0.3",
"@expo/config-types": "^53.0.4",
"@expo/image-utils": "^0.7.4",
"@expo/json-file": "^9.1.4",
- "@react-native/normalize-colors": "0.79.2",
+ "@react-native/normalize-colors": "0.79.4",
"debug": "^4.3.1",
"resolve-from": "^5.0.0",
"semver": "^7.6.0",
@@ -6686,9 +6715,9 @@
}
},
"node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": {
- "version": "1.1.11",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
- "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -6983,6 +7012,29 @@
"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",
@@ -7781,13 +7833,13 @@
}
},
"node_modules/@playwright/test": {
- "version": "1.53.0",
- "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.53.0.tgz",
- "integrity": "sha512-15hjKreZDcp7t6TL/7jkAo6Df5STZN09jGiv5dbP9A6vMVncXRqE7/B2SncsyOwrkZRBH2i6/TPOL8BVmm3c7w==",
+ "version": "1.53.1",
+ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.53.1.tgz",
+ "integrity": "sha512-Z4c23LHV0muZ8hfv4jw6HngPJkbbtZxTkxPNIg7cJcTc9C28N/p2q7g3JZS2SiKBBHJ3uM1dgDye66bB7LEk5w==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
- "playwright": "1.53.0"
+ "playwright": "1.53.1"
},
"bin": {
"playwright": "cli.js"
@@ -7839,9 +7891,9 @@
}
},
"node_modules/@react-native/assets-registry": {
- "version": "0.79.3",
- "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.79.3.tgz",
- "integrity": "sha512-Vy8DQXCJ21YSAiHxrNBz35VqVlZPpRYm50xRTWRf660JwHuJkFQG8cUkrLzm7AUriqUXxwpkQHcY+b0ibw9ejQ==",
+ "version": "0.80.0",
+ "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.80.0.tgz",
+ "integrity": "sha512-MlScsKAz99zoYghe5Rf5mUqsqz2rMB02640NxtPtBMSHNdGxxRlWu/pp1bFexDa1DYJwyIjnLgt3Z/Y90ikHfw==",
"license": "MIT",
"optional": true,
"peer": true,
@@ -7850,24 +7902,24 @@
}
},
"node_modules/@react-native/babel-plugin-codegen": {
- "version": "0.79.3",
- "resolved": "https://registry.npmjs.org/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.79.3.tgz",
- "integrity": "sha512-Zb8F4bSEKKZfms5n1MQ0o5mudDcpAINkKiFuFTU0PErYGjY3kZ+JeIP+gS6KCXsckxCfMEKQwqKicP/4DWgsZQ==",
+ "version": "0.79.4",
+ "resolved": "https://registry.npmjs.org/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.79.4.tgz",
+ "integrity": "sha512-quhytIlDedR3ircRwifa22CaWVUVnkxccrrgztroCZaemSJM+HLurKJrjKWm0J5jV9ed+d+9Qyb1YB0syTHDjg==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@babel/traverse": "^7.25.3",
- "@react-native/codegen": "0.79.3"
+ "@react-native/codegen": "0.79.4"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@react-native/babel-preset": {
- "version": "0.79.3",
- "resolved": "https://registry.npmjs.org/@react-native/babel-preset/-/babel-preset-0.79.3.tgz",
- "integrity": "sha512-VHGNP02bDD2Ul1my0pLVwe/0dsEBHxR343ySpgnkCNEEm9C1ANQIL2wvnJrHZPcqfAkWfFQ8Ln3t+6fdm4A/Dg==",
+ "version": "0.79.4",
+ "resolved": "https://registry.npmjs.org/@react-native/babel-preset/-/babel-preset-0.79.4.tgz",
+ "integrity": "sha512-El9JvYKiNfnkQ3qR7zJvvRdP3DX2i4BGYlIricWQishI3gWAfm88FQYFC2CcGoMQWJQEPN4jnDMpoISAJDEN4g==",
"license": "MIT",
"optional": true,
"peer": true,
@@ -7913,7 +7965,7 @@
"@babel/plugin-transform-typescript": "^7.25.2",
"@babel/plugin-transform-unicode-regex": "^7.24.7",
"@babel/template": "^7.25.0",
- "@react-native/babel-plugin-codegen": "0.79.3",
+ "@react-native/babel-plugin-codegen": "0.79.4",
"babel-plugin-syntax-hermes-parser": "0.25.1",
"babel-plugin-transform-flow-enums": "^0.0.2",
"react-refresh": "^0.14.0"
@@ -7926,9 +7978,9 @@
}
},
"node_modules/@react-native/codegen": {
- "version": "0.79.3",
- "resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.79.3.tgz",
- "integrity": "sha512-CZejXqKch/a5/s/MO5T8mkAgvzCXgsTkQtpCF15kWR9HN8T+16k0CsN7TXAxXycltoxiE3XRglOrZNEa/TiZUQ==",
+ "version": "0.79.4",
+ "resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.79.4.tgz",
+ "integrity": "sha512-K0moZDTJtqZqSs+u9tnDPSxNsdxi5irq8Nu4mzzOYlJTVNGy5H9BiIDg/NeKGfjAdo43yTDoaPSbUCvVV8cgIw==",
"license": "MIT",
"optional": true,
"peer": true,
@@ -7947,20 +7999,20 @@
}
},
"node_modules/@react-native/community-cli-plugin": {
- "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==",
+ "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==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
- "@react-native/dev-middleware": "0.79.3",
+ "@react-native/dev-middleware": "0.80.0",
"chalk": "^4.0.0",
- "debug": "^2.2.0",
+ "debug": "^4.4.0",
"invariant": "^2.2.4",
- "metro": "^0.82.0",
- "metro-config": "^0.82.0",
- "metro-core": "^0.82.0",
+ "metro": "^0.82.2",
+ "metro-config": "^0.82.2",
+ "metro-core": "^0.82.2",
"semver": "^7.1.3"
},
"engines": {
@@ -7975,29 +8027,101 @@
}
}
},
+ "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": "2.6.9",
- "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
- "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
+ "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
- "ms": "2.0.0"
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
}
},
"node_modules/@react-native/community-cli-plugin/node_modules/ms": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
- "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"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",
- "integrity": "sha512-ImNDuEeKH6lEsLXms3ZsgIrNF94jymfuhPcVY5L0trzaYNo9ZFE9Ni2/18E1IbfXxdeIHrCSBJlWD6CTm7wu5A==",
+ "version": "0.79.4",
+ "resolved": "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.79.4.tgz",
+ "integrity": "sha512-Gg4LhxHIK86Bi2RiT1rbFAB6fuwANRsaZJ1sFZ1OZEMQEx6stEnzaIrmfgzcv4z0bTQdQ8lzCrpsz0qtdaD4eA==",
"license": "BSD-3-Clause",
"optional": true,
"peer": true,
@@ -8006,15 +8130,15 @@
}
},
"node_modules/@react-native/dev-middleware": {
- "version": "0.79.3",
- "resolved": "https://registry.npmjs.org/@react-native/dev-middleware/-/dev-middleware-0.79.3.tgz",
- "integrity": "sha512-x88+RGOyG71+idQefnQg7wLhzjn/Scs+re1O5vqCkTVzRAc/f7SdHMlbmECUxJPd08FqMcOJr7/X3nsJBrNuuw==",
+ "version": "0.79.4",
+ "resolved": "https://registry.npmjs.org/@react-native/dev-middleware/-/dev-middleware-0.79.4.tgz",
+ "integrity": "sha512-OWRDNkgrFEo+OSC5QKfiiBmGXKoU8gmIABK8rj2PkgwisFQ/22p7MzE5b6oB2lxWaeJT7jBX5KVniNqO46VhHA==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@isaacs/ttlcache": "^1.4.1",
- "@react-native/debugger-frontend": "0.79.3",
+ "@react-native/debugger-frontend": "0.79.4",
"chrome-launcher": "^0.15.2",
"chromium-edge-launcher": "^0.2.0",
"connect": "^3.6.5",
@@ -8078,9 +8202,9 @@
}
},
"node_modules/@react-native/gradle-plugin": {
- "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==",
+ "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==",
"license": "MIT",
"optional": true,
"peer": true,
@@ -8089,9 +8213,9 @@
}
},
"node_modules/@react-native/js-polyfills": {
- "version": "0.79.3",
- "resolved": "https://registry.npmjs.org/@react-native/js-polyfills/-/js-polyfills-0.79.3.tgz",
- "integrity": "sha512-PEBtg6Kox6KahjCAch0UrqCAmHiNLEbp2SblUEoFAQnov4DSxBN9safh+QSVaCiMAwLjvNfXrJyygZz60Dqz3Q==",
+ "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==",
"license": "MIT",
"optional": true,
"peer": true,
@@ -8100,17 +8224,17 @@
}
},
"node_modules/@react-native/normalize-colors": {
- "version": "0.79.2",
- "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.79.2.tgz",
- "integrity": "sha512-+b+GNrupWrWw1okHnEENz63j7NSMqhKeFMOyzYLBwKcprG8fqJQhDIGXfizKdxeIa5NnGSAevKL1Ev1zJ56X8w==",
+ "version": "0.79.4",
+ "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.79.4.tgz",
+ "integrity": "sha512-247/8pHghbYY2wKjJpUsY6ZNbWcdUa5j5517LZMn6pXrbSSgWuj3JA4OYibNnocCHBaVrt+3R8XC3VEJqLlHFg==",
"license": "MIT",
"optional": true,
"peer": true
},
"node_modules/@react-native/virtualized-lists": {
- "version": "0.79.3",
- "resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.79.3.tgz",
- "integrity": "sha512-/0rRozkn+iIHya2vnnvprDgT7QkfI54FLrACAN3BLP7MRlfOIGOrZsXpRLndnLBVnjNzkcre84i1RecjoXnwIA==",
+ "version": "0.80.0",
+ "resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.80.0.tgz",
+ "integrity": "sha512-d9zZdPS/ZRexVAkxo1eRp85U7XnnEpXA1ZpSomRKxBuStYKky1YohfEX5YD5MhphemKK24tT7JR4UhaLlmeX8Q==",
"license": "MIT",
"optional": true,
"peer": true,
@@ -8181,9 +8305,9 @@
}
},
"node_modules/@rollup/pluginutils": {
- "version": "5.1.4",
- "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.4.tgz",
- "integrity": "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==",
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.2.0.tgz",
+ "integrity": "sha512-qWJ2ZTbmumwiLFomfzTyt5Kng4hwPi9rwCYN4SHb6eaRU1KNO4ccxINHr/VhH4GgPlt1XfSTLX2LBTme8ne4Zw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -8217,9 +8341,9 @@
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
- "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==",
+ "version": "4.44.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.44.0.tgz",
+ "integrity": "sha512-xEiEE5oDW6tK4jXCAyliuntGR+amEMO7HLtdSshVuhFnKTYoeYMyXQK7pLouAJJj5KHdwdn87bfHAR2nSdNAUA==",
"cpu": [
"arm"
],
@@ -8231,9 +8355,9 @@
]
},
"node_modules/@rollup/rollup-android-arm64": {
- "version": "4.42.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.42.0.tgz",
- "integrity": "sha512-bpRipfTgmGFdCZDFLRvIkSNO1/3RGS74aWkJJTFJBH7h3MRV4UijkaEUeOMbi9wxtxYmtAbVcnMtHTPBhLEkaw==",
+ "version": "4.44.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.44.0.tgz",
+ "integrity": "sha512-uNSk/TgvMbskcHxXYHzqwiyBlJ/lGcv8DaUfcnNwict8ba9GTTNxfn3/FAoFZYgkaXXAdrAA+SLyKplyi349Jw==",
"cpu": [
"arm64"
],
@@ -8271,9 +8395,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
- "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==",
+ "version": "4.44.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.44.0.tgz",
+ "integrity": "sha512-u5AZzdQJYJXByB8giQ+r4VyfZP+walV+xHWdaFx/1VxsOn6eWJhK2Vl2eElvDJFKQBo/hcYIBg/jaKS8ZmKeNQ==",
"cpu": [
"arm64"
],
@@ -8285,9 +8409,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
- "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==",
+ "version": "4.44.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.44.0.tgz",
+ "integrity": "sha512-qC0kS48c/s3EtdArkimctY7h3nHicQeEUdjJzYVJYR3ct3kWSafmn6jkNCA8InbUdge6PVx6keqjk5lVGJf99g==",
"cpu": [
"x64"
],
@@ -8299,9 +8423,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
- "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==",
+ "version": "4.44.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.44.0.tgz",
+ "integrity": "sha512-x+e/Z9H0RAWckn4V2OZZl6EmV0L2diuX3QB0uM1r6BvhUIv6xBPL5mrAX2E3e8N8rEHVPwFfz/ETUbV4oW9+lQ==",
"cpu": [
"arm"
],
@@ -8313,9 +8437,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
- "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==",
+ "version": "4.44.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.44.0.tgz",
+ "integrity": "sha512-1exwiBFf4PU/8HvI8s80icyCcnAIB86MCBdst51fwFmH5dyeoWVPVgmQPcKrMtBQ0W5pAs7jBCWuRXgEpRzSCg==",
"cpu": [
"arm"
],
@@ -8353,9 +8477,9 @@
]
},
"node_modules/@rollup/rollup-linux-loongarch64-gnu": {
- "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==",
+ "version": "4.44.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.44.0.tgz",
+ "integrity": "sha512-xw+FTGcov/ejdusVOqKgMGW3c4+AgqrfvzWEVXcNP6zq2ue+lsYUgJ+5Rtn/OTJf7e2CbgTFvzLW2j0YAtj0Gg==",
"cpu": [
"loong64"
],
@@ -8367,9 +8491,9 @@
]
},
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
- "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==",
+ "version": "4.44.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.44.0.tgz",
+ "integrity": "sha512-bKGibTr9IdF0zr21kMvkZT4K6NV+jjRnBoVMt2uNMG0BYWm3qOVmYnXKzx7UhwrviKnmK46IKMByMgvpdQlyJQ==",
"cpu": [
"ppc64"
],
@@ -8381,9 +8505,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
- "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==",
+ "version": "4.44.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.44.0.tgz",
+ "integrity": "sha512-vV3cL48U5kDaKZtXrti12YRa7TyxgKAIDoYdqSIOMOFBXqFj2XbChHAtXquEn2+n78ciFgr4KIqEbydEGPxXgA==",
"cpu": [
"riscv64"
],
@@ -8395,9 +8519,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-musl": {
- "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==",
+ "version": "4.44.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.44.0.tgz",
+ "integrity": "sha512-TDKO8KlHJuvTEdfw5YYFBjhFts2TR0VpZsnLLSYmB7AaohJhM8ctDSdDnUGq77hUh4m/djRafw+9zQpkOanE2Q==",
"cpu": [
"riscv64"
],
@@ -8409,9 +8533,9 @@
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
- "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==",
+ "version": "4.44.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.44.0.tgz",
+ "integrity": "sha512-8541GEyktXaw4lvnGp9m84KENcxInhAt6vPWJ9RodsB/iGjHoMB2Pp5MVBCiKIRxrxzJhGCxmNzdu+oDQ7kwRA==",
"cpu": [
"s390x"
],
@@ -8462,9 +8586,9 @@
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
- "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==",
+ "version": "4.44.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.44.0.tgz",
+ "integrity": "sha512-3XJ0NQtMAXTWFW8FqZKcw3gOQwBtVWP/u8TpHP3CRPXD7Pd6s8lLdH3sHWh8vqKCyyiI8xW5ltJScQmBU9j7WA==",
"cpu": [
"ia32"
],
@@ -8874,9 +8998,9 @@
}
},
"node_modules/@stencil/core": {
- "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==",
+ "version": "4.35.1",
+ "resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.35.1.tgz",
+ "integrity": "sha512-u65m3TbzOtpn679gUV4Yvi8YpInhRJ62js30a7YtXief9Ej/vzrhwDE22U0w4DMWJOYwAsJl133BUaZkWwnmzg==",
"license": "MIT",
"bin": {
"stencil": "bin/stencil"
@@ -9368,9 +9492,9 @@
"license": "MIT"
},
"node_modules/@types/estree": {
- "version": "1.0.7",
- "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
- "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==",
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
"dev": true,
"license": "MIT"
},
@@ -9500,9 +9624,9 @@
"license": "MIT"
},
"node_modules/@types/node": {
- "version": "20.19.0",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.0.tgz",
- "integrity": "sha512-hfrc+1tud1xcdVTABC2JiomZJEklMcXYNTVtZLAeqTVWD+qL5jkHKT+1lOtqDdGxt+mB53DTtiz673vfjU8D1Q==",
+ "version": "20.19.1",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.1.tgz",
+ "integrity": "sha512-jJD50LtlD2dodAEO653i3YF04NWak6jN3ky+Ri3Em3mGR39/glWiboM/IePaRbgwSfqM1TpGXfAg8ohn/4dTgA==",
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
@@ -10381,53 +10505,53 @@
}
},
"node_modules/@vue/compiler-core": {
- "version": "3.5.16",
- "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.16.tgz",
- "integrity": "sha512-AOQS2eaQOaaZQoL1u+2rCJIKDruNXVBZSiUD3chnUrsoX5ZTQMaCvXlWNIfxBJuU15r1o7+mpo5223KVtIhAgQ==",
+ "version": "3.5.17",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.17.tgz",
+ "integrity": "sha512-Xe+AittLbAyV0pabcN7cP7/BenRBNcteM4aSDCtRvGw0d9OL+HG1u/XHLY/kt1q4fyMeZYXyIYrsHuPSiDPosA==",
"license": "MIT",
"dependencies": {
- "@babel/parser": "^7.27.2",
- "@vue/shared": "3.5.16",
+ "@babel/parser": "^7.27.5",
+ "@vue/shared": "3.5.17",
"entities": "^4.5.0",
"estree-walker": "^2.0.2",
"source-map-js": "^1.2.1"
}
},
"node_modules/@vue/compiler-dom": {
- "version": "3.5.16",
- "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.16.tgz",
- "integrity": "sha512-SSJIhBr/teipXiXjmWOVWLnxjNGo65Oj/8wTEQz0nqwQeP75jWZ0n4sF24Zxoht1cuJoWopwj0J0exYwCJ0dCQ==",
+ "version": "3.5.17",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.17.tgz",
+ "integrity": "sha512-+2UgfLKoaNLhgfhV5Ihnk6wB4ljyW1/7wUIog2puUqajiC29Lp5R/IKDdkebh9jTbTogTbsgB+OY9cEWzG95JQ==",
"license": "MIT",
"dependencies": {
- "@vue/compiler-core": "3.5.16",
- "@vue/shared": "3.5.16"
+ "@vue/compiler-core": "3.5.17",
+ "@vue/shared": "3.5.17"
}
},
"node_modules/@vue/compiler-sfc": {
- "version": "3.5.16",
- "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.16.tgz",
- "integrity": "sha512-rQR6VSFNpiinDy/DVUE0vHoIDUF++6p910cgcZoaAUm3POxgNOOdS/xgoll3rNdKYTYPnnbARDCZOyZ+QSe6Pw==",
+ "version": "3.5.17",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.17.tgz",
+ "integrity": "sha512-rQQxbRJMgTqwRugtjw0cnyQv9cP4/4BxWfTdRBkqsTfLOHWykLzbOc3C4GGzAmdMDxhzU/1Ija5bTjMVrddqww==",
"license": "MIT",
"dependencies": {
- "@babel/parser": "^7.27.2",
- "@vue/compiler-core": "3.5.16",
- "@vue/compiler-dom": "3.5.16",
- "@vue/compiler-ssr": "3.5.16",
- "@vue/shared": "3.5.16",
+ "@babel/parser": "^7.27.5",
+ "@vue/compiler-core": "3.5.17",
+ "@vue/compiler-dom": "3.5.17",
+ "@vue/compiler-ssr": "3.5.17",
+ "@vue/shared": "3.5.17",
"estree-walker": "^2.0.2",
"magic-string": "^0.30.17",
- "postcss": "^8.5.3",
+ "postcss": "^8.5.6",
"source-map-js": "^1.2.1"
}
},
"node_modules/@vue/compiler-ssr": {
- "version": "3.5.16",
- "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.16.tgz",
- "integrity": "sha512-d2V7kfxbdsjrDSGlJE7my1ZzCXViEcqN6w14DOsDrUCHEA6vbnVCpRFfrc4ryCP/lCKzX2eS1YtnLE/BuC9f/A==",
+ "version": "3.5.17",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.17.tgz",
+ "integrity": "sha512-hkDbA0Q20ZzGgpj5uZjb9rBzQtIHLS78mMilwrlpWk2Ep37DYntUz0PonQ6kr113vfOEdM+zTBuJDaceNIW0tQ==",
"license": "MIT",
"dependencies": {
- "@vue/compiler-dom": "3.5.16",
- "@vue/shared": "3.5.16"
+ "@vue/compiler-dom": "3.5.17",
+ "@vue/shared": "3.5.17"
}
},
"node_modules/@vue/devtools-api": {
@@ -10682,53 +10806,53 @@
}
},
"node_modules/@vue/reactivity": {
- "version": "3.5.16",
- "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.16.tgz",
- "integrity": "sha512-FG5Q5ee/kxhIm1p2bykPpPwqiUBV3kFySsHEQha5BJvjXdZTUfmya7wP7zC39dFuZAcf/PD5S4Lni55vGLMhvA==",
+ "version": "3.5.17",
+ "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.17.tgz",
+ "integrity": "sha512-l/rmw2STIscWi7SNJp708FK4Kofs97zc/5aEPQh4bOsReD/8ICuBcEmS7KGwDj5ODQLYWVN2lNibKJL1z5b+Lw==",
"license": "MIT",
"dependencies": {
- "@vue/shared": "3.5.16"
+ "@vue/shared": "3.5.17"
}
},
"node_modules/@vue/runtime-core": {
- "version": "3.5.16",
- "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.16.tgz",
- "integrity": "sha512-bw5Ykq6+JFHYxrQa7Tjr+VSzw7Dj4ldR/udyBZbq73fCdJmyy5MPIFR9IX/M5Qs+TtTjuyUTCnmK3lWWwpAcFQ==",
+ "version": "3.5.17",
+ "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.17.tgz",
+ "integrity": "sha512-QQLXa20dHg1R0ri4bjKeGFKEkJA7MMBxrKo2G+gJikmumRS7PTD4BOU9FKrDQWMKowz7frJJGqBffYMgQYS96Q==",
"license": "MIT",
"dependencies": {
- "@vue/reactivity": "3.5.16",
- "@vue/shared": "3.5.16"
+ "@vue/reactivity": "3.5.17",
+ "@vue/shared": "3.5.17"
}
},
"node_modules/@vue/runtime-dom": {
- "version": "3.5.16",
- "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.16.tgz",
- "integrity": "sha512-T1qqYJsG2xMGhImRUV9y/RseB9d0eCYZQ4CWca9ztCuiPj/XWNNN+lkNBuzVbia5z4/cgxdL28NoQCvC0Xcfww==",
+ "version": "3.5.17",
+ "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.17.tgz",
+ "integrity": "sha512-8El0M60TcwZ1QMz4/os2MdlQECgGoVHPuLnQBU3m9h3gdNRW9xRmI8iLS4t/22OQlOE6aJvNNlBiCzPHur4H9g==",
"license": "MIT",
"dependencies": {
- "@vue/reactivity": "3.5.16",
- "@vue/runtime-core": "3.5.16",
- "@vue/shared": "3.5.16",
+ "@vue/reactivity": "3.5.17",
+ "@vue/runtime-core": "3.5.17",
+ "@vue/shared": "3.5.17",
"csstype": "^3.1.3"
}
},
"node_modules/@vue/server-renderer": {
- "version": "3.5.16",
- "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.16.tgz",
- "integrity": "sha512-BrX0qLiv/WugguGsnQUJiYOE0Fe5mZTwi6b7X/ybGB0vfrPH9z0gD/Y6WOR1sGCgX4gc25L1RYS5eYQKDMoNIg==",
+ "version": "3.5.17",
+ "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.17.tgz",
+ "integrity": "sha512-BOHhm8HalujY6lmC3DbqF6uXN/K00uWiEeF22LfEsm9Q93XeJ/plHTepGwf6tqFcF7GA5oGSSAAUock3VvzaCA==",
"license": "MIT",
"dependencies": {
- "@vue/compiler-ssr": "3.5.16",
- "@vue/shared": "3.5.16"
+ "@vue/compiler-ssr": "3.5.17",
+ "@vue/shared": "3.5.17"
},
"peerDependencies": {
- "vue": "3.5.16"
+ "vue": "3.5.17"
}
},
"node_modules/@vue/shared": {
- "version": "3.5.16",
- "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.16.tgz",
- "integrity": "sha512-c/0fWy3Jw6Z8L9FmTyYfkpM5zklnqqa9+a6dz3DvONRKW2NEbh46BP0FHuLFSWi2TnQEtp91Z6zOWNrU6QiyPg==",
+ "version": "3.5.17",
+ "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.17.tgz",
+ "integrity": "sha512-CabR+UN630VnsJO/jHWYBC1YVXyMq94KKp6iF5MQgZJs5I8cmjw6oVMO1oDbtBkENSHSSn/UadWlW/OAgdmKrg==",
"license": "MIT"
},
"node_modules/@vueuse/core": {
@@ -11184,13 +11308,13 @@
}
},
"node_modules/app-builder-lib/node_modules/minimatch": {
- "version": "10.0.1",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz",
- "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==",
+ "version": "10.0.3",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz",
+ "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==",
"dev": true,
"license": "ISC",
"dependencies": {
- "brace-expansion": "^2.0.1"
+ "@isaacs/brace-expansion": "^5.0.0"
},
"engines": {
"node": "20 || >=22"
@@ -11612,9 +11736,9 @@
}
},
"node_modules/axios": {
- "version": "1.9.0",
- "resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz",
- "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==",
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz",
+ "integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
@@ -11818,9 +11942,9 @@
}
},
"node_modules/babel-preset-expo": {
- "version": "13.2.0",
- "resolved": "https://registry.npmjs.org/babel-preset-expo/-/babel-preset-expo-13.2.0.tgz",
- "integrity": "sha512-oNUeUZPMNRPmx/2jaKJLSQFP/MFI1M91vP+Gp+j8/FPl9p/ps603DNwCaRdcT/Vj3FfREdlIwRio1qDCjY0oAA==",
+ "version": "13.2.1",
+ "resolved": "https://registry.npmjs.org/babel-preset-expo/-/babel-preset-expo-13.2.1.tgz",
+ "integrity": "sha512-Ol3w0uLJNQ5tDfCf4L+IDTDMgJkVMQHhvYqMxs18Ib0DcaBQIfE8mneSSk7FcuI6FS0phw/rZhoEquQh1/Q3wA==",
"license": "MIT",
"optional": true,
"peer": true,
@@ -11839,7 +11963,7 @@
"@babel/plugin-transform-runtime": "^7.24.7",
"@babel/preset-react": "^7.22.15",
"@babel/preset-typescript": "^7.23.0",
- "@react-native/babel-preset": "0.79.3",
+ "@react-native/babel-preset": "0.79.4",
"babel-plugin-react-native-web": "~0.19.13",
"babel-plugin-syntax-hermes-parser": "^0.25.1",
"babel-plugin-transform-flow-enums": "^0.0.2",
@@ -12199,9 +12323,9 @@
}
},
"node_modules/brace-expansion": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
- "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
+ "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
@@ -12648,9 +12772,9 @@
}
},
"node_modules/cacache/node_modules/rimraf/node_modules/brace-expansion": {
- "version": "1.1.11",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
- "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -12868,9 +12992,9 @@
}
},
"node_modules/caniuse-lite": {
- "version": "1.0.30001721",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001721.tgz",
- "integrity": "sha512-cOuvmUVtKrtEaoKiO0rSc29jcjwMwX5tOHDy4MgVFEWiUXj4uBMJkwI8MDySkgXidpMiHUcviogAvFi4pA2hDQ==",
+ "version": "1.0.30001723",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001723.tgz",
+ "integrity": "sha512-1R/elMjtehrFejxwmexeXAtae5UO9iSyFn6G/I806CYC/BLyyBk1EPhrKBkWhy6wM6Xnm47dSJQec+tLJ39WHw==",
"devOptional": true,
"funding": [
{
@@ -14507,9 +14631,9 @@
}
},
"node_modules/decode-named-character-reference": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.1.0.tgz",
- "integrity": "sha512-Wy+JTSbFThEOXQIR2L6mxJvEs+veIzpmqD7ynWxMXGpnk3smkHQOp6forLdHsKpAMW9iJpaBBIxz285t1n1C3w==",
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz",
+ "integrity": "sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -14931,9 +15055,9 @@
}
},
"node_modules/dir-compare/node_modules/brace-expansion": {
- "version": "1.1.11",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
- "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -15347,9 +15471,9 @@
}
},
"node_modules/electron-to-chromium": {
- "version": "1.5.166",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.166.tgz",
- "integrity": "sha512-QPWqHL0BglzPYyJJ1zSSmwFFL6MFXhbACOCcsCdUMCkzPdS9/OIBVxg516X/Ado2qwAq8k0nJJ7phQPCqiaFAw==",
+ "version": "1.5.170",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.170.tgz",
+ "integrity": "sha512-GP+M7aeluQo9uAyiTCxgIj/j+PrWhMlY7LFVj8prlsPljd0Fdg9AprlfUi+OCSFWy9Y5/2D/Jrj9HS8Z4rpKWA==",
"devOptional": true,
"license": "ISC"
},
@@ -15420,9 +15544,9 @@
}
},
"node_modules/end-of-stream": {
- "version": "1.4.4",
- "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
- "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
+ "version": "1.4.5",
+ "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
+ "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
"license": "MIT",
"dependencies": {
"once": "^1.4.0"
@@ -15781,9 +15905,9 @@
}
},
"node_modules/eslint-plugin-prettier": {
- "version": "5.4.1",
- "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.4.1.tgz",
- "integrity": "sha512-9dF+KuU/Ilkq27A8idRP7N2DH8iUR6qXcjF3FR2wETY21PZdBrIjwCau8oboyGj9b7etWmTGEeM8e7oOed6ZWg==",
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.0.tgz",
+ "integrity": "sha512-8qsOYwkkGrahrgoUv76NZi23koqXOGiiEzXMrT8Q7VcYaUISR+5MorIUxfWqYXN0fN/31WbSrxCxFkVQ43wwrA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -15865,9 +15989,9 @@
}
},
"node_modules/eslint/node_modules/brace-expansion": {
- "version": "1.1.11",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
- "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -16079,9 +16203,9 @@
}
},
"node_modules/ethers": {
- "version": "6.14.3",
- "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.14.3.tgz",
- "integrity": "sha512-qq7ft/oCJohoTcsNPFaXSQUm457MA5iWqkf1Mb11ujONdg7jBI6sAOrHaTi3j0CBqIGFSCeR/RMc+qwRRub7IA==",
+ "version": "6.14.4",
+ "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.14.4.tgz",
+ "integrity": "sha512-Jm/dzRs2Z9iBrT6e9TvGxyb5YVKAPLlpna7hjxH7KH/++DSh2T/JVmQUv7iHI5E55hDbp/gEVvstWYXVxXFzsA==",
"funding": [
{
"type": "individual",
@@ -16178,21 +16302,21 @@
"license": "MIT"
},
"node_modules/ethr-did": {
- "version": "3.0.37",
- "resolved": "https://registry.npmjs.org/ethr-did/-/ethr-did-3.0.37.tgz",
- "integrity": "sha512-L9UUhAS8B1T7jTRdKLwAt514lx2UrJebJK7uc6UU4AJ9RhY8Vcfwc93Ux82jREE7yvvqDPXsVNH+lS3aw18a9A==",
+ "version": "3.0.38",
+ "resolved": "https://registry.npmjs.org/ethr-did/-/ethr-did-3.0.38.tgz",
+ "integrity": "sha512-gUxtErXVOQUJf+bmnxRdSJdlU9aFbQSBNaJCYGt+PLqw6l4qqInTfMRiWpwe/brhRtdjE+64tnayOVk8ataeQA==",
"license": "Apache-2.0",
"dependencies": {
"did-jwt": "^8.0.0",
"did-resolver": "^4.1.0",
"ethers": "^6.8.1",
- "ethr-did-resolver": "11.0.3"
+ "ethr-did-resolver": "11.0.4"
}
},
"node_modules/ethr-did-resolver": {
- "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==",
+ "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==",
"license": "Apache-2.0",
"dependencies": {
"did-resolver": "^4.1.0",
@@ -16289,27 +16413,27 @@
}
},
"node_modules/expo": {
- "version": "53.0.11",
- "resolved": "https://registry.npmjs.org/expo/-/expo-53.0.11.tgz",
- "integrity": "sha512-+QtvU+6VPd7/o4vmtwuRE/Li2rAiJtD25I6BOnoQSxphaWWaD0PdRQnIV3VQ0HESuJYRuKJ3DkAHNJ3jI6xwzA==",
+ "version": "53.0.12",
+ "resolved": "https://registry.npmjs.org/expo/-/expo-53.0.12.tgz",
+ "integrity": "sha512-dtmED749hkxDWCcvtD++tb8bAm3Twv8qnUOXzVyXA5owNG0mwDIz0HveJTpWK1UzkY4HcTVRezDf0tflZJ+JXQ==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@babel/runtime": "^7.20.0",
- "@expo/cli": "0.24.14",
+ "@expo/cli": "0.24.15",
"@expo/config": "~11.0.10",
- "@expo/config-plugins": "~10.0.2",
- "@expo/fingerprint": "0.13.0",
- "@expo/metro-config": "0.20.14",
+ "@expo/config-plugins": "~10.0.3",
+ "@expo/fingerprint": "0.13.1",
+ "@expo/metro-config": "0.20.15",
"@expo/vector-icons": "^14.0.0",
- "babel-preset-expo": "~13.2.0",
+ "babel-preset-expo": "~13.2.1",
"expo-asset": "~11.1.5",
"expo-constants": "~17.1.6",
"expo-file-system": "~18.1.10",
"expo-font": "~13.3.1",
"expo-keep-awake": "~14.1.4",
- "expo-modules-autolinking": "2.1.11",
+ "expo-modules-autolinking": "2.1.12",
"expo-modules-core": "2.4.0",
"react-native-edge-to-edge": "1.6.0",
"whatwg-url-without-unicode": "8.0.0-3"
@@ -16490,9 +16614,9 @@
}
},
"node_modules/expo/node_modules/expo-modules-autolinking": {
- "version": "2.1.11",
- "resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-2.1.11.tgz",
- "integrity": "sha512-KrWQo+cE4gWYNePBBhmHGVzf63gYV19ZLXe9EIH3GHTkViVzIX+Lp618H/7GxfawpN5kbhvilATH1QEKKnUUww==",
+ "version": "2.1.12",
+ "resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-2.1.12.tgz",
+ "integrity": "sha512-rW5YSW66pUx1nLqn7TO0eWRnP4LDvySW1Tom0wjexk3Tx/upg9LYE5tva7p5AX/cdFfiZcEqPcOxP4RyT++Xlg==",
"license": "MIT",
"optional": true,
"peer": true,
@@ -17617,9 +17741,9 @@
}
},
"node_modules/glob/node_modules/brace-expansion": {
- "version": "1.1.11",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
- "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"devOptional": true,
"license": "MIT",
"dependencies": {
@@ -18331,9 +18455,9 @@
}
},
"node_modules/ipfs-unixfs": {
- "version": "11.2.1",
- "resolved": "https://registry.npmjs.org/ipfs-unixfs/-/ipfs-unixfs-11.2.1.tgz",
- "integrity": "sha512-gUeeX63EFgiaMgcs0cUs2ZUPvlOeEZ38okjK8twdWGZX2jYd2rCk8k/TJ3DSRIDZ2t/aZMv6I23guxHaofZE3w==",
+ "version": "11.2.5",
+ "resolved": "https://registry.npmjs.org/ipfs-unixfs/-/ipfs-unixfs-11.2.5.tgz",
+ "integrity": "sha512-uasYJ0GLPbViaTFsOLnL9YPjX5VmhnqtWRriogAHOe4ApmIi9VAOFBzgDHsUW2ub4pEa/EysbtWk126g2vkU/g==",
"license": "Apache-2.0 OR MIT",
"dependencies": {
"protons-runtime": "^5.5.0",
@@ -19127,9 +19251,9 @@
}
},
"node_modules/jake/node_modules/brace-expansion": {
- "version": "1.1.11",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
- "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -22523,7 +22647,7 @@
"version": "9.0.3",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
"integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
- "devOptional": true,
+ "dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
@@ -23978,13 +24102,13 @@
}
},
"node_modules/playwright": {
- "version": "1.53.0",
- "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.53.0.tgz",
- "integrity": "sha512-ghGNnIEYZC4E+YtclRn4/p6oYbdPiASELBIYkBXfaTVKreQUYbMUYQDwS12a8F0/HtIjr/CkGjtwABeFPGcS4Q==",
+ "version": "1.53.1",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.53.1.tgz",
+ "integrity": "sha512-LJ13YLr/ocweuwxyGf1XNFWIU4M2zUSo149Qbp+A4cpwDjsxRPj7k6H25LBrEHiEwxvRbD8HdwvQmRMSvquhYw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
- "playwright-core": "1.53.0"
+ "playwright-core": "1.53.1"
},
"bin": {
"playwright": "cli.js"
@@ -23997,9 +24121,9 @@
}
},
"node_modules/playwright-core": {
- "version": "1.53.0",
- "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.53.0.tgz",
- "integrity": "sha512-mGLg8m0pm4+mmtB7M89Xw/GSqoNC+twivl8ITteqvAndachozYe2ZA7srU6uleV1vEdAHYqjq+SV8SNxRRFYBw==",
+ "version": "1.53.1",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.53.1.tgz",
+ "integrity": "sha512-Z46Oq7tLAyT0lGoFx4DOuB1IA9D1TPj0QkYxpPVUnGDqHHvDpCftu1J2hM2PiWsNMoZh8+LQaarAWcDfPBc6zg==",
"dev": true,
"license": "Apache-2.0",
"bin": {
@@ -24054,9 +24178,9 @@
}
},
"node_modules/postcss": {
- "version": "8.5.4",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.4.tgz",
- "integrity": "sha512-QSa9EBe+uwlGTFmHsPKokv3B/oEMQZxfqW0QqNCyhpa6mB1afzulwn8hihglqAb2pOw+BJgNlmXQ8la2VeHB7w==",
+ "version": "8.5.6",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
+ "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
"funding": [
{
"type": "opencollective",
@@ -24480,9 +24604,9 @@
"license": "MIT"
},
"node_modules/pump": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz",
- "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==",
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
+ "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==",
"license": "MIT",
"dependencies": {
"end-of-stream": "^1.1.0",
@@ -24954,44 +25078,43 @@
"peer": true
},
"node_modules/react-native": {
- "version": "0.79.3",
- "resolved": "https://registry.npmjs.org/react-native/-/react-native-0.79.3.tgz",
- "integrity": "sha512-EzH1+9gzdyEo9zdP6u7Sh3Jtf5EOMwzy+TK65JysdlgAzfEVfq4mNeXcAZ6SmD+CW6M7ARJbvXLyTD0l2S5rpg==",
+ "version": "0.80.0",
+ "resolved": "https://registry.npmjs.org/react-native/-/react-native-0.80.0.tgz",
+ "integrity": "sha512-b9K1ygb2MWCBtKAodKmE3UsbUuC29Pt4CrJMR0ocTA8k+8HJQTPleBPDNKL4/p0P01QO9aL/gZUddoxHempLow==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"@jest/create-cache-key-function": "^29.7.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",
+ "@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",
"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.25.1",
+ "babel-plugin-syntax-hermes-parser": "0.28.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.0",
- "metro-source-map": "^0.82.0",
+ "metro-runtime": "^0.82.2",
+ "metro-source-map": "^0.82.2",
"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.25.0",
+ "scheduler": "0.26.0",
"semver": "^7.1.3",
"stacktrace-parser": "^0.1.10",
"whatwg-fetch": "^3.0.0",
@@ -25005,8 +25128,8 @@
"node": ">=18"
},
"peerDependencies": {
- "@types/react": "^19.0.0",
- "react": "^19.0.0"
+ "@types/react": "^19.1.0",
+ "react": "^19.1.0"
},
"peerDependenciesMeta": {
"@types/react": {
@@ -25039,14 +25162,46 @@
"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.79.3",
- "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.79.3.tgz",
- "integrity": "sha512-T75NIQPRFCj6DFMxtcVMJTZR+3vHXaUMSd15t+CkJpc5LnyX91GVaPxpRSAdjFh7m3Yppl5MpdjV/fntImheYQ==",
+ "version": "0.80.0",
+ "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.80.0.tgz",
+ "integrity": "sha512-bJZDSopadjJxMDvysc634eTfLL4w7cAx5diPe14Ez5l+xcKjvpfofS/1Ja14DlgdMJhxGd03MTXlrxoWust3zg==",
"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",
@@ -25058,6 +25213,25 @@
"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",
@@ -25514,9 +25688,9 @@
}
},
"node_modules/replace/node_modules/brace-expansion": {
- "version": "1.1.11",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
- "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -25960,15 +26134,15 @@
}
},
"node_modules/rimraf/node_modules/glob": {
- "version": "11.0.2",
- "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.2.tgz",
- "integrity": "sha512-YT7U7Vye+t5fZ/QMkBFrTJ7ZQxInIUjwyAjVj84CYXqgBdv30MFUPGnBR6sQaVq6Is15wYJUsnzTuWaGRBhBAQ==",
+ "version": "11.0.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz",
+ "integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==",
"dev": true,
"license": "ISC",
"dependencies": {
- "foreground-child": "^3.1.0",
- "jackspeak": "^4.0.1",
- "minimatch": "^10.0.0",
+ "foreground-child": "^3.3.1",
+ "jackspeak": "^4.1.1",
+ "minimatch": "^10.0.3",
"minipass": "^7.1.2",
"package-json-from-dist": "^1.0.0",
"path-scurry": "^2.0.0"
@@ -26010,13 +26184,13 @@
}
},
"node_modules/rimraf/node_modules/minimatch": {
- "version": "10.0.1",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz",
- "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==",
+ "version": "10.0.3",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz",
+ "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==",
"dev": true,
"license": "ISC",
"dependencies": {
- "brace-expansion": "^2.0.1"
+ "@isaacs/brace-expansion": "^5.0.0"
},
"engines": {
"node": "20 || >=22"
@@ -26094,13 +26268,13 @@
}
},
"node_modules/rollup": {
- "version": "4.42.0",
- "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.42.0.tgz",
- "integrity": "sha512-LW+Vse3BJPyGJGAJt1j8pWDKPd73QM8cRXYK1IxOBgL2AGLu7Xd2YOW0M2sLUBCkF5MshXXtMApyEAEzMVMsnw==",
+ "version": "4.44.0",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.44.0.tgz",
+ "integrity": "sha512-qHcdEzLCiktQIfwBq420pn2dP+30uzqYxv9ETm91wdt2R9AFcWfjNAmje4NWlnCIQ5RMTzVf0ZyisOKqHR6RwA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@types/estree": "1.0.7"
+ "@types/estree": "1.0.8"
},
"bin": {
"rollup": "dist/bin/rollup"
@@ -26110,33 +26284,33 @@
"npm": ">=8.0.0"
},
"optionalDependencies": {
- "@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",
+ "@rollup/rollup-android-arm-eabi": "4.44.0",
+ "@rollup/rollup-android-arm64": "4.44.0",
+ "@rollup/rollup-darwin-arm64": "4.44.0",
+ "@rollup/rollup-darwin-x64": "4.44.0",
+ "@rollup/rollup-freebsd-arm64": "4.44.0",
+ "@rollup/rollup-freebsd-x64": "4.44.0",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.44.0",
+ "@rollup/rollup-linux-arm-musleabihf": "4.44.0",
+ "@rollup/rollup-linux-arm64-gnu": "4.44.0",
+ "@rollup/rollup-linux-arm64-musl": "4.44.0",
+ "@rollup/rollup-linux-loongarch64-gnu": "4.44.0",
+ "@rollup/rollup-linux-powerpc64le-gnu": "4.44.0",
+ "@rollup/rollup-linux-riscv64-gnu": "4.44.0",
+ "@rollup/rollup-linux-riscv64-musl": "4.44.0",
+ "@rollup/rollup-linux-s390x-gnu": "4.44.0",
+ "@rollup/rollup-linux-x64-gnu": "4.44.0",
+ "@rollup/rollup-linux-x64-musl": "4.44.0",
+ "@rollup/rollup-win32-arm64-msvc": "4.44.0",
+ "@rollup/rollup-win32-ia32-msvc": "4.44.0",
+ "@rollup/rollup-win32-x64-msvc": "4.44.0",
"fsevents": "~2.3.2"
}
},
"node_modules/rollup/node_modules/@rollup/rollup-darwin-arm64": {
- "version": "4.42.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.42.0.tgz",
- "integrity": "sha512-JxHtA081izPBVCHLKnl6GEA0w3920mlJPLh89NojpU2GsBSB6ypu4erFg/Wx1qbpUbepn0jY4dVWMGZM8gplgA==",
+ "version": "4.44.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.44.0.tgz",
+ "integrity": "sha512-VGF3wy0Eq1gcEIkSCr8Ke03CWT+Pm2yveKLaDvq51pPpZza3JX/ClxXOCmTYYq3us5MvEuNRTaeyFThCKRQhOA==",
"cpu": [
"arm64"
],
@@ -26148,9 +26322,9 @@
]
},
"node_modules/rollup/node_modules/@rollup/rollup-darwin-x64": {
- "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==",
+ "version": "4.44.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.44.0.tgz",
+ "integrity": "sha512-fBkyrDhwquRvrTxSGH/qqt3/T0w5Rg0L7ZIDypvBPc1/gzjJle6acCpZ36blwuwcKD/u6oCE/sRWlUAcxLWQbQ==",
"cpu": [
"x64"
],
@@ -26162,9 +26336,9 @@
]
},
"node_modules/rollup/node_modules/@rollup/rollup-linux-arm64-gnu": {
- "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==",
+ "version": "4.44.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.44.0.tgz",
+ "integrity": "sha512-ZTR2mxBHb4tK4wGf9b8SYg0Y6KQPjGpR4UWwTFdnmjB4qRtoATZ5dWn3KsDwGa5Z2ZBOE7K52L36J9LueKBdOQ==",
"cpu": [
"arm64"
],
@@ -26176,9 +26350,9 @@
]
},
"node_modules/rollup/node_modules/@rollup/rollup-linux-arm64-musl": {
- "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==",
+ "version": "4.44.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.44.0.tgz",
+ "integrity": "sha512-GFWfAhVhWGd4r6UxmnKRTBwP1qmModHtd5gkraeW2G490BpFOZkFtem8yuX2NyafIP/mGpRJgTJ2PwohQkUY/Q==",
"cpu": [
"arm64"
],
@@ -26190,9 +26364,9 @@
]
},
"node_modules/rollup/node_modules/@rollup/rollup-linux-x64-gnu": {
- "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==",
+ "version": "4.44.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.44.0.tgz",
+ "integrity": "sha512-iUVJc3c0o8l9Sa/qlDL2Z9UP92UZZW1+EmQ4xfjTc1akr0iUFZNfxrXJ/R1T90h/ILm9iXEY6+iPrmYB3pXKjw==",
"cpu": [
"x64"
],
@@ -26204,9 +26378,9 @@
]
},
"node_modules/rollup/node_modules/@rollup/rollup-linux-x64-musl": {
- "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==",
+ "version": "4.44.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.44.0.tgz",
+ "integrity": "sha512-PQUobbhLTQT5yz/SPg116VJBgz+XOtXt8D1ck+sfJJhuEsMj2jSej5yTdp8CvWBSceu+WW+ibVL6dm0ptG5fcA==",
"cpu": [
"x64"
],
@@ -26218,9 +26392,9 @@
]
},
"node_modules/rollup/node_modules/@rollup/rollup-win32-arm64-msvc": {
- "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==",
+ "version": "4.44.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.44.0.tgz",
+ "integrity": "sha512-M0CpcHf8TWn+4oTxJfh7LQuTuaYeXGbk0eageVjQCKzYLsajWS/lFC94qlRqOlyC2KvRT90ZrfXULYmukeIy7w==",
"cpu": [
"arm64"
],
@@ -26232,9 +26406,9 @@
]
},
"node_modules/rollup/node_modules/@rollup/rollup-win32-x64-msvc": {
- "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==",
+ "version": "4.44.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.44.0.tgz",
+ "integrity": "sha512-Q2Mgwt+D8hd5FIPUuPDsvPR7Bguza6yTkJxspDGkZj7tBRn2y4KSWYuIXpftFSjBra76TbKerCV7rgFPQrn+wQ==",
"cpu": [
"x64"
],
@@ -26410,9 +26584,9 @@
"license": "ISC"
},
"node_modules/scheduler": {
- "version": "0.25.0",
- "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz",
- "integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==",
+ "version": "0.26.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz",
+ "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==",
"license": "MIT",
"optional": true,
"peer": true
@@ -26424,9 +26598,9 @@
"license": "MIT"
},
"node_modules/sdp": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/sdp/-/sdp-3.2.0.tgz",
- "integrity": "sha512-d7wDPgDV3DDiqulJjKiV2865wKsJ34YI+NDREbm+FySq6WuKOikwyNQcm+doLAZ1O6ltdO0SeKle2xMpN3Brgw==",
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/sdp/-/sdp-3.2.1.tgz",
+ "integrity": "sha512-lwsAIzOPlH8/7IIjjz3K0zYBk7aBVVcvjMwt3M4fLxpjMYyy7i3I97SLHebgn4YBjirkzfp3RvRDWSKsh/+WFw==",
"license": "MIT"
},
"node_modules/secp256k1": {
@@ -28265,9 +28439,9 @@
}
},
"node_modules/tar-fs": {
- "version": "3.0.9",
- "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.9.tgz",
- "integrity": "sha512-XF4w9Xp+ZQgifKakjZYmFdkLoSWd34VGKcsTCwlNWM7QG3ZbaxnTsaBwnjFZqHRf/rROxaR8rXnbtwdvaDI+lA==",
+ "version": "3.0.10",
+ "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.10.tgz",
+ "integrity": "sha512-C1SwlQGNLe/jPNqapK8epDsXME7CAJR5RL3GcE6KWx1d9OUByzoHVcbu1VPI8tevg9H8Alae0AApHHFGzrD5zA==",
"devOptional": true,
"license": "MIT",
"dependencies": {
@@ -28453,9 +28627,9 @@
}
},
"node_modules/terser": {
- "version": "5.42.0",
- "resolved": "https://registry.npmjs.org/terser/-/terser-5.42.0.tgz",
- "integrity": "sha512-UYCvU9YQW2f/Vwl+P0GfhxJxbUGLwd+5QrrGgLajzWAtC/23AX0vcise32kkP7Eu0Wu9VlzzHAXkLObgjQfFlQ==",
+ "version": "5.43.1",
+ "resolved": "https://registry.npmjs.org/terser/-/terser-5.43.1.tgz",
+ "integrity": "sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==",
"devOptional": true,
"license": "BSD-2-Clause",
"dependencies": {
@@ -28495,9 +28669,9 @@
}
},
"node_modules/test-exclude/node_modules/brace-expansion": {
- "version": "1.1.11",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
- "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"license": "MIT",
"optional": true,
"peer": true,
@@ -29784,16 +29958,16 @@
"peer": true
},
"node_modules/vue": {
- "version": "3.5.16",
- "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.16.tgz",
- "integrity": "sha512-rjOV2ecxMd5SiAmof2xzh2WxntRcigkX/He4YFJ6WdRvVUrbt6DxC1Iujh10XLl8xCDRDtGKMeO3D+pRQ1PP9w==",
+ "version": "3.5.17",
+ "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.17.tgz",
+ "integrity": "sha512-LbHV3xPN9BeljML+Xctq4lbz2lVHCR6DtbpTf5XIO6gugpXUN49j2QQPcMj086r9+AkJ0FfUT8xjulKKBkkr9g==",
"license": "MIT",
"dependencies": {
- "@vue/compiler-dom": "3.5.16",
- "@vue/compiler-sfc": "3.5.16",
- "@vue/runtime-dom": "3.5.16",
- "@vue/server-renderer": "3.5.16",
- "@vue/shared": "3.5.16"
+ "@vue/compiler-dom": "3.5.17",
+ "@vue/compiler-sfc": "3.5.17",
+ "@vue/runtime-dom": "3.5.17",
+ "@vue/server-renderer": "3.5.17",
+ "@vue/shared": "3.5.17"
},
"peerDependencies": {
"typescript": "*"
@@ -31045,9 +31219,9 @@
}
},
"node_modules/zod": {
- "version": "3.25.58",
- "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.58.tgz",
- "integrity": "sha512-DVLmMQzSZwNYzQoMaM3MQWnxr2eq+AtM9Hx3w1/Yl0pH8sLTSjN4jGP7w6f7uand6Hw44tsnSu1hz1AOA6qI2Q==",
+ "version": "3.25.67",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.67.tgz",
+ "integrity": "sha512-idA2YXwpCdqUSKRCACDE6ItZD9TZzy3OZMtpfLoh6oPR47lipysRrJfjzMqFxQ3uJuUPyUeWe1r9vLH33xO/Qw==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
diff --git a/package.json b/package.json
index 19895a0d..a2152348 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "timesafari",
- "version": "0.5.1",
+ "version": "1.0.3-beta",
"description": "Time Safari Application",
"author": {
"name": "Time Safari Team"
diff --git a/src/assets/icons.json b/src/assets/icons.json
new file mode 100644
index 00000000..434421e6
--- /dev/null
+++ b/src/assets/icons.json
@@ -0,0 +1,75 @@
+{
+ "warning": {
+ "fillRule": "evenodd",
+ "d": "M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z",
+ "clipRule": "evenodd"
+ },
+ "spinner": {
+ "d": "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
+ },
+ "chart": {
+ "strokeLinecap": "round",
+ "strokeLinejoin": "round",
+ "strokeWidth": "2",
+ "d": "M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"
+ },
+ "plus": {
+ "strokeLinecap": "round",
+ "strokeLinejoin": "round",
+ "strokeWidth": "2",
+ "d": "M12 4v16m8-8H4"
+ },
+ "settings": {
+ "strokeLinecap": "round",
+ "strokeLinejoin": "round",
+ "strokeWidth": "2",
+ "d": "M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
+ },
+ "settingsDot": {
+ "strokeLinecap": "round",
+ "strokeLinejoin": "round",
+ "strokeWidth": "2",
+ "d": "M15 12a3 3 0 11-6 0 3 3 0 016 0z"
+ },
+ "lock": {
+ "strokeLinecap": "round",
+ "strokeLinejoin": "round",
+ "strokeWidth": "2",
+ "d": "M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
+ },
+ "download": {
+ "strokeLinecap": "round",
+ "strokeLinejoin": "round",
+ "strokeWidth": "2",
+ "d": "M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
+ },
+ "check": {
+ "strokeLinecap": "round",
+ "strokeLinejoin": "round",
+ "strokeWidth": "2",
+ "d": "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
+ },
+ "edit": {
+ "strokeLinecap": "round",
+ "strokeLinejoin": "round",
+ "strokeWidth": "2",
+ "d": "M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"
+ },
+ "trash": {
+ "strokeLinecap": "round",
+ "strokeLinejoin": "round",
+ "strokeWidth": "2",
+ "d": "M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
+ },
+ "plusCircle": {
+ "strokeLinecap": "round",
+ "strokeLinejoin": "round",
+ "strokeWidth": "2",
+ "d": "M12 6v6m0 0v6m0-6h6m-6 0H6"
+ },
+ "info": {
+ "fillRule": "evenodd",
+ "d": "M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z",
+ "clipRule": "evenodd"
+ }
+}
\ No newline at end of file
diff --git a/src/components/GiftedDialog.vue b/src/components/GiftedDialog.vue
index fe3b2054..e79f104c 100644
--- a/src/components/GiftedDialog.vue
+++ b/src/components/GiftedDialog.vue
@@ -853,7 +853,7 @@ export default class GiftedDialog extends Vue {
);
if (!result.success) {
- const errorMessage = this.getGiveCreationErrorMessage(result);
+ const errorMessage = result.error;
logger.error("Error with give creation result:", result);
this.$notify(
{
@@ -899,19 +899,6 @@ export default class GiftedDialog extends Vue {
// Helper functions for readability
- /**
- * @param result direct response eg. ErrorResult or SuccessResult (potentially with embedded "data")
- * @returns best guess at an error message
- */
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- getGiveCreationErrorMessage(result: any) {
- return (
- result.error?.userMessage ||
- result.error?.error ||
- result.response?.data?.error?.message
- );
- }
-
explainData() {
this.$notify(
{
diff --git a/src/components/HiddenDidDialog.vue b/src/components/HiddenDidDialog.vue
index a94c34ae..8593009e 100644
--- a/src/components/HiddenDidDialog.vue
+++ b/src/components/HiddenDidDialog.vue
@@ -48,12 +48,15 @@
{{ didInfo(visDid) }}
-
+
-
+
@@ -74,7 +77,7 @@
If you'd like an introduction,
click here to copy this page, paste it into a message, and ask if
they'll tell you more about the {{ roleName }}.
@@ -101,7 +104,7 @@ import * as R from "ramda";
import { useClipboard } from "@vueuse/core";
import { Contact } from "../db/tables/contacts";
import * as serverUtil from "../libs/endorserServer";
-import { NotificationIface } from "../constants/app";
+import { APP_SERVER, NotificationIface } from "../constants/app";
@Component
export default class HiddenDidDialog extends Vue {
@@ -114,7 +117,8 @@ export default class HiddenDidDialog extends Vue {
activeDid = "";
allMyDids: Array = [];
canShare = false;
- windowLocation = window.location.href;
+ deepLinkPathSuffix = "";
+ deepLinkUrl = window.location.href; // this is changed to a deep link in the setup
R = R;
serverUtil = serverUtil;
@@ -126,17 +130,21 @@ export default class HiddenDidDialog extends Vue {
}
open(
+ deepLinkPathSuffix: string,
roleName: string,
visibleToDids: string[],
allContacts: Array,
activeDid: string,
allMyDids: Array,
) {
+ this.deepLinkPathSuffix = deepLinkPathSuffix;
this.roleName = roleName;
this.visibleToDids = visibleToDids;
this.allContacts = allContacts;
this.activeDid = activeDid;
this.allMyDids = allMyDids;
+
+ this.deepLinkUrl = APP_SERVER + "/deep-link/" + this.deepLinkPathSuffix;
this.isOpen = true;
}
@@ -170,11 +178,11 @@ export default class HiddenDidDialog extends Vue {
}
onClickShareClaim() {
- this.copyToClipboard("A link to this page", this.windowLocation);
+ this.copyToClipboard("A link to this page", this.deepLinkUrl);
window.navigator.share({
title: "Help Connect Me",
text: "I'm trying to find the people who recorded this. Can you help me?",
- url: this.windowLocation,
+ url: this.deepLinkUrl,
});
}
}
diff --git a/src/components/IconRenderer.vue b/src/components/IconRenderer.vue
new file mode 100644
index 00000000..83a0b14c
--- /dev/null
+++ b/src/components/IconRenderer.vue
@@ -0,0 +1,90 @@
+
+
+
+
+
diff --git a/src/components/OfferDialog.vue b/src/components/OfferDialog.vue
index 659488f1..c5c54e7a 100644
--- a/src/components/OfferDialog.vue
+++ b/src/components/OfferDialog.vue
@@ -83,10 +83,7 @@
import { Vue, Component, Prop } from "vue-facing-decorator";
import { NotificationIface, USE_DEXIE_DB } from "../constants/app";
-import {
- createAndSubmitOffer,
- serverMessageForUser,
-} from "../libs/endorserServer";
+import { createAndSubmitOffer } from "../libs/endorserServer";
import * as libsUtil from "../libs/util";
import * as databaseUtil from "../db/databaseUtil";
import { retrieveSettingsForActiveAccount } from "../db/index";
@@ -250,7 +247,7 @@ export default class OfferDialog extends Vue {
);
if (!result.success) {
- const errorMessage = this.getOfferCreationErrorMessage(result);
+ const errorMessage = result.error;
logger.error("Error with offer creation result:", result);
this.$notify(
{
@@ -290,21 +287,6 @@ export default class OfferDialog extends Vue {
);
}
}
-
- // Helper functions for readability
-
- /**
- * @param result direct response eg. ErrorResult or SuccessResult (potentially with embedded "data")
- * @returns best guess at an error message
- */
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- getOfferCreationErrorMessage(result: any) {
- return (
- serverMessageForUser(result) ||
- result.error?.userMessage ||
- result.error?.error
- );
- }
}
diff --git a/src/components/TopMessage.vue b/src/components/TopMessage.vue
index 49981e4b..54b6a01b 100644
--- a/src/components/TopMessage.vue
+++ b/src/components/TopMessage.vue
@@ -38,14 +38,13 @@ export default class TopMessage extends Vue {
settings.apiServer !== AppString.PROD_ENDORSER_API_SERVER
) {
const didPrefix = settings.activeDid?.slice(11, 15);
- this.message = "You're linked to a non-prod server, user " + didPrefix;
+ this.message = "You're not using prod, user " + didPrefix;
} else if (
settings.warnIfProdServer &&
settings.apiServer === AppString.PROD_ENDORSER_API_SERVER
) {
const didPrefix = settings.activeDid?.slice(11, 15);
- this.message =
- "You're linked to the production server, user " + didPrefix;
+ this.message = "You are using prod, user " + didPrefix;
}
} catch (err: unknown) {
this.$notify(
diff --git a/src/db-sql/migration.ts b/src/db-sql/migration.ts
index 9cadd260..e06636bd 100644
--- a/src/db-sql/migration.ts
+++ b/src/db-sql/migration.ts
@@ -1,4 +1,7 @@
-import migrationService from "../services/migrationService";
+import {
+ registerMigration,
+ runMigrations as runMigrationsService,
+} from "../services/migrationService";
import { DEFAULT_ENDORSER_API_SERVER } from "@/constants/app";
import { arrayBufferToBase64 } from "@/libs/crypto";
@@ -31,7 +34,6 @@ const secretBase64 = arrayBufferToBase64(randomBytes);
const MIGRATIONS = [
{
name: "001_initial",
- // see ../db/tables files for explanations of the fields
sql: `
CREATE TABLE IF NOT EXISTS accounts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -116,6 +118,12 @@ const MIGRATIONS = [
);
`,
},
+ {
+ name: "002_add_iViewContent_to_contacts",
+ sql: `
+ ALTER TABLE contacts ADD COLUMN iViewContent BOOLEAN DEFAULT TRUE;
+ `,
+ },
];
/**
@@ -123,16 +131,12 @@ const MIGRATIONS = [
* @param extractMigrationNames - A function that extracts the names (string array) from "select name from migrations"
*/
export async function runMigrations(
- sqlExec: (sql: string) => Promise,
- sqlQuery: (sql: string) => Promise,
+ sqlExec: (sql: string, params?: unknown[]) => Promise,
+ sqlQuery: (sql: string, params?: unknown[]) => Promise,
extractMigrationNames: (result: T) => Set,
): Promise {
for (const migration of MIGRATIONS) {
- migrationService.registerMigration(migration);
+ registerMigration(migration);
}
- await migrationService.runMigrations(
- sqlExec,
- sqlQuery,
- extractMigrationNames,
- );
+ await runMigrationsService(sqlExec, sqlQuery, extractMigrationNames);
}
diff --git a/src/db/databaseUtil.ts b/src/db/databaseUtil.ts
index c8688b83..aca9933d 100644
--- a/src/db/databaseUtil.ts
+++ b/src/db/databaseUtil.ts
@@ -219,18 +219,36 @@ export async function logConsoleAndDb(
isError = false,
): Promise {
if (isError) {
- logger.error(`${new Date().toISOString()} ${message}`);
+ logger.error(`${new Date().toISOString()}`, message);
} else {
- logger.log(`${new Date().toISOString()} ${message}`);
+ logger.log(`${new Date().toISOString()}`, message);
}
await logToDb(message);
}
/**
- * Generates an SQL INSERT statement and parameters from a model object.
- * @param model The model object containing fields to update
- * @param tableName The name of the table to update
- * @returns Object containing the SQL statement and parameters array
+ * Generates SQL INSERT statement and parameters from a model object
+ *
+ * This helper function creates a parameterized SQL INSERT statement
+ * from a JavaScript object. It filters out undefined values and
+ * creates the appropriate SQL syntax with placeholders.
+ *
+ * The function is used internally by the migration functions to
+ * safely insert data into the SQLite database.
+ *
+ * @function generateInsertStatement
+ * @param {Record} model - The model object containing fields to insert
+ * @param {string} tableName - The name of the table to insert into
+ * @returns {Object} Object containing the SQL statement and parameters array
+ * @returns {string} returns.sql - The SQL INSERT statement
+ * @returns {unknown[]} returns.params - Array of parameter values
+ * @example
+ * ```typescript
+ * const contact = { did: 'did:example:123', name: 'John Doe' };
+ * const { sql, params } = generateInsertStatement(contact, 'contacts');
+ * // sql: "INSERT INTO contacts (did, name) VALUES (?, ?)"
+ * // params: ['did:example:123', 'John Doe']
+ * ```
*/
export function generateInsertStatement(
model: Record,
@@ -248,12 +266,30 @@ export function generateInsertStatement(
}
/**
- * Generates an SQL UPDATE statement and parameters from a model object.
- * @param model The model object containing fields to update
- * @param tableName The name of the table to update
- * @param whereClause The WHERE clause for the update (e.g. "id = ?")
- * @param whereParams Parameters for the WHERE clause
- * @returns Object containing the SQL statement and parameters array
+ * Generates SQL UPDATE statement and parameters from a model object
+ *
+ * This helper function creates a parameterized SQL UPDATE statement
+ * from a JavaScript object. It filters out undefined values and
+ * creates the appropriate SQL syntax with placeholders.
+ *
+ * The function is used internally by the migration functions to
+ * safely update data in the SQLite database.
+ *
+ * @function generateUpdateStatement
+ * @param {Record} model - The model object containing fields to update
+ * @param {string} tableName - The name of the table to update
+ * @param {string} whereClause - The WHERE clause for the update (e.g. "id = ?")
+ * @param {unknown[]} [whereParams=[]] - Parameters for the WHERE clause
+ * @returns {Object} Object containing the SQL statement and parameters array
+ * @returns {string} returns.sql - The SQL UPDATE statement
+ * @returns {unknown[]} returns.params - Array of parameter values
+ * @example
+ * ```typescript
+ * const contact = { name: 'Jane Doe' };
+ * const { sql, params } = generateUpdateStatement(contact, 'contacts', 'did = ?', ['did:example:123']);
+ * // sql: "UPDATE contacts SET name = ? WHERE did = ?"
+ * // params: ['Jane Doe', 'did:example:123']
+ * ```
*/
export function generateUpdateStatement(
model: Record,
diff --git a/src/db/tables/README.md b/src/db/tables/README.md
index ae2f2688..617dae94 100644
--- a/src/db/tables/README.md
+++ b/src/db/tables/README.md
@@ -1 +1 @@
-Check the contact & settings export to see whether you want your new table to be included in it.
+# Check the contact & settings export to see whether you want your new table to be included in it
diff --git a/src/db/tables/contacts.ts b/src/db/tables/contacts.ts
index 1b496b21..147323b9 100644
--- a/src/db/tables/contacts.ts
+++ b/src/db/tables/contacts.ts
@@ -1,15 +1,16 @@
-export interface ContactMethod {
+export type ContactMethod = {
label: string;
type: string; // eg. "EMAIL", "SMS", "WHATSAPP", maybe someday "GOOGLE-CONTACT-API"
value: string;
-}
+};
-export interface Contact {
+export type Contact = {
//
- // When adding a property, consider whether it should be added when exporting & sharing contacts.
+ // When adding a property, consider whether it should be added when exporting & sharing contacts, eg. DataExportSection
did: string;
contactMethods?: Array;
+ iViewContent?: boolean;
name?: string;
nextPubKeyHashB64?: string; // base64-encoded SHA256 hash of next public key
notes?: string;
@@ -17,7 +18,17 @@ export interface Contact {
publicKeyBase64?: string;
seesMe?: boolean; // cached value of the server setting
registered?: boolean; // cached value of the server setting
-}
+};
+
+/**
+ * This is for those cases (eg. with a DB) where every field is a primitive (and not an object).
+ *
+ * This is so that we can reuse most of the type and don't have to maintain another copy.
+ * Another approach uses typescript conditionals: https://chatgpt.com/share/6855cdc3-ab5c-8007-8525-726612016eb2
+ */
+export type ContactWithJsonStrings = Omit & {
+ contactMethods?: string;
+};
export const ContactSchema = {
contacts: "&did, name", // no need to key by other things
diff --git a/src/db/tables/settings.ts b/src/db/tables/settings.ts
index 726a41ee..bcf33982 100644
--- a/src/db/tables/settings.ts
+++ b/src/db/tables/settings.ts
@@ -64,6 +64,11 @@ export type Settings = {
webPushServer?: string; // Web Push server URL
};
+// type of settings where the searchBoxes are JSON strings instead of objects
+export type SettingsWithJsonStrings = Settings & {
+ searchBoxes: string;
+};
+
export function checkIsAnyFeedFilterOn(settings: Settings): boolean {
return !!(settings?.filterFeedByNearby || settings?.filterFeedByVisible);
}
diff --git a/src/interfaces/claims-result.ts b/src/interfaces/claims-result.ts
index d8f60f0c..920c652d 100644
--- a/src/interfaces/claims-result.ts
+++ b/src/interfaces/claims-result.ts
@@ -1,6 +1,4 @@
-import { AxiosResponse } from "axios";
import { GiverReceiverInputInfo } from "../libs/util";
-import { ErrorResult, ResultWithType } from "./common";
export interface GiverOutputInfo {
action: string;
@@ -47,12 +45,3 @@ export interface ProviderInfo {
*/
linkConfirmed: boolean;
}
-
-// Type for createAndSubmitClaim result
-export type CreateAndSubmitClaimResult = SuccessResult | ErrorResult;
-
-// Update SuccessResult to use ClaimResult
-export interface SuccessResult extends ResultWithType {
- type: "success";
- response: AxiosResponse;
-}
diff --git a/src/interfaces/common.ts b/src/interfaces/common.ts
index 566db958..cce40568 100644
--- a/src/interfaces/common.ts
+++ b/src/interfaces/common.ts
@@ -15,10 +15,6 @@ export interface GenericCredWrapper {
publicUrls?: Record;
}
-export interface ResultWithType {
- type: string;
-}
-
export interface ErrorResponse {
error?: {
message?: string;
@@ -30,11 +26,6 @@ export interface InternalError {
userMessage?: string;
}
-export interface ErrorResult extends ResultWithType {
- type: "error";
- error: InternalError;
-}
-
export interface KeyMeta {
did: string;
publicKeyHex: string;
diff --git a/src/interfaces/deepLinks.ts b/src/interfaces/deepLinks.ts
index 1e06aab1..a275eecf 100644
--- a/src/interfaces/deepLinks.ts
+++ b/src/interfaces/deepLinks.ts
@@ -29,18 +29,17 @@ import { z } from "zod";
// Add a union type of all valid route paths
export const VALID_DEEP_LINK_ROUTES = [
- "user-profile",
- "project-details",
- "onboard-meeting-setup",
- "invite-one-accept",
- "contact-import",
- "confirm-gift",
+ // note that similar lists are below in deepLinkSchemas and in src/services/deepLinks.ts
"claim",
- "claim-cert",
"claim-add-raw",
- "contact-edit",
- "contacts",
+ "claim-cert",
+ "confirm-gift",
+ "contact-import",
"did",
+ "invite-one-accept",
+ "onboard-meeting-setup",
+ "project",
+ "user-profile",
] as const;
// Create a type from the array
@@ -58,43 +57,38 @@ export const routeSchema = z.enum(VALID_DEEP_LINK_ROUTES);
// Parameter validation schemas for each route type
export const deepLinkSchemas = {
- "user-profile": z.object({
+ // note that similar lists are above in VALID_DEEP_LINK_ROUTES and in src/services/deepLinks.ts
+ claim: z.object({
id: z.string(),
}),
- "project-details": z.object({
+ "claim-add-raw": z.object({
id: z.string(),
+ claim: z.string().optional(),
+ claimJwtId: z.string().optional(),
}),
- "onboard-meeting-setup": z.object({
+ "claim-cert": z.object({
id: z.string(),
}),
- "invite-one-accept": z.object({
+ "confirm-gift": z.object({
id: z.string(),
}),
"contact-import": z.object({
jwt: z.string(),
}),
- "confirm-gift": z.object({
- id: z.string(),
+ did: z.object({
+ did: z.string(),
}),
- claim: z.object({
- id: z.string(),
+ "invite-one-accept": z.object({
+ jwt: z.string(),
}),
- "claim-cert": z.object({
+ "onboard-meeting-setup": z.object({
id: z.string(),
}),
- "claim-add-raw": z.object({
+ project: z.object({
id: z.string(),
- claim: z.string().optional(),
- claimJwtId: z.string().optional(),
- }),
- "contact-edit": z.object({
- did: z.string(),
}),
- contacts: z.object({
- contacts: z.string(), // JSON string of contacts array
- }),
- did: z.object({
- did: z.string(),
+ "user-profile": z.object({
+ id: z.string(),
}),
};
diff --git a/src/interfaces/index.ts b/src/interfaces/index.ts
index 5d4b499d..b8e76493 100644
--- a/src/interfaces/index.ts
+++ b/src/interfaces/index.ts
@@ -1,5 +1,6 @@
export type {
// From common.ts
+ CreateAndSubmitClaimResult,
GenericCredWrapper,
GenericVerifiableCredential,
KeyMeta,
@@ -18,11 +19,6 @@ export type {
RegisterActionClaim,
} from "./claims";
-export type {
- // From claims-result.ts
- CreateAndSubmitClaimResult,
-} from "./claims-result";
-
export type {
// From records.ts
PlanSummaryRecord,
diff --git a/src/libs/endorserServer.ts b/src/libs/endorserServer.ts
index 8409f413..f2dc79a4 100644
--- a/src/libs/endorserServer.ts
+++ b/src/libs/endorserServer.ts
@@ -979,7 +979,7 @@ export const createAndSubmitConfirmation = async (
handleId: string | undefined,
apiServer: string,
axios: Axios,
-) => {
+): Promise => {
const goodClaim = removeSchemaContext(
removeVisibleToDids(
addLastClaimOrHandleAsIdIfMissing(claim, lastClaimId, handleId),
@@ -1074,7 +1074,8 @@ export async function generateEndorserJwtUrlForAccount(
const vcJwt = await createEndorserJwtForDid(account.did, contactInfo);
- const viewPrefix = APP_SERVER + CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI;
+ const viewPrefix =
+ APP_SERVER + "/deep-link" + CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI;
return viewPrefix + vcJwt;
}
diff --git a/src/libs/fontawesome.ts b/src/libs/fontawesome.ts
index 4fa56f0a..2fa70e3c 100644
--- a/src/libs/fontawesome.ts
+++ b/src/libs/fontawesome.ts
@@ -10,8 +10,8 @@ import {
faArrowLeft,
faArrowRight,
faArrowRotateBackward,
- faArrowUpRightFromSquare,
faArrowUp,
+ faArrowUpRightFromSquare,
faBan,
faBitcoinSign,
faBurst,
@@ -95,8 +95,8 @@ library.add(
faArrowLeft,
faArrowRight,
faArrowRotateBackward,
- faArrowUpRightFromSquare,
faArrowUp,
+ faArrowUpRightFromSquare,
faBan,
faBitcoinSign,
faBurst,
diff --git a/src/libs/util.ts b/src/libs/util.ts
index 81d04835..79ef1120 100644
--- a/src/libs/util.ts
+++ b/src/libs/util.ts
@@ -17,7 +17,7 @@ import {
updateDefaultSettings,
} from "../db/index";
import { Account, AccountEncrypted } from "../db/tables/accounts";
-import { Contact } from "../db/tables/contacts";
+import { Contact, ContactWithJsonStrings } from "../db/tables/contacts";
import * as databaseUtil from "../db/databaseUtil";
import { DEFAULT_PASSKEY_EXPIRATION_MINUTES } from "../db/tables/settings";
import {
@@ -45,6 +45,7 @@ import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
import { sha256 } from "ethereum-cryptography/sha256";
import { IIdentifier } from "@veramo/core";
import { insertDidSpecificSettings, parseJsonField } from "../db/databaseUtil";
+import { DEFAULT_ROOT_DERIVATION_PATH } from "./crypto";
export interface GiverReceiverInputInfo {
did?: string;
@@ -884,6 +885,71 @@ export const contactToCsvLine = (contact: Contact): string => {
return fields.join(",");
};
+/**
+ * Parses a CSV line into a Contact object. See contactToCsvLine for the format.
+ * @param lineRaw - The CSV line to parse
+ * @returns A Contact object
+ */
+export const csvLineToContact = (lineRaw: string): Contact => {
+ // Note that Endorser Mobile puts name first, then did, etc.
+ let line = lineRaw.trim();
+ let did, publicKeyInput, seesMe, registered;
+ let name;
+ let commaPos1 = -1;
+ if (line.startsWith('"')) {
+ let doubleDoubleQuotePos = line.lastIndexOf('""') + 2;
+ if (doubleDoubleQuotePos === -1) {
+ doubleDoubleQuotePos = 1;
+ }
+ const quote2Pos = line.indexOf('"', doubleDoubleQuotePos);
+ if (quote2Pos > -1) {
+ commaPos1 = line.indexOf(",", quote2Pos);
+ name = line.substring(1, quote2Pos).trim();
+ name = name.replace(/""/g, '"');
+ } else {
+ // something is weird with one " to start, so ignore it and start after "
+ line = line.substring(1);
+ commaPos1 = line.indexOf(",");
+ name = line.substring(0, commaPos1).trim();
+ }
+ } else {
+ commaPos1 = line.indexOf(",");
+ name = line.substring(0, commaPos1).trim();
+ }
+ if (commaPos1 > -1) {
+ did = line.substring(commaPos1 + 1).trim();
+ const commaPos2 = line.indexOf(",", commaPos1 + 1);
+ if (commaPos2 > -1) {
+ did = line.substring(commaPos1 + 1, commaPos2).trim();
+ publicKeyInput = line.substring(commaPos2 + 1).trim();
+ const commaPos3 = line.indexOf(",", commaPos2 + 1);
+ if (commaPos3 > -1) {
+ publicKeyInput = line.substring(commaPos2 + 1, commaPos3).trim();
+ seesMe = line.substring(commaPos3 + 1).trim() == "true";
+ const commaPos4 = line.indexOf(",", commaPos3 + 1);
+ if (commaPos4 > -1) {
+ seesMe = line.substring(commaPos3 + 1, commaPos4).trim() == "true";
+ registered = line.substring(commaPos4 + 1).trim() == "true";
+ }
+ }
+ }
+ }
+ // help with potential mistakes while this sharing requires copy-and-paste
+ let publicKeyBase64 = publicKeyInput;
+ if (publicKeyBase64 && /^[0-9A-Fa-f]{66}$/i.test(publicKeyBase64)) {
+ // it must be all hex (compressed public key), so convert
+ publicKeyBase64 = Buffer.from(publicKeyBase64, "hex").toString("base64");
+ }
+ const newContact: Contact = {
+ did: did || "",
+ name,
+ publicKeyBase64,
+ seesMe,
+ registered,
+ };
+ return newContact;
+};
+
/**
* Interface for the JSON export format of database tables
*/
@@ -910,19 +976,16 @@ export interface DatabaseExport {
*/
export const contactsToExportJson = (contacts: Contact[]): DatabaseExport => {
// Convert each contact to a plain object and ensure all fields are included
- 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,
- }));
+ const rows = contacts.map((contact) => {
+ const exContact: ContactWithJsonStrings = R.omit(
+ ["contactMethods"],
+ contact,
+ );
+ exContact.contactMethods = contact.contactMethods
+ ? JSON.stringify(contact.contactMethods, [])
+ : undefined;
+ return exContact;
+ });
return {
data: {
@@ -935,3 +998,38 @@ export const contactsToExportJson = (contacts: Contact[]): DatabaseExport => {
},
};
};
+
+/**
+ * Imports an account from a mnemonic phrase
+ * @param mnemonic - The seed phrase to import from
+ * @param derivationPath - The derivation path to use (defaults to DEFAULT_ROOT_DERIVATION_PATH)
+ * @param shouldErase - Whether to erase existing accounts before importing
+ * @returns Promise that resolves when import is complete
+ * @throws Error if mnemonic is invalid or import fails
+ */
+export async function importFromMnemonic(
+ mnemonic: string,
+ derivationPath: string = DEFAULT_ROOT_DERIVATION_PATH,
+ shouldErase: boolean = false,
+): Promise {
+ const mne: string = mnemonic.trim().toLowerCase();
+
+ // Derive address and keys from mnemonic
+ const [address, privateHex, publicHex] = deriveAddress(mne, derivationPath);
+
+ // Create new identifier
+ const newId = newIdentifier(address, publicHex, privateHex, derivationPath);
+
+ // Handle erasures
+ if (shouldErase) {
+ const platformService = PlatformServiceFactory.getInstance();
+ await platformService.dbExec("DELETE FROM accounts");
+ if (USE_DEXIE_DB) {
+ const accountsDB = await accountsDBPromise;
+ await accountsDB.accounts.clear();
+ }
+ }
+
+ // Save the new identity
+ await saveNewIdentity(newId, mne, derivationPath);
+}
diff --git a/src/main.capacitor.ts b/src/main.capacitor.ts
index dc4074c4..3ac12d1f 100644
--- a/src/main.capacitor.ts
+++ b/src/main.capacitor.ts
@@ -34,8 +34,7 @@ import router from "./router";
import { handleApiError } from "./services/api";
import { AxiosError } from "axios";
import { DeepLinkHandler } from "./services/deepLinks";
-import { logConsoleAndDb } from "./db/databaseUtil";
-import { logger } from "./utils/logger";
+import { logger, safeStringify } from "./utils/logger";
logger.log("[Capacitor] Starting initialization");
logger.log("[Capacitor] Platform:", process.env.VITE_PLATFORM);
@@ -72,10 +71,10 @@ const handleDeepLink = async (data: { url: string }) => {
await router.isReady();
await deepLinkHandler.handleDeepLink(data.url);
} catch (error) {
- logConsoleAndDb("[DeepLink] Error handling deep link: " + error, true);
+ logger.error("[DeepLink] Error handling deep link: ", error);
handleApiError(
{
- message: error instanceof Error ? error.message : String(error),
+ message: error instanceof Error ? error.message : safeStringify(error),
} as AxiosError,
"deep-link",
);
diff --git a/src/main.common.ts b/src/main.common.ts
index cb44cd6f..27d06333 100644
--- a/src/main.common.ts
+++ b/src/main.common.ts
@@ -10,15 +10,11 @@ import { FontAwesomeIcon } from "./libs/fontawesome";
import Camera from "simple-vue-camera";
import { logger } from "./utils/logger";
-const platform = process.env.VITE_PLATFORM;
-const pwa_enabled = process.env.VITE_PWA_ENABLED === "true";
-
-logger.log("Platform", JSON.stringify({ platform }));
-logger.log("PWA enabled", JSON.stringify({ pwa_enabled }));
+// const platform = process.env.VITE_PLATFORM;
+// const pwa_enabled = process.env.VITE_PWA_ENABLED === "true";
// Global Error Handler
function setupGlobalErrorHandler(app: VueApp) {
- logger.log("[App Init] Setting up global error handler");
app.config.errorHandler = (
err: unknown,
instance: ComponentPublicInstance | null,
@@ -38,30 +34,13 @@ function setupGlobalErrorHandler(app: VueApp) {
// Function to initialize the app
export function initializeApp() {
- logger.log("[App Init] Starting app initialization");
- logger.log("[App Init] Platform:", process.env.VITE_PLATFORM);
-
const app = createApp(App);
- logger.log("[App Init] Vue app created");
-
app.component("FontAwesome", FontAwesomeIcon).component("camera", Camera);
- logger.log("[App Init] Components registered");
-
const pinia = createPinia();
app.use(pinia);
- logger.log("[App Init] Pinia store initialized");
-
app.use(VueAxios, axios);
- logger.log("[App Init] Axios initialized");
-
app.use(router);
- logger.log("[App Init] Router initialized");
-
app.use(Notifications);
- logger.log("[App Init] Notifications initialized");
-
setupGlobalErrorHandler(app);
- logger.log("[App Init] App initialization complete");
-
return app;
}
diff --git a/src/main.web.ts b/src/main.web.ts
index dc5d1e56..ff149056 100644
--- a/src/main.web.ts
+++ b/src/main.web.ts
@@ -5,9 +5,6 @@ import { logger } from "./utils/logger";
const platform = process.env.VITE_PLATFORM;
const pwa_enabled = process.env.VITE_PWA_ENABLED === "true";
-logger.info("[Web] PWA enabled", { pwa_enabled });
-logger.info("[Web] Platform", { platform });
-
// Only import service worker for web builds
if (platform !== "electron" && pwa_enabled) {
import("./registerServiceWorker"); // Web PWA support
@@ -31,7 +28,7 @@ function sqlInit() {
if (platform === "web" || platform === "development") {
sqlInit();
} else {
- logger.info("[Web] SQL not initialized for platform", { platform });
+ logger.warn("[Web] SQL not initialized for platform", { platform });
}
app.mount("#app");
diff --git a/src/router/index.ts b/src/router/index.ts
index 0b9aa52b..010972bf 100644
--- a/src/router/index.ts
+++ b/src/router/index.ts
@@ -83,6 +83,11 @@ const routes: Array = [
name: "discover",
component: () => import("../views/DiscoverView.vue"),
},
+ {
+ path: "/deep-link/:path*",
+ name: "deep-link",
+ component: () => import("../views/DeepLinkRedirectView.vue"),
+ },
{
path: "/gifted-details",
name: "gifted-details",
@@ -143,6 +148,11 @@ const routes: Array = [
name: "logs",
component: () => import("../views/LogView.vue"),
},
+ {
+ path: "/database-migration",
+ name: "database-migration",
+ component: () => import("../views/DatabaseMigration.vue"),
+ },
{
path: "/new-activity",
name: "new-activity",
diff --git a/src/services/api.ts b/src/services/api.ts
index 3235100e..d7b67beb 100644
--- a/src/services/api.ts
+++ b/src/services/api.ts
@@ -6,7 +6,7 @@
*/
import { AxiosError } from "axios";
-import { logger } from "../utils/logger";
+import { logger, safeStringify } from "../utils/logger";
/**
* Handles API errors with platform-specific logging and error processing.
@@ -37,7 +37,8 @@ import { logger } from "../utils/logger";
*/
export const handleApiError = (error: AxiosError, endpoint: string) => {
if (process.env.VITE_PLATFORM === "capacitor") {
- logger.error(`[Capacitor API Error] ${endpoint}:`, {
+ const endpointStr = safeStringify(endpoint); // we've seen this as an object in deep links
+ logger.error(`[Capacitor API Error] ${endpointStr}:`, {
message: error.message,
status: error.response?.status,
data: error.response?.data,
diff --git a/src/services/deepLinks.ts b/src/services/deepLinks.ts
index 550353f7..bff3346c 100644
--- a/src/services/deepLinks.ts
+++ b/src/services/deepLinks.ts
@@ -27,18 +27,16 @@
* timesafari://[/][?queryParam1=value1&queryParam2=value2]
*
* Supported Routes:
- * - user-profile: View user profile
- * - project-details: View project details
- * - onboard-meeting-setup: Setup onboarding meeting
- * - invite-one-accept: Accept invitation
- * - contact-import: Import contacts
- * - confirm-gift: Confirm gift
* - claim: View claim
- * - claim-cert: View claim certificate
* - claim-add-raw: Add raw claim
- * - contact-edit: Edit contact
- * - contacts: View contacts
+ * - claim-cert: View claim certificate
+ * - confirm-gift
+ * - contact-import: Import contacts
* - did: View DID
+ * - invite-one-accept: Accept invitation
+ * - onboard-meeting-members
+ * - project: View project details
+ * - user-profile: View user profile
*
* @example
* const handler = new DeepLinkHandler(router);
@@ -81,18 +79,17 @@ export class DeepLinkHandler {
string,
{ name: string; paramKey?: string }
> = {
- "user-profile": { name: "user-profile" },
- "project-details": { name: "project-details" },
- "onboard-meeting-setup": { name: "onboard-meeting-setup" },
- "invite-one-accept": { name: "invite-one-accept" },
- "contact-import": { name: "contact-import" },
- "confirm-gift": { name: "confirm-gift" },
+ // note that similar lists are in src/interfaces/deepLinks.ts
claim: { name: "claim" },
- "claim-cert": { name: "claim-cert" },
"claim-add-raw": { name: "claim-add-raw" },
- "contact-edit": { name: "contact-edit", paramKey: "did" },
- contacts: { name: "contacts" },
+ "claim-cert": { name: "claim-cert" },
+ "confirm-gift": { name: "confirm-gift" },
+ "contact-import": { name: "contact-import", paramKey: "jwt" },
did: { name: "did", paramKey: "did" },
+ "invite-one-accept": { name: "invite-one-accept", paramKey: "jwt" },
+ "onboard-meeting-members": { name: "onboard-meeting-members" },
+ project: { name: "project" },
+ "user-profile": { name: "user-profile" },
};
/**
@@ -101,7 +98,7 @@ export class DeepLinkHandler {
*
* @param url - The deep link URL to parse (format: scheme://path[?query])
* @throws {DeepLinkError} If URL format is invalid
- * @returns Parsed URL components (path, params, query)
+ * @returns Parsed URL components (path: string, params: {KEY: string}, query: {KEY: string})
*/
private parseDeepLink(url: string) {
const parts = url.split("://");
@@ -117,7 +114,16 @@ export class DeepLinkHandler {
});
const [path, queryString] = parts[1].split("?");
- const [routePath, param] = path.split("/");
+ const [routePath, ...pathParams] = path.split("/");
+ // logger.info(
+ // "[DeepLink] Debug:",
+ // "Route Path:",
+ // routePath,
+ // "Path Params:",
+ // pathParams,
+ // "Query String:",
+ // queryString,
+ // );
// Validate route exists before proceeding
if (!this.ROUTE_MAP[routePath]) {
@@ -136,45 +142,14 @@ export class DeepLinkHandler {
}
const params: Record = {};
- if (param) {
+ if (pathParams) {
// Now we know routePath exists in ROUTE_MAP
const routeConfig = this.ROUTE_MAP[routePath];
- params[routeConfig.paramKey ?? "id"] = param;
+ params[routeConfig.paramKey ?? "id"] = pathParams.join("/");
}
return { path: routePath, params, query };
}
- /**
- * Processes incoming deep links and routes them appropriately.
- * Handles validation, error handling, and routing to the correct view.
- *
- * @param url - The deep link URL to process
- * @throws {DeepLinkError} If URL processing fails
- */
- async handleDeepLink(url: string): Promise {
- try {
- logConsoleAndDb("[DeepLink] Processing URL: " + url, false);
- const { path, params, query } = this.parseDeepLink(url);
- // Ensure params is always a Record by converting undefined to empty string
- const sanitizedParams = Object.fromEntries(
- Object.entries(params).map(([key, value]) => [key, value ?? ""]),
- );
- await this.validateAndRoute(path, sanitizedParams, query);
- } catch (error) {
- const deepLinkError = error as DeepLinkError;
- logConsoleAndDb(
- `[DeepLink] Error (${deepLinkError.code}): ${deepLinkError.message}`,
- true,
- );
-
- throw {
- code: deepLinkError.code || "UNKNOWN_ERROR",
- message: deepLinkError.message,
- details: deepLinkError.details,
- };
- }
- }
-
/**
* Routes the deep link to appropriate view with validated parameters.
* Validates route and parameters using Zod schemas before routing.
@@ -245,6 +220,39 @@ export class DeepLinkHandler {
code: "INVALID_PARAMETERS",
message: (error as Error).message,
details: error,
+ params: params,
+ query: query,
+ };
+ }
+ }
+
+ /**
+ * Processes incoming deep links and routes them appropriately.
+ * Handles validation, error handling, and routing to the correct view.
+ *
+ * @param url - The deep link URL to process
+ * @throws {DeepLinkError} If URL processing fails
+ */
+ async handleDeepLink(url: string): Promise {
+ try {
+ logConsoleAndDb("[DeepLink] Processing URL: " + url, false);
+ const { path, params, query } = this.parseDeepLink(url);
+ // Ensure params is always a Record by converting undefined to empty string
+ const sanitizedParams = Object.fromEntries(
+ Object.entries(params).map(([key, value]) => [key, value ?? ""]),
+ );
+ await this.validateAndRoute(path, sanitizedParams, query);
+ } catch (error) {
+ const deepLinkError = error as DeepLinkError;
+ logConsoleAndDb(
+ `[DeepLink] Error (${deepLinkError.code}): ${deepLinkError.message}`,
+ true,
+ );
+
+ throw {
+ code: deepLinkError.code || "UNKNOWN_ERROR",
+ message: deepLinkError.message,
+ details: deepLinkError.details,
};
}
}
diff --git a/src/services/indexedDBMigrationService.ts b/src/services/indexedDBMigrationService.ts
new file mode 100644
index 00000000..88fb9d09
--- /dev/null
+++ b/src/services/indexedDBMigrationService.ts
@@ -0,0 +1,1397 @@
+/**
+ * Migration Service for transferring data from Dexie (IndexedDB) to SQLite
+ *
+ * This service provides functions to:
+ * 1. Compare data between Dexie and SQLite databases
+ * 2. Transfer contacts and settings from Dexie to SQLite
+ * 3. Generate YAML-formatted data comparisons
+ *
+ * The service is designed to work with the TimeSafari app's dual database architecture,
+ * where data can exist in both Dexie (IndexedDB) and SQLite databases. This allows
+ * for safe migration of data between the two storage systems.
+ *
+ * Usage:
+ * 1. Enable Dexie temporarily by setting USE_DEXIE_DB = true in constants/app.ts
+ * 2. Use compareDatabases() to see differences between databases
+ * 3. Use migrateContacts() and/or migrateSettings() to transfer data
+ * 4. Disable Dexie again after migration is complete
+ *
+ * @author Matthew Raymer
+ * @version 1.0.0
+ * @since 2024
+ */
+
+import "dexie-export-import";
+
+import { PlatformServiceFactory } from "./PlatformServiceFactory";
+import { db, accountsDBPromise } from "../db/index";
+import { Contact, ContactMethod } from "../db/tables/contacts";
+import {
+ Settings,
+ MASTER_SETTINGS_KEY,
+ BoundingBox,
+} from "../db/tables/settings";
+import { Account } from "../db/tables/accounts";
+import { logger } from "../utils/logger";
+import {
+ mapColumnsToValues,
+ parseJsonField,
+ generateUpdateStatement,
+ generateInsertStatement,
+} from "../db/databaseUtil";
+import { importFromMnemonic } from "../libs/util";
+
+/**
+ * Interface for data comparison results between Dexie and SQLite databases
+ *
+ * This interface provides a comprehensive view of the differences between
+ * the two database systems, including counts and detailed lists of
+ * added, modified, and missing records.
+ *
+ * @interface DataComparison
+ * @property {Contact[]} dexieContacts - All contacts from Dexie database
+ * @property {Contact[]} sqliteContacts - All contacts from SQLite database
+ * @property {Settings[]} dexieSettings - All settings from Dexie database
+ * @property {Settings[]} sqliteSettings - All settings from SQLite database
+ * @property {Account[]} dexieAccounts - All accounts from Dexie database
+ * @property {Account[]} sqliteAccounts - All accounts from SQLite database
+ * @property {Object} differences - Detailed differences between databases
+ * @property {Object} differences.contacts - Contact-specific differences
+ * @property {Contact[]} differences.contacts.added - Contacts in Dexie but not SQLite
+ * @property {Contact[]} differences.contacts.modified - Contacts that differ between databases
+ * @property {Contact[]} differences.contacts.missing - Contacts in SQLite but not Dexie
+ * @property {Object} differences.settings - Settings-specific differences
+ * @property {Settings[]} differences.settings.added - Settings in Dexie but not SQLite
+ * @property {Settings[]} differences.settings.modified - Settings that differ between databases
+ * @property {Settings[]} differences.settings.missing - Settings in SQLite but not Dexie
+ * @property {Object} differences.accounts - Account-specific differences
+ * @property {Account[]} differences.accounts.added - Accounts in Dexie but not SQLite
+ * @property {Account[]} differences.accounts.modified - Accounts that differ between databases
+ * @property {Account[]} differences.accounts.missing - Accounts in SQLite but not Dexie
+ */
+export interface DataComparison {
+ dexieContacts: Contact[];
+ sqliteContacts: Contact[];
+ dexieSettings: Settings[];
+ sqliteSettings: Settings[];
+ dexieAccounts: Account[];
+ sqliteAccounts: string[];
+ differences: {
+ contacts: {
+ added: Contact[];
+ modified: Contact[];
+ unmodified: Contact[];
+ missing: Contact[];
+ };
+ settings: {
+ added: Settings[];
+ modified: Settings[];
+ unmodified: Settings[];
+ missing: Settings[];
+ };
+ accounts: {
+ added: Account[];
+ unmodified: Account[];
+ missing: string[];
+ };
+ };
+}
+
+/**
+ * Interface for migration operation results
+ *
+ * Provides detailed feedback about the success or failure of migration
+ * operations, including counts of migrated records and any errors or
+ * warnings that occurred during the process.
+ *
+ * @interface MigrationResult
+ * @property {boolean} success - Whether the migration operation completed successfully
+ * @property {number} contactsMigrated - Number of contacts successfully migrated
+ * @property {number} settingsMigrated - Number of settings successfully migrated
+ * @property {number} accountsMigrated - Number of accounts successfully migrated
+ * @property {string[]} errors - Array of error messages encountered during migration
+ * @property {string[]} warnings - Array of warning messages (non-fatal issues)
+ */
+export interface MigrationResult {
+ success: boolean;
+ contactsMigrated: number;
+ settingsMigrated: number;
+ accountsMigrated: number;
+ errors: string[];
+ warnings: string[];
+}
+
+export async function getDexieExportBlob(): Promise {
+ await db.open();
+ const blob = db.export({ prettyJson: true });
+ return blob;
+}
+
+/**
+ * Retrieves all contacts from the Dexie (IndexedDB) database
+ *
+ * This function connects to the Dexie database and retrieves all contact
+ * records. It requires that USE_DEXIE_DB is enabled in the app constants.
+ *
+ * The function handles database opening and error conditions, providing
+ * detailed logging for debugging purposes.
+ *
+ * @async
+ * @function getDexieContacts
+ * @returns {Promise} Array of all contacts from Dexie database
+ * @throws {Error} If Dexie database is not enabled or if database access fails
+ * @example
+ * ```typescript
+ * try {
+ * const contacts = await getDexieContacts();
+ * console.log(`Retrieved ${contacts.length} contacts from Dexie`);
+ * } catch (error) {
+ * console.error('Failed to retrieve Dexie contacts:', error);
+ * }
+ * ```
+ */
+export async function getDexieContacts(): Promise {
+ try {
+ await db.open();
+ const contacts = await db.contacts.toArray();
+ logger.info(
+ `[IndexedDBMigrationService] Retrieved ${contacts.length} contacts from Dexie`,
+ );
+ return contacts;
+ } catch (error) {
+ logger.error(
+ "[IndexedDBMigrationService] Error retrieving Dexie contacts:",
+ error,
+ );
+ throw new Error(`Failed to retrieve Dexie contacts: ${error}`);
+ }
+}
+
+/**
+ * Retrieves all contacts from the SQLite database
+ *
+ * This function uses the platform service to query the SQLite database
+ * and retrieve all contact records. It handles the conversion of raw
+ * database results into properly typed Contact objects.
+ *
+ * The function also handles JSON parsing for complex fields like
+ * contactMethods, ensuring proper type conversion.
+ *
+ * @async
+ * @function getSqliteContacts
+ * @returns {Promise} Array of all contacts from SQLite database
+ * @throws {Error} If database query fails or data conversion fails
+ * @example
+ * ```typescript
+ * try {
+ * const contacts = await getSqliteContacts();
+ * console.log(`Retrieved ${contacts.length} contacts from SQLite`);
+ * } catch (error) {
+ * console.error('Failed to retrieve SQLite contacts:', error);
+ * }
+ * ```
+ */
+export async function getSqliteContacts(): Promise {
+ try {
+ const platformService = PlatformServiceFactory.getInstance();
+ const result = await platformService.dbQuery("SELECT * FROM contacts");
+
+ let contacts: Contact[] = [];
+ if (result?.values?.length) {
+ const preContacts = mapColumnsToValues(
+ result.columns,
+ result.values,
+ ) as unknown as Contact[];
+ // This is redundant since absurd-sql auto-parses JSON strings to objects.
+ // But we started it, and it should be known everywhere, so we're keeping it.
+ contacts = preContacts.map((contact) => {
+ if (contact.contactMethods) {
+ contact.contactMethods = parseJsonField(
+ contact.contactMethods,
+ [],
+ ) as ContactMethod[];
+ }
+ return contact;
+ });
+ }
+
+ logger.info(
+ `[IndexedDBMigrationService] Retrieved ${contacts.length} contacts from SQLite`,
+ );
+ return contacts;
+ } catch (error) {
+ logger.error(
+ "[IndexedDBMigrationService] Error retrieving SQLite contacts:",
+ error,
+ );
+ throw new Error(`Failed to retrieve SQLite contacts: ${error}`);
+ }
+}
+
+/**
+ * Retrieves all settings from the Dexie (IndexedDB) database
+ *
+ * This function connects to the Dexie database and retrieves all settings
+ * records.
+ *
+ * Settings include both master settings (id=1) and account-specific settings
+ * that override the master settings for particular user accounts.
+ *
+ * @async
+ * @function getDexieSettings
+ * @returns {Promise} Array of all settings from Dexie database
+ * @throws {Error} If Dexie database is not enabled or if database access fails
+ * @example
+ * ```typescript
+ * try {
+ * const settings = await getDexieSettings();
+ * console.log(`Retrieved ${settings.length} settings from Dexie`);
+ * } catch (error) {
+ * console.error('Failed to retrieve Dexie settings:', error);
+ * }
+ * ```
+ */
+export async function getDexieSettings(): Promise {
+ try {
+ await db.open();
+ const settings = await db.settings.toArray();
+ logger.info(
+ `[IndexedDBMigrationService] Retrieved ${settings.length} settings from Dexie`,
+ );
+ return settings;
+ } catch (error) {
+ logger.error(
+ "[IndexedDBMigrationService] Error retrieving Dexie settings:",
+ error,
+ );
+ throw new Error(`Failed to retrieve Dexie settings: ${error}`);
+ }
+}
+
+/**
+ * Retrieves all settings from the SQLite database
+ *
+ * This function uses the platform service to query the SQLite database
+ * and retrieve all settings records. It handles the conversion of raw
+ * database results into properly typed Settings objects.
+ *
+ * The function also handles JSON parsing for complex fields like
+ * searchBoxes, ensuring proper type conversion.
+ *
+ * @async
+ * @function getSqliteSettings
+ * @returns {Promise} Array of all settings from SQLite database
+ * @throws {Error} If database query fails or data conversion fails
+ * @example
+ * ```typescript
+ * try {
+ * const settings = await getSqliteSettings();
+ * console.log(`Retrieved ${settings.length} settings from SQLite`);
+ * } catch (error) {
+ * console.error('Failed to retrieve SQLite settings:', error);
+ * }
+ * ```
+ */
+export async function getSqliteSettings(): Promise {
+ try {
+ const platformService = PlatformServiceFactory.getInstance();
+ const result = await platformService.dbQuery("SELECT * FROM settings");
+
+ let settings: Settings[] = [];
+ if (result?.values?.length) {
+ const presettings = mapColumnsToValues(
+ result.columns,
+ result.values,
+ ) as Settings[];
+ // This is redundant since absurd-sql auto-parses JSON strings to objects.
+ // But we started it, and it should be known everywhere, so we're keeping it.
+ settings = presettings.map((setting) => {
+ if (setting.searchBoxes) {
+ setting.searchBoxes = parseJsonField(
+ setting.searchBoxes,
+ [],
+ ) as Array<{ name: string; bbox: BoundingBox }>;
+ }
+ return setting;
+ });
+ }
+
+ logger.info(
+ `[IndexedDBMigrationService] Retrieved ${settings.length} settings from SQLite`,
+ );
+ return settings;
+ } catch (error) {
+ logger.error(
+ "[IndexedDBMigrationService] Error retrieving SQLite settings:",
+ error,
+ );
+ throw new Error(`Failed to retrieve SQLite settings: ${error}`);
+ }
+}
+
+/**
+ * Retrieves all accounts from the SQLite database
+ *
+ * This function uses the platform service to query the SQLite database
+ * and retrieve all account records. It handles the conversion of raw
+ * database results into properly typed Account objects.
+ *
+ * The function also handles JSON parsing for complex fields like
+ * identity, ensuring proper type conversion.
+ *
+ * @async
+ * @function getSqliteAccounts
+ * @returns {Promise} Array of all accounts from SQLite database
+ * @throws {Error} If database query fails or data conversion fails
+ * @example
+ * ```typescript
+ * try {
+ * const accounts = await getSqliteAccounts();
+ * console.log(`Retrieved ${accounts.length} accounts from SQLite`);
+ * } catch (error) {
+ * console.error('Failed to retrieve SQLite accounts:', error);
+ * }
+ * ```
+ */
+export async function getSqliteAccounts(): Promise {
+ try {
+ const platformService = PlatformServiceFactory.getInstance();
+ const result = await platformService.dbQuery("SELECT did FROM accounts");
+
+ let dids: string[] = [];
+ if (result?.values?.length) {
+ dids = result.values.map((row) => row[0] as string);
+ }
+
+ logger.info(
+ `[IndexedDBMigrationService] Retrieved ${dids.length} accounts from SQLite`,
+ );
+ return dids;
+ } catch (error) {
+ logger.error(
+ "[IndexedDBMigrationService] Error retrieving SQLite accounts:",
+ error,
+ );
+ throw new Error(`Failed to retrieve SQLite accounts: ${error}`);
+ }
+}
+
+/**
+ * Retrieves all accounts from the Dexie (IndexedDB) database
+ *
+ * This function connects to the Dexie database and retrieves all account
+ * records.
+ *
+ * The function handles database opening and error conditions, providing
+ * detailed logging for debugging purposes.
+ *
+ * @async
+ * @function getDexieAccounts
+ * @returns {Promise} Array of all accounts from Dexie database
+ * @throws {Error} If Dexie database is not enabled or if database access fails
+ * @example
+ * ```typescript
+ * try {
+ * const accounts = await getDexieAccounts();
+ * console.log(`Retrieved ${accounts.length} accounts from Dexie`);
+ * } catch (error) {
+ * console.error('Failed to retrieve Dexie accounts:', error);
+ * }
+ * ```
+ */
+export async function getDexieAccounts(): Promise {
+ try {
+ const accountsDB = await accountsDBPromise;
+ await accountsDB.open();
+ const accounts = await accountsDB.accounts.toArray();
+ logger.info(
+ `[IndexedDBMigrationService] Retrieved ${accounts.length} accounts from Dexie`,
+ );
+ return accounts;
+ } catch (error) {
+ logger.error(
+ "[IndexedDBMigrationService] Error retrieving Dexie accounts:",
+ error,
+ );
+ throw new Error(`Failed to retrieve Dexie accounts: ${error}`);
+ }
+}
+
+/**
+ * Compares data between Dexie and SQLite databases
+ *
+ * This is the main comparison function that retrieves data from both
+ * databases and identifies differences. It provides a comprehensive
+ * view of what data exists in each database and what needs to be
+ * migrated.
+ *
+ * The function performs parallel data retrieval for efficiency and
+ * then compares the results to identify added, modified, and missing
+ * records in each table.
+ *
+ * @async
+ * @function compareDatabases
+ * @returns {Promise} Comprehensive comparison results
+ * @throws {Error} If any database access fails
+ * @example
+ * ```typescript
+ * try {
+ * const comparison = await compareDatabases();
+ * console.log(`Dexie contacts: ${comparison.dexieContacts.length}`);
+ * console.log(`SQLite contacts: ${comparison.sqliteContacts.length}`);
+ * console.log(`Added contacts: ${comparison.differences.contacts.added.length}`);
+ * } catch (error) {
+ * console.error('Database comparison failed:', error);
+ * }
+ * ```
+ */
+export async function compareDatabases(): Promise {
+ logger.info("[IndexedDBMigrationService] Starting database comparison");
+
+ const [
+ dexieContacts,
+ sqliteContacts,
+ dexieSettings,
+ sqliteSettings,
+ dexieAccounts,
+ sqliteAccounts,
+ ] = await Promise.all([
+ getDexieContacts(),
+ getSqliteContacts(),
+ getDexieSettings(),
+ getSqliteSettings(),
+ getDexieAccounts(),
+ getSqliteAccounts(),
+ ]);
+
+ // Compare contacts
+ const contactDifferences = compareContacts(dexieContacts, sqliteContacts);
+
+ // Compare settings
+ const settingsDifferences = compareSettings(dexieSettings, sqliteSettings);
+
+ // Compare accounts
+ const accountDifferences = compareAccounts(dexieAccounts, sqliteAccounts);
+
+ const comparison: DataComparison = {
+ dexieContacts,
+ sqliteContacts,
+ dexieSettings,
+ sqliteSettings,
+ dexieAccounts,
+ sqliteAccounts,
+ differences: {
+ contacts: contactDifferences,
+ settings: settingsDifferences,
+ accounts: accountDifferences,
+ },
+ };
+
+ logger.info("[IndexedDBMigrationService] Database comparison completed", {
+ dexieContacts: dexieContacts.length,
+ sqliteContacts: sqliteContacts.length,
+ dexieSettings: dexieSettings.length,
+ sqliteSettings: sqliteSettings.length,
+ dexieAccounts: dexieAccounts.length,
+ sqliteAccounts: sqliteAccounts.length,
+ contactDifferences: contactDifferences,
+ settingsDifferences: settingsDifferences,
+ accountDifferences: accountDifferences,
+ });
+
+ return comparison;
+}
+
+/**
+ * Compares contacts between Dexie and SQLite databases
+ *
+ * This helper function analyzes two arrays of contacts and identifies
+ * which contacts are added (in Dexie but not SQLite), modified
+ * (different between databases), or missing (in SQLite but not Dexie).
+ *
+ * The comparison is based on the contact's DID (Decentralized Identifier)
+ * as the primary key, with detailed field-by-field comparison for
+ * modified contacts.
+ *
+ * @function compareContacts
+ * @param {Contact[]} dexieContacts - Contacts from Dexie database
+ * @param {Contact[]} sqliteContacts - Contacts from SQLite database
+ * @returns {Object} Object containing added, modified, and missing contacts
+ * @returns {Contact[]} returns.added - Contacts in Dexie but not SQLite
+ * @returns {Contact[]} returns.modified - Contacts that differ between databases
+ * @returns {Contact[]} returns.missing - Contacts in SQLite but not Dexie
+ * @example
+ * ```typescript
+ * const differences = compareContacts(dexieContacts, sqliteContacts);
+ * console.log(`Added: ${differences.added.length}`);
+ * console.log(`Modified: ${differences.modified.length}`);
+ * console.log(`Missing: ${differences.missing.length}`);
+ * ```
+ */
+function compareContacts(dexieContacts: Contact[], sqliteContacts: Contact[]) {
+ const added: Contact[] = [];
+ const modified: Contact[] = [];
+ const unmodified: Contact[] = [];
+ const missing: Contact[] = [];
+
+ // Find contacts that exist in Dexie but not in SQLite
+ for (const dexieContact of dexieContacts) {
+ const sqliteContact = sqliteContacts.find(
+ (c) => c.did === dexieContact.did,
+ );
+ if (!sqliteContact) {
+ added.push(dexieContact);
+ } else if (!contactsEqual(dexieContact, sqliteContact)) {
+ modified.push(dexieContact);
+ } else {
+ unmodified.push(dexieContact);
+ }
+ }
+
+ // Find contacts that exist in SQLite but not in Dexie
+ for (const sqliteContact of sqliteContacts) {
+ const dexieContact = dexieContacts.find((c) => c.did === sqliteContact.did);
+ if (!dexieContact) {
+ missing.push(sqliteContact);
+ }
+ }
+
+ return { added, modified, unmodified, missing };
+}
+
+/**
+ * Compares settings between Dexie and SQLite databases
+ *
+ * This helper function analyzes two arrays of settings and identifies
+ * which settings are added (in Dexie but not SQLite), modified
+ * (different between databases), or missing (in SQLite but not Dexie).
+ *
+ * The comparison is based on the setting's ID as the primary key,
+ * with detailed field-by-field comparison for modified settings.
+ *
+ * @function compareSettings
+ * @param {Settings[]} dexieSettings - Settings from Dexie database
+ * @param {Settings[]} sqliteSettings - Settings from SQLite database
+ * @returns {Object} Object containing added, modified, and missing settings
+ * @returns {Settings[]} returns.added - Settings in Dexie but not SQLite
+ * @returns {Settings[]} returns.modified - Settings that differ between databases
+ * @returns {Settings[]} returns.missing - Settings in SQLite but not Dexie
+ * @example
+ * ```typescript
+ * const differences = compareSettings(dexieSettings, sqliteSettings);
+ * console.log(`Added: ${differences.added.length}`);
+ * console.log(`Modified: ${differences.modified.length}`);
+ * console.log(`Missing: ${differences.missing.length}`);
+ * ```
+ */
+function compareSettings(
+ dexieSettings: Settings[],
+ sqliteSettings: Settings[],
+) {
+ const added: Settings[] = [];
+ const modified: Settings[] = [];
+ const unmodified: Settings[] = [];
+ const missing: Settings[] = [];
+
+ // Find settings that exist in Dexie but not in SQLite
+ for (const dexieSetting of dexieSettings) {
+ const sqliteSetting = sqliteSettings.find(
+ (s) => s.accountDid == dexieSetting.accountDid,
+ );
+ if (!sqliteSetting) {
+ added.push(dexieSetting);
+ } else if (!settingsEqual(dexieSetting, sqliteSetting)) {
+ modified.push(dexieSetting);
+ } else {
+ unmodified.push(dexieSetting);
+ }
+ }
+
+ // Find settings that exist in SQLite but not in Dexie
+ for (const sqliteSetting of sqliteSettings) {
+ const dexieSetting = dexieSettings.find(
+ (s) => s.accountDid == sqliteSetting.accountDid,
+ );
+ if (!dexieSetting) {
+ missing.push(sqliteSetting);
+ }
+ }
+
+ return { added, modified, unmodified, missing };
+}
+
+/**
+ * Compares accounts between Dexie and SQLite databases
+ *
+ * This helper function analyzes two arrays of accounts and identifies
+ * which accounts are added (in Dexie but not SQLite), modified
+ * (different between databases), or missing (in SQLite but not Dexie).
+ *
+ * The comparison is based on the account's ID as the primary key,
+ * with detailed field-by-field comparison for modified accounts.
+ *
+ * @function compareAccounts
+ * @param {Account[]} dexieAccounts - Accounts from Dexie database
+ * @param {Account[]} sqliteDids - Accounts from SQLite database
+ * @returns {Object} Object containing added, modified, and missing accounts
+ * @returns {Account[]} returns.added - Accounts in Dexie but not SQLite
+ * @returns {Account[]} returns.modified - always 0 because we don't check
+ * @returns {string[]} returns.missing - Accounts in SQLite but not Dexie
+ * @example
+ * ```typescript
+ * const differences = compareAccounts(dexieAccounts, sqliteAccounts);
+ * console.log(`Added: ${differences.added.length}`);
+ * console.log(`Modified: ${differences.modified.length}`);
+ * console.log(`Missing: ${differences.missing.length}`);
+ * ```
+ */
+function compareAccounts(dexieAccounts: Account[], sqliteDids: string[]) {
+ const added: Account[] = [];
+ const unmodified: Account[] = [];
+ const missing: string[] = [];
+
+ // Find accounts that exist in Dexie but not in SQLite
+ for (const dexieAccount of dexieAccounts) {
+ const sqliteDid = sqliteDids.find((a) => a === dexieAccount.did);
+ if (!sqliteDid) {
+ added.push(dexieAccount);
+ } else {
+ unmodified.push(dexieAccount);
+ }
+ }
+
+ // Find accounts that exist in SQLite but not in Dexie
+ for (const sqliteDid of sqliteDids) {
+ const dexieAccount = dexieAccounts.find((a) => a.did === sqliteDid);
+ if (!dexieAccount) {
+ missing.push(sqliteDid);
+ }
+ }
+
+ return { added, unmodified, missing };
+}
+
+/**
+ * Compares two contacts for equality
+ *
+ * This helper function performs a deep comparison of two Contact objects
+ * to determine if they are identical. The comparison includes all
+ * relevant fields including complex objects like contactMethods.
+ *
+ * For contactMethods, the function uses JSON.stringify to compare
+ * the arrays, ensuring that both structure and content are identical.
+ *
+ * @function contactsEqual
+ * @param {Contact} contact1 - First contact to compare
+ * @param {Contact} contact2 - Second contact to compare
+ * @returns {boolean} True if contacts are identical, false otherwise
+ * @example
+ * ```typescript
+ * const areEqual = contactsEqual(contact1, contact2);
+ * if (areEqual) {
+ * console.log('Contacts are identical');
+ * } else {
+ * console.log('Contacts differ');
+ * }
+ * ```
+ */
+function contactsEqual(contact1: Contact, contact2: Contact): boolean {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const ifEmpty = (arg: any, def: any) => (arg ? arg : def);
+ const contact1Methods =
+ contact1.contactMethods &&
+ Array.isArray(contact1.contactMethods) &&
+ contact1.contactMethods.length > 0
+ ? JSON.stringify(contact1.contactMethods)
+ : "[]";
+ const contact2Methods =
+ contact2.contactMethods &&
+ Array.isArray(contact2.contactMethods) &&
+ contact2.contactMethods.length > 0
+ ? JSON.stringify(contact2.contactMethods)
+ : "[]";
+ return (
+ ifEmpty(contact1.did, "") == ifEmpty(contact2.did, "") &&
+ ifEmpty(contact1.name, "") == ifEmpty(contact2.name, "") &&
+ ifEmpty(contact1.notes, "") == ifEmpty(contact2.notes, "") &&
+ ifEmpty(contact1.profileImageUrl, "") ==
+ ifEmpty(contact2.profileImageUrl, "") &&
+ ifEmpty(contact1.publicKeyBase64, "") ==
+ ifEmpty(contact2.publicKeyBase64, "") &&
+ ifEmpty(contact1.nextPubKeyHashB64, "") ==
+ ifEmpty(contact2.nextPubKeyHashB64, "") &&
+ !!contact1.seesMe == !!contact2.seesMe &&
+ !!contact1.registered == !!contact2.registered &&
+ contact1Methods == contact2Methods
+ );
+}
+
+/**
+ * Compares two settings for equality
+ *
+ * This helper function performs a deep comparison of two Settings objects
+ * to determine if they are identical. The comparison includes all
+ * relevant fields including complex objects like searchBoxes.
+ *
+ * For searchBoxes, the function uses JSON.stringify to compare
+ * the arrays, ensuring that both structure and content are identical.
+ *
+ * @function settingsEqual
+ * @param {Settings} settings1 - First settings to compare
+ * @param {Settings} settings2 - Second settings to compare
+ * @returns {boolean} True if settings are identical, false otherwise
+ * @example
+ * ```typescript
+ * const areEqual = settingsEqual(settings1, settings2);
+ * if (areEqual) {
+ * console.log('Settings are identical');
+ * } else {
+ * console.log('Settings differ');
+ * }
+ * ```
+ */
+function settingsEqual(settings1: Settings, settings2: Settings): boolean {
+ return (
+ settings1.id == settings2.id &&
+ settings1.accountDid == settings2.accountDid &&
+ settings1.activeDid == settings2.activeDid &&
+ settings1.apiServer == settings2.apiServer &&
+ settings1.filterFeedByNearby == settings2.filterFeedByNearby &&
+ settings1.filterFeedByVisible == settings2.filterFeedByVisible &&
+ settings1.finishedOnboarding == settings2.finishedOnboarding &&
+ settings1.firstName == settings2.firstName &&
+ settings1.hideRegisterPromptOnNewContact ==
+ settings2.hideRegisterPromptOnNewContact &&
+ settings1.isRegistered == settings2.isRegistered &&
+ settings1.lastName == settings2.lastName &&
+ settings1.lastAckedOfferToUserJwtId ==
+ settings2.lastAckedOfferToUserJwtId &&
+ settings1.lastAckedOfferToUserProjectsJwtId ==
+ settings2.lastAckedOfferToUserProjectsJwtId &&
+ settings1.lastNotifiedClaimId == settings2.lastNotifiedClaimId &&
+ settings1.lastViewedClaimId == settings2.lastViewedClaimId &&
+ settings1.notifyingNewActivityTime == settings2.notifyingNewActivityTime &&
+ settings1.notifyingReminderMessage == settings2.notifyingReminderMessage &&
+ settings1.notifyingReminderTime == settings2.notifyingReminderTime &&
+ settings1.partnerApiServer == settings2.partnerApiServer &&
+ settings1.passkeyExpirationMinutes == settings2.passkeyExpirationMinutes &&
+ settings1.profileImageUrl == settings2.profileImageUrl &&
+ settings1.showContactGivesInline == settings2.showContactGivesInline &&
+ settings1.showGeneralAdvanced == settings2.showGeneralAdvanced &&
+ settings1.showShortcutBvc == settings2.showShortcutBvc &&
+ settings1.vapid == settings2.vapid &&
+ settings1.warnIfProdServer == settings2.warnIfProdServer &&
+ settings1.warnIfTestServer == settings2.warnIfTestServer &&
+ settings1.webPushServer == settings2.webPushServer &&
+ JSON.stringify(settings1.searchBoxes) ==
+ JSON.stringify(settings2.searchBoxes)
+ );
+}
+
+/**
+ * Compares two accounts for equality
+ *
+ * This helper function performs a deep comparison of two Account objects
+ * to determine if they are identical. The comparison includes all
+ * relevant fields including complex objects like identity.
+ *
+ * For identity, the function uses JSON.stringify to compare
+ * the objects, ensuring that both structure and content are identical.
+ *
+ * @function accountsEqual
+ * @param {Account} account1 - First account to compare
+ * @param {Account} account2 - Second account to compare
+ * @returns {boolean} True if accounts are identical, false otherwise
+ * @example
+ * ```typescript
+ * const areEqual = accountsEqual(account1, account2);
+ * if (areEqual) {
+ * console.log('Accounts are identical');
+ * } else {
+ * console.log('Accounts differ');
+ * }
+ * ```
+ */
+//
+// unused
+//
+// function accountsEqual(account1: Account, account2: Account): boolean {
+// return (
+// account1.id === account2.id &&
+// account1.dateCreated === account2.dateCreated &&
+// account1.derivationPath === account2.derivationPath &&
+// account1.did === account2.did &&
+// account1.identity === account2.identity &&
+// account1.mnemonic === account2.mnemonic &&
+// account1.passkeyCredIdHex === account2.passkeyCredIdHex &&
+// account1.publicKeyHex === account2.publicKeyHex
+// );
+// }
+
+/**
+ * Generates YAML-formatted comparison data
+ *
+ * This function converts the database comparison results into a
+ * structured format that can be exported and analyzed. The output
+ * is actually JSON but formatted in a YAML-like structure for
+ * better readability.
+ *
+ * The generated data includes summary statistics, detailed differences,
+ * and the actual data from both databases for inspection purposes.
+ *
+ * @function generateComparisonYaml
+ * @param {DataComparison} comparison - The comparison results to format
+ * @returns {string} JSON string formatted for readability
+ * @example
+ * ```typescript
+ * const comparison = await compareDatabases();
+ * const yaml = generateComparisonYaml(comparison);
+ * console.log(yaml);
+ * // Save to file or display in UI
+ * ```
+ */
+export function generateComparisonYaml(comparison: DataComparison): string {
+ const yaml = {
+ summary: {
+ dexieContacts: comparison.dexieContacts.length,
+ sqliteContacts: comparison.sqliteContacts.filter((c) => c.did).length,
+ dexieSettings: comparison.dexieSettings.length,
+ sqliteSettings: comparison.sqliteSettings.filter(
+ (s) => s.accountDid || s.activeDid,
+ ).length,
+ dexieAccounts: comparison.dexieAccounts.length,
+ sqliteAccounts: comparison.sqliteAccounts.filter((a) => a).length,
+ },
+ differences: {
+ contacts: {
+ added: comparison.differences.contacts.added.length,
+ modified: comparison.differences.contacts.modified.length,
+ unmodified: comparison.differences.contacts.unmodified.length,
+ missing: comparison.differences.contacts.missing.filter((c) => c.did)
+ .length,
+ },
+ settings: {
+ added: comparison.differences.settings.added.length,
+ modified: comparison.differences.settings.modified.length,
+ unmodified: comparison.differences.settings.unmodified.length,
+ missing: comparison.differences.settings.missing.filter(
+ (s) => s.accountDid || s.activeDid,
+ ).length,
+ },
+ accounts: {
+ added: comparison.differences.accounts.added.length,
+ unmodified: comparison.differences.accounts.unmodified.length,
+ missing: comparison.differences.accounts.missing.filter((a) => a)
+ .length,
+ },
+ },
+ details: {
+ contacts: {
+ dexie: comparison.dexieContacts.map((c) => ({
+ did: c.did,
+ name: c.name || "",
+ contactMethods: (c.contactMethods || []).length,
+ })),
+ sqlite: comparison.sqliteContacts
+ .filter((c) => c.did)
+ .map((c) => ({
+ did: c.did,
+ name: c.name || "",
+ contactMethods: (c.contactMethods || []).length,
+ })),
+ },
+ settings: {
+ dexie: comparison.dexieSettings.map((s) => ({
+ id: s.id,
+ type: s.id === MASTER_SETTINGS_KEY ? "master" : "account",
+ did: s.activeDid || s.accountDid,
+ isRegistered: s.isRegistered || false,
+ })),
+ sqlite: comparison.sqliteSettings
+ .filter((s) => s.accountDid || s.activeDid)
+ .map((s) => ({
+ id: s.id,
+ type: s.id === MASTER_SETTINGS_KEY ? "master" : "account",
+ did: s.activeDid || s.accountDid,
+ isRegistered: s.isRegistered || false,
+ })),
+ },
+ accounts: {
+ dexie: comparison.dexieAccounts.map((a) => ({
+ id: a.id,
+ did: a.did,
+ dateCreated: a.dateCreated,
+ hasIdentity: !!a.identity,
+ hasMnemonic: !!a.mnemonic,
+ })),
+ sqlite: comparison.sqliteAccounts.map((a) => ({
+ did: a,
+ })),
+ },
+ },
+ };
+
+ return JSON.stringify(yaml, null, 2);
+}
+
+/**
+ * Migrates contacts from Dexie to SQLite database
+ *
+ * This function transfers all contacts from the Dexie database to the
+ * SQLite database. It handles both new contacts (INSERT) and existing
+ * contacts (UPDATE) based on the overwriteExisting parameter.
+ *
+ * The function processes contacts one by one to ensure data integrity
+ * and provides detailed logging of the migration process. It returns
+ * comprehensive results including success status, counts, and any
+ * errors or warnings encountered.
+ *
+ * @async
+ * @function migrateContacts
+ * @param {boolean} [overwriteExisting=false] - Whether to overwrite existing contacts in SQLite
+ * @returns {Promise} Detailed results of the migration operation
+ * @throws {Error} If the migration process fails completely
+ * @example
+ * ```typescript
+ * try {
+ * const result = await migrateContacts(true); // Overwrite existing
+ * if (result.success) {
+ * console.log(`Successfully migrated ${result.contactsMigrated} contacts`);
+ * } else {
+ * console.error('Migration failed:', result.errors);
+ * }
+ * } catch (error) {
+ * console.error('Migration process failed:', error);
+ * }
+ * ```
+ */
+/**
+ *
+ * I recommend using the existing contact import view to migrate contacts.
+ *
+export async function migrateContacts(
+ overwriteExisting: boolean = false,
+): Promise {
+ logger.info("[IndexedDBMigrationService] Starting contact migration", {
+ overwriteExisting,
+ });
+
+ const result: MigrationResult = {
+ success: true,
+ contactsMigrated: 0,
+ settingsMigrated: 0,
+ accountsMigrated: 0,
+ errors: [],
+ warnings: [],
+ };
+
+ try {
+ const dexieContacts = await getDexieContacts();
+ const platformService = PlatformServiceFactory.getInstance();
+
+ for (const contact of dexieContacts) {
+ try {
+ // Check if contact already exists
+ const existingResult = await platformService.dbQuery(
+ "SELECT did FROM contacts WHERE did = ?",
+ [contact.did],
+ );
+
+ if (existingResult?.values?.length) {
+ if (overwriteExisting) {
+ // Update existing contact
+ const { sql, params } = generateUpdateStatement(
+ contact as unknown as Record,
+ "contacts",
+ "did = ?",
+ [contact.did],
+ );
+ await platformService.dbExec(sql, params);
+ result.contactsMigrated++;
+ logger.info(`[IndexedDBMigrationService] Updated contact: ${contact.did}`);
+ } else {
+ result.warnings.push(
+ `Contact ${contact.did} already exists, skipping`,
+ );
+ }
+ } else {
+ // Insert new contact
+ const { sql, params } = generateInsertStatement(
+ contact as unknown as Record,
+ "contacts",
+ );
+ await platformService.dbExec(sql, params);
+ result.contactsMigrated++;
+ logger.info(`[IndexedDBMigrationService] Added contact: ${contact.did}`);
+ }
+ } catch (error) {
+ const errorMsg = `Failed to migrate contact ${contact.did}: ${error}`;
+ logger.error("[IndexedDBMigrationService]", errorMsg);
+ result.errors.push(errorMsg);
+ result.success = false;
+ }
+ }
+
+ logger.info("[IndexedDBMigrationService] Contact migration completed", {
+ contactsMigrated: result.contactsMigrated,
+ errors: result.errors.length,
+ warnings: result.warnings.length,
+ });
+
+ return result;
+ } catch (error) {
+ const errorMsg = `Contact migration failed: ${error}`;
+ logger.error("[IndexedDBMigrationService]", errorMsg);
+ result.errors.push(errorMsg);
+ result.success = false;
+ return result;
+ }
+}
+ *
+ */
+
+/**
+ * Migrates specific settings fields from Dexie to SQLite database
+ *
+ * This function transfers specific settings fields from the Dexie database
+ * to the SQLite database. It focuses on the most important user-facing
+ * settings: firstName, isRegistered, profileImageUrl, showShortcutBvc,
+ * and searchBoxes.
+ *
+ * The function handles duplicate settings by merging master settings (id=1)
+ * with account-specific settings (id=2) for the same DID, preferring
+ * the most recent values for the specified fields.
+ *
+ * @async
+ * @function migrateSettings
+ * @returns {Promise} Detailed results of the migration operation
+ * @throws {Error} If the migration process fails completely
+ * @example
+ * ```typescript
+ * try {
+ * const result = await migrateSettings();
+ * if (result.success) {
+ * console.log(`Successfully migrated ${result.settingsMigrated} settings`);
+ * } else {
+ * console.error('Migration failed:', result.errors);
+ * }
+ * } catch (error) {
+ * console.error('Migration process failed:', error);
+ * }
+ * ```
+ */
+export async function migrateSettings(): Promise {
+ logger.info("[IndexedDBMigrationService] Starting settings migration");
+
+ const result: MigrationResult = {
+ success: true,
+ contactsMigrated: 0,
+ settingsMigrated: 0,
+ accountsMigrated: 0,
+ errors: [],
+ warnings: [],
+ };
+
+ try {
+ const dexieSettings = await getDexieSettings();
+ logger.info("[IndexedDBMigrationService] Migrating settings", {
+ dexieSettings: dexieSettings.length,
+ });
+ const platformService = PlatformServiceFactory.getInstance();
+
+ // Create an array of promises for all settings migrations
+ const migrationPromises = dexieSettings.map(async (setting) => {
+ logger.info(
+ "[IndexedDBMigrationService] Starting to migrate settings",
+ setting,
+ );
+
+ // adjust SQL based on the accountDid key, maybe null
+ let conditional: string;
+ let preparams: unknown[];
+ if (!setting.accountDid) {
+ conditional = "accountDid is null";
+ preparams = [];
+ } else {
+ conditional = "accountDid = ?";
+ preparams = [setting.accountDid];
+ }
+ const sqliteSettingRaw = await platformService.dbQuery(
+ "SELECT * FROM settings WHERE " + conditional,
+ preparams,
+ );
+
+ logger.info(
+ "[IndexedDBMigrationService] Migrating one set of settings:",
+ {
+ setting,
+ sqliteSettingRaw,
+ },
+ );
+ if (sqliteSettingRaw?.values?.length) {
+ // should cover the master settings, where accountDid is null
+ delete setting.id; // don't conflict with the id in the sqlite database
+ delete setting.accountDid; // this is part of the where clause
+ const { sql, params } = generateUpdateStatement(
+ setting,
+ "settings",
+ conditional,
+ preparams,
+ );
+ logger.info("[IndexedDBMigrationService] Updating settings", {
+ sql,
+ params,
+ });
+ await platformService.dbExec(sql, params);
+ result.settingsMigrated++;
+ } else {
+ // insert new setting
+ delete setting.id; // don't conflict with the id in the sqlite database
+ delete setting.activeDid; // ensure we don't set the activeDid (since master settings are an update and don't hit this case)
+ const { sql, params } = generateInsertStatement(setting, "settings");
+ await platformService.dbExec(sql, params);
+ result.settingsMigrated++;
+ }
+ });
+
+ // Wait for all migrations to complete
+ const updatedSettings = await Promise.all(migrationPromises);
+
+ logger.info(
+ "[IndexedDBMigrationService] Finished migrating settings",
+ updatedSettings,
+ result,
+ );
+
+ return result;
+ } catch (error) {
+ logger.error(
+ "[IndexedDBMigrationService] Complete settings migration failed:",
+ error,
+ );
+ const errorMessage = `Settings migration failed: ${error}`;
+ result.errors.push(errorMessage);
+ result.success = false;
+ return result;
+ }
+}
+
+/**
+ * Migrates accounts from Dexie to SQLite database
+ *
+ * This function transfers all accounts from the Dexie database to the
+ * SQLite database. It handles both new accounts (INSERT) and existing
+ * accounts (UPDATE).
+ *
+ * For accounts with mnemonic data, the function uses importFromMnemonic
+ * to ensure proper key derivation and identity creation during migration.
+ *
+ * The function processes accounts one by one to ensure data integrity
+ * and provides detailed logging of the migration process. It returns
+ * comprehensive results including success status, counts, and any
+ * errors or warnings encountered.
+ *
+ * @async
+ * @function migrateAccounts
+ * @returns {Promise} Detailed results of the migration operation
+ * @throws {Error} If the migration process fails completely
+ * @example
+ * ```typescript
+ * try {
+ * const result = await migrateAccounts();
+ * if (result.success) {
+ * console.log(`Successfully migrated ${result.accountsMigrated} accounts`);
+ * } else {
+ * console.error('Migration failed:', result.errors);
+ * }
+ * } catch (error) {
+ * console.error('Migration process failed:', error);
+ * }
+ * ```
+ */
+export async function migrateAccounts(): Promise {
+ logger.info("[IndexedDBMigrationService] Starting account migration");
+
+ const result: MigrationResult = {
+ success: true,
+ contactsMigrated: 0,
+ settingsMigrated: 0,
+ accountsMigrated: 0,
+ errors: [],
+ warnings: [],
+ };
+
+ try {
+ const dexieAccounts = await getDexieAccounts();
+ const platformService = PlatformServiceFactory.getInstance();
+
+ // Group accounts by DID and keep only the most recent one
+ const accountsByDid = new Map();
+ dexieAccounts.forEach((account) => {
+ const existingAccount = accountsByDid.get(account.did);
+ if (
+ !existingAccount ||
+ new Date(account.dateCreated) > new Date(existingAccount.dateCreated)
+ ) {
+ accountsByDid.set(account.did, account);
+ if (existingAccount) {
+ result.warnings.push(
+ `Found duplicate account for DID ${account.did}, keeping most recent`,
+ );
+ }
+ }
+ });
+
+ // Process each unique account
+ for (const [did, account] of accountsByDid.entries()) {
+ try {
+ // Check if account already exists
+ const existingResult = await platformService.dbQuery(
+ "SELECT did FROM accounts WHERE did = ?",
+ [did],
+ );
+
+ if (existingResult?.values?.length) {
+ result.warnings.push(
+ `Account with DID ${did} already exists, skipping`,
+ );
+ continue;
+ }
+ if (account.mnemonic) {
+ await importFromMnemonic(account.mnemonic, account.derivationPath);
+ result.accountsMigrated++;
+ } else {
+ result.errors.push(
+ `Account with DID ${did} has no mnemonic, skipping`,
+ );
+ }
+
+ logger.info(
+ "[IndexedDBMigrationService] Successfully migrated account",
+ {
+ did,
+ dateCreated: account.dateCreated,
+ },
+ );
+ } catch (error) {
+ const errorMessage = `Failed to migrate account ${did}: ${error}`;
+ result.errors.push(errorMessage);
+ logger.error("[IndexedDBMigrationService] Account migration failed:", {
+ error,
+ did,
+ });
+ }
+ }
+
+ if (result.errors.length > 0) {
+ result.success = false;
+ }
+
+ return result;
+ } catch (error) {
+ const errorMessage = `Account migration failed: ${error}`;
+ result.errors.push(errorMessage);
+ result.success = false;
+ logger.error(
+ "[IndexedDBMigrationService] Complete account migration failed:",
+ error,
+ );
+ return result;
+ }
+}
+
+/**
+ * Migrates all data from Dexie to SQLite in the proper order
+ *
+ * This function performs a complete migration of all data from Dexie to SQLite
+ * in the correct order to avoid foreign key constraint issues:
+ * 1. Accounts (foundational - contains DIDs)
+ * 2. Settings (references accountDid, activeDid)
+ * 3. ActiveDid (depends on accounts and settings)
+ * 4. Contacts (independent, but migrated after accounts for consistency)
+ *
+ * The migration runs within a transaction to ensure atomicity. If any step fails,
+ * the entire migration is rolled back.
+ *
+ * @returns Promise - Detailed result of the migration operation
+ */
+export async function migrateAll(): Promise {
+ const result: MigrationResult = {
+ success: false,
+ contactsMigrated: 0,
+ settingsMigrated: 0,
+ accountsMigrated: 0,
+ errors: [],
+ warnings: [],
+ };
+
+ try {
+ logger.info(
+ "[IndexedDBMigrationService] Starting complete migration from Dexie to SQLite",
+ );
+
+ // Step 1: Migrate Accounts (foundational)
+ logger.info("[IndexedDBMigrationService] Step 1: Migrating accounts...");
+ const accountsResult = await migrateAccounts();
+ if (!accountsResult.success) {
+ result.errors.push(
+ `Account migration failed: ${accountsResult.errors.join(", ")}`,
+ );
+ return result;
+ }
+ result.accountsMigrated = accountsResult.accountsMigrated;
+ result.warnings.push(...accountsResult.warnings);
+
+ // Step 2: Migrate Settings (depends on accounts)
+ logger.info("[IndexedDBMigrationService] Step 2: Migrating settings...");
+ const settingsResult = await migrateSettings();
+ if (!settingsResult.success) {
+ result.errors.push(
+ `Settings migration failed: ${settingsResult.errors.join(", ")}`,
+ );
+ return result;
+ }
+ result.settingsMigrated = settingsResult.settingsMigrated;
+ result.warnings.push(...settingsResult.warnings);
+
+ // Step 4: Migrate Contacts (independent, but after accounts for consistency)
+ // ... but which is better done through the contact import view
+ // logger.info("[IndexedDBMigrationService] Step 4: Migrating contacts...");
+ // const contactsResult = await migrateContacts();
+ // if (!contactsResult.success) {
+ // result.errors.push(
+ // `Contact migration failed: ${contactsResult.errors.join(", ")}`,
+ // );
+ // return result;
+ // }
+ // result.contactsMigrated = contactsResult.contactsMigrated;
+ // result.warnings.push(...contactsResult.warnings);
+
+ // All migrations successful
+ result.success = true;
+ const totalMigrated =
+ result.accountsMigrated +
+ result.settingsMigrated +
+ result.contactsMigrated;
+
+ logger.info(
+ `[IndexedDBMigrationService] Complete migration successful: ${totalMigrated} total records migrated`,
+ {
+ accounts: result.accountsMigrated,
+ settings: result.settingsMigrated,
+ contacts: result.contactsMigrated,
+ warnings: result.warnings.length,
+ },
+ );
+
+ return result;
+ } catch (error) {
+ const errorMessage = `Complete migration failed: ${error}`;
+ result.errors.push(errorMessage);
+ logger.error(
+ "[IndexedDBMigrationService] Complete migration failed:",
+ error,
+ );
+ return result;
+ }
+}
diff --git a/src/services/migrationService.ts b/src/services/migrationService.ts
index 81d9de74..089c7d65 100644
--- a/src/services/migrationService.ts
+++ b/src/services/migrationService.ts
@@ -1,60 +1,133 @@
+/**
+ * Manage database migrations as people upgrade their app over time
+ */
+
+import { logger } from "../utils/logger";
+
+/**
+ * Migration interface for database schema migrations
+ */
interface Migration {
name: string;
sql: string;
}
-export class MigrationService {
- private static instance: MigrationService;
+/**
+ * Migration registry to store and manage database migrations
+ */
+class MigrationRegistry {
private migrations: Migration[] = [];
- private constructor() {}
-
- static getInstance(): MigrationService {
- if (!MigrationService.instance) {
- MigrationService.instance = new MigrationService();
- }
- return MigrationService.instance;
+ /**
+ * Register a migration with the registry
+ *
+ * @param migration - The migration to register
+ */
+ registerMigration(migration: Migration): void {
+ this.migrations.push(migration);
}
- registerMigration(migration: Migration) {
- this.migrations.push(migration);
+ /**
+ * Get all registered migrations
+ *
+ * @returns Array of registered migrations
+ */
+ getMigrations(): Migration[] {
+ return this.migrations;
}
/**
- * @param sqlExec - A function that executes a SQL statement and returns some update result
- * @param sqlQuery - A function that executes a SQL query and returns the result in some format
- * @param extractMigrationNames - A function that extracts the names (string array) from a "select name from migrations" query
+ * Clear all registered migrations
*/
- async runMigrations(
- // note that this does not take parameters because the Capacitor SQLite 'execute' is different
- sqlExec: (sql: string) => Promise,
- sqlQuery: (sql: string) => Promise,
- extractMigrationNames: (result: T) => Set,
- ): Promise {
+ clearMigrations(): void {
+ this.migrations = [];
+ }
+}
+
+// Create a singleton instance of the migration registry
+const migrationRegistry = new MigrationRegistry();
+
+/**
+ * Register a migration with the migration service
+ *
+ * This function is used by the migration system to register database
+ * schema migrations that need to be applied to the database.
+ *
+ * @param migration - The migration to register
+ */
+export function registerMigration(migration: Migration): void {
+ migrationRegistry.registerMigration(migration);
+}
+
+/**
+ * Run all registered migrations against the database
+ *
+ * This function executes all registered migrations in order, checking
+ * which ones have already been applied to avoid duplicate execution.
+ * It creates a migrations table if it doesn't exist to track applied
+ * migrations.
+ *
+ * @param sqlExec - Function to execute SQL statements
+ * @param sqlQuery - Function to query SQL data
+ * @param extractMigrationNames - Function to extract migration names from query results
+ * @returns Promise that resolves when all migrations are complete
+ */
+export async function runMigrations(
+ sqlExec: (sql: string, params?: unknown[]) => Promise,
+ sqlQuery: (sql: string, params?: unknown[]) => Promise,
+ extractMigrationNames: (result: T) => Set,
+): Promise {
+ try {
// Create migrations table if it doesn't exist
await sqlExec(`
CREATE TABLE IF NOT EXISTS migrations (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- name TEXT NOT NULL UNIQUE,
- executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+ name TEXT PRIMARY KEY,
+ applied_at TEXT DEFAULT CURRENT_TIMESTAMP
);
`);
- // Get list of executed migrations
- const result1: T = await sqlQuery("SELECT name FROM migrations;");
- const executedMigrations = extractMigrationNames(result1);
+ // Get list of already applied migrations
+ const appliedMigrationsResult = await sqlQuery(
+ "SELECT name FROM migrations",
+ );
+ const appliedMigrations = extractMigrationNames(appliedMigrationsResult);
+
+ // Get all registered migrations
+ const migrations = migrationRegistry.getMigrations();
+
+ if (migrations.length === 0) {
+ logger.warn("[MigrationService] No migrations registered");
+ return;
+ }
+
+ // Run each migration that hasn't been applied yet
+ for (const migration of migrations) {
+ if (appliedMigrations.has(migration.name)) {
+ continue;
+ }
- // Run pending migrations in order
- for (const migration of this.migrations) {
- if (!executedMigrations.has(migration.name)) {
+ try {
+ // Execute the migration SQL
await sqlExec(migration.sql);
- await sqlExec(
- `INSERT INTO migrations (name) VALUES ('${migration.name}')`,
+ // Record that the migration was applied
+ await sqlExec("INSERT INTO migrations (name) VALUES (?)", [
+ migration.name,
+ ]);
+
+ logger.info(
+ `[MigrationService] Successfully applied migration: ${migration.name}`,
+ );
+ } catch (error) {
+ logger.error(
+ `[MigrationService] Failed to apply migration ${migration.name}:`,
+ error,
);
+ throw new Error(`Migration ${migration.name} failed: ${error}`);
}
}
+ } catch (error) {
+ logger.error("[MigrationService] Migration process failed:", error);
+ throw error;
}
}
-
-export default MigrationService.getInstance();
diff --git a/src/services/platforms/CapacitorPlatformService.ts b/src/services/platforms/CapacitorPlatformService.ts
index 26bef6f8..0e9008b8 100644
--- a/src/services/platforms/CapacitorPlatformService.ts
+++ b/src/services/platforms/CapacitorPlatformService.ts
@@ -5,6 +5,7 @@ import {
CameraSource,
CameraDirection,
} from "@capacitor/camera";
+import { Capacitor } from "@capacitor/core";
import { Share } from "@capacitor/share";
import {
SQLiteConnection,
@@ -247,7 +248,7 @@ export class CapacitorPlatformService implements PlatformService {
hasFileSystem: true,
hasCamera: true,
isMobile: true,
- isIOS: /iPad|iPhone|iPod/.test(navigator.userAgent),
+ isIOS: Capacitor.getPlatform() === "ios",
hasFileDownload: false,
needsFileHandlingInstructions: true,
isNativeApp: true,
diff --git a/src/utils/logger.ts b/src/utils/logger.ts
index 2cbb228b..8184c28b 100644
--- a/src/utils/logger.ts
+++ b/src/utils/logger.ts
@@ -1,6 +1,6 @@
import { logToDb } from "../db/databaseUtil";
-function safeStringify(obj: unknown) {
+export function safeStringify(obj: unknown) {
const seen = new WeakSet();
return JSON.stringify(obj, (_key, value) => {
@@ -52,23 +52,18 @@ export const logger = {
}
},
warn: (message: string, ...args: unknown[]) => {
- if (
- process.env.NODE_ENV !== "production" ||
- process.env.VITE_PLATFORM === "capacitor" ||
- process.env.VITE_PLATFORM === "electron"
- ) {
- // eslint-disable-next-line no-console
- console.warn(message, ...args);
- const argsString = args.length > 0 ? " - " + safeStringify(args) : "";
- logToDb(message + argsString);
- }
+ // eslint-disable-next-line no-console
+ console.warn(message, ...args);
+ const argsString = args.length > 0 ? " - " + safeStringify(args) : "";
+ logToDb(message + argsString);
},
error: (message: string, ...args: unknown[]) => {
// Errors will always be logged
// eslint-disable-next-line no-console
console.error(message, ...args);
- const argsString = args.length > 0 ? " - " + safeStringify(args) : "";
- logToDb(message + argsString);
+ const messageString = safeStringify(message);
+ const argsString = args.length > 0 ? safeStringify(args) : "";
+ logToDb(messageString + argsString);
},
};
diff --git a/src/views/AccountViewView.vue b/src/views/AccountViewView.vue
index c3fee747..b91dc224 100644
--- a/src/views/AccountViewView.vue
+++ b/src/views/AccountViewView.vue
@@ -349,8 +349,9 @@
- For your security, choose a location nearby but not exactly at your
- place.
+ The location you choose will be shared with the world until you remove
+ this checkbox. For your security, choose a location nearby but not
+ exactly at your true location, like at your town center.
You have done
{{ endorserLimits?.doneClaimsThisWeek || "?" }} claim{{
+ >{{ endorserLimits?.doneClaimsThisWeek ?? "?" }} claim{{
endorserLimits?.doneClaimsThisWeek === 1 ? "" : "s"
}}
- out of {{ endorserLimits?.maxClaimsPerWeek || "?" }} for this
+ out of {{ endorserLimits?.maxClaimsPerWeek ?? "?" }} for this
week. Your claims counter resets at
{{
readableDate(endorserLimits?.nextWeekBeginDateTime)
@@ -449,14 +450,14 @@
You have done
{{
- endorserLimits?.doneRegistrationsThisMonth || "?"
+ endorserLimits?.doneRegistrationsThisMonth ?? "?"
}}
registration{{
endorserLimits?.doneRegistrationsThisMonth === 1 ? "" : "s"
}}
out of
- {{ endorserLimits?.maxRegistrationsPerMonth || "?" }} for this
+ {{ endorserLimits?.maxRegistrationsPerMonth ?? "?" }} for this
this month.
(You cannot register anyone on your first day.)
Your registration counter resets at
@@ -467,11 +468,11 @@
You have uploaded
{{ imageLimits?.doneImagesThisWeek || "?" }} image{{
+ >{{ imageLimits?.doneImagesThisWeek ?? "?" }} image{{
imageLimits?.doneImagesThisWeek === 1 ? "" : "s"
}}
- out of {{ imageLimits?.maxImagesPerWeek || "?" }} for this
+ out of {{ imageLimits?.maxImagesPerWeek ?? "?" }} for this
week. Your image counter resets at
{{
readableDate(imageLimits?.nextWeekBeginDateTime)
diff --git a/src/views/ClaimAddRawView.vue b/src/views/ClaimAddRawView.vue
index 3ff401f0..c63c33ad 100644
--- a/src/views/ClaimAddRawView.vue
+++ b/src/views/ClaimAddRawView.vue
@@ -198,7 +198,7 @@ export default class ClaimAddRawView extends Vue {
this.apiServer,
this.axios,
);
- if (result.type === "success") {
+ if (result.success) {
this.$notify(
{
group: "alert",
diff --git a/src/views/ClaimView.vue b/src/views/ClaimView.vue
index 602373a4..8edf3bf8 100644
--- a/src/views/ClaimView.vue
+++ b/src/views/ClaimView.vue
@@ -46,23 +46,35 @@
@@ -436,7 +436,7 @@ import { Component, Vue } from "vue-facing-decorator";
import { useClipboard } from "@vueuse/core";
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
import QuickNav from "../components/QuickNav.vue";
-import { NotificationIface, USE_DEXIE_DB } from "../constants/app";
+import { APP_SERVER, NotificationIface, USE_DEXIE_DB } from "../constants/app";
import { db, retrieveSettingsForActiveAccount } from "../db/index";
import { Contact } from "../db/tables/contacts";
import * as databaseUtil from "../db/databaseUtil";
@@ -494,7 +494,7 @@ export default class ConfirmGiftView extends Vue {
veriClaim = serverUtil.BLANK_GENERIC_SERVER_RECORD;
veriClaimDump = "";
veriClaimDidsVisible: { [key: string]: string[] } = {};
- windowLocation = window.location.href;
+ windowLocation = window.location.href; // this is changed to a deep link in the setup
R = R;
yaml = yaml;
@@ -566,6 +566,9 @@ export default class ConfirmGiftView extends Vue {
}
const claimId = decodeURIComponent(pathParam);
+
+ this.windowLocation = APP_SERVER + "/deep-link/confirm-gift/" + claimId;
+
await this.loadClaim(claimId, this.activeDid);
}
@@ -676,12 +679,12 @@ export default class ConfirmGiftView extends Vue {
/**
* Add participant (giver/recipient) name & URL info
*/
+ this.giverName = this.didInfo(this.giveDetails?.agentDid);
if (this.giveDetails?.agentDid) {
- this.giverName = this.didInfo(this.giveDetails.agentDid);
this.urlForNewGive += `&giverDid=${encodeURIComponent(this.giveDetails.agentDid)}&giverName=${encodeURIComponent(this.giverName)}`;
}
+ this.recipientName = this.didInfo(this.giveDetails?.recipientDid);
if (this.giveDetails?.recipientDid) {
- this.recipientName = this.didInfo(this.giveDetails.recipientDid);
this.urlForNewGive += `&recipientDid=${encodeURIComponent(this.giveDetails.recipientDid)}&recipientName=${encodeURIComponent(this.recipientName)}`;
}
@@ -831,7 +834,7 @@ export default class ConfirmGiftView extends Vue {
this.apiServer,
this.axios,
);
- if (result.type === "success") {
+ if (result.success) {
this.$notify(
{
group: "alert",
diff --git a/src/views/ContactImportView.vue b/src/views/ContactImportView.vue
index f933aee4..294a45fe 100644
--- a/src/views/ContactImportView.vue
+++ b/src/views/ContactImportView.vue
@@ -566,7 +566,7 @@ export default class ContactImportView extends Vue {
this.checkingImports = true;
try {
- const jwt: string = getContactJwtFromJwtUrl(jwtInput);
+ const jwt: string = getContactJwtFromJwtUrl(jwtInput) || "";
const payload = decodeEndorserJwt(jwt).payload;
if (Array.isArray(payload.contacts)) {
diff --git a/src/views/ContactQRScanFullView.vue b/src/views/ContactQRScanFullView.vue
index 64f58829..c5a08b74 100644
--- a/src/views/ContactQRScanFullView.vue
+++ b/src/views/ContactQRScanFullView.vue
@@ -104,6 +104,7 @@
diff --git a/src/views/DatabaseMigration.vue b/src/views/DatabaseMigration.vue
new file mode 100644
index 00000000..a2edc1c9
--- /dev/null
+++ b/src/views/DatabaseMigration.vue
@@ -0,0 +1,1492 @@
+
+
+
+
+
+
Database Migration
+
+ Compare and migrate data between Dexie (IndexedDB) and SQLite
+ databases
+
+
+ First, we recommend you hit these first two buttons to get your
+ backups, just in case! If anything doesn't look right when you're
+ done, Trent will help with... anything and everything. 😁
+
+
+
+
+
+
+
+
+ Beware: you have unexpected existing data in the new database that
+ will be overwritten. Talk with Trent.
+
+
+ We cannot find your main account. Talk with Trent.
+
+
+ You have multiple accounts. If you didn't mean to, talk with Trent.
+
+
+
+
+ Here is your seed for account
+ {{
+ downloadMnemonicAddress
+ ?.substring("did:ethr:0x".length)
+ .substring(0, 3)
+ }}
+ -- write it down!
+
+ {{ downloadMnemonic }}
+
+
+
+
+
diff --git a/src/views/DiscoverView.vue b/src/views/DiscoverView.vue
index 3c63a57b..07bde02f 100644
--- a/src/views/DiscoverView.vue
+++ b/src/views/DiscoverView.vue
@@ -523,9 +523,7 @@ export default class DiscoverView extends Vue {
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (e: any) {
- logger.error("Error with search all:", e);
- // this sometimes gives different information
- logger.error("Error with search all (error added): " + e);
+ logger.error("Error with search all: " + errorStringForLog(e));
this.$notify(
{
group: "alert",
@@ -617,7 +615,7 @@ export default class DiscoverView extends Vue {
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (e: any) {
- logger.error("Error with search local:", e);
+ logger.error("Error with search local: " + errorStringForLog(e));
this.$notify(
{
group: "alert",
@@ -788,7 +786,7 @@ export default class DiscoverView extends Vue {
const route = {
path: this.isProjectsActive
? "/project/" + encodeURIComponent(id)
- : "/userProfile/" + encodeURIComponent(id),
+ : "/user-profile/" + encodeURIComponent(id),
};
this.$router.push(route);
}
diff --git a/src/views/GiftedDetailsView.vue b/src/views/GiftedDetailsView.vue
index 211f42ca..05f86a9c 100644
--- a/src/views/GiftedDetailsView.vue
+++ b/src/views/GiftedDetailsView.vue
@@ -826,7 +826,7 @@ export default class GiftedDetails extends Vue {
}
if (!result.success) {
- const errorMessage = this.getGiveCreationErrorMessage(result);
+ const errorMessage = result.error;
logger.error("Error with give creation result:", result);
this.$notify(
{
@@ -899,19 +899,6 @@ export default class GiftedDetails extends Vue {
// Helper functions for readability
- /**
- * @param result direct response eg. ErrorResult or SuccessResult (potentially with embedded "data")
- * @returns best guess at an error message
- */
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- getGiveCreationErrorMessage(result: any) {
- return (
- result.error?.userMessage ||
- result.error?.error ||
- result.response?.data?.error?.message
- );
- }
-
explainData() {
this.$notify(
{
diff --git a/src/views/HelpView.vue b/src/views/HelpView.vue
index 1e972f77..dea5eb27 100644
--- a/src/views/HelpView.vue
+++ b/src/views/HelpView.vue
@@ -24,11 +24,11 @@
- This app focuses on gifts & gratitude, using them to build cool things together with your network.
+ This app focuses on raw gratitude, using it to build cool things together with your network.
- If you'd like to see the page-by-page help,
+ If you'd like to see the page-by-page help again,
What is the idea here?
- We are building networks of people who want to grow good society from the ground up, using modern
- technology that connects people peer-to-peer.
- First of all, let's showcase gratitude: see what people have given, and recognize
- gifts you've seen. This is done in a way that leaves a permanent record -- one that
- came from you, and one that the recipient can prove it was for them. This can be
- personally gratifying, but it extends to broader work: volunteers get
- confirmation of activity, and they can selectively show off their contributions
- and network.
+ We are building networks of people who want to grow good society from the ground up, using
+ modern technology that connects people peer-to-peer.
+ First of all, let's showcase gratitude: see what people have given, and recognize gifts
+ you've seen. This is done in a way that leaves a permanent record -- one that provably
+ came from you, and one that the recipient can prove they were mentioned.
+ This can be personally gratifying, but it extends to broader work: volunteers get
+ confirmation of activity, and they can selectively show off their contributions and
+ network.
+ This is a way to build trust and reputation. It's a way to build a network of people who
+ are willing to help each other.
With this, you highlight giving and you also offer help --
@@ -555,9 +557,6 @@
initiative.
-
What app version is this?
-
{{ package.version }} ({{ commitHash }})
-
I have other questions or feedback, like getting a new profile or removing my data or requesting an improvement.
+ Sorry, your platform of '{{ Capacitor.getPlatform() }}' is not recognized.
+
+
@@ -603,6 +624,7 @@ export default class HelpView extends Vue {
showVerifiable = false;
APP_SERVER = APP_SERVER;
+ Capacitor = Capacitor;
// Ideally, we put no functionality in here, especially in the setup,
// because we never want this page to have a chance of throwing an error.
diff --git a/src/views/HomeView.vue b/src/views/HomeView.vue
index 1a145a1c..8548c6ba 100644
--- a/src/views/HomeView.vue
+++ b/src/views/HomeView.vue
@@ -12,6 +12,7 @@ Raymer * @version 1.0.0 */