From 297c5a2dbb809acf1aeb90014029276efcec71d2 Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Tue, 3 Jun 2025 19:59:28 -0600 Subject: [PATCH 01/20] disable SQLite in Java & Swift (since they don't compile) & add SQL queueing on startup At this point, the app compiles and runs in Android & iOS but DB operations fail. --- BUILDING.md | 2 + .../java/app/timesafari/MainActivity.java | 4 +- .../java/timesafari/app/MainActivity.java | 5 - ios/App/App.xcodeproj/project.pbxproj | 57 +++-- ios/App/App/AppDelegate.swift | 4 +- ios/App/Podfile | 5 + ios/App/Podfile.lock | 20 +- package-lock.json | 40 +--- package.json | 2 +- pkgx.yaml | 1 + src/db/databaseUtil.ts | 5 + .../platforms/CapacitorPlatformService.ts | 219 ++++++++++++++---- .../platforms/ElectronPlatformService.ts | 13 +- src/views/LogView.vue | 9 + 14 files changed, 273 insertions(+), 113 deletions(-) delete mode 100644 android/app/src/main/java/timesafari/app/MainActivity.java diff --git a/BUILDING.md b/BUILDING.md index d9debb77..62fb27e8 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -369,6 +369,8 @@ Prerequisites: macOS with Xcode installed 6. Use Xcode to build and run on simulator or device. + * Select Product -> Destination with some Simulator version. Then click the run arrow. + 7. Release * Under "General" renamed a bunch of things to "Time Safari" diff --git a/android/app/src/main/java/app/timesafari/MainActivity.java b/android/app/src/main/java/app/timesafari/MainActivity.java index 6fbf940e..12429d63 100644 --- a/android/app/src/main/java/app/timesafari/MainActivity.java +++ b/android/app/src/main/java/app/timesafari/MainActivity.java @@ -2,7 +2,7 @@ package app.timesafari; import android.os.Bundle; import com.getcapacitor.BridgeActivity; -import com.getcapacitor.community.sqlite.SQLite; +//import com.getcapacitor.community.sqlite.SQLite; public class MainActivity extends BridgeActivity { @Override @@ -10,6 +10,6 @@ public class MainActivity extends BridgeActivity { super.onCreate(savedInstanceState); // Initialize SQLite - registerPlugin(SQLite.class); + //registerPlugin(SQLite.class); } } \ No newline at end of file diff --git a/android/app/src/main/java/timesafari/app/MainActivity.java b/android/app/src/main/java/timesafari/app/MainActivity.java deleted file mode 100644 index cb343faa..00000000 --- a/android/app/src/main/java/timesafari/app/MainActivity.java +++ /dev/null @@ -1,5 +0,0 @@ -package timesafari.app; - -import com.getcapacitor.BridgeActivity; - -public class MainActivity extends BridgeActivity {} diff --git a/ios/App/App.xcodeproj/project.pbxproj b/ios/App/App.xcodeproj/project.pbxproj index 1766f7e7..0c89bf53 100644 --- a/ios/App/App.xcodeproj/project.pbxproj +++ b/ios/App/App.xcodeproj/project.pbxproj @@ -14,7 +14,7 @@ 504EC30F1FED79650016851F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30E1FED79650016851F /* Assets.xcassets */; }; 504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC3101FED79650016851F /* LaunchScreen.storyboard */; }; 50B271D11FEDC1A000F3C39B /* public in Resources */ = {isa = PBXBuildFile; fileRef = 50B271D01FEDC1A000F3C39B /* public */; }; - A084ECDBA7D38E1E42DFC39D /* Pods_App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */; }; + 97EF2DC6FD76C3643D680B8D /* Pods_App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 90DCAFB4D8948F7A50C13800 /* Pods_App.framework */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -27,9 +27,9 @@ 504EC3111FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 504EC3131FED79650016851F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 50B271D01FEDC1A000F3C39B /* public */ = {isa = PBXFileReference; lastKnownFileType = folder; path = public; sourceTree = ""; }; - AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.release.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.release.xcconfig"; sourceTree = ""; }; - FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.debug.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.debug.xcconfig"; sourceTree = ""; }; + 90DCAFB4D8948F7A50C13800 /* Pods_App.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + E2E9297D5D02C549106C77F9 /* Pods-App.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.release.xcconfig"; path = "Target Support Files/Pods-App/Pods-App.release.xcconfig"; sourceTree = ""; }; + EAEC6436E595F7CD3A1C9E96 /* Pods-App.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.debug.xcconfig"; path = "Target Support Files/Pods-App/Pods-App.debug.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -37,17 +37,17 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - A084ECDBA7D38E1E42DFC39D /* Pods_App.framework in Frameworks */, + 97EF2DC6FD76C3643D680B8D /* Pods_App.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 27E2DDA53C4D2A4D1A88CE4A /* Frameworks */ = { + 4B546315E668C7A13939F417 /* Frameworks */ = { isa = PBXGroup; children = ( - AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */, + 90DCAFB4D8948F7A50C13800 /* Pods_App.framework */, ); name = Frameworks; sourceTree = ""; @@ -57,8 +57,8 @@ children = ( 504EC3061FED79650016851F /* App */, 504EC3051FED79650016851F /* Products */, - 7F8756D8B27F46E3366F6CEA /* Pods */, - 27E2DDA53C4D2A4D1A88CE4A /* Frameworks */, + BA325FFCDCE8D334E5C7AEBE /* Pods */, + 4B546315E668C7A13939F417 /* Frameworks */, ); sourceTree = ""; }; @@ -85,13 +85,14 @@ path = App; sourceTree = ""; }; - 7F8756D8B27F46E3366F6CEA /* Pods */ = { + BA325FFCDCE8D334E5C7AEBE /* Pods */ = { isa = PBXGroup; children = ( - FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */, - AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */, + EAEC6436E595F7CD3A1C9E96 /* Pods-App.debug.xcconfig */, + E2E9297D5D02C549106C77F9 /* Pods-App.release.xcconfig */, ); name = Pods; + path = Pods; sourceTree = ""; }; /* End PBXGroup section */ @@ -101,12 +102,12 @@ isa = PBXNativeTarget; buildConfigurationList = 504EC3161FED79650016851F /* Build configuration list for PBXNativeTarget "App" */; buildPhases = ( - 6634F4EFEBD30273BCE97C65 /* [CP] Check Pods Manifest.lock */, + 92977BEA1068CC097A57FC77 /* [CP] Check Pods Manifest.lock */, 504EC3001FED79650016851F /* Sources */, 504EC3011FED79650016851F /* Frameworks */, 504EC3021FED79650016851F /* Resources */, - 9592DBEFFC6D2A0C8D5DEB22 /* [CP] Embed Pods Frameworks */, 012076E8FFE4BF260A79B034 /* Fix Privacy Manifest */, + 3525031ED1C96EF4CF6E9959 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -189,37 +190,41 @@ shellScript = "\"${PROJECT_DIR}/app_privacy_manifest_fixer/fixer.sh\" "; showEnvVarsInLog = 0; }; - 6634F4EFEBD30273BCE97C65 /* [CP] Check Pods Manifest.lock */ = { + 3525031ED1C96EF4CF6E9959 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", ); - name = "[CP] Check Pods Manifest.lock"; + name = "[CP] Embed Pods Frameworks"; outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-App-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-App/Pods-App-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; - 9592DBEFFC6D2A0C8D5DEB22 /* [CP] Embed Pods Frameworks */ = { + 92977BEA1068CC097A57FC77 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); + inputFileListPaths = ( + ); inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( ); - name = "[CP] Embed Pods Frameworks"; outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-App-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-App/Pods-App-frameworks.sh\"\n"; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ @@ -375,11 +380,12 @@ }; 504EC3171FED79650016851F /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */; + baseConfigurationReference = EAEC6436E595F7CD3A1C9E96 /* Pods-App.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 18; + DEVELOPMENT_TEAM = GM3FS5JQPH; ENABLE_APP_SANDBOX = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO; INFOPLIST_FILE = App/Info.plist; @@ -401,11 +407,12 @@ }; 504EC3181FED79650016851F /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */; + baseConfigurationReference = E2E9297D5D02C549106C77F9 /* Pods-App.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 18; + DEVELOPMENT_TEAM = GM3FS5JQPH; ENABLE_APP_SANDBOX = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO; INFOPLIST_FILE = App/Info.plist; diff --git a/ios/App/App/AppDelegate.swift b/ios/App/App/AppDelegate.swift index c1c6a182..7a1b41b3 100644 --- a/ios/App/App/AppDelegate.swift +++ b/ios/App/App/AppDelegate.swift @@ -9,8 +9,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Initialize SQLite - let sqlite = SQLite() - sqlite.initialize() + //let sqlite = SQLite() + //sqlite.initialize() // Override point for customization after application launch. return true diff --git a/ios/App/Podfile b/ios/App/Podfile index 228eeecb..da98dfe6 100644 --- a/ios/App/Podfile +++ b/ios/App/Podfile @@ -27,4 +27,9 @@ end post_install do |installer| assertDeploymentTarget(installer) + installer.pods_project.targets.each do |target| + target.build_configurations.each do |config| + config.build_settings['EXCLUDED_ARCHS[sdk=iphonesimulator*]'] = 'arm64' + end + end end diff --git a/ios/App/Podfile.lock b/ios/App/Podfile.lock index f513a8ba..fdd82e86 100644 --- a/ios/App/Podfile.lock +++ b/ios/App/Podfile.lock @@ -5,6 +5,10 @@ PODS: - Capacitor - CapacitorCamera (6.1.2): - Capacitor + - CapacitorCommunitySqlite (6.0.2): + - Capacitor + - SQLCipher + - ZIPFoundation - CapacitorCordova (6.2.1) - CapacitorFilesystem (6.0.3): - Capacitor @@ -73,11 +77,18 @@ PODS: - nanopb/decode (2.30910.0) - nanopb/encode (2.30910.0) - PromisesObjC (2.4.0) + - SQLCipher (4.9.0): + - SQLCipher/standard (= 4.9.0) + - SQLCipher/common (4.9.0) + - SQLCipher/standard (4.9.0): + - SQLCipher/common + - ZIPFoundation (0.9.19) DEPENDENCIES: - "Capacitor (from `../../node_modules/@capacitor/ios`)" - "CapacitorApp (from `../../node_modules/@capacitor/app`)" - "CapacitorCamera (from `../../node_modules/@capacitor/camera`)" + - "CapacitorCommunitySqlite (from `../../node_modules/@capacitor-community/sqlite`)" - "CapacitorCordova (from `../../node_modules/@capacitor/ios`)" - "CapacitorFilesystem (from `../../node_modules/@capacitor/filesystem`)" - "CapacitorMlkitBarcodeScanning (from `../../node_modules/@capacitor-mlkit/barcode-scanning`)" @@ -98,6 +109,8 @@ SPEC REPOS: - MLKitVision - nanopb - PromisesObjC + - SQLCipher + - ZIPFoundation EXTERNAL SOURCES: Capacitor: @@ -106,6 +119,8 @@ EXTERNAL SOURCES: :path: "../../node_modules/@capacitor/app" CapacitorCamera: :path: "../../node_modules/@capacitor/camera" + CapacitorCommunitySqlite: + :path: "../../node_modules/@capacitor-community/sqlite" CapacitorCordova: :path: "../../node_modules/@capacitor/ios" CapacitorFilesystem: @@ -121,6 +136,7 @@ SPEC CHECKSUMS: Capacitor: c95400d761e376be9da6be5a05f226c0e865cebf CapacitorApp: e1e6b7d05e444d593ca16fd6d76f2b7c48b5aea7 CapacitorCamera: 9bc7b005d0e6f1d5f525b8137045b60cffffce79 + CapacitorCommunitySqlite: 0299d20f4b00c2e6aa485a1d8932656753937b9b CapacitorCordova: 8d93e14982f440181be7304aa9559ca631d77fff CapacitorFilesystem: 59270a63c60836248812671aa3b15df673fbaf74 CapacitorMlkitBarcodeScanning: 7652be9c7922f39203a361de735d340ae37e134e @@ -138,7 +154,9 @@ SPEC CHECKSUMS: MLKitVision: 90922bca854014a856f8b649d1f1f04f63fd9c79 nanopb: 438bc412db1928dac798aa6fd75726007be04262 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 + SQLCipher: 31878d8ebd27e5c96db0b7cb695c96e9f8ad77da + ZIPFoundation: b8c29ea7ae353b309bc810586181fd073cb3312c -PODFILE CHECKSUM: 7e7e09e6937de7f015393aecf2cf7823645689b3 +PODFILE CHECKSUM: f987510f7383b04a1b09ea8472bdadcd88b6c924 COCOAPODS: 1.16.2 diff --git a/package-lock.json b/package-lock.json index c0e0569a..579ccd38 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "name": "timesafari", "version": "0.4.6", "dependencies": { - "@capacitor-community/sqlite": "^6.0.2", + "@capacitor-community/sqlite": "6.0.2", "@capacitor-mlkit/barcode-scanning": "^6.0.0", "@capacitor/android": "^6.2.0", "@capacitor/app": "^6.0.0", @@ -2516,7 +2516,6 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/@capacitor-community/sqlite/-/sqlite-6.0.2.tgz", "integrity": "sha512-sj+2SPLu7E/3dM3xxcWwfNomG+aQHuN96/EFGrOtp4Dv30/2y5oIPyi6hZGjQGjPc5GDNoTQwW7vxWNzybjuMg==", - "license": "MIT", "dependencies": { "jeep-sqlite": "^2.7.2" }, @@ -8195,7 +8194,6 @@ "cpu": [ "arm64" ], - "license": "MIT", "optional": true, "os": [ "darwin" @@ -8208,7 +8206,6 @@ "cpu": [ "x64" ], - "license": "MIT", "optional": true, "os": [ "darwin" @@ -8277,7 +8274,6 @@ "cpu": [ "arm64" ], - "license": "MIT", "optional": true, "os": [ "linux" @@ -8290,7 +8286,6 @@ "cpu": [ "arm64" ], - "license": "MIT", "optional": true, "os": [ "linux" @@ -8373,7 +8368,6 @@ "cpu": [ "x64" ], - "license": "MIT", "optional": true, "os": [ "linux" @@ -8386,7 +8380,6 @@ "cpu": [ "x64" ], - "license": "MIT", "optional": true, "os": [ "linux" @@ -8399,7 +8392,6 @@ "cpu": [ "arm64" ], - "license": "MIT", "optional": true, "os": [ "win32" @@ -8426,7 +8418,6 @@ "cpu": [ "x64" ], - "license": "MIT", "optional": true, "os": [ "win32" @@ -8818,10 +8809,9 @@ } }, "node_modules/@stencil/core": { - "version": "4.31.0", - "resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.31.0.tgz", - "integrity": "sha512-Ei9MFJ6LPD9BMFs+klkHylbVOOYhG10Jv4bvoFf3GMH15kA41rSYkEdr4DiX84ZdErQE2qtFV/2SUyWoXh0AhA==", - "license": "MIT", + "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==", "bin": { "stencil": "bin/stencil" }, @@ -12173,8 +12163,7 @@ "node_modules/browser-fs-access": { "version": "0.35.0", "resolved": "https://registry.npmjs.org/browser-fs-access/-/browser-fs-access-0.35.0.tgz", - "integrity": "sha512-sLoadumpRfsjprP8XzVjpQc0jK8yqHBx0PtUTGYj2fftT+P/t+uyDAQdMgGAPKD011in/O+YYGh7fIs0oG/viw==", - "license": "Apache-2.0" + "integrity": "sha512-sLoadumpRfsjprP8XzVjpQc0jK8yqHBx0PtUTGYj2fftT+P/t+uyDAQdMgGAPKD011in/O+YYGh7fIs0oG/viw==" }, "node_modules/browserify-aes": { "version": "1.2.0", @@ -18144,8 +18133,7 @@ "node_modules/immediate": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", - "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", - "license": "MIT" + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" }, "node_modules/import-fresh": { "version": "3.3.1", @@ -19090,7 +19078,6 @@ "version": "2.8.0", "resolved": "https://registry.npmjs.org/jeep-sqlite/-/jeep-sqlite-2.8.0.tgz", "integrity": "sha512-FWNUP6OAmrUHwiW7H1xH5YUQ8tN2O4l4psT1sLd7DQtHd5PfrA1nvNdeKPNj+wQBtu7elJa8WoUibTytNTaaCg==", - "license": "MIT", "dependencies": { "@stencil/core": "^4.20.0", "browser-fs-access": "^0.35.0", @@ -19591,7 +19578,6 @@ "version": "3.10.1", "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", - "license": "(MIT OR GPL-3.0-or-later)", "dependencies": { "lie": "~3.3.0", "pako": "~1.0.2", @@ -19602,20 +19588,17 @@ "node_modules/jszip/node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "license": "MIT" + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" }, "node_modules/jszip/node_modules/pako": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", - "license": "(MIT AND Zlib)" + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" }, "node_modules/jszip/node_modules/readable-stream": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "license": "MIT", "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -19629,14 +19612,12 @@ "node_modules/jszip/node_modules/safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "license": "MIT" + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, "node_modules/jszip/node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "license": "MIT", "dependencies": { "safe-buffer": "~5.1.0" } @@ -20159,7 +20140,6 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", - "license": "MIT", "dependencies": { "immediate": "~3.0.5" } @@ -20526,7 +20506,6 @@ "version": "1.10.0", "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz", "integrity": "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==", - "license": "Apache-2.0", "dependencies": { "lie": "3.1.1" } @@ -20535,7 +20514,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz", "integrity": "sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==", - "license": "MIT", "dependencies": { "immediate": "~3.0.5" } diff --git a/package.json b/package.json index 9d87540b..b9fafaae 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "electron:build-mac-universal": "npm run build:electron-prod && electron-builder --mac --universal" }, "dependencies": { - "@capacitor-community/sqlite": "^6.0.2", + "@capacitor-community/sqlite": "6.0.2", "@capacitor-mlkit/barcode-scanning": "^6.0.0", "@capacitor/android": "^6.2.0", "@capacitor/app": "^6.0.0", diff --git a/pkgx.yaml b/pkgx.yaml index 08559bf2..89c92cb2 100644 --- a/pkgx.yaml +++ b/pkgx.yaml @@ -2,5 +2,6 @@ dependencies: - gradle - java - pod + - rubygems.org # other dependencies are discovered via package.json & requirements.txt & Gemfile (I'm guessing). diff --git a/src/db/databaseUtil.ts b/src/db/databaseUtil.ts index f8079c97..a0fdd87e 100644 --- a/src/db/databaseUtil.ts +++ b/src/db/databaseUtil.ts @@ -124,6 +124,7 @@ export async function retrieveSettingsForActiveAccount(): Promise { } let lastCleanupDate: string | null = null; +export let memoryLogs: string[] = []; /** * Logs a message to the database with proper handling of concurrent writes @@ -136,6 +137,7 @@ export async function logToDb(message: string): Promise { const nowKey = new Date().toISOString(); try { + memoryLogs.push(`${new Date().toISOString()} ${message}`); // Try to insert first, if it fails due to UNIQUE constraint, update instead await platform.dbExec("INSERT INTO logs (date, message) VALUES (?, ?)", [ nowKey, @@ -148,6 +150,9 @@ export async function logToDb(message: string): Promise { const sevenDaysAgo = new Date( new Date().getTime() - 7 * 24 * 60 * 60 * 1000, ); + memoryLogs = memoryLogs.filter( + (log) => log.split(" ")[0] > sevenDaysAgo.toDateString(), + ); await platform.dbExec("DELETE FROM logs WHERE date < ?", [ sevenDaysAgo.toDateString(), ]); diff --git a/src/services/platforms/CapacitorPlatformService.ts b/src/services/platforms/CapacitorPlatformService.ts index 8df1985f..cd237dfe 100644 --- a/src/services/platforms/CapacitorPlatformService.ts +++ b/src/services/platforms/CapacitorPlatformService.ts @@ -10,10 +10,9 @@ import { SQLiteConnection, SQLiteDBConnection, CapacitorSQLite, - Changes, } from "@capacitor-community/sqlite"; import { logger } from "../../utils/logger"; -import { QueryExecResult, SqlValue } from "@/interfaces/database"; +import { QueryExecResult } from "@/interfaces/database"; import { DEFAULT_ENDORSER_API_SERVER } from "@/constants/app"; interface Migration { @@ -21,6 +20,14 @@ interface Migration { sql: string; } +interface QueuedOperation { + type: "run" | "query" | "getOneRow" | "getAll"; + sql: string; + params: unknown[]; + resolve: (value: unknown) => void; + reject: (reason: unknown) => void; +} + /** * Platform service implementation for Capacitor (mobile) platform. * Provides native mobile functionality through Capacitor plugins for: @@ -34,12 +41,40 @@ export class CapacitorPlatformService implements PlatformService { private db: SQLiteDBConnection | null = null; private dbName = "timesafari.db"; private initialized = false; + private initializationPromise: Promise | null = null; + private operationQueue: Array = []; + private isProcessingQueue: boolean = false; constructor() { this.sqlite = new SQLiteConnection(CapacitorSQLite); } private async initializeDatabase(): Promise { + // If already initialized, return immediately + if (this.initialized) { + return; + } + + // If initialization is in progress, wait for it + if (this.initializationPromise) { + return this.initializationPromise; + } + + // Start initialization + this.initializationPromise = this._initialize(); + try { + await this.initializationPromise; + } catch (error) { + logger.error( + "[CapacitorPlatformService] Initialize method failed:", + error, + ); + this.initializationPromise = null; // Reset on failure + throw error; + } + } + + private async _initialize(): Promise { if (this.initialized) { return; } @@ -57,16 +92,142 @@ export class CapacitorPlatformService implements PlatformService { await this.db.open(); // Set journal mode to WAL for better performance - await this.db.execute("PRAGMA journal_mode=WAL;"); + // await this.db.execute("PRAGMA journal_mode=WAL;"); // Run migrations await this.runMigrations(); this.initialized = true; - logger.log("SQLite database initialized successfully"); + logger.log( + "[CapacitorPlatformService] SQLite database initialized successfully", + ); + + // Start processing the queue after initialization + this.processQueue(); } catch (error) { - logger.error("Error initializing SQLite database:", error); - throw new Error("Failed to initialize database"); + logger.error( + "[CapacitorPlatformService] Error initializing SQLite database:", + error, + ); + throw new Error( + "[CapacitorPlatformService] Failed to initialize database", + ); + } + } + + private async processQueue(): Promise { + if (this.isProcessingQueue || !this.initialized || !this.db) { + return; + } + + this.isProcessingQueue = true; + + while (this.operationQueue.length > 0) { + const operation = this.operationQueue.shift(); + if (!operation) continue; + + try { + let result: unknown; + switch (operation.type) { + case "run": { + const runResult = await this.db.run( + operation.sql, + operation.params, + ); + result = { + changes: runResult.changes?.changes || 0, + lastId: runResult.changes?.lastId, + }; + break; + } + case "query": { + const queryResult = await this.db.query( + operation.sql, + operation.params, + ); + result = { + columns: [], // SQLite plugin doesn't provide column names + values: queryResult.values || [], + }; + break; + } + case "getOneRow": { + const oneRowResult = await this.db.query( + operation.sql, + operation.params, + ); + result = oneRowResult.values?.[0]; + break; + } + case "getAll": { + const allResult = await this.db.query( + operation.sql, + operation.params, + ); + result = allResult.values || []; + break; + } + } + operation.resolve(result); + } catch (error) { + logger.error( + "[CapacitorPlatformService] Error while processing SQL queue:", + error, + " ... for sql:", + operation.sql, + " ... with params:", + operation.params, + ); + operation.reject(error); + } + } + + this.isProcessingQueue = false; + } + + private async queueOperation( + type: QueuedOperation["type"], + sql: string, + params: unknown[] = [], + ): Promise { + return new Promise((resolve, reject) => { + const operation: QueuedOperation = { + type, + sql, + params, + resolve: (value: unknown) => resolve(value as R), + reject, + }; + this.operationQueue.push(operation); + + // If we're already initialized, start processing the queue + if (this.initialized && this.db) { + this.processQueue(); + } + }); + } + + private async waitForInitialization(): Promise { + // If we have an initialization promise, wait for it + if (this.initializationPromise) { + await this.initializationPromise; + return; + } + + // If not initialized and no promise, start initialization + if (!this.initialized) { + await this.initializeDatabase(); + return; + } + + // If initialized but no db, something went wrong + if (!this.db) { + logger.error( + "[CapacitorPlatformService] Database not properly initialized after await waitForInitialization() - initialized flag is true but db is null", + ); + throw new Error( + "[CapacitorPlatformService] The database could not be initialized. We recommend you restart or reinstall.", + ); } } @@ -166,7 +327,7 @@ export class CapacitorPlatformService implements PlatformService { CREATE INDEX IF NOT EXISTS idx_contacts_name ON contacts(name); CREATE TABLE IF NOT EXISTS logs ( - date TEXT PRIMARY KEY, + date TEXT, message TEXT NOT NULL ); @@ -660,24 +821,8 @@ export class CapacitorPlatformService implements PlatformService { * @see PlatformService.dbQuery */ async dbQuery(sql: string, params?: unknown[]): Promise { - await this.initializeDatabase(); - if (!this.db) { - throw new Error("Database not initialized"); - } - - try { - const result = await this.db.query(sql, params || []); - const values = result.values || []; - return { - columns: [], // SQLite plugin doesn't provide column names in query result - values: values as SqlValue[][], - }; - } catch (error) { - logger.error("Error executing query:", error); - throw new Error( - `Database query failed: ${error instanceof Error ? error.message : String(error)}`, - ); - } + await this.waitForInitialization(); + return this.queueOperation("query", sql, params || []); } /** @@ -687,23 +832,11 @@ export class CapacitorPlatformService implements PlatformService { sql: string, params?: unknown[], ): Promise<{ changes: number; lastId?: number }> { - await this.initializeDatabase(); - if (!this.db) { - throw new Error("Database not initialized"); - } - - try { - const result = await this.db.run(sql, params || []); - const changes = result.changes as Changes; - return { - changes: changes?.changes || 0, - lastId: changes?.lastId, - }; - } catch (error) { - logger.error("Error executing statement:", error); - throw new Error( - `Database execution failed: ${error instanceof Error ? error.message : String(error)}`, - ); - } + await this.waitForInitialization(); + return this.queueOperation<{ changes: number; lastId?: number }>( + "run", + sql, + params || [], + ); } } diff --git a/src/services/platforms/ElectronPlatformService.ts b/src/services/platforms/ElectronPlatformService.ts index 5bdd455f..5700f7d6 100644 --- a/src/services/platforms/ElectronPlatformService.ts +++ b/src/services/platforms/ElectronPlatformService.ts @@ -60,10 +60,17 @@ export class ElectronPlatformService implements PlatformService { await this.runMigrations(); this.initialized = true; - logger.log("SQLite database initialized successfully"); + logger.log( + "[ElectronPlatformService] SQLite database initialized successfully", + ); } catch (error) { - logger.error("Error initializing SQLite database:", error); - throw new Error("Failed to initialize database"); + logger.error( + "[ElectronPlatformService] Error initializing SQLite database:", + error, + ); + throw new Error( + "[ElectronPlatformService] Failed to initialize database", + ); } } diff --git a/src/views/LogView.vue b/src/views/LogView.vue index deecbb56..8ef7bc39 100644 --- a/src/views/LogView.vue +++ b/src/views/LogView.vue @@ -42,6 +42,13 @@ > +
+

Memory Logs

+
{{ memoryLogs.join("\n") }}
+
@@ -70,6 +77,7 @@ export default class LogView extends Vue { loading = true; logs: Log[] = []; error: string | null = null; + memoryLogs: string[] = []; async mounted() { await this.loadLogs(); @@ -78,6 +86,7 @@ export default class LogView extends Vue { async loadLogs() { try { this.error = null; // Clear any previous errors + this.memoryLogs = databaseUtil.memoryLogs; let allLogs: Log[] = []; const platformService = PlatformServiceFactory.getInstance(); -- 2.30.2 From 03cb4720b8d5e56b22d100186a48aa29527fdf9a Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Wed, 4 Jun 2025 22:01:14 -0600 Subject: [PATCH 02/20] fix Capacitor to use the same migrations (migrations run but accounts aren't created) --- -1748433586226.log | 101 ------------ src/db-sql/migration.ts | 26 +-- src/main.common.ts | 4 +- src/services/AbsurdSqlDatabaseService.ts | 21 ++- src/services/migrationService.ts | 56 ++++--- .../platforms/CapacitorPlatformService.ts | 156 ++++-------------- 6 files changed, 95 insertions(+), 269 deletions(-) delete mode 100644 -1748433586226.log diff --git a/-1748433586226.log b/-1748433586226.log deleted file mode 100644 index c460a7d7..00000000 --- a/-1748433586226.log +++ /dev/null @@ -1,101 +0,0 @@ -VM5:29 [Preload] Preload script starting... -VM5:29 [Preload] Preload script completed successfully -main.common-DiOUyXe7.js:27 Platform Object -error @ main.common-DiOUyXe7.js:27 -main.common-DiOUyXe7.js:27 PWA enabled Object -error @ main.common-DiOUyXe7.js:27 -main.common-DiOUyXe7.js:27 [Web] PWA enabled Object -error @ main.common-DiOUyXe7.js:27 -main.common-DiOUyXe7.js:27 [Web] Platform Object -error @ main.common-DiOUyXe7.js:27 -main.common-DiOUyXe7.js:29 Opened! -main.common-DiOUyXe7.js:2552 Failed to log to database: Error: no such column: value - at E.handleError (main.common-DiOUyXe7.js:27:21133) - at E.exec (main.common-DiOUyXe7.js:27:19785) - at Rc.processQueue (main.common-DiOUyXe7.js:2379:2368) -F7 @ main.common-DiOUyXe7.js:2552 -main.common-DiOUyXe7.js:2552 Original message: PWA enabled - [{"pwa_enabled":false}] -F7 @ main.common-DiOUyXe7.js:2552 -main.common-DiOUyXe7.js:2552 Failed to log to database: Error: no such column: value - at E.handleError (main.common-DiOUyXe7.js:27:21133) - at E.exec (main.common-DiOUyXe7.js:27:19785) - at Rc.processQueue (main.common-DiOUyXe7.js:2379:2368) - at main.common-DiOUyXe7.js:2379:2816 - at new Promise () - at Rc.queueOperation (main.common-DiOUyXe7.js:2379:2685) - at Rc.query (main.common-DiOUyXe7.js:2379:3378) - at async F7 (main.common-DiOUyXe7.js:2552:117) -F7 @ main.common-DiOUyXe7.js:2552 -main.common-DiOUyXe7.js:2552 Original message: [Web] PWA enabled - [{"pwa_enabled":false}] -F7 @ main.common-DiOUyXe7.js:2552 -main.common-DiOUyXe7.js:2552 Failed to log to database: Error: no such column: value - at E.handleError (main.common-DiOUyXe7.js:27:21133) - at E.exec (main.common-DiOUyXe7.js:27:19785) - at Rc.processQueue (main.common-DiOUyXe7.js:2379:2368) - at main.common-DiOUyXe7.js:2379:2816 - at new Promise () - at Rc.queueOperation (main.common-DiOUyXe7.js:2379:2685) - at Rc.query (main.common-DiOUyXe7.js:2379:3378) - at async F7 (main.common-DiOUyXe7.js:2552:117) -F7 @ main.common-DiOUyXe7.js:2552 -main.common-DiOUyXe7.js:2552 Original message: [Web] Platform - [{"platform":"web"}] -F7 @ main.common-DiOUyXe7.js:2552 -main.common-DiOUyXe7.js:2552 Failed to log to database: Error: no such column: value - at E.handleError (main.common-DiOUyXe7.js:27:21133) - at E.exec (main.common-DiOUyXe7.js:27:19785) - at Rc.processQueue (main.common-DiOUyXe7.js:2379:2368) - at main.common-DiOUyXe7.js:2379:2816 - at new Promise () - at Rc.queueOperation (main.common-DiOUyXe7.js:2379:2685) - at Rc.query (main.common-DiOUyXe7.js:2379:3378) - at async F7 (main.common-DiOUyXe7.js:2552:117) -F7 @ main.common-DiOUyXe7.js:2552 -main.common-DiOUyXe7.js:2552 Original message: Platform - [{"platform":"web"}] -F7 @ main.common-DiOUyXe7.js:2552 -main.common-DiOUyXe7.js:2100 - - - GET https://api.endorser.ch/api/report/rateLimits 400 (Bad Request) -(anonymous) @ main.common-DiOUyXe7.js:2100 -xhr @ main.common-DiOUyXe7.js:2100 -p6 @ main.common-DiOUyXe7.js:2102 -_request @ main.common-DiOUyXe7.js:2103 -request @ main.common-DiOUyXe7.js:2102 -Yc. @ main.common-DiOUyXe7.js:2103 -(anonymous) @ main.common-DiOUyXe7.js:2098 -dJ @ main.common-DiOUyXe7.js:2295 -main.common-DiOUyXe7.js:2100 - - - GET https://api.endorser.ch/api/report/rateLimits 400 (Bad Request) -(anonymous) @ main.common-DiOUyXe7.js:2100 -xhr @ main.common-DiOUyXe7.js:2100 -p6 @ main.common-DiOUyXe7.js:2102 -_request @ main.common-DiOUyXe7.js:2103 -request @ main.common-DiOUyXe7.js:2102 -Yc. @ main.common-DiOUyXe7.js:2103 -(anonymous) @ main.common-DiOUyXe7.js:2098 -dJ @ main.common-DiOUyXe7.js:2295 -await in dJ -checkRegistrationStatus @ HomeView-DJMSCuMg.js:1 -mounted @ HomeView-DJMSCuMg.js:1 -XMLHttpRequest.send -(anonymous) @ main.common-DiOUyXe7.js:2100 -xhr @ main.common-DiOUyXe7.js:2100 -p6 @ main.common-DiOUyXe7.js:2102 -_request @ main.common-DiOUyXe7.js:2103 -request @ main.common-DiOUyXe7.js:2102 -Yc. @ main.common-DiOUyXe7.js:2103 -(anonymous) @ main.common-DiOUyXe7.js:2098 -ZG @ main.common-DiOUyXe7.js:2295 -await in ZG -initializeIdentity @ HomeView-DJMSCuMg.js:1 -XMLHttpRequest.send -(anonymous) @ main.common-DiOUyXe7.js:2100 -xhr @ main.common-DiOUyXe7.js:2100 -p6 @ main.common-DiOUyXe7.js:2102 -_request @ main.common-DiOUyXe7.js:2103 -request @ main.common-DiOUyXe7.js:2102 -Yc. @ main.common-DiOUyXe7.js:2103 -(anonymous) @ main.common-DiOUyXe7.js:2098 -dJ @ main.common-DiOUyXe7.js:2295 diff --git a/src/db-sql/migration.ts b/src/db-sql/migration.ts index 7a99724f..9cadd260 100644 --- a/src/db-sql/migration.ts +++ b/src/db-sql/migration.ts @@ -1,5 +1,4 @@ import migrationService from "../services/migrationService"; -import type { QueryExecResult } from "../interfaces/database"; import { DEFAULT_ENDORSER_API_SERVER } from "@/constants/app"; import { arrayBufferToBase64 } from "@/libs/crypto"; @@ -119,16 +118,21 @@ const MIGRATIONS = [ }, ]; -export async function registerMigrations(): Promise { - // Register all migrations +/** + * @param sqlExec - A function that executes a SQL statement and returns the result + * @param extractMigrationNames - A function that extracts the names (string array) from "select name from migrations" + */ +export async function runMigrations( + sqlExec: (sql: string) => Promise, + sqlQuery: (sql: string) => Promise, + extractMigrationNames: (result: T) => Set, +): Promise { for (const migration of MIGRATIONS) { - await migrationService.registerMigration(migration); + migrationService.registerMigration(migration); } -} - -export async function runMigrations( - sqlExec: (sql: string, params?: unknown[]) => Promise>, -): Promise { - await registerMigrations(); - await migrationService.runMigrations(sqlExec); + await migrationService.runMigrations( + sqlExec, + sqlQuery, + extractMigrationNames, + ); } diff --git a/src/main.common.ts b/src/main.common.ts index ce7b6495..93891f01 100644 --- a/src/main.common.ts +++ b/src/main.common.ts @@ -13,8 +13,8 @@ import { logger } from "./utils/logger"; const platform = process.env.VITE_PLATFORM; const pwa_enabled = process.env.VITE_PWA_ENABLED === "true"; -logger.error("Platform", { platform }); -logger.error("PWA enabled", { pwa_enabled }); +logger.error("Platform", JSON.stringify({ platform })); +logger.error("PWA enabled", JSON.stringify({ pwa_enabled })); // Global Error Handler function setupGlobalErrorHandler(app: VueApp) { diff --git a/src/services/AbsurdSqlDatabaseService.ts b/src/services/AbsurdSqlDatabaseService.ts index 878dbea5..6d7dd9c9 100644 --- a/src/services/AbsurdSqlDatabaseService.ts +++ b/src/services/AbsurdSqlDatabaseService.ts @@ -100,10 +100,27 @@ class AbsurdSqlDatabaseService implements DatabaseService { // An error is thrown without this pragma: "File has invalid page size. (the first block of a new file must be written first)" await this.db.exec(`PRAGMA journal_mode=MEMORY;`); - const sqlExec = this.db.exec.bind(this.db); + const sqlExec = this.db.run.bind(this.db); + const sqlQuery = this.db.exec.bind(this.db); + + // Extract the migration names for the absurd-sql format + const extractMigrationNames: (result: QueryExecResult[]) => Set = ( + result, + ) => { + const queryResult = result as QueryExecResult[]; + // Even with the "select name" query, the QueryExecResult may be [] (which doesn't make sense to me). + if (queryResult.length > 0) { + const singleResult = queryResult[0]; + const executedMigrations: Set = new Set( + singleResult.values.map((row) => row[0] as string), + ); + return executedMigrations; + } + return new Set(); + }; // Run migrations - await runMigrations(sqlExec); + await runMigrations(sqlExec, sqlQuery, extractMigrationNames); this.initialized = true; diff --git a/src/services/migrationService.ts b/src/services/migrationService.ts index d571f1ba..8d90b0f4 100644 --- a/src/services/migrationService.ts +++ b/src/services/migrationService.ts @@ -1,6 +1,3 @@ -import { logger } from "@/utils/logger"; -import { QueryExecResult } from "../interfaces/database"; - interface Migration { name: string; sql: string; @@ -19,46 +16,55 @@ export class MigrationService { return MigrationService.instance; } - async registerMigration(migration: Migration): Promise { + registerMigration(migration: Migration) { this.migrations.push(migration); } - async runMigrations( - sqlExec: ( - sql: string, - params?: unknown[], - ) => Promise>, + /** + * @param sqlExec - A function that executes a SQL statement and returns some update result + * @param sqlQuery - A function that executes a SQL query and returns the result in some format + * @param extractMigrationNames - A function that extracts the names (string array) from a "select name from migrations" query + */ + async runMigrations( + // note that this does not take parameters because the Capacitor SQLite 'execute' is different + sqlExec: (sql: string) => Promise, + sqlQuery: (sql: string) => Promise, + extractMigrationNames: (result: T) => Set, ): Promise { + // eslint-disable-next-line no-console + console.log("Will run migrations"); + // Create migrations table if it doesn't exist - await sqlExec(` + const result0 = await sqlExec(` CREATE TABLE IF NOT EXISTS migrations ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE, executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); `); + // eslint-disable-next-line no-console + console.log("Created migrations table", JSON.stringify(result0)); // Get list of executed migrations - const result: QueryExecResult[] = await sqlExec( - "SELECT name FROM migrations;", + const result1: T = await sqlQuery("SELECT name FROM migrations;"); + const executedMigrations = extractMigrationNames(result1); + // eslint-disable-next-line no-console + console.log( + "Executed migration select", + JSON.stringify(executedMigrations), ); - let executedMigrations: Set = new Set(); - // Even with that query, the QueryExecResult may be [] (which doesn't make sense to me). - if (result.length > 0) { - const singleResult = result[0]; - executedMigrations = new Set( - singleResult.values.map((row: unknown[]) => row[0]), - ); - } // Run pending migrations in order for (const migration of this.migrations) { if (!executedMigrations.has(migration.name)) { - await sqlExec(migration.sql); - await sqlExec("INSERT INTO migrations (name) VALUES (?)", [ - migration.name, - ]); - logger.log(`Migration ${migration.name} executed successfully`); + const result2 = await sqlExec(migration.sql); + // eslint-disable-next-line no-console + console.log("Executed migration", JSON.stringify(result2)); + const result3 = await sqlExec( + `INSERT INTO migrations (name) VALUES ('${migration.name}')`, + ); + // eslint-disable-next-line no-console + console.log("Updated migrations table", JSON.stringify(result3)); } } } diff --git a/src/services/platforms/CapacitorPlatformService.ts b/src/services/platforms/CapacitorPlatformService.ts index cd237dfe..cbe6a639 100644 --- a/src/services/platforms/CapacitorPlatformService.ts +++ b/src/services/platforms/CapacitorPlatformService.ts @@ -1,8 +1,3 @@ -import { - ImageResult, - PlatformService, - PlatformCapabilities, -} from "../PlatformService"; import { Filesystem, Directory, Encoding } from "@capacitor/filesystem"; import { Camera, CameraResultType, CameraSource } from "@capacitor/camera"; import { Share } from "@capacitor/share"; @@ -10,15 +5,18 @@ import { SQLiteConnection, SQLiteDBConnection, CapacitorSQLite, + capSQLiteChanges, + DBSQLiteValues, } from "@capacitor-community/sqlite"; -import { logger } from "../../utils/logger"; -import { QueryExecResult } from "@/interfaces/database"; -import { DEFAULT_ENDORSER_API_SERVER } from "@/constants/app"; -interface Migration { - name: string; - sql: string; -} +import { runMigrations } from "@/db-sql/migration"; +import { QueryExecResult } from "@/interfaces/database"; +import { + ImageResult, + PlatformService, + PlatformCapabilities, +} from "../PlatformService"; +import { logger } from "../../utils/logger"; interface QueuedOperation { type: "run" | "query" | "getOneRow" | "getAll"; @@ -39,7 +37,7 @@ interface QueuedOperation { export class CapacitorPlatformService implements PlatformService { private sqlite: SQLiteConnection; private db: SQLiteDBConnection | null = null; - private dbName = "timesafari.db"; + private dbName = "timesafari.sqlite"; private initialized = false; private initializationPromise: Promise | null = null; private operationQueue: Array = []; @@ -95,7 +93,7 @@ export class CapacitorPlatformService implements PlatformService { // await this.db.execute("PRAGMA journal_mode=WAL;"); // Run migrations - await this.runMigrations(); + await this.runCapacitorMigrations(); this.initialized = true; logger.log( @@ -170,7 +168,9 @@ export class CapacitorPlatformService implements PlatformService { } operation.resolve(result); } catch (error) { - logger.error( + // make sure you don't try to log to the DB... infinite loop! + // eslint-disable-next-line no-console + console.error( "[CapacitorPlatformService] Error while processing SQL queue:", error, " ... for sql:", @@ -231,123 +231,23 @@ export class CapacitorPlatformService implements PlatformService { } } - private async runMigrations(): Promise { + private async runCapacitorMigrations(): Promise { if (!this.db) { throw new Error("Database not initialized"); } - // Create migrations table if it doesn't exist - await this.db.execute(` - CREATE TABLE IF NOT EXISTS migrations ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL UNIQUE, - executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ); - `); - - // Get list of executed migrations - const result = await this.db.query("SELECT name FROM migrations;"); - const executedMigrations = new Set( - result.values?.map((row) => row[0]) || [], - ); - - // Run pending migrations in order - const migrations: Migration[] = [ - { - name: "001_initial", - sql: ` - CREATE TABLE IF NOT EXISTS accounts ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - dateCreated TEXT NOT NULL, - derivationPath TEXT, - did TEXT NOT NULL, - identityEncrBase64 TEXT, - mnemonicEncrBase64 TEXT, - passkeyCredIdHex TEXT, - publicKeyHex TEXT NOT NULL - ); - - CREATE INDEX IF NOT EXISTS idx_accounts_did ON accounts(did); - - CREATE TABLE IF NOT EXISTS secret ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - secretBase64 TEXT NOT NULL - ); - - CREATE TABLE IF NOT EXISTS settings ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - accountDid TEXT, - activeDid TEXT, - apiServer TEXT, - filterFeedByNearby BOOLEAN, - filterFeedByVisible BOOLEAN, - finishedOnboarding BOOLEAN, - firstName TEXT, - hideRegisterPromptOnNewContact BOOLEAN, - isRegistered BOOLEAN, - lastName TEXT, - lastAckedOfferToUserJwtId TEXT, - lastAckedOfferToUserProjectsJwtId TEXT, - lastNotifiedClaimId TEXT, - lastViewedClaimId TEXT, - notifyingNewActivityTime TEXT, - notifyingReminderMessage TEXT, - notifyingReminderTime TEXT, - partnerApiServer TEXT, - passkeyExpirationMinutes INTEGER, - profileImageUrl TEXT, - searchBoxes TEXT, - showContactGivesInline BOOLEAN, - showGeneralAdvanced BOOLEAN, - showShortcutBvc BOOLEAN, - vapid TEXT, - warnIfProdServer BOOLEAN, - warnIfTestServer BOOLEAN, - webPushServer TEXT - ); - - CREATE INDEX IF NOT EXISTS idx_settings_accountDid ON settings(accountDid); - - INSERT INTO settings (id, apiServer) VALUES (1, '${DEFAULT_ENDORSER_API_SERVER}'); - - CREATE TABLE IF NOT EXISTS contacts ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - did TEXT NOT NULL, - name TEXT, - contactMethods TEXT, - nextPubKeyHashB64 TEXT, - notes TEXT, - profileImageUrl TEXT, - publicKeyBase64 TEXT, - seesMe BOOLEAN, - registered BOOLEAN - ); - - CREATE INDEX IF NOT EXISTS idx_contacts_did ON contacts(did); - CREATE INDEX IF NOT EXISTS idx_contacts_name ON contacts(name); - - CREATE TABLE IF NOT EXISTS logs ( - date TEXT, - message TEXT NOT NULL - ); - - CREATE TABLE IF NOT EXISTS temp ( - id TEXT PRIMARY KEY, - blobB64 TEXT - ); - `, - }, - ]; - - for (const migration of migrations) { - if (!executedMigrations.has(migration.name)) { - await this.db.execute(migration.sql); - await this.db.run("INSERT INTO migrations (name) VALUES (?)", [ - migration.name, - ]); - logger.log(`Migration ${migration.name} executed successfully`); - } - } + const extractMigrationNames: (result: DBSQLiteValues) => Set = ( + result, + ) => { + const names = + result.values?.map((row: unknown[]) => row[0] as string) || []; + return new Set(names); + }; + const sqlExec: (sql: string) => Promise = + this.db.execute.bind(this.db); + const sqlQuery: (sql: string) => Promise = + this.db.query.bind(this.db); + runMigrations(sqlExec, sqlQuery, extractMigrationNames); } /** -- 2.30.2 From aa177a9b8c360110c49d2568e7637698779525c7 Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Thu, 5 Jun 2025 18:13:33 -0600 Subject: [PATCH 03/20] fix extraction of migration names for SQLite via Capacitor --- src/main.common.ts | 4 ++-- src/services/platforms/CapacitorPlatformService.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main.common.ts b/src/main.common.ts index 93891f01..cb44cd6f 100644 --- a/src/main.common.ts +++ b/src/main.common.ts @@ -13,8 +13,8 @@ import { logger } from "./utils/logger"; const platform = process.env.VITE_PLATFORM; const pwa_enabled = process.env.VITE_PWA_ENABLED === "true"; -logger.error("Platform", JSON.stringify({ platform })); -logger.error("PWA enabled", JSON.stringify({ pwa_enabled })); +logger.log("Platform", JSON.stringify({ platform })); +logger.log("PWA enabled", JSON.stringify({ pwa_enabled })); // Global Error Handler function setupGlobalErrorHandler(app: VueApp) { diff --git a/src/services/platforms/CapacitorPlatformService.ts b/src/services/platforms/CapacitorPlatformService.ts index cbe6a639..320c8cc8 100644 --- a/src/services/platforms/CapacitorPlatformService.ts +++ b/src/services/platforms/CapacitorPlatformService.ts @@ -240,7 +240,7 @@ export class CapacitorPlatformService implements PlatformService { result, ) => { const names = - result.values?.map((row: unknown[]) => row[0] as string) || []; + result.values?.map((row: { name: string }) => row.name) || []; return new Set(names); }; const sqlExec: (sql: string) => Promise = -- 2.30.2 From a3951c9d66f35a945e7864dae37d92e5ec1e44cf Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Thu, 5 Jun 2025 18:30:25 -0600 Subject: [PATCH 04/20] refactor for clarity (no logic changes) --- src/services/AbsurdSqlDatabaseService.ts | 11 ++--------- src/services/platforms/CapacitorPlatformService.ts | 8 ++++---- 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/src/services/AbsurdSqlDatabaseService.ts b/src/services/AbsurdSqlDatabaseService.ts index 6d7dd9c9..ee2f00e3 100644 --- a/src/services/AbsurdSqlDatabaseService.ts +++ b/src/services/AbsurdSqlDatabaseService.ts @@ -107,16 +107,9 @@ class AbsurdSqlDatabaseService implements DatabaseService { const extractMigrationNames: (result: QueryExecResult[]) => Set = ( result, ) => { - const queryResult = result as QueryExecResult[]; // Even with the "select name" query, the QueryExecResult may be [] (which doesn't make sense to me). - if (queryResult.length > 0) { - const singleResult = queryResult[0]; - const executedMigrations: Set = new Set( - singleResult.values.map((row) => row[0] as string), - ); - return executedMigrations; - } - return new Set(); + const names = result?.[0]?.values.map((row) => row[0] as string) || []; + return new Set(names); }; // Run migrations diff --git a/src/services/platforms/CapacitorPlatformService.ts b/src/services/platforms/CapacitorPlatformService.ts index 320c8cc8..2fc08599 100644 --- a/src/services/platforms/CapacitorPlatformService.ts +++ b/src/services/platforms/CapacitorPlatformService.ts @@ -236,6 +236,10 @@ export class CapacitorPlatformService implements PlatformService { throw new Error("Database not initialized"); } + const sqlExec: (sql: string) => Promise = + this.db.execute.bind(this.db); + const sqlQuery: (sql: string) => Promise = + this.db.query.bind(this.db); const extractMigrationNames: (result: DBSQLiteValues) => Set = ( result, ) => { @@ -243,10 +247,6 @@ export class CapacitorPlatformService implements PlatformService { result.values?.map((row: { name: string }) => row.name) || []; return new Set(names); }; - const sqlExec: (sql: string) => Promise = - this.db.execute.bind(this.db); - const sqlQuery: (sql: string) => Promise = - this.db.query.bind(this.db); runMigrations(sqlExec, sqlQuery, extractMigrationNames); } -- 2.30.2 From 5bb563d69486b52a29805eb4a45a0f761aa993f3 Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Thu, 5 Jun 2025 19:57:59 -0600 Subject: [PATCH 05/20] fix extraction of values from SQLite queries --- src/services/platforms/CapacitorPlatformService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/platforms/CapacitorPlatformService.ts b/src/services/platforms/CapacitorPlatformService.ts index 2fc08599..eb142196 100644 --- a/src/services/platforms/CapacitorPlatformService.ts +++ b/src/services/platforms/CapacitorPlatformService.ts @@ -145,7 +145,7 @@ export class CapacitorPlatformService implements PlatformService { ); result = { columns: [], // SQLite plugin doesn't provide column names - values: queryResult.values || [], + values: (queryResult.values || []).map((row) => Object.values(row)), }; break; } -- 2.30.2 From 6e1fcd8dee28ae603fb91ed1dbf0b30500ee1531 Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Thu, 5 Jun 2025 20:00:51 -0600 Subject: [PATCH 06/20] remove unused DB methods (for now) --- src/interfaces/database.ts | 2 -- src/services/AbsurdSqlDatabaseService.ts | 26 ++----------------- .../platforms/CapacitorPlatformService.ts | 22 +++------------- 3 files changed, 6 insertions(+), 44 deletions(-) diff --git a/src/interfaces/database.ts b/src/interfaces/database.ts index 4dda80ef..7f491e6b 100644 --- a/src/interfaces/database.ts +++ b/src/interfaces/database.ts @@ -12,6 +12,4 @@ export interface DatabaseService { sql: string, params?: unknown[], ): Promise<{ changes: number; lastId?: number }>; - getOneRow(sql: string, params?: unknown[]): Promise; - getAll(sql: string, params?: unknown[]): Promise; } diff --git a/src/services/AbsurdSqlDatabaseService.ts b/src/services/AbsurdSqlDatabaseService.ts index ee2f00e3..0b107280 100644 --- a/src/services/AbsurdSqlDatabaseService.ts +++ b/src/services/AbsurdSqlDatabaseService.ts @@ -7,7 +7,7 @@ import type { DatabaseService, QueryExecResult } from "../interfaces/database"; import { logger } from "@/utils/logger"; interface QueuedOperation { - type: "run" | "query" | "getOneRow" | "getAll"; + type: "run" | "query"; sql: string; params: unknown[]; resolve: (value: unknown) => void; @@ -84,7 +84,7 @@ class AbsurdSqlDatabaseService implements DatabaseService { SQL.FS.mkdir("/sql"); SQL.FS.mount(sqlFS, {}, "/sql"); - const path = "/sql/timesafari.sqlite"; + const path = "/sql/timesafari.absurd-sql"; if (typeof SharedArrayBuffer === "undefined") { const stream = SQL.FS.open(path, "a+"); await stream.node.contents.readIfFallback(); @@ -133,7 +133,6 @@ class AbsurdSqlDatabaseService implements DatabaseService { if (!operation) continue; try { - let queryResult: QueryExecResult[] = []; let result: unknown; switch (operation.type) { case "run": @@ -142,14 +141,6 @@ class AbsurdSqlDatabaseService implements DatabaseService { case "query": result = await this.db.exec(operation.sql, operation.params); break; - case "getOneRow": - queryResult = await this.db.exec(operation.sql, operation.params); - result = queryResult[0]?.values[0]; - break; - case "getAll": - queryResult = await this.db.exec(operation.sql, operation.params); - result = queryResult[0]?.values || []; - break; } operation.resolve(result); } catch (error) { @@ -232,19 +223,6 @@ class AbsurdSqlDatabaseService implements DatabaseService { await this.waitForInitialization(); return this.queueOperation("query", sql, params); } - - async getOneRow( - sql: string, - params: unknown[] = [], - ): Promise { - await this.waitForInitialization(); - return this.queueOperation("getOneRow", sql, params); - } - - async getAll(sql: string, params: unknown[] = []): Promise { - await this.waitForInitialization(); - return this.queueOperation("getAll", sql, params); - } } // Create a singleton instance diff --git a/src/services/platforms/CapacitorPlatformService.ts b/src/services/platforms/CapacitorPlatformService.ts index eb142196..32579b85 100644 --- a/src/services/platforms/CapacitorPlatformService.ts +++ b/src/services/platforms/CapacitorPlatformService.ts @@ -19,7 +19,7 @@ import { import { logger } from "../../utils/logger"; interface QueuedOperation { - type: "run" | "query" | "getOneRow" | "getAll"; + type: "run" | "query"; sql: string; params: unknown[]; resolve: (value: unknown) => void; @@ -145,26 +145,12 @@ export class CapacitorPlatformService implements PlatformService { ); result = { columns: [], // SQLite plugin doesn't provide column names - values: (queryResult.values || []).map((row) => Object.values(row)), + values: (queryResult.values || []).map((row) => + Object.values(row), + ), }; break; } - case "getOneRow": { - const oneRowResult = await this.db.query( - operation.sql, - operation.params, - ); - result = oneRowResult.values?.[0]; - break; - } - case "getAll": { - const allResult = await this.db.query( - operation.sql, - operation.params, - ); - result = allResult.values || []; - break; - } } operation.resolve(result); } catch (error) { -- 2.30.2 From c5228ba7ecd8b022c9d0dd22b82d04626817bde8 Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Thu, 5 Jun 2025 20:06:32 -0600 Subject: [PATCH 07/20] fix retrieval of column names -- so now most ops are working (but not all, eg. set name) --- src/services/platforms/CapacitorPlatformService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/platforms/CapacitorPlatformService.ts b/src/services/platforms/CapacitorPlatformService.ts index 32579b85..cbf83f79 100644 --- a/src/services/platforms/CapacitorPlatformService.ts +++ b/src/services/platforms/CapacitorPlatformService.ts @@ -144,7 +144,7 @@ export class CapacitorPlatformService implements PlatformService { operation.params, ); result = { - columns: [], // SQLite plugin doesn't provide column names + columns: Object.keys(queryResult.values?.[0] || {}), values: (queryResult.values || []).map((row) => Object.values(row), ), -- 2.30.2 From 1d7f6266458cb3446aedd4738cd11ff50406359f Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Thu, 5 Jun 2025 20:11:32 -0600 Subject: [PATCH 08/20] fix SQL references to bad "key" -> "id" --- src/components/UserNameDialog.vue | 2 +- src/views/ContactQRScanShowView.vue | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/UserNameDialog.vue b/src/components/UserNameDialog.vue index af504930..36191771 100644 --- a/src/components/UserNameDialog.vue +++ b/src/components/UserNameDialog.vue @@ -74,7 +74,7 @@ export default class UserNameDialog extends Vue { async onClickSaveChanges() { const platformService = PlatformServiceFactory.getInstance(); await platformService.dbExec( - "UPDATE settings SET firstName = ? WHERE key = ?", + "UPDATE settings SET firstName = ? WHERE id = ?", [this.givenName, MASTER_SETTINGS_KEY], ); if (USE_DEXIE_DB) { diff --git a/src/views/ContactQRScanShowView.vue b/src/views/ContactQRScanShowView.vue index d4f6ae9e..ae1d2e6c 100644 --- a/src/views/ContactQRScanShowView.vue +++ b/src/views/ContactQRScanShowView.vue @@ -848,7 +848,7 @@ export default class ContactQRScanShow extends Vue { if (stopAsking) { const platformService = PlatformServiceFactory.getInstance(); await platformService.dbExec( - "UPDATE settings SET hideRegisterPromptOnNewContact = ? WHERE key = ?", + "UPDATE settings SET hideRegisterPromptOnNewContact = ? WHERE id = ?", [stopAsking, MASTER_SETTINGS_KEY], ); if (USE_DEXIE_DB) { @@ -863,7 +863,7 @@ export default class ContactQRScanShow extends Vue { if (stopAsking) { const platformService = PlatformServiceFactory.getInstance(); await platformService.dbExec( - "UPDATE settings SET hideRegisterPromptOnNewContact = ? WHERE key = ?", + "UPDATE settings SET hideRegisterPromptOnNewContact = ? WHERE id = ?", [stopAsking, MASTER_SETTINGS_KEY], ); if (USE_DEXIE_DB) { -- 2.30.2 From 9d4f726c3153a3006137711a830c2a68b17375bf Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Thu, 5 Jun 2025 20:30:27 -0600 Subject: [PATCH 09/20] bump to build # 19 version 0.4.7 for mobile packages --- BUILDING.md | 4 ++-- CHANGELOG.md | 7 +++++++ android/app/build.gradle | 2 +- ios/App/App.xcodeproj/project.pbxproj | 4 ++-- package.json | 2 +- 5 files changed, 13 insertions(+), 6 deletions(-) diff --git a/BUILDING.md b/BUILDING.md index 62fb27e8..8e21d218 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -356,7 +356,7 @@ Prerequisites: macOS with Xcode installed xcrun agvtool new-version 15 # 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.4.5;/g" > temp + cat App.xcodeproj/project.pbxproj | sed "s/MARKETING_VERSION = .*;/MARKETING_VERSION = 0.4.7;/g" > temp mv temp App.xcodeproj/project.pbxproj cd - ``` @@ -374,7 +374,7 @@ Prerequisites: macOS with Xcode installed 7. Release * Under "General" renamed a bunch of things to "Time Safari" - * Choose Product -> Destination -> Build Any iOS + * Choose Product -> Destination -> Any iOS Device * Choose Product -> Archive * This will trigger a build and take time, needing user's "login" keychain password which is just their login password, repeatedly. * If it fails with `building for 'iOS', but linking in dylib (.../.pkgx/zlib.net/v1.3.0/lib/libz.1.3.dylib) built for 'macOS'` then run XCode outside that terminal (ie. not with `npx cap open ios`). diff --git a/CHANGELOG.md b/CHANGELOG.md index c4c03a02..71657a57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 +## [0.4.7] +### Fixed +- Cameras everywhere +### Changed +- IndexedDB -> SQLite + + ## [0.4.5] - 2025.02.23 ### Added - Total amounts of gives on project page diff --git a/android/app/build.gradle b/android/app/build.gradle index 6bd27bb8..fb01f52d 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -31,7 +31,7 @@ android { applicationId "app.timesafari.app" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 18 + versionCode 19 versionName "0.4.7" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" aaptOptions { diff --git a/ios/App/App.xcodeproj/project.pbxproj b/ios/App/App.xcodeproj/project.pbxproj index 0c89bf53..45b03396 100644 --- a/ios/App/App.xcodeproj/project.pbxproj +++ b/ios/App/App.xcodeproj/project.pbxproj @@ -384,7 +384,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 18; + CURRENT_PROJECT_VERSION = 19; DEVELOPMENT_TEAM = GM3FS5JQPH; ENABLE_APP_SANDBOX = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO; @@ -411,7 +411,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 18; + CURRENT_PROJECT_VERSION = 19; DEVELOPMENT_TEAM = GM3FS5JQPH; ENABLE_APP_SANDBOX = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO; diff --git a/package.json b/package.json index b9fafaae..22988a40 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "timesafari", - "version": "0.4.6", + "version": "0.4.7", "description": "Time Safari Application", "author": { "name": "Time Safari Team" -- 2.30.2 From c1f2c3951abbbada25868c13690ecce513b9a400 Mon Sep 17 00:00:00 2001 From: Matthew Raymer Date: Fri, 6 Jun 2025 09:22:35 +0000 Subject: [PATCH 10/20] feat(db): improve settings retrieval resilience and logging Enhance retrieveSettingsForActiveAccount with better error handling and logging while maintaining core functionality. Changes focus on making the system more debuggable and resilient without overcomplicating the logic. Key improvements: - Add structured error handling with specific try-catch blocks - Implement detailed logging with [databaseUtil] prefix for easy filtering - Add graceful fallbacks for searchBoxes parsing and missing settings - Improve error recovery paths with safe defaults - Maintain existing security model and data integrity Security: - No sensitive data in logs - Safe JSON parsing with fallbacks - Proper error boundaries - Consistent state management - Clear fallback paths Testing: - Verify settings retrieval works with/without active DID - Check error handling for invalid searchBoxes - Confirm logging provides clear debugging context - Validate fallback to default settings works --- package-lock.json | 4 +- src/db/databaseUtil.ts | 107 +++++++--- src/libs/util.ts | 39 ++-- .../platforms/CapacitorPlatformService.ts | 4 + src/views/AccountViewView.vue | 2 + src/views/HomeView.vue | 184 +++++++++++------- src/views/IdentitySwitcherView.vue | 21 +- 7 files changed, 253 insertions(+), 108 deletions(-) diff --git a/package-lock.json b/package-lock.json index 579ccd38..4e5c1b11 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "timesafari", - "version": "0.4.6", + "version": "0.4.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "timesafari", - "version": "0.4.6", + "version": "0.4.7", "dependencies": { "@capacitor-community/sqlite": "6.0.2", "@capacitor-mlkit/barcode-scanning": "^6.0.0", diff --git a/src/db/databaseUtil.ts b/src/db/databaseUtil.ts index a0fdd87e..69c80b05 100644 --- a/src/db/databaseUtil.ts +++ b/src/db/databaseUtil.ts @@ -79,10 +79,13 @@ const DEFAULT_SETTINGS: Settings = { // retrieves default settings export async function retrieveSettingsForDefaultAccount(): Promise { + console.log("[databaseUtil] retrieveSettingsForDefaultAccount"); const platform = PlatformServiceFactory.getInstance(); - const result = await platform.dbQuery("SELECT * FROM settings WHERE id = ?", [ - MASTER_SETTINGS_KEY, - ]); + const sql = "SELECT * FROM settings WHERE id = ?"; + console.log("[databaseUtil] sql", sql); + const result = await platform.dbQuery(sql, [MASTER_SETTINGS_KEY]); + console.log("[databaseUtil] result", JSON.stringify(result, null, 2)); + console.trace("Trace from [retrieveSettingsForDefaultAccount]"); if (!result) { return DEFAULT_SETTINGS; } else { @@ -98,28 +101,86 @@ export async function retrieveSettingsForDefaultAccount(): Promise { } } +/** + * Retrieves settings for the active account, merging with default settings + * + * @returns Promise Combined settings with account-specific overrides + * @throws Will log specific errors for debugging but returns default settings on failure + */ export async function retrieveSettingsForActiveAccount(): Promise { - const defaultSettings = await retrieveSettingsForDefaultAccount(); - if (!defaultSettings.activeDid) { - return defaultSettings; - } else { - const platform = PlatformServiceFactory.getInstance(); - const result = await platform.dbQuery( - "SELECT * FROM settings WHERE accountDid = ?", - [defaultSettings.activeDid], - ); - const overrideSettings = result - ? (mapColumnsToValues(result.columns, result.values)[0] as Settings) - : {}; - const overrideSettingsFiltered = Object.fromEntries( - Object.entries(overrideSettings).filter(([_, v]) => v !== null), - ); - const settings = { ...defaultSettings, ...overrideSettingsFiltered }; - if (settings.searchBoxes) { - // @ts-expect-error - the searchBoxes field is a string in the DB - settings.searchBoxes = JSON.parse(settings.searchBoxes); + logConsoleAndDb("[databaseUtil] Starting settings retrieval for active account"); + + try { + // Get default settings first + const defaultSettings = await retrieveSettingsForDefaultAccount(); + logConsoleAndDb(`[databaseUtil] Retrieved default settings (hasActiveDid: ${!!defaultSettings.activeDid})`); + + // If no active DID, return defaults + if (!defaultSettings.activeDid) { + logConsoleAndDb("[databaseUtil] No active DID found, returning default settings"); + return defaultSettings; } - return settings; + + // Get account-specific settings + try { + const platform = PlatformServiceFactory.getInstance(); + const result = await platform.dbQuery( + "SELECT * FROM settings WHERE accountDid = ?", + [defaultSettings.activeDid], + ); + + if (!result?.values?.length) { + logConsoleAndDb(`[databaseUtil] No account-specific settings found for ${defaultSettings.activeDid}`); + return defaultSettings; + } + + // Map and filter settings + const overrideSettings = mapColumnsToValues(result.columns, result.values)[0] as Settings; + const overrideSettingsFiltered = Object.fromEntries( + Object.entries(overrideSettings).filter(([_, v]) => v !== null), + ); + + // Merge settings + const settings = { ...defaultSettings, ...overrideSettingsFiltered }; + + // Handle searchBoxes parsing + if (settings.searchBoxes) { + try { + // @ts-expect-error - the searchBoxes field is a string in the DB + settings.searchBoxes = JSON.parse(settings.searchBoxes); + } catch (error) { + logConsoleAndDb( + `[databaseUtil] Failed to parse searchBoxes for ${defaultSettings.activeDid}: ${error}`, + true + ); + // Reset to empty array on parse failure + settings.searchBoxes = []; + } + } + + logConsoleAndDb( + `[databaseUtil] Successfully merged settings for ${defaultSettings.activeDid} ` + + `(overrides: ${Object.keys(overrideSettingsFiltered).length})` + ); + return settings; + + } catch (error) { + logConsoleAndDb( + `[databaseUtil] Failed to retrieve account settings for ${defaultSettings.activeDid}: ${error}`, + true + ); + // Return defaults on error + return defaultSettings; + } + + } catch (error) { + logConsoleAndDb(`[databaseUtil] Failed to retrieve default settings: ${error}`, true); + // Return minimal default settings on complete failure + return { + id: MASTER_SETTINGS_KEY, + activeDid: undefined, + apiServer: DEFAULT_ENDORSER_API_SERVER, + }; } } diff --git a/src/libs/util.ts b/src/libs/util.ts index b93f38c5..035bd164 100644 --- a/src/libs/util.ts +++ b/src/libs/util.ts @@ -548,14 +548,20 @@ export const retrieveAccountMetadata = async ( }; export const retrieveAllAccountsMetadata = async (): Promise => { + console.log("[retrieveAllAccountsMetadata] start"); const platformService = PlatformServiceFactory.getInstance(); - const dbAccounts = await platformService.dbQuery(`SELECT * FROM accounts`); + const sql = `SELECT * FROM accounts`; + console.log("[retrieveAllAccountsMetadata] sql: ", sql); + const dbAccounts = await platformService.dbQuery(sql); + console.log("[retrieveAllAccountsMetadata] dbAccounts: ", dbAccounts); const accounts = databaseUtil.mapQueryResultToValues(dbAccounts) as Account[]; let result = accounts.map((account) => { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { identity, mnemonic, ...metadata } = account; return metadata as Account; }); + console.log("[retrieveAllAccountsMetadata] result: ", result); + console.log("[retrieveAllAccountsMetadata] USE_DEXIE_DB: ", USE_DEXIE_DB); if (USE_DEXIE_DB) { // one of the few times we use accountsDBPromise directly; try to avoid more usage const accountsDB = await accountsDBPromise; @@ -566,6 +572,7 @@ export const retrieveAllAccountsMetadata = async (): Promise => { return metadata as Account; }); } + console.log("[retrieveAllAccountsMetadata] end", JSON.stringify(result, null, 2)); return result; }; @@ -646,6 +653,10 @@ export async function saveNewIdentity( derivationPath: string, ): Promise { try { + console.log("[saveNewIdentity] identity", identity); + console.log("[saveNewIdentity] mnemonic", mnemonic); + console.log("[saveNewIdentity] newId", newId); + console.log("[saveNewIdentity] derivationPath", derivationPath); // add to the new sql db const platformService = PlatformServiceFactory.getInstance(); const secrets = await platformService.dbQuery( @@ -662,18 +673,19 @@ export async function saveNewIdentity( const encryptedMnemonic = await simpleEncrypt(mnemonic, secret); const encryptedIdentityBase64 = arrayBufferToBase64(encryptedIdentity); const encryptedMnemonicBase64 = arrayBufferToBase64(encryptedMnemonic); - await platformService.dbExec( - `INSERT INTO accounts (dateCreated, derivationPath, did, identityEncrBase64, mnemonicEncrBase64, publicKeyHex) - VALUES (?, ?, ?, ?, ?, ?)`, - [ - new Date().toISOString(), - derivationPath, - newId.did, - encryptedIdentityBase64, - encryptedMnemonicBase64, - newId.keys[0].publicKeyHex, - ], - ); + const sql = `INSERT INTO accounts (dateCreated, derivationPath, did, identityEncrBase64, mnemonicEncrBase64, publicKeyHex) + VALUES (?, ?, ?, ?, ?, ?)`; + console.log("[saveNewIdentity] sql: ", sql); + const params = [ + new Date().toISOString(), + derivationPath, + newId.did, + encryptedIdentityBase64, + encryptedMnemonicBase64, + newId.keys[0].publicKeyHex, + ]; + console.log("[saveNewIdentity] params: ", params); + await platformService.dbExec(sql, params); await databaseUtil.updateDefaultSettings({ activeDid: newId.did }); if (USE_DEXIE_DB) { @@ -690,6 +702,7 @@ export async function saveNewIdentity( await updateDefaultSettings({ activeDid: newId.did }); } } catch (error) { + console.log("[saveNewIdentity] error: ", error); logger.error("Failed to update default settings:", error); throw new Error( "Failed to set default settings. Please try again or restart the app.", diff --git a/src/services/platforms/CapacitorPlatformService.ts b/src/services/platforms/CapacitorPlatformService.ts index cbf83f79..1ce0bca7 100644 --- a/src/services/platforms/CapacitorPlatformService.ts +++ b/src/services/platforms/CapacitorPlatformService.ts @@ -128,6 +128,8 @@ export class CapacitorPlatformService implements PlatformService { let result: unknown; switch (operation.type) { case "run": { + console.log("[CapacitorPlatformService] running sql:", operation.sql); + console.log("[CapacitorPlatformService] params:", operation.params); const runResult = await this.db.run( operation.sql, operation.params, @@ -139,6 +141,8 @@ export class CapacitorPlatformService implements PlatformService { break; } case "query": { + console.log("[CapacitorPlatformService] querying sql:", operation.sql); + console.log("[CapacitorPlatformService] params:", operation.params); const queryResult = await this.db.query( operation.sql, operation.params, diff --git a/src/views/AccountViewView.vue b/src/views/AccountViewView.vue index 22f342f1..66e4bf74 100644 --- a/src/views/AccountViewView.vue +++ b/src/views/AccountViewView.vue @@ -1119,6 +1119,7 @@ export default class AccountViewView extends Vue { */ async mounted() { try { + console.log("[AccountViewView] mounted"); // Initialize component state with values from the database or defaults await this.initializeState(); await this.processIdentity(); @@ -1171,6 +1172,7 @@ export default class AccountViewView extends Vue { } } } catch (error) { + console.log("[AccountViewView] error: ", JSON.stringify(error, null, 2)); // this can happen when running automated tests in dev mode because notifications don't work logger.error( "Telling user to clear cache at page create because:", diff --git a/src/views/HomeView.vue b/src/views/HomeView.vue index 4bd03745..e42f0929 100644 --- a/src/views/HomeView.vue +++ b/src/views/HomeView.vue @@ -515,49 +515,85 @@ export default class HomeView extends Vue { */ private async initializeIdentity() { try { - this.allMyDids = await retrieveAccountDids(); + // 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("Failed to load existing identities. Please try restarting the app."); + } + + // Create new DID if needed if (this.allMyDids.length === 0) { - this.isCreatingIdentifier = true; - const newDid = await generateSaveAndActivateIdentity(); - this.isCreatingIdentifier = false; - this.allMyDids = [newDid]; + try { + this.isCreatingIdentifier = true; + const newDid = await generateSaveAndActivateIdentity(); + this.isCreatingIdentifier = false; + this.allMyDids = [newDid]; + logConsoleAndDb(`[HomeView] Created new identity: ${newDid}`); + } catch (error) { + this.isCreatingIdentifier = false; + logConsoleAndDb(`[HomeView] Failed to create new identity: ${error}`, true); + throw new Error("Failed to create new identity. Please try again."); + } } - let settings = await databaseUtil.retrieveSettingsForActiveAccount(); - if (USE_DEXIE_DB) { - settings = await retrieveSettingsForActiveAccount(); + // Load settings with better error context + let settings; + try { + settings = await databaseUtil.retrieveSettingsForActiveAccount(); + 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}`, true); + throw new Error("Failed to load user settings. Some features may be limited."); } + + // Update component state this.apiServer = settings.apiServer || ""; this.activeDid = settings.activeDid || ""; - const platformService = PlatformServiceFactory.getInstance(); - const dbContacts = await platformService.dbQuery( - "SELECT * FROM contacts", - ); - this.allContacts = databaseUtil.mapQueryResultToValues( - dbContacts, - ) as unknown as Contact[]; - if (USE_DEXIE_DB) { - this.allContacts = await db.contacts.toArray(); + + // Load contacts with graceful fallback + try { + const platformService = PlatformServiceFactory.getInstance(); + const dbContacts = await platformService.dbQuery("SELECT * FROM contacts"); + this.allContacts = databaseUtil.mapQueryResultToValues(dbContacts) as Contact[]; + 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}`, true); + this.allContacts = []; // Ensure we have a valid empty array + this.$notify({ + group: "alert", + type: "warning", + title: "Contact Loading Issue", + text: "Some contact information may be unavailable.", + }, 5000); } + + // Update remaining settings this.feedLastViewedClaimId = settings.lastViewedClaimId; this.givenName = settings.firstName || ""; this.isFeedFilteredByVisible = !!settings.filterFeedByVisible; this.isFeedFilteredByNearby = !!settings.filterFeedByNearby; this.isRegistered = !!settings.isRegistered; this.lastAckedOfferToUserJwtId = settings.lastAckedOfferToUserJwtId; - this.lastAckedOfferToUserProjectsJwtId = - settings.lastAckedOfferToUserProjectsJwtId; + this.lastAckedOfferToUserProjectsJwtId = settings.lastAckedOfferToUserProjectsJwtId; this.searchBoxes = settings.searchBoxes || []; this.showShortcutBvc = !!settings.showShortcutBvc; this.isAnyFeedFilterOn = checkIsAnyFeedFilterOn(settings); + // Check onboarding status if (!settings.finishedOnboarding) { - (this.$refs.onboardingDialog as OnboardingDialog).open( - OnboardPage.Home, - ); + (this.$refs.onboardingDialog as OnboardingDialog).open(OnboardPage.Home); } - // someone may have have registered after sharing contact info, so recheck + // Check registration status if needed if (!this.isRegistered && this.activeDid) { try { const resp = await fetchEndorserRateLimits( @@ -577,51 +613,62 @@ export default class HomeView extends Vue { }); } this.isRegistered = true; + logConsoleAndDb(`[HomeView] User ${this.activeDid} is now registered`); } - } catch (e) { - // ignore the error... just keep us unregistered + } catch (error) { + logConsoleAndDb(`[HomeView] Registration check failed: ${error}`, true); + // Continue as unregistered - this is expected for new users } } - // this returns a Promise but we don't need to wait for it - this.updateAllFeed(); - - if (this.activeDid) { - const offersToUserData = await getNewOffersToUser( - this.axios, - this.apiServer, - this.activeDid, - this.lastAckedOfferToUserJwtId, - ); - this.numNewOffersToUser = offersToUserData.data.length; - this.newOffersToUserHitLimit = offersToUserData.hitLimit; - } + // Initialize feed and offers + try { + // Start feed update in background + this.updateAllFeed().catch(error => { + logConsoleAndDb(`[HomeView] Background feed update failed: ${error}`, true); + }); - if (this.activeDid) { - const offersToUserProjects = await getNewOffersToUserProjects( - this.axios, - this.apiServer, - this.activeDid, - this.lastAckedOfferToUserProjectsJwtId, - ); - this.numNewOffersToUserProjects = offersToUserProjects.data.length; - this.newOffersToUserProjectsHitLimit = offersToUserProjects.hitLimit; + // Load new offers if we have an active DID + if (this.activeDid) { + const [offersToUser, offersToProjects] = await Promise.all([ + getNewOffersToUser( + this.axios, + this.apiServer, + this.activeDid, + this.lastAckedOfferToUserJwtId, + ), + getNewOffersToUserProjects( + this.axios, + this.apiServer, + this.activeDid, + this.lastAckedOfferToUserProjectsJwtId, + ), + ]); + + this.numNewOffersToUser = offersToUser.data.length; + 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(`[HomeView] Failed to initialize feed/offers: ${error}`, true); + // Don't throw - we can continue with empty feed + this.$notify({ + group: "alert", + type: "warning", + title: "Feed Loading Issue", + text: "Some feed data may be unavailable. Pull to refresh.", + }, 5000); } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } catch (err: any) { - logConsoleAndDb("Error retrieving settings or feed: " + err, true); - this.$notify( - { - group: "alert", - type: "danger", - title: "Error", - text: - (err as { userMessage?: string })?.userMessage || - "There was an error retrieving your settings or the latest activity.", - }, - 5000, - ); + } catch (error) { + this.handleError(error); + throw error; // Re-throw to be caught by mounted() } } @@ -784,19 +831,24 @@ export default class HomeView extends Vue { * - Displays user notification * * @internal - * Called by mounted() + * Called by mounted() and initializeIdentity() * @param err Error object with optional userMessage */ private handleError(err: unknown) { - logConsoleAndDb("Error retrieving settings or feed: " + err, true); + const errorMessage = err instanceof Error ? err.message : String(err); + const userMessage = (err as { userMessage?: string })?.userMessage; + + logConsoleAndDb( + `[HomeView] Initialization error: ${errorMessage}${userMessage ? ` (${userMessage})` : ''}`, + true + ); + this.$notify( { group: "alert", type: "danger", title: "Error", - text: - (err as { userMessage?: string })?.userMessage || - "There was an error retrieving your settings or the latest activity.", + text: userMessage || "There was an error loading your data. Please try refreshing the page.", }, 5000, ); diff --git a/src/views/IdentitySwitcherView.vue b/src/views/IdentitySwitcherView.vue index 143506e8..cfb6908b 100644 --- a/src/views/IdentitySwitcherView.vue +++ b/src/views/IdentitySwitcherView.vue @@ -115,6 +115,7 @@ import { MASTER_SETTINGS_KEY } from "../db/tables/settings"; import * as databaseUtil from "../db/databaseUtil"; import { retrieveAllAccountsMetadata } from "../libs/util"; import { logger } from "../utils/logger"; +import { PlatformServiceFactory } from "@/services/PlatformServiceFactory"; @Component({ components: { QuickNav } }) export default class IdentitySwitcherView extends Vue { @@ -138,8 +139,10 @@ export default class IdentitySwitcherView extends Vue { this.apiServerInput = settings.apiServer || ""; const accounts = await retrieveAllAccountsMetadata(); + console.log("[IdentitySwitcherView] accounts: ", JSON.stringify(accounts, null, 2)); for (let n = 0; n < accounts.length; n++) { const acct = accounts[n]; + console.log("[IdentitySwitcherView] acct: ", JSON.stringify(acct, null, 2)); this.otherIdentities.push({ id: (acct.id ?? 0).toString(), did: acct.did, @@ -149,6 +152,7 @@ export default class IdentitySwitcherView extends Vue { } } } catch (err) { + console.log("[IdentitySwitcherView] error: ", JSON.stringify(err, null, 2)); this.$notify( { group: "alert", @@ -160,6 +164,7 @@ export default class IdentitySwitcherView extends Vue { ); logger.error("Telling user to clear cache at page create because:", err); } + console.log("[IdentitySwitcherView] end"); } async switchAccount(did?: string) { @@ -167,10 +172,18 @@ export default class IdentitySwitcherView extends Vue { if (did === "0") { did = undefined; } - await db.open(); - await db.settings.update(MASTER_SETTINGS_KEY, { - activeDid: did, - }); + if (USE_DEXIE_DB) { + await db.open(); + await db.settings.update(MASTER_SETTINGS_KEY, { + activeDid: did ?? "", + }); + } else { + const platformService = PlatformServiceFactory.getInstance(); + await platformService.dbExec( + `UPDATE settings SET activeDid = ? WHERE id = ?`, + [did ?? "", MASTER_SETTINGS_KEY], + ); + } this.$router.push({ name: "account" }); } -- 2.30.2 From fc50a9d4c68cbb22181ae13e7a5df73b82700c5b Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Fri, 6 Jun 2025 19:06:29 -0600 Subject: [PATCH 11/20] fix problem finding offer identifiers --- src/libs/endorserServer.ts | 2 +- src/libs/util.ts | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/libs/endorserServer.ts b/src/libs/endorserServer.ts index 41c2a6ac..6febf7e9 100644 --- a/src/libs/endorserServer.ts +++ b/src/libs/endorserServer.ts @@ -136,7 +136,7 @@ export function isDid(did: string): boolean { * @param {string} did - The DID to check * @returns {boolean} True if DID is hidden */ -export function isHiddenDid(did: string): boolean { +export function isHiddenDid(did: string | undefined): boolean { return did === HIDDEN_DID; } diff --git a/src/libs/util.ts b/src/libs/util.ts index 035bd164..7c2dc7a4 100644 --- a/src/libs/util.ts +++ b/src/libs/util.ts @@ -42,6 +42,7 @@ import { createPeerDid } from "../libs/crypto/vc/didPeer"; import { registerCredential } from "../libs/crypto/vc/passkeyDidPeer"; import { logger } from "../utils/logger"; import { PlatformServiceFactory } from "@/services/PlatformServiceFactory"; +import OfferDetailsView from "@/views/OfferDetailsView.vue"; export interface GiverReceiverInputInfo { did?: string; @@ -382,11 +383,11 @@ export function offerGiverDid( ): string | undefined { let giver; const claim = veriClaim.claim as OfferVerifiableCredential; - if ( - claim.credentialSubject.offeredBy?.identifier && - !serverUtil.isHiddenDid(claim.credentialSubject.offeredBy.identifier) - ) { - giver = claim.credentialSubject.offeredBy.identifier; + const offeredBy: { identifier?: string } | undefined = + claim.offeredBy || claim.credentialSubject?.offeredBy; + const offeredById = offeredBy?.identifier; + if (offeredById && !serverUtil.isHiddenDid(offeredById)) { + giver = offeredById; } else if (veriClaim.issuer && !serverUtil.isHiddenDid(veriClaim.issuer)) { giver = veriClaim.issuer; } -- 2.30.2 From 6a47f0d3e706b1de4d9d4910276e96e33f83e009 Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Fri, 6 Jun 2025 19:40:53 -0600 Subject: [PATCH 12/20] format total numbers better --- src/libs/util.ts | 13 +++++++++++-- src/views/ProjectViewView.vue | 7 +++---- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/libs/util.ts b/src/libs/util.ts index 7c2dc7a4..b286c7f0 100644 --- a/src/libs/util.ts +++ b/src/libs/util.ts @@ -42,7 +42,6 @@ import { createPeerDid } from "../libs/crypto/vc/didPeer"; import { registerCredential } from "../libs/crypto/vc/passkeyDidPeer"; import { logger } from "../utils/logger"; import { PlatformServiceFactory } from "@/services/PlatformServiceFactory"; -import OfferDetailsView from "@/views/OfferDetailsView.vue"; export interface GiverReceiverInputInfo { did?: string; @@ -81,18 +80,21 @@ export const UNIT_LONG: Record = { }; /* eslint-enable prettier/prettier */ -const UNIT_CODES: Record> = { +const UNIT_CODES: Record = { BTC: { name: "Bitcoin", faIcon: "bitcoin-sign", + decimals: 4, }, HUR: { name: "hours", faIcon: "clock", + decimals: 0, }, USD: { name: "US Dollars", faIcon: "dollar", + decimals: 2, }, }; @@ -100,6 +102,13 @@ export function iconForUnitCode(unitCode: string) { return UNIT_CODES[unitCode]?.faIcon || "question"; } +export function formattedAmount(amount: number, unitCode: string) { + const unit = UNIT_CODES[unitCode]; + const amountStr = amount.toFixed(unit?.decimals ?? 4); + const unitName = unit?.name || "?"; + return amountStr + " " + unitName; +} + // from https://stackoverflow.com/a/175787/845494 // ... though it appears even this isn't precisely right so keep doing "|| 0" or something in sensitive places // diff --git a/src/views/ProjectViewView.vue b/src/views/ProjectViewView.vue index e3876af1..b2f23258 100644 --- a/src/views/ProjectViewView.vue +++ b/src/views/ProjectViewView.vue @@ -386,11 +386,10 @@ > - {{ givenTotalHours() }} {{ libsUtil.UNIT_SHORT["HUR"] }} + {{ libsUtil.formattedAmount(givenTotalHours(), "HUR") }} - {{ givesTotalsByUnit[0].amount }} - {{ libsUtil.UNIT_SHORT[givesTotalsByUnit[0].unit] }} + {{ libsUtil.formattedAmount(givesTotalsByUnit[0].amount, givesTotalsByUnit[0].unit) }} ... @@ -411,7 +410,7 @@ :icon="libsUtil.iconForUnitCode(total.unit)" class="fa-fw text-slate-400 mr-1" /> - {{ total.amount }} {{ libsUtil.UNIT_LONG[total.unit] }} + {{ libsUtil.formattedAmount(total.amount, total.unit) }} -- 2.30.2 From 7f63ee7c807c6124d31b1680bdd9f5250126fe4c Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Fri, 6 Jun 2025 19:45:41 -0600 Subject: [PATCH 13/20] add another way to fix the privacy policy manifest for third parties like GoogleToolboxForMac --- ios/App/App.xcodeproj/project.pbxproj | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/ios/App/App.xcodeproj/project.pbxproj b/ios/App/App.xcodeproj/project.pbxproj index 45b03396..0d2a1334 100644 --- a/ios/App/App.xcodeproj/project.pbxproj +++ b/ios/App/App.xcodeproj/project.pbxproj @@ -91,7 +91,6 @@ EAEC6436E595F7CD3A1C9E96 /* Pods-App.debug.xcconfig */, E2E9297D5D02C549106C77F9 /* Pods-App.release.xcconfig */, ); - name = Pods; path = Pods; sourceTree = ""; }; @@ -108,6 +107,7 @@ 504EC3021FED79650016851F /* Resources */, 012076E8FFE4BF260A79B034 /* Fix Privacy Manifest */, 3525031ED1C96EF4CF6E9959 /* [CP] Embed Pods Frameworks */, + 96A7EF592DF3366D00084D51 /* Fix Privacy Manifest */, ); buildRules = ( ); @@ -187,7 +187,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PROJECT_DIR}/app_privacy_manifest_fixer/fixer.sh\" "; + shellScript = "\"${PROJECT_DIR}/app_privacy_manifest_fixer/fixer.sh\" \n"; showEnvVarsInLog = 0; }; 3525031ED1C96EF4CF6E9959 /* [CP] Embed Pods Frameworks */ = { @@ -227,6 +227,25 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; + 96A7EF592DF3366D00084D51 /* Fix Privacy Manifest */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Fix Privacy Manifest"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "$PROJECT_DIR/app_privacy_manifest_fixer/fixer.sh\n"; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -384,7 +403,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 19; + CURRENT_PROJECT_VERSION = 21; DEVELOPMENT_TEAM = GM3FS5JQPH; ENABLE_APP_SANDBOX = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO; @@ -411,7 +430,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 19; + CURRENT_PROJECT_VERSION = 21; DEVELOPMENT_TEAM = GM3FS5JQPH; ENABLE_APP_SANDBOX = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO; -- 2.30.2 From 38e67f3533c20a97fc5594e84ab3aedbdd491362 Mon Sep 17 00:00:00 2001 From: Trent Larson Date: Fri, 6 Jun 2025 19:50:16 -0600 Subject: [PATCH 14/20] update a DB save to match others, ie. first SQL then maybe Dexie --- src/views/IdentitySwitcherView.vue | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/views/IdentitySwitcherView.vue b/src/views/IdentitySwitcherView.vue index cfb6908b..04d9c4ac 100644 --- a/src/views/IdentitySwitcherView.vue +++ b/src/views/IdentitySwitcherView.vue @@ -172,17 +172,16 @@ export default class IdentitySwitcherView extends Vue { if (did === "0") { did = undefined; } + const platformService = PlatformServiceFactory.getInstance(); + await platformService.dbExec( + `UPDATE settings SET activeDid = ? WHERE id = ?`, + [did ?? "", MASTER_SETTINGS_KEY], + ); if (USE_DEXIE_DB) { await db.open(); await db.settings.update(MASTER_SETTINGS_KEY, { activeDid: did ?? "", }); - } else { - const platformService = PlatformServiceFactory.getInstance(); - await platformService.dbExec( - `UPDATE settings SET activeDid = ? WHERE id = ?`, - [did ?? "", MASTER_SETTINGS_KEY], - ); } this.$router.push({ name: "account" }); } -- 2.30.2 From 3d8e40e92ba5555c9cc41e77df5ecc2c01a1a372 Mon Sep 17 00:00:00 2001 From: Matthew Raymer Date: Sat, 7 Jun 2025 05:02:33 +0000 Subject: [PATCH 15/20] feat(export): Replace CSV export with standardized JSON format - Add contactsToExportJson utility function for standardized data export - Replace CSV export with JSON format in DataExportSection - Update file extension and MIME type to application/json - Remove Dexie-specific export logic in favor of unified SQLite/Dexie approach - Update success notifications to reflect JSON format - Add TypeScript interfaces for export data structure This change improves data portability and standardization by: - Using a consistent JSON format for data export/import - Supporting both SQLite and Dexie databases - Including all contact fields in export - Properly handling contactMethods as stringified JSON - Maintaining backward compatibility with existing import tools Security: No sensitive data exposure, maintains existing access controls --- src/components/DataExportSection.vue | 51 ++++---- src/db/databaseUtil.ts | 36 +++--- src/libs/util.ts | 109 ++++++++++++++--- src/services/migrationService.ts | 20 +-- .../platforms/CapacitorPlatformService.ts | 12 +- src/views/AccountViewView.vue | 3 - src/views/HomeView.vue | 114 ++++++++++++------ src/views/IdentitySwitcherView.vue | 4 - src/views/ProjectViewView.vue | 7 +- 9 files changed, 230 insertions(+), 126 deletions(-) diff --git a/src/components/DataExportSection.vue b/src/components/DataExportSection.vue index 27f36418..c44cab7b 100644 --- a/src/components/DataExportSection.vue +++ b/src/components/DataExportSection.vue @@ -63,13 +63,18 @@ backup and database export, with platform-specific download instructions. * *