Start with SQL and go directly into mobile Capacitor #137

Open
trentlarson wants to merge 19 commits from sql-absurd-sql-back into sql-absurd-sql-further
  1. 101
      -1748433586226.log
  2. 11
      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. 20
      src/components/ActivityListItem.vue
  15. 52
      src/components/DataExportSection.vue
  16. 2
      src/components/UserNameDialog.vue
  17. 26
      src/db-sql/migration.ts
  18. 109
      src/db/databaseUtil.ts
  19. 2
      src/interfaces/database.ts
  20. 2
      src/libs/endorserServer.ts
  21. 144
      src/libs/util.ts
  22. 4
      src/main.common.ts
  23. 4
      src/main.web.ts
  24. 40
      src/services/AbsurdSqlDatabaseService.ts
  25. 41
      src/services/migrationService.ts
  26. 327
      src/services/platforms/CapacitorPlatformService.ts
  27. 13
      src/services/platforms/ElectronPlatformService.ts
  28. 27
      src/views/AccountViewView.vue
  29. 100
      src/views/ContactImportView.vue
  30. 4
      src/views/ContactQRScanShowView.vue
  31. 212
      src/views/HomeView.vue
  32. 16
      src/views/IdentitySwitcherView.vue
  33. 9
      src/views/LogView.vue
  34. 12
      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

11
BUILDING.md

@ -353,10 +353,10 @@ Prerequisites: macOS with Xcode installed
``` ```
cd ios/App cd ios/App
xcrun agvtool new-version 15 xcrun agvtool new-version 21
# Unfortunately this edits Info.plist directly. # Unfortunately this edits Info.plist directly.
#xcrun agvtool new-marketing-version 0.4.5 #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 mv temp App.xcodeproj/project.pbxproj
cd - cd -
``` ```
@ -369,15 +369,18 @@ Prerequisites: macOS with Xcode installed
6. Use Xcode to build and run on simulator or device. 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 7. Release
* Under "General" renamed a bunch of things to "Time Safari" * 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 * Choose Product -> Archive
* This will trigger a build and take time, needing user's "login" keychain password which is just their login password, repeatedly. * This will trigger a build and take time, needing user's "login" keychain password (user's 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`). * 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`).
* Click Distribute -> App Store Connect * Click Distribute -> App Store Connect
* In AppStoreConnect, add the build to the distribution: remove the current build with the "-" when you hover over it, then "Add Build" with the new build. * In AppStoreConnect, add the build to the distribution: remove the current build with the "-" when you hover over it, then "Add Build" with the new build.
* May have to go to App Review, click Submission, then hover over the build and click "-".
* It can take 15 minutes for the build to show up in the list of builds. * It can take 15 minutes for the build to show up in the list of builds.
* You'll probably have to "Manage" something about encryption, disallowed in France. * You'll probably have to "Manage" something about encryption, disallowed in France.
* Then "Save" and "Add to Review" and "Resubmit to App Review". * Then "Save" and "Add to Review" and "Resubmit to App Review".

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 ## [0.4.5] - 2025.02.23
### Added ### Added
- Total amounts of gives on project page - Total amounts of gives on project page

2
android/app/build.gradle

@ -31,7 +31,7 @@ android {
applicationId "app.timesafari.app" applicationId "app.timesafari.app"
minSdkVersion rootProject.ext.minSdkVersion minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 18 versionCode 19
versionName "0.4.7" versionName "0.4.7"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions { aaptOptions {

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

@ -2,7 +2,7 @@ package app.timesafari;
import android.os.Bundle; import android.os.Bundle;
import com.getcapacitor.BridgeActivity; import com.getcapacitor.BridgeActivity;
import com.getcapacitor.community.sqlite.SQLite; //import com.getcapacitor.community.sqlite.SQLite;
public class MainActivity extends BridgeActivity { public class MainActivity extends BridgeActivity {
@Override @Override
@ -10,6 +10,6 @@ public class MainActivity extends BridgeActivity {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
// Initialize SQLite // 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 */; }; 504EC30F1FED79650016851F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30E1FED79650016851F /* Assets.xcassets */; };
504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC3101FED79650016851F /* LaunchScreen.storyboard */; }; 504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC3101FED79650016851F /* LaunchScreen.storyboard */; };
50B271D11FEDC1A000F3C39B /* public in Resources */ = {isa = PBXBuildFile; fileRef = 50B271D01FEDC1A000F3C39B /* public */; }; 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 */ /* End PBXBuildFile section */
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
@ -27,9 +27,9 @@
504EC3111FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; }; 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>"; }; 504EC3131FED79650016851F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
50B271D01FEDC1A000F3C39B /* public */ = {isa = PBXFileReference; lastKnownFileType = folder; path = public; 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; }; 90DCAFB4D8948F7A50C13800 /* 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>"; }; 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>"; };
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>"; }; 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 */ /* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
@ -37,17 +37,17 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
A084ECDBA7D38E1E42DFC39D /* Pods_App.framework in Frameworks */, 97EF2DC6FD76C3643D680B8D /* Pods_App.framework in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
/* End PBXFrameworksBuildPhase section */ /* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */ /* Begin PBXGroup section */
27E2DDA53C4D2A4D1A88CE4A /* Frameworks */ = { 4B546315E668C7A13939F417 /* Frameworks */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */, 90DCAFB4D8948F7A50C13800 /* Pods_App.framework */,
); );
name = Frameworks; name = Frameworks;
sourceTree = "<group>"; sourceTree = "<group>";
@ -57,8 +57,8 @@
children = ( children = (
504EC3061FED79650016851F /* App */, 504EC3061FED79650016851F /* App */,
504EC3051FED79650016851F /* Products */, 504EC3051FED79650016851F /* Products */,
7F8756D8B27F46E3366F6CEA /* Pods */, BA325FFCDCE8D334E5C7AEBE /* Pods */,
27E2DDA53C4D2A4D1A88CE4A /* Frameworks */, 4B546315E668C7A13939F417 /* Frameworks */,
); );
sourceTree = "<group>"; sourceTree = "<group>";
}; };
@ -85,13 +85,13 @@
path = App; path = App;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
7F8756D8B27F46E3366F6CEA /* Pods */ = { BA325FFCDCE8D334E5C7AEBE /* Pods */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */, EAEC6436E595F7CD3A1C9E96 /* Pods-App.debug.xcconfig */,
AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */, E2E9297D5D02C549106C77F9 /* Pods-App.release.xcconfig */,
); );
name = Pods; path = Pods;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
/* End PBXGroup section */ /* End PBXGroup section */
@ -101,12 +101,13 @@
isa = PBXNativeTarget; isa = PBXNativeTarget;
buildConfigurationList = 504EC3161FED79650016851F /* Build configuration list for PBXNativeTarget "App" */; buildConfigurationList = 504EC3161FED79650016851F /* Build configuration list for PBXNativeTarget "App" */;
buildPhases = ( buildPhases = (
6634F4EFEBD30273BCE97C65 /* [CP] Check Pods Manifest.lock */, 92977BEA1068CC097A57FC77 /* [CP] Check Pods Manifest.lock */,
504EC3001FED79650016851F /* Sources */, 504EC3001FED79650016851F /* Sources */,
504EC3011FED79650016851F /* Frameworks */, 504EC3011FED79650016851F /* Frameworks */,
504EC3021FED79650016851F /* Resources */, 504EC3021FED79650016851F /* Resources */,
9592DBEFFC6D2A0C8D5DEB22 /* [CP] Embed Pods Frameworks */,
012076E8FFE4BF260A79B034 /* Fix Privacy Manifest */, 012076E8FFE4BF260A79B034 /* Fix Privacy Manifest */,
3525031ED1C96EF4CF6E9959 /* [CP] Embed Pods Frameworks */,
96A7EF592DF3366D00084D51 /* Fix Privacy Manifest */,
); );
buildRules = ( buildRules = (
); );
@ -186,19 +187,38 @@
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = "\"${PROJECT_DIR}/app_privacy_manifest_fixer/fixer.sh\" "; shellScript = "\"${PROJECT_DIR}/app_privacy_manifest_fixer/fixer.sh\" \n";
showEnvVarsInLog = 0; showEnvVarsInLog = 0;
}; };
6634F4EFEBD30273BCE97C65 /* [CP] Check Pods Manifest.lock */ = { 3525031ED1C96EF4CF6E9959 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( 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 = ( inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock", "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock", "${PODS_ROOT}/Manifest.lock",
); );
name = "[CP] Check Pods Manifest.lock"; name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = ( outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-App-checkManifestLockResult.txt", "$(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"; 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; showEnvVarsInLog = 0;
}; };
9592DBEFFC6D2A0C8D5DEB22 /* [CP] Embed Pods Frameworks */ = { 96A7EF592DF3366D00084D51 /* Fix Privacy Manifest */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
); );
inputFileListPaths = (
);
inputPaths = ( inputPaths = (
); );
name = "[CP] Embed Pods Frameworks"; name = "Fix Privacy Manifest";
outputFileListPaths = (
);
outputPaths = ( outputPaths = (
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-App/Pods-App-frameworks.sh\"\n"; shellScript = "$PROJECT_DIR/app_privacy_manifest_fixer/fixer.sh\n";
showEnvVarsInLog = 0;
}; };
/* End PBXShellScriptBuildPhase section */ /* End PBXShellScriptBuildPhase section */
@ -375,11 +399,12 @@
}; };
504EC3171FED79650016851F /* Debug */ = { 504EC3171FED79650016851F /* Debug */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */; baseConfigurationReference = EAEC6436E595F7CD3A1C9E96 /* Pods-App.debug.xcconfig */;
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 18; CURRENT_PROJECT_VERSION = 21;
DEVELOPMENT_TEAM = GM3FS5JQPH;
ENABLE_APP_SANDBOX = NO; ENABLE_APP_SANDBOX = NO;
ENABLE_USER_SCRIPT_SANDBOXING = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO;
INFOPLIST_FILE = App/Info.plist; INFOPLIST_FILE = App/Info.plist;
@ -401,11 +426,12 @@
}; };
504EC3181FED79650016851F /* Release */ = { 504EC3181FED79650016851F /* Release */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */; baseConfigurationReference = E2E9297D5D02C549106C77F9 /* Pods-App.release.xcconfig */;
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 18; CURRENT_PROJECT_VERSION = 21;
DEVELOPMENT_TEAM = GM3FS5JQPH;
ENABLE_APP_SANDBOX = NO; ENABLE_APP_SANDBOX = NO;
ENABLE_USER_SCRIPT_SANDBOXING = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO;
INFOPLIST_FILE = App/Info.plist; 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 { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
// Initialize SQLite // Initialize SQLite
let sqlite = SQLite() //let sqlite = SQLite()
sqlite.initialize() //sqlite.initialize()
// Override point for customization after application launch. // Override point for customization after application launch.
return true return true

5
ios/App/Podfile

@ -27,4 +27,9 @@ end
post_install do |installer| post_install do |installer|
assertDeploymentTarget(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 end

20
ios/App/Podfile.lock

@ -5,6 +5,10 @@ PODS:
- Capacitor - Capacitor
- CapacitorCamera (6.1.2): - CapacitorCamera (6.1.2):
- Capacitor - Capacitor
- CapacitorCommunitySqlite (6.0.2):
- Capacitor
- SQLCipher
- ZIPFoundation
- CapacitorCordova (6.2.1) - CapacitorCordova (6.2.1)
- CapacitorFilesystem (6.0.3): - CapacitorFilesystem (6.0.3):
- Capacitor - Capacitor
@ -73,11 +77,18 @@ PODS:
- nanopb/decode (2.30910.0) - nanopb/decode (2.30910.0)
- nanopb/encode (2.30910.0) - nanopb/encode (2.30910.0)
- PromisesObjC (2.4.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: DEPENDENCIES:
- "Capacitor (from `../../node_modules/@capacitor/ios`)" - "Capacitor (from `../../node_modules/@capacitor/ios`)"
- "CapacitorApp (from `../../node_modules/@capacitor/app`)" - "CapacitorApp (from `../../node_modules/@capacitor/app`)"
- "CapacitorCamera (from `../../node_modules/@capacitor/camera`)" - "CapacitorCamera (from `../../node_modules/@capacitor/camera`)"
- "CapacitorCommunitySqlite (from `../../node_modules/@capacitor-community/sqlite`)"
- "CapacitorCordova (from `../../node_modules/@capacitor/ios`)" - "CapacitorCordova (from `../../node_modules/@capacitor/ios`)"
- "CapacitorFilesystem (from `../../node_modules/@capacitor/filesystem`)" - "CapacitorFilesystem (from `../../node_modules/@capacitor/filesystem`)"
- "CapacitorMlkitBarcodeScanning (from `../../node_modules/@capacitor-mlkit/barcode-scanning`)" - "CapacitorMlkitBarcodeScanning (from `../../node_modules/@capacitor-mlkit/barcode-scanning`)"
@ -98,6 +109,8 @@ SPEC REPOS:
- MLKitVision - MLKitVision
- nanopb - nanopb
- PromisesObjC - PromisesObjC
- SQLCipher
- ZIPFoundation
EXTERNAL SOURCES: EXTERNAL SOURCES:
Capacitor: Capacitor:
@ -106,6 +119,8 @@ EXTERNAL SOURCES:
:path: "../../node_modules/@capacitor/app" :path: "../../node_modules/@capacitor/app"
CapacitorCamera: CapacitorCamera:
:path: "../../node_modules/@capacitor/camera" :path: "../../node_modules/@capacitor/camera"
CapacitorCommunitySqlite:
:path: "../../node_modules/@capacitor-community/sqlite"
CapacitorCordova: CapacitorCordova:
:path: "../../node_modules/@capacitor/ios" :path: "../../node_modules/@capacitor/ios"
CapacitorFilesystem: CapacitorFilesystem:
@ -121,6 +136,7 @@ SPEC CHECKSUMS:
Capacitor: c95400d761e376be9da6be5a05f226c0e865cebf Capacitor: c95400d761e376be9da6be5a05f226c0e865cebf
CapacitorApp: e1e6b7d05e444d593ca16fd6d76f2b7c48b5aea7 CapacitorApp: e1e6b7d05e444d593ca16fd6d76f2b7c48b5aea7
CapacitorCamera: 9bc7b005d0e6f1d5f525b8137045b60cffffce79 CapacitorCamera: 9bc7b005d0e6f1d5f525b8137045b60cffffce79
CapacitorCommunitySqlite: 0299d20f4b00c2e6aa485a1d8932656753937b9b
CapacitorCordova: 8d93e14982f440181be7304aa9559ca631d77fff CapacitorCordova: 8d93e14982f440181be7304aa9559ca631d77fff
CapacitorFilesystem: 59270a63c60836248812671aa3b15df673fbaf74 CapacitorFilesystem: 59270a63c60836248812671aa3b15df673fbaf74
CapacitorMlkitBarcodeScanning: 7652be9c7922f39203a361de735d340ae37e134e CapacitorMlkitBarcodeScanning: 7652be9c7922f39203a361de735d340ae37e134e
@ -138,7 +154,9 @@ SPEC CHECKSUMS:
MLKitVision: 90922bca854014a856f8b649d1f1f04f63fd9c79 MLKitVision: 90922bca854014a856f8b649d1f1f04f63fd9c79
nanopb: 438bc412db1928dac798aa6fd75726007be04262 nanopb: 438bc412db1928dac798aa6fd75726007be04262
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
SQLCipher: 31878d8ebd27e5c96db0b7cb695c96e9f8ad77da
ZIPFoundation: b8c29ea7ae353b309bc810586181fd073cb3312c
PODFILE CHECKSUM: 7e7e09e6937de7f015393aecf2cf7823645689b3 PODFILE CHECKSUM: f987510f7383b04a1b09ea8472bdadcd88b6c924
COCOAPODS: 1.16.2 COCOAPODS: 1.16.2

44
package-lock.json

@ -1,14 +1,14 @@
{ {
"name": "timesafari", "name": "timesafari",
"version": "0.4.6", "version": "0.4.7",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "timesafari", "name": "timesafari",
"version": "0.4.6", "version": "0.4.7",
"dependencies": { "dependencies": {
"@capacitor-community/sqlite": "^6.0.2", "@capacitor-community/sqlite": "6.0.2",
"@capacitor-mlkit/barcode-scanning": "^6.0.0", "@capacitor-mlkit/barcode-scanning": "^6.0.0",
"@capacitor/android": "^6.2.0", "@capacitor/android": "^6.2.0",
"@capacitor/app": "^6.0.0", "@capacitor/app": "^6.0.0",
@ -2516,7 +2516,6 @@
"version": "6.0.2", "version": "6.0.2",
"resolved": "https://registry.npmjs.org/@capacitor-community/sqlite/-/sqlite-6.0.2.tgz", "resolved": "https://registry.npmjs.org/@capacitor-community/sqlite/-/sqlite-6.0.2.tgz",
"integrity": "sha512-sj+2SPLu7E/3dM3xxcWwfNomG+aQHuN96/EFGrOtp4Dv30/2y5oIPyi6hZGjQGjPc5GDNoTQwW7vxWNzybjuMg==", "integrity": "sha512-sj+2SPLu7E/3dM3xxcWwfNomG+aQHuN96/EFGrOtp4Dv30/2y5oIPyi6hZGjQGjPc5GDNoTQwW7vxWNzybjuMg==",
"license": "MIT",
"dependencies": { "dependencies": {
"jeep-sqlite": "^2.7.2" "jeep-sqlite": "^2.7.2"
}, },
@ -8195,7 +8194,6 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"darwin" "darwin"
@ -8208,7 +8206,6 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"darwin" "darwin"
@ -8277,7 +8274,6 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"linux" "linux"
@ -8290,7 +8286,6 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"linux" "linux"
@ -8373,7 +8368,6 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"linux" "linux"
@ -8386,7 +8380,6 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"linux" "linux"
@ -8399,7 +8392,6 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"win32" "win32"
@ -8426,7 +8418,6 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"win32" "win32"
@ -8818,10 +8809,9 @@
} }
}, },
"node_modules/@stencil/core": { "node_modules/@stencil/core": {
"version": "4.31.0", "version": "4.33.1",
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.31.0.tgz", "resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.33.1.tgz",
"integrity": "sha512-Ei9MFJ6LPD9BMFs+klkHylbVOOYhG10Jv4bvoFf3GMH15kA41rSYkEdr4DiX84ZdErQE2qtFV/2SUyWoXh0AhA==", "integrity": "sha512-12k9xhAJBkpg598it+NRmaYIdEe6TSnsL/v6/KRXDcUyTK11VYwZQej2eHnMWtqot+znJ+GNTqb5YbiXi+5Low==",
"license": "MIT",
"bin": { "bin": {
"stencil": "bin/stencil" "stencil": "bin/stencil"
}, },
@ -12173,8 +12163,7 @@
"node_modules/browser-fs-access": { "node_modules/browser-fs-access": {
"version": "0.35.0", "version": "0.35.0",
"resolved": "https://registry.npmjs.org/browser-fs-access/-/browser-fs-access-0.35.0.tgz", "resolved": "https://registry.npmjs.org/browser-fs-access/-/browser-fs-access-0.35.0.tgz",
"integrity": "sha512-sLoadumpRfsjprP8XzVjpQc0jK8yqHBx0PtUTGYj2fftT+P/t+uyDAQdMgGAPKD011in/O+YYGh7fIs0oG/viw==", "integrity": "sha512-sLoadumpRfsjprP8XzVjpQc0jK8yqHBx0PtUTGYj2fftT+P/t+uyDAQdMgGAPKD011in/O+YYGh7fIs0oG/viw=="
"license": "Apache-2.0"
}, },
"node_modules/browserify-aes": { "node_modules/browserify-aes": {
"version": "1.2.0", "version": "1.2.0",
@ -18144,8 +18133,7 @@
"node_modules/immediate": { "node_modules/immediate": {
"version": "3.0.6", "version": "3.0.6",
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="
"license": "MIT"
}, },
"node_modules/import-fresh": { "node_modules/import-fresh": {
"version": "3.3.1", "version": "3.3.1",
@ -19090,7 +19078,6 @@
"version": "2.8.0", "version": "2.8.0",
"resolved": "https://registry.npmjs.org/jeep-sqlite/-/jeep-sqlite-2.8.0.tgz", "resolved": "https://registry.npmjs.org/jeep-sqlite/-/jeep-sqlite-2.8.0.tgz",
"integrity": "sha512-FWNUP6OAmrUHwiW7H1xH5YUQ8tN2O4l4psT1sLd7DQtHd5PfrA1nvNdeKPNj+wQBtu7elJa8WoUibTytNTaaCg==", "integrity": "sha512-FWNUP6OAmrUHwiW7H1xH5YUQ8tN2O4l4psT1sLd7DQtHd5PfrA1nvNdeKPNj+wQBtu7elJa8WoUibTytNTaaCg==",
"license": "MIT",
"dependencies": { "dependencies": {
"@stencil/core": "^4.20.0", "@stencil/core": "^4.20.0",
"browser-fs-access": "^0.35.0", "browser-fs-access": "^0.35.0",
@ -19591,7 +19578,6 @@
"version": "3.10.1", "version": "3.10.1",
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
"integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
"license": "(MIT OR GPL-3.0-or-later)",
"dependencies": { "dependencies": {
"lie": "~3.3.0", "lie": "~3.3.0",
"pako": "~1.0.2", "pako": "~1.0.2",
@ -19602,20 +19588,17 @@
"node_modules/jszip/node_modules/isarray": { "node_modules/jszip/node_modules/isarray": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="
"license": "MIT"
}, },
"node_modules/jszip/node_modules/pako": { "node_modules/jszip/node_modules/pako": {
"version": "1.0.11", "version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="
"license": "(MIT AND Zlib)"
}, },
"node_modules/jszip/node_modules/readable-stream": { "node_modules/jszip/node_modules/readable-stream": {
"version": "2.3.8", "version": "2.3.8",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"license": "MIT",
"dependencies": { "dependencies": {
"core-util-is": "~1.0.0", "core-util-is": "~1.0.0",
"inherits": "~2.0.3", "inherits": "~2.0.3",
@ -19629,14 +19612,12 @@
"node_modules/jszip/node_modules/safe-buffer": { "node_modules/jszip/node_modules/safe-buffer": {
"version": "5.1.2", "version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
"license": "MIT"
}, },
"node_modules/jszip/node_modules/string_decoder": { "node_modules/jszip/node_modules/string_decoder": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"license": "MIT",
"dependencies": { "dependencies": {
"safe-buffer": "~5.1.0" "safe-buffer": "~5.1.0"
} }
@ -20159,7 +20140,6 @@
"version": "3.3.0", "version": "3.3.0",
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
"license": "MIT",
"dependencies": { "dependencies": {
"immediate": "~3.0.5" "immediate": "~3.0.5"
} }
@ -20526,7 +20506,6 @@
"version": "1.10.0", "version": "1.10.0",
"resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz", "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz",
"integrity": "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==", "integrity": "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==",
"license": "Apache-2.0",
"dependencies": { "dependencies": {
"lie": "3.1.1" "lie": "3.1.1"
} }
@ -20535,7 +20514,6 @@
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz", "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz",
"integrity": "sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==", "integrity": "sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==",
"license": "MIT",
"dependencies": { "dependencies": {
"immediate": "~3.0.5" "immediate": "~3.0.5"
} }

4
package.json

@ -1,6 +1,6 @@
{ {
"name": "timesafari", "name": "timesafari",
"version": "0.4.6", "version": "0.4.7",
"description": "Time Safari Application", "description": "Time Safari Application",
"author": { "author": {
"name": "Time Safari Team" "name": "Time Safari Team"
@ -46,7 +46,7 @@
"electron:build-mac-universal": "npm run build:electron-prod && electron-builder --mac --universal" "electron:build-mac-universal": "npm run build:electron-prod && electron-builder --mac --universal"
}, },
"dependencies": { "dependencies": {
"@capacitor-community/sqlite": "^6.0.2", "@capacitor-community/sqlite": "6.0.2",
"@capacitor-mlkit/barcode-scanning": "^6.0.0", "@capacitor-mlkit/barcode-scanning": "^6.0.0",
"@capacitor/android": "^6.2.0", "@capacitor/android": "^6.2.0",
"@capacitor/app": "^6.0.0", "@capacitor/app": "^6.0.0",

1
pkgx.yaml

@ -2,5 +2,6 @@ dependencies:
- gradle - gradle
- java - java
- pod - pod
- rubygems.org
# other dependencies are discovered via package.json & requirements.txt & Gemfile (I'm guessing). # other dependencies are discovered via package.json & requirements.txt & Gemfile (I'm guessing).

20
src/components/ActivityListItem.vue

@ -46,7 +46,7 @@
<!-- Record Image --> <!-- Record Image -->
<div <div
v-if="record.image" v-if="record.image"
class="bg-cover mb-6 -mt-3 sm:-mt-4 -mx-3 sm:-mx-4" class="bg-cover mb-2 -mt-3 sm:-mt-4 -mx-3 sm:-mx-4"
:style="`background-image: url(${record.image});`" :style="`background-image: url(${record.image});`"
> >
<a <a
@ -62,8 +62,15 @@
</a> </a>
</div> </div>
<!-- Description -->
<p class="font-medium">
<a class="cursor-pointer" @click="$emit('loadClaim', record.jwtId)">
{{ description }}
</a>
</p>
<div <div
class="relative flex justify-between gap-4 max-w-[40rem] mx-auto mb-5" class="relative flex justify-between gap-4 max-w-[40rem] mx-auto mt-4"
> >
<!-- Source --> <!-- Source -->
<div <div
@ -170,13 +177,6 @@
</div> </div>
</div> </div>
</div> </div>
<!-- Description -->
<p class="font-medium">
<a class="cursor-pointer" @click="$emit('loadClaim', record.jwtId)">
{{ description }}
</a>
</p>
</div> </div>
</li> </li>
</template> </template>
@ -222,7 +222,7 @@ export default class ActivityListItem extends Vue {
const claim = const claim =
(this.record.fullClaim as unknown).claim || this.record.fullClaim; (this.record.fullClaim as unknown).claim || this.record.fullClaim;
return `${claim.description}`; return `${claim?.description || ""}`;
} }
private displayAmount(code: string, amt: number) { private displayAmount(code: string, amt: number) {

52
src/components/DataExportSection.vue

@ -62,14 +62,18 @@ backup and database export, with platform-specific download instructions. * *
<script lang="ts"> <script lang="ts">
import { Component, Prop, Vue } from "vue-facing-decorator"; import { Component, Prop, Vue } from "vue-facing-decorator";
import { AppString, NotificationIface, USE_DEXIE_DB } from "../constants/app"; import { AppString, NotificationIface } from "../constants/app";
import { db } from "../db/index";
import { Contact } from "../db/tables/contacts";
import * as databaseUtil from "../db/databaseUtil";
import { logger } from "../utils/logger"; import { logger } from "../utils/logger";
import { PlatformServiceFactory } from "../services/PlatformServiceFactory"; import { PlatformServiceFactory } from "../services/PlatformServiceFactory";
import { import {
PlatformService, PlatformService,
PlatformCapabilities, PlatformCapabilities,
} from "../services/PlatformService"; } from "../services/PlatformService";
import { contactsToExportJson } from "../libs/util";
/** /**
* @vue-component * @vue-component
@ -131,24 +135,25 @@ export default class DataExportSection extends Vue {
*/ */
public async exportDatabase() { public async exportDatabase() {
try { try {
if (!USE_DEXIE_DB) { let allContacts: Contact[] = [];
throw new Error("Not implemented"); const platformService = PlatformServiceFactory.getInstance();
const result = await platformService.dbQuery(`SELECT * FROM contacts`);
if (result) {
allContacts = databaseUtil.mapQueryResultToValues(
result,
) as unknown as Contact[];
} }
const blob = await db.export({ // if (USE_DEXIE_DB) {
prettyJson: true, // await db.open();
transform: (table, value, key) => { // allContacts = await db.contacts.toArray();
if (table === "contacts") { // }
// Dexie inserts a number 0 when some are undefined, so we need to totally remove them.
Object.keys(value).forEach((prop) => { // Convert contacts to export format
if (value[prop] === undefined) { const exportData = contactsToExportJson(allContacts);
delete value[prop]; const jsonStr = JSON.stringify(exportData, null, 2);
} const blob = new Blob([jsonStr], { type: "application/json" });
});
} const fileName = `${AppString.APP_NAME_NO_SPACES}-backup-contacts.json`;
return { value, key };
},
});
const fileName = `${AppString.APP_NAME_NO_SPACES}-backup.json`;
if (this.platformCapabilities.hasFileDownload) { if (this.platformCapabilities.hasFileDownload) {
// Web platform: Use download link // Web platform: Use download link
@ -160,8 +165,7 @@ export default class DataExportSection extends Vue {
setTimeout(() => URL.revokeObjectURL(this.downloadUrl), 1000); setTimeout(() => URL.revokeObjectURL(this.downloadUrl), 1000);
} else if (this.platformCapabilities.hasFileSystem) { } else if (this.platformCapabilities.hasFileSystem) {
// Native platform: Write to app directory // Native platform: Write to app directory
const content = await blob.text(); await this.platformService.writeAndShareFile(fileName, jsonStr);
await this.platformService.writeAndShareFile(fileName, content);
} else { } else {
throw new Error("This platform does not support file downloads."); throw new Error("This platform does not support file downloads.");
} }
@ -172,10 +176,10 @@ export default class DataExportSection extends Vue {
type: "success", type: "success",
title: "Export Successful", title: "Export Successful",
text: this.platformCapabilities.hasFileDownload text: this.platformCapabilities.hasFileDownload
? "See your downloads directory for the backup. It is in the Dexie format." ? "See your downloads directory for the backup."
: "You should have been prompted to save your backup file.", : "The backup file has been saved.",
}, },
-1, 3000,
); );
} catch (error) { } catch (error) {
logger.error("Export Error:", error); logger.error("Export Error:", error);

2
src/components/UserNameDialog.vue

@ -74,7 +74,7 @@ export default class UserNameDialog extends Vue {
async onClickSaveChanges() { async onClickSaveChanges() {
const platformService = PlatformServiceFactory.getInstance(); const platformService = PlatformServiceFactory.getInstance();
await platformService.dbExec( await platformService.dbExec(
"UPDATE settings SET firstName = ? WHERE key = ?", "UPDATE settings SET firstName = ? WHERE id = ?",
[this.givenName, MASTER_SETTINGS_KEY], [this.givenName, MASTER_SETTINGS_KEY],
); );
if (USE_DEXIE_DB) { if (USE_DEXIE_DB) {

26
src/db-sql/migration.ts

@ -1,5 +1,4 @@
import migrationService from "../services/migrationService"; import migrationService from "../services/migrationService";
import type { QueryExecResult } from "../interfaces/database";
import { DEFAULT_ENDORSER_API_SERVER } from "@/constants/app"; import { DEFAULT_ENDORSER_API_SERVER } from "@/constants/app";
import { arrayBufferToBase64 } from "@/libs/crypto"; 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) { for (const migration of MIGRATIONS) {
await migrationService.registerMigration(migration); migrationService.registerMigration(migration);
} }
} await migrationService.runMigrations(
sqlExec,
export async function runMigrations( sqlQuery,
sqlExec: (sql: string, params?: unknown[]) => Promise<Array<QueryExecResult>>, extractMigrationNames,
): Promise<void> { );
await registerMigrations();
await migrationService.runMigrations(sqlExec);
} }

109
src/db/databaseUtil.ts

@ -80,9 +80,8 @@ const DEFAULT_SETTINGS: Settings = {
// retrieves default settings // retrieves default settings
export async function retrieveSettingsForDefaultAccount(): Promise<Settings> { export async function retrieveSettingsForDefaultAccount(): Promise<Settings> {
const platform = PlatformServiceFactory.getInstance(); const platform = PlatformServiceFactory.getInstance();
const result = await platform.dbQuery("SELECT * FROM settings WHERE id = ?", [ const sql = "SELECT * FROM settings WHERE id = ?";
MASTER_SETTINGS_KEY, const result = await platform.dbQuery(sql, [MASTER_SETTINGS_KEY]);
]);
if (!result) { if (!result) {
return DEFAULT_SETTINGS; return DEFAULT_SETTINGS;
} else { } else {
@ -98,32 +97,92 @@ 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> { export async function retrieveSettingsForActiveAccount(): Promise<Settings> {
const defaultSettings = await retrieveSettingsForDefaultAccount(); try {
if (!defaultSettings.activeDid) { // Get default settings first
return defaultSettings; const defaultSettings = await retrieveSettingsForDefaultAccount();
} else {
const platform = PlatformServiceFactory.getInstance(); // If no active DID, return defaults
const result = await platform.dbQuery( if (!defaultSettings.activeDid) {
"SELECT * FROM settings WHERE accountDid = ?", logConsoleAndDb(
[defaultSettings.activeDid], "[databaseUtil] No active DID found, returning default settings",
); );
const overrideSettings = result return defaultSettings;
? (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);
} }
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 = [];
}
}
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; let lastCleanupDate: string | null = null;
export let memoryLogs: string[] = [];
/** /**
* Logs a message to the database with proper handling of concurrent writes * Logs a message to the database with proper handling of concurrent writes
@ -136,6 +195,7 @@ export async function logToDb(message: string): Promise<void> {
const nowKey = new Date().toISOString(); const nowKey = new Date().toISOString();
try { try {
memoryLogs.push(`${new Date().toISOString()} ${message}`);
// Try to insert first, if it fails due to UNIQUE constraint, update instead // Try to insert first, if it fails due to UNIQUE constraint, update instead
await platform.dbExec("INSERT INTO logs (date, message) VALUES (?, ?)", [ await platform.dbExec("INSERT INTO logs (date, message) VALUES (?, ?)", [
nowKey, nowKey,
@ -148,6 +208,9 @@ export async function logToDb(message: string): Promise<void> {
const sevenDaysAgo = new Date( const sevenDaysAgo = new Date(
new Date().getTime() - 7 * 24 * 60 * 60 * 1000, 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 < ?", [ await platform.dbExec("DELETE FROM logs WHERE date < ?", [
sevenDaysAgo.toDateString(), sevenDaysAgo.toDateString(),
]); ]);

2
src/interfaces/database.ts

@ -12,6 +12,4 @@ export interface DatabaseService {
sql: string, sql: string,
params?: unknown[], params?: unknown[],
): Promise<{ changes: number; lastId?: number }>; ): 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 * @param {string} did - The DID to check
* @returns {boolean} True if DID is hidden * @returns {boolean} True if DID is hidden
*/ */
export function isHiddenDid(did: string): boolean { export function isHiddenDid(did: string | undefined): boolean {
return did === HIDDEN_DID; return did === HIDDEN_DID;
} }

144
src/libs/util.ts

@ -80,18 +80,24 @@ export const UNIT_LONG: Record<string, string> = {
}; };
/* eslint-enable prettier/prettier */ /* eslint-enable prettier/prettier */
const UNIT_CODES: Record<string, Record<string, string>> = { const UNIT_CODES: Record<
string,
{ name: string; faIcon: string; decimals: number }
> = {
BTC: { BTC: {
name: "Bitcoin", name: "Bitcoin",
faIcon: "bitcoin-sign", faIcon: "bitcoin-sign",
decimals: 4,
}, },
HUR: { HUR: {
name: "hours", name: "hours",
faIcon: "clock", faIcon: "clock",
decimals: 0,
}, },
USD: { USD: {
name: "US Dollars", name: "US Dollars",
faIcon: "dollar", faIcon: "dollar",
decimals: 2,
}, },
}; };
@ -99,6 +105,13 @@ export function iconForUnitCode(unitCode: string) {
return UNIT_CODES[unitCode]?.faIcon || "question"; 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 // 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 // ... though it appears even this isn't precisely right so keep doing "|| 0" or something in sensitive places
// //
@ -382,11 +395,11 @@ export function offerGiverDid(
): string | undefined { ): string | undefined {
let giver; let giver;
const claim = veriClaim.claim as OfferVerifiableCredential; const claim = veriClaim.claim as OfferVerifiableCredential;
if ( const offeredBy: { identifier?: string } | undefined =
claim.credentialSubject.offeredBy?.identifier && claim.offeredBy || claim.credentialSubject?.offeredBy;
!serverUtil.isHiddenDid(claim.credentialSubject.offeredBy.identifier) const offeredById = offeredBy?.identifier;
) { if (offeredById && !serverUtil.isHiddenDid(offeredById)) {
giver = claim.credentialSubject.offeredBy.identifier; giver = offeredById;
} else if (veriClaim.issuer && !serverUtil.isHiddenDid(veriClaim.issuer)) { } else if (veriClaim.issuer && !serverUtil.isHiddenDid(veriClaim.issuer)) {
giver = veriClaim.issuer; giver = veriClaim.issuer;
} }
@ -549,7 +562,8 @@ export const retrieveAccountMetadata = async (
export const retrieveAllAccountsMetadata = async (): Promise<Account[]> => { export const retrieveAllAccountsMetadata = async (): Promise<Account[]> => {
const platformService = PlatformServiceFactory.getInstance(); const platformService = PlatformServiceFactory.getInstance();
const dbAccounts = await platformService.dbQuery(`SELECT * FROM accounts`); const sql = `SELECT * FROM accounts`;
const dbAccounts = await platformService.dbQuery(sql);
const accounts = databaseUtil.mapQueryResultToValues(dbAccounts) as Account[]; const accounts = databaseUtil.mapQueryResultToValues(dbAccounts) as Account[];
let result = accounts.map((account) => { let result = accounts.map((account) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
@ -662,18 +676,17 @@ export async function saveNewIdentity(
const encryptedMnemonic = await simpleEncrypt(mnemonic, secret); const encryptedMnemonic = await simpleEncrypt(mnemonic, secret);
const encryptedIdentityBase64 = arrayBufferToBase64(encryptedIdentity); const encryptedIdentityBase64 = arrayBufferToBase64(encryptedIdentity);
const encryptedMnemonicBase64 = arrayBufferToBase64(encryptedMnemonic); const encryptedMnemonicBase64 = arrayBufferToBase64(encryptedMnemonic);
await platformService.dbExec( const sql = `INSERT INTO accounts (dateCreated, derivationPath, did, identityEncrBase64, mnemonicEncrBase64, publicKeyHex)
`INSERT INTO accounts (dateCreated, derivationPath, did, identityEncrBase64, mnemonicEncrBase64, publicKeyHex) VALUES (?, ?, ?, ?, ?, ?)`;
VALUES (?, ?, ?, ?, ?, ?)`, const params = [
[ new Date().toISOString(),
new Date().toISOString(), derivationPath,
derivationPath, newId.did,
newId.did, encryptedIdentityBase64,
encryptedIdentityBase64, encryptedMnemonicBase64,
encryptedMnemonicBase64, newId.keys[0].publicKeyHex,
newId.keys[0].publicKeyHex, ];
], await platformService.dbExec(sql, params);
);
await databaseUtil.updateDefaultSettings({ activeDid: newId.did }); await databaseUtil.updateDefaultSettings({ activeDid: newId.did });
if (USE_DEXIE_DB) { if (USE_DEXIE_DB) {
@ -814,3 +827,96 @@ export const sendTestThroughPushServer = async (
logger.log("Got response from web push server:", response); logger.log("Got response from web push server:", response);
return response; return response;
}; };
/**
* Converts a Contact object to a CSV line string following the established format.
* The format matches CONTACT_CSV_HEADER: "name,did,pubKeyBase64,seesMe,registered,contactMethods"
* where contactMethods is stored as a stringified JSON array.
*
* @param contact - The Contact object to convert
* @returns A CSV-formatted string representing the contact
* @throws {Error} If the contact object is missing required fields
*/
export const contactToCsvLine = (contact: Contact): string => {
if (!contact.did) {
throw new Error("Contact must have a did field");
}
// Escape fields that might contain commas or quotes
const escapeField = (field: string | boolean | undefined): string => {
if (field === undefined) return "";
const str = String(field);
if (str.includes(",") || str.includes('"')) {
return `"${str.replace(/"/g, '""')}"`;
}
return str;
};
// Handle contactMethods array by stringifying it
const contactMethodsStr = contact.contactMethods
? escapeField(JSON.stringify(contact.contactMethods))
: "";
const fields = [
escapeField(contact.name),
escapeField(contact.did),
escapeField(contact.publicKeyBase64),
escapeField(contact.seesMe),
escapeField(contact.registered),
contactMethodsStr,
];
return fields.join(",");
};
/**
* Interface for the JSON export format of database tables
*/
export interface TableExportData {
tableName: string;
rows: Array<Record<string, unknown>>;
}
/**
* Interface for the complete database export format
*/
export interface DatabaseExport {
data: {
data: Array<TableExportData>;
};
}
/**
* Converts an array of contacts to the standardized database export JSON format.
* This format is used for data migration and backup purposes.
*
* @param contacts - Array of Contact objects to convert
* @returns DatabaseExport object in the standardized format
*/
export const contactsToExportJson = (contacts: Contact[]): DatabaseExport => {
// Convert each contact to a plain object and ensure all fields are included
const rows = contacts.map((contact) => ({
did: contact.did,
name: contact.name || null,
contactMethods: contact.contactMethods
? JSON.stringify(contact.contactMethods)
: null,
nextPubKeyHashB64: contact.nextPubKeyHashB64 || null,
notes: contact.notes || null,
profileImageUrl: contact.profileImageUrl || null,
publicKeyBase64: contact.publicKeyBase64 || null,
seesMe: contact.seesMe || false,
registered: contact.registered || false,
}));
return {
data: {
data: [
{
tableName: "contacts",
rows,
},
],
},
};
};

4
src/main.common.ts

@ -13,8 +13,8 @@ import { logger } from "./utils/logger";
const platform = process.env.VITE_PLATFORM; const platform = process.env.VITE_PLATFORM;
const pwa_enabled = process.env.VITE_PWA_ENABLED === "true"; const pwa_enabled = process.env.VITE_PWA_ENABLED === "true";
logger.error("Platform", { platform }); logger.log("Platform", JSON.stringify({ platform }));
logger.error("PWA enabled", { pwa_enabled }); logger.log("PWA enabled", JSON.stringify({ pwa_enabled }));
// Global Error Handler // Global Error Handler
function setupGlobalErrorHandler(app: VueApp) { function setupGlobalErrorHandler(app: VueApp) {

4
src/main.web.ts

@ -5,8 +5,8 @@ import { logger } from "./utils/logger";
const platform = process.env.VITE_PLATFORM; const platform = process.env.VITE_PLATFORM;
const pwa_enabled = process.env.VITE_PWA_ENABLED === "true"; const pwa_enabled = process.env.VITE_PWA_ENABLED === "true";
logger.error("[Web] PWA enabled", { pwa_enabled }); logger.info("[Web] PWA enabled", { pwa_enabled });
logger.error("[Web] Platform", { platform }); logger.info("[Web] Platform", { platform });
// Only import service worker for web builds // Only import service worker for web builds
if (platform !== "electron" && pwa_enabled) { if (platform !== "electron" && pwa_enabled) {

40
src/services/AbsurdSqlDatabaseService.ts

@ -7,7 +7,7 @@ import type { DatabaseService, QueryExecResult } from "../interfaces/database";
import { logger } from "@/utils/logger"; import { logger } from "@/utils/logger";
interface QueuedOperation { interface QueuedOperation {
type: "run" | "query" | "getOneRow" | "getAll"; type: "run" | "query";
sql: string; sql: string;
params: unknown[]; params: unknown[];
resolve: (value: unknown) => void; resolve: (value: unknown) => void;
@ -84,7 +84,7 @@ class AbsurdSqlDatabaseService implements DatabaseService {
SQL.FS.mkdir("/sql"); SQL.FS.mkdir("/sql");
SQL.FS.mount(sqlFS, {}, "/sql"); SQL.FS.mount(sqlFS, {}, "/sql");
const path = "/sql/timesafari.sqlite"; const path = "/sql/timesafari.absurd-sql";
if (typeof SharedArrayBuffer === "undefined") { if (typeof SharedArrayBuffer === "undefined") {
const stream = SQL.FS.open(path, "a+"); const stream = SQL.FS.open(path, "a+");
await stream.node.contents.readIfFallback(); 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)" // 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;`); 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 // Run migrations
await runMigrations(sqlExec); await runMigrations(sqlExec, sqlQuery, extractMigrationNames);
this.initialized = true; this.initialized = true;
@ -123,7 +133,6 @@ class AbsurdSqlDatabaseService implements DatabaseService {
if (!operation) continue; if (!operation) continue;
try { try {
let queryResult: QueryExecResult[] = [];
let result: unknown; let result: unknown;
switch (operation.type) { switch (operation.type) {
case "run": case "run":
@ -132,14 +141,6 @@ class AbsurdSqlDatabaseService implements DatabaseService {
case "query": case "query":
result = await this.db.exec(operation.sql, operation.params); result = await this.db.exec(operation.sql, operation.params);
break; 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); operation.resolve(result);
} catch (error) { } catch (error) {
@ -222,19 +223,6 @@ class AbsurdSqlDatabaseService implements DatabaseService {
await this.waitForInitialization(); await this.waitForInitialization();
return this.queueOperation<QueryExecResult[]>("query", sql, params); 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 // Create a singleton instance

41
src/services/migrationService.ts

@ -1,6 +1,3 @@
import { logger } from "@/utils/logger";
import { QueryExecResult } from "../interfaces/database";
interface Migration { interface Migration {
name: string; name: string;
sql: string; sql: string;
@ -19,15 +16,20 @@ export class MigrationService {
return MigrationService.instance; return MigrationService.instance;
} }
async registerMigration(migration: Migration): Promise<void> { registerMigration(migration: Migration) {
this.migrations.push(migration); this.migrations.push(migration);
} }
async runMigrations( /**
sqlExec: ( * @param sqlExec - A function that executes a SQL statement and returns some update result
sql: string, * @param sqlQuery - A function that executes a SQL query and returns the result in some format
params?: unknown[], * @param extractMigrationNames - A function that extracts the names (string array) from a "select name from migrations" query
) => Promise<Array<QueryExecResult>>, */
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> { ): Promise<void> {
// Create migrations table if it doesn't exist // Create migrations table if it doesn't exist
await sqlExec(` await sqlExec(`
@ -39,26 +41,17 @@ export class MigrationService {
`); `);
// Get list of executed migrations // Get list of executed migrations
const result: QueryExecResult[] = await sqlExec( const result1: T = await sqlQuery("SELECT name FROM migrations;");
"SELECT name FROM migrations;", const executedMigrations = extractMigrationNames(result1);
);
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 // Run pending migrations in order
for (const migration of this.migrations) { for (const migration of this.migrations) {
if (!executedMigrations.has(migration.name)) { if (!executedMigrations.has(migration.name)) {
await sqlExec(migration.sql); await sqlExec(migration.sql);
await sqlExec("INSERT INTO migrations (name) VALUES (?)", [
migration.name, await sqlExec(
]); `INSERT INTO migrations (name) VALUES ('${migration.name}')`,
logger.log(`Migration ${migration.name} executed successfully`); );
} }
} }
} }

327
src/services/platforms/CapacitorPlatformService.ts

@ -1,8 +1,3 @@
import {
ImageResult,
PlatformService,
PlatformCapabilities,
} from "../PlatformService";
import { Filesystem, Directory, Encoding } from "@capacitor/filesystem"; import { Filesystem, Directory, Encoding } from "@capacitor/filesystem";
import { Camera, CameraResultType, CameraSource } from "@capacitor/camera"; import { Camera, CameraResultType, CameraSource } from "@capacitor/camera";
import { Share } from "@capacitor/share"; import { Share } from "@capacitor/share";
@ -10,15 +5,25 @@ import {
SQLiteConnection, SQLiteConnection,
SQLiteDBConnection, SQLiteDBConnection,
CapacitorSQLite, CapacitorSQLite,
Changes, capSQLiteChanges,
DBSQLiteValues,
} from "@capacitor-community/sqlite"; } 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 { logger } from "../../utils/logger";
import { QueryExecResult, SqlValue } from "@/interfaces/database";
import { DEFAULT_ENDORSER_API_SERVER } from "@/constants/app";
interface Migration { interface QueuedOperation {
name: string; type: "run" | "query";
sql: string; sql: string;
params: unknown[];
resolve: (value: unknown) => void;
reject: (reason: unknown) => void;
} }
/** /**
@ -32,14 +37,42 @@ interface Migration {
export class CapacitorPlatformService implements PlatformService { export class CapacitorPlatformService implements PlatformService {
private sqlite: SQLiteConnection; private sqlite: SQLiteConnection;
private db: SQLiteDBConnection | null = null; private db: SQLiteDBConnection | null = null;
private dbName = "timesafari.db"; private dbName = "timesafari.sqlite";
private initialized = false; private initialized = false;
private initializationPromise: Promise<void> | null = null;
private operationQueue: Array<QueuedOperation> = [];
private isProcessingQueue: boolean = false;
constructor() { constructor() {
this.sqlite = new SQLiteConnection(CapacitorSQLite); this.sqlite = new SQLiteConnection(CapacitorSQLite);
} }
private async initializeDatabase(): Promise<void> { 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) { if (this.initialized) {
return; return;
} }
@ -57,136 +90,144 @@ export class CapacitorPlatformService implements PlatformService {
await this.db.open(); await this.db.open();
// Set journal mode to WAL for better performance // 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 // Run migrations
await this.runMigrations(); await this.runCapacitorMigrations();
this.initialized = true; 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) { } catch (error) {
logger.error("Error initializing SQLite database:", error); logger.error(
throw new Error("Failed to initialize database"); "[CapacitorPlatformService] Error initializing SQLite database:",
error,
);
throw new Error(
"[CapacitorPlatformService] Failed to initialize database",
);
} }
} }
private async runMigrations(): Promise<void> { private async processQueue(): Promise<void> {
if (!this.db) { if (this.isProcessingQueue || !this.initialized || !this.db) {
throw new Error("Database not initialized"); return;
} }
// Create migrations table if it doesn't exist this.isProcessingQueue = true;
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 while (this.operationQueue.length > 0) {
const result = await this.db.query("SELECT name FROM migrations;"); const operation = this.operationQueue.shift();
const executedMigrations = new Set( if (!operation) continue;
result.values?.map((row) => row[0]) || [],
);
// Run pending migrations in order try {
const migrations: Migration[] = [ let result: unknown;
{ switch (operation.type) {
name: "001_initial", case "run": {
sql: ` const runResult = await this.db.run(
CREATE TABLE IF NOT EXISTS accounts ( operation.sql,
id INTEGER PRIMARY KEY AUTOINCREMENT, operation.params,
dateCreated TEXT NOT NULL, );
derivationPath TEXT, result = {
did TEXT NOT NULL, changes: runResult.changes?.changes || 0,
identityEncrBase64 TEXT, lastId: runResult.changes?.lastId,
mnemonicEncrBase64 TEXT, };
passkeyCredIdHex TEXT, break;
publicKeyHex TEXT NOT NULL }
); case "query": {
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) {
logger.error(
"[CapacitorPlatformService] Error while processing SQL queue:",
error,
);
operation.reject(error);
}
}
CREATE INDEX IF NOT EXISTS idx_accounts_did ON accounts(did); this.isProcessingQueue = false;
}
CREATE TABLE IF NOT EXISTS secret ( private async queueOperation<R>(
id INTEGER PRIMARY KEY AUTOINCREMENT, type: QueuedOperation["type"],
secretBase64 TEXT NOT NULL 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 ( // If we're already initialized, start processing the queue
id INTEGER PRIMARY KEY AUTOINCREMENT, if (this.initialized && this.db) {
accountDid TEXT, this.processQueue();
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); private async waitForInitialization(): Promise<void> {
// If we have an initialization promise, wait for it
INSERT INTO settings (id, apiServer) VALUES (1, '${DEFAULT_ENDORSER_API_SERVER}'); if (this.initializationPromise) {
await this.initializationPromise;
CREATE TABLE IF NOT EXISTS contacts ( return;
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); // If not initialized and no promise, start initialization
CREATE INDEX IF NOT EXISTS idx_contacts_name ON contacts(name); if (!this.initialized) {
await this.initializeDatabase();
return;
}
CREATE TABLE IF NOT EXISTS logs ( // If initialized but no db, something went wrong
date TEXT PRIMARY KEY, if (!this.db) {
message TEXT NOT NULL 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 ( private async runCapacitorMigrations(): Promise<void> {
id TEXT PRIMARY KEY, if (!this.db) {
blobB64 TEXT throw new Error("Database not initialized");
);
`,
},
];
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 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 +701,8 @@ export class CapacitorPlatformService implements PlatformService {
* @see PlatformService.dbQuery * @see PlatformService.dbQuery
*/ */
async dbQuery(sql: string, params?: unknown[]): Promise<QueryExecResult> { async dbQuery(sql: string, params?: unknown[]): Promise<QueryExecResult> {
await this.initializeDatabase(); await this.waitForInitialization();
if (!this.db) { return this.queueOperation<QueryExecResult>("query", sql, params || []);
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)}`,
);
}
} }
/** /**
@ -687,23 +712,11 @@ export class CapacitorPlatformService implements PlatformService {
sql: string, sql: string,
params?: unknown[], params?: unknown[],
): Promise<{ changes: number; lastId?: number }> { ): Promise<{ changes: number; lastId?: number }> {
await this.initializeDatabase(); await this.waitForInitialization();
if (!this.db) { return this.queueOperation<{ changes: number; lastId?: number }>(
throw new Error("Database not initialized"); "run",
} sql,
params || [],
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)}`,
);
}
} }
} }

13
src/services/platforms/ElectronPlatformService.ts

@ -60,10 +60,17 @@ export class ElectronPlatformService implements PlatformService {
await this.runMigrations(); await this.runMigrations();
this.initialized = true; this.initialized = true;
logger.log("SQLite database initialized successfully"); logger.log(
"[ElectronPlatformService] SQLite database initialized successfully",
);
} catch (error) { } catch (error) {
logger.error("Error initializing SQLite database:", error); logger.error(
throw new Error("Failed to initialize database"); "[ElectronPlatformService] Error initializing SQLite database:",
error,
);
throw new Error(
"[ElectronPlatformService] Failed to initialize database",
);
} }
} }

27
src/views/AccountViewView.vue

@ -430,12 +430,16 @@
<div class="mb-4 text-center"> <div class="mb-4 text-center">
{{ limitsMessage }} {{ limitsMessage }}
</div> </div>
<div> <div v-if="endorserLimits">
<p class="text-sm"> <p class="text-sm">
You have done You have done
<b>{{ endorserLimits?.doneClaimsThisWeek || "?" }} claims</b> out of <b
<b>{{ endorserLimits?.maxClaimsPerWeek || "?" }}</b> for this week. >{{ endorserLimits?.doneClaimsThisWeek || "?" }} claim{{
Your claims counter resets at endorserLimits?.doneClaimsThisWeek === 1 ? "" : "s"
}}</b
>
out of <b>{{ endorserLimits?.maxClaimsPerWeek || "?" }}</b> for this
week. Your claims counter resets at
<b class="whitespace-nowrap">{{ <b class="whitespace-nowrap">{{
readableDate(endorserLimits?.nextWeekBeginDateTime) readableDate(endorserLimits?.nextWeekBeginDateTime)
}}</b> }}</b>
@ -446,7 +450,9 @@
>{{ >{{
endorserLimits?.doneRegistrationsThisMonth || "?" endorserLimits?.doneRegistrationsThisMonth || "?"
}} }}
registrations</b registration{{
endorserLimits?.doneRegistrationsThisMonth === 1 ? "" : "s"
}}</b
> >
out of out of
<b>{{ endorserLimits?.maxRegistrationsPerMonth || "?" }}</b> for this <b>{{ endorserLimits?.maxRegistrationsPerMonth || "?" }}</b> for this
@ -459,9 +465,13 @@
</p> </p>
<p class="mt-3 text-sm"> <p class="mt-3 text-sm">
You have uploaded You have uploaded
<b>{{ imageLimits?.doneImagesThisWeek || "?" }} images</b> out of <b
<b>{{ imageLimits?.maxImagesPerWeek || "?" }}</b> for this week. Your >{{ imageLimits?.doneImagesThisWeek || "?" }} image{{
image counter resets at imageLimits?.doneImagesThisWeek === 1 ? "" : "s"
}}</b
>
out of <b>{{ imageLimits?.maxImagesPerWeek || "?" }}</b> for this
week. Your image counter resets at
<b class="whitespace-nowrap">{{ <b class="whitespace-nowrap">{{
readableDate(imageLimits?.nextWeekBeginDateTime) readableDate(imageLimits?.nextWeekBeginDateTime)
}}</b> }}</b>
@ -1204,7 +1214,6 @@ export default class AccountViewView extends Vue {
this.turnOffNotifyingFlags(); this.turnOffNotifyingFlags();
} }
} }
// console.log("Got to the end of 'mounted' call in AccountViewView.");
/** /**
* Beware! I've seen where we never get to this point because "ready" never resolves. * Beware! I've seen where we never get to this point because "ready" never resolves.
*/ */

100
src/views/ContactImportView.vue

@ -223,6 +223,76 @@ import { getContactJwtFromJwtUrl } from "../libs/crypto";
import { decodeEndorserJwt } from "../libs/crypto/vc"; import { decodeEndorserJwt } from "../libs/crypto/vc";
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory"; import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
/**
* Interface for contact data as stored in the database
* Differs from Contact interface in that contactMethods is stored as a JSON string
*/
interface ContactDbRecord {
did: string;
contactMethods: string;
name: string;
notes: string;
profileImageUrl: string;
publicKeyBase64: string;
nextPubKeyHashB64: string;
seesMe: boolean;
registered: boolean;
}
/**
* Ensures a value is a string, never null or undefined
*/
function safeString(val: unknown): string {
return typeof val === "string" ? val : val == null ? "" : String(val);
}
/**
* Converts a Contact object to a ContactDbRecord for database storage
* @param contact The contact object to convert
* @returns A ContactDbRecord with contactMethods as a JSON string
* @throws Error if contact.did is missing or invalid
*/
function contactToDbRecord(contact: Contact): ContactDbRecord {
if (!contact.did) {
throw new Error("Contact must have a DID");
}
// Convert contactMethods array to JSON string, defaulting to empty array
const contactMethodsStr =
contact.contactMethods != null
? JSON.stringify(contact.contactMethods)
: "[]";
return {
did: safeString(contact.did), // Required field, must be present
contactMethods: contactMethodsStr,
name: safeString(contact.name),
notes: safeString(contact.notes),
profileImageUrl: safeString(contact.profileImageUrl),
publicKeyBase64: safeString(contact.publicKeyBase64),
nextPubKeyHashB64: safeString(contact.nextPubKeyHashB64),
seesMe: contact.seesMe ?? false,
registered: contact.registered ?? false,
};
}
/**
* Converts a ContactDbRecord back to a Contact object
* @param record The database record to convert
* @returns A Contact object with parsed contactMethods array
*/
function dbRecordToContact(record: ContactDbRecord): Contact {
return {
...record,
name: safeString(record.name),
notes: safeString(record.notes),
profileImageUrl: safeString(record.profileImageUrl),
publicKeyBase64: safeString(record.publicKeyBase64),
nextPubKeyHashB64: safeString(record.nextPubKeyHashB64),
contactMethods: JSON.parse(record.contactMethods || "[]"),
};
}
/** /**
* Contact Import View Component * Contact Import View Component
* @author Matthew Raymer * @author Matthew Raymer
@ -533,25 +603,39 @@ export default class ContactImportView extends Vue {
if (this.contactsSelected[i]) { if (this.contactsSelected[i]) {
const contact = this.contactsImporting[i]; const contact = this.contactsImporting[i];
const existingContact = this.contactsExisting[contact.did]; const existingContact = this.contactsExisting[contact.did];
const platformService = PlatformServiceFactory.getInstance();
// Convert contact to database record format
const contactToStore = contactToDbRecord(contact);
if (existingContact) { if (existingContact) {
const platformService = PlatformServiceFactory.getInstance(); // Update existing contact
// @ts-expect-error because we're just using the value to store to the DB
contact.contactMethods = JSON.stringify(contact.contactMethods);
const { sql, params } = databaseUtil.generateUpdateStatement( const { sql, params } = databaseUtil.generateUpdateStatement(
contact as unknown as Record<string, unknown>, contactToStore as unknown as Record<string, unknown>,
"contacts", "contacts",
"did = ?", "did = ?",
[contact.did], [contact.did],
); );
await platformService.dbExec(sql, params); await platformService.dbExec(sql, params);
if (USE_DEXIE_DB) { if (USE_DEXIE_DB) {
await db.contacts.update(contact.did, contact); // For Dexie, we need to parse the contactMethods back to an array
await db.contacts.update(
contact.did,
dbRecordToContact(contactToStore),
);
} }
updatedCount++; updatedCount++;
} else { } else {
// without explicit clone on the Proxy, we get: DataCloneError: Failed to execute 'add' on 'IDBObjectStore': #<Object> could not be cloned. // Add new contact
// DataError: Failed to execute 'add' on 'IDBObjectStore': Evaluating the object store's key path yielded a value that is not a valid key. const { sql, params } = databaseUtil.generateInsertStatement(
await db.contacts.add(R.clone(contact)); contactToStore as unknown as Record<string, unknown>,
"contacts",
);
await platformService.dbExec(sql, params);
if (USE_DEXIE_DB) {
// For Dexie, we need to parse the contactMethods back to an array
await db.contacts.add(dbRecordToContact(contactToStore));
}
importedCount++; importedCount++;
} }
} }

4
src/views/ContactQRScanShowView.vue

@ -848,7 +848,7 @@ export default class ContactQRScanShow extends Vue {
if (stopAsking) { if (stopAsking) {
const platformService = PlatformServiceFactory.getInstance(); const platformService = PlatformServiceFactory.getInstance();
await platformService.dbExec( await platformService.dbExec(
"UPDATE settings SET hideRegisterPromptOnNewContact = ? WHERE key = ?", "UPDATE settings SET hideRegisterPromptOnNewContact = ? WHERE id = ?",
[stopAsking, MASTER_SETTINGS_KEY], [stopAsking, MASTER_SETTINGS_KEY],
); );
if (USE_DEXIE_DB) { if (USE_DEXIE_DB) {
@ -863,7 +863,7 @@ export default class ContactQRScanShow extends Vue {
if (stopAsking) { if (stopAsking) {
const platformService = PlatformServiceFactory.getInstance(); const platformService = PlatformServiceFactory.getInstance();
await platformService.dbExec( await platformService.dbExec(
"UPDATE settings SET hideRegisterPromptOnNewContact = ? WHERE key = ?", "UPDATE settings SET hideRegisterPromptOnNewContact = ? WHERE id = ?",
[stopAsking, MASTER_SETTINGS_KEY], [stopAsking, MASTER_SETTINGS_KEY],
); );
if (USE_DEXIE_DB) { if (USE_DEXIE_DB) {

212
src/views/HomeView.vue

@ -515,30 +515,92 @@ export default class HomeView extends Vue {
*/ */
private async initializeIdentity() { private async initializeIdentity() {
try { 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) { if (this.allMyDids.length === 0) {
this.isCreatingIdentifier = true; try {
const newDid = await generateSaveAndActivateIdentity(); this.isCreatingIdentifier = true;
this.isCreatingIdentifier = false; const newDid = await generateSaveAndActivateIdentity();
this.allMyDids = [newDid]; 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(); // Load settings with better error context
if (USE_DEXIE_DB) { let settings;
settings = await retrieveSettingsForActiveAccount(); 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.apiServer = settings.apiServer || "";
this.activeDid = settings.activeDid || ""; this.activeDid = settings.activeDid || "";
const platformService = PlatformServiceFactory.getInstance();
const dbContacts = await platformService.dbQuery( // Load contacts with graceful fallback
"SELECT * FROM contacts", try {
); const platformService = PlatformServiceFactory.getInstance();
this.allContacts = databaseUtil.mapQueryResultToValues( const dbContacts = await platformService.dbQuery(
dbContacts, "SELECT * FROM contacts",
) as unknown as Contact[]; );
if (USE_DEXIE_DB) { this.allContacts = databaseUtil.mapQueryResultToValues(
this.allContacts = await db.contacts.toArray(); 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.feedLastViewedClaimId = settings.lastViewedClaimId;
this.givenName = settings.firstName || ""; this.givenName = settings.firstName || "";
this.isFeedFilteredByVisible = !!settings.filterFeedByVisible; this.isFeedFilteredByVisible = !!settings.filterFeedByVisible;
@ -551,13 +613,14 @@ export default class HomeView extends Vue {
this.showShortcutBvc = !!settings.showShortcutBvc; this.showShortcutBvc = !!settings.showShortcutBvc;
this.isAnyFeedFilterOn = checkIsAnyFeedFilterOn(settings); this.isAnyFeedFilterOn = checkIsAnyFeedFilterOn(settings);
// Check onboarding status
if (!settings.finishedOnboarding) { if (!settings.finishedOnboarding) {
(this.$refs.onboardingDialog as OnboardingDialog).open( (this.$refs.onboardingDialog as OnboardingDialog).open(
OnboardPage.Home, OnboardPage.Home,
); );
} }
// someone may have have registered after sharing contact info, so recheck // Check registration status if needed
if (!this.isRegistered && this.activeDid) { if (!this.isRegistered && this.activeDid) {
try { try {
const resp = await fetchEndorserRateLimits( const resp = await fetchEndorserRateLimits(
@ -577,51 +640,75 @@ export default class HomeView extends Vue {
}); });
} }
this.isRegistered = true; this.isRegistered = true;
logConsoleAndDb(
`[HomeView] User ${this.activeDid} is now registered`,
);
} }
} catch (e) { } catch (error) {
// ignore the error... just keep us unregistered 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 // Initialize feed and offers
this.updateAllFeed(); try {
// Start feed update in background
this.updateAllFeed().catch((error) => {
logConsoleAndDb(
`[HomeView] Background feed update failed: ${error}`,
true,
);
});
if (this.activeDid) { // Load new offers if we have an active DID
const offersToUserData = await getNewOffersToUser( if (this.activeDid) {
this.axios, const [offersToUser, offersToProjects] = await Promise.all([
this.apiServer, getNewOffersToUser(
this.activeDid, this.axios,
this.lastAckedOfferToUserJwtId, this.apiServer,
); this.activeDid,
this.numNewOffersToUser = offersToUserData.data.length; this.lastAckedOfferToUserJwtId,
this.newOffersToUserHitLimit = offersToUserData.hitLimit; ),
} getNewOffersToUserProjects(
this.axios,
this.apiServer,
this.activeDid,
this.lastAckedOfferToUserProjectsJwtId,
),
]);
if (this.activeDid) { this.numNewOffersToUser = offersToUser.data.length;
const offersToUserProjects = await getNewOffersToUserProjects( this.newOffersToUserHitLimit = offersToUser.hitLimit;
this.axios, this.numNewOffersToUserProjects = offersToProjects.data.length;
this.apiServer, this.newOffersToUserProjectsHitLimit = offersToProjects.hitLimit;
this.activeDid,
this.lastAckedOfferToUserProjectsJwtId, 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,
); );
this.numNewOffersToUserProjects = offersToUserProjects.data.length;
this.newOffersToUserProjectsHitLimit = offersToUserProjects.hitLimit;
} }
} catch (error) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any this.handleError(error);
} catch (err: any) { throw error; // Re-throw to be caught by mounted()
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,
);
} }
} }
@ -784,19 +871,26 @@ export default class HomeView extends Vue {
* - Displays user notification * - Displays user notification
* *
* @internal * @internal
* Called by mounted() * Called by mounted() and initializeIdentity()
* @param err Error object with optional userMessage * @param err Error object with optional userMessage
*/ */
private handleError(err: unknown) { 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( this.$notify(
{ {
group: "alert", group: "alert",
type: "danger", type: "danger",
title: "Error", title: "Error",
text: text:
(err as { userMessage?: string })?.userMessage || userMessage ||
"There was an error retrieving your settings or the latest activity.", "There was an error loading your data. Please try refreshing the page.",
}, },
5000, 5000,
); );

16
src/views/IdentitySwitcherView.vue

@ -115,6 +115,7 @@ import { MASTER_SETTINGS_KEY } from "../db/tables/settings";
import * as databaseUtil from "../db/databaseUtil"; import * as databaseUtil from "../db/databaseUtil";
import { retrieveAllAccountsMetadata } from "../libs/util"; import { retrieveAllAccountsMetadata } from "../libs/util";
import { logger } from "../utils/logger"; import { logger } from "../utils/logger";
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
@Component({ components: { QuickNav } }) @Component({ components: { QuickNav } })
export default class IdentitySwitcherView extends Vue { export default class IdentitySwitcherView extends Vue {
@ -167,10 +168,17 @@ export default class IdentitySwitcherView extends Vue {
if (did === "0") { if (did === "0") {
did = undefined; did = undefined;
} }
await db.open(); const platformService = PlatformServiceFactory.getInstance();
await db.settings.update(MASTER_SETTINGS_KEY, { await platformService.dbExec(
activeDid: did, `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" }); this.$router.push({ name: "account" });
} }

9
src/views/LogView.vue

@ -42,6 +42,13 @@
> >
</div> </div>
</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> </section>
</template> </template>
@ -70,6 +77,7 @@ export default class LogView extends Vue {
loading = true; loading = true;
logs: Log[] = []; logs: Log[] = [];
error: string | null = null; error: string | null = null;
memoryLogs: string[] = [];
async mounted() { async mounted() {
await this.loadLogs(); await this.loadLogs();
@ -78,6 +86,7 @@ export default class LogView extends Vue {
async loadLogs() { async loadLogs() {
try { try {
this.error = null; // Clear any previous errors this.error = null; // Clear any previous errors
this.memoryLogs = databaseUtil.memoryLogs;
let allLogs: Log[] = []; let allLogs: Log[] = [];
const platformService = PlatformServiceFactory.getInstance(); const platformService = PlatformServiceFactory.getInstance();

12
src/views/ProjectViewView.vue

@ -386,11 +386,15 @@
> >
<!-- just show the hours, or alternatively whatever is first --> <!-- just show the hours, or alternatively whatever is first -->
<span v-if="givenTotalHours() > 0"> <span v-if="givenTotalHours() > 0">
{{ givenTotalHours() }} {{ libsUtil.UNIT_SHORT["HUR"] }} {{ libsUtil.formattedAmount(givenTotalHours(), "HUR") }}
</span> </span>
<span v-else> <span v-else>
{{ givesTotalsByUnit[0].amount }} {{
{{ libsUtil.UNIT_SHORT[givesTotalsByUnit[0].unit] }} libsUtil.formattedAmount(
givesTotalsByUnit[0].amount,
givesTotalsByUnit[0].unit,
)
}}
</span> </span>
<span v-if="givesTotalsByUnit.length > 1">...</span> <span v-if="givesTotalsByUnit.length > 1">...</span>
<span> <span>
@ -411,7 +415,7 @@
:icon="libsUtil.iconForUnitCode(total.unit)" :icon="libsUtil.iconForUnitCode(total.unit)"
class="fa-fw text-slate-400 mr-1" class="fa-fw text-slate-400 mr-1"
/> />
{{ total.amount }} {{ libsUtil.UNIT_LONG[total.unit] }} {{ libsUtil.formattedAmount(total.amount, total.unit) }}
</div> </div>
</div> </div>
</span> </span>

Loading…
Cancel
Save