From 530c7c1a1323a04720d620527261bd38c418c965 Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Thu, 12 Jun 2025 19:16:02 -0600 Subject: [PATCH 01/64] fix problem with user-profile page, and bump to build 29 & version 0.5.3 --- BUILDING.md | 33 +++++++++++++++------------ android/app/build.gradle | 4 ++-- ios/App/App.xcodeproj/project.pbxproj | 8 +++---- package-lock.json | 4 ++-- package.json | 2 +- src/views/DiscoverView.vue | 2 +- 6 files changed, 29 insertions(+), 24 deletions(-) diff --git a/BUILDING.md b/BUILDING.md index ddc6e6d2..a77228d8 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -321,11 +321,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,12 +334,9 @@ Prerequisites: macOS with Xcode installed export GEM_PATH=$shortened_path ``` - ```bash - cd ios/App - pod install - ``` +1. Check the iOS flag isIOS in CapacitorPlatformService (currently hard-coded for iOS build). -1. Build the web assets: +2. Build the web assets: ```bash rm -rf dist @@ -347,8 +344,15 @@ Prerequisites: macOS with Xcode installed npm run build:capacitor ``` +2. In case dependencies change: + + ```bash + cd ios/App + pod install + cd - + ``` -2. Update iOS project with latest build: +3. Update iOS project with latest build: ```bash npx cap sync ios @@ -356,7 +360,7 @@ Prerequisites: macOS with Xcode installed - If that fails with "Could not find..." then look at the "gem_path" instructions above. -3. Copy the assets: +4. Copy the assets: ```bash # It makes no sense why capacitor-assets will not run without these but it actually changes the contents. @@ -367,15 +371,14 @@ 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 + xcrun agvtool new-version 29 # 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 + cat App.xcodeproj/project.pbxproj | sed "s/MARKETING_VERSION = .*;/MARKETING_VERSION = 0.5.3;/g" > temp && mv temp App.xcodeproj/project.pbxproj cd - ``` @@ -403,6 +406,8 @@ Prerequisites: macOS with Xcode installed * You'll probably have to "Manage" something about encryption, disallowed in France. * Then "Save" and "Add to Review" and "Resubmit to App Review". +8. Revert the iOS flag isIOS in CapacitorPlatformService. + ### Android Build Prerequisites: Android Studio with Java SDK installed @@ -427,7 +432,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: diff --git a/android/app/build.gradle b/android/app/build.gradle index f105fb52..7a1f3671 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 29 + versionName "0.5.3" 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/ios/App/App.xcodeproj/project.pbxproj b/ios/App/App.xcodeproj/project.pbxproj index 17665bc7..85579124 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 = 29; 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 = 0.5.3; 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 = 29; 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 = 0.5.3; PRODUCT_BUNDLE_IDENTIFIER = app.timesafari; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = ""; diff --git a/package-lock.json b/package-lock.json index 32c3ce63..42a31fd4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "timesafari", - "version": "0.4.8", + "version": "0.5.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "timesafari", - "version": "0.4.8", + "version": "0.5.3", "dependencies": { "@capacitor-community/sqlite": "6.0.2", "@capacitor-mlkit/barcode-scanning": "^6.0.0", diff --git a/package.json b/package.json index 19895a0d..6f694aa2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "timesafari", - "version": "0.5.1", + "version": "0.5.3", "description": "Time Safari Application", "author": { "name": "Time Safari Team" diff --git a/src/views/DiscoverView.vue b/src/views/DiscoverView.vue index 3c63a57b..7f6b303e 100644 --- a/src/views/DiscoverView.vue +++ b/src/views/DiscoverView.vue @@ -788,7 +788,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); } From a23416ead19035519dd2057d6a9906d778dbf1d3 Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Thu, 12 Jun 2025 20:10:31 -0600 Subject: [PATCH 02/64] fix optional message at top to not overflow --- BUILDING.md | 8 -------- src/components/TopMessage.vue | 4 ++-- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/BUILDING.md b/BUILDING.md index a77228d8..e44a0c44 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -344,14 +344,6 @@ Prerequisites: macOS with Xcode installed npm run build:capacitor ``` -2. In case dependencies change: - - ```bash - cd ios/App - pod install - cd - - ``` - 3. Update iOS project with latest build: ```bash diff --git a/src/components/TopMessage.vue b/src/components/TopMessage.vue index 49981e4b..c50d9709 100644 --- a/src/components/TopMessage.vue +++ b/src/components/TopMessage.vue @@ -38,14 +38,14 @@ 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; + "You are using prod, user " + didPrefix; } } catch (err: unknown) { this.$notify( From fb81f7b96e56d67e32334fc8a547c31720dddb1b Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Fri, 13 Jun 2025 20:39:12 -0600 Subject: [PATCH 03/64] fix problems with :href links causing the app to reload for DB errors on mobile --- src/components/HiddenDidDialog.vue | 7 +++++-- src/views/ClaimView.vue | 29 ++++++++++++++++++++++------- src/views/ConfirmGiftView.vue | 6 +++--- src/views/ProjectViewView.vue | 18 +++++++++++++----- 4 files changed, 43 insertions(+), 17 deletions(-) diff --git a/src/components/HiddenDidDialog.vue b/src/components/HiddenDidDialog.vue index a94c34ae..980a3852 100644 --- a/src/components/HiddenDidDialog.vue +++ b/src/components/HiddenDidDialog.vue @@ -48,12 +48,15 @@ {{ didInfo(visDid) }} - + - + diff --git a/src/views/ClaimView.vue b/src/views/ClaimView.vue index 602373a4..8c0ac55a 100644 --- a/src/views/ClaimView.vue +++ b/src/views/ClaimView.vue @@ -292,12 +292,17 @@
{{ didInfo(confirmerId) }} - + - +
@@ -329,12 +334,17 @@
{{ didInfo(confsVisibleTo) }} - + - +
@@ -443,12 +453,17 @@ {{ didInfo(visDid) }} - + - + , found at  diff --git a/src/views/ProjectViewView.vue b/src/views/ProjectViewView.vue index 0aaacc98..bd7b9f79 100644 --- a/src/views/ProjectViewView.vue +++ b/src/views/ProjectViewView.vue @@ -52,16 +52,24 @@ icon="user" class="fa-fw text-slate-400" > - {{ issuerInfoObject?.displayName }} - - + + {{ issuerInfoObject?.displayName }} + + + - + - + Date: Sat, 14 Jun 2025 03:31:12 +0000 Subject: [PATCH 04/64] feat: add conditional rendering for claim certificate link and update gitignore - Add v-if directive to show claim certificate link only when veriClaim.id exists - Update .gitignore to exclude android app resource directory - Prevents broken links when claim data is not fully loaded - Improves build process by ignoring generated Android resources This change ensures the certificate link is only displayed when there's valid claim data available, preventing navigation errors and improving user experience. The gitignore update helps keep the repository clean by excluding Android-specific generated files. --- .gitignore | 1 + src/views/ClaimView.vue | 1 + 2 files changed, 2 insertions(+) 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/src/views/ClaimView.vue b/src/views/ClaimView.vue index 8c0ac55a..1d647d7a 100644 --- a/src/views/ClaimView.vue +++ b/src/views/ClaimView.vue @@ -46,6 +46,7 @@
Date: Fri, 13 Jun 2025 21:58:57 -0600 Subject: [PATCH 05/64] fix some result types and refactor types themselves --- src/components/GiftedDialog.vue | 15 +-------------- src/components/OfferDialog.vue | 17 +---------------- src/interfaces/claims-result.ts | 11 ----------- src/interfaces/common.ts | 9 --------- src/interfaces/index.ts | 6 +----- src/libs/endorserServer.ts | 2 +- src/views/ClaimAddRawView.vue | 2 +- src/views/ConfirmGiftView.vue | 2 +- src/views/GiftedDetailsView.vue | 15 +-------------- src/views/HomeView.vue | 2 +- src/views/ProjectViewView.vue | 2 +- src/views/QuickActionBvcEndView.vue | 14 +++++++------- 12 files changed, 16 insertions(+), 81 deletions(-) diff --git a/src/components/GiftedDialog.vue b/src/components/GiftedDialog.vue index 754c9bf6..c6eb2064 100644 --- a/src/components/GiftedDialog.vue +++ b/src/components/GiftedDialog.vue @@ -321,7 +321,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( { @@ -367,19 +367,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/OfferDialog.vue b/src/components/OfferDialog.vue index 659488f1..1ca6dc2b 100644 --- a/src/components/OfferDialog.vue +++ b/src/components/OfferDialog.vue @@ -250,7 +250,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 +290,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/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/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..cbdcbaee 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), 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/ConfirmGiftView.vue b/src/views/ConfirmGiftView.vue index e58882ac..63225259 100644 --- a/src/views/ConfirmGiftView.vue +++ b/src/views/ConfirmGiftView.vue @@ -831,7 +831,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/GiftedDetailsView.vue b/src/views/GiftedDetailsView.vue index 70f16e89..277546bc 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/HomeView.vue b/src/views/HomeView.vue index 24ce8255..70a569a2 100644 --- a/src/views/HomeView.vue +++ b/src/views/HomeView.vue @@ -1843,7 +1843,7 @@ export default class HomeView extends Vue { this.axios, ); - if (result.type === "success") { + if (result.success) { this.$notify( { group: "alert", diff --git a/src/views/ProjectViewView.vue b/src/views/ProjectViewView.vue index bd7b9f79..bcbcf666 100644 --- a/src/views/ProjectViewView.vue +++ b/src/views/ProjectViewView.vue @@ -1433,7 +1433,7 @@ export default class ProjectViewView extends Vue { this.apiServer, this.axios, ); - if (result.type === "success") { + if (result.success) { this.$notify( { group: "alert", diff --git a/src/views/QuickActionBvcEndView.vue b/src/views/QuickActionBvcEndView.vue index 18e96bcd..2a5330b3 100644 --- a/src/views/QuickActionBvcEndView.vue +++ b/src/views/QuickActionBvcEndView.vue @@ -155,7 +155,7 @@ import { Contact } from "../db/tables/contacts"; import { GenericCredWrapper, GenericVerifiableCredential, - ErrorResult, + CreateAndSubmitClaimResult, } from "../interfaces"; import { BVC_MEETUPS_PROJECT_CLAIM_ID, @@ -298,13 +298,13 @@ export default class QuickActionBvcBeginView extends Vue { } // in parallel, make a confirmation for each selected claim and send them all to the server - const confirmResults = await Promise.allSettled( + const confirmResults: PromiseSettledResult[] = await Promise.allSettled( this.claimsToConfirmSelected.map(async (jwtId) => { const record = this.claimsToConfirm.find( (claim) => claim.id === jwtId, ); if (!record) { - return { type: "error", error: "Record not found." }; + return { success: false, error: "Record not found." }; } return createAndSubmitConfirmation( this.activeDid, @@ -318,8 +318,8 @@ export default class QuickActionBvcBeginView extends Vue { ); // check for any rejected confirmations const confirmsSucceeded = confirmResults.filter( - (result) => - result.status === "fulfilled" && result.value.type === "success", + // 'fulfilled' is the status in a successful PromiseFulfilledResult + (result) => result.status === "fulfilled" && result.value.success, ); if (confirmsSucceeded.length < this.claimsToConfirmSelected.length) { logger.error("Error sending confirmations:", confirmResults); @@ -353,7 +353,7 @@ export default class QuickActionBvcBeginView extends Vue { undefined, BVC_MEETUPS_PROJECT_CLAIM_ID, ); - giveSucceeded = giveResult.type === "success"; + giveSucceeded = giveResult.success; if (!giveSucceeded) { logger.error("Error sending give:", giveResult); this.$notify( @@ -362,7 +362,7 @@ export default class QuickActionBvcBeginView extends Vue { type: "danger", title: "Error", text: - (giveResult as ErrorResult)?.error?.userMessage || + (giveResult as CreateAndSubmitClaimResult)?.error || "There was an error sending that give.", }, 5000, From 676a3013319830a787004478201ddfa6c8bab4cb Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Fri, 13 Jun 2025 22:36:28 -0600 Subject: [PATCH 06/64] bump to build 30 version 0.5.4 --- BUILDING.md | 4 ++-- android/app/build.gradle | 4 ++-- ios/App/App.xcodeproj/project.pbxproj | 8 ++++---- package.json | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/BUILDING.md b/BUILDING.md index e44a0c44..9384a669 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -367,10 +367,10 @@ Prerequisites: macOS with Xcode installed ``` cd ios/App - xcrun agvtool new-version 29 + xcrun agvtool new-version 30 # 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.3;/g" > temp && mv temp App.xcodeproj/project.pbxproj + cat App.xcodeproj/project.pbxproj | sed "s/MARKETING_VERSION = .*;/MARKETING_VERSION = 0.5.4;/g" > temp && mv temp App.xcodeproj/project.pbxproj cd - ``` diff --git a/android/app/build.gradle b/android/app/build.gradle index 7a1f3671..aa23a0b8 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 29 - versionName "0.5.3" + versionCode 30 + versionName "0.5.4" 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/ios/App/App.xcodeproj/project.pbxproj b/ios/App/App.xcodeproj/project.pbxproj index 85579124..5ba178bd 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 = 29; + CURRENT_PROJECT_VERSION = 30; DEVELOPMENT_TEAM = GM3FS5JQPH; ENABLE_APP_SANDBOX = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO; @@ -413,7 +413,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 0.5.3; + MARKETING_VERSION = 0.5.4; 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 = 29; + CURRENT_PROJECT_VERSION = 30; DEVELOPMENT_TEAM = GM3FS5JQPH; ENABLE_APP_SANDBOX = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO; @@ -440,7 +440,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 0.5.3; + MARKETING_VERSION = 0.5.4; PRODUCT_BUNDLE_IDENTIFIER = app.timesafari; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = ""; diff --git a/package.json b/package.json index 6f694aa2..5b01d39c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "timesafari", - "version": "0.5.3", + "version": "0.5.4", "description": "Time Safari Application", "author": { "name": "Time Safari Team" From cead308800cec34b59afb1e5dd8b39b0ea9c352f Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Fri, 13 Jun 2025 22:37:03 -0600 Subject: [PATCH 07/64] incorporate one of the BUILDING steps directly into the file --- BUILDING.md | 14 -------------- android/app/src/main/AndroidManifest.xml | 2 +- 2 files changed, 1 insertion(+), 15 deletions(-) diff --git a/BUILDING.md b/BUILDING.md index 9384a669..773d5279 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -473,17 +473,3 @@ At play.google.com/console: - Save, go to "Publishing Overview" as prompted, and click "Send changes for review". - 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 - -You must add the following intent filter to the `android/app/src/main/AndroidManifest.xml` file: - - ```xml - - - - - - - ``` \ No newline at end of file diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 991dd37e..b7cea5c4 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -19,7 +19,7 @@ - + From 0d152adbf2816281bd19fbaac6ac1eef887c6803 Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Sat, 14 Jun 2025 22:06:12 -0600 Subject: [PATCH 08/64] remove the deep-link autoVerify because it caused a build failure --- BUILDING.md | 16 ++++++++++++++++ android/app/src/main/AndroidManifest.xml | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/BUILDING.md b/BUILDING.md index 773d5279..6c79573f 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -473,3 +473,19 @@ At play.google.com/console: - Save, go to "Publishing Overview" as prompted, and click "Send changes for review". - 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. + + +## Android Configuration for deep links + +You must add the following intent filter to the `android/app/src/main/AndroidManifest.xml` file: + + ```xml + + + + + + + ``` + +... 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/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index b7cea5c4..991dd37e 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -19,7 +19,7 @@ - + From 9f0fed0a60bfbb5b1d91e22f6c7aca0515290743 Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Sat, 14 Jun 2025 22:10:49 -0600 Subject: [PATCH 09/64] update ios check to work, and add links to app stores --- .../platforms/CapacitorPlatformService.ts | 3 +- src/views/HelpView.vue | 30 +++++++++++++++---- 2 files changed, 27 insertions(+), 6 deletions(-) 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/views/HelpView.vue b/src/views/HelpView.vue index 1e972f77..c4253f08 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 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.

@@ -567,6 +564,28 @@ >info@TimeSafari.app

+ +

What app version is this?

+

{{ package.version }} ({{ commitHash }})

+ +
+

+ Do I have the latest version? +

+

+ + Check the App Store. + +

+

+ + Download the latest APK to see. + +

+

+ Sorry, your platform of '{{ Capacitor.getPlatform() }}' is not recognized. +

+
@@ -603,6 +622,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. From 54dca9e745895d87b92b8a8b5815bd33ab089425 Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Sun, 15 Jun 2025 11:02:16 -0600 Subject: [PATCH 10/64] fix project deep-link (and reorder alphabetically) --- src/services/deepLinks.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/services/deepLinks.ts b/src/services/deepLinks.ts index 550353f7..acde506e 100644 --- a/src/services/deepLinks.ts +++ b/src/services/deepLinks.ts @@ -81,18 +81,18 @@ 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" }, - claim: { name: "claim" }, - "claim-cert": { name: "claim-cert" }, + "claim": { name: "claim" }, "claim-add-raw": { name: "claim-add-raw" }, + "claim-cert": { name: "claim-cert" }, + "confirm-gift": { name: "confirm-gift" }, + "contacts": { name: "contacts" }, "contact-edit": { name: "contact-edit", paramKey: "did" }, - contacts: { name: "contacts" }, - did: { name: "did", paramKey: "did" }, + "contact-import": { name: "contact-import" }, + "did": { name: "did", paramKey: "did" }, + "invite-one-accept": { name: "invite-one-accept" }, + "onboard-meeting-setup": { name: "onboard-meeting-setup" }, + "project": { name: "project" }, + "user-profile": { name: "user-profile" }, }; /** From e240c2940a79a9e823c11b263e1e9a81325be0a1 Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Sun, 15 Jun 2025 13:54:12 -0600 Subject: [PATCH 11/64] remove unused deep links and add another --- src/services/deepLinks.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/services/deepLinks.ts b/src/services/deepLinks.ts index acde506e..1cd1cf69 100644 --- a/src/services/deepLinks.ts +++ b/src/services/deepLinks.ts @@ -85,11 +85,9 @@ export class DeepLinkHandler { "claim-add-raw": { name: "claim-add-raw" }, "claim-cert": { name: "claim-cert" }, "confirm-gift": { name: "confirm-gift" }, - "contacts": { name: "contacts" }, - "contact-edit": { name: "contact-edit", paramKey: "did" }, - "contact-import": { name: "contact-import" }, "did": { name: "did", paramKey: "did" }, "invite-one-accept": { name: "invite-one-accept" }, + "onboard-meeting-members": { name: "onboard-meeting-members" }, "onboard-meeting-setup": { name: "onboard-meeting-setup" }, "project": { name: "project" }, "user-profile": { name: "user-profile" }, From 048dded27822a9173d48f1bc050a5a4d2292953d Mon Sep 17 00:00:00 2001 From: Matthew Raymer Date: Mon, 16 Jun 2025 05:48:13 +0000 Subject: [PATCH 12/64] fix: resolve deep link route mismatch for project links - Fix schema validation mismatch between "project-details" and "project" - Update VALID_DEEP_LINK_ROUTES to include "project" instead of "project-details" - Update deepLinkSchemas to use "project" route name - Update documentation to reflect correct route name - Resolves "Invalid route path: project" errors in deep link handling The deep link timesafari://project/01JWH0YAB3MAGBD751VAJAXQ17 now works correctly and routes to the ProjectViewView component as expected. Fixes: Deep link validation errors for project routes --- package-lock.json | 577 ++++++++++++++++++++++-------------- src/interfaces/deepLinks.ts | 4 +- src/services/deepLinks.ts | 2 +- 3 files changed, 364 insertions(+), 219 deletions(-) diff --git a/package-lock.json b/package-lock.json index 42a31fd4..a3e24731 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "timesafari", - "version": "0.5.3", + "version": "0.5.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "timesafari", - "version": "0.5.3", + "version": "0.5.4", "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": { @@ -6686,9 +6686,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 +6983,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", @@ -7839,9 +7862,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, @@ -7947,20 +7970,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,25 +7998,97 @@ } } }, + "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", @@ -8078,9 +8173,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 +8184,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, @@ -8108,9 +8203,9 @@ "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, @@ -8217,9 +8312,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.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.43.0.tgz", + "integrity": "sha512-Krjy9awJl6rKbruhQDgivNbD1WuLb8xAclM4IR4cN5pHGAs2oIMMQJEiC3IC/9TZJ+QZkmZhlMO/6MBGxPidpw==", "cpu": [ "arm" ], @@ -8231,9 +8326,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.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.43.0.tgz", + "integrity": "sha512-ss4YJwRt5I63454Rpj+mXCXicakdFmKnUNxr1dLK+5rv5FJgAxnN7s31a5VchRYxCFWdmnDWKd0wbAdTr0J5EA==", "cpu": [ "arm64" ], @@ -8271,9 +8366,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.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.43.0.tgz", + "integrity": "sha512-SV+U5sSo0yujrjzBF7/YidieK2iF6E7MdF6EbYxNz94lA+R0wKl3SiixGyG/9Klab6uNBIqsN7j4Y/Fya7wAjQ==", "cpu": [ "arm64" ], @@ -8285,9 +8380,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.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.43.0.tgz", + "integrity": "sha512-J7uCsiV13L/VOeHJBo5SjasKiGxJ0g+nQTrBkAsmQBIdil3KhPnSE9GnRon4ejX1XDdsmK/l30IYLiAaQEO0Cg==", "cpu": [ "x64" ], @@ -8299,9 +8394,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.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.43.0.tgz", + "integrity": "sha512-gTJ/JnnjCMc15uwB10TTATBEhK9meBIY+gXP4s0sHD1zHOaIh4Dmy1X9wup18IiY9tTNk5gJc4yx9ctj/fjrIw==", "cpu": [ "arm" ], @@ -8313,9 +8408,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.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.43.0.tgz", + "integrity": "sha512-ZJ3gZynL1LDSIvRfz0qXtTNs56n5DI2Mq+WACWZ7yGHFUEirHBRt7fyIk0NsCKhmRhn7WAcjgSkSVVxKlPNFFw==", "cpu": [ "arm" ], @@ -8353,9 +8448,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.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.43.0.tgz", + "integrity": "sha512-HPGDIH0/ZzAZjvtlXj6g+KDQ9ZMHfSP553za7o2Odegb/BEfwJcR0Sw0RLNpQ9nC6Gy8s+3mSS9xjZ0n3rhcYg==", "cpu": [ "loong64" ], @@ -8367,9 +8462,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.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.43.0.tgz", + "integrity": "sha512-gEmwbOws4U4GLAJDhhtSPWPXUzDfMRedT3hFMyRAvM9Mrnj+dJIFIeL7otsv2WF3D7GrV0GIewW0y28dOYWkmw==", "cpu": [ "ppc64" ], @@ -8381,9 +8476,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.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.43.0.tgz", + "integrity": "sha512-XXKvo2e+wFtXZF/9xoWohHg+MuRnvO29TI5Hqe9xwN5uN8NKUYy7tXUG3EZAlfchufNCTHNGjEx7uN78KsBo0g==", "cpu": [ "riscv64" ], @@ -8395,9 +8490,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.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.43.0.tgz", + "integrity": "sha512-ruf3hPWhjw6uDFsOAzmbNIvlXFXlBQ4nk57Sec8E8rUxs/AI4HD6xmiiasOOx/3QxS2f5eQMKTAwk7KHwpzr/Q==", "cpu": [ "riscv64" ], @@ -8409,9 +8504,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.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.43.0.tgz", + "integrity": "sha512-QmNIAqDiEMEvFV15rsSnjoSmO0+eJLoKRD9EAa9rrYNwO/XRCtOGM3A5A0X+wmG+XRrw9Fxdsw+LnyYiZWWcVw==", "cpu": [ "s390x" ], @@ -8462,9 +8557,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.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.43.0.tgz", + "integrity": "sha512-fYCTEyzf8d+7diCw8b+asvWDCLMjsCEA8alvtAutqJOJp/wL5hs1rWSqJ1vkjgW0L2NB4bsYJrpKkiIPRR9dvw==", "cpu": [ "ia32" ], @@ -8874,9 +8969,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.0", + "resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.35.0.tgz", + "integrity": "sha512-x0IFtj7IJStK+ZqIkhReWbiC0UMjMJnNXV8OXG+DCLDExZaVaxL3MLuq6BJBBcQ1MHZduTHDv3Iz0Zshoj3zjQ==", "license": "MIT", "bin": { "stencil": "bin/stencil" @@ -11184,13 +11279,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 +11707,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", @@ -12199,9 +12294,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 +12743,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 +12963,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 +14602,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 +15026,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 +15442,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.167", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.167.tgz", + "integrity": "sha512-LxcRvnYO5ez2bMOFpbuuVuAI5QNeY1ncVytE/KXaL6ZNfzX1yPlAO0nSOyIHx2fVAuUprMqPs/TdVhUFZy7SIQ==", "devOptional": true, "license": "ISC" }, @@ -15865,9 +15960,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 +16174,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 +16273,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", @@ -17617,9 +17712,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": { @@ -19127,9 +19222,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": { @@ -24054,9 +24149,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.5", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.5.tgz", + "integrity": "sha512-d/jtm+rdNT8tpXuHY5MMtcbJFBkhXE6593XVR9UoGCH8jSFGci7jGvMGH5RYd5PBJW+00NZQt6gf7CbagJCrhg==", "funding": [ { "type": "opencollective", @@ -24954,44 +25049,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 +25099,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 +25133,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 +25184,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 +25659,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 +26105,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 +26155,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,9 +26239,9 @@ } }, "node_modules/rollup": { - "version": "4.42.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.42.0.tgz", - "integrity": "sha512-LW+Vse3BJPyGJGAJt1j8pWDKPd73QM8cRXYK1IxOBgL2AGLu7Xd2YOW0M2sLUBCkF5MshXXtMApyEAEzMVMsnw==", + "version": "4.43.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.43.0.tgz", + "integrity": "sha512-wdN2Kd3Twh8MAEOEJZsuxuLKCsBEo4PVNLK6tQWAn10VhsVewQLzcucMgLolRlhFybGxfclbPeEYBaP6RvUFGg==", "dev": true, "license": "MIT", "dependencies": { @@ -26110,33 +26255,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.43.0", + "@rollup/rollup-android-arm64": "4.43.0", + "@rollup/rollup-darwin-arm64": "4.43.0", + "@rollup/rollup-darwin-x64": "4.43.0", + "@rollup/rollup-freebsd-arm64": "4.43.0", + "@rollup/rollup-freebsd-x64": "4.43.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.43.0", + "@rollup/rollup-linux-arm-musleabihf": "4.43.0", + "@rollup/rollup-linux-arm64-gnu": "4.43.0", + "@rollup/rollup-linux-arm64-musl": "4.43.0", + "@rollup/rollup-linux-loongarch64-gnu": "4.43.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.43.0", + "@rollup/rollup-linux-riscv64-gnu": "4.43.0", + "@rollup/rollup-linux-riscv64-musl": "4.43.0", + "@rollup/rollup-linux-s390x-gnu": "4.43.0", + "@rollup/rollup-linux-x64-gnu": "4.43.0", + "@rollup/rollup-linux-x64-musl": "4.43.0", + "@rollup/rollup-win32-arm64-msvc": "4.43.0", + "@rollup/rollup-win32-ia32-msvc": "4.43.0", + "@rollup/rollup-win32-x64-msvc": "4.43.0", "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.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.43.0.tgz", + "integrity": "sha512-eKoL8ykZ7zz8MjgBenEF2OoTNFAPFz1/lyJ5UmmFSz5jW+7XbH1+MAgCVHy72aG59rbuQLcJeiMrP8qP5d/N0A==", "cpu": [ "arm64" ], @@ -26148,9 +26293,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.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.43.0.tgz", + "integrity": "sha512-SYwXJgaBYW33Wi/q4ubN+ldWC4DzQY62S4Ll2dgfr/dbPoF50dlQwEaEHSKrQdSjC6oIe1WgzosoaNoHCdNuMg==", "cpu": [ "x64" ], @@ -26162,9 +26307,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.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.43.0.tgz", + "integrity": "sha512-8FnkipasmOOSSlfucGYEu58U8cxEdhziKjPD2FIa0ONVMxvl/hmONtX/7y4vGjdUhjcTHlKlDhw3H9t98fPvyA==", "cpu": [ "arm64" ], @@ -26176,9 +26321,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.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.43.0.tgz", + "integrity": "sha512-KPPyAdlcIZ6S9C3S2cndXDkV0Bb1OSMsX0Eelr2Bay4EsF9yi9u9uzc9RniK3mcUGCLhWY9oLr6er80P5DE6XA==", "cpu": [ "arm64" ], @@ -26190,9 +26335,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.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.43.0.tgz", + "integrity": "sha512-jAHr/S0iiBtFyzjhOkAics/2SrXE092qyqEg96e90L3t9Op8OTzS6+IX0Fy5wCt2+KqeHAkti+eitV0wvblEoQ==", "cpu": [ "x64" ], @@ -26204,9 +26349,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.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.43.0.tgz", + "integrity": "sha512-3yATWgdeXyuHtBhrLt98w+5fKurdqvs8B53LaoKD7P7H7FKOONLsBVMNl9ghPQZQuYcceV5CDyPfyfGpMWD9mQ==", "cpu": [ "x64" ], @@ -26218,9 +26363,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.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.43.0.tgz", + "integrity": "sha512-wVzXp2qDSCOpcBCT5WRWLmpJRIzv23valvcTwMHEobkjippNf+C3ys/+wf07poPkeNix0paTNemB2XrHr2TnGw==", "cpu": [ "arm64" ], @@ -26232,9 +26377,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.43.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.43.0.tgz", + "integrity": "sha512-SnGhLiE5rlK0ofq8kzuDkM0g7FN1s5VYY+YSMTibP7CqShxCQvqtNxTARS4xX4PFJfHjG0ZQYX9iGzI3FQh5Aw==", "cpu": [ "x64" ], @@ -26410,9 +26555,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 +26569,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": { @@ -28495,9 +28640,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, @@ -31045,9 +31190,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.64", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.64.tgz", + "integrity": "sha512-hbP9FpSZf7pkS7hRVUrOjhwKJNyampPgtXKc3AN6DsWtoHsg2Sb4SQaS4Tcay380zSwd2VPo9G9180emBACp5g==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/src/interfaces/deepLinks.ts b/src/interfaces/deepLinks.ts index 1e06aab1..3f6a1d03 100644 --- a/src/interfaces/deepLinks.ts +++ b/src/interfaces/deepLinks.ts @@ -30,7 +30,7 @@ import { z } from "zod"; // Add a union type of all valid route paths export const VALID_DEEP_LINK_ROUTES = [ "user-profile", - "project-details", + "project", "onboard-meeting-setup", "invite-one-accept", "contact-import", @@ -61,7 +61,7 @@ export const deepLinkSchemas = { "user-profile": z.object({ id: z.string(), }), - "project-details": z.object({ + "project": z.object({ id: z.string(), }), "onboard-meeting-setup": z.object({ diff --git a/src/services/deepLinks.ts b/src/services/deepLinks.ts index 1cd1cf69..04ff1a44 100644 --- a/src/services/deepLinks.ts +++ b/src/services/deepLinks.ts @@ -28,7 +28,7 @@ * * Supported Routes: * - user-profile: View user profile - * - project-details: View project details + * - project: View project details * - onboard-meeting-setup: Setup onboarding meeting * - invite-one-accept: Accept invitation * - contact-import: Import contacts From bb2a4ab76e86b883a422a3339753a9544f746c96 Mon Sep 17 00:00:00 2001 From: Jose Olarte III Date: Mon, 16 Jun 2025 16:09:08 +0800 Subject: [PATCH 13/64] URL scheme config for iOS - Registers the timesafari:// URL scheme - Sets the bundle URL name to app.timesafari --- ios/App/App/Info.plist | 11 +++++++++++ 1 file changed, 11 insertions(+) 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 + + + From 60de8cee6208e88eba8b0f18b8d105411890fc6c Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Mon, 16 Jun 2025 07:24:37 -0600 Subject: [PATCH 14/64] reword some of the help-page introduction (no code changes) --- src/views/HelpView.vue | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/views/HelpView.vue b/src/views/HelpView.vue index c4253f08..dea5eb27 100644 --- a/src/views/HelpView.vue +++ b/src/views/HelpView.vue @@ -37,14 +37,16 @@

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 -- From 4a43bc9c6cbfd79277c9d7b9f0ecfc8fd73936ec Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Mon, 16 Jun 2025 07:38:16 -0600 Subject: [PATCH 15/64] bump build to 31 and version to 0.5.5 --- BUILDING.md | 12 ++++-------- android/app/build.gradle | 4 ++-- ios/App/App.xcodeproj/project.pbxproj | 8 ++++---- package.json | 2 +- 4 files changed, 11 insertions(+), 15 deletions(-) diff --git a/BUILDING.md b/BUILDING.md index 6c79573f..4d7ca466 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -334,9 +334,7 @@ Prerequisites: macOS with Xcode installed export GEM_PATH=$shortened_path ``` -1. Check the iOS flag isIOS in CapacitorPlatformService (currently hard-coded for iOS build). - -2. Build the web assets: +1. Build the web assets: ```bash rm -rf dist @@ -344,7 +342,7 @@ Prerequisites: macOS with Xcode installed npm run build:capacitor ``` -3. Update iOS project with latest build: +2. Update iOS project with latest build: ```bash npx cap sync ios @@ -352,7 +350,7 @@ Prerequisites: macOS with Xcode installed - If that fails with "Could not find..." then look at the "gem_path" instructions above. -4. Copy the assets: +3. Copy the assets: ```bash # It makes no sense why capacitor-assets will not run without these but it actually changes the contents. @@ -370,7 +368,7 @@ Prerequisites: macOS with Xcode installed xcrun agvtool new-version 30 # 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.4;/g" > temp && mv temp App.xcodeproj/project.pbxproj + cat App.xcodeproj/project.pbxproj | sed "s/MARKETING_VERSION = .*;/MARKETING_VERSION = 0.5.5;/g" > temp && mv temp App.xcodeproj/project.pbxproj cd - ``` @@ -398,8 +396,6 @@ Prerequisites: macOS with Xcode installed * You'll probably have to "Manage" something about encryption, disallowed in France. * Then "Save" and "Add to Review" and "Resubmit to App Review". -8. Revert the iOS flag isIOS in CapacitorPlatformService. - ### Android Build Prerequisites: Android Studio with Java SDK installed diff --git a/android/app/build.gradle b/android/app/build.gradle index aa23a0b8..9cf402ac 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 30 - versionName "0.5.4" + versionCode 31 + versionName "0.5.5" 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/ios/App/App.xcodeproj/project.pbxproj b/ios/App/App.xcodeproj/project.pbxproj index 5ba178bd..a9eeeaa5 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 = 30; + CURRENT_PROJECT_VERSION = 31; DEVELOPMENT_TEAM = GM3FS5JQPH; ENABLE_APP_SANDBOX = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO; @@ -413,7 +413,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 0.5.4; + MARKETING_VERSION = 0.5.5; 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 = 30; + CURRENT_PROJECT_VERSION = 31; DEVELOPMENT_TEAM = GM3FS5JQPH; ENABLE_APP_SANDBOX = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO; @@ -440,7 +440,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 0.5.4; + MARKETING_VERSION = 0.5.5; PRODUCT_BUNDLE_IDENTIFIER = app.timesafari; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = ""; diff --git a/package.json b/package.json index 5b01d39c..bf339b6c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "timesafari", - "version": "0.5.4", + "version": "0.5.5", "description": "Time Safari Application", "author": { "name": "Time Safari Team" From 5e851e442f11fba239941d66f41687fedd1a8506 Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Mon, 16 Jun 2025 15:38:11 -0600 Subject: [PATCH 16/64] shrink the contents of the QR code so people can scan it --- src/components/OfferDialog.vue | 5 +- src/components/TopMessage.vue | 3 +- src/interfaces/deepLinks.ts | 2 +- src/libs/util.ts | 65 ++++++++++++++ src/services/deepLinks.ts | 6 +- src/views/ContactQRScanFullView.vue | 126 ++++++++++++++++------------ src/views/ContactQRScanShowView.vue | 119 +++++++++++++++----------- src/views/ContactsView.vue | 45 +--------- src/views/QuickActionBvcEndView.vue | 37 ++++---- 9 files changed, 233 insertions(+), 175 deletions(-) diff --git a/src/components/OfferDialog.vue b/src/components/OfferDialog.vue index 1ca6dc2b..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"; diff --git a/src/components/TopMessage.vue b/src/components/TopMessage.vue index c50d9709..54b6a01b 100644 --- a/src/components/TopMessage.vue +++ b/src/components/TopMessage.vue @@ -44,8 +44,7 @@ export default class TopMessage extends Vue { settings.apiServer === AppString.PROD_ENDORSER_API_SERVER ) { const didPrefix = settings.activeDid?.slice(11, 15); - this.message = - "You are using prod, user " + didPrefix; + this.message = "You are using prod, user " + didPrefix; } } catch (err: unknown) { this.$notify( diff --git a/src/interfaces/deepLinks.ts b/src/interfaces/deepLinks.ts index 3f6a1d03..b56862c1 100644 --- a/src/interfaces/deepLinks.ts +++ b/src/interfaces/deepLinks.ts @@ -61,7 +61,7 @@ export const deepLinkSchemas = { "user-profile": z.object({ id: z.string(), }), - "project": z.object({ + project: z.object({ id: z.string(), }), "onboard-meeting-setup": z.object({ diff --git a/src/libs/util.ts b/src/libs/util.ts index 3bc86866..03ee7637 100644 --- a/src/libs/util.ts +++ b/src/libs/util.ts @@ -882,6 +882,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 */ diff --git a/src/services/deepLinks.ts b/src/services/deepLinks.ts index 04ff1a44..149665fc 100644 --- a/src/services/deepLinks.ts +++ b/src/services/deepLinks.ts @@ -81,15 +81,15 @@ export class DeepLinkHandler { string, { name: string; paramKey?: string } > = { - "claim": { name: "claim" }, + claim: { name: "claim" }, "claim-add-raw": { name: "claim-add-raw" }, "claim-cert": { name: "claim-cert" }, "confirm-gift": { name: "confirm-gift" }, - "did": { name: "did", paramKey: "did" }, + did: { name: "did", paramKey: "did" }, "invite-one-accept": { name: "invite-one-accept" }, "onboard-meeting-members": { name: "onboard-meeting-members" }, "onboard-meeting-setup": { name: "onboard-meeting-setup" }, - "project": { name: "project" }, + project: { name: "project" }, "user-profile": { name: "user-profile" }, }; diff --git a/src/views/ContactQRScanFullView.vue b/src/views/ContactQRScanFullView.vue index 64f58829..49366fbd 100644 --- a/src/views/ContactQRScanFullView.vue +++ b/src/views/ContactQRScanFullView.vue @@ -104,6 +104,7 @@ From 3fd6c2b80dbd7482dd922cc151566dfd8af9fd74 Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Wed, 18 Jun 2025 13:16:17 -0600 Subject: [PATCH 22/64] add first cut at deep-link redirecting, with one example contact-import that works on mobile --- doc/DEEP_LINKS.md | 1 + src/db/databaseUtil.ts | 4 +- src/interfaces/deepLinks.ts | 52 +++---- src/main.capacitor.ts | 7 +- src/router/index.ts | 5 + src/services/api.ts | 5 +- src/services/deepLinks.ts | 39 +++-- src/utils/logger.ts | 7 +- src/views/ContactsView.vue | 3 +- src/views/DeepLinkErrorView.vue | 11 +- src/views/DeepLinkRedirectView.vue | 221 +++++++++++++++++++++++++++++ src/views/HomeView.vue | 15 -- 12 files changed, 297 insertions(+), 73 deletions(-) create mode 100644 src/views/DeepLinkRedirectView.vue 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/src/db/databaseUtil.ts b/src/db/databaseUtil.ts index c8688b83..1d9ab4bc 100644 --- a/src/db/databaseUtil.ts +++ b/src/db/databaseUtil.ts @@ -219,9 +219,9 @@ 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); } diff --git a/src/interfaces/deepLinks.ts b/src/interfaces/deepLinks.ts index b56862c1..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", - "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: 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/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/router/index.ts b/src/router/index.ts index 0b9aa52b..fabce1b5 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", 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 e9f63a88..93fe9aa3 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: 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); @@ -54,6 +52,7 @@ import { } from "../interfaces/deepLinks"; import { logConsoleAndDb } from "../db/databaseUtil"; import type { DeepLinkError } from "../interfaces/deepLinks"; +import { logger } from "@/utils/logger"; /** * Handles processing and routing of deep links in the application. @@ -81,14 +80,15 @@ export class DeepLinkHandler { string, { name: string; paramKey?: string } > = { + // note that similar lists are in src/interfaces/deepLinks.ts claim: { name: "claim" }, "claim-add-raw": { name: "claim-add-raw" }, "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" }, - "onboard-meeting-setup": { name: "onboard-meeting-setup" }, project: { name: "project" }, "user-profile": { name: "user-profile" }, }; @@ -99,7 +99,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("://"); @@ -115,7 +115,16 @@ export class DeepLinkHandler { }); const [path, queryString] = parts[1].split("?"); - const [routePath, param] = path.split("/"); + const [routePath, ...pathParams] = path.split("/"); + // logger.log( + // "[DeepLink] Debug:", + // "Route Path:", + // routePath, + // "Path Params:", + // pathParams, + // "Query String:", + // queryString, + // ); // Validate route exists before proceeding if (!this.ROUTE_MAP[routePath]) { @@ -134,10 +143,10 @@ 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 }; } @@ -243,6 +252,8 @@ export class DeepLinkHandler { code: "INVALID_PARAMETERS", message: (error as Error).message, details: error, + params: params, + query: query, }; } } diff --git a/src/utils/logger.ts b/src/utils/logger.ts index 2cbb228b..89425d77 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) => { @@ -67,8 +67,9 @@ export const logger = { // 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/ContactsView.vue b/src/views/ContactsView.vue index cdbadb00..02f8c6a4 100644 --- a/src/views/ContactsView.vue +++ b/src/views/ContactsView.vue @@ -1397,7 +1397,8 @@ export default class ContactsView extends Vue { const contactsJwt = await createEndorserJwtForDid(this.activeDid, { contacts: selectedContacts, }); - const contactsJwtUrl = APP_SERVER + "/contact-import/" + contactsJwt; + const contactsJwtUrl = + APP_SERVER + "/deep-link/contact-import/" + contactsJwt; useClipboard() .copy(contactsJwtUrl) .then(() => { diff --git a/src/views/DeepLinkErrorView.vue b/src/views/DeepLinkErrorView.vue index 406f0b5c..e65d3b58 100644 --- a/src/views/DeepLinkErrorView.vue +++ b/src/views/DeepLinkErrorView.vue @@ -66,9 +66,14 @@ const formattedPath = computed(() => { const path = originalPath.value.replace(/^\/+/, ""); // Log for debugging - logger.log("Original Path:", originalPath.value); - logger.log("Route Params:", route.params); - logger.log("Route Query:", route.query); + logger.log( + "[DeepLinkError] Original Path:", + originalPath.value, + "Route Params:", + route.params, + "Route Query:", + route.query, + ); return path; }); diff --git a/src/views/DeepLinkRedirectView.vue b/src/views/DeepLinkRedirectView.vue new file mode 100644 index 00000000..1615cfd4 --- /dev/null +++ b/src/views/DeepLinkRedirectView.vue @@ -0,0 +1,221 @@ + + + diff --git a/src/views/HomeView.vue b/src/views/HomeView.vue index 70a569a2..b6ea5378 100644 --- a/src/views/HomeView.vue +++ b/src/views/HomeView.vue @@ -519,7 +519,6 @@ export default class HomeView extends Vue { // Retrieve DIDs with better error handling try { this.allMyDids = await retrieveAccountDids(); - logConsoleAndDb(`[HomeView] Retrieved ${this.allMyDids.length} DIDs`); } catch (error) { logConsoleAndDb(`[HomeView] Failed to retrieve DIDs: ${error}`, true); throw new Error( @@ -552,9 +551,6 @@ export default class HomeView extends Vue { if (USE_DEXIE_DB) { settings = await retrieveSettingsForActiveAccount(); } - logConsoleAndDb( - `[HomeView] Retrieved settings for ${settings.activeDid || "no active DID"}`, - ); } catch (error) { logConsoleAndDb( `[HomeView] Failed to retrieve settings: ${error}`, @@ -581,9 +577,6 @@ export default class HomeView extends Vue { if (USE_DEXIE_DB) { this.allContacts = await db.contacts.toArray(); } - logConsoleAndDb( - `[HomeView] Retrieved ${this.allContacts.length} contacts`, - ); } catch (error) { logConsoleAndDb( `[HomeView] Failed to retrieve contacts: ${error}`, @@ -641,9 +634,6 @@ export default class HomeView extends Vue { }); } this.isRegistered = true; - logConsoleAndDb( - `[HomeView] User ${this.activeDid} is now registered`, - ); } } catch (error) { logConsoleAndDb( @@ -685,11 +675,6 @@ export default class HomeView extends Vue { this.newOffersToUserHitLimit = offersToUser.hitLimit; this.numNewOffersToUserProjects = offersToProjects.data.length; this.newOffersToUserProjectsHitLimit = offersToProjects.hitLimit; - - logConsoleAndDb( - `[HomeView] Retrieved ${this.numNewOffersToUser} user offers and ` + - `${this.numNewOffersToUserProjects} project offers`, - ); } } catch (error) { logConsoleAndDb( From 66895202701d4bb20a458acdf4e93cb6a6374d24 Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Wed, 18 Jun 2025 15:53:16 -0600 Subject: [PATCH 23/64] fix all copies for externally-shared links to redirected deep links --- src/components/HiddenDidDialog.vue | 15 ++++++--- src/libs/endorserServer.ts | 3 +- src/services/deepLinks.ts | 1 - src/views/ClaimView.vue | 33 +++++++++++++------ src/views/ConfirmGiftView.vue | 13 +++++--- src/views/ContactQRScanFullView.vue | 16 +++++++-- src/views/ContactQRScanShowView.vue | 17 ++++++++-- src/views/ContactsView.vue | 4 +-- src/views/InviteOneView.vue | 2 +- src/views/OnboardMeetingSetupView.vue | 2 +- src/views/ProjectViewView.vue | 47 ++++++++++++++++++++++++--- src/views/ShareMyContactInfoView.vue | 2 +- src/views/UserProfileView.vue | 33 +++++++++++++++++++ 13 files changed, 151 insertions(+), 37 deletions(-) diff --git a/src/components/HiddenDidDialog.vue b/src/components/HiddenDidDialog.vue index 980a3852..8593009e 100644 --- a/src/components/HiddenDidDialog.vue +++ b/src/components/HiddenDidDialog.vue @@ -77,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 }}. @@ -104,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 { @@ -117,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; @@ -129,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; } @@ -173,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/libs/endorserServer.ts b/src/libs/endorserServer.ts index cbdcbaee..f2dc79a4 100644 --- a/src/libs/endorserServer.ts +++ b/src/libs/endorserServer.ts @@ -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/services/deepLinks.ts b/src/services/deepLinks.ts index 93fe9aa3..8c8aa501 100644 --- a/src/services/deepLinks.ts +++ b/src/services/deepLinks.ts @@ -52,7 +52,6 @@ import { } from "../interfaces/deepLinks"; import { logConsoleAndDb } from "../db/databaseUtil"; import type { DeepLinkError } from "../interfaces/deepLinks"; -import { logger } from "@/utils/logger"; /** * Handles processing and routing of deep links in the application. diff --git a/src/views/ClaimView.vue b/src/views/ClaimView.vue index 1d647d7a..8edf3bf8 100644 --- a/src/views/ClaimView.vue +++ b/src/views/ClaimView.vue @@ -49,21 +49,32 @@ v-if="veriClaim.id" :to="'/claim-cert/' + encodeURIComponent(veriClaim.id)" class="text-blue-500 mt-2" - title="Printable Certificate" + title="View Printable Certificate" > +

@@ -405,7 +416,7 @@ contacts can see more details: click to copy this page info and see if they can make an introduction. Someone is connected to @@ -428,7 +439,7 @@ If you'd like an introduction, share this page with them and ask if they'll tell you more about about the participants. @@ -546,7 +557,7 @@ import { useClipboard } from "@vueuse/core"; import { GenericVerifiableCredential } from "../interfaces"; import GiftedDialog from "../components/GiftedDialog.vue"; 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 * as databaseUtil from "../db/databaseUtil"; import { db } from "../db/index"; import { logConsoleAndDb } from "../db/databaseUtil"; @@ -593,8 +604,9 @@ export default class ClaimView extends Vue { veriClaim = serverUtil.BLANK_GENERIC_SERVER_RECORD; veriClaimDump = ""; veriClaimDidsVisible: { [key: string]: string[] } = {}; - windowLocation = window.location.href; + windowDeepLink = window.location.href; // changed in the setup for deep linking + APP_SERVER = APP_SERVER; R = R; yaml = yaml; libsUtil = libsUtil; @@ -671,6 +683,7 @@ export default class ClaimView extends Vue { 5000, ); } + this.windowDeepLink = `${APP_SERVER}/deep-link/claim/${claimId}`; this.canShare = !!navigator.share; } @@ -1006,11 +1019,11 @@ export default class ClaimView extends Vue { } onClickShareClaim() { - this.copyToClipboard("A link to this page", this.windowLocation); + this.copyToClipboard("A link to this page", this.windowDeepLink); 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.windowDeepLink, }); } diff --git a/src/views/ConfirmGiftView.vue b/src/views/ConfirmGiftView.vue index 63225259..793df516 100644 --- a/src/views/ConfirmGiftView.vue +++ b/src/views/ConfirmGiftView.vue @@ -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 */ - if (this.giveDetails?.agentDid) { - this.giverName = this.didInfo(this.giveDetails.agentDid); + this.giverName = this.didInfo(this.giveDetails?.agentDid); + if (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)}`; } diff --git a/src/views/ContactQRScanFullView.vue b/src/views/ContactQRScanFullView.vue index eba485ca..1ca08edc 100644 --- a/src/views/ContactQRScanFullView.vue +++ b/src/views/ContactQRScanFullView.vue @@ -124,12 +124,14 @@ import * as databaseUtil from "../db/databaseUtil"; import { CONTACT_CSV_HEADER, CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI, + generateEndorserJwtUrlForAccount, setVisibilityUtil, } from "../libs/endorserServer"; import UserNameDialog from "../components/UserNameDialog.vue"; import { retrieveAccountMetadata } from "../libs/util"; import { PlatformServiceFactory } from "@/services/PlatformServiceFactory"; import { parseJsonField } from "../db/databaseUtil"; +import { Account } from "@/db/tables/accounts"; interface QRScanResult { rawValue?: string; @@ -157,6 +159,7 @@ export default class ContactQRScanFull extends Vue { apiServer = ""; givenName = ""; isRegistered = false; + profileImageUrl = ""; qrValue = ""; ETHR_DID_PREFIX = ETHR_DID_PREFIX; @@ -179,6 +182,7 @@ export default class ContactQRScanFull extends Vue { this.apiServer = settings.apiServer || ""; this.givenName = settings.firstName || ""; this.isRegistered = !!settings.isRegistered; + this.profileImageUrl = settings.profileImageUrl || ""; const account = await retrieveAccountMetadata(this.activeDid); if (account) { @@ -588,9 +592,17 @@ export default class ContactQRScanFull extends Vue { ); } - onCopyUrlToClipboard() { + async onCopyUrlToClipboard() { + const account = await libsUtil.retrieveFullyDecryptedAccount(this.activeDid) as Account; + const jwtUrl = await generateEndorserJwtUrlForAccount( + account, + this.isRegistered, + this.givenName, + this.profileImageUrl, + true, + ); useClipboard() - .copy(this.qrValue) + .copy(jwtUrl) .then(() => { this.$notify( { diff --git a/src/views/ContactQRScanShowView.vue b/src/views/ContactQRScanShowView.vue index 076ee279..675559e7 100644 --- a/src/views/ContactQRScanShowView.vue +++ b/src/views/ContactQRScanShowView.vue @@ -177,6 +177,7 @@ import { getContactJwtFromJwtUrl } from "../libs/crypto"; import { CONTACT_CSV_HEADER, CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI, + generateEndorserJwtUrlForAccount, register, setVisibilityUtil, } from "../libs/endorserServer"; @@ -187,6 +188,7 @@ import { logger } from "../utils/logger"; import { QRScannerFactory } from "@/services/QRScanner/QRScannerFactory"; import { CameraState } from "@/services/QRScanner/types"; import { PlatformServiceFactory } from "@/services/PlatformServiceFactory"; +import { Account } from "@/db/tables/accounts"; interface QRScanResult { rawValue?: string; @@ -216,6 +218,7 @@ export default class ContactQRScanShow extends Vue { isRegistered = false; qrValue = ""; isScanning = false; + profileImageUrl = ""; error: string | null = null; // QR Scanner properties @@ -253,6 +256,7 @@ export default class ContactQRScanShow extends Vue { this.hideRegisterPromptOnNewContact = !!settings.hideRegisterPromptOnNewContact; this.isRegistered = !!settings.isRegistered; + this.profileImageUrl = settings.profileImageUrl || ""; const account = await libsUtil.retrieveAccountMetadata(this.activeDid); if (account) { @@ -667,10 +671,17 @@ export default class ContactQRScanShow extends Vue { }); } - onCopyUrlToClipboard() { - //this.onScanDetect([{ rawValue: this.qrValue }]); // good for testing + async onCopyUrlToClipboard() { + const account = await libsUtil.retrieveFullyDecryptedAccount(this.activeDid) as Account; + const jwtUrl = await generateEndorserJwtUrlForAccount( + account, + this.isRegistered, + this.givenName, + this.profileImageUrl, + true, + ); useClipboard() - .copy(this.qrValue) + .copy(jwtUrl) .then(() => { this.$notify( { diff --git a/src/views/ContactsView.vue b/src/views/ContactsView.vue index 02f8c6a4..faf63dbf 100644 --- a/src/views/ContactsView.vue +++ b/src/views/ContactsView.vue @@ -126,7 +126,6 @@
+
@@ -55,7 +61,11 @@ {{ issuerInfoObject?.displayName }} - + +
@@ -632,7 +642,7 @@ import TopMessage from "../components/TopMessage.vue"; import QuickNav from "../components/QuickNav.vue"; import EntityIcon from "../components/EntityIcon.vue"; import ProjectIcon from "../components/ProjectIcon.vue"; -import { NotificationIface, USE_DEXIE_DB } from "../constants/app"; +import { APP_SERVER, NotificationIface, USE_DEXIE_DB } from "../constants/app"; import * as databaseUtil from "../db/databaseUtil"; import { db, @@ -646,6 +656,7 @@ import { retrieveAccountDids } from "../libs/util"; import HiddenDidDialog from "../components/HiddenDidDialog.vue"; import { logger } from "../utils/logger"; import { PlatformServiceFactory } from "@/services/PlatformServiceFactory"; +import { useClipboard } from "@vueuse/core"; /** * Project View Component * @author Matthew Raymer @@ -842,6 +853,28 @@ export default class ProjectViewView extends Vue { }); } + onCopyLinkClick() { + const shortestProjectId = this.projectId.startsWith( + serverUtil.ENDORSER_CH_HANDLE_PREFIX, + ) + ? this.projectId.substring(serverUtil.ENDORSER_CH_HANDLE_PREFIX.length) + : this.projectId; + const deepLink = `${APP_SERVER}/deep-link/project/${shortestProjectId}`; + useClipboard() + .copy(deepLink) + .then(() => { + this.$notify( + { + group: "alert", + type: "toast", + title: "Copied", + text: "A link to this project was copied to the clipboard.", + }, + 2000, + ); + }); + } + // Isn't there a better way to make this available to the template? expandText() { this.expanded = true; @@ -1304,7 +1337,7 @@ export default class ProjectViewView extends Vue { } // return an HTTPS URL if it's not a global URL - addScheme(url: string) { + ensureScheme(url: string) { if (!libsUtil.isGlobalUri(url)) { return "https://" + url; } @@ -1465,7 +1498,13 @@ export default class ProjectViewView extends Vue { } openHiddenDidDialog() { + const shortestProjectId = this.projectId.startsWith( + serverUtil.ENDORSER_CH_HANDLE_PREFIX, + ) + ? this.projectId.substring(serverUtil.ENDORSER_CH_HANDLE_PREFIX.length) + : this.projectId; (this.$refs.hiddenDidDialog as HiddenDidDialog).open( + "project/" + shortestProjectId, "creator", this.issuerVisibleToDids, this.allContacts, diff --git a/src/views/ShareMyContactInfoView.vue b/src/views/ShareMyContactInfoView.vue index 445c2775..ca1f2e94 100644 --- a/src/views/ShareMyContactInfoView.vue +++ b/src/views/ShareMyContactInfoView.vue @@ -105,7 +105,7 @@ export default class ShareMyContactInfoView extends Vue { group: "alert", type: "info", title: "Copied", - text: "Your contact info was copied to the clipboard. Have them paste it in the box on their 'Contacts' screen.", + text: "Your contact info was copied to the clipboard. Have them click on it, or paste it in the box on their 'Contacts' screen.", }, 5000, ); diff --git a/src/views/UserProfileView.vue b/src/views/UserProfileView.vue index 0e1471b8..11db4a09 100644 --- a/src/views/UserProfileView.vue +++ b/src/views/UserProfileView.vue @@ -16,6 +16,9 @@ Individual Profile +
+ +
@@ -32,6 +35,12 @@
{{ didInfo(profile.issuerDid, activeDid, allMyDids, allContacts) }} +

{{ profile.description }} @@ -100,6 +109,7 @@ import { Router, RouteLocationNormalizedLoaded } from "vue-router"; import QuickNav from "../components/QuickNav.vue"; import TopMessage from "../components/TopMessage.vue"; import { + APP_SERVER, DEFAULT_PARTNER_API_SERVER, NotificationIface, USE_DEXIE_DB, @@ -113,6 +123,7 @@ import { retrieveAccountDids } from "../libs/util"; import { logger } from "../utils/logger"; import { PlatformServiceFactory } from "@/services/PlatformServiceFactory"; import { Settings } from "@/db/tables/settings"; +import { useClipboard } from "@vueuse/core"; @Component({ components: { LMap, @@ -186,6 +197,10 @@ export default class UserProfileView extends Vue { if (response.status === 200) { const result = await response.json(); this.profile = result.data; + if (this.profile && this.profile.rowId !== profileId) { + // currently the server returns "rowid" with lowercase "i"; remove when that's fixed + this.profile.rowId = profileId; + } } else { throw new Error("Failed to load profile"); } @@ -204,5 +219,23 @@ export default class UserProfileView extends Vue { this.isLoading = false; } } + + onCopyLinkClick() { + console.log("onCopyLinkClick", this.profile); + const deepLink = `${APP_SERVER}/deep-link/user-profile/${this.profile?.rowId}`; + useClipboard() + .copy(deepLink) + .then(() => { + this.$notify( + { + group: "alert", + type: "toast", + title: "Copied", + text: "A link to this profile was copied to the clipboard.", + }, + 2000, + ); + }); + } } From 20ade415dc9e26eb0cedcc9e30168ad198fabe4e Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Wed, 18 Jun 2025 16:31:31 -0600 Subject: [PATCH 24/64] bump to version 0.5.8 build 34 --- BUILDING.md | 5 +++-- CHANGELOG.md | 7 +++++++ android/app/build.gradle | 4 ++-- ios/App/App.xcodeproj/project.pbxproj | 8 ++++---- package.json | 2 +- 5 files changed, 17 insertions(+), 9 deletions(-) diff --git a/BUILDING.md b/BUILDING.md index a159d900..b9167c1c 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 ``` @@ -360,10 +361,10 @@ Prerequisites: macOS with Xcode installed ``` cd ios/App - xcrun agvtool new-version 33 + xcrun agvtool new-version 34 # 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.7;/g" > temp && mv temp App.xcodeproj/project.pbxproj + cat App.xcodeproj/project.pbxproj | sed "s/MARKETING_VERSION = .*;/MARKETING_VERSION = 0.5.8;/g" > temp && mv temp App.xcodeproj/project.pbxproj cd - ``` diff --git a/CHANGELOG.md b/CHANGELOG.md index 71657a57..b6ce5430 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,13 @@ 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). +## [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/android/app/build.gradle b/android/app/build.gradle index 810eccff..bfa25355 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 33 - versionName "0.5.7" + versionCode 34 + versionName "0.5.8" 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/ios/App/App.xcodeproj/project.pbxproj b/ios/App/App.xcodeproj/project.pbxproj index e4a7b5e3..d3e6b9b4 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 = 33; + CURRENT_PROJECT_VERSION = 34; DEVELOPMENT_TEAM = GM3FS5JQPH; ENABLE_APP_SANDBOX = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO; @@ -413,7 +413,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 0.5.7; + MARKETING_VERSION = 0.5.8; 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 = 33; + CURRENT_PROJECT_VERSION = 34; DEVELOPMENT_TEAM = GM3FS5JQPH; ENABLE_APP_SANDBOX = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO; @@ -440,7 +440,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 0.5.7; + MARKETING_VERSION = 0.5.8; PRODUCT_BUNDLE_IDENTIFIER = app.timesafari; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = ""; diff --git a/package.json b/package.json index 935ece64..06722ca3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "timesafari", - "version": "0.5.6", + "version": "0.5.8", "description": "Time Safari Application", "author": { "name": "Time Safari Team" From c4a54967bc140430f46844edeb719cc6dcf9e353 Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Wed, 18 Jun 2025 16:33:55 -0600 Subject: [PATCH 25/64] fix linting --- src/views/ConfirmGiftView.vue | 4 ++-- src/views/ContactQRScanFullView.vue | 4 +++- src/views/ContactQRScanShowView.vue | 4 +++- src/views/UserProfileView.vue | 15 ++++++--------- 4 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/views/ConfirmGiftView.vue b/src/views/ConfirmGiftView.vue index 793df516..fbf81bab 100644 --- a/src/views/ConfirmGiftView.vue +++ b/src/views/ConfirmGiftView.vue @@ -679,8 +679,8 @@ 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); + if (this.giveDetails?.agentDid) { this.urlForNewGive += `&giverDid=${encodeURIComponent(this.giveDetails.agentDid)}&giverName=${encodeURIComponent(this.giverName)}`; } this.recipientName = this.didInfo(this.giveDetails?.recipientDid); diff --git a/src/views/ContactQRScanFullView.vue b/src/views/ContactQRScanFullView.vue index 1ca08edc..c5a08b74 100644 --- a/src/views/ContactQRScanFullView.vue +++ b/src/views/ContactQRScanFullView.vue @@ -593,7 +593,9 @@ export default class ContactQRScanFull extends Vue { } async onCopyUrlToClipboard() { - const account = await libsUtil.retrieveFullyDecryptedAccount(this.activeDid) as Account; + const account = (await libsUtil.retrieveFullyDecryptedAccount( + this.activeDid, + )) as Account; const jwtUrl = await generateEndorserJwtUrlForAccount( account, this.isRegistered, diff --git a/src/views/ContactQRScanShowView.vue b/src/views/ContactQRScanShowView.vue index 675559e7..29b28e5f 100644 --- a/src/views/ContactQRScanShowView.vue +++ b/src/views/ContactQRScanShowView.vue @@ -672,7 +672,9 @@ export default class ContactQRScanShow extends Vue { } async onCopyUrlToClipboard() { - const account = await libsUtil.retrieveFullyDecryptedAccount(this.activeDid) as Account; + const account = (await libsUtil.retrieveFullyDecryptedAccount( + this.activeDid, + )) as Account; const jwtUrl = await generateEndorserJwtUrlForAccount( account, this.isRegistered, diff --git a/src/views/UserProfileView.vue b/src/views/UserProfileView.vue index 11db4a09..1defd348 100644 --- a/src/views/UserProfileView.vue +++ b/src/views/UserProfileView.vue @@ -16,9 +16,7 @@ Individual Profile -

- -
+
@@ -35,11 +33,11 @@
{{ didInfo(profile.issuerDid, activeDid, allMyDids, allContacts) }} -

@@ -221,7 +219,6 @@ export default class UserProfileView extends Vue { } onCopyLinkClick() { - console.log("onCopyLinkClick", this.profile); const deepLink = `${APP_SERVER}/deep-link/user-profile/${this.profile?.rowId}`; useClipboard() .copy(deepLink) From 16557f1e4b55eecfaa314aefb8d5623f710cd68e Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Wed, 18 Jun 2025 17:32:41 -0600 Subject: [PATCH 26/64] update build instruction & package-lock.json --- BUILDING.md | 4 +++- package-lock.json | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/BUILDING.md b/BUILDING.md index b9167c1c..831d5d40 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -65,6 +65,8 @@ Install dependencies: * Commit everything (since the commit hash is used the app). +* Run a build to make sure package-lock version is updated, linting works, etc: `npm install && npm run build` + * Put the commit hash in the changelog (which will help you remember to bump the version 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`. @@ -72,7 +74,7 @@ Install dependencies: * 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: diff --git a/package-lock.json b/package-lock.json index a3e24731..7b2d9a83 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "timesafari", - "version": "0.5.4", + "version": "0.5.8", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "timesafari", - "version": "0.5.4", + "version": "0.5.8", "dependencies": { "@capacitor-community/sqlite": "6.0.2", "@capacitor-mlkit/barcode-scanning": "^6.0.0", From 1e0efe601189d8f37dd8daf0530537e1a702800b Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Wed, 18 Jun 2025 18:32:55 -0600 Subject: [PATCH 27/64] lengthen the error timeout when the message may be complicated, eg. with details from the server --- src/views/ContactsView.vue | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/views/ContactsView.vue b/src/views/ContactsView.vue index faf63dbf..fbf85774 100644 --- a/src/views/ContactsView.vue +++ b/src/views/ContactsView.vue @@ -617,7 +617,7 @@ export default class ContactsView extends Vue { title: "Error with Invite", text: message, }, - 5000, + -1, ); } // if we're here, they haven't redirected anywhere, so we'll redirect here without a query parameter @@ -1122,7 +1122,7 @@ export default class ContactsView extends Vue { (regResult.error as string) || "Something went wrong during registration.", }, - 5000, + -1, ); } } catch (error) { @@ -1156,7 +1156,7 @@ export default class ContactsView extends Vue { title: "Registration Error", text: userMessage, }, - 5000, + -1, ); } } From e9a8a3c1e7f6916b824a4642a8bd9d644c68537f Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Wed, 18 Jun 2025 19:31:16 -0600 Subject: [PATCH 28/64] add support for deep-link query parameters --- src/services/deepLinks.ts | 64 +++++++++++++++--------------- src/views/DeepLinkRedirectView.vue | 44 +++++++++++--------- 2 files changed, 57 insertions(+), 51 deletions(-) diff --git a/src/services/deepLinks.ts b/src/services/deepLinks.ts index 8c8aa501..bff3346c 100644 --- a/src/services/deepLinks.ts +++ b/src/services/deepLinks.ts @@ -115,7 +115,7 @@ export class DeepLinkHandler { const [path, queryString] = parts[1].split("?"); const [routePath, ...pathParams] = path.split("/"); - // logger.log( + // logger.info( // "[DeepLink] Debug:", // "Route Path:", // routePath, @@ -150,37 +150,6 @@ export class DeepLinkHandler { 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. @@ -256,4 +225,35 @@ export class DeepLinkHandler { }; } } + + /** + * 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/views/DeepLinkRedirectView.vue b/src/views/DeepLinkRedirectView.vue index 1615cfd4..74ea25d7 100644 --- a/src/views/DeepLinkRedirectView.vue +++ b/src/views/DeepLinkRedirectView.vue @@ -122,17 +122,31 @@ export default class DeepLinkRedirectView extends Vue { // If pathParam is an array (catch-all parameter), join it const fullPath = Array.isArray(pathParam) ? pathParam.join("/") : pathParam; - this.destinationUrl = fullPath; - this.deepLinkUrl = `timesafari://${fullPath}`; - this.webUrl = `${APP_SERVER}/${fullPath}`; - - // Log for debugging - logger.info("Deep link processing:", { - fullPath, - deepLinkUrl: this.deepLinkUrl, - webUrl: this.webUrl, - userAgent: this.userAgent, - }); + + // Get query parameters from the route + const queryParams = this.$route.query; + + // Build query string if there are query parameters + let queryString = ""; + if (Object.keys(queryParams).length > 0) { + const searchParams = new URLSearchParams(); + Object.entries(queryParams).forEach(([key, value]) => { + if (value !== undefined && value !== null) { + const stringValue = Array.isArray(value) ? value[0] : value; + if (stringValue !== null && stringValue !== undefined) { + searchParams.append(key, stringValue); + } + } + }); + queryString = "?" + searchParams.toString(); + } + + // Combine path with query parameters + const fullPathWithQuery = fullPath + queryString; + + this.destinationUrl = fullPathWithQuery; + this.deepLinkUrl = `timesafari://${fullPathWithQuery}`; + this.webUrl = `${APP_SERVER}/${fullPathWithQuery}`; this.isDevelopment = process.env.NODE_ENV !== "production"; this.userAgent = navigator.userAgent; @@ -147,13 +161,6 @@ export default class DeepLinkRedirectView extends Vue { return; } - logger.info("Attempting deep link redirect:", { - deepLinkUrl: this.deepLinkUrl, - webUrl: this.webUrl, - isMobile: this.isMobile, - userAgent: this.userAgent, - }); - try { // For mobile, try the deep link URL; for desktop, use the web URL const redirectUrl = this.isMobile ? this.deepLinkUrl : this.webUrl; @@ -170,7 +177,6 @@ export default class DeepLinkRedirectView extends Vue { document.body.appendChild(link); link.click(); document.body.removeChild(link); - logger.info("Fallback link click completed"); } catch (error) { logger.error( "Fallback deep link failed: " + errorStringForLog(error), From 3118f7132070acc4da1c515eedb58dd0a2414dbf Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Wed, 18 Jun 2025 21:44:11 -0600 Subject: [PATCH 29/64] fix linting (whitespace only) --- src/views/DeepLinkRedirectView.vue | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/views/DeepLinkRedirectView.vue b/src/views/DeepLinkRedirectView.vue index 74ea25d7..8d2cb08c 100644 --- a/src/views/DeepLinkRedirectView.vue +++ b/src/views/DeepLinkRedirectView.vue @@ -122,10 +122,10 @@ export default class DeepLinkRedirectView extends Vue { // If pathParam is an array (catch-all parameter), join it const fullPath = Array.isArray(pathParam) ? pathParam.join("/") : pathParam; - + // Get query parameters from the route const queryParams = this.$route.query; - + // Build query string if there are query parameters let queryString = ""; if (Object.keys(queryParams).length > 0) { @@ -140,10 +140,10 @@ export default class DeepLinkRedirectView extends Vue { }); queryString = "?" + searchParams.toString(); } - + // Combine path with query parameters const fullPathWithQuery = fullPath + queryString; - + this.destinationUrl = fullPathWithQuery; this.deepLinkUrl = `timesafari://${fullPathWithQuery}`; this.webUrl = `${APP_SERVER}/${fullPathWithQuery}`; From f375a4e11ab1e192f78bb20ed0827cbeefd4b886 Mon Sep 17 00:00:00 2001 From: Matthew Raymer Date: Thu, 19 Jun 2025 05:51:59 +0000 Subject: [PATCH 30/64] feat: move database migration link from account view to start view - Remove database migration link from AccountViewView.vue - Add new "Database Tools" section to StartView.vue - Improve user flow by making database tools accessible from start page - Maintain consistent styling and functionality - Clean up account view to focus on account-specific settings The database migration feature is now logically grouped with other identity-related operations and more discoverable for users. --- src/libs/util.ts | 36 ++++++++++++++++++++++++++++ src/views/AccountViewView.vue | 6 ----- src/views/ImportAccountView.vue | 42 ++++++--------------------------- src/views/StartView.vue | 13 ++++++++++ 4 files changed, 56 insertions(+), 41 deletions(-) diff --git a/src/libs/util.ts b/src/libs/util.ts index 03ee7637..030ba416 100644 --- a/src/libs/util.ts +++ b/src/libs/util.ts @@ -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; @@ -998,3 +999,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 database operations + const accountsDB = await accountsDBPromise; + if (shouldErase) { + const platformService = PlatformServiceFactory.getInstance(); + await platformService.dbExec("DELETE FROM accounts"); + if (USE_DEXIE_DB) { + await accountsDB.accounts.clear(); + } + } + + // Save the new identity + await saveNewIdentity(newId, mne, derivationPath); +} diff --git a/src/views/AccountViewView.vue b/src/views/AccountViewView.vue index 896b22fa..c3fee747 100644 --- a/src/views/AccountViewView.vue +++ b/src/views/AccountViewView.vue @@ -953,12 +953,6 @@ > Logs - - Database Migration - + + +

+

Database Tools

+
+ + Database Migration + +
+
From 8a7f142cb7322b901c74e24910f055345fddae0c Mon Sep 17 00:00:00 2001 From: Matthew Raymer Date: Thu, 19 Jun 2025 06:13:25 +0000 Subject: [PATCH 31/64] feat: integrate importFromMnemonic utility into migration service and UI - Add account migration support to migrationService with importFromMnemonic integration - Extend DataComparison and MigrationResult interfaces to include accounts - Add getDexieAccounts() and getSqliteAccounts() functions for account retrieval - Implement compareAccounts() and migrateAccounts() functions with proper error handling - Update DatabaseMigration.vue UI to support account migration: - Add "Migrate Accounts" button with lock icon - Extend summary cards grid to show Dexie/SQLite account counts - Add Account Differences section with added/modified/missing indicators - Update success message to include account migration counts - Enhance grid layouts to accommodate 6 summary cards and 3 difference sections The migration service now provides complete data migration capabilities for contacts, settings, and accounts, with enhanced reliability through the importFromMnemonic utility for mnemonic-based account handling. --- src/services/migrationService.ts | 406 ++++++++++++++++++++++++++++++- src/views/DatabaseMigration.vue | 241 +++++++++++++++++- 2 files changed, 636 insertions(+), 11 deletions(-) diff --git a/src/services/migrationService.ts b/src/services/migrationService.ts index fd08cba5..73936210 100644 --- a/src/services/migrationService.ts +++ b/src/services/migrationService.ts @@ -22,12 +22,14 @@ */ import { PlatformServiceFactory } from "./PlatformServiceFactory"; -import { db } from "../db/index"; +import { db, accountsDBPromise } from "../db/index"; import { Contact, ContactMethod } from "../db/tables/contacts"; import { Settings } from "../db/tables/settings"; +import { Account } from "../db/tables/accounts"; import { logger } from "../utils/logger"; import { parseJsonField } from "../db/databaseUtil"; import { USE_DEXIE_DB } from "../constants/app"; +import { importFromMnemonic } from "../libs/util"; /** * Interface for data comparison results between Dexie and SQLite databases @@ -41,6 +43,8 @@ import { USE_DEXIE_DB } from "../constants/app"; * @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 @@ -50,12 +54,18 @@ import { USE_DEXIE_DB } from "../constants/app"; * @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: Account[]; differences: { contacts: { added: Contact[]; @@ -67,6 +77,11 @@ export interface DataComparison { modified: Settings[]; missing: Settings[]; }; + accounts: { + added: Account[]; + modified: Account[]; + missing: Account[]; + }; }; } @@ -81,6 +96,7 @@ export interface DataComparison { * @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) */ @@ -88,6 +104,7 @@ export interface MigrationResult { success: boolean; contactsMigrated: number; settingsMigrated: number; + accountsMigrated: number; errors: string[]; warnings: string[]; } @@ -315,6 +332,105 @@ export async function getSqliteSettings(): Promise { } } +/** + * 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 * FROM accounts"); + + if (!result?.values?.length) { + return []; + } + + const accounts = result.values.map((row) => { + const account = parseJsonField(row, {}) as any; + return { + id: account.id, + dateCreated: account.dateCreated || "", + derivationPath: account.derivationPath || "", + did: account.did || "", + identity: account.identity || "", + mnemonic: account.mnemonic || "", + passkeyCredIdHex: account.passkeyCredIdHex || "", + publicKeyHex: account.publicKeyHex || "", + } as Account; + }); + + logger.info( + `[MigrationService] Retrieved ${accounts.length} accounts from SQLite`, + ); + return accounts; + } catch (error) { + logger.error("[MigrationService] 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. 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 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 { + if (!USE_DEXIE_DB) { + throw new Error("Dexie database is not enabled"); + } + + try { + const accountsDB = await accountsDBPromise; + await accountsDB.open(); + const accounts = await accountsDB.accounts.toArray(); + logger.info( + `[MigrationService] Retrieved ${accounts.length} accounts from Dexie`, + ); + return accounts; + } catch (error) { + logger.error("[MigrationService] Error retrieving Dexie accounts:", error); + throw new Error(`Failed to retrieve Dexie accounts: ${error}`); + } +} + /** * Compares data between Dexie and SQLite databases * @@ -346,13 +462,21 @@ export async function getSqliteSettings(): Promise { export async function compareDatabases(): Promise { logger.info("[MigrationService] Starting database comparison"); - const [dexieContacts, sqliteContacts, dexieSettings, sqliteSettings] = - await Promise.all([ - getDexieContacts(), - getSqliteContacts(), - getDexieSettings(), - getSqliteSettings(), - ]); + const [ + dexieContacts, + sqliteContacts, + dexieSettings, + sqliteSettings, + dexieAccounts, + sqliteAccounts, + ] = await Promise.all([ + getDexieContacts(), + getSqliteContacts(), + getDexieSettings(), + getSqliteSettings(), + getDexieAccounts(), + getSqliteAccounts(), + ]); // Compare contacts const contactDifferences = compareContacts(dexieContacts, sqliteContacts); @@ -360,14 +484,20 @@ export async function compareDatabases(): Promise { // 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, }, }; @@ -376,8 +506,11 @@ export async function compareDatabases(): Promise { sqliteContacts: sqliteContacts.length, dexieSettings: dexieSettings.length, sqliteSettings: sqliteSettings.length, + dexieAccounts: dexieAccounts.length, + sqliteAccounts: sqliteAccounts.length, contactDifferences: contactDifferences, settingsDifferences: settingsDifferences, + accountDifferences: accountDifferences, }); return comparison; @@ -491,6 +624,57 @@ function compareSettings( return { added, modified, 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[]} sqliteAccounts - 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 - Accounts that differ between databases + * @returns {Account[]} 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[], sqliteAccounts: Account[]) { + const added: Account[] = []; + const modified: Account[] = []; + const missing: Account[] = []; + + // Find accounts that exist in Dexie but not in SQLite + for (const dexieAccount of dexieAccounts) { + const sqliteAccount = sqliteAccounts.find((a) => a.id === dexieAccount.id); + if (!sqliteAccount) { + added.push(dexieAccount); + } else if (!accountsEqual(dexieAccount, sqliteAccount)) { + modified.push(dexieAccount); + } + } + + // Find accounts that exist in SQLite but not in Dexie + for (const sqliteAccount of sqliteAccounts) { + const dexieAccount = dexieAccounts.find((a) => a.id === sqliteAccount.id); + if (!dexieAccount) { + missing.push(sqliteAccount); + } + } + + return { added, modified, missing }; +} + /** * Compares two contacts for equality * @@ -592,6 +776,43 @@ function settingsEqual(settings1: Settings, settings2: Settings): boolean { ); } +/** + * 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'); + * } + * ``` + */ +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 * @@ -622,6 +843,8 @@ export function generateComparisonYaml(comparison: DataComparison): string { sqliteContacts: comparison.sqliteContacts.length, dexieSettings: comparison.dexieSettings.length, sqliteSettings: comparison.sqliteSettings.length, + dexieAccounts: comparison.dexieAccounts.length, + sqliteAccounts: comparison.sqliteAccounts.length, }, differences: { contacts: { @@ -634,6 +857,11 @@ export function generateComparisonYaml(comparison: DataComparison): string { modified: comparison.differences.settings.modified.length, missing: comparison.differences.settings.missing.length, }, + accounts: { + added: comparison.differences.accounts.added.length, + modified: comparison.differences.accounts.modified.length, + missing: comparison.differences.accounts.missing.length, + }, }, contacts: { dexie: comparison.dexieContacts.map((c) => ({ @@ -677,6 +905,28 @@ export function generateComparisonYaml(comparison: DataComparison): string { searchBoxes: s.searchBoxes, })), }, + accounts: { + dexie: comparison.dexieAccounts.map((a) => ({ + id: a.id, + dateCreated: a.dateCreated, + derivationPath: a.derivationPath, + did: a.did, + identity: a.identity, + mnemonic: a.mnemonic, + passkeyCredIdHex: a.passkeyCredIdHex, + publicKeyHex: a.publicKeyHex, + })), + sqlite: comparison.sqliteAccounts.map((a) => ({ + id: a.id, + dateCreated: a.dateCreated, + derivationPath: a.derivationPath, + did: a.did, + identity: a.identity, + mnemonic: a.mnemonic, + passkeyCredIdHex: a.passkeyCredIdHex, + publicKeyHex: a.publicKeyHex, + })), + }, }, }; @@ -725,6 +975,7 @@ export async function migrateContacts( success: true, contactsMigrated: 0, settingsMigrated: 0, + accountsMigrated: 0, errors: [], warnings: [], }; @@ -834,6 +1085,7 @@ export async function migrateSettings( success: true, contactsMigrated: 0, settingsMigrated: 0, + accountsMigrated: 0, errors: [], warnings: [], }; @@ -921,6 +1173,144 @@ export async function migrateSettings( } } +/** + * 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) based on the overwriteExisting parameter. + * + * 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 + * @param {boolean} [overwriteExisting=false] - Whether to overwrite existing accounts in SQLite + * @returns {Promise} Detailed results of the migration operation + * @throws {Error} If the migration process fails completely + * @example + * ```typescript + * try { + * const result = await migrateAccounts(true); // Overwrite existing + * 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( + overwriteExisting: boolean = false, +): Promise { + logger.info("[MigrationService] Starting account migration", { + overwriteExisting, + }); + + const result: MigrationResult = { + success: true, + contactsMigrated: 0, + settingsMigrated: 0, + accountsMigrated: 0, + errors: [], + warnings: [], + }; + + try { + const dexieAccounts = await getDexieAccounts(); + const platformService = PlatformServiceFactory.getInstance(); + + for (const account of dexieAccounts) { + try { + // Check if account already exists + const existingResult = await platformService.dbQuery( + "SELECT id FROM accounts WHERE id = ?", + [account.id], + ); + + if (existingResult?.values?.length) { + if (overwriteExisting) { + // Update existing account + const { sql, params } = generateUpdateStatement( + account as unknown as Record, + "accounts", + "id = ?", + [account.id], + ); + await platformService.dbExec(sql, params); + result.accountsMigrated++; + logger.info(`[MigrationService] Updated account: ${account.id}`); + } else { + result.warnings.push( + `Account ${account.id} already exists, skipping`, + ); + } + } else { + // For new accounts with mnemonic, use importFromMnemonic for proper key derivation + if (account.mnemonic && account.derivationPath) { + try { + // Use importFromMnemonic to ensure proper key derivation and identity creation + await importFromMnemonic( + account.mnemonic, + account.derivationPath, + false, // Don't erase existing accounts during migration + ); + logger.info( + `[MigrationService] Imported account with mnemonic: ${account.id}`, + ); + } catch (importError) { + // Fall back to direct insertion if importFromMnemonic fails + logger.warn( + `[MigrationService] importFromMnemonic failed for account ${account.id}, falling back to direct insertion: ${importError}`, + ); + const { sql, params } = generateInsertStatement( + account as unknown as Record, + "accounts", + ); + await platformService.dbExec(sql, params); + } + } else { + // Insert new account without mnemonic + const { sql, params } = generateInsertStatement( + account as unknown as Record, + "accounts", + ); + await platformService.dbExec(sql, params); + } + result.accountsMigrated++; + logger.info(`[MigrationService] Added account: ${account.id}`); + } + } catch (error) { + const errorMsg = `Failed to migrate account ${account.id}: ${error}`; + logger.error("[MigrationService]", errorMsg); + result.errors.push(errorMsg); + result.success = false; + } + } + + logger.info("[MigrationService] Account migration completed", { + accountsMigrated: result.accountsMigrated, + errors: result.errors.length, + warnings: result.warnings.length, + }); + + return result; + } catch (error) { + const errorMsg = `Account migration failed: ${error}`; + logger.error("[MigrationService]", errorMsg); + result.errors.push(errorMsg); + result.success = false; + return result; + } +} + /** * Generates SQL INSERT statement and parameters from a model object * diff --git a/src/views/DatabaseMigration.vue b/src/views/DatabaseMigration.vue index 7c62aa94..c80f38ab 100644 --- a/src/views/DatabaseMigration.vue +++ b/src/views/DatabaseMigration.vue @@ -140,6 +140,27 @@ Migrate Settings + + @@ -97,19 +69,7 @@ class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 disabled:opacity-50 disabled:cursor-not-allowed" @click="migrateContacts" > - - - + Migrate Contacts @@ -118,25 +78,7 @@ class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-purple-600 hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500 disabled:opacity-50 disabled:cursor-not-allowed" @click="migrateSettings" > - - - - + Migrate Settings @@ -145,19 +87,7 @@ class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-orange-600 hover:bg-orange-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-orange-500 disabled:opacity-50 disabled:cursor-not-allowed" @click="migrateAccounts" > - - - + Migrate Accounts @@ -166,21 +96,18 @@ class="inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed" @click="exportComparison" > - - - + Export Comparison + + @@ -188,26 +115,11 @@
- - - - + {{ loadingMessage }}
@@ -219,17 +131,11 @@ >
- - - + />

Error

@@ -247,17 +153,10 @@ >
- - - +

Success

@@ -276,19 +175,10 @@
- - - +
@@ -308,19 +198,10 @@
- - - +
@@ -340,25 +221,10 @@
- - - - +
@@ -378,19 +244,10 @@
- - - +
@@ -410,19 +267,10 @@
- - - +
@@ -442,19 +290,10 @@
- - - +
@@ -485,19 +324,10 @@ class="flex items-center justify-between p-3 bg-blue-50 rounded-lg" >
- - - + Added
{{ @@ -509,19 +339,10 @@ class="flex items-center justify-between p-3 bg-yellow-50 rounded-lg" >
- - - + Modified @@ -535,19 +356,10 @@ class="flex items-center justify-between p-3 bg-red-50 rounded-lg" >
- - - + Missing @@ -593,19 +405,10 @@ class="flex items-center justify-between p-3 bg-blue-50 rounded-lg" >
- - - + Added
{{ @@ -617,19 +420,10 @@ class="flex items-center justify-between p-3 bg-yellow-50 rounded-lg" >
- - - + Modified @@ -643,19 +437,10 @@ class="flex items-center justify-between p-3 bg-red-50 rounded-lg" >
- - - + Missing @@ -699,19 +484,10 @@ class="flex items-center justify-between p-3 bg-blue-50 rounded-lg" >
- - - + Added
{{ @@ -723,19 +499,10 @@ class="flex items-center justify-between p-3 bg-yellow-50 rounded-lg" >
- - - + Modified @@ -749,19 +516,10 @@ class="flex items-center justify-between p-3 bg-red-50 rounded-lg" >
- - - + Missing @@ -832,6 +590,7 @@ From 9d054074e4816efc2298539f43c4c322a35b0f9d Mon Sep 17 00:00:00 2001 From: Matthew Raymer Date: Thu, 19 Jun 2025 14:45:58 +0000 Subject: [PATCH 36/64] fix(migration): update UI to handle transformed JSON format The DatabaseMigration view has been updated to properly handle both live comparison data and exported JSON format, fixing count mismatches and field name differences. Changes: - Added helper methods in DatabaseMigration.vue to handle both data formats: - getSettingDisplayName() for settings with type/did or activeDid/accountDid - getAccountHasIdentity() and getAccountHasMnemonic() for boolean fields - Updated template to use new helper methods for consistent display - Added exportComparison() method to handle JSON export format - Fixed settings count display to match actual data state Technical Details: - Settings now handle both 'type'/'did' (JSON) and 'activeDid'/'accountDid' (live) - Account display properly shows boolean values from either format - Export functionality preserves data structure while maintaining readability Resolves count mismatch between UI (showing 1 SQLite setting) and JSON data (showing 0 SQLite settings). Testing: - Verified UI displays correct counts from both live and exported data - Confirmed settings display works with both data formats - Validated account boolean fields display correctly --- src/views/DatabaseMigration.vue | 243 +++++++++++++++++--------------- 1 file changed, 130 insertions(+), 113 deletions(-) diff --git a/src/views/DatabaseMigration.vue b/src/views/DatabaseMigration.vue index 7edd6628..2335884d 100644 --- a/src/views/DatabaseMigration.vue +++ b/src/views/DatabaseMigration.vue @@ -580,8 +580,8 @@ :key="setting.id" class="text-xs text-gray-600 bg-gray-50 p-2 rounded" > -
ID: {{ setting.id }} ({{ setting.id === 1 ? 'master' : 'account' }})
-
{{ setting.activeDid || setting.accountDid }}
+
{{ getSettingDisplayName(setting) }}
+
ID: {{ setting.id }}
Registered: {{ setting.isRegistered ? 'Yes' : 'No' }}
@@ -601,8 +601,8 @@ :key="setting.id" class="text-xs text-gray-600 bg-gray-50 p-2 rounded" > -
ID: {{ setting.id }} ({{ setting.id === 1 ? 'master' : 'account' }})
-
{{ setting.activeDid || setting.accountDid }}
+
{{ getSettingDisplayName(setting) }}
+
ID: {{ setting.id }}
Registered: {{ setting.isRegistered ? 'Yes' : 'No' }}
@@ -622,8 +622,8 @@ :key="setting.id" class="text-xs text-gray-600 bg-gray-50 p-2 rounded" > -
ID: {{ setting.id }} ({{ setting.id === 1 ? 'master' : 'account' }})
-
{{ setting.activeDid || setting.accountDid }}
+
{{ getSettingDisplayName(setting) }}
+
ID: {{ setting.id }}
Registered: {{ setting.isRegistered ? 'Yes' : 'No' }}
@@ -706,8 +706,8 @@
ID: {{ account.id }}
{{ account.did }}
Created: {{ account.dateCreated }}
-
Has Identity: {{ account.identity ? 'Yes' : 'No' }}
-
Has Mnemonic: {{ account.mnemonic ? 'Yes' : 'No' }}
+
Has Identity: {{ getAccountHasIdentity(account) ? 'Yes' : 'No' }}
+
Has Mnemonic: {{ getAccountHasMnemonic(account) ? 'Yes' : 'No' }}
@@ -729,8 +729,8 @@
ID: {{ account.id }}
{{ account.did }}
Created: {{ account.dateCreated }}
-
Has Identity: {{ account.identity ? 'Yes' : 'No' }}
-
Has Mnemonic: {{ account.mnemonic ? 'Yes' : 'No' }}
+
Has Identity: {{ getAccountHasIdentity(account) ? 'Yes' : 'No' }}
+
Has Mnemonic: {{ getAccountHasMnemonic(account) ? 'Yes' : 'No' }}
@@ -752,8 +752,8 @@
ID: {{ account.id }}
{{ account.did }}
Created: {{ account.dateCreated }}
-
Has Identity: {{ account.identity ? 'Yes' : 'No' }}
-
Has Mnemonic: {{ account.mnemonic ? 'Yes' : 'No' }}
+
Has Identity: {{ getAccountHasIdentity(account) ? 'Yes' : 'No' }}
+
Has Mnemonic: {{ getAccountHasMnemonic(account) ? 'Yes' : 'No' }}
@@ -857,6 +857,76 @@ export default class DatabaseMigration extends Vue { return USE_DEXIE_DB; } + /** + * Computed property to get the display name for a setting + * Handles both live comparison data and exported JSON format + * + * @param {any} setting - The setting object + * @returns {string} The display name for the setting + */ + getSettingDisplayName(setting: any): string { + // Handle exported JSON format (has 'type' and 'did' fields) + if (setting.type && setting.did) { + return `${setting.type} (${setting.did})`; + } + + // Handle live comparison data (has 'activeDid' or 'accountDid' fields) + const did = setting.activeDid || setting.accountDid; + const type = setting.id === 1 ? 'master' : 'account'; + return `${type} (${did || 'no DID'})`; + } + + /** + * Computed property to get the DID for a setting + * Handles both live comparison data and exported JSON format + * + * @param {any} setting - The setting object + * @returns {string} The DID for the setting + */ + getSettingDid(setting: any): string { + // Handle exported JSON format (has 'did' field) + if (setting.did) { + return setting.did; + } + + // Handle live comparison data (has 'activeDid' or 'accountDid' fields) + return setting.activeDid || setting.accountDid || 'no DID'; + } + + /** + * Computed property to check if an account has identity + * Handles both live comparison data and exported JSON format + * + * @param {any} account - The account object + * @returns {boolean} True if account has identity + */ + getAccountHasIdentity(account: any): boolean { + // Handle exported JSON format (has 'hasIdentity' field) + if (account.hasIdentity !== undefined) { + return account.hasIdentity; + } + + // Handle live comparison data (has 'identity' field) + return !!account.identity; + } + + /** + * Computed property to check if an account has mnemonic + * Handles both live comparison data and exported JSON format + * + * @param {any} account - The account object + * @returns {boolean} True if account has mnemonic + */ + getAccountHasMnemonic(account: any): boolean { + // Handle exported JSON format (has 'hasMnemonic' field) + if (account.hasMnemonic !== undefined) { + return account.hasMnemonic; + } + + // Handle live comparison data (has 'mnemonic' field) + return !!account.mnemonic; + } + /** * Migrates all data from Dexie to SQLite in the proper order * @@ -1078,12 +1148,10 @@ export default class DatabaseMigration extends Vue { } /** - * Verifies the migration by running another comparison + * Verifies the migration by running a fresh comparison * - * This method runs a fresh comparison between Dexie and SQLite databases - * to verify that the migration was successful. It's useful to run this - * after completing migrations to ensure data integrity and relationship - * preservation. + * This method runs a new comparison after migration to verify + * that the data was transferred correctly. * * @async * @returns {Promise} @@ -1094,75 +1162,24 @@ export default class DatabaseMigration extends Vue { try { const newComparison = await compareDatabases(); - - // Calculate differences by type for each table - const differences = { - contacts: { - added: newComparison.differences.contacts.added.length, - modified: newComparison.differences.contacts.modified.length, - missing: newComparison.differences.contacts.missing.length, - }, - settings: { - added: newComparison.differences.settings.added.length, - modified: newComparison.differences.settings.modified.length, - missing: newComparison.differences.settings.missing.length, - }, - accounts: { - added: newComparison.differences.accounts.added.length, - modified: newComparison.differences.accounts.modified.length, - missing: newComparison.differences.accounts.missing.length, - }, - }; - - const totalRemaining = Object.values(differences).reduce( - (sum, table) => - sum + table.added + table.modified + table.missing, - 0 - ); - - // Build a detailed message - const detailMessages = []; - if (differences.contacts.added + differences.contacts.modified + differences.contacts.missing > 0) { - detailMessages.push( - `Contacts: ${differences.contacts.added} to add, ${differences.contacts.modified} modified, ${differences.contacts.missing} missing` - ); - } - - if (differences.settings.added + differences.settings.modified + differences.settings.missing > 0) { - detailMessages.push( - `Settings: ${differences.settings.added} to add, ${differences.settings.modified} modified, ${differences.settings.missing} missing` - ); - } + const totalRemaining = + newComparison.differences.contacts.added.length + + newComparison.differences.settings.added.length + + newComparison.differences.accounts.added.length; - if (differences.accounts.added + differences.accounts.modified + differences.accounts.missing > 0) { - detailMessages.push( - `Accounts: ${differences.accounts.added} to add, ${differences.accounts.modified} modified, ${differences.accounts.missing} missing` - ); - } - if (totalRemaining === 0) { - this.successMessage = - "✅ Migration verification successful! All data has been migrated correctly."; - logger.info( - "[DatabaseMigration] Migration verification successful - no differences found" - ); + this.successMessage = "Migration verification successful! All data has been migrated."; + this.comparison = newComparison; } else { - this.successMessage = `⚠️ Migration verification completed. Found ${totalRemaining} remaining differences:\n${detailMessages.join("\n")}`; - if (differences.settings.modified > 0 || differences.settings.missing > 0) { - this.successMessage += "\n\nNote: Some settings differences may be expected due to default values in SQLite."; - } - logger.warn( - "[DatabaseMigration] Migration verification found remaining differences", - { - remaining: totalRemaining, - differences: differences, - } - ); + this.error = `Migration verification failed. ${totalRemaining} items still need to be migrated.`; + this.comparison = newComparison; } - - // Update the comparison to show the current state - this.comparison = newComparison; + + logger.info("[DatabaseMigration] Migration verification completed", { + totalRemaining, + success: totalRemaining === 0 + }); } catch (error) { this.error = `Failed to verify migration: ${error}`; logger.error("[DatabaseMigration] Migration verification failed:", error); @@ -1172,10 +1189,10 @@ export default class DatabaseMigration extends Vue { } /** - * Exports comparison results to a file + * Exports the comparison data to a JSON file * - * This method generates a YAML-formatted comparison and triggers - * a file download for the user. + * This method generates a JSON file containing the complete comparison + * data in a format that matches the exported JSON structure. * * @async * @returns {Promise} @@ -1190,41 +1207,23 @@ export default class DatabaseMigration extends Vue { const yamlData = generateComparisonYaml(this.comparison); const blob = new Blob([yamlData], { type: "application/json" }); const url = URL.createObjectURL(blob); - - const link = document.createElement("a"); - link.href = url; - link.download = `database-comparison-${new Date().toISOString().split("T")[0]}.json`; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); + + const a = document.createElement("a"); + a.href = url; + a.download = `database-comparison-${new Date().toISOString().split('T')[0]}.json`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); URL.revokeObjectURL(url); - - this.successMessage = "Comparison exported successfully"; - logger.info("[DatabaseMigration] Comparison exported successfully"); + + this.successMessage = "Comparison data exported successfully"; + logger.info("[DatabaseMigration] Comparison data exported successfully"); } catch (error) { - this.error = `Failed to export comparison: ${error}`; + this.error = `Failed to export comparison data: ${error}`; logger.error("[DatabaseMigration] Export failed:", error); } } - /** - * Sets the loading state and message - * - * @param {string} message - The loading message to display - */ - private setLoading(message: string): void { - this.isLoading = message !== ""; - this.loadingMessage = message; - } - - /** - * Clears all error and success messages - */ - private clearMessages(): void { - this.error = ""; - this.successMessage = ""; - } - /** * Tests the specific settings migration for the fields you mentioned * @@ -1252,5 +1251,23 @@ export default class DatabaseMigration extends Vue { this.setLoading(""); } } + + /** + * Sets the loading state and message + * + * @param {string} message - The loading message to display + */ + private setLoading(message: string): void { + this.isLoading = message !== ""; + this.loadingMessage = message; + } + + /** + * Clears all error and success messages + */ + private clearMessages(): void { + this.error = ""; + this.successMessage = ""; + } } From e759e4785bbb46ac1e9e4d28d8b1f2e48ff12a7c Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Thu, 19 Jun 2025 09:47:18 -0600 Subject: [PATCH 37/64] IndexedDB migration: set USE_DEXIE_DB to false, remove unused functions, add raw display of data --- BUILDING.md | 6 +- src/constants/app.ts | 2 +- src/services/migrationService.ts | 76 +-------- src/views/DatabaseMigration.vue | 264 ++++++++++++------------------- 4 files changed, 108 insertions(+), 240 deletions(-) diff --git a/BUILDING.md b/BUILDING.md index a159d900..0ab17341 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -66,7 +66,7 @@ Install dependencies: * Put the commit hash in the changelog (which will help you remember to bump the version 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 0.5.8 && git push origin 0.5.8`. * For test, build the app (because test server is not yet set up to build): @@ -90,9 +90,9 @@ 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 0.5.8 && 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/` diff --git a/src/constants/app.ts b/src/constants/app.ts index b8dadbd4..a08cb15a 100644 --- a/src/constants/app.ts +++ b/src/constants/app.ts @@ -51,7 +51,7 @@ export const IMAGE_TYPE_PROFILE = "profile"; export const PASSKEYS_ENABLED = !!import.meta.env.VITE_PASSKEYS_ENABLED || false; -export const USE_DEXIE_DB = true; +export const USE_DEXIE_DB = false; /** * The possible values for "group" and "type" are in App.vue. diff --git a/src/services/migrationService.ts b/src/services/migrationService.ts index 234629af..45fcdb95 100644 --- a/src/services/migrationService.ts +++ b/src/services/migrationService.ts @@ -28,7 +28,6 @@ import { Settings, MASTER_SETTINGS_KEY } from "../db/tables/settings"; import { Account } from "../db/tables/accounts"; import { logger } from "../utils/logger"; import { parseJsonField } from "../db/databaseUtil"; -import { USE_DEXIE_DB } from "../constants/app"; import { importFromMnemonic } from "../libs/util"; /** @@ -133,10 +132,6 @@ export interface MigrationResult { * ``` */ export async function getDexieContacts(): Promise { - if (!USE_DEXIE_DB) { - throw new Error("Dexie database is not enabled"); - } - try { await db.open(); const contacts = await db.contacts.toArray(); @@ -215,8 +210,8 @@ export async function getSqliteContacts(): Promise { * Retrieves all settings from the Dexie (IndexedDB) database * * This function connects to the Dexie database and retrieves all settings - * records. It requires that USE_DEXIE_DB is enabled in the app constants. - * + * records. + * * Settings include both master settings (id=1) and account-specific settings * that override the master settings for particular user accounts. * @@ -235,10 +230,6 @@ export async function getSqliteContacts(): Promise { * ``` */ export async function getDexieSettings(): Promise { - if (!USE_DEXIE_DB) { - throw new Error("Dexie database is not enabled"); - } - try { await db.open(); const settings = await db.settings.toArray(); @@ -388,7 +379,7 @@ export async function getSqliteAccounts(): Promise { * Retrieves all accounts from the Dexie (IndexedDB) database * * This function connects to the Dexie database and retrieves all account - * records. It requires that USE_DEXIE_DB is enabled in the app constants. + * records. * * The function handles database opening and error conditions, providing * detailed logging for debugging purposes. @@ -408,10 +399,6 @@ export async function getSqliteAccounts(): Promise { * ``` */ export async function getDexieAccounts(): Promise { - if (!USE_DEXIE_DB) { - throw new Error("Dexie database is not enabled"); - } - try { const accountsDB = await accountsDBPromise; await accountsDB.open(); @@ -1799,60 +1786,3 @@ export async function migrateAll( return result; } } - -/** - * Test function to verify migration of specific settings fields - * - * This function tests the migration of the specific fields you mentioned: - * firstName, isRegistered, profileImageUrl, showShortcutBvc, and searchBoxes - * - * @returns Promise - */ -export async function testSettingsMigration(): Promise { - logger.info("[MigrationService] Starting settings migration test"); - - try { - // First, compare databases to see current state - const comparison = await compareDatabases(); - logger.info("[MigrationService] Pre-migration comparison:", { - dexieSettings: comparison.dexieSettings.length, - sqliteSettings: comparison.sqliteSettings.length, - dexieAccounts: comparison.dexieAccounts.length, - sqliteAccounts: comparison.sqliteAccounts.length - }); - - // Run settings migration - const settingsResult = await migrateSettings(true); - logger.info("[MigrationService] Settings migration result:", settingsResult); - - // Run accounts migration - const accountsResult = await migrateAccounts(true); - logger.info("[MigrationService] Accounts migration result:", accountsResult); - - // Compare databases again to see changes - const postComparison = await compareDatabases(); - logger.info("[MigrationService] Post-migration comparison:", { - dexieSettings: postComparison.dexieSettings.length, - sqliteSettings: postComparison.sqliteSettings.length, - dexieAccounts: postComparison.dexieAccounts.length, - sqliteAccounts: postComparison.sqliteAccounts.length - }); - - // Check if the specific fields were migrated - if (postComparison.sqliteSettings.length > 0) { - const sqliteSettings = postComparison.sqliteSettings[0]; - logger.info("[MigrationService] Migrated settings fields:", { - firstName: sqliteSettings.firstName, - isRegistered: sqliteSettings.isRegistered, - profileImageUrl: sqliteSettings.profileImageUrl, - showShortcutBvc: sqliteSettings.showShortcutBvc, - searchBoxes: sqliteSettings.searchBoxes - }); - } - - logger.info("[MigrationService] Migration test completed successfully"); - } catch (error) { - logger.error("[MigrationService] Migration test failed:", error); - throw error; - } -} diff --git a/src/views/DatabaseMigration.vue b/src/views/DatabaseMigration.vue index 2335884d..580497e4 100644 --- a/src/views/DatabaseMigration.vue +++ b/src/views/DatabaseMigration.vue @@ -2,7 +2,7 @@
-
+

Database Migration

Compare and migrate data between Dexie (IndexedDB) and SQLite @@ -10,33 +10,34 @@

- -
-
-
- -
-
-

- Dexie Database Disabled +
+ +
+
+

+ Migration Options

-
-

- To use migration features, enable Dexie database by setting - - USE_DEXIE_DB = true - - in - - constants/app.ts - + +

+
+ + +
+ +

+ When enabled, existing records in SQLite will be updated with + data from Dexie. When disabled, existing records will be skipped + during migration.

@@ -44,9 +45,28 @@
-
+
+ + - - - -
@@ -760,45 +762,27 @@
- - -
-
-

- Migration Options -

- -
-
- - -
- -

- When enabled, existing records in SQLite will be updated with - data from Dexie. When disabled, existing records will be skipped - during migration. -

-
-
-

+ + +
+

Exported Data

+ + Copy to Clipboard + +
{{ JSON.stringify(exportedData, null, 2) }}
+
+### Migration Steps - -``` +2. **Database Connection Issues** + - **Error**: "Failed to retrieve data" + - **Solution**: Check database initialization and permissions -## Testing Strategy +3. **Migration Failures** + - **Error**: "Migration failed: [specific error]" + - **Solution**: Review error details and check data integrity -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); - }); - }); - ``` +### Error Recovery -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(); - }); - }); - ``` +1. **Review** error messages carefully +2. **Check** 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 -## Success Criteria +## Best Practices -1. **Data Integrity** - - [ ] All accounts migrated successfully - - [ ] All settings preserved - - [ ] All contacts transferred - - [ ] No data corruption +### Before Migration -2. **Performance** - - [ ] Migration completes within acceptable time - - [ ] No significant performance degradation - - [ ] Efficient storage usage - - [ ] Smooth user experience +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 -3. **Security** - - [ ] Encrypted data remains secure - - [ ] Access controls maintained - - [ ] No sensitive data exposure - - [ ] Secure backup process +### During Migration -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. **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 + +## Performance Considerations + +### 1. Migration Performance +- Use transactions for bulk data transfer +- Implement progress indicators +- Process data in background when possible + +### 2. Application Performance +- Optimize SQLite queries +- Maintain proper database indexes +- Use efficient memory management + +## Security Considerations + +### 1. Data Protection +- Maintain encryption standards across migration +- Preserve user privacy during migration +- Log all migration operations + +### 2. Error Handling +- Handle migration failures gracefully +- Provide clear user messaging +- Maintain rollback capabilities + +## Testing Strategy + +### 1. Migration Testing +```typescript +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 + }); +}); +``` + +### 2. Application Testing +```typescript +describe('Feature with Database', () => { + it('should work with SQLite only', async () => { + // Test with USE_DEXIE_DB = false + // Verify all operations use PlatformService + }); +}); +``` + +## Conclusion + +The migration from Dexie to absurd-sql provides: +- **Better Performance**: Improved query performance and storage efficiency +- **Cross-Platform Consistency**: Unified database interface across platforms +- **Enhanced Security**: Better encryption and access controls +- **Future-Proof Architecture**: Modern SQLite-based storage system + +The migration fence ensures a controlled and safe transition while maintaining data integrity and application stability. \ No newline at end of file From 4d01f64fe749e22f5284dc59add0748e93c47160 Mon Sep 17 00:00:00 2001 From: Matthew Raymer Date: Fri, 20 Jun 2025 04:48:51 +0000 Subject: [PATCH 50/64] feat: Implement activeDid migration from Dexie to SQLite - Add migrateActiveDid() function for dedicated activeDid migration - Enhance migrateSettings() to handle activeDid extraction and validation - Update migrateAll() to include activeDid migration step - Add comprehensive error handling and validation - Update migration documentation with activeDid migration details - Ensure user identity continuity during migration process Files changed: - src/services/indexedDBMigrationService.ts (153 lines added) - doc/migration-to-wa-sqlite.md (documentation updated) Migration order: Accounts -> Settings -> ActiveDid -> Contacts --- doc/migration-to-wa-sqlite.md | 512 +++++++--------------- src/services/indexedDBMigrationService.ts | 153 ++++++- 2 files changed, 312 insertions(+), 353 deletions(-) diff --git a/doc/migration-to-wa-sqlite.md b/doc/migration-to-wa-sqlite.md index 616cf4ea..2249b0df 100644 --- a/doc/migration-to-wa-sqlite.md +++ b/doc/migration-to-wa-sqlite.md @@ -4,7 +4,7 @@ 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. +**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 @@ -12,403 +12,215 @@ This document outlines the migration process from Dexie.js to absurd-sql for the - 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 - -## Migration Fence - -The migration is controlled by a **migration fence** that separates legacy Dexie code from the new SQLite implementation. See [Migration Fence Definition](./migration-fence-definition.md) for complete details. - -### Key Fence Components - -1. **Configuration Control**: `USE_DEXIE_DB = false` (default) -2. **Service Layer**: All database operations go through `PlatformService` -3. **Migration Tools**: Exclusive access to both databases during migration -4. **Code Boundaries**: Clear separation between legacy and new code - -## 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 - -## Current Migration Status - -### ✅ Completed -- **SQLite Database Service**: Fully implemented with absurd-sql -- **Platform Service Layer**: Unified database interface -- **Migration Tools**: Data comparison and transfer utilities -- **Settings Migration**: Core user settings transferred -- **Account Migration**: Identity and key management -- **Schema Migration**: Complete table structure migration - -### 🔄 In Progress -- **Contact Migration**: User contact data (via import interface) -- **Data Verification**: Comprehensive integrity checks -- **Performance Optimization**: Query optimization and indexing - -### 📋 Planned -- **Code Cleanup**: Remove unused Dexie imports -- **Documentation Updates**: Complete migration guides -- **Testing**: Comprehensive migration testing + - 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 -### 1. Preparation +## Migration Architecture -```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'; - -class MigrationService { - 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 - } - }; - } -} -``` +### 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) -### 2. Data Migration +### Migration Order +The migration follows a specific order to maintain data integrity: -```typescript -// src/services/storage/migration/DataMigration.ts -class DataMigration { - async migrateAccounts(): Promise { - const result: MigrationResult = { - success: true, - accountsMigrated: 0, - errors: [], - warnings: [] - }; - - try { - const dexieAccounts = await this.getDexieAccounts(); - - for (const account of dexieAccounts) { - try { - await this.migrateAccount(account); - result.accountsMigrated++; - } catch (error) { - result.errors.push(`Failed to migrate account ${account.did}: ${error}`); - result.success = false; - } - } - } catch (error) { - result.errors.push(`Account migration failed: ${error}`); - result.success = false; - } - - return result; - } - - async migrateSettings(): Promise { - const result: MigrationResult = { - success: true, - settingsMigrated: 0, - errors: [], - warnings: [] - }; - - try { - const dexieSettings = await this.getDexieSettings(); - - for (const setting of dexieSettings) { - try { - await this.migrateSetting(setting); - result.settingsMigrated++; - } catch (error) { - result.errors.push(`Failed to migrate setting ${setting.id}: ${error}`); - result.success = false; - } - } - } catch (error) { - result.errors.push(`Settings migration failed: ${error}`); - result.success = false; - } - - return result; - } - - async migrateContacts(): Promise { - // Contact migration is handled through the contact import interface - // This provides better user control and validation - const result: MigrationResult = { - success: true, - contactsMigrated: 0, - errors: [], - warnings: [] - }; - - try { - const dexieContacts = await this.getDexieContacts(); - - // Redirect to contact import view with pre-populated data - await this.redirectToContactImport(dexieContacts); - - result.contactsMigrated = dexieContacts.length; - } catch (error) { - result.errors.push(`Contact migration failed: ${error}`); - result.success = false; - } - - return result; - } -} -``` +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** -### 3. Verification +### 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 -class MigrationVerification { - async verifyMigration(dexieData: MigrationData): Promise { - // Verify account count - const accountResult = await this.sqliteDB.exec('SELECT COUNT(*) as count FROM accounts'); - const accountCount = accountResult[0].values[0][0]; - if (accountCount !== dexieData.accounts.length) { - return false; - } - - // Verify settings count - const settingsResult = await this.sqliteDB.exec('SELECT COUNT(*) as count FROM settings'); - const settingsCount = settingsResult[0].values[0][0]; - if (settingsCount !== dexieData.settings.length) { - return false; - } - - // Verify data integrity - for (const account of dexieData.accounts) { - const result = await this.sqliteDB.exec( - 'SELECT * FROM accounts WHERE did = ?', - [account.did] - ); - const migratedAccount = result[0]?.values[0]; - if (!migratedAccount || - migratedAccount[1] !== account.publicKeyHex) { - return false; - } - } - - return true; - } +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 }); } ``` -## Using the Migration Interface +#### 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` -### Accessing Migration Tools +#### Updated `migrateAll()` Function +The complete migration now includes a dedicated step for activeDid: +```typescript +// Step 3: Migrate ActiveDid (depends on accounts and settings) +logger.info("[MigrationService] Step 3: Migrating activeDid..."); +const activeDidResult = await migrateActiveDid(); +``` -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 +### 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 Steps +## Migration Process -1. **Compare Databases** - - Click "Compare Databases" to see differences - - Review the comparison results - - Identify data that needs migration +### Phase 1: Preparation ✅ +- [x] Enable Dexie database access +- [x] Implement data comparison tools +- [x] Create migration service structure -2. **Migrate Settings** - - Click "Migrate Settings" to transfer user settings - - Verify settings are correctly transferred - - Check application functionality +### Phase 2: Core Migration ✅ +- [x] Account migration with `importFromMnemonic` +- [x] Settings migration (excluding activeDid) +- [x] **ActiveDid migration** ⭐ **COMPLETED** +- [x] Contact migration framework -3. **Migrate Contacts** - - Click "Migrate Contacts" to open contact import - - Review and confirm contact data - - Complete the import process +### Phase 3: Validation and Cleanup 🔄 +- [ ] Comprehensive data validation +- [ ] Performance testing +- [ ] User acceptance testing +- [ ] Dexie removal -4. **Verify Migration** - - Run comparison again to verify completion - - Test application functionality - - Export backup data if needed +## Usage -## Error Handling +### Manual Migration +```typescript +import { migrateAll, migrateActiveDid } from '../services/indexedDBMigrationService'; -### Common Issues +// Complete migration +const result = await migrateAll(); -1. **Dexie Database Not Enabled** - - **Error**: "Dexie database is not enabled" - - **Solution**: Set `USE_DEXIE_DB = true` in `constants/app.ts` temporarily +// Or migrate just the activeDid +const activeDidResult = await migrateActiveDid(); +``` -2. **Database Connection Issues** - - **Error**: "Failed to retrieve data" - - **Solution**: Check database initialization and permissions +### Migration Verification +```typescript +import { compareDatabases } from '../services/indexedDBMigrationService'; -3. **Migration Failures** - - **Error**: "Migration failed: [specific error]" - - **Solution**: Review error details and check data integrity +const comparison = await compareDatabases(); +console.log('Migration differences:', comparison.differences); +``` -### Error Recovery +## Error Handling -1. **Review** error messages carefully -2. **Check** 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 +### ActiveDid Migration Errors +- **Missing Account**: If the `activeDid` from Dexie doesn't exist in SQLite accounts +- **Database Errors**: Connection or query failures +- **Settings Update Failures**: Issues updating SQLite master settings -## Best Practices +### Recovery Strategies +1. **Automatic Recovery**: Migration continues even if activeDid migration fails +2. **Manual Recovery**: Users can manually select their identity after migration +3. **Fallback**: System creates new identity if none exists -### Before Migration +## Security Considerations -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 +### Data Protection +- All sensitive data (mnemonics, private keys) are encrypted +- Migration preserves encryption standards +- No plaintext data exposure during migration -### During Migration +### Identity Verification +- ActiveDid migration validates account existence +- Prevents setting non-existent identities as active +- Maintains cryptographic integrity -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 +## Testing -### After Migration +### Migration Testing +```bash +# Enable Dexie for testing +# Set USE_DEXIE_DB = true in constants/app.ts -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 +# Run migration +npm run migrate -## Performance Considerations +# Verify results +npm run test:migration +``` -### 1. Migration Performance -- Use transactions for bulk data transfer -- Implement progress indicators -- Process data in background when possible +### ActiveDid Testing +```typescript +// Test activeDid migration specifically +const result = await migrateActiveDid(); +expect(result.success).toBe(true); +expect(result.warnings).toContain('Successfully migrated activeDid'); +``` -### 2. Application Performance -- Optimize SQLite queries -- Maintain proper database indexes -- Use efficient memory management +## Troubleshooting -## Security Considerations +### Common Issues -### 1. Data Protection -- Maintain encryption standards across migration -- Preserve user privacy during migration -- Log all migration operations +1. **ActiveDid Not Found** + - Ensure accounts were migrated before activeDid migration + - Check that the Dexie activeDid exists in SQLite accounts -### 2. Error Handling -- Handle migration failures gracefully -- Provide clear user messaging -- Maintain rollback capabilities +2. **Migration Failures** + - Verify Dexie database is accessible + - Check SQLite database permissions + - Review migration logs for specific errors -## Testing Strategy +3. **Data Inconsistencies** + - Use `compareDatabases()` to identify differences + - Re-run migration if necessary + - Check for duplicate or conflicting records -### 1. Migration Testing +### Debugging ```typescript -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 - }); -}); -``` +// Enable detailed logging +logger.setLevel('debug'); -### 2. Application Testing -```typescript -describe('Feature with Database', () => { - it('should work with SQLite only', async () => { - // Test with USE_DEXIE_DB = false - // Verify all operations use PlatformService - }); -}); +// 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 migration from Dexie to absurd-sql provides: -- **Better Performance**: Improved query performance and storage efficiency -- **Cross-Platform Consistency**: Unified database interface across platforms -- **Enhanced Security**: Better encryption and access controls -- **Future-Proof Architecture**: Modern SQLite-based storage system +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 ensures a controlled and safe transition while maintaining data integrity and application stability. \ No newline at end of file +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/src/services/indexedDBMigrationService.ts b/src/services/indexedDBMigrationService.ts index 1eb33a1e..2cf4441d 100644 --- a/src/services/indexedDBMigrationService.ts +++ b/src/services/indexedDBMigrationService.ts @@ -39,6 +39,7 @@ import { generateUpdateStatement, generateInsertStatement, } from "../db/databaseUtil"; +import { updateDefaultSettings } from "../db/databaseUtil"; import { importFromMnemonic } from "../libs/util"; /** @@ -1080,6 +1081,17 @@ export async function migrateSettings(): Promise { }); const platformService = PlatformServiceFactory.getInstance(); + // Find the master settings (accountDid is null) which contains the activeDid + const masterSettings = dexieSettings.find(setting => !setting.accountDid); + let dexieActiveDid: string | undefined; + + if (masterSettings?.activeDid) { + dexieActiveDid = masterSettings.activeDid; + logger.info("[MigrationService] Found activeDid in Dexie master settings", { + activeDid: dexieActiveDid, + }); + } + // Create an array of promises for all settings migrations const migrationPromises = dexieSettings.map(async (setting) => { logger.info("[MigrationService] Starting to migrate settings", setting); @@ -1139,6 +1151,38 @@ export async function migrateSettings(): Promise { // Wait for all migrations to complete const updatedSettings = await Promise.all(migrationPromises); + // Step 2: Migrate the activeDid if it exists in Dexie + if (dexieActiveDid) { + try { + // Verify that the activeDid exists in SQLite accounts + const accountExists = await platformService.dbQuery( + "SELECT did FROM accounts WHERE did = ?", + [dexieActiveDid], + ); + + if (accountExists?.values?.length) { + // Update the master settings with the activeDid + await updateDefaultSettings({ activeDid: dexieActiveDid }); + logger.info("[MigrationService] Successfully migrated activeDid", { + activeDid: dexieActiveDid, + }); + result.warnings.push(`Migrated activeDid: ${dexieActiveDid}`); + } else { + logger.warn("[MigrationService] activeDid from Dexie not found in SQLite accounts", { + activeDid: dexieActiveDid, + }); + result.warnings.push( + `activeDid from Dexie (${dexieActiveDid}) not found in SQLite accounts - skipping activeDid migration`, + ); + } + } catch (error) { + logger.error("[MigrationService] Failed to migrate activeDid:", error); + result.errors.push(`Failed to migrate activeDid: ${error}`); + } + } else { + logger.info("[MigrationService] No activeDid found in Dexie settings"); + } + logger.info( "[MigrationService] Finished migrating settings", updatedSettings, @@ -1279,6 +1323,96 @@ export async function migrateAccounts(): Promise { } } +/** + * Migrates the activeDid from Dexie to SQLite + * + * This function specifically handles the migration of the activeDid setting + * from the Dexie database to the SQLite database. It ensures that the + * activeDid exists in the SQLite accounts table before setting it as active. + * + * The function is designed to be called after accounts have been migrated + * to ensure the target DID exists in the SQLite database. + * + * @async + * @function migrateActiveDid + * @returns {Promise} Result of the activeDid migration + * @throws {Error} If the migration process fails + * @example + * ```typescript + * try { + * const result = await migrateActiveDid(); + * if (result.success) { + * console.log('ActiveDid migration successful'); + * } else { + * console.error('ActiveDid migration failed:', result.errors); + * } + * } catch (error) { + * console.error('ActiveDid migration process failed:', error); + * } + * ``` + */ +export async function migrateActiveDid(): Promise { + logger.info("[MigrationService] Starting activeDid migration"); + + const result: MigrationResult = { + success: true, + contactsMigrated: 0, + settingsMigrated: 0, + accountsMigrated: 0, + errors: [], + warnings: [], + }; + + try { + // Get Dexie settings to find the activeDid + const dexieSettings = await getDexieSettings(); + const masterSettings = dexieSettings.find(setting => !setting.accountDid); + + if (!masterSettings?.activeDid) { + logger.info("[MigrationService] No activeDid found in Dexie master settings"); + result.warnings.push("No activeDid found in Dexie settings"); + return result; + } + + const dexieActiveDid = masterSettings.activeDid; + logger.info("[MigrationService] Found activeDid in Dexie", { + activeDid: dexieActiveDid, + }); + + const platformService = PlatformServiceFactory.getInstance(); + + // Verify that the activeDid exists in SQLite accounts + const accountExists = await platformService.dbQuery( + "SELECT did FROM accounts WHERE did = ?", + [dexieActiveDid], + ); + + if (!accountExists?.values?.length) { + const errorMessage = `activeDid from Dexie (${dexieActiveDid}) not found in SQLite accounts`; + logger.error("[MigrationService]", errorMessage); + result.errors.push(errorMessage); + result.success = false; + return result; + } + + // Update the master settings with the activeDid + await updateDefaultSettings({ activeDid: dexieActiveDid }); + + logger.info("[MigrationService] Successfully migrated activeDid", { + activeDid: dexieActiveDid, + }); + result.warnings.push(`Successfully migrated activeDid: ${dexieActiveDid}`); + + return result; + } catch (error) { + const errorMessage = `ActiveDid migration failed: ${error}`; + logger.error("[MigrationService]", errorMessage, error); + result.errors.push(errorMessage); + result.success = false; + return result; + } +} + /** * Migrates all data from Dexie to SQLite in the proper order * @@ -1286,7 +1420,8 @@ export async function migrateAccounts(): Promise { * in the correct order to avoid foreign key constraint issues: * 1. Accounts (foundational - contains DIDs) * 2. Settings (references accountDid, activeDid) - * 3. Contacts (independent, but migrated after accounts for consistency) + * 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. @@ -1332,9 +1467,21 @@ export async function migrateAll(): Promise { result.settingsMigrated = settingsResult.settingsMigrated; result.warnings.push(...settingsResult.warnings); - // Step 3: Migrate Contacts (independent, but after accounts for consistency) + // Step 3: Migrate ActiveDid (depends on accounts and settings) + logger.info("[MigrationService] Step 3: Migrating activeDid..."); + const activeDidResult = await migrateActiveDid(); + if (!activeDidResult.success) { + result.errors.push( + `ActiveDid migration failed: ${activeDidResult.errors.join(", ")}`, + ); + // Don't fail the entire migration for activeDid issues + logger.warn("[MigrationService] ActiveDid migration failed, but continuing with migration"); + } + result.warnings.push(...activeDidResult.warnings); + + // Step 4: Migrate Contacts (independent, but after accounts for consistency) // ... but which is better done through the contact import view - // logger.info("[MigrationService] Step 3: Migrating contacts..."); + // logger.info("[MigrationService] Step 4: Migrating contacts..."); // const contactsResult = await migrateContacts(); // if (!contactsResult.success) { // result.errors.push( From 0cf5cf266d7c65492e7bf75cc33edb80626b5b72 Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Fri, 20 Jun 2025 06:26:56 -0600 Subject: [PATCH 51/64] IndexedDB migration: don't run activeDid migration twice, include warnings in output, don't automatically compare afterward --- src/services/indexedDBMigrationService.ts | 54 +---------------------- src/views/DatabaseMigration.vue | 35 ++++++++++++--- 2 files changed, 30 insertions(+), 59 deletions(-) diff --git a/src/services/indexedDBMigrationService.ts b/src/services/indexedDBMigrationService.ts index 2cf4441d..27688fcb 100644 --- a/src/services/indexedDBMigrationService.ts +++ b/src/services/indexedDBMigrationService.ts @@ -1081,17 +1081,6 @@ export async function migrateSettings(): Promise { }); const platformService = PlatformServiceFactory.getInstance(); - // Find the master settings (accountDid is null) which contains the activeDid - const masterSettings = dexieSettings.find(setting => !setting.accountDid); - let dexieActiveDid: string | undefined; - - if (masterSettings?.activeDid) { - dexieActiveDid = masterSettings.activeDid; - logger.info("[MigrationService] Found activeDid in Dexie master settings", { - activeDid: dexieActiveDid, - }); - } - // Create an array of promises for all settings migrations const migrationPromises = dexieSettings.map(async (setting) => { logger.info("[MigrationService] Starting to migrate settings", setting); @@ -1152,36 +1141,7 @@ export async function migrateSettings(): Promise { const updatedSettings = await Promise.all(migrationPromises); // Step 2: Migrate the activeDid if it exists in Dexie - if (dexieActiveDid) { - try { - // Verify that the activeDid exists in SQLite accounts - const accountExists = await platformService.dbQuery( - "SELECT did FROM accounts WHERE did = ?", - [dexieActiveDid], - ); - - if (accountExists?.values?.length) { - // Update the master settings with the activeDid - await updateDefaultSettings({ activeDid: dexieActiveDid }); - logger.info("[MigrationService] Successfully migrated activeDid", { - activeDid: dexieActiveDid, - }); - result.warnings.push(`Migrated activeDid: ${dexieActiveDid}`); - } else { - logger.warn("[MigrationService] activeDid from Dexie not found in SQLite accounts", { - activeDid: dexieActiveDid, - }); - result.warnings.push( - `activeDid from Dexie (${dexieActiveDid}) not found in SQLite accounts - skipping activeDid migration`, - ); - } - } catch (error) { - logger.error("[MigrationService] Failed to migrate activeDid:", error); - result.errors.push(`Failed to migrate activeDid: ${error}`); - } - } else { - logger.info("[MigrationService] No activeDid found in Dexie settings"); - } + await migrateActiveDid(); logger.info( "[MigrationService] Finished migrating settings", @@ -1467,18 +1427,6 @@ export async function migrateAll(): Promise { result.settingsMigrated = settingsResult.settingsMigrated; result.warnings.push(...settingsResult.warnings); - // Step 3: Migrate ActiveDid (depends on accounts and settings) - logger.info("[MigrationService] Step 3: Migrating activeDid..."); - const activeDidResult = await migrateActiveDid(); - if (!activeDidResult.success) { - result.errors.push( - `ActiveDid migration failed: ${activeDidResult.errors.join(", ")}`, - ); - // Don't fail the entire migration for activeDid issues - logger.warn("[MigrationService] ActiveDid migration failed, but continuing with migration"); - } - result.warnings.push(...activeDidResult.warnings); - // Step 4: Migrate Contacts (independent, but after accounts for consistency) // ... but which is better done through the contact import view // logger.info("[MigrationService] Step 4: Migrating contacts..."); diff --git a/src/views/DatabaseMigration.vue b/src/views/DatabaseMigration.vue index d5b6ecd8..a0efcb06 100644 --- a/src/views/DatabaseMigration.vue +++ b/src/views/DatabaseMigration.vue @@ -200,6 +200,28 @@
+ +
+
+
+ +
+
+

Warning

+
+

{{ warning }}

+
+
+
+
+
| null = null; private successMessage = ""; @@ -1248,6 +1271,7 @@ export default class DatabaseMigration extends Vue { this.successMessage = `Successfully migrated ${totalMigrated} total records: ${result.accountsMigrated} accounts, ${result.settingsMigrated} settings, ${result.contactsMigrated} contacts.`; if (result.warnings.length > 0) { this.successMessage += ` ${result.warnings.length} warnings.`; + this.warning += result.warnings.join(", "); } this.successMessage += " Now finish by migrating contacts."; logger.info( @@ -1255,8 +1279,7 @@ export default class DatabaseMigration extends Vue { result, ); - // Refresh comparison data after successful migration - this.comparison = await compareDatabases(); + this.comparison = null; } else { this.error = `Migration failed: ${result.errors.join(", ")}`; logger.error( @@ -1342,14 +1365,14 @@ export default class DatabaseMigration extends Vue { this.successMessage = `Successfully migrated ${result.settingsMigrated} settings.`; if (result.warnings.length > 0) { this.successMessage += ` ${result.warnings.length} warnings.`; + this.warning += result.warnings.join(", "); } logger.info( "[DatabaseMigration] Settings migration completed successfully", result, ); - // Refresh comparison data after successful migration - this.comparison = await compareDatabases(); + this.comparison = null; } else { this.error = `Migration failed: ${result.errors.join(", ")}`; logger.error( @@ -1385,14 +1408,14 @@ export default class DatabaseMigration extends Vue { this.successMessage = `Successfully migrated ${result.accountsMigrated} accounts.`; if (result.warnings.length > 0) { this.successMessage += ` ${result.warnings.length} warnings.`; + this.warning += result.warnings.join(", "); } logger.info( "[DatabaseMigration] Account migration completed successfully", result, ); - // Refresh comparison data after successful migration - this.comparison = await compareDatabases(); + this.comparison = null; } else { this.error = `Migration failed: ${result.errors.join(", ")}`; logger.error( From ab2270d8b20bb8a05dfce63261f2afaebf9fa9b1 Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Fri, 20 Jun 2025 06:51:33 -0600 Subject: [PATCH 52/64] IndexedDB migration: fix where the existing settings (eg. master) were not updated --- src/services/indexedDBMigrationService.ts | 140 ++++------------------ 1 file changed, 23 insertions(+), 117 deletions(-) diff --git a/src/services/indexedDBMigrationService.ts b/src/services/indexedDBMigrationService.ts index 27688fcb..84755541 100644 --- a/src/services/indexedDBMigrationService.ts +++ b/src/services/indexedDBMigrationService.ts @@ -1087,49 +1087,48 @@ export async function migrateSettings(): Promise { let sqliteSettingRaw: | { columns: string[]; values: unknown[][] } | undefined; + + // adjust SQL based on the accountDid key, maybe null + let conditional: string; + let preparams: unknown[]; if (!setting.accountDid) { - sqliteSettingRaw = await platformService.dbQuery( - "SELECT * FROM settings WHERE accountDid is null", - ); + conditional = "accountDid is null"; + preparams = []; } else { - sqliteSettingRaw = await platformService.dbQuery( - "SELECT * FROM settings WHERE accountDid = ?", - [setting.accountDid], - ); + conditional = "accountDid = ?"; + preparams = [setting.accountDid]; } + sqliteSettingRaw = await platformService.dbQuery( + "SELECT * FROM settings WHERE " + conditional, + preparams, + ); + logger.info("[MigrationService] Migrating one set of settings:", { setting, sqliteSettingRaw, }); if (sqliteSettingRaw?.values?.length) { - // should cover the master settings, were accountDid is null - const sqliteSettings = mapColumnsToValues( - sqliteSettingRaw.columns, - sqliteSettingRaw.values, - ) as unknown as Settings[]; - const sqliteSetting = sqliteSettings[0]; - let conditional: string; - let preparams: unknown[]; - if (!setting.accountDid) { - conditional = "accountDid is null"; - preparams = []; - } else { - conditional = "accountDid = ?"; - preparams = [setting.accountDid]; - } + // 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( - sqliteSetting as unknown as Record, + setting, "settings", conditional, preparams, ); + logger.info("[MigrationService] 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 as unknown as Record, + setting, "settings", ); await platformService.dbExec(sql, params); @@ -1140,9 +1139,6 @@ export async function migrateSettings(): Promise { // Wait for all migrations to complete const updatedSettings = await Promise.all(migrationPromises); - // Step 2: Migrate the activeDid if it exists in Dexie - await migrateActiveDid(); - logger.info( "[MigrationService] Finished migrating settings", updatedSettings, @@ -1283,96 +1279,6 @@ export async function migrateAccounts(): Promise { } } -/** - * Migrates the activeDid from Dexie to SQLite - * - * This function specifically handles the migration of the activeDid setting - * from the Dexie database to the SQLite database. It ensures that the - * activeDid exists in the SQLite accounts table before setting it as active. - * - * The function is designed to be called after accounts have been migrated - * to ensure the target DID exists in the SQLite database. - * - * @async - * @function migrateActiveDid - * @returns {Promise} Result of the activeDid migration - * @throws {Error} If the migration process fails - * @example - * ```typescript - * try { - * const result = await migrateActiveDid(); - * if (result.success) { - * console.log('ActiveDid migration successful'); - * } else { - * console.error('ActiveDid migration failed:', result.errors); - * } - * } catch (error) { - * console.error('ActiveDid migration process failed:', error); - * } - * ``` - */ -export async function migrateActiveDid(): Promise { - logger.info("[MigrationService] Starting activeDid migration"); - - const result: MigrationResult = { - success: true, - contactsMigrated: 0, - settingsMigrated: 0, - accountsMigrated: 0, - errors: [], - warnings: [], - }; - - try { - // Get Dexie settings to find the activeDid - const dexieSettings = await getDexieSettings(); - const masterSettings = dexieSettings.find(setting => !setting.accountDid); - - if (!masterSettings?.activeDid) { - logger.info("[MigrationService] No activeDid found in Dexie master settings"); - result.warnings.push("No activeDid found in Dexie settings"); - return result; - } - - const dexieActiveDid = masterSettings.activeDid; - logger.info("[MigrationService] Found activeDid in Dexie", { - activeDid: dexieActiveDid, - }); - - const platformService = PlatformServiceFactory.getInstance(); - - // Verify that the activeDid exists in SQLite accounts - const accountExists = await platformService.dbQuery( - "SELECT did FROM accounts WHERE did = ?", - [dexieActiveDid], - ); - - if (!accountExists?.values?.length) { - const errorMessage = `activeDid from Dexie (${dexieActiveDid}) not found in SQLite accounts`; - logger.error("[MigrationService]", errorMessage); - result.errors.push(errorMessage); - result.success = false; - return result; - } - - // Update the master settings with the activeDid - await updateDefaultSettings({ activeDid: dexieActiveDid }); - - logger.info("[MigrationService] Successfully migrated activeDid", { - activeDid: dexieActiveDid, - }); - result.warnings.push(`Successfully migrated activeDid: ${dexieActiveDid}`); - - return result; - } catch (error) { - const errorMessage = `ActiveDid migration failed: ${error}`; - logger.error("[MigrationService]", errorMessage, error); - result.errors.push(errorMessage); - result.success = false; - return result; - } -} - /** * Migrates all data from Dexie to SQLite in the proper order * From 9b69c0b22c7e3ac0584219f5ac434a02bda2e01b Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Fri, 20 Jun 2025 10:41:48 -0600 Subject: [PATCH 53/64] bump to version 0.5.9 --- BUILDING.md | 2 +- CHANGELOG.md | 5 +++++ package-lock.json | 4 ++-- package.json | 2 +- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/BUILDING.md b/BUILDING.md index 0ab17341..4d7f196b 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -71,7 +71,7 @@ Install dependencies: * 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: diff --git a/CHANGELOG.md b/CHANGELOG.md index 71657a57..15d192e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 +## [0.5.9] +### Added +- Migration from IndexedDB to SQLite + + ## [0.4.7] ### Fixed - Cameras everywhere diff --git a/package-lock.json b/package-lock.json index edcd2365..e47c74de 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "timesafari", - "version": "0.5.6", + "version": "0.5.9", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "timesafari", - "version": "0.5.6", + "version": "0.5.9", "dependencies": { "@capacitor-community/sqlite": "6.0.2", "@capacitor-mlkit/barcode-scanning": "^6.0.0", diff --git a/package.json b/package.json index 935ece64..5ab72276 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "timesafari", - "version": "0.5.6", + "version": "0.5.9", "description": "Time Safari Application", "author": { "name": "Time Safari Team" From 6f2272eea72fa83312c891b78a601f911ec02024 Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Fri, 20 Jun 2025 11:11:33 -0600 Subject: [PATCH 54/64] fix problem where prod users don't see other DB options --- src/views/HomeView.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/views/HomeView.vue b/src/views/HomeView.vue index 70a569a2..32a8e7d7 100644 --- a/src/views/HomeView.vue +++ b/src/views/HomeView.vue @@ -106,7 +106,7 @@ Raymer * @version 1.0.0 */
-
+
Date: Fri, 20 Jun 2025 11:20:57 -0600 Subject: [PATCH 55/64] bump to version 1.0.0 --- BUILDING.md | 6 +++--- CHANGELOG.md | 4 ++-- package-lock.json | 4 ++-- package.json | 2 +- src/views/HomeView.vue | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/BUILDING.md b/BUILDING.md index 4d7f196b..57fb686c 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -64,9 +64,9 @@ Install dependencies: * 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.5.8 && git push origin 0.5.8`. +* Tag with the new version, [online](https://gitea.anomalistdesign.com/trent_larson/crowd-funder-for-time-pwa/releases) or `git tag 1.0.0 && git push origin 1.0.0`. * For test, build the app (because test server is not yet set up to build): @@ -90,7 +90,7 @@ 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.5.8 && npm install && npm run build:web && cd -` + * `cd crowd-funder-for-time-pwa && git checkout master && git pull && git checkout 0.5.9 && npm install && npm run build:web && cd -` (The plain `npm run build:web` uses the .env.production file.) diff --git a/CHANGELOG.md b/CHANGELOG.md index 15d192e2..4f0c9acf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,9 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 -## [0.5.9] +## [1.0.0] - 2025.06.20 - 9b69c0b22c7e3ac0584219f5ac434a02bda2e01b ### Added -- Migration from IndexedDB to SQLite +- Web-oriented migration from IndexedDB to SQLite ## [0.4.7] diff --git a/package-lock.json b/package-lock.json index e47c74de..cf836ebd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "timesafari", - "version": "0.5.9", + "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "timesafari", - "version": "0.5.9", + "version": "1.0.0", "dependencies": { "@capacitor-community/sqlite": "6.0.2", "@capacitor-mlkit/barcode-scanning": "^6.0.0", diff --git a/package.json b/package.json index 5ab72276..527b91bd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "timesafari", - "version": "0.5.9", + "version": "1.0.0", "description": "Time Safari Application", "author": { "name": "Time Safari Team" diff --git a/src/views/HomeView.vue b/src/views/HomeView.vue index 32a8e7d7..226a8178 100644 --- a/src/views/HomeView.vue +++ b/src/views/HomeView.vue @@ -111,7 +111,7 @@ Raymer * @version 1.0.0 */ :to="{ name: 'start' }" class="block text-right text-md font-bold bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mt-2 px-2 py-3 rounded-md" > - See all your options first + See advanced options
From 73733345ffa86e2fa8e47b232b2688f08e1dd936 Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Fri, 20 Jun 2025 11:46:09 -0600 Subject: [PATCH 56/64] bump to version 1.0.0-beta --- BUILDING.md | 6 +++--- CHANGELOG.md | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/BUILDING.md b/BUILDING.md index 57fb686c..f97be978 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -90,13 +90,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.5.9 && npm install && npm run build:web && cd -` + * `cd crowd-funder-for-time-pwa && git checkout master && git pull && git checkout 1.0.0 && npm install && npm run build:web && cd -` (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-1 && 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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f0c9acf..8cba8577 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 -## [1.0.0] - 2025.06.20 - 9b69c0b22c7e3ac0584219f5ac434a02bda2e01b +## [1.0.0] - 2025.06.20 - 5aa693de6337e5dbb278bfddc6bd39094bc14f73 ### Added - Web-oriented migration from IndexedDB to SQLite diff --git a/package-lock.json b/package-lock.json index cf836ebd..f7ce5fdd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "timesafari", - "version": "1.0.0", + "version": "1.0.1-beta", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "timesafari", - "version": "1.0.0", + "version": "1.0.1-beta", "dependencies": { "@capacitor-community/sqlite": "6.0.2", "@capacitor-mlkit/barcode-scanning": "^6.0.0", diff --git a/package.json b/package.json index 527b91bd..3cbb4506 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "timesafari", - "version": "1.0.0", + "version": "1.0.1-beta", "description": "Time Safari Application", "author": { "name": "Time Safari Team" From bb6eb92ba14a3bc6b2da671499fef70cf8fb2d98 Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Fri, 20 Jun 2025 13:34:14 -0600 Subject: [PATCH 57/64] fix ? instead of 0 in rate limits, update location verbiage --- src/views/AccountViewView.vue | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/views/AccountViewView.vue b/src/views/AccountViewView.vue index c3fee747..be8a132e 100644 --- a/src/views/AccountViewView.vue +++ b/src/views/AccountViewView.vue @@ -349,8 +349,8 @@

- 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 +449,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 +467,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) From 838723c26b82df46551c3360d77e6031c7791d2d Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Fri, 20 Jun 2025 14:01:08 -0600 Subject: [PATCH 58/64] remove debugging info messages (change to debug if we want these -- and tell us how to turn off debug locally) --- src/main.common.ts | 25 ++----------------------- src/main.web.ts | 5 +---- src/services/migrationService.ts | 17 ----------------- src/utils/logger.ts | 14 ++++---------- 4 files changed, 7 insertions(+), 54 deletions(-) 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/services/migrationService.ts b/src/services/migrationService.ts index 00587967..089c7d65 100644 --- a/src/services/migrationService.ts +++ b/src/services/migrationService.ts @@ -25,7 +25,6 @@ class MigrationRegistry { */ registerMigration(migration: Migration): void { this.migrations.push(migration); - logger.info(`[MigrationService] Registered migration: ${migration.name}`); } /** @@ -42,7 +41,6 @@ class MigrationRegistry { */ clearMigrations(): void { this.migrations = []; - logger.info("[MigrationService] Cleared all registered migrations"); } } @@ -94,10 +92,6 @@ export async function runMigrations( ); const appliedMigrations = extractMigrationNames(appliedMigrationsResult); - logger.info( - `[MigrationService] Found ${appliedMigrations.size} applied migrations`, - ); - // Get all registered migrations const migrations = migrationRegistry.getMigrations(); @@ -106,21 +100,12 @@ export async function runMigrations( return; } - logger.info( - `[MigrationService] Running ${migrations.length} registered migrations`, - ); - // Run each migration that hasn't been applied yet for (const migration of migrations) { if (appliedMigrations.has(migration.name)) { - logger.info( - `[MigrationService] Skipping already applied migration: ${migration.name}`, - ); continue; } - logger.info(`[MigrationService] Applying migration: ${migration.name}`); - try { // Execute the migration SQL await sqlExec(migration.sql); @@ -141,8 +126,6 @@ export async function runMigrations( throw new Error(`Migration ${migration.name} failed: ${error}`); } } - - logger.info("[MigrationService] All migrations completed successfully"); } catch (error) { logger.error("[MigrationService] Migration process failed:", error); throw error; diff --git a/src/utils/logger.ts b/src/utils/logger.ts index 89425d77..8184c28b 100644 --- a/src/utils/logger.ts +++ b/src/utils/logger.ts @@ -52,16 +52,10 @@ 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 From 94994a725154734ff423feec9d0c3bd9de8084f2 Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Fri, 20 Jun 2025 15:53:31 -0600 Subject: [PATCH 59/64] allow blocking another person's content from this user (with iViewContent contact field) --- src/db-sql/migration.ts | 7 +- src/db/tables/contacts.ts | 19 +++- src/libs/fontawesome.ts | 4 +- src/libs/util.ts | 25 ++--- src/services/indexedDBMigrationService.ts | 128 +++++++++++++--------- src/views/AccountViewView.vue | 5 +- src/views/DIDView.vue | 91 ++++++++++++++- src/views/DatabaseMigration.vue | 4 + src/views/HomeView.vue | 23 ++-- 9 files changed, 215 insertions(+), 91 deletions(-) diff --git a/src/db-sql/migration.ts b/src/db-sql/migration.ts index b6cbe17c..e06636bd 100644 --- a/src/db-sql/migration.ts +++ b/src/db-sql/migration.ts @@ -34,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, @@ -119,6 +118,12 @@ const MIGRATIONS = [ ); `, }, + { + name: "002_add_iViewContent_to_contacts", + sql: ` + ALTER TABLE contacts ADD COLUMN iViewContent BOOLEAN DEFAULT TRUE; + `, + }, ]; /** diff --git a/src/db/tables/contacts.ts b/src/db/tables/contacts.ts index a8f763f3..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,9 +18,15 @@ export interface Contact { publicKeyBase64?: string; seesMe?: boolean; // cached value of the server setting registered?: boolean; // cached value of the server setting -} +}; -export type ContactWithJsonStrings = Contact & { +/** + * 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; }; diff --git a/src/libs/fontawesome.ts b/src/libs/fontawesome.ts index 37b5343c..b1768d38 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, @@ -92,8 +92,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 a95a3e4a..17ba3c8e 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 { @@ -974,19 +974,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: { diff --git a/src/services/indexedDBMigrationService.ts b/src/services/indexedDBMigrationService.ts index 84755541..88fb9d09 100644 --- a/src/services/indexedDBMigrationService.ts +++ b/src/services/indexedDBMigrationService.ts @@ -39,7 +39,6 @@ import { generateUpdateStatement, generateInsertStatement, } from "../db/databaseUtil"; -import { updateDefaultSettings } from "../db/databaseUtil"; import { importFromMnemonic } from "../libs/util"; /** @@ -156,11 +155,14 @@ export async function getDexieContacts(): Promise { await db.open(); const contacts = await db.contacts.toArray(); logger.info( - `[MigrationService] Retrieved ${contacts.length} contacts from Dexie`, + `[IndexedDBMigrationService] Retrieved ${contacts.length} contacts from Dexie`, ); return contacts; } catch (error) { - logger.error("[MigrationService] Error retrieving Dexie contacts:", error); + logger.error( + "[IndexedDBMigrationService] Error retrieving Dexie contacts:", + error, + ); throw new Error(`Failed to retrieve Dexie contacts: ${error}`); } } @@ -214,11 +216,14 @@ export async function getSqliteContacts(): Promise { } logger.info( - `[MigrationService] Retrieved ${contacts.length} contacts from SQLite`, + `[IndexedDBMigrationService] Retrieved ${contacts.length} contacts from SQLite`, ); return contacts; } catch (error) { - logger.error("[MigrationService] Error retrieving SQLite contacts:", error); + logger.error( + "[IndexedDBMigrationService] Error retrieving SQLite contacts:", + error, + ); throw new Error(`Failed to retrieve SQLite contacts: ${error}`); } } @@ -251,11 +256,14 @@ export async function getDexieSettings(): Promise { await db.open(); const settings = await db.settings.toArray(); logger.info( - `[MigrationService] Retrieved ${settings.length} settings from Dexie`, + `[IndexedDBMigrationService] Retrieved ${settings.length} settings from Dexie`, ); return settings; } catch (error) { - logger.error("[MigrationService] Error retrieving Dexie settings:", error); + logger.error( + "[IndexedDBMigrationService] Error retrieving Dexie settings:", + error, + ); throw new Error(`Failed to retrieve Dexie settings: ${error}`); } } @@ -309,11 +317,14 @@ export async function getSqliteSettings(): Promise { } logger.info( - `[MigrationService] Retrieved ${settings.length} settings from SQLite`, + `[IndexedDBMigrationService] Retrieved ${settings.length} settings from SQLite`, ); return settings; } catch (error) { - logger.error("[MigrationService] Error retrieving SQLite settings:", error); + logger.error( + "[IndexedDBMigrationService] Error retrieving SQLite settings:", + error, + ); throw new Error(`Failed to retrieve SQLite settings: ${error}`); } } @@ -353,11 +364,14 @@ export async function getSqliteAccounts(): Promise { } logger.info( - `[MigrationService] Retrieved ${dids.length} accounts from SQLite`, + `[IndexedDBMigrationService] Retrieved ${dids.length} accounts from SQLite`, ); return dids; } catch (error) { - logger.error("[MigrationService] Error retrieving SQLite accounts:", error); + logger.error( + "[IndexedDBMigrationService] Error retrieving SQLite accounts:", + error, + ); throw new Error(`Failed to retrieve SQLite accounts: ${error}`); } } @@ -391,11 +405,14 @@ export async function getDexieAccounts(): Promise { await accountsDB.open(); const accounts = await accountsDB.accounts.toArray(); logger.info( - `[MigrationService] Retrieved ${accounts.length} accounts from Dexie`, + `[IndexedDBMigrationService] Retrieved ${accounts.length} accounts from Dexie`, ); return accounts; } catch (error) { - logger.error("[MigrationService] Error retrieving Dexie accounts:", error); + logger.error( + "[IndexedDBMigrationService] Error retrieving Dexie accounts:", + error, + ); throw new Error(`Failed to retrieve Dexie accounts: ${error}`); } } @@ -429,7 +446,7 @@ export async function getDexieAccounts(): Promise { * ``` */ export async function compareDatabases(): Promise { - logger.info("[MigrationService] Starting database comparison"); + logger.info("[IndexedDBMigrationService] Starting database comparison"); const [ dexieContacts, @@ -470,7 +487,7 @@ export async function compareDatabases(): Promise { }, }; - logger.info("[MigrationService] Database comparison completed", { + logger.info("[IndexedDBMigrationService] Database comparison completed", { dexieContacts: dexieContacts.length, sqliteContacts: sqliteContacts.length, dexieSettings: dexieSettings.length, @@ -679,6 +696,7 @@ function compareAccounts(dexieAccounts: Account[], sqliteDids: string[]) { * ``` */ 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 && @@ -954,7 +972,7 @@ export function generateComparisonYaml(comparison: DataComparison): string { export async function migrateContacts( overwriteExisting: boolean = false, ): Promise { - logger.info("[MigrationService] Starting contact migration", { + logger.info("[IndexedDBMigrationService] Starting contact migration", { overwriteExisting, }); @@ -990,7 +1008,7 @@ export async function migrateContacts( ); await platformService.dbExec(sql, params); result.contactsMigrated++; - logger.info(`[MigrationService] Updated contact: ${contact.did}`); + logger.info(`[IndexedDBMigrationService] Updated contact: ${contact.did}`); } else { result.warnings.push( `Contact ${contact.did} already exists, skipping`, @@ -1004,17 +1022,17 @@ export async function migrateContacts( ); await platformService.dbExec(sql, params); result.contactsMigrated++; - logger.info(`[MigrationService] Added contact: ${contact.did}`); + logger.info(`[IndexedDBMigrationService] Added contact: ${contact.did}`); } } catch (error) { const errorMsg = `Failed to migrate contact ${contact.did}: ${error}`; - logger.error("[MigrationService]", errorMsg); + logger.error("[IndexedDBMigrationService]", errorMsg); result.errors.push(errorMsg); result.success = false; } } - logger.info("[MigrationService] Contact migration completed", { + logger.info("[IndexedDBMigrationService] Contact migration completed", { contactsMigrated: result.contactsMigrated, errors: result.errors.length, warnings: result.warnings.length, @@ -1023,7 +1041,7 @@ export async function migrateContacts( return result; } catch (error) { const errorMsg = `Contact migration failed: ${error}`; - logger.error("[MigrationService]", errorMsg); + logger.error("[IndexedDBMigrationService]", errorMsg); result.errors.push(errorMsg); result.success = false; return result; @@ -1063,7 +1081,7 @@ export async function migrateContacts( * ``` */ export async function migrateSettings(): Promise { - logger.info("[MigrationService] Starting settings migration"); + logger.info("[IndexedDBMigrationService] Starting settings migration"); const result: MigrationResult = { success: true, @@ -1076,17 +1094,17 @@ export async function migrateSettings(): Promise { try { const dexieSettings = await getDexieSettings(); - logger.info("[MigrationService] Migrating settings", { + 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("[MigrationService] Starting to migrate settings", setting); - let sqliteSettingRaw: - | { columns: string[]; values: unknown[][] } - | undefined; + logger.info( + "[IndexedDBMigrationService] Starting to migrate settings", + setting, + ); // adjust SQL based on the accountDid key, maybe null let conditional: string; @@ -1098,15 +1116,18 @@ export async function migrateSettings(): Promise { conditional = "accountDid = ?"; preparams = [setting.accountDid]; } - sqliteSettingRaw = await platformService.dbQuery( + const sqliteSettingRaw = await platformService.dbQuery( "SELECT * FROM settings WHERE " + conditional, preparams, ); - logger.info("[MigrationService] Migrating one set of settings:", { - setting, - sqliteSettingRaw, - }); + 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 @@ -1117,7 +1138,7 @@ export async function migrateSettings(): Promise { conditional, preparams, ); - logger.info("[MigrationService] Updating settings", { + logger.info("[IndexedDBMigrationService] Updating settings", { sql, params, }); @@ -1127,10 +1148,7 @@ export async function migrateSettings(): Promise { // 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", - ); + const { sql, params } = generateInsertStatement(setting, "settings"); await platformService.dbExec(sql, params); result.settingsMigrated++; } @@ -1140,7 +1158,7 @@ export async function migrateSettings(): Promise { const updatedSettings = await Promise.all(migrationPromises); logger.info( - "[MigrationService] Finished migrating settings", + "[IndexedDBMigrationService] Finished migrating settings", updatedSettings, result, ); @@ -1148,7 +1166,7 @@ export async function migrateSettings(): Promise { return result; } catch (error) { logger.error( - "[MigrationService] Complete settings migration failed:", + "[IndexedDBMigrationService] Complete settings migration failed:", error, ); const errorMessage = `Settings migration failed: ${error}`; @@ -1192,7 +1210,7 @@ export async function migrateSettings(): Promise { * ``` */ export async function migrateAccounts(): Promise { - logger.info("[MigrationService] Starting account migration"); + logger.info("[IndexedDBMigrationService] Starting account migration"); const result: MigrationResult = { success: true, @@ -1248,14 +1266,17 @@ export async function migrateAccounts(): Promise { ); } - logger.info("[MigrationService] Successfully migrated account", { - did, - dateCreated: account.dateCreated, - }); + 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("[MigrationService] Account migration failed:", { + logger.error("[IndexedDBMigrationService] Account migration failed:", { error, did, }); @@ -1272,7 +1293,7 @@ export async function migrateAccounts(): Promise { result.errors.push(errorMessage); result.success = false; logger.error( - "[MigrationService] Complete account migration failed:", + "[IndexedDBMigrationService] Complete account migration failed:", error, ); return result; @@ -1306,11 +1327,11 @@ export async function migrateAll(): Promise { try { logger.info( - "[MigrationService] Starting complete migration from Dexie to SQLite", + "[IndexedDBMigrationService] Starting complete migration from Dexie to SQLite", ); // Step 1: Migrate Accounts (foundational) - logger.info("[MigrationService] Step 1: Migrating accounts..."); + logger.info("[IndexedDBMigrationService] Step 1: Migrating accounts..."); const accountsResult = await migrateAccounts(); if (!accountsResult.success) { result.errors.push( @@ -1322,7 +1343,7 @@ export async function migrateAll(): Promise { result.warnings.push(...accountsResult.warnings); // Step 2: Migrate Settings (depends on accounts) - logger.info("[MigrationService] Step 2: Migrating settings..."); + logger.info("[IndexedDBMigrationService] Step 2: Migrating settings..."); const settingsResult = await migrateSettings(); if (!settingsResult.success) { result.errors.push( @@ -1335,7 +1356,7 @@ export async function migrateAll(): Promise { // Step 4: Migrate Contacts (independent, but after accounts for consistency) // ... but which is better done through the contact import view - // logger.info("[MigrationService] Step 4: Migrating contacts..."); + // logger.info("[IndexedDBMigrationService] Step 4: Migrating contacts..."); // const contactsResult = await migrateContacts(); // if (!contactsResult.success) { // result.errors.push( @@ -1354,7 +1375,7 @@ export async function migrateAll(): Promise { result.contactsMigrated; logger.info( - `[MigrationService] Complete migration successful: ${totalMigrated} total records migrated`, + `[IndexedDBMigrationService] Complete migration successful: ${totalMigrated} total records migrated`, { accounts: result.accountsMigrated, settings: result.settingsMigrated, @@ -1367,7 +1388,10 @@ export async function migrateAll(): Promise { } catch (error) { const errorMessage = `Complete migration failed: ${error}`; result.errors.push(errorMessage); - logger.error("[MigrationService] Complete migration failed:", error); + logger.error( + "[IndexedDBMigrationService] Complete migration failed:", + error, + ); return result; } } diff --git a/src/views/AccountViewView.vue b/src/views/AccountViewView.vue index be8a132e..b91dc224 100644 --- a/src/views/AccountViewView.vue +++ b/src/views/AccountViewView.vue @@ -349,8 +349,9 @@

- 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. + 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.

+ + + +