Compare commits
24 Commits
build-with
...
master-pat
| Author | SHA1 | Date | |
|---|---|---|---|
| 6482cfa6a3 | |||
| d3c6f0ec27 | |||
| 3ea4dd6f9d | |||
| 0f35b16ddb | |||
| 61370ce0ad | |||
| b3e342c733 | |||
| 3c463b1a2a | |||
| de476210c5 | |||
| ff61a0bdf3 | |||
| e0b9481be5 | |||
| bcbb80e034 | |||
| 64f24dc473 | |||
| 6ddde21a86 | |||
| fd0026ac2d | |||
| 3fce10ae98 | |||
| 002f240720 | |||
| ffe8d90161 | |||
| 6d6816d1a8 | |||
| c1477d0266 | |||
| 33ce6bdb72 | |||
| dc21e8dac3 | |||
| a9a8ba217c | |||
| b0d99e7c1e | |||
| 861408c7bc |
@@ -3,7 +3,7 @@
|
|||||||
# iOS doesn't like spaces in the app title.
|
# iOS doesn't like spaces in the app title.
|
||||||
TIME_SAFARI_APP_TITLE="TimeSafari_Test"
|
TIME_SAFARI_APP_TITLE="TimeSafari_Test"
|
||||||
VITE_APP_SERVER=https://test.timesafari.app
|
VITE_APP_SERVER=https://test.timesafari.app
|
||||||
# This is the claim ID for actions in the BVC project.
|
# This is the claim ID for actions in the BVC project, with the JWT ID on this environment (not production).
|
||||||
VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HWE8FWHQ1YGP7GFZYYPS272F
|
VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HWE8FWHQ1YGP7GFZYYPS272F
|
||||||
VITE_DEFAULT_ENDORSER_API_SERVER=https://test-api.endorser.ch
|
VITE_DEFAULT_ENDORSER_API_SERVER=https://test-api.endorser.ch
|
||||||
|
|
||||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -8,7 +8,6 @@ signature.bin
|
|||||||
# generated during `npm run build`
|
# generated during `npm run build`
|
||||||
sw_scripts-combined.js
|
sw_scripts-combined.js
|
||||||
*.pem
|
*.pem
|
||||||
tsconfig.node.tsbuildinfo
|
|
||||||
verified.txt
|
verified.txt
|
||||||
myenv
|
myenv
|
||||||
|
|
||||||
@@ -41,15 +40,19 @@ pnpm-debug.log*
|
|||||||
playwright-tests
|
playwright-tests
|
||||||
dist-electron-packages
|
dist-electron-packages
|
||||||
.ruby-version
|
.ruby-version
|
||||||
|
+.env
|
||||||
|
|
||||||
# Test files generated by scripts test-ios.js & test-android.js
|
# Test files generated by scripts test-ios.js & test-android.js
|
||||||
.generated/
|
.generated/
|
||||||
|
|
||||||
|
.env.default
|
||||||
vendor/
|
vendor/
|
||||||
|
|
||||||
|
# Build logs
|
||||||
build_logs/
|
build_logs/
|
||||||
|
|
||||||
# PWA icon files generated by capacitor-assets
|
# PWA icon files generated by capacitor-assets
|
||||||
icons
|
icons
|
||||||
|
|
||||||
|
|
||||||
android/app/src/main/res/
|
android/app/src/main/res/
|
||||||
@@ -11,6 +11,17 @@ For a quick dev environment setup, use [pkgx](https://pkgx.dev).
|
|||||||
- Git
|
- Git
|
||||||
- For desktop builds: Additional build tools based on your OS
|
- For desktop builds: Additional build tools based on your OS
|
||||||
|
|
||||||
|
## Forks
|
||||||
|
|
||||||
|
If you have forked this to make your own app, you'll want to customize the iOS & Android files. You can either edit existing ones, or you can remove the `ios` and `android` directories and regenerate them before the `npx cap sync` step in each setup.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx cap add android
|
||||||
|
npx cap add ios
|
||||||
|
```
|
||||||
|
|
||||||
|
You'll also want to edit the deep link configuration (see below).
|
||||||
|
|
||||||
## Initial Setup
|
## Initial Setup
|
||||||
|
|
||||||
Install dependencies:
|
Install dependencies:
|
||||||
@@ -22,7 +33,7 @@ Install dependencies:
|
|||||||
## Web Dev Locally
|
## Web Dev Locally
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
NODE_ENV=dev npm run start:web
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
## Web Build for Server
|
## Web Build for Server
|
||||||
@@ -31,7 +42,7 @@ Install dependencies:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
rm -rf dist
|
rm -rf dist
|
||||||
NODE_ENV=prod npm run build:web
|
npm run build:web
|
||||||
```
|
```
|
||||||
|
|
||||||
The built files will be in the `dist` directory.
|
The built files will be in the `dist` directory.
|
||||||
@@ -41,7 +52,7 @@ Install dependencies:
|
|||||||
You'll likely want to use test locations for the Endorser & image & partner servers; see "DEFAULT_ENDORSER_API_SERVER" & "DEFAULT_IMAGE_API_SERVER" & "DEFAULT_PARTNER_API_SERVER" below.
|
You'll likely want to use test locations for the Endorser & image & partner servers; see "DEFAULT_ENDORSER_API_SERVER" & "DEFAULT_IMAGE_API_SERVER" & "DEFAULT_PARTNER_API_SERVER" below.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
NODE_ENV=dev npm run serve:web
|
npm run serve
|
||||||
```
|
```
|
||||||
|
|
||||||
### Compile and minify for test & production
|
### Compile and minify for test & production
|
||||||
@@ -50,9 +61,11 @@ Install dependencies:
|
|||||||
|
|
||||||
* `npx prettier --write ./sw_scripts/`
|
* `npx prettier --write ./sw_scripts/`
|
||||||
|
|
||||||
* Update the ClickUp tasks & CHANGELOG.md & the version in package.json, run `npm install`.
|
* Update the ClickUp tasks & CHANGELOG.md & the version in package.json.
|
||||||
|
|
||||||
* Run a build to make sure package-lock version is updated, linting works, etc: `npm install && npm run build:web`
|
* Run install & build to make sure package-lock version is updated, linting works, etc: `npm install && npm run build:web`
|
||||||
|
|
||||||
|
* If also deploying to mobile, update the new version in the ios & android filed.
|
||||||
|
|
||||||
* Commit everything (since the commit hash is used the app).
|
* Commit everything (since the commit hash is used the app).
|
||||||
|
|
||||||
@@ -63,7 +76,7 @@ Install dependencies:
|
|||||||
* For test, build the app (because test server is not yet set up to build):
|
* For test, build the app (because test server is not yet set up to build):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
NODE_ENV=test TIME_SAFARI_APP_TITLE="TimeSafari_Test" VITE_APP_SERVER=https://test.timesafari.app VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HWE8FWHQ1YGP7GFZYYPS272F VITE_DEFAULT_ENDORSER_API_SERVER=https://test-api.endorser.ch VITE_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app VITE_DEFAULT_PARTNER_API_SERVER=https://test-partner-api.endorser.ch VITE_DEFAULT_PUSH_SERVER=https://test.timesafari.app VITE_PASSKEYS_ENABLED=true npm run build:web
|
TIME_SAFARI_APP_TITLE="TimeSafari_Test" VITE_APP_SERVER=https://test.timesafari.app VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HWE8FWHQ1YGP7GFZYYPS272F VITE_DEFAULT_ENDORSER_API_SERVER=https://test-api.endorser.ch VITE_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app VITE_DEFAULT_PARTNER_API_SERVER=https://test-partner-api.endorser.ch VITE_DEFAULT_PUSH_SERVER=https://test.timesafari.app VITE_PASSKEYS_ENABLED=true npm run build:web
|
||||||
```
|
```
|
||||||
|
|
||||||
... and transfer to the test server:
|
... and transfer to the test server:
|
||||||
@@ -205,10 +218,10 @@ docker run -d \
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# For AppImage (recommended)
|
# For AppImage (recommended)
|
||||||
npm run build:electron-linux
|
npm run electron:build-linux
|
||||||
|
|
||||||
# For .deb package
|
# For .deb package
|
||||||
npm run build:electron-linux-deb
|
npm run electron:build-linux-deb
|
||||||
```
|
```
|
||||||
|
|
||||||
3. The packaged applications will be in `dist-electron-packages/`:
|
3. The packaged applications will be in `dist-electron-packages/`:
|
||||||
@@ -220,19 +233,19 @@ docker run -d \
|
|||||||
1. Build the electron app in production mode:
|
1. Build the electron app in production mode:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
NODE_ENV=prod npm run build:web
|
npm run build:web
|
||||||
npm run build:electron
|
npm run build:electron
|
||||||
npm run build:electron-mac
|
npm run electron:build-mac
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Package the Electron app for macOS:
|
2. Package the Electron app for macOS:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# For Intel Macs
|
# For Intel Macs
|
||||||
npm run build:electron-mac
|
npm run electron:build-mac
|
||||||
|
|
||||||
# For Universal build (Intel + Apple Silicon)
|
# For Universal build (Intel + Apple Silicon)
|
||||||
npm run build:electron-mac-universal
|
npm run electron:build-mac-universal
|
||||||
```
|
```
|
||||||
|
|
||||||
3. The packaged applications will be in `dist-electron-packages/`:
|
3. The packaged applications will be in `dist-electron-packages/`:
|
||||||
@@ -254,7 +267,7 @@ For public distribution on macOS, you need to code sign and notarize your app:
|
|||||||
|
|
||||||
2. Build with signing:
|
2. Build with signing:
|
||||||
```bash
|
```bash
|
||||||
npm run build:electron-mac
|
npm run electron:build-mac
|
||||||
```
|
```
|
||||||
|
|
||||||
### Running the Packaged App
|
### Running the Packaged App
|
||||||
@@ -293,10 +306,10 @@ For testing the Electron build before packaging:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Build and run in development mode (includes DevTools)
|
# Build and run in development mode (includes DevTools)
|
||||||
npm run build:electron
|
npm run electron:dev
|
||||||
|
|
||||||
# Build in production mode and test
|
# Build in production mode and test
|
||||||
npm run build:electron-prod && npm run start:electron
|
npm run build:electron-prod && npm run electron:start
|
||||||
```
|
```
|
||||||
|
|
||||||
## Mobile Builds (Capacitor)
|
## Mobile Builds (Capacitor)
|
||||||
@@ -351,7 +364,7 @@ Prerequisites: macOS with Xcode installed
|
|||||||
4. Bump the version to match Android & package.json:
|
4. Bump the version to match Android & package.json:
|
||||||
|
|
||||||
```
|
```
|
||||||
cd ios/App && xcrun agvtool new-version 35 && perl -p -i -e "s/MARKETING_VERSION = .*;/MARKETING_VERSION = 1.0.2;/g" App.xcodeproj/project.pbxproj && cd -
|
cd ios/App && xcrun agvtool new-version 44 && perl -p -i -e "s/MARKETING_VERSION = .*;/MARKETING_VERSION = 1.0.10;/g" App.xcodeproj/project.pbxproj && cd -
|
||||||
# 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
|
||||||
```
|
```
|
||||||
@@ -374,11 +387,12 @@ Prerequisites: macOS with Xcode installed
|
|||||||
* This will trigger a build and take time, needing user's "login" keychain password (user's 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. You may have to 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 "-".
|
* 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".
|
||||||
|
* Eventually it'll be "Ready for Distribution" which means
|
||||||
|
|
||||||
### Android Build
|
### Android Build
|
||||||
|
|
||||||
@@ -468,12 +482,3 @@ You must add the following intent filter to the `android/app/src/main/AndroidMan
|
|||||||
```
|
```
|
||||||
|
|
||||||
... though when we tried that most recently it failed to 'build' the APK with: http(s) scheme and host attribute are missing, but are required for Android App Links [AppLinkUrlError]
|
... though when we tried that most recently it failed to 'build' the APK with: http(s) scheme and host attribute are missing, but are required for Android App Links [AppLinkUrlError]
|
||||||
|
|
||||||
## Forks
|
|
||||||
|
|
||||||
If you have forked this to make your own app, you'll want to customize the iOS & Android files. You can either edit existing ones, or you can remove the `ios` and `android` directories and regenerate them before the `npx cap sync` step in each setup.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npx cap add android
|
|
||||||
npx cap add ios
|
|
||||||
```
|
|
||||||
33
CHANGELOG.md
33
CHANGELOG.md
@@ -6,12 +6,37 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
|
||||||
## [Unreleased]
|
## [1.0.10] - 2025.08.25
|
||||||
### Fixed
|
### Fixed
|
||||||
- Set the environment variables correctly (so simulator deep link domain is right).
|
- Now shows confirmable claims 30 minutes before meeting starts
|
||||||
|
|
||||||
|
|
||||||
|
## [1.0.9] - 2025.08.20
|
||||||
|
### Fixed
|
||||||
|
- Deep link errors for meeting members
|
||||||
|
|
||||||
|
|
||||||
|
## [1.0.6] - 2025.08.09
|
||||||
|
### Fixed
|
||||||
|
- Deep link errors where none would validate
|
||||||
|
|
||||||
|
|
||||||
|
## [1.0.5] - 2025.07.24
|
||||||
|
### Fixed
|
||||||
|
- Export & import of contacts corrupted contact methods
|
||||||
|
|
||||||
|
|
||||||
|
## [1.0.4] - 2025.07.20 - 002f2407208d56cc59c0aa7c880535ae4cbace8b
|
||||||
|
### Fixed
|
||||||
|
- Deep link for invite-one-accept
|
||||||
|
|
||||||
|
|
||||||
|
## [1.0.3] - 2025.07.12 - a9a8ba217cd6015321911e98e6843e988dc2c4ae
|
||||||
### Changed
|
### Changed
|
||||||
- Photo is pinned to profile mode.
|
- Photo is pinned to profile mode
|
||||||
- NODE_ENV is now mandatory.
|
### Fixed
|
||||||
|
- Deep link URLs (and other prod settings)
|
||||||
|
- Error in BVC begin view
|
||||||
|
|
||||||
|
|
||||||
## [1.0.2] - 2025.06.20 - 276e0a741bc327de3380c4e508cccb7fee58c06d
|
## [1.0.2] - 2025.06.20 - 276e0a741bc327de3380c4e508cccb7fee58c06d
|
||||||
|
|||||||
40
README.md
40
README.md
@@ -3,22 +3,49 @@
|
|||||||
[Time Safari](https://timesafari.org/) allows people to ease into collaboration: start with expressions of gratitude
|
[Time Safari](https://timesafari.org/) allows people to ease into collaboration: start with expressions of gratitude
|
||||||
and expand to crowd-fund with time & money, then record and see the impact of contributions.
|
and expand to crowd-fund with time & money, then record and see the impact of contributions.
|
||||||
|
|
||||||
|
## Database Migration Status
|
||||||
|
|
||||||
|
**Current Status**: The application is undergoing a migration from Dexie (IndexedDB) to SQLite using absurd-sql. This migration is in **Phase 2** with a well-defined migration fence in place.
|
||||||
|
|
||||||
|
### Migration Progress
|
||||||
|
- ✅ **SQLite Database Service**: Fully implemented with absurd-sql
|
||||||
|
- ✅ **Platform Service Layer**: Unified database interface across platforms
|
||||||
|
- ✅ **Settings Migration**: Core user settings transferred
|
||||||
|
- ✅ **Account Migration**: Identity and key management
|
||||||
|
- 🔄 **Contact Migration**: User contact data (via import interface)
|
||||||
|
- 📋 **Code Cleanup**: Remove unused Dexie imports
|
||||||
|
|
||||||
|
### Migration Fence
|
||||||
|
The migration is controlled by a **migration fence** that separates legacy Dexie code from the new SQLite implementation. See [Migration Fence Definition](doc/migration-fence-definition.md) for complete details.
|
||||||
|
|
||||||
|
**Key Points**:
|
||||||
|
- Legacy Dexie database is disabled by default (`USE_DEXIE_DB = false`)
|
||||||
|
- All database operations go through `PlatformService`
|
||||||
|
- Migration tools provide controlled access to both databases
|
||||||
|
- Clear separation between legacy and new code
|
||||||
|
|
||||||
|
### Migration Documentation
|
||||||
|
- [Migration Guide](doc/migration-to-wa-sqlite.md) - Complete migration process
|
||||||
|
- [Migration Fence Definition](doc/migration-fence-definition.md) - Fence boundaries and rules
|
||||||
|
- [Database Migration Guide](doc/database-migration-guide.md) - User-facing migration tools
|
||||||
|
|
||||||
## Roadmap
|
## Roadmap
|
||||||
|
|
||||||
See [ClickUp](https://sharing.clickup.com/9014278710/l/h/8cmnyhp-174/10573fec74e2ba0) for current priorities.
|
See [project.task.yaml](project.task.yaml) for current priorities.
|
||||||
|
(Numbers at the beginning of lines are estimated hours. See [taskyaml.org](https://taskyaml.org/) for details.)
|
||||||
|
|
||||||
## Setup & Building
|
## Setup & Building
|
||||||
|
|
||||||
Quick start:
|
Quick start:
|
||||||
|
|
||||||
* For setup, we recommend [pkgx](https://pkgx.dev), which installs what you need (either automatically or with the `dev` command). Core dependencies are typescript & npm; when building for other platforms, you'll need other things such as those in the pkgx.yaml & doc/BUILDING.md files.
|
* For setup, we recommend [pkgx](https://pkgx.dev), which installs what you need (either automatically or with the `dev` command). Core dependencies are typescript & npm; when building for other platforms, you'll need other things such as those in the pkgx.yaml & BUILDING.md files.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install
|
npm install
|
||||||
NODE_ENV=dev npm run start:web
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
See [BUILDING.md](doc/BUILDING.md) for more details.
|
See [BUILDING.md](BUILDING.md) for more details.
|
||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
|
|
||||||
@@ -28,7 +55,7 @@ See [TESTING.md](test-playwright/TESTING.md) for detailed test instructions.
|
|||||||
|
|
||||||
Application icons are in the `assets` directory, processed by the `capacitor-assets` command.
|
Application icons are in the `assets` directory, processed by the `capacitor-assets` command.
|
||||||
|
|
||||||
To add a Font Awesome icon, add to main.ts and reference with `font-awesome` element and `icon` attribute with the hyphenated name.
|
To add a Font Awesome icon, add to fontawesome.ts and reference with `font-awesome` element and `icon` attribute with the hyphenated name.
|
||||||
|
|
||||||
## Other
|
## Other
|
||||||
|
|
||||||
@@ -70,6 +97,9 @@ The application uses a platform-agnostic database layer:
|
|||||||
|
|
||||||
**Development Guidelines**:
|
**Development Guidelines**:
|
||||||
- Always use `PlatformService` for database operations
|
- Always use `PlatformService` for database operations
|
||||||
|
- Never import Dexie directly in application code
|
||||||
|
- Test with `USE_DEXIE_DB = false` for new features
|
||||||
|
- Use migration tools for data transfer between systems
|
||||||
|
|
||||||
### Kudos
|
### Kudos
|
||||||
|
|
||||||
|
|||||||
@@ -31,8 +31,8 @@ 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 35
|
versionCode 44
|
||||||
versionName "1.0.2"
|
versionName "1.0.10"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
aaptOptions {
|
aaptOptions {
|
||||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||||
|
|||||||
@@ -27,32 +27,6 @@ The Database Migration feature allows you to compare and migrate data between De
|
|||||||
- Clear success and error messaging
|
- Clear success and error messaging
|
||||||
- Export functionality for comparison data
|
- Export functionality for comparison data
|
||||||
|
|
||||||
## Database Migration Status
|
|
||||||
|
|
||||||
**Current Status**: The application is undergoing a migration from Dexie (IndexedDB) to SQLite using absurd-sql. This migration is in **Phase 2** with a well-defined migration fence in place.
|
|
||||||
|
|
||||||
### Migration Progress
|
|
||||||
- ✅ **SQLite Database Service**: Fully implemented with absurd-sql
|
|
||||||
- ✅ **Platform Service Layer**: Unified database interface across platforms
|
|
||||||
- ✅ **Settings Migration**: Core user settings transferred
|
|
||||||
- ✅ **Account Migration**: Identity and key management
|
|
||||||
- 🔄 **Contact Migration**: User contact data (via import interface)
|
|
||||||
- 📋 **Code Cleanup**: Remove unused Dexie imports
|
|
||||||
|
|
||||||
### Migration Fence
|
|
||||||
The migration is controlled by a **migration fence** that separates legacy Dexie code from the new SQLite implementation. See [Migration Fence Definition](doc/migration-fence-definition.md) for complete details.
|
|
||||||
|
|
||||||
**Key Points**:
|
|
||||||
- Legacy Dexie database is disabled by default (`USE_DEXIE_DB = false`)
|
|
||||||
- All database operations go through `PlatformService`
|
|
||||||
- Migration tools provide controlled access to both databases
|
|
||||||
- Clear separation between legacy and new code
|
|
||||||
|
|
||||||
### Migration Documentation
|
|
||||||
- [Migration Guide](doc/migration-to-wa-sqlite.md) - Complete migration process
|
|
||||||
- [Migration Fence Definition](doc/migration-fence-definition.md) - Fence boundaries and rules
|
|
||||||
- [Database Migration Guide](doc/database-migration-guide.md) - User-facing migration tools
|
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
### Enable Dexie Database
|
### Enable Dexie Database
|
||||||
|
|||||||
12
index.html
12
index.html
@@ -15,21 +15,17 @@
|
|||||||
<script type="module">
|
<script type="module">
|
||||||
const platform = process.env.VITE_PLATFORM;
|
const platform = process.env.VITE_PLATFORM;
|
||||||
switch (platform) {
|
switch (platform) {
|
||||||
case 'capacitor': // BuildPlatform.Capacitor
|
case 'capacitor':
|
||||||
import('./src/main.capacitor.ts');
|
import('./src/main.capacitor.ts');
|
||||||
break;
|
break;
|
||||||
case 'electron': // BuildPlatform.Electron
|
case 'electron':
|
||||||
import('./src/main.electron.ts');
|
import('./src/main.electron.ts');
|
||||||
break;
|
break;
|
||||||
case 'pywebview': // BuildPlatform.PyWebView
|
case 'pywebview':
|
||||||
import('./src/main.pywebview.ts');
|
import('./src/main.pywebview.ts');
|
||||||
break;
|
break;
|
||||||
case 'web': // BuildPlatform.Web
|
|
||||||
import('./src/main.web.ts');
|
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
console.error(`Unknown platform: ${platform}`);
|
import('./src/main.web.ts');
|
||||||
throw new Error(`Unknown platform: ${platform}`);
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -403,7 +403,7 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 35;
|
CURRENT_PROJECT_VERSION = 44;
|
||||||
DEVELOPMENT_TEAM = GM3FS5JQPH;
|
DEVELOPMENT_TEAM = GM3FS5JQPH;
|
||||||
ENABLE_APP_SANDBOX = NO;
|
ENABLE_APP_SANDBOX = NO;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||||
@@ -413,7 +413,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.0.2;
|
MARKETING_VERSION = 1.0.10;
|
||||||
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
|
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari;
|
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
@@ -430,7 +430,7 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 35;
|
CURRENT_PROJECT_VERSION = 44;
|
||||||
DEVELOPMENT_TEAM = GM3FS5JQPH;
|
DEVELOPMENT_TEAM = GM3FS5JQPH;
|
||||||
ENABLE_APP_SANDBOX = NO;
|
ENABLE_APP_SANDBOX = NO;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||||
@@ -440,7 +440,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.0.2;
|
MARKETING_VERSION = 1.0.10;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari;
|
PRODUCT_BUNDLE_IDENTIFIER = app.timesafari;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
|
||||||
|
|||||||
4596
package-lock.json
generated
4596
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
59
package.json
59
package.json
@@ -1,42 +1,49 @@
|
|||||||
{
|
{
|
||||||
"name": "timesafari",
|
"name": "timesafari",
|
||||||
"version": "1.0.3-beta",
|
"version": "1.0.10",
|
||||||
"description": "Time Safari Application",
|
"description": "Time Safari Application",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Time Safari Team"
|
"name": "Time Safari Team"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build-start:pywebview": "vite build --config vite.config.pywebview.mts && .venv/bin/python src/pywebview/main.py",
|
"dev": "vite --config vite.config.dev.mts --host",
|
||||||
"build:android": "npm run clean:android && rm -rf dist && npm run build:web && npm run build:capacitor && cd android && ./gradlew clean && ./gradlew assembleDebug && cd .. && npx cap sync android && npx capacitor-assets generate --android && npx cap open android",
|
"serve": "vite preview",
|
||||||
"build:capacitor": "vite build --config vite.config.capacitor.mts",
|
"build": "VITE_GIT_HASH=`git log -1 --pretty=format:%h` vite build --config vite.config.mts",
|
||||||
"build:electron": "npm run clean:electron && tsc -p tsconfig.electron.json && vite build --config vite.config.electron.mts && node scripts/build-electron.js",
|
|
||||||
"build:electron-linux": "npm run build:electron && electron-builder --linux AppImage",
|
|
||||||
"build:electron-linux-deb": "npm run build:electron && electron-builder --linux deb",
|
|
||||||
"build:electron-linux-prod": "NODE_ENV=prod npm run build:electron && electron-builder --linux AppImage",
|
|
||||||
"build:electron-mac": "npm run build:electron-prod && electron-builder --mac",
|
|
||||||
"build:electron-mac-universal": "npm run build:electron-prod && electron-builder --mac --universal",
|
|
||||||
"build:electron-prod": "NODE_ENV=prod npm run build:electron",
|
|
||||||
"build:pywebview": "vite build --config vite.config.pywebview.mts",
|
|
||||||
"build:web": "VITE_GIT_HASH=`git log -1 --pretty=format:%h` vite build --config vite.config.web.mts",
|
|
||||||
"check:android-device": "adb devices | grep -w 'device' || (echo 'No Android device connected' && exit 1)",
|
|
||||||
"check:ios-device": "xcrun xctrace list devices 2>&1 | grep -w 'Booted' || (echo 'No iOS simulator running' && exit 1)",
|
|
||||||
"clean:android": "adb uninstall app.timesafari.app || true",
|
|
||||||
"clean:electron": "rimraf dist-electron",
|
|
||||||
"lint": "eslint --ext .js,.ts,.vue --ignore-path .gitignore src",
|
"lint": "eslint --ext .js,.ts,.vue --ignore-path .gitignore src",
|
||||||
"lint-fix": "eslint --ext .js,.ts,.vue --ignore-path .gitignore --fix src",
|
"lint-fix": "eslint --ext .js,.ts,.vue --ignore-path .gitignore --fix src",
|
||||||
"package:pywebview-linux": "vite build --config vite.config.pywebview.mts && .venv/bin/python -m PyInstaller --name TimeSafari --add-data 'dist:www' src/pywebview/main.py",
|
|
||||||
"package:pywebview-mac": "vite build --config vite.config.pywebview.mts && .venv/bin/python -m PyInstaller --name TimeSafari --add-data 'dist:www' src/pywebview/main.py",
|
|
||||||
"package:pywebview-win": "vite build --config vite.config.pywebview.mts && .venv/Scripts/python -m PyInstaller --name TimeSafari --add-data 'dist;www' src/pywebview/main.py",
|
|
||||||
"prebuild": "eslint --ext .js,.ts,.vue --ignore-path .gitignore src && node sw_combine.js && node scripts/copy-wasm.js",
|
"prebuild": "eslint --ext .js,.ts,.vue --ignore-path .gitignore src && node sw_combine.js && node scripts/copy-wasm.js",
|
||||||
"serve:web": "vite preview",
|
|
||||||
"start:electron": "electron .",
|
|
||||||
"start:web": "vite --config vite.config.web.mts --host",
|
|
||||||
"test:all": "npm run test:prerequisites && npm run build && npm run test:web && npm run test:mobile",
|
"test:all": "npm run test:prerequisites && npm run build && npm run test:web && npm run test:mobile",
|
||||||
|
"test:prerequisites": "node scripts/check-prerequisites.js",
|
||||||
|
"test:web": "npx playwright test -c playwright.config-local.ts --trace on",
|
||||||
|
"test:mobile": "npm run build:capacitor && npm run test:android && npm run test:ios",
|
||||||
"test:android": "node scripts/test-android.js",
|
"test:android": "node scripts/test-android.js",
|
||||||
"test:ios": "node scripts/test-ios.js",
|
"test:ios": "node scripts/test-ios.js",
|
||||||
"test:mobile": "npm run build:capacitor && npm run test:android && npm run test:ios",
|
"check:android-device": "adb devices | grep -w 'device' || (echo 'No Android device connected' && exit 1)",
|
||||||
"test:prerequisites": "node scripts/check-prerequisites.js",
|
"check:ios-device": "xcrun xctrace list devices 2>&1 | grep -w 'Booted' || (echo 'No iOS simulator running' && exit 1)",
|
||||||
"test:web": "npx playwright test -c playwright.config-local.ts --trace on"
|
"clean:electron": "rimraf dist-electron",
|
||||||
|
"build:pywebview": "vite build --config vite.config.pywebview.mts",
|
||||||
|
"build:electron": "npm run clean:electron && tsc -p tsconfig.electron.json && vite build --config vite.config.electron.mts && node scripts/build-electron.js",
|
||||||
|
"build:capacitor": "vite build --mode capacitor --config vite.config.capacitor.mts",
|
||||||
|
"build:web": "VITE_GIT_HASH=`git log -1 --pretty=format:%h` vite build --config vite.config.web.mts",
|
||||||
|
"electron:dev": "npm run build && electron .",
|
||||||
|
"electron:start": "electron .",
|
||||||
|
"clean:android": "adb uninstall app.timesafari.app || true",
|
||||||
|
"build:android": "npm run clean:android && rm -rf dist && npm run build:web && npm run build:capacitor && cd android && ./gradlew clean && ./gradlew assembleDebug && cd .. && npx cap sync android && npx capacitor-assets generate --android && npx cap open android",
|
||||||
|
"electron:build-linux": "npm run build:electron && electron-builder --linux AppImage",
|
||||||
|
"electron:build-linux-deb": "npm run build:electron && electron-builder --linux deb",
|
||||||
|
"electron:build-linux-prod": "NODE_ENV=production npm run build:electron && electron-builder --linux AppImage",
|
||||||
|
"build:electron-prod": "NODE_ENV=production npm run build:electron",
|
||||||
|
"pywebview:dev": "vite build --config vite.config.pywebview.mts && .venv/bin/python src/pywebview/main.py",
|
||||||
|
"pywebview:build": "vite build --config vite.config.pywebview.mts && .venv/bin/python src/pywebview/main.py",
|
||||||
|
"pywebview:package-linux": "vite build --mode pywebview --config vite.config.pywebview.mts && .venv/bin/python -m PyInstaller --name TimeSafari --add-data 'dist:www' src/pywebview/main.py",
|
||||||
|
"pywebview:package-win": "vite build --mode pywebview --config vite.config.pywebview.mts && .venv/Scripts/python -m PyInstaller --name TimeSafari --add-data 'dist;www' src/pywebview/main.py",
|
||||||
|
"pywebview:package-mac": "vite build --mode pywebview --config vite.config.pywebview.mts && .venv/bin/python -m PyInstaller --name TimeSafari --add-data 'dist:www' src/pywebview/main.py",
|
||||||
|
"fastlane:ios:beta": "cd ios && fastlane beta",
|
||||||
|
"fastlane:ios:release": "cd ios && fastlane release",
|
||||||
|
"fastlane:android:beta": "cd android && fastlane beta",
|
||||||
|
"fastlane:android:release": "cd android && fastlane release",
|
||||||
|
"electron:build-mac": "npm run build:electron-prod && electron-builder --mac",
|
||||||
|
"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",
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ export default defineConfig({
|
|||||||
/**
|
/**
|
||||||
* This could be an array of servers, meaning we could start the Endorser server as well:
|
* This could be an array of servers, meaning we could start the Endorser server as well:
|
||||||
* {
|
* {
|
||||||
* command: "cd ../endorser-ch; NODE_ENV=test-local npm run start:web",
|
* command: "cd ../endorser-ch; NODE_ENV=test-local npm run dev",
|
||||||
* url: 'http://localhost:3000',
|
* url: 'http://localhost:3000',
|
||||||
* reuseExistingServer: !process.env.CI,
|
* reuseExistingServer: !process.env.CI,
|
||||||
* },
|
* },
|
||||||
@@ -112,7 +112,7 @@ export default defineConfig({
|
|||||||
*/
|
*/
|
||||||
webServer: {
|
webServer: {
|
||||||
command:
|
command:
|
||||||
"NODE_ENV=dev VITE_APP_SERVER=http://localhost:8081 VITE_DEFAULT_ENDORSER_API_SERVER=http://localhost:3000 VITE_DEFAULT_PARTNER_API_SERVER=http://localhost:3000 VITE_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app VITE_PASSKEYS_ENABLED=true npm run start:web -- --port=8081",
|
"VITE_APP_SERVER=http://localhost:8081 VITE_DEFAULT_ENDORSER_API_SERVER=http://localhost:3000 VITE_DEFAULT_PARTNER_API_SERVER=http://localhost:3000 VITE_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app VITE_PASSKEYS_ENABLED=true npm run dev -- --port=8081",
|
||||||
url: "http://localhost:8081",
|
url: "http://localhost:8081",
|
||||||
reuseExistingServer: !process.env.CI,
|
reuseExistingServer: !process.env.CI,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ export default defineConfig({
|
|||||||
/* Run your local dev server before starting the tests */
|
/* Run your local dev server before starting the tests */
|
||||||
// webServer: {
|
// webServer: {
|
||||||
// command:
|
// command:
|
||||||
// "VITE_PASSKEYS_ENABLED=true VITE_DEFAULT_ENDORSER_API_SERVER=http://localhost:3000 npm run start:web",
|
// "VITE_PASSKEYS_ENABLED=true VITE_DEFAULT_ENDORSER_API_SERVER=http://localhost:3000 npm run dev",
|
||||||
// url: "http://localhost:8080",
|
// url: "http://localhost:8080",
|
||||||
// reuseExistingServer: !process.env.CI,
|
// reuseExistingServer: !process.env.CI,
|
||||||
// },
|
// },
|
||||||
|
|||||||
@@ -50,8 +50,8 @@ backup and database export, with platform-specific download instructions. * *
|
|||||||
v-if="platformCapabilities.isMobile && !platformCapabilities.isIOS"
|
v-if="platformCapabilities.isMobile && !platformCapabilities.isIOS"
|
||||||
class="list-disc list-outside ml-4"
|
class="list-disc list-outside ml-4"
|
||||||
>
|
>
|
||||||
On Android: You will be prompted to choose an app for sharing your
|
On Android: You will be prompted to choose a location to save your
|
||||||
backup file. To save on your phone, you will need a file manager app.
|
backup file.
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -60,9 +60,11 @@ 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 * as R from "ramda";
|
||||||
|
|
||||||
import { AppString, NotificationIface } from "../constants/app";
|
import { AppString, NotificationIface } from "../constants/app";
|
||||||
|
|
||||||
import { Contact } from "../db/tables/contacts";
|
import { Contact, ContactMaybeWithJsonStrings, ContactMethod } from "../db/tables/contacts";
|
||||||
import * as databaseUtil from "../db/databaseUtil";
|
import * as databaseUtil from "../db/databaseUtil";
|
||||||
|
|
||||||
import { logger } from "../utils/logger";
|
import { logger } from "../utils/logger";
|
||||||
@@ -72,6 +74,7 @@ import {
|
|||||||
PlatformCapabilities,
|
PlatformCapabilities,
|
||||||
} from "../services/PlatformService";
|
} from "../services/PlatformService";
|
||||||
import { contactsToExportJson } from "../libs/util";
|
import { contactsToExportJson } from "../libs/util";
|
||||||
|
import { parseJsonField } from "../db/databaseUtil";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @vue-component
|
* @vue-component
|
||||||
@@ -133,13 +136,13 @@ export default class DataExportSection extends Vue {
|
|||||||
*/
|
*/
|
||||||
public async exportDatabase() {
|
public async exportDatabase() {
|
||||||
try {
|
try {
|
||||||
let allContacts: Contact[] = [];
|
let allDbContacts: ContactMaybeWithJsonStrings[] = [];
|
||||||
const platformService = PlatformServiceFactory.getInstance();
|
const platformService = PlatformServiceFactory.getInstance();
|
||||||
const result = await platformService.dbQuery(`SELECT * FROM contacts`);
|
const result = await platformService.dbQuery(`SELECT * FROM contacts`);
|
||||||
if (result) {
|
if (result) {
|
||||||
allContacts = databaseUtil.mapQueryResultToValues(
|
allDbContacts = databaseUtil.mapQueryResultToValues(
|
||||||
result,
|
result,
|
||||||
) as unknown as Contact[];
|
) as unknown as ContactMaybeWithJsonStrings[];
|
||||||
}
|
}
|
||||||
// if (USE_DEXIE_DB) {
|
// if (USE_DEXIE_DB) {
|
||||||
// await db.open();
|
// await db.open();
|
||||||
@@ -147,6 +150,19 @@ export default class DataExportSection extends Vue {
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
// Convert contacts to export format
|
// Convert contacts to export format
|
||||||
|
const allContacts: Contact[] = allDbContacts.map((contact) => {
|
||||||
|
// first remove the contactMethods field, mostly to cast to a clear type (that will end up with JSON objects)
|
||||||
|
const exContact: Contact = R.omit(
|
||||||
|
["contactMethods"],
|
||||||
|
contact,
|
||||||
|
);
|
||||||
|
// now add contactMethods as a true array of ContactMethod objects
|
||||||
|
exContact.contactMethods = contact.contactMethods
|
||||||
|
? parseJsonField(contact.contactMethods, [] as Array<ContactMethod>)
|
||||||
|
: undefined;
|
||||||
|
return exContact;
|
||||||
|
});
|
||||||
|
|
||||||
const exportData = contactsToExportJson(allContacts);
|
const exportData = contactsToExportJson(allContacts);
|
||||||
const jsonStr = JSON.stringify(exportData, null, 2);
|
const jsonStr = JSON.stringify(exportData, null, 2);
|
||||||
const blob = new Blob([jsonStr], { type: "application/json" });
|
const blob = new Blob([jsonStr], { type: "application/json" });
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ export enum AppString {
|
|||||||
// This is used in titles and verbiage inside the app.
|
// This is used in titles and verbiage inside the app.
|
||||||
// There is also an app name without spaces, for packaging in the package.json file used in the manifest.
|
// There is also an app name without spaces, for packaging in the package.json file used in the manifest.
|
||||||
APP_NAME = "Time Safari",
|
APP_NAME = "Time Safari",
|
||||||
// iOS doesn't like spaces in the app title.
|
|
||||||
APP_NAME_NO_SPACES = "TimeSafari",
|
APP_NAME_NO_SPACES = "TimeSafari",
|
||||||
|
|
||||||
PROD_ENDORSER_API_SERVER = "https://api.endorser.ch",
|
PROD_ENDORSER_API_SERVER = "https://api.endorser.ch",
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ import { arrayBufferToBase64 } from "@/libs/crypto";
|
|||||||
|
|
||||||
const randomBytes = crypto.getRandomValues(new Uint8Array(32));
|
const randomBytes = crypto.getRandomValues(new Uint8Array(32));
|
||||||
const secretBase64 = arrayBufferToBase64(randomBytes);
|
const secretBase64 = arrayBufferToBase64(randomBytes);
|
||||||
console.log('secretBase64', secretBase64); // useful while we have multiple DBs activating (at least on web)
|
|
||||||
|
|
||||||
// Each migration can include multiple SQL statements (with semicolons)
|
// Each migration can include multiple SQL statements (with semicolons)
|
||||||
const MIGRATIONS = [
|
const MIGRATIONS = [
|
||||||
|
|||||||
@@ -30,6 +30,17 @@ export type ContactWithJsonStrings = Omit<Contact, "contactMethods"> & {
|
|||||||
contactMethods?: string;
|
contactMethods?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is for those cases (eg. with a DB) where field values may be all primitives or may be JSON values.
|
||||||
|
* See src/db/databaseUtil.ts parseJsonField for more details.
|
||||||
|
*
|
||||||
|
* This is so that we can reuse most of the type and don't have to maintain another copy.
|
||||||
|
* Another approach uses typescript conditionals: https://chatgpt.com/share/6855cdc3-ab5c-8007-8525-726612016eb2
|
||||||
|
*/
|
||||||
|
export type ContactMaybeWithJsonStrings = Omit<Contact, "contactMethods"> & {
|
||||||
|
contactMethods?: string | Array<ContactMethod>;
|
||||||
|
};
|
||||||
|
|
||||||
export const ContactSchema = {
|
export const ContactSchema = {
|
||||||
contacts: "&did, name", // no need to key by other things
|
contacts: "&did, name", // no need to key by other things
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
const { contextBridge, ipcRenderer } = require("electron");
|
const { contextBridge, ipcRenderer } = require("electron");
|
||||||
import { NodeEnv, BuildPlatform } from "@/interfaces/build";
|
|
||||||
|
|
||||||
const logger = {
|
const logger = {
|
||||||
log: (message, ...args) => {
|
log: (message, ...args) => {
|
||||||
// Always log in development, log with context in production
|
// Always log in development, log with context in production
|
||||||
if (process.env.NODE_ENV !== NodeEnv.Prod) {
|
if (process.env.NODE_ENV !== "production") {
|
||||||
/* eslint-disable no-console */
|
/* eslint-disable no-console */
|
||||||
console.log(`[Preload] ${message}`, ...args);
|
console.log(`[Preload] ${message}`, ...args);
|
||||||
/* eslint-enable no-console */
|
/* eslint-enable no-console */
|
||||||
@@ -24,7 +23,7 @@ const logger = {
|
|||||||
},
|
},
|
||||||
info: (message, ...args) => {
|
info: (message, ...args) => {
|
||||||
// Always log info in development, log with context in production
|
// Always log info in development, log with context in production
|
||||||
if (process.env.NODE_ENV !== NodeEnv.Prod) {
|
if (process.env.NODE_ENV !== "production") {
|
||||||
/* eslint-disable no-console */
|
/* eslint-disable no-console */
|
||||||
console.info(`[Preload] ${message}`, ...args);
|
console.info(`[Preload] ${message}`, ...args);
|
||||||
/* eslint-enable no-console */
|
/* eslint-enable no-console */
|
||||||
@@ -54,7 +53,7 @@ const getPath = (pathType) => {
|
|||||||
logger.info("Preload script starting...");
|
logger.info("Preload script starting...");
|
||||||
|
|
||||||
// Force electron platform in the renderer process
|
// Force electron platform in the renderer process
|
||||||
window.process = { env: { VITE_PLATFORM: BuildPlatform.Electron } };
|
window.process = { env: { VITE_PLATFORM: "electron" } };
|
||||||
|
|
||||||
try {
|
try {
|
||||||
contextBridge.exposeInMainWorld("electronAPI", {
|
contextBridge.exposeInMainWorld("electronAPI", {
|
||||||
@@ -77,12 +76,12 @@ try {
|
|||||||
// Environment info
|
// Environment info
|
||||||
env: {
|
env: {
|
||||||
isElectron: true,
|
isElectron: true,
|
||||||
isDev: process.env.NODE_ENV === NodeEnv.Dev,
|
isDev: process.env.NODE_ENV === "development",
|
||||||
platform: BuildPlatform.Electron, // Explicitly set platform
|
platform: "electron", // Explicitly set platform
|
||||||
},
|
},
|
||||||
// Path utilities
|
// Path utilities
|
||||||
getBasePath: () => {
|
getBasePath: () => {
|
||||||
return process.env.NODE_ENV === NodeEnv.Dev ? "/" : "./";
|
return process.env.NODE_ENV === "development" ? "/" : "./";
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
export const NodeEnv = {
|
|
||||||
Dev: "dev",
|
|
||||||
Test: "test",
|
|
||||||
Prod: "prod",
|
|
||||||
} as const;
|
|
||||||
export type NodeEnv = typeof NodeEnv[keyof typeof NodeEnv];
|
|
||||||
|
|
||||||
export const BuildEnv = {
|
|
||||||
Development: "development",
|
|
||||||
Testing: "testing",
|
|
||||||
Production: "production",
|
|
||||||
} as const;
|
|
||||||
export type BuildEnv = typeof BuildEnv[keyof typeof BuildEnv];
|
|
||||||
|
|
||||||
export const BuildPlatform = {
|
|
||||||
Web: "web",
|
|
||||||
Electron: "electron",
|
|
||||||
Capacitor: "capacitor",
|
|
||||||
PyWebView: "pywebview",
|
|
||||||
} as const;
|
|
||||||
export type BuildPlatform = typeof BuildPlatform[keyof typeof BuildPlatform];
|
|
||||||
@@ -27,20 +27,59 @@
|
|||||||
*/
|
*/
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
// Parameter validation schemas for each route type
|
||||||
|
export const deepLinkPathSchemas = {
|
||||||
|
claim: z.object({
|
||||||
|
id: z.string(),
|
||||||
|
}),
|
||||||
|
"claim-add-raw": z.object({
|
||||||
|
claim: z.string().optional(),
|
||||||
|
claimJwtId: z.string().optional(),
|
||||||
|
}),
|
||||||
|
"claim-cert": z.object({
|
||||||
|
id: z.string(),
|
||||||
|
}),
|
||||||
|
"confirm-gift": z.object({
|
||||||
|
id: z.string(),
|
||||||
|
}),
|
||||||
|
"contact-edit": z.object({
|
||||||
|
did: z.string(),
|
||||||
|
}),
|
||||||
|
"contact-import": z.object({
|
||||||
|
jwt: z.string(),
|
||||||
|
}),
|
||||||
|
contacts: z.object({
|
||||||
|
contactJwt: z.string().optional(),
|
||||||
|
inviteJwt: z.string().optional(),
|
||||||
|
}),
|
||||||
|
did: z.object({
|
||||||
|
did: z.string(),
|
||||||
|
}),
|
||||||
|
"invite-one-accept": z.object({
|
||||||
|
// optional because A) it could be a query param, and B) the page displays an input if things go wrong
|
||||||
|
jwt: z.string().optional(),
|
||||||
|
}),
|
||||||
|
"onboard-meeting-members": z.object({
|
||||||
|
groupId: z.string(),
|
||||||
|
}),
|
||||||
|
project: z.object({
|
||||||
|
id: z.string(),
|
||||||
|
}),
|
||||||
|
"user-profile": z.object({
|
||||||
|
id: z.string(),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deepLinkQuerySchemas = {
|
||||||
|
"onboard-meeting-members": z.object({
|
||||||
|
password: z.string(),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
// Add a union type of all valid route paths
|
// Add a union type of all valid route paths
|
||||||
export const VALID_DEEP_LINK_ROUTES = [
|
export const VALID_DEEP_LINK_ROUTES = Object.keys(
|
||||||
// note that similar lists are below in deepLinkSchemas and in src/services/deepLinks.ts
|
deepLinkPathSchemas,
|
||||||
"claim",
|
) as readonly (keyof typeof deepLinkPathSchemas)[];
|
||||||
"claim-add-raw",
|
|
||||||
"claim-cert",
|
|
||||||
"confirm-gift",
|
|
||||||
"contact-import",
|
|
||||||
"did",
|
|
||||||
"invite-one-accept",
|
|
||||||
"onboard-meeting-setup",
|
|
||||||
"project",
|
|
||||||
"user-profile",
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
// Create a type from the array
|
// Create a type from the array
|
||||||
export type DeepLinkRoute = (typeof VALID_DEEP_LINK_ROUTES)[number];
|
export type DeepLinkRoute = (typeof VALID_DEEP_LINK_ROUTES)[number];
|
||||||
@@ -52,51 +91,20 @@ export const baseUrlSchema = z.object({
|
|||||||
queryParams: z.record(z.string()).optional(),
|
queryParams: z.record(z.string()).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Use the type to ensure route validation
|
// export type DeepLinkPathParams = {
|
||||||
export const routeSchema = z.enum(VALID_DEEP_LINK_ROUTES);
|
// [K in keyof typeof deepLinkPathSchemas]: z.infer<(typeof deepLinkPathSchemas)[K]>;
|
||||||
|
// };
|
||||||
|
|
||||||
// Parameter validation schemas for each route type
|
// export type DeepLinkQueryParams = {
|
||||||
export const deepLinkSchemas = {
|
// [K in keyof typeof deepLinkQuerySchemas]: z.infer<(typeof deepLinkQuerySchemas)[K]>;
|
||||||
// note that similar lists are above in VALID_DEEP_LINK_ROUTES and in src/services/deepLinks.ts
|
// };
|
||||||
claim: z.object({
|
|
||||||
id: z.string(),
|
|
||||||
}),
|
|
||||||
"claim-add-raw": z.object({
|
|
||||||
id: z.string(),
|
|
||||||
claim: z.string().optional(),
|
|
||||||
claimJwtId: z.string().optional(),
|
|
||||||
}),
|
|
||||||
"claim-cert": z.object({
|
|
||||||
id: z.string(),
|
|
||||||
}),
|
|
||||||
"confirm-gift": z.object({
|
|
||||||
id: z.string(),
|
|
||||||
}),
|
|
||||||
"contact-import": z.object({
|
|
||||||
jwt: z.string(),
|
|
||||||
}),
|
|
||||||
did: z.object({
|
|
||||||
did: z.string(),
|
|
||||||
}),
|
|
||||||
"invite-one-accept": z.object({
|
|
||||||
jwt: z.string(),
|
|
||||||
}),
|
|
||||||
"onboard-meeting-setup": z.object({
|
|
||||||
id: z.string(),
|
|
||||||
}),
|
|
||||||
project: z.object({
|
|
||||||
id: z.string(),
|
|
||||||
}),
|
|
||||||
"user-profile": z.object({
|
|
||||||
id: z.string(),
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
export type DeepLinkParams = {
|
|
||||||
[K in keyof typeof deepLinkSchemas]: z.infer<(typeof deepLinkSchemas)[K]>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface DeepLinkError extends Error {
|
export interface DeepLinkError extends Error {
|
||||||
code: string;
|
code: string;
|
||||||
details?: unknown;
|
details?: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Use the type to ensure route validation
|
||||||
|
export const routeSchema = z.enum(
|
||||||
|
VALID_DEEP_LINK_ROUTES as [string, ...string[]],
|
||||||
|
);
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ import {
|
|||||||
KeyMetaMaybeWithPrivate,
|
KeyMetaMaybeWithPrivate,
|
||||||
} from "../interfaces/common";
|
} from "../interfaces/common";
|
||||||
import { PlanSummaryRecord } from "../interfaces/records";
|
import { PlanSummaryRecord } from "../interfaces/records";
|
||||||
import { logger, safeStringify } from "../utils/logger";
|
import { logger } from "../utils/logger";
|
||||||
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -214,13 +214,13 @@ const testRecursivelyOnStrings = (
|
|||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
export function containsHiddenDid(obj: any) {
|
export function containsHiddenDid(obj: any) {
|
||||||
return testRecursivelyOnStrings(isHiddenDid, obj);
|
return testRecursivelyOnStrings(obj, isHiddenDid);
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
export const containsNonHiddenDid = (obj: any) => {
|
export const containsNonHiddenDid = (obj: any) => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
return testRecursivelyOnStrings((s: any) => isDid(s) && !isHiddenDid(s), obj);
|
return testRecursivelyOnStrings(obj, (s: any) => isDid(s) && !isHiddenDid(s));
|
||||||
};
|
};
|
||||||
|
|
||||||
export function stripEndorserPrefix(claimId: string) {
|
export function stripEndorserPrefix(claimId: string) {
|
||||||
@@ -437,23 +437,19 @@ export async function getHeaders(
|
|||||||
}
|
}
|
||||||
headers["Authorization"] = "Bearer " + token;
|
headers["Authorization"] = "Bearer " + token;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
||||||
// This rarely happens: we've seen it when they have account info but the
|
// This rarely happens: we've seen it when they have account info but the
|
||||||
// encryption secret got lost.
|
// encryption secret got lost. But in most cases we want users to at
|
||||||
// Replicate this in Chrome: go to Storage and hit 'Clear site data'.
|
// least see their feed -- and anything else that returns results for
|
||||||
// Check the util.ts retrieveFullyDecryptedAccount method where it calls simpleDecrypt.
|
// anonymous users.
|
||||||
|
|
||||||
// In most cases we want users to at least see their feed -- and anything
|
// We'll continue with an anonymous request... still want to show feed and other things, but ideally let them know.
|
||||||
// else that returns results for anonymous users.
|
|
||||||
// We'll continue with an anonymous request... still want to show feed
|
|
||||||
// and other things, but we need to let them know.
|
|
||||||
logConsoleAndDb(
|
logConsoleAndDb(
|
||||||
"Something failed in getHeaders call (will proceed anonymously" +
|
"Something failed in getHeaders call (will proceed anonymously" +
|
||||||
($notify ? " and notify user" : "") +
|
($notify ? " and notify user" : "") +
|
||||||
"): " +
|
"): " +
|
||||||
// IntelliJ type system complains about getCircularReplacer() with: Argument of type '(obj: any, key: string, value: any) => any' is not assignable to parameter of type '(this: any, key: string, value: any) => any'.
|
// IntelliJ type system complains about getCircularReplacer() with: Argument of type '(obj: any, key: string, value: any) => any' is not assignable to parameter of type '(this: any, key: string, value: any) => any'.
|
||||||
//JSON.stringify(error, getCircularReplacer()), // JSON.stringify(error) on a Dexie error throws another error about: Converting circular structure to JSON
|
//JSON.stringify(error, getCircularReplacer()), // JSON.stringify(error) on a Dexie error throws another error about: Converting circular structure to JSON
|
||||||
error + " - " + safeStringify(error),
|
error,
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
if ($notify) {
|
if ($notify) {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
updateDefaultSettings,
|
updateDefaultSettings,
|
||||||
} from "../db/index";
|
} from "../db/index";
|
||||||
import { Account, AccountEncrypted } from "../db/tables/accounts";
|
import { Account, AccountEncrypted } from "../db/tables/accounts";
|
||||||
import { Contact, ContactWithJsonStrings } from "../db/tables/contacts";
|
import { Contact } from "../db/tables/contacts";
|
||||||
import * as databaseUtil from "../db/databaseUtil";
|
import * as databaseUtil from "../db/databaseUtil";
|
||||||
import { DEFAULT_PASSKEY_EXPIRATION_MINUTES } from "../db/tables/settings";
|
import { DEFAULT_PASSKEY_EXPIRATION_MINUTES } from "../db/tables/settings";
|
||||||
import {
|
import {
|
||||||
@@ -605,7 +605,7 @@ export const retrieveFullyDecryptedAccount = async (
|
|||||||
dbAccount.values.length === 0 ||
|
dbAccount.values.length === 0 ||
|
||||||
dbAccount.values[0].length === 0
|
dbAccount.values[0].length === 0
|
||||||
) {
|
) {
|
||||||
throw new Error("Account not found for did: " + activeDid);
|
throw new Error("Account not found.");
|
||||||
}
|
}
|
||||||
const fullAccountData = databaseUtil.mapQueryResultToValues(
|
const fullAccountData = databaseUtil.mapQueryResultToValues(
|
||||||
dbAccount,
|
dbAccount,
|
||||||
@@ -966,31 +966,19 @@ export interface DatabaseExport {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts an array of contacts to the standardized database export JSON format.
|
* Converts an array of contacts to the export JSON format.
|
||||||
* This format is used for data migration and backup purposes.
|
* This format is used for data migration and backup purposes.
|
||||||
*
|
*
|
||||||
* @param contacts - Array of Contact objects to convert
|
* @param contacts - Array of Contact objects to convert
|
||||||
* @returns DatabaseExport object in the standardized format
|
* @returns DatabaseExport object in the standardized format
|
||||||
*/
|
*/
|
||||||
export const contactsToExportJson = (contacts: Contact[]): DatabaseExport => {
|
export const contactsToExportJson = (contacts: Contact[]): DatabaseExport => {
|
||||||
// Convert each contact to a plain object and ensure all fields are included
|
|
||||||
const rows = contacts.map((contact) => {
|
|
||||||
const exContact: ContactWithJsonStrings = R.omit(
|
|
||||||
["contactMethods"],
|
|
||||||
contact,
|
|
||||||
);
|
|
||||||
exContact.contactMethods = contact.contactMethods
|
|
||||||
? JSON.stringify(contact.contactMethods, [])
|
|
||||||
: undefined;
|
|
||||||
return exContact;
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data: {
|
data: {
|
||||||
data: [
|
data: [
|
||||||
{
|
{
|
||||||
tableName: "contacts",
|
tableName: "contacts",
|
||||||
rows,
|
rows: contacts,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -72,12 +72,12 @@ const handleDeepLink = async (data: { url: string }) => {
|
|||||||
await deepLinkHandler.handleDeepLink(data.url);
|
await deepLinkHandler.handleDeepLink(data.url);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("[DeepLink] Error handling deep link: ", error);
|
logger.error("[DeepLink] Error handling deep link: ", error);
|
||||||
handleApiError(
|
let message: string =
|
||||||
{
|
error instanceof Error ? error.message : safeStringify(error);
|
||||||
message: error instanceof Error ? error.message : safeStringify(error),
|
if (data.url) {
|
||||||
} as AxiosError,
|
message += `\nURL: ${data.url}`;
|
||||||
"deep-link",
|
}
|
||||||
);
|
handleApiError({ message } as AxiosError, "deep-link");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
import { initBackend } from "absurd-sql/dist/indexeddb-main-thread";
|
import { initBackend } from "absurd-sql/dist/indexeddb-main-thread";
|
||||||
|
|
||||||
import { initializeApp } from "./main.common";
|
import { initializeApp } from "./main.common";
|
||||||
import { logger } from "./utils/logger";
|
import { logger } from "./utils/logger";
|
||||||
import { BuildPlatform } from "@/interfaces/build";
|
|
||||||
|
|
||||||
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";
|
||||||
|
|
||||||
// Only import service worker for web builds
|
// Only import service worker for web builds
|
||||||
if (platform !== BuildPlatform.Electron && pwa_enabled) {
|
if (platform !== "electron" && pwa_enabled) {
|
||||||
import("./registerServiceWorker"); // Web PWA support
|
import("./registerServiceWorker"); // Web PWA support
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,7 +25,7 @@ function sqlInit() {
|
|||||||
// workers through the main thread
|
// workers through the main thread
|
||||||
initBackend(worker);
|
initBackend(worker);
|
||||||
}
|
}
|
||||||
if (platform === BuildPlatform.Web) {
|
if (platform === "web" || platform === "development") {
|
||||||
sqlInit();
|
sqlInit();
|
||||||
} else {
|
} else {
|
||||||
logger.warn("[Web] SQL not initialized for platform", { platform });
|
logger.warn("[Web] SQL not initialized for platform", { platform });
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
/* eslint-disable no-console */
|
/* eslint-disable no-console */
|
||||||
|
|
||||||
import { register } from "register-service-worker";
|
import { register } from "register-service-worker";
|
||||||
import { NodeEnv, BuildPlatform } from "@/interfaces/build";
|
|
||||||
|
|
||||||
// Check if we're in an Electron environment
|
// Check if we're in an Electron environment
|
||||||
const isElectron =
|
const isElectron =
|
||||||
process.env.VITE_PLATFORM === BuildPlatform.Electron ||
|
process.env.VITE_PLATFORM === "electron" ||
|
||||||
process.env.VITE_DISABLE_PWA === "true" ||
|
process.env.VITE_DISABLE_PWA === "true" ||
|
||||||
window.navigator.userAgent.toLowerCase().includes("electron");
|
window.navigator.userAgent.toLowerCase().includes("electron");
|
||||||
|
|
||||||
@@ -16,7 +15,7 @@ const isElectron =
|
|||||||
if (
|
if (
|
||||||
!isElectron &&
|
!isElectron &&
|
||||||
process.env.VITE_PWA_ENABLED === "true" &&
|
process.env.VITE_PWA_ENABLED === "true" &&
|
||||||
process.env.NODE_ENV === NodeEnv.Prod
|
process.env.NODE_ENV === "production"
|
||||||
) {
|
) {
|
||||||
register(`${process.env.BASE_URL}sw.js`, {
|
register(`${process.env.BASE_URL}sw.js`, {
|
||||||
ready() {
|
ready() {
|
||||||
|
|||||||
@@ -73,6 +73,11 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
name: "contacts",
|
name: "contacts",
|
||||||
component: () => import("../views/ContactsView.vue"),
|
component: () => import("../views/ContactsView.vue"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/database-migration",
|
||||||
|
name: "database-migration",
|
||||||
|
component: () => import("../views/DatabaseMigration.vue"),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/did/:did?",
|
path: "/did/:did?",
|
||||||
name: "did",
|
name: "did",
|
||||||
@@ -139,8 +144,9 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
component: () => import("../views/InviteOneView.vue"),
|
component: () => import("../views/InviteOneView.vue"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
// optional because A) it could be a query param, and B) the page displays an input if things go wrong
|
||||||
path: "/invite-one-accept/:jwt?",
|
path: "/invite-one-accept/:jwt?",
|
||||||
name: "InviteOneAcceptView",
|
name: "invite-one-accept",
|
||||||
component: () => import("../views/InviteOneAcceptView.vue"),
|
component: () => import("../views/InviteOneAcceptView.vue"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -148,11 +154,6 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
name: "logs",
|
name: "logs",
|
||||||
component: () => import("../views/LogView.vue"),
|
component: () => import("../views/LogView.vue"),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: "/database-migration",
|
|
||||||
name: "database-migration",
|
|
||||||
component: () => import("../views/DatabaseMigration.vue"),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: "/new-activity",
|
path: "/new-activity",
|
||||||
name: "new-activity",
|
name: "new-activity",
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { WebPlatformService } from "./platforms/WebPlatformService";
|
|||||||
import { CapacitorPlatformService } from "./platforms/CapacitorPlatformService";
|
import { CapacitorPlatformService } from "./platforms/CapacitorPlatformService";
|
||||||
import { ElectronPlatformService } from "./platforms/ElectronPlatformService";
|
import { ElectronPlatformService } from "./platforms/ElectronPlatformService";
|
||||||
import { PyWebViewPlatformService } from "./platforms/PyWebViewPlatformService";
|
import { PyWebViewPlatformService } from "./platforms/PyWebViewPlatformService";
|
||||||
import { BuildPlatform } from "@/interfaces/build";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Factory class for creating platform-specific service implementations.
|
* Factory class for creating platform-specific service implementations.
|
||||||
@@ -36,19 +35,19 @@ export class PlatformServiceFactory {
|
|||||||
return PlatformServiceFactory.instance;
|
return PlatformServiceFactory.instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
const platform = process.env.VITE_PLATFORM || BuildPlatform.Web;
|
const platform = process.env.VITE_PLATFORM || "web";
|
||||||
|
|
||||||
switch (platform) {
|
switch (platform) {
|
||||||
case BuildPlatform.Capacitor:
|
case "capacitor":
|
||||||
PlatformServiceFactory.instance = new CapacitorPlatformService();
|
PlatformServiceFactory.instance = new CapacitorPlatformService();
|
||||||
break;
|
break;
|
||||||
case BuildPlatform.Electron:
|
case "electron":
|
||||||
PlatformServiceFactory.instance = new ElectronPlatformService();
|
PlatformServiceFactory.instance = new ElectronPlatformService();
|
||||||
break;
|
break;
|
||||||
case BuildPlatform.PyWebView:
|
case "pywebview":
|
||||||
PlatformServiceFactory.instance = new PyWebViewPlatformService();
|
PlatformServiceFactory.instance = new PyWebViewPlatformService();
|
||||||
break;
|
break;
|
||||||
case BuildPlatform.Web:
|
case "web":
|
||||||
default:
|
default:
|
||||||
PlatformServiceFactory.instance = new WebPlatformService();
|
PlatformServiceFactory.instance = new WebPlatformService();
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -7,7 +7,6 @@
|
|||||||
|
|
||||||
import { AxiosError } from "axios";
|
import { AxiosError } from "axios";
|
||||||
import { logger, safeStringify } from "../utils/logger";
|
import { logger, safeStringify } from "../utils/logger";
|
||||||
import { BuildPlatform } from "@/interfaces/build";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles API errors with platform-specific logging and error processing.
|
* Handles API errors with platform-specific logging and error processing.
|
||||||
@@ -37,7 +36,7 @@ import { BuildPlatform } from "@/interfaces/build";
|
|||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export const handleApiError = (error: AxiosError, endpoint: string) => {
|
export const handleApiError = (error: AxiosError, endpoint: string) => {
|
||||||
if (process.env.VITE_PLATFORM === BuildPlatform.Capacitor) {
|
if (process.env.VITE_PLATFORM === "capacitor") {
|
||||||
const endpointStr = safeStringify(endpoint); // we've seen this as an object in deep links
|
const endpointStr = safeStringify(endpoint); // we've seen this as an object in deep links
|
||||||
logger.error(`[Capacitor API Error] ${endpointStr}:`, {
|
logger.error(`[Capacitor API Error] ${endpointStr}:`, {
|
||||||
message: error.message,
|
message: error.message,
|
||||||
|
|||||||
@@ -44,15 +44,50 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Router } from "vue-router";
|
import { Router } from "vue-router";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
deepLinkSchemas,
|
deepLinkPathSchemas,
|
||||||
baseUrlSchema,
|
baseUrlSchema,
|
||||||
routeSchema,
|
routeSchema,
|
||||||
DeepLinkRoute,
|
DeepLinkRoute,
|
||||||
|
deepLinkQuerySchemas,
|
||||||
} from "../interfaces/deepLinks";
|
} from "../interfaces/deepLinks";
|
||||||
import { logConsoleAndDb } from "../db/databaseUtil";
|
import { logConsoleAndDb } from "../db/databaseUtil";
|
||||||
import type { DeepLinkError } from "../interfaces/deepLinks";
|
import type { DeepLinkError } from "../interfaces/deepLinks";
|
||||||
|
|
||||||
|
// Helper function to extract the first key from a Zod object schema
|
||||||
|
function getFirstKeyFromZodObject(
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
schema: z.ZodObject<any>,
|
||||||
|
): string | undefined {
|
||||||
|
const shape = schema.shape;
|
||||||
|
const keys = Object.keys(shape);
|
||||||
|
return keys.length > 0 ? keys[0] : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps deep link routes to their corresponding Vue router names and optional parameter keys.
|
||||||
|
*
|
||||||
|
* It's an object where keys are the deep link routes and values are objects with 'name' and 'paramKey'.
|
||||||
|
*
|
||||||
|
* The paramKey is used to extract the parameter from the route path,
|
||||||
|
* because "router.replace" expects the right parameter name for the route.
|
||||||
|
*/
|
||||||
|
export const ROUTE_MAP: Record<string, { name: string; paramKey?: string }> =
|
||||||
|
Object.entries(deepLinkPathSchemas).reduce(
|
||||||
|
(acc, [routeName, schema]) => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const paramKey = getFirstKeyFromZodObject(schema as z.ZodObject<any>);
|
||||||
|
acc[routeName] = {
|
||||||
|
name: routeName,
|
||||||
|
paramKey,
|
||||||
|
};
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<string, { name: string; paramKey?: string }>,
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles processing and routing of deep links in the application.
|
* Handles processing and routing of deep links in the application.
|
||||||
* Provides validation, error handling, and routing for deep link URLs.
|
* Provides validation, error handling, and routing for deep link URLs.
|
||||||
@@ -69,30 +104,7 @@ export class DeepLinkHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Maps deep link routes to their corresponding Vue router names and optional parameter keys.
|
|
||||||
*
|
|
||||||
* The paramKey is used to extract the parameter from the route path,
|
|
||||||
* because "router.replace" expects the right parameter name for the route.
|
|
||||||
* The default is "id".
|
|
||||||
*/
|
|
||||||
private readonly ROUTE_MAP: Record<
|
|
||||||
string,
|
|
||||||
{ name: string; paramKey?: string }
|
|
||||||
> = {
|
|
||||||
// note that similar lists are in src/interfaces/deepLinks.ts
|
|
||||||
claim: { name: "claim" },
|
|
||||||
"claim-add-raw": { name: "claim-add-raw" },
|
|
||||||
"claim-cert": { name: "claim-cert" },
|
|
||||||
"confirm-gift": { name: "confirm-gift" },
|
|
||||||
"contact-import": { name: "contact-import", paramKey: "jwt" },
|
|
||||||
did: { name: "did", paramKey: "did" },
|
|
||||||
"invite-one-accept": { name: "invite-one-accept", paramKey: "jwt" },
|
|
||||||
"onboard-meeting-members": { name: "onboard-meeting-members" },
|
|
||||||
project: { name: "project" },
|
|
||||||
"user-profile": { name: "user-profile" },
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses deep link URL into path, params and query components.
|
* Parses deep link URL into path, params and query components.
|
||||||
* Validates URL structure using Zod schemas.
|
* Validates URL structure using Zod schemas.
|
||||||
*
|
*
|
||||||
@@ -115,18 +127,9 @@ export class DeepLinkHandler {
|
|||||||
|
|
||||||
const [path, queryString] = parts[1].split("?");
|
const [path, queryString] = parts[1].split("?");
|
||||||
const [routePath, ...pathParams] = path.split("/");
|
const [routePath, ...pathParams] = path.split("/");
|
||||||
// logger.info(
|
|
||||||
// "[DeepLink] Debug:",
|
|
||||||
// "Route Path:",
|
|
||||||
// routePath,
|
|
||||||
// "Path Params:",
|
|
||||||
// pathParams,
|
|
||||||
// "Query String:",
|
|
||||||
// queryString,
|
|
||||||
// );
|
|
||||||
|
|
||||||
// Validate route exists before proceeding
|
// Validate route exists before proceeding
|
||||||
if (!this.ROUTE_MAP[routePath]) {
|
if (!ROUTE_MAP[routePath]) {
|
||||||
throw {
|
throw {
|
||||||
code: "INVALID_ROUTE",
|
code: "INVALID_ROUTE",
|
||||||
message: `Invalid route path: ${routePath}`,
|
message: `Invalid route path: ${routePath}`,
|
||||||
@@ -144,9 +147,14 @@ export class DeepLinkHandler {
|
|||||||
const params: Record<string, string> = {};
|
const params: Record<string, string> = {};
|
||||||
if (pathParams) {
|
if (pathParams) {
|
||||||
// Now we know routePath exists in ROUTE_MAP
|
// Now we know routePath exists in ROUTE_MAP
|
||||||
const routeConfig = this.ROUTE_MAP[routePath];
|
const routeConfig = ROUTE_MAP[routePath];
|
||||||
params[routeConfig.paramKey ?? "id"] = pathParams.join("/");
|
params[routeConfig.paramKey ?? "id"] = pathParams.join("/");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// logConsoleAndDb(
|
||||||
|
// `[DeepLink] Debug: Route Path: ${routePath} Path Params: ${JSON.stringify(params)} Query String: ${JSON.stringify(query)}`,
|
||||||
|
// false,
|
||||||
|
// );
|
||||||
return { path: routePath, params, query };
|
return { path: routePath, params, query };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,7 +178,7 @@ export class DeepLinkHandler {
|
|||||||
try {
|
try {
|
||||||
// Validate route exists
|
// Validate route exists
|
||||||
const validRoute = routeSchema.parse(path) as DeepLinkRoute;
|
const validRoute = routeSchema.parse(path) as DeepLinkRoute;
|
||||||
routeName = this.ROUTE_MAP[validRoute].name;
|
routeName = ROUTE_MAP[validRoute].name;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Log the invalid route attempt
|
// Log the invalid route attempt
|
||||||
logConsoleAndDb(`[DeepLink] Invalid route path: ${path}`, true);
|
logConsoleAndDb(`[DeepLink] Invalid route path: ${path}`, true);
|
||||||
@@ -178,51 +186,73 @@ export class DeepLinkHandler {
|
|||||||
// Redirect to error page with information about the invalid link
|
// Redirect to error page with information about the invalid link
|
||||||
await this.router.replace({
|
await this.router.replace({
|
||||||
name: "deep-link-error",
|
name: "deep-link-error",
|
||||||
|
params,
|
||||||
query: {
|
query: {
|
||||||
originalPath: path,
|
originalPath: path,
|
||||||
errorCode: "INVALID_ROUTE",
|
errorCode: "INVALID_ROUTE",
|
||||||
message: `The link you followed (${path}) is not supported`,
|
errorMessage: `The link you followed (${path}) is not supported`,
|
||||||
|
...query,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
throw {
|
// This previously threw an error but we're redirecting so there's no need.
|
||||||
code: "INVALID_ROUTE",
|
return;
|
||||||
message: `Unsupported route: ${path}`,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Continue with parameter validation as before...
|
// Continue with parameter validation as before...
|
||||||
const schema = deepLinkSchemas[path as keyof typeof deepLinkSchemas];
|
const pathSchema = deepLinkPathSchemas[path as keyof typeof deepLinkPathSchemas];
|
||||||
|
const querySchema = deepLinkQuerySchemas[path as keyof typeof deepLinkQuerySchemas];
|
||||||
|
|
||||||
|
let validatedPathParams: Record<string, string> = {};
|
||||||
|
let validatedQueryParams: Record<string, string> = {};
|
||||||
try {
|
try {
|
||||||
const validatedParams = await schema.parseAsync({
|
if (pathSchema) {
|
||||||
...params,
|
validatedPathParams = await pathSchema.parseAsync(params);
|
||||||
...query,
|
}
|
||||||
});
|
if (querySchema) {
|
||||||
|
validatedQueryParams = await querySchema.parseAsync(query);
|
||||||
await this.router.replace({
|
}
|
||||||
name: routeName,
|
|
||||||
params: validatedParams,
|
|
||||||
query,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// For parameter validation errors, provide specific error feedback
|
// For parameter validation errors, provide specific error feedback
|
||||||
|
logConsoleAndDb(
|
||||||
|
`[DeepLink] Invalid parameters for route name ${routeName} for path: ${path} ... with error: ${JSON.stringify(error)} ... with params: ${JSON.stringify(params)} ... and query: ${JSON.stringify(query)}`,
|
||||||
|
);
|
||||||
await this.router.replace({
|
await this.router.replace({
|
||||||
name: "deep-link-error",
|
name: "deep-link-error",
|
||||||
|
params,
|
||||||
query: {
|
query: {
|
||||||
originalPath: path,
|
originalPath: path,
|
||||||
errorCode: "INVALID_PARAMETERS",
|
errorCode: "INVALID_PARAMETERS",
|
||||||
message: `The link parameters are invalid: ${(error as Error).message}`,
|
errorMessage: `The link parameters are invalid: ${(error as Error).message}`,
|
||||||
|
...query,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
throw {
|
// This previously threw an error but we're redirecting so there's no need.
|
||||||
code: "INVALID_PARAMETERS",
|
return;
|
||||||
message: (error as Error).message,
|
}
|
||||||
details: error,
|
|
||||||
params: params,
|
try {
|
||||||
query: query,
|
await this.router.replace({
|
||||||
};
|
name: routeName,
|
||||||
|
params: validatedPathParams,
|
||||||
|
query: validatedQueryParams
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logConsoleAndDb(
|
||||||
|
`[DeepLink] Error routing to route name ${routeName} for path: ${path}: ${JSON.stringify(error)} ... with validated params: ${JSON.stringify(validatedPathParams)} ... and query: ${JSON.stringify(validatedQueryParams)}`,
|
||||||
|
);
|
||||||
|
// For parameter validation errors, provide specific error feedback
|
||||||
|
await this.router.replace({
|
||||||
|
name: "deep-link-error",
|
||||||
|
params: validatedPathParams,
|
||||||
|
query: {
|
||||||
|
originalPath: path,
|
||||||
|
errorCode: "ROUTING_ERROR",
|
||||||
|
errorMessage: `Error routing to ${routeName}: ${JSON.stringify(error)}`,
|
||||||
|
...validatedQueryParams,
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,7 +265,6 @@ export class DeepLinkHandler {
|
|||||||
*/
|
*/
|
||||||
async handleDeepLink(url: string): Promise<void> {
|
async handleDeepLink(url: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
logConsoleAndDb("[DeepLink] Processing URL: " + url, false);
|
|
||||||
const { path, params, query } = this.parseDeepLink(url);
|
const { path, params, query } = this.parseDeepLink(url);
|
||||||
// Ensure params is always a Record<string,string> by converting undefined to empty string
|
// Ensure params is always a Record<string,string> by converting undefined to empty string
|
||||||
const sanitizedParams = Object.fromEntries(
|
const sanitizedParams = Object.fromEntries(
|
||||||
@@ -245,7 +274,7 @@ export class DeepLinkHandler {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
const deepLinkError = error as DeepLinkError;
|
const deepLinkError = error as DeepLinkError;
|
||||||
logConsoleAndDb(
|
logConsoleAndDb(
|
||||||
`[DeepLink] Error (${deepLinkError.code}): ${deepLinkError.message}`,
|
`[DeepLink] Error (${deepLinkError.code}): ${deepLinkError.details}`,
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { logToDb } from "../db/databaseUtil";
|
import { logToDb } from "../db/databaseUtil";
|
||||||
import { BuildPlatform, NodeEnv } from "@/interfaces/build";
|
|
||||||
|
|
||||||
export function safeStringify(obj: unknown) {
|
export function safeStringify(obj: unknown) {
|
||||||
const seen = new WeakSet();
|
const seen = new WeakSet();
|
||||||
@@ -22,7 +21,7 @@ export function safeStringify(obj: unknown) {
|
|||||||
|
|
||||||
export const logger = {
|
export const logger = {
|
||||||
debug: (message: string, ...args: unknown[]) => {
|
debug: (message: string, ...args: unknown[]) => {
|
||||||
if (process.env.NODE_ENV !== NodeEnv.Prod) {
|
if (process.env.NODE_ENV !== "production") {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.debug(message, ...args);
|
console.debug(message, ...args);
|
||||||
// const argsString = args.length > 0 ? " - " + safeStringify(args) : "";
|
// const argsString = args.length > 0 ? " - " + safeStringify(args) : "";
|
||||||
@@ -31,8 +30,8 @@ export const logger = {
|
|||||||
},
|
},
|
||||||
log: (message: string, ...args: unknown[]) => {
|
log: (message: string, ...args: unknown[]) => {
|
||||||
if (
|
if (
|
||||||
process.env.NODE_ENV !== NodeEnv.Prod ||
|
process.env.NODE_ENV !== "production" ||
|
||||||
process.env.VITE_PLATFORM === BuildPlatform.Capacitor
|
process.env.VITE_PLATFORM === "capacitor"
|
||||||
) {
|
) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.log(message, ...args);
|
console.log(message, ...args);
|
||||||
@@ -42,9 +41,9 @@ export const logger = {
|
|||||||
},
|
},
|
||||||
info: (message: string, ...args: unknown[]) => {
|
info: (message: string, ...args: unknown[]) => {
|
||||||
if (
|
if (
|
||||||
process.env.NODE_ENV !== NodeEnv.Prod ||
|
process.env.NODE_ENV !== "production" ||
|
||||||
process.env.VITE_PLATFORM === BuildPlatform.Capacitor ||
|
process.env.VITE_PLATFORM === "capacitor" ||
|
||||||
process.env.VITE_PLATFORM === BuildPlatform.Electron
|
process.env.VITE_PLATFORM === "electron"
|
||||||
) {
|
) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.info(message, ...args);
|
console.info(message, ...args);
|
||||||
|
|||||||
@@ -126,7 +126,7 @@
|
|||||||
<div class="flex justify-center text-center text-sm leading-tight mb-1">
|
<div class="flex justify-center text-center text-sm leading-tight mb-1">
|
||||||
People {{ profileImageUrl ? "without your image" : "" }} see this
|
People {{ profileImageUrl ? "without your image" : "" }} see this
|
||||||
<br />
|
<br />
|
||||||
(if you've let them see which posts are yours):
|
(if you've let them see your activity):
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
<EntityIcon
|
<EntityIcon
|
||||||
@@ -1154,6 +1154,7 @@ export default class AccountViewView extends Vue {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.status === 404) {
|
if (error.status === 404) {
|
||||||
// this is ok: the profile is not yet created
|
// this is ok: the profile is not yet created
|
||||||
|
logger.info("Note that axios may have logged an error but it just doesn't exist.");
|
||||||
} else {
|
} else {
|
||||||
databaseUtil.logConsoleAndDb(
|
databaseUtil.logConsoleAndDb(
|
||||||
"Error loading profile: " + errorStringForLog(error),
|
"Error loading profile: " + errorStringForLog(error),
|
||||||
@@ -1573,25 +1574,24 @@ export default class AccountViewView extends Vue {
|
|||||||
* @throws Will notify the user if there is an export error.
|
* @throws Will notify the user if there is an export error.
|
||||||
*/
|
*/
|
||||||
public async exportDatabase() {
|
public async exportDatabase() {
|
||||||
throw new Error("Not implemented");
|
try {
|
||||||
// try {
|
// Generate the blob from the database
|
||||||
// // Generate the blob from the database
|
const blob = await this.generateDatabaseBlob();
|
||||||
// const blob = await this.generateDatabaseBlob();
|
|
||||||
|
|
||||||
// // Create a temporary URL for the blob
|
// Create a temporary URL for the blob
|
||||||
// this.downloadUrl = this.createBlobURL(blob);
|
this.downloadUrl = this.createBlobURL(blob);
|
||||||
|
|
||||||
// // Trigger the download
|
// Trigger the download
|
||||||
// this.downloadDatabaseBackup(this.downloadUrl);
|
this.downloadDatabaseBackup(this.downloadUrl);
|
||||||
|
|
||||||
// // Notify the user that the download has started
|
// Notify the user that the download has started
|
||||||
// this.notifyDownloadStarted();
|
this.notifyDownloadStarted();
|
||||||
|
|
||||||
// // Revoke the temporary URL -- after a pause to avoid DuckDuckGo download failure
|
// Revoke the temporary URL -- after a pause to avoid DuckDuckGo download failure
|
||||||
// setTimeout(() => URL.revokeObjectURL(this.downloadUrl), 1000);
|
setTimeout(() => URL.revokeObjectURL(this.downloadUrl), 1000);
|
||||||
// } catch (error) {
|
} catch (error) {
|
||||||
// this.handleExportError(error);
|
this.handleExportError(error);
|
||||||
// }
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -30,7 +30,6 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Vue } from "vue-facing-decorator";
|
import { Component, Vue } from "vue-facing-decorator";
|
||||||
import { AxiosInstance } from "axios";
|
|
||||||
|
|
||||||
import QuickNav from "../components/QuickNav.vue";
|
import QuickNav from "../components/QuickNav.vue";
|
||||||
import { NotificationIface, USE_DEXIE_DB } from "../constants/app";
|
import { NotificationIface, USE_DEXIE_DB } from "../constants/app";
|
||||||
@@ -54,7 +53,6 @@ export default class ClaimAddRawView extends Vue {
|
|||||||
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
$notify!: (notification: NotificationIface, timeout?: number) => void;
|
||||||
$route!: RouteLocationNormalizedLoaded;
|
$route!: RouteLocationNormalizedLoaded;
|
||||||
$router!: Router;
|
$router!: Router;
|
||||||
axios!: AxiosInstance;
|
|
||||||
|
|
||||||
accountIdentityStr: string = "null";
|
accountIdentityStr: string = "null";
|
||||||
activeDid = "";
|
activeDid = "";
|
||||||
|
|||||||
@@ -31,7 +31,11 @@
|
|||||||
<h2>Supported Deep Links</h2>
|
<h2>Supported Deep Links</h2>
|
||||||
<ul>
|
<ul>
|
||||||
<li v-for="(routeItem, index) in validRoutes" :key="index">
|
<li v-for="(routeItem, index) in validRoutes" :key="index">
|
||||||
<code>timesafari://{{ routeItem }}/:id</code>
|
<code
|
||||||
|
>timesafari://{{ routeItem }}/:{{
|
||||||
|
deepLinkSchemaKeys[routeItem]
|
||||||
|
}}</code
|
||||||
|
>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -41,12 +45,22 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted } from "vue";
|
import { computed, onMounted } from "vue";
|
||||||
import { useRoute, useRouter } from "vue-router";
|
import { useRoute, useRouter } from "vue-router";
|
||||||
import { VALID_DEEP_LINK_ROUTES } from "../interfaces/deepLinks";
|
import {
|
||||||
|
VALID_DEEP_LINK_ROUTES,
|
||||||
|
deepLinkPathSchemas,
|
||||||
|
} from "../interfaces/deepLinks";
|
||||||
import { logConsoleAndDb } from "../db/databaseUtil";
|
import { logConsoleAndDb } from "../db/databaseUtil";
|
||||||
import { logger } from "../utils/logger";
|
import { logger } from "../utils/logger";
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
// an object with the route as the key and the first param name as the value
|
||||||
|
const deepLinkSchemaKeys = Object.fromEntries(
|
||||||
|
Object.entries(deepLinkPathSchemas).map(([route, schema]) => {
|
||||||
|
const param = Object.keys(schema.shape)[0];
|
||||||
|
return [route, param];
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
// Extract error information from query params
|
// Extract error information from query params
|
||||||
const errorCode = computed(
|
const errorCode = computed(
|
||||||
@@ -54,7 +68,7 @@ const errorCode = computed(
|
|||||||
);
|
);
|
||||||
const errorMessage = computed(
|
const errorMessage = computed(
|
||||||
() =>
|
() =>
|
||||||
(route.query.message as string) ||
|
(route.query.errorMessage as string) ||
|
||||||
"The deep link you followed is invalid or not supported.",
|
"The deep link you followed is invalid or not supported.",
|
||||||
);
|
);
|
||||||
const originalPath = computed(() => route.query.originalPath as string);
|
const originalPath = computed(() => route.query.originalPath as string);
|
||||||
@@ -93,7 +107,7 @@ const reportIssue = () => {
|
|||||||
// Log the error for analytics
|
// Log the error for analytics
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
logConsoleAndDb(
|
logConsoleAndDb(
|
||||||
`[DeepLink] Error page displayed for path: ${originalPath.value}, code: ${errorCode.value}, params: ${JSON.stringify(route.params)}`,
|
`[DeepLinkError] Error page displayed for path: ${originalPath.value}, code: ${errorCode.value}, params: ${JSON.stringify(route.params)}, query: ${JSON.stringify(route.query)}`,
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -100,7 +100,6 @@ import { Component, Vue } from "vue-facing-decorator";
|
|||||||
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
|
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
|
||||||
|
|
||||||
import { APP_SERVER } from "@/constants/app";
|
import { APP_SERVER } from "@/constants/app";
|
||||||
import { NodeEnv } from "@/interfaces/build";
|
|
||||||
import { logger } from "@/utils/logger";
|
import { logger } from "@/utils/logger";
|
||||||
import { errorStringForLog } from "@/libs/endorserServer";
|
import { errorStringForLog } from "@/libs/endorserServer";
|
||||||
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
||||||
@@ -149,7 +148,7 @@ export default class DeepLinkRedirectView extends Vue {
|
|||||||
this.deepLinkUrl = `timesafari://${fullPathWithQuery}`;
|
this.deepLinkUrl = `timesafari://${fullPathWithQuery}`;
|
||||||
this.webUrl = `${APP_SERVER}/${fullPathWithQuery}`;
|
this.webUrl = `${APP_SERVER}/${fullPathWithQuery}`;
|
||||||
|
|
||||||
this.isDevelopment = process.env.NODE_ENV !== NodeEnv.Prod;
|
this.isDevelopment = process.env.NODE_ENV !== "production";
|
||||||
this.userAgent = navigator.userAgent;
|
this.userAgent = navigator.userAgent;
|
||||||
|
|
||||||
this.openDeepLink();
|
this.openDeepLink();
|
||||||
|
|||||||
@@ -102,7 +102,8 @@ Raymer * @version 1.0.0 */
|
|||||||
class="text-md font-bold bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mt-2 px-2 py-3 rounded-md"
|
class="text-md font-bold bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mt-2 px-2 py-3 rounded-md"
|
||||||
@click="showNameThenIdDialog()"
|
@click="showNameThenIdDialog()"
|
||||||
>
|
>
|
||||||
Show them your identification info
|
Show them {{ PASSKEYS_ENABLED ? "default" : "your" }} identifier
|
||||||
|
info
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<UserNameDialog ref="userNameDialog" />
|
<UserNameDialog ref="userNameDialog" />
|
||||||
@@ -682,7 +683,7 @@ export default class HomeView extends Vue {
|
|||||||
group: "alert",
|
group: "alert",
|
||||||
type: "warning",
|
type: "warning",
|
||||||
title: "Feed Loading Issue",
|
title: "Feed Loading Issue",
|
||||||
text: "Some feed data may be unavailable. Try refreshing the page.",
|
text: "Some feed data may be unavailable. Pull to refresh.",
|
||||||
},
|
},
|
||||||
5000,
|
5000,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -128,7 +128,10 @@ export default class InviteOneAcceptView extends Vue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Extract JWT from route path
|
// Extract JWT from route path
|
||||||
const jwt = (this.$route.params.jwt as string) || "";
|
const jwt =
|
||||||
|
(this.$route.params.jwt as string) ||
|
||||||
|
(this.$route.query.jwt as string) ||
|
||||||
|
"";
|
||||||
await this.processInvite(jwt, false);
|
await this.processInvite(jwt, false);
|
||||||
|
|
||||||
this.checkingInvite = false;
|
this.checkingInvite = false;
|
||||||
|
|||||||
@@ -117,6 +117,9 @@ export default class OnboardMeetingMembersView extends Vue {
|
|||||||
this.isRegistered = settings.isRegistered || false;
|
this.isRegistered = settings.isRegistered || false;
|
||||||
try {
|
try {
|
||||||
if (!this.activeDid) {
|
if (!this.activeDid) {
|
||||||
|
logConsoleAndDb(
|
||||||
|
"[OnboardMeetingMembersView] No active DID found, creating identity as fallback for meeting setup",
|
||||||
|
);
|
||||||
this.activeDid = await generateSaveAndActivateIdentity();
|
this.activeDid = await generateSaveAndActivateIdentity();
|
||||||
this.isRegistered = false;
|
this.isRegistered = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -153,9 +153,7 @@ export default class QuickActionBvcBeginView extends Vue {
|
|||||||
group: "alert",
|
group: "alert",
|
||||||
type: "danger",
|
type: "danger",
|
||||||
title: "Error",
|
title: "Error",
|
||||||
text:
|
text: timeResult?.error || "There was an error sending the time.",
|
||||||
timeResult?.error ||
|
|
||||||
"There was an error sending the time.",
|
|
||||||
},
|
},
|
||||||
5000,
|
5000,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -221,7 +221,8 @@ export default class QuickActionBvcBeginView extends Vue {
|
|||||||
}
|
}
|
||||||
const eventStartDateObj = currentOrPreviousSat
|
const eventStartDateObj = currentOrPreviousSat
|
||||||
.set({ weekday: 6 })
|
.set({ weekday: 6 })
|
||||||
.set({ hour: 9 })
|
.set({ hour: 8 })
|
||||||
|
.set({ minute: 30 }) // to catch if people put their claims 30 minutes early
|
||||||
.startOf("hour");
|
.startOf("hour");
|
||||||
|
|
||||||
// Hack, but full ISO pushes the length to 340 which crashes verifyJWT!
|
// Hack, but full ISO pushes the length to 340 which crashes verifyJWT!
|
||||||
|
|||||||
55
src/vite.config.utils.js
Normal file
55
src/vite.config.utils.js
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import * as path from "path";
|
||||||
|
import { promises as fs } from "fs";
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
|
||||||
|
export async function loadAppConfig() {
|
||||||
|
const packageJson = await loadPackageJson();
|
||||||
|
const appName = process.env.TIME_SAFARI_APP_TITLE || packageJson.name;
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
|
return {
|
||||||
|
pwaConfig: {
|
||||||
|
manifest: {
|
||||||
|
name: appName,
|
||||||
|
short_name: appName,
|
||||||
|
icons: [
|
||||||
|
{
|
||||||
|
src: "./img/icons/android-chrome-192x192.png",
|
||||||
|
sizes: "192x192",
|
||||||
|
type: "image/png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: "./img/icons/android-chrome-512x512.png",
|
||||||
|
sizes: "512x512",
|
||||||
|
type: "image/png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: "./img/icons/android-chrome-maskable-192x192.png",
|
||||||
|
sizes: "192x192",
|
||||||
|
type: "image/png",
|
||||||
|
purpose: "maskable",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: "./img/icons/android-chrome-maskable-512x512.png",
|
||||||
|
sizes: "512x512",
|
||||||
|
type: "image/png",
|
||||||
|
purpose: "maskable",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
aliasConfig: {
|
||||||
|
"@": path.resolve(path.dirname(__dirname), "src"),
|
||||||
|
buffer: path.resolve(path.dirname(__dirname), "node_modules", "buffer"),
|
||||||
|
"dexie-export-import/dist/import":
|
||||||
|
"dexie-export-import/dist/import/index.js",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadPackageJson() {
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
const packageJsonPath = path.resolve(path.dirname(__dirname), "package.json");
|
||||||
|
const packageJsonData = await fs.readFile(packageJsonPath, "utf-8");
|
||||||
|
return JSON.parse(packageJsonData);
|
||||||
|
}
|
||||||
@@ -2,14 +2,14 @@ import { test, expect } from '@playwright/test';
|
|||||||
import { importUser, generateNewEthrUser, switchToUser } from './testUtils';
|
import { importUser, generateNewEthrUser, switchToUser } from './testUtils';
|
||||||
|
|
||||||
test('New offers for another user', async ({ page }) => {
|
test('New offers for another user', async ({ page }) => {
|
||||||
const newUserDid = await generateNewEthrUser(page);
|
const user01Did = await generateNewEthrUser(page);
|
||||||
await page.goto('./');
|
await page.goto('./');
|
||||||
await page.getByTestId('closeOnboardingAndFinish').click();
|
await page.getByTestId('closeOnboardingAndFinish').click();
|
||||||
await expect(page.getByTestId('newDirectOffersActivityNumber')).toBeHidden();
|
await expect(page.getByTestId('newDirectOffersActivityNumber')).toBeHidden();
|
||||||
|
|
||||||
await importUser(page, '00');
|
await importUser(page, '00');
|
||||||
await page.goto('./contacts');
|
await page.goto('./contacts');
|
||||||
await page.getByPlaceholder('URL or DID, Name, Public Key').fill(newUserDid + ', A Friend');
|
await page.getByPlaceholder('URL or DID, Name, Public Key').fill(user01Did + ', A Friend');
|
||||||
await expect(page.locator('button > svg.fa-plus')).toBeVisible();
|
await expect(page.locator('button > svg.fa-plus')).toBeVisible();
|
||||||
await page.locator('button > svg.fa-plus').click();
|
await page.locator('button > svg.fa-plus').click();
|
||||||
await expect(page.locator('div[role="alert"] span:has-text("Contact Added")')).toBeVisible();
|
await expect(page.locator('div[role="alert"] span:has-text("Contact Added")')).toBeVisible();
|
||||||
@@ -18,7 +18,7 @@ test('New offers for another user', async ({ page }) => {
|
|||||||
await expect(page.locator('div[role="alert"] button > svg.fa-xmark')).toBeHidden(); // ensure alert is gone
|
await expect(page.locator('div[role="alert"] button > svg.fa-xmark')).toBeHidden(); // ensure alert is gone
|
||||||
|
|
||||||
// show buttons to make offers directly to people
|
// show buttons to make offers directly to people
|
||||||
await page.getByRole('button').filter({ hasText: /See Actions/i }).click();
|
await page.getByRole('button').filter({ hasText: /See Hours/i }).click();
|
||||||
|
|
||||||
// make an offer directly to user 1
|
// make an offer directly to user 1
|
||||||
// Generate a random string of 3 characters, skipping the "0." at the beginning
|
// Generate a random string of 3 characters, skipping the "0." at the beginning
|
||||||
@@ -42,7 +42,7 @@ test('New offers for another user', async ({ page }) => {
|
|||||||
await expect(page.locator('div[role="alert"] button > svg.fa-xmark')).toBeHidden(); // ensure alert is gone
|
await expect(page.locator('div[role="alert"] button > svg.fa-xmark')).toBeHidden(); // ensure alert is gone
|
||||||
|
|
||||||
// as user 1, go to the home page and check that two offers are shown as new
|
// as user 1, go to the home page and check that two offers are shown as new
|
||||||
await switchToUser(page, newUserDid);
|
await switchToUser(page, user01Did);
|
||||||
await page.goto('./');
|
await page.goto('./');
|
||||||
let offerNumElem = page.getByTestId('newDirectOffersActivityNumber');
|
let offerNumElem = page.getByTestId('newDirectOffersActivityNumber');
|
||||||
await expect(offerNumElem).toHaveText('2');
|
await expect(offerNumElem).toHaveText('2');
|
||||||
|
|||||||
@@ -8,5 +8,5 @@
|
|||||||
"allowImportingTsExtensions": true,
|
"allowImportingTsExtensions": true,
|
||||||
"noEmit": true
|
"noEmit": true
|
||||||
},
|
},
|
||||||
"include": ["vite.config.*", "./src/interfaces/build.ts"]
|
"include": ["vite.config.*"]
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
import { createBuildConfig } from "./vite.config.common.mts";
|
import { createBuildConfig } from "./vite.config.common.mts";
|
||||||
import { BuildPlatform } from "./src/interfaces/build.ts";
|
|
||||||
|
|
||||||
export default defineConfig(async () => createBuildConfig(BuildPlatform.Capacitor));
|
export default defineConfig(async () => createBuildConfig('capacitor'));
|
||||||
@@ -1,45 +1,33 @@
|
|||||||
import { defineConfig, UserConfig } from "vite";
|
import { defineConfig, UserConfig, Plugin } from "vite";
|
||||||
import vue from "@vitejs/plugin-vue";
|
import vue from "@vitejs/plugin-vue";
|
||||||
import dotenv from "dotenv";
|
import dotenv from "dotenv";
|
||||||
import { loadAppConfig } from "./vite.config.common-utils.mts";
|
import { loadAppConfig } from "./vite.config.utils.mts";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import { NodeEnv, BuildEnv, BuildPlatform } from "./src/interfaces/build.ts";
|
|
||||||
|
|
||||||
// Load environment variables
|
// Load environment variables
|
||||||
let buildEnv: BuildEnv;
|
console.log('NODE_ENV:', process.env.NODE_ENV)
|
||||||
if (process.env.NODE_ENV === NodeEnv.Dev) {
|
dotenv.config({ path: `.env.${process.env.NODE_ENV}` })
|
||||||
buildEnv = BuildEnv.Development;
|
|
||||||
} else if (process.env.NODE_ENV === NodeEnv.Test) {
|
|
||||||
buildEnv = BuildEnv.Testing;
|
|
||||||
} else if (process.env.NODE_ENV === NodeEnv.Prod) {
|
|
||||||
buildEnv = BuildEnv.Production;
|
|
||||||
} else {
|
|
||||||
console.error("NODE_ENV is not set. Invoke with NODE_ENV=" + Object.values(NodeEnv).join("|"));
|
|
||||||
throw new Error("NODE_ENV is not set. Invoke with NODE_ENV=" + Object.values(NodeEnv).join("|"));
|
|
||||||
}
|
|
||||||
console.log(`Environment: ${buildEnv}`);
|
|
||||||
|
|
||||||
dotenv.config({ path: `.env.${buildEnv}` });
|
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
export async function createBuildConfig(platform: BuildPlatform): Promise<UserConfig> {
|
export async function createBuildConfig(mode: string): Promise<UserConfig> {
|
||||||
const appConfig = await loadAppConfig();
|
const appConfig = await loadAppConfig();
|
||||||
|
const isElectron = mode === "electron";
|
||||||
console.log(`Platform: ${platform}`);
|
const isCapacitor = mode === "capacitor";
|
||||||
const isElectron = platform === BuildPlatform.Electron;
|
const isPyWebView = mode === "pywebview";
|
||||||
const isCapacitor = platform === BuildPlatform.Capacitor;
|
|
||||||
const isPyWebView = platform === BuildPlatform.PyWebView;
|
|
||||||
|
|
||||||
// Explicitly set platform and disable PWA for Electron
|
// Explicitly set platform and disable PWA for Electron
|
||||||
process.env.VITE_PLATFORM = platform;
|
process.env.VITE_PLATFORM = mode;
|
||||||
process.env.VITE_PWA_ENABLED = (isElectron || isPyWebView || isCapacitor)
|
process.env.VITE_PWA_ENABLED = isElectron ? 'false' : 'true';
|
||||||
? 'false'
|
|
||||||
: 'true';
|
|
||||||
process.env.VITE_DISABLE_PWA = isElectron ? 'true' : 'false';
|
process.env.VITE_DISABLE_PWA = isElectron ? 'true' : 'false';
|
||||||
|
|
||||||
|
if (isElectron || isPyWebView || isCapacitor) {
|
||||||
|
process.env.VITE_PWA_ENABLED = 'false';
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
base: isElectron || isPyWebView ? "./" : "/",
|
base: isElectron || isPyWebView ? "./" : "/",
|
||||||
plugins: [vue()],
|
plugins: [vue()],
|
||||||
@@ -70,7 +58,7 @@ export async function createBuildConfig(platform: BuildPlatform): Promise<UserCo
|
|||||||
},
|
},
|
||||||
define: {
|
define: {
|
||||||
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
|
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
|
||||||
'process.env.VITE_PLATFORM': JSON.stringify(platform),
|
'process.env.VITE_PLATFORM': JSON.stringify(mode),
|
||||||
'process.env.VITE_PWA_ENABLED': JSON.stringify(!isElectron),
|
'process.env.VITE_PWA_ENABLED': JSON.stringify(!isElectron),
|
||||||
'process.env.VITE_DISABLE_PWA': JSON.stringify(isElectron),
|
'process.env.VITE_DISABLE_PWA': JSON.stringify(isElectron),
|
||||||
__dirname: isElectron ? JSON.stringify(process.cwd()) : '""',
|
__dirname: isElectron ? JSON.stringify(process.cwd()) : '""',
|
||||||
@@ -92,10 +80,10 @@ export async function createBuildConfig(platform: BuildPlatform): Promise<UserCo
|
|||||||
'path': path.resolve(__dirname, './src/utils/node-modules/path.js'),
|
'path': path.resolve(__dirname, './src/utils/node-modules/path.js'),
|
||||||
'fs': path.resolve(__dirname, './src/utils/node-modules/fs.js'),
|
'fs': path.resolve(__dirname, './src/utils/node-modules/fs.js'),
|
||||||
'crypto': path.resolve(__dirname, './src/utils/node-modules/crypto.js'),
|
'crypto': path.resolve(__dirname, './src/utils/node-modules/crypto.js'),
|
||||||
'nostr-tools/nip06': buildEnv === BuildEnv.Development
|
'nostr-tools/nip06': mode === 'development'
|
||||||
? 'nostr-tools/nip06'
|
? 'nostr-tools/nip06'
|
||||||
: path.resolve(__dirname, 'node_modules/nostr-tools/nip06'),
|
: path.resolve(__dirname, 'node_modules/nostr-tools/nip06'),
|
||||||
'nostr-tools/core': buildEnv === BuildEnv.Development
|
'nostr-tools/core': mode === 'development'
|
||||||
? 'nostr-tools'
|
? 'nostr-tools'
|
||||||
: path.resolve(__dirname, 'node_modules/nostr-tools'),
|
: path.resolve(__dirname, 'node_modules/nostr-tools'),
|
||||||
'nostr-tools': path.resolve(__dirname, 'node_modules/nostr-tools'),
|
'nostr-tools': path.resolve(__dirname, 'node_modules/nostr-tools'),
|
||||||
@@ -122,4 +110,4 @@ export async function createBuildConfig(platform: BuildPlatform): Promise<UserCo
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default defineConfig(async () => createBuildConfig(BuildPlatform.Web));
|
export default defineConfig(async () => createBuildConfig('web'));
|
||||||
|
|||||||
4
vite.config.dev.mts
Normal file
4
vite.config.dev.mts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import { defineConfig } from "vite";
|
||||||
|
import { createBuildConfig } from "./vite.config.common.mts";
|
||||||
|
|
||||||
|
export default defineConfig(async () => createBuildConfig('development'));
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
import { defineConfig, mergeConfig } from "vite";
|
import { defineConfig, mergeConfig } from "vite";
|
||||||
import { createBuildConfig } from "./vite.config.common.mts";
|
import { createBuildConfig } from "./vite.config.common.mts";
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { BuildPlatform } from "./src/interfaces/build.ts";
|
|
||||||
|
|
||||||
export default defineConfig(async () => {
|
export default defineConfig(async () => {
|
||||||
const baseConfig = await createBuildConfig(BuildPlatform.Electron);
|
const baseConfig = await createBuildConfig('electron');
|
||||||
|
|
||||||
return mergeConfig(baseConfig, {
|
return mergeConfig(baseConfig, {
|
||||||
build: {
|
build: {
|
||||||
|
|||||||
71
vite.config.mts
Normal file
71
vite.config.mts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import { defineConfig } from "vite";
|
||||||
|
import vue from "@vitejs/plugin-vue";
|
||||||
|
import path from "path";
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [vue()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': path.resolve(__dirname, './src'),
|
||||||
|
'nostr-tools': path.resolve(__dirname, 'node_modules/nostr-tools'),
|
||||||
|
'nostr-tools/nip06': path.resolve(__dirname, 'node_modules/nostr-tools/nip06'),
|
||||||
|
'nostr-tools/core': path.resolve(__dirname, 'node_modules/nostr-tools/core'),
|
||||||
|
stream: 'stream-browserify',
|
||||||
|
util: 'util',
|
||||||
|
crypto: 'crypto-browserify',
|
||||||
|
assert: 'assert/',
|
||||||
|
http: 'stream-http',
|
||||||
|
https: 'https-browserify',
|
||||||
|
url: 'url/',
|
||||||
|
zlib: 'browserify-zlib',
|
||||||
|
path: 'path-browserify',
|
||||||
|
fs: false,
|
||||||
|
tty: 'tty-browserify',
|
||||||
|
net: false,
|
||||||
|
dns: false,
|
||||||
|
child_process: false,
|
||||||
|
os: false
|
||||||
|
},
|
||||||
|
mainFields: ['module', 'jsnext:main', 'jsnext', 'main'],
|
||||||
|
},
|
||||||
|
optimizeDeps: {
|
||||||
|
include: ['nostr-tools', 'nostr-tools/nip06', 'nostr-tools/core'],
|
||||||
|
esbuildOptions: {
|
||||||
|
define: {
|
||||||
|
global: 'globalThis'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
sourcemap: true,
|
||||||
|
target: 'esnext',
|
||||||
|
chunkSizeWarningLimit: 1000,
|
||||||
|
commonjsOptions: {
|
||||||
|
include: [/node_modules/],
|
||||||
|
transformMixedEsModules: true
|
||||||
|
},
|
||||||
|
rollupOptions: {
|
||||||
|
external: [
|
||||||
|
'stream', 'util', 'crypto', 'http', 'https', 'url', 'zlib',
|
||||||
|
'path', 'fs', 'tty', 'assert', 'net', 'dns', 'child_process', 'os'
|
||||||
|
],
|
||||||
|
output: {
|
||||||
|
globals: {
|
||||||
|
stream: 'stream',
|
||||||
|
util: 'util',
|
||||||
|
crypto: 'crypto',
|
||||||
|
http: 'http',
|
||||||
|
https: 'https',
|
||||||
|
url: 'url',
|
||||||
|
zlib: 'zlib',
|
||||||
|
path: 'path',
|
||||||
|
assert: 'assert',
|
||||||
|
tty: 'tty'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
import { createBuildConfig } from "./vite.config.common.mts";
|
import { createBuildConfig } from "./vite.config.common.mts";
|
||||||
import { BuildPlatform } from "./src/interfaces/build.ts";
|
|
||||||
|
|
||||||
export default defineConfig(async () => createBuildConfig(BuildPlatform.PyWebView));
|
export default defineConfig(async () => createBuildConfig('pywebview'));
|
||||||
53
vite.config.ts
Normal file
53
vite.config.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { defineConfig } from "vite";
|
||||||
|
import vue from "@vitejs/plugin-vue";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [vue()],
|
||||||
|
server: {
|
||||||
|
headers: {
|
||||||
|
'Cross-Origin-Opener-Policy': 'same-origin',
|
||||||
|
'Cross-Origin-Embedder-Policy': 'require-corp'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': path.resolve(__dirname, './src'),
|
||||||
|
'nostr-tools': path.resolve(__dirname, 'node_modules/nostr-tools'),
|
||||||
|
'nostr-tools/nip06': path.resolve(__dirname, 'node_modules/nostr-tools/nip06'),
|
||||||
|
'nostr-tools/core': path.resolve(__dirname, 'node_modules/nostr-tools/core'),
|
||||||
|
stream: 'stream-browserify',
|
||||||
|
util: 'util',
|
||||||
|
crypto: 'crypto-browserify'
|
||||||
|
},
|
||||||
|
mainFields: ['module', 'jsnext:main', 'jsnext', 'main'],
|
||||||
|
},
|
||||||
|
optimizeDeps: {
|
||||||
|
include: ['nostr-tools', 'nostr-tools/nip06', 'nostr-tools/core', '@jlongster/sql.js'],
|
||||||
|
esbuildOptions: {
|
||||||
|
define: {
|
||||||
|
global: 'globalThis'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
sourcemap: true,
|
||||||
|
target: 'esnext',
|
||||||
|
chunkSizeWarningLimit: 1000,
|
||||||
|
commonjsOptions: {
|
||||||
|
include: [/node_modules/],
|
||||||
|
transformMixedEsModules: true
|
||||||
|
},
|
||||||
|
rollupOptions: {
|
||||||
|
external: ['stream', 'util', 'crypto'],
|
||||||
|
output: {
|
||||||
|
globals: {
|
||||||
|
stream: 'stream',
|
||||||
|
util: 'util',
|
||||||
|
crypto: 'crypto'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
assetsInclude: ['**/*.wasm']
|
||||||
|
});
|
||||||
@@ -45,73 +45,71 @@ interface PWAConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface AppConfig {
|
interface AppConfig {
|
||||||
|
pwaConfig: PWAConfig;
|
||||||
aliasConfig: {
|
aliasConfig: {
|
||||||
[key: string]: string;
|
[key: string]: string;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadAppConfig(): Promise<AppConfig> {
|
export async function loadAppConfig(): Promise<AppConfig> {
|
||||||
|
const packageJson = await loadPackageJson();
|
||||||
|
const appName = process.env.TIME_SAFARI_APP_TITLE || packageJson.name;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
pwaConfig: {
|
||||||
|
registerType: "autoUpdate",
|
||||||
|
strategies: "injectManifest",
|
||||||
|
srcDir: ".",
|
||||||
|
filename: "sw_scripts-combined.js",
|
||||||
|
manifest: {
|
||||||
|
name: appName,
|
||||||
|
short_name: appName,
|
||||||
|
theme_color: "#4a90e2",
|
||||||
|
background_color: "#ffffff",
|
||||||
|
icons: [
|
||||||
|
{
|
||||||
|
src: "./img/icons/android-chrome-192x192.png",
|
||||||
|
sizes: "192x192",
|
||||||
|
type: "image/png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: "./img/icons/android-chrome-512x512.png",
|
||||||
|
sizes: "512x512",
|
||||||
|
type: "image/png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: "./img/icons/android-chrome-maskable-192x192.png",
|
||||||
|
sizes: "192x192",
|
||||||
|
type: "image/png",
|
||||||
|
purpose: "maskable",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: "./img/icons/android-chrome-maskable-512x512.png",
|
||||||
|
sizes: "512x512",
|
||||||
|
type: "image/png",
|
||||||
|
purpose: "maskable",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
share_target: {
|
||||||
|
action: "/share-target",
|
||||||
|
method: "POST",
|
||||||
|
enctype: "multipart/form-data",
|
||||||
|
params: {
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
name: "photo",
|
||||||
|
accept: ["image/*"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
aliasConfig: {
|
aliasConfig: {
|
||||||
"@": path.resolve(__dirname, "src"),
|
"@": path.resolve(__dirname, "src"),
|
||||||
buffer: path.resolve(__dirname, "node_modules", "buffer"),
|
buffer: path.resolve(__dirname, "node_modules", "buffer"),
|
||||||
"dexie-export-import/dist/import":
|
"dexie-export-import/dist/import":
|
||||||
"dexie-export-import/dist/import/index.js",
|
"dexie-export-import/dist/import/index.js",
|
||||||
},
|
},
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function loadPwaConfig(): Promise<PWAConfig> {
|
|
||||||
const packageJson = await loadPackageJson();
|
|
||||||
const appName = process.env.TIME_SAFARI_APP_TITLE || packageJson.name;
|
|
||||||
|
|
||||||
return {
|
|
||||||
registerType: "autoUpdate",
|
|
||||||
strategies: "injectManifest",
|
|
||||||
srcDir: ".",
|
|
||||||
filename: "sw_scripts-combined.js",
|
|
||||||
manifest: {
|
|
||||||
name: appName,
|
|
||||||
short_name: appName,
|
|
||||||
theme_color: "#4a90e2",
|
|
||||||
background_color: "#ffffff",
|
|
||||||
icons: [
|
|
||||||
{
|
|
||||||
src: "./img/icons/android-chrome-192x192.png",
|
|
||||||
sizes: "192x192",
|
|
||||||
type: "image/png",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
src: "./img/icons/android-chrome-512x512.png",
|
|
||||||
sizes: "512x512",
|
|
||||||
type: "image/png",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
src: "./img/icons/android-chrome-maskable-192x192.png",
|
|
||||||
sizes: "192x192",
|
|
||||||
type: "image/png",
|
|
||||||
purpose: "maskable",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
src: "./img/icons/android-chrome-maskable-512x512.png",
|
|
||||||
sizes: "512x512",
|
|
||||||
type: "image/png",
|
|
||||||
purpose: "maskable",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
share_target: {
|
|
||||||
action: "/share-target",
|
|
||||||
method: "POST",
|
|
||||||
enctype: "multipart/form-data",
|
|
||||||
params: {
|
|
||||||
files: [
|
|
||||||
{
|
|
||||||
name: "photo",
|
|
||||||
accept: ["image/*"],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1,18 +1,17 @@
|
|||||||
import { defineConfig, mergeConfig } from "vite";
|
import { defineConfig, mergeConfig } from "vite";
|
||||||
import { VitePWA } from "vite-plugin-pwa";
|
import { VitePWA } from "vite-plugin-pwa";
|
||||||
import { createBuildConfig } from "./vite.config.common.mts";
|
import { createBuildConfig } from "./vite.config.common.mts";
|
||||||
import { loadPwaConfig } from "./vite.config.common-utils.mts";
|
import { loadAppConfig } from "./vite.config.utils.mts";
|
||||||
import { BuildPlatform } from "./src/interfaces/build.ts";
|
|
||||||
|
|
||||||
export default defineConfig(async () => {
|
export default defineConfig(async () => {
|
||||||
const baseConfig = await createBuildConfig(BuildPlatform.Web);
|
const baseConfig = await createBuildConfig('web');
|
||||||
const pwaConfig = await loadPwaConfig();
|
const appConfig = await loadAppConfig();
|
||||||
|
|
||||||
return mergeConfig(baseConfig, {
|
return mergeConfig(baseConfig, {
|
||||||
plugins: [
|
plugins: [
|
||||||
VitePWA({
|
VitePWA({
|
||||||
registerType: 'autoUpdate',
|
registerType: 'autoUpdate',
|
||||||
manifest: pwaConfig.manifest,
|
manifest: appConfig.pwaConfig?.manifest,
|
||||||
devOptions: {
|
devOptions: {
|
||||||
enabled: false
|
enabled: false
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user