Start with SQL and go directly into mobile Capacitor #137

Open
trentlarson wants to merge 14 commits from sql-absurd-sql-back into sql-absurd-sql-further
  1. 101
      -1748433586226.log
  2. 6
      BUILDING.md
  3. 7
      CHANGELOG.md
  4. 2
      android/app/build.gradle
  5. 4
      android/app/src/main/java/app/timesafari/MainActivity.java
  6. 5
      android/app/src/main/java/timesafari/app/MainActivity.java
  7. 76
      ios/App/App.xcodeproj/project.pbxproj
  8. 4
      ios/App/App/AppDelegate.swift
  9. 5
      ios/App/Podfile
  10. 20
      ios/App/Podfile.lock
  11. 44
      package-lock.json
  12. 4
      package.json
  13. 1
      pkgx.yaml
  14. 2
      src/components/UserNameDialog.vue
  15. 26
      src/db-sql/migration.ts
  16. 112
      src/db/databaseUtil.ts
  17. 2
      src/interfaces/database.ts
  18. 2
      src/libs/endorserServer.ts
  19. 61
      src/libs/util.ts
  20. 4
      src/main.common.ts
  21. 40
      src/services/AbsurdSqlDatabaseService.ts
  22. 56
      src/services/migrationService.ts
  23. 337
      src/services/platforms/CapacitorPlatformService.ts
  24. 13
      src/services/platforms/ElectronPlatformService.ts
  25. 2
      src/views/AccountViewView.vue
  26. 4
      src/views/ContactQRScanShowView.vue
  27. 184
      src/views/HomeView.vue
  28. 20
      src/views/IdentitySwitcherView.vue
  29. 9
      src/views/LogView.vue
  30. 7
      src/views/ProjectViewView.vue

101
-1748433586226.log

@ -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 (<anonymous>)
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 (<anonymous>)
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 (<anonymous>)
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.<computed> @ 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.<computed> @ 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.<computed> @ 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.<computed> @ main.common-DiOUyXe7.js:2103
(anonymous) @ main.common-DiOUyXe7.js:2098
dJ @ main.common-DiOUyXe7.js:2295

6
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 -
```
@ -369,10 +369,12 @@ 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"
* 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`).

7
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

2
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 {

4
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);
}
}

5
android/app/src/main/java/timesafari/app/MainActivity.java

@ -1,5 +0,0 @@
package timesafari.app;
import com.getcapacitor.BridgeActivity;
public class MainActivity extends BridgeActivity {}

76
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 = "<group>"; };
504EC3131FED79650016851F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
50B271D01FEDC1A000F3C39B /* public */ = {isa = PBXFileReference; lastKnownFileType = folder; path = public; sourceTree = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
/* 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 = "<group>";
@ -57,8 +57,8 @@
children = (
504EC3061FED79650016851F /* App */,
504EC3051FED79650016851F /* Products */,
7F8756D8B27F46E3366F6CEA /* Pods */,
27E2DDA53C4D2A4D1A88CE4A /* Frameworks */,
BA325FFCDCE8D334E5C7AEBE /* Pods */,
4B546315E668C7A13939F417 /* Frameworks */,
);
sourceTree = "<group>";
};
@ -85,13 +85,13 @@
path = App;
sourceTree = "<group>";
};
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 = "<group>";
};
/* End PBXGroup section */
@ -101,12 +101,13 @@
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 */,
96A7EF592DF3366D00084D51 /* Fix Privacy Manifest */,
);
buildRules = (
);
@ -186,19 +187,38 @@
);
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;
};
6634F4EFEBD30273BCE97C65 /* [CP] Check Pods Manifest.lock */ = {
3525031ED1C96EF4CF6E9959 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "[CP] Embed Pods Frameworks";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-App/Pods-App-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
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 = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-App-checkManifestLockResult.txt",
);
@ -207,20 +227,24 @@
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;
};
9592DBEFFC6D2A0C8D5DEB22 /* [CP] Embed Pods Frameworks */ = {
96A7EF592DF3366D00084D51 /* Fix Privacy Manifest */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
);
name = "[CP] Embed Pods Frameworks";
name = "Fix Privacy Manifest";
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-App/Pods-App-frameworks.sh\"\n";
showEnvVarsInLog = 0;
shellScript = "$PROJECT_DIR/app_privacy_manifest_fixer/fixer.sh\n";
};
/* End PBXShellScriptBuildPhase section */
@ -375,11 +399,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;
CURRENT_PROJECT_VERSION = 21;
DEVELOPMENT_TEAM = GM3FS5JQPH;
ENABLE_APP_SANDBOX = NO;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
INFOPLIST_FILE = App/Info.plist;
@ -401,11 +426,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;
CURRENT_PROJECT_VERSION = 21;
DEVELOPMENT_TEAM = GM3FS5JQPH;
ENABLE_APP_SANDBOX = NO;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
INFOPLIST_FILE = App/Info.plist;

4
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

5
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

20
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

44
package-lock.json

@ -1,14 +1,14 @@
{
"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-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"
}

4
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"
@ -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",

1
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).

2
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) {

26
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<void> {
// 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<T>(
sqlExec: (sql: string) => Promise<unknown>,
sqlQuery: (sql: string) => Promise<T>,
extractMigrationNames: (result: T) => Set<string>,
): Promise<void> {
for (const migration of MIGRATIONS) {
await migrationService.registerMigration(migration);
migrationService.registerMigration(migration);
}
}
export async function runMigrations(
sqlExec: (sql: string, params?: unknown[]) => Promise<Array<QueryExecResult>>,
): Promise<void> {
await registerMigrations();
await migrationService.runMigrations(sqlExec);
await migrationService.runMigrations(
sqlExec,
sqlQuery,
extractMigrationNames,
);
}

112
src/db/databaseUtil.ts

@ -79,10 +79,13 @@ const DEFAULT_SETTINGS: Settings = {
// retrieves default settings
export async function retrieveSettingsForDefaultAccount(): Promise<Settings> {
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,32 +101,91 @@ export async function retrieveSettingsForDefaultAccount(): Promise<Settings> {
}
}
/**
* Retrieves settings for the active account, merging with default settings
*
* @returns Promise<Settings> Combined settings with account-specific overrides
* @throws Will log specific errors for debugging but returns default settings on failure
*/
export async function retrieveSettingsForActiveAccount(): Promise<Settings> {
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,
};
}
}
let lastCleanupDate: string | null = null;
export let memoryLogs: string[] = [];
/**
* Logs a message to the database with proper handling of concurrent writes
@ -136,6 +198,7 @@ export async function logToDb(message: string): Promise<void> {
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 +211,9 @@ export async function logToDb(message: string): Promise<void> {
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(),
]);

2
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<unknown[] | undefined>;
getAll(sql: string, params?: unknown[]): Promise<unknown[][]>;
}

2
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;
}

61
src/libs/util.ts

@ -80,18 +80,21 @@ export const UNIT_LONG: Record<string, string> = {
};
/* eslint-enable prettier/prettier */
const UNIT_CODES: Record<string, Record<string, string>> = {
const UNIT_CODES: Record<string, { name: string; faIcon: string; decimals: number }> = {
BTC: {
name: "Bitcoin",
faIcon: "bitcoin-sign",
decimals: 4,
},
HUR: {
name: "hours",
faIcon: "clock",
decimals: 0,
},
USD: {
name: "US Dollars",
faIcon: "dollar",
decimals: 2,
},
};
@ -99,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
//
@ -382,11 +392,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;
}
@ -548,14 +558,20 @@ export const retrieveAccountMetadata = async (
};
export const retrieveAllAccountsMetadata = async (): Promise<Account[]> => {
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 +582,7 @@ export const retrieveAllAccountsMetadata = async (): Promise<Account[]> => {
return metadata as Account;
});
}
console.log("[retrieveAllAccountsMetadata] end", JSON.stringify(result, null, 2));
return result;
};
@ -646,6 +663,10 @@ export async function saveNewIdentity(
derivationPath: string,
): Promise<void> {
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 +683,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 +712,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.",

4
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.log("Platform", JSON.stringify({ platform }));
logger.log("PWA enabled", JSON.stringify({ pwa_enabled }));
// Global Error Handler
function setupGlobalErrorHandler(app: VueApp) {

40
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();
@ -100,10 +100,20 @@ 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<string> = (
result,
) => {
// Even with the "select name" query, the QueryExecResult may be [] (which doesn't make sense to me).
const names = result?.[0]?.values.map((row) => row[0] as string) || [];
return new Set(names);
};
// Run migrations
await runMigrations(sqlExec);
await runMigrations(sqlExec, sqlQuery, extractMigrationNames);
this.initialized = true;
@ -123,7 +133,6 @@ class AbsurdSqlDatabaseService implements DatabaseService {
if (!operation) continue;
try {
let queryResult: QueryExecResult[] = [];
let result: unknown;
switch (operation.type) {
case "run":
@ -132,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) {
@ -222,19 +223,6 @@ class AbsurdSqlDatabaseService implements DatabaseService {
await this.waitForInitialization();
return this.queueOperation<QueryExecResult[]>("query", sql, params);
}
async getOneRow(
sql: string,
params: unknown[] = [],
): Promise<unknown[] | undefined> {
await this.waitForInitialization();
return this.queueOperation<unknown[] | undefined>("getOneRow", sql, params);
}
async getAll(sql: string, params: unknown[] = []): Promise<unknown[][]> {
await this.waitForInitialization();
return this.queueOperation<unknown[][]>("getAll", sql, params);
}
}
// Create a singleton instance

56
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<void> {
registerMigration(migration: Migration) {
this.migrations.push(migration);
}
async runMigrations(
sqlExec: (
sql: string,
params?: unknown[],
) => Promise<Array<QueryExecResult>>,
/**
* @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<T>(
// note that this does not take parameters because the Capacitor SQLite 'execute' is different
sqlExec: (sql: string) => Promise<unknown>,
sqlQuery: (sql: string) => Promise<T>,
extractMigrationNames: (result: T) => Set<string>,
): Promise<void> {
// 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<unknown> = 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));
}
}
}

337
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,25 @@ import {
SQLiteConnection,
SQLiteDBConnection,
CapacitorSQLite,
Changes,
capSQLiteChanges,
DBSQLiteValues,
} from "@capacitor-community/sqlite";
import { runMigrations } from "@/db-sql/migration";
import { QueryExecResult } from "@/interfaces/database";
import {
ImageResult,
PlatformService,
PlatformCapabilities,
} from "../PlatformService";
import { logger } from "../../utils/logger";
import { QueryExecResult, SqlValue } from "@/interfaces/database";
import { DEFAULT_ENDORSER_API_SERVER } from "@/constants/app";
interface Migration {
name: string;
interface QueuedOperation {
type: "run" | "query";
sql: string;
params: unknown[];
resolve: (value: unknown) => void;
reject: (reason: unknown) => void;
}
/**
@ -32,14 +37,42 @@ interface Migration {
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<void> | null = null;
private operationQueue: Array<QueuedOperation> = [];
private isProcessingQueue: boolean = false;
constructor() {
this.sqlite = new SQLiteConnection(CapacitorSQLite);
}
private async initializeDatabase(): Promise<void> {
// 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<void> {
if (this.initialized) {
return;
}
@ -57,136 +90,154 @@ 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();
await this.runCapacitorMigrations();
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 runMigrations(): Promise<void> {
if (!this.db) {
throw new Error("Database not initialized");
private async processQueue(): Promise<void> {
if (this.isProcessingQueue || !this.initialized || !this.db) {
return;
}
// 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
);
`);
this.isProcessingQueue = true;
// 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]) || [],
);
while (this.operationQueue.length > 0) {
const operation = this.operationQueue.shift();
if (!operation) continue;
// 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
);
try {
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,
);
result = {
changes: runResult.changes?.changes || 0,
lastId: runResult.changes?.lastId,
};
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,
);
result = {
columns: Object.keys(queryResult.values?.[0] || {}),
values: (queryResult.values || []).map((row) =>
Object.values(row),
),
};
break;
}
}
operation.resolve(result);
} catch (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:",
operation.sql,
" ... with params:",
operation.params,
);
operation.reject(error);
}
}
CREATE INDEX IF NOT EXISTS idx_accounts_did ON accounts(did);
this.isProcessingQueue = false;
}
CREATE TABLE IF NOT EXISTS secret (
id INTEGER PRIMARY KEY AUTOINCREMENT,
secretBase64 TEXT NOT NULL
);
private async queueOperation<R>(
type: QueuedOperation["type"],
sql: string,
params: unknown[] = [],
): Promise<R> {
return new Promise<R>((resolve, reject) => {
const operation: QueuedOperation = {
type,
sql,
params,
resolve: (value: unknown) => resolve(value as R),
reject,
};
this.operationQueue.push(operation);
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
);
// If we're already initialized, start processing the queue
if (this.initialized && this.db) {
this.processQueue();
}
});
}
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
);
private async waitForInitialization(): Promise<void> {
// If we have an initialization promise, wait for it
if (this.initializationPromise) {
await this.initializationPromise;
return;
}
CREATE INDEX IF NOT EXISTS idx_contacts_did ON contacts(did);
CREATE INDEX IF NOT EXISTS idx_contacts_name ON contacts(name);
// If not initialized and no promise, start initialization
if (!this.initialized) {
await this.initializeDatabase();
return;
}
CREATE TABLE IF NOT EXISTS logs (
date TEXT PRIMARY KEY,
message TEXT NOT NULL
);
// 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.",
);
}
}
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`);
}
private async runCapacitorMigrations(): Promise<void> {
if (!this.db) {
throw new Error("Database not initialized");
}
const sqlExec: (sql: string) => Promise<capSQLiteChanges> =
this.db.execute.bind(this.db);
const sqlQuery: (sql: string) => Promise<DBSQLiteValues> =
this.db.query.bind(this.db);
const extractMigrationNames: (result: DBSQLiteValues) => Set<string> = (
result,
) => {
const names =
result.values?.map((row: { name: string }) => row.name) || [];
return new Set(names);
};
runMigrations(sqlExec, sqlQuery, extractMigrationNames);
}
/**
@ -660,24 +711,8 @@ export class CapacitorPlatformService implements PlatformService {
* @see PlatformService.dbQuery
*/
async dbQuery(sql: string, params?: unknown[]): Promise<QueryExecResult> {
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<QueryExecResult>("query", sql, params || []);
}
/**
@ -687,23 +722,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 || [],
);
}
}

13
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",
);
}
}

2
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:",

4
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) {

184
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,
);

20
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,17 @@ export default class IdentitySwitcherView extends Vue {
if (did === "0") {
did = undefined;
}
await db.open();
await db.settings.update(MASTER_SETTINGS_KEY, {
activeDid: did,
});
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 ?? "",
});
}
this.$router.push({ name: "account" });
}

9
src/views/LogView.vue

@ -42,6 +42,13 @@
>
</div>
</div>
<div class="text-slate-500">
<h2 class="text-lg font-bold mb-2">Memory Logs</h2>
<pre
class="bg-slate-100 p-4 rounded-md overflow-x-auto whitespace-pre-wrap"
>{{ memoryLogs.join("\n") }}</pre
>
</div>
</section>
</template>
@ -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();

7
src/views/ProjectViewView.vue

@ -386,11 +386,10 @@
>
<!-- just show the hours, or alternatively whatever is first -->
<span v-if="givenTotalHours() > 0">
{{ givenTotalHours() }} {{ libsUtil.UNIT_SHORT["HUR"] }}
{{ libsUtil.formattedAmount(givenTotalHours(), "HUR") }}
</span>
<span v-else>
{{ givesTotalsByUnit[0].amount }}
{{ libsUtil.UNIT_SHORT[givesTotalsByUnit[0].unit] }}
{{ libsUtil.formattedAmount(givesTotalsByUnit[0].amount, givesTotalsByUnit[0].unit) }}
</span>
<span v-if="givesTotalsByUnit.length > 1">...</span>
<span>
@ -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) }}
</div>
</div>
</span>

Loading…
Cancel
Save