Compare commits

..

29 Commits

Author SHA1 Message Date
25512d3db1 some fixes to the gifted-dialog logic 2025-06-29 18:18:18 -06:00
Jose Olarte III
bf9fee7ee9 Various aesthetic improvements and optimizations 2025-06-27 21:59:19 +08:00
Jose Olarte III
08c46a27d3 Add project-to-project case 2025-06-26 18:52:52 +08:00
Jose Olarte III
c9405839c3 Merge branch 'gifting-ui-2025-05' into gifting-periphery-improvements 2025-06-26 15:27:55 +08:00
0e6a9c4f89 adjust grammar for recording receipt 2025-06-25 20:51:57 -06:00
Jose Olarte III
b6278ca148 Unit codes pulled from util.ts 2025-06-25 21:28:25 +08:00
Jose Olarte III
d8e237f8cb Describe firstStep variable 2025-06-25 21:17:11 +08:00
Jose Olarte III
4b539ccc55 Better handling of No-name and Unnamed entities 2025-06-25 21:15:05 +08:00
Jose Olarte III
ea49173885 Changed currentStep to boolean 2025-06-25 17:38:33 +08:00
Jose Olarte III
447a7cb089 Style "unnamed" entity 2025-06-25 17:35:01 +08:00
Jose Olarte III
c0ddba8898 Various design tweaks 2025-06-24 19:22:09 +08:00
Jose Olarte III
fe4ae90849 Giver-recipient display fixes
- Truncate very long texts (such as dids)
- Stacked layout in mobile, row layout in wider screens
- Minor design adjustments
2025-06-24 19:19:51 +08:00
Jose Olarte III
ce04312baa Updated amount input controls
Now consistent with gifting dialog version
2025-06-24 19:17:30 +08:00
Jose Olarte III
a8cc480960 Merge branch 'master' into gifting-periphery-improvements 2025-06-24 16:20:07 +08:00
Jose Olarte III
357822d713 Fix: truncate text blocks
- Avoid did display stretching screen width
2025-06-24 16:18:22 +08:00
Jose Olarte III
ca22161f12 Fix: entity-type identifier validation
- Ensure claims contain only correct and necessary giver and recipient identifiers, as per Endorser.ch documentation
2025-06-20 20:37:14 +08:00
Jose Olarte III
d3b80fbe47 Feature: giver-recipient validation
- Ensures person-to-person gifting won't allow the same entity as giver and recipient
- Disable user item selection if it would create conflict
- Error messaging fallback
2025-06-20 18:38:35 +08:00
Jose Olarte III
0342c872f4 Fix: added context for ContactGiftingView 2025-06-20 15:50:57 +08:00
Jose Olarte III
a7e65b3b49 Giver-recipient controls
- Dialog now shows separate cards for giver and recipient
- Ability to change giver and/or recipient
- Project giver/recipient is locked in ProjectView (context reinforcement)
2025-06-19 21:16:56 +08:00
Jose Olarte III
eb7605991c Fixed more gifting use cases 2025-06-18 19:58:10 +08:00
fa21660fd1 fix spelling 2025-06-15 12:43:22 -06:00
Jose Olarte III
df1c1f0186 Fix: pass project info
In GiftingDialog, project information is passed along if:
- Selecting "Show All" to go to ContactGiftingView
- Selecting "Photos and Other Options" to go to GiftedDetailsView
2025-06-13 20:52:26 +08:00
Jose Olarte III
3daf1c8a5c Feature: Project Gifting
- Gifting dialog: added ability to pick a project to benefit from
- Project view: modified dialog calls in Project view to toggle between giving to and benefiting from a project
- Project view: removed redundant person selection
- Project view: benefiting from a project locks the project selection in dialog to enforce context.
2025-06-12 20:50:27 +08:00
Jose Olarte III
7eefee1ea5 Fix: Conditional show-all link
- Only show "Show All" when user has contacts
2025-06-12 14:34:00 +08:00
Jose Olarte III
140c36a416 Merge branch 'master' into gifting-ui-2025-05 2025-06-11 19:10:59 +08:00
Jose Olarte III
988244b7ae Added check for "Unnamed" giver
Pass string "Unnamed" to select unnamed giver and skip contact selection step of dialog.
2025-05-16 20:22:09 +08:00
Jose Olarte III
4b355a5448 WIP: two-step dialog + functionality
- Dialog is now presented as two distinct steps
- Gifting functionality reinstated
- Minor UI tweaks
- IN PROGRESS: ensuring calls to dialog from other parts of the app remain functional
2025-05-14 21:47:12 +08:00
Jose Olarte III
b511f9cd24 WIP: adjustments to bring closer to original mockups 2025-05-13 21:16:39 +08:00
Jose Olarte III
579cecbe6e WIP: gifting UI revamp
Started to transform the gifting dialog into the two-step setup as per previous mockups
2025-05-12 21:22:05 +08:00
46 changed files with 1631 additions and 638 deletions

View File

@@ -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

7
.gitignore vendored
View File

@@ -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/

View File

@@ -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
@@ -52,7 +63,7 @@ Install dependencies:
* 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 `npm install`.
* Run a build to make sure package-lock version is updated, linting works, etc: `npm install && npm run build:web` * Run a build to make sure package-lock version is updated, linting works, etc: `npm install && npm run build`
* Commit everything (since the commit hash is used the app). * Commit everything (since the commit hash is used the app).
@@ -63,7 +74,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 +216,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 +231,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 +265,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 +304,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)
@@ -468,12 +479,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
```

View File

@@ -6,14 +6,6 @@ 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]
### Fixed
- Set the environment variables correctly (so simulator deep link domain is right).
### Changed
- Photo is pinned to profile mode.
- NODE_ENV is now mandatory.
## [1.0.2] - 2025.06.20 - 276e0a741bc327de3380c4e508cccb7fee58c06d ## [1.0.2] - 2025.06.20 - 276e0a741bc327de3380c4e508cccb7fee58c06d
### Added ### Added
- Version on feed title - Version on feed title

View File

@@ -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
@@ -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

View File

@@ -13,7 +13,6 @@
android:exported="true" android:exported="true"
android:label="@string/title_activity_main" android:label="@string/title_activity_main"
android:launchMode="singleTask" android:launchMode="singleTask"
android:screenOrientation="portrait"
android:theme="@style/AppTheme.NoActionBarLaunch"> android:theme="@style/AppTheme.NoActionBarLaunch">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />

View File

@@ -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

View File

@@ -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>

View File

@@ -37,6 +37,8 @@
<key>UISupportedInterfaceOrientations</key> <key>UISupportedInterfaceOrientations</key>
<array> <array>
<string>UIInterfaceOrientationPortrait</string> <string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array> </array>
<key>UISupportedInterfaceOrientations~ipad</key> <key>UISupportedInterfaceOrientations~ipad</key>
<array> <array>

View File

@@ -6,37 +6,44 @@
"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",

View File

@@ -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,
}, },

View File

@@ -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,
// }, // },

View File

@@ -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>

View File

@@ -1,99 +1,534 @@
<template> <template>
<div v-if="visible" class="dialog-overlay"> <div v-if="visible" class="dialog-overlay">
<div class="dialog"> <div class="dialog">
<h1 class="text-xl font-bold text-center mb-4"> <!-- Step 1: Giver -->
{{ customTitle }} <div v-show="firstStep" id="sectionGiftedGiver">
</h1> <label class="block font-bold mb-4">
<input {{
v-model="description" stepType === "recipient"
type="text" ? "Choose who received the gift:"
class="block w-full rounded border border-slate-400 mb-2 px-3 py-2" : showProjects
:placeholder="prompt || 'What was given?'" ? "Choose a project benefitted from:"
/> : "Choose a person received from:"
<div class="flex flex-row justify-center"> }}
<span </label>
class="rounded-l border border-r-0 border-slate-400 bg-slate-200 text-center text-blue-500 px-2 py-2 w-20"
@click="changeUnitCode()" <!-- Unified Quick-pick grid for People and Projects -->
<ul
:class="
shouldShowProjects
? 'grid grid-cols-3 md:grid-cols-4 gap-x-2 gap-y-4 text-center mb-4'
: 'grid grid-cols-4 sm:grid-cols-5 md:grid-cols-6 gap-x-2 gap-y-4 text-center mb-4'
"
> >
{{ libsUtil.UNIT_SHORT[unitCode] || unitCode }} <template v-if="shouldShowProjects">
</span> <!-- show projects -->
<div <li
class="border border-r-0 border-slate-400 bg-slate-200 px-4 py-2" v-for="project in projects.slice(0, 7)"
@click="amountInput === '0' ? null : decrement()" :key="project.handleId"
> class="cursor-pointer"
<font-awesome icon="chevron-left" /> @click="
</div> stepType === 'recipient'
<input ? selectRecipientProject(project)
id="inputGivenAmount" : selectProject(project)
v-model="amountInput" "
type="number" >
class="border border-r-0 border-slate-400 px-2 py-2 text-center w-20" <div class="relative w-fit mx-auto mb-1">
/> <ProjectIcon
<div :entity-id="project.handleId"
class="rounded-r border border-slate-400 bg-slate-200 px-4 py-2" :icon-size="48"
@click="increment()" :image-url="project.image"
> class="!size-[3rem] mx-auto border border-slate-300 bg-white overflow-hidden rounded-full"
<font-awesome icon="chevron-right" /> />
</div> </div>
</div> <h3
<div class="mt-4 flex justify-center"> class="text-xs font-medium text-ellipsis whitespace-nowrap overflow-hidden"
<span> >
<router-link {{ project.name }}
:to="{ </h3>
name: 'gifted-details', <div class="text-xs text-slate-500 truncate">
query: { <font-awesome icon="user" class="fa-fw text-slate-400" />
amountInput, {{
description, didInfo(project.issuerDid, activeDid, allMyDids, allContacts)
giverDid: giver?.did, }}
giverName: giver?.name, </div>
offerId, </li>
fulfillsProjectId: toProjectId, <li
providerProjectId: fromProjectId, v-if="projects.length === 0"
recipientDid: receiver?.did, class="text-xs text-slate-500 italic col-span-full"
recipientName: receiver?.name, >
unitCode, (No projects found.)
}, </li>
}" <li v-if="projects.length > 0">
class="text-blue-500" <router-link :to="{ name: 'discover' }" class="cursor-pointer">
> <font-awesome
Photo & more options ... icon="circle-right"
</router-link> class="text-blue-500 text-5xl mb-1"
</span> />
</div> <h3
<p class="text-center mb-2 mt-6 italic"> class="text-xs text-slate-400 font-medium italic text-ellipsis whitespace-nowrap overflow-hidden"
Sign & Send to publish to the world >
<font-awesome Show All
icon="circle-info" </h3>
class="pl-2 text-blue-500 cursor-pointer" </router-link>
@click="explainData()" </li>
/> </template>
</p> <template v-else>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2"> <!-- show people (contacts) -->
<li
v-if="
stepType === 'recipient' ||
(stepType === 'giver' && isFromProjectView)
"
:class="{
'cursor-pointer': !wouldCreateConflict(activeDid),
'cursor-not-allowed opacity-50': wouldCreateConflict(activeDid)
}"
@click="
!wouldCreateConflict(activeDid) &&
(stepType === 'recipient'
? selectRecipient({ did: activeDid, name: 'You' })
: selectGiver({ did: activeDid, name: 'You' }))
"
>
<font-awesome
:class="{
'text-blue-500 text-5xl mb-1': !wouldCreateConflict(activeDid),
'text-slate-400 text-5xl mb-1': wouldCreateConflict(activeDid)
}"
icon="hand"
/>
<h3
:class="{
'text-xs text-blue-500 font-medium text-ellipsis whitespace-nowrap overflow-hidden': !wouldCreateConflict(activeDid),
'text-xs text-slate-400 font-medium text-ellipsis whitespace-nowrap overflow-hidden': wouldCreateConflict(activeDid)
}"
>
You
</h3>
</li>
<li
class="cursor-pointer"
@click="
stepType === 'recipient' ? selectRecipient() : selectGiver()
"
>
<font-awesome
icon="circle-question"
class="text-slate-400 text-5xl mb-1"
/>
<h3
class="text-xs text-slate-400 font-medium italic text-ellipsis whitespace-nowrap overflow-hidden"
>
(Unnamed)
</h3>
</li>
<li
v-if="allContacts.length === 0"
class="text-xs text-slate-500 italic col-span-full"
>
(Add friends to see more people worthy of recognition.)
</li>
<li
v-for="contact in allContacts.slice(0, 10)"
:key="contact.did"
:class="{
'cursor-pointer': !wouldCreateConflict(contact.did),
'cursor-not-allowed opacity-50': wouldCreateConflict(contact.did)
}"
@click="
!wouldCreateConflict(contact.did) &&
(stepType === 'recipient'
? selectRecipient(contact)
: selectGiver(contact))
"
>
<div class="relative w-fit mx-auto mb-1">
<EntityIcon
:contact="contact"
class="!size-[3rem] mx-auto border border-slate-300 bg-white overflow-hidden rounded-full"
/>
<div
class="rounded-full bg-slate-400 absolute bottom-0 right-0 p-1 translate-x-1/3"
>
<font-awesome
icon="clock"
class="block text-white text-xs w-[1em]"
/>
</div>
</div>
<h3
:class="{
'text-xs font-medium text-ellipsis whitespace-nowrap overflow-hidden': !wouldCreateConflict(contact.did) && contact.name,
'text-xs font-medium text-ellipsis whitespace-nowrap overflow-hidden text-slate-400 italic': !contact.name,
'text-xs font-medium text-ellipsis whitespace-nowrap overflow-hidden text-slate-400': wouldCreateConflict(contact.did) && contact.name
}"
>
{{ contact.name || "(No name)" }}
</h3>
</li>
<li v-if="allContacts.length > 0" class="cursor-pointer">
<router-link
:to="{
name: 'contact-gift',
query: {
stepType: stepType,
giverEntityType: giverEntityType,
recipientEntityType: recipientEntityType,
...(stepType === 'giver'
? {
recipientProjectId: toProjectId,
recipientProjectName: receiver?.name,
recipientProjectImage: receiver?.image,
recipientProjectHandleId: receiver?.handleId,
recipientDid: receiver?.did,
}
: {
giverProjectId: fromProjectId,
giverProjectName: giver?.name,
giverProjectImage: giver?.image,
giverProjectHandleId: giver?.handleId,
giverDid: giver?.did,
}),
fromProjectId: fromProjectId,
toProjectId: toProjectId,
showProjects: (showProjects || false).toString(),
isFromProjectView: (isFromProjectView || false).toString(),
},
}"
>
<font-awesome
icon="circle-right"
class="text-blue-500 text-5xl mb-1"
/>
<h3
class="text-xs text-slate-400 font-medium italic text-ellipsis whitespace-nowrap overflow-hidden"
>
Show All
</h3>
</router-link>
</li>
</template>
</ul>
<button <button
class="block w-full text-center text-lg font-bold uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md" class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-lg"
@click="confirm"
>
Sign &amp; Send
</button>
<button
class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md"
@click="cancel" @click="cancel"
> >
Cancel Cancel
</button> </button>
</div> </div>
<!-- Step 2: Gift -->
<div v-show="!firstStep" id="sectionGiftedGift">
<div class="grid grid-cols-2 gap-2 mb-4">
<!-- Giver Button -->
<button
v-if="
(giverEntityType === 'person' || giverEntityType === 'project') &&
!(isFromProjectView && giverEntityType === 'project')
"
class="flex-1 flex items-center gap-2 bg-slate-100 border border-slate-300 rounded-md p-2"
@click="goBackToStep1('giver')"
>
<div>
<template v-if="giverEntityType === 'project'">
<ProjectIcon
v-if="giver?.handleId"
:entity-id="giver.handleId"
:icon-size="32"
:image-url="giver.image"
class="rounded-full bg-white overflow-hidden !size-[2rem] object-cover"
/>
</template>
<template v-else>
<EntityIcon
v-if="giver?.did"
:contact="giver"
class="rounded-full bg-white overflow-hidden !size-[2rem] object-cover"
/>
<font-awesome
v-else
icon="circle-question"
class="text-slate-400 text-3xl"
/>
</template>
</div>
<div class="text-start min-w-0">
<p class="text-xs text-slate-500 leading-1 -mb-1 uppercase">
{{
giverEntityType === "project"
? "Benefited from:"
: "Received from:"
}}
</p>
<h3
v-if="giver?.name && giver.name !== giver.did"
class="font-semibold truncate"
>
{{ giver.name }}
</h3>
<h3
v-if="giver?.name && giver.name === giver.did"
class="font-semibold truncate text-slate-400 italic"
>
(No name)
</h3>
<h3
v-else-if="!giver?.name"
class="font-semibold truncate text-slate-400 italic"
>
(Unnamed)
</h3>
</div>
<p class="ms-auto text-sm text-blue-500 pe-1">
<font-awesome icon="pen" title="Change" />
</p>
</button>
<div
v-else
class="flex-1 flex items-center gap-2 bg-slate-100 border border-slate-300 rounded-md p-2"
>
<div>
<template v-if="giverEntityType === 'project'">
<ProjectIcon
v-if="giver?.handleId"
:entity-id="giver.handleId"
:icon-size="32"
:image-url="giver.image"
class="rounded-full bg-white overflow-hidden !size-[2rem] object-cover"
/>
</template>
<template v-else>
<EntityIcon
v-if="giver?.did"
:contact="giver"
class="rounded-full bg-white overflow-hidden !size-[2rem] object-cover"
/>
<font-awesome
v-else
icon="circle-question"
class="text-slate-400 text-3xl"
/>
</template>
</div>
<div class="text-start min-w-0">
<p class="text-xs text-slate-500 leading-1 -mb-1 uppercase">
{{
giverEntityType === "project"
? "Benefited from:"
: "Received from:"
}}
</p>
<h3
v-if="giver?.name && giver.name !== giver.did"
class="font-semibold truncate"
>
{{ giver.name }}
</h3>
<h3
v-if="giver?.name && giver.name === giver.did"
class="font-semibold truncate text-slate-400 italic"
>
(No name)
</h3>
<h3
v-else-if="!giver?.name"
class="font-semibold truncate text-slate-400 italic"
>
(Unnamed)
</h3>
</div>
<p class="ms-auto text-sm text-slate-400 pe-1">
<font-awesome icon="lock" title="Can't be changed" />
</p>
</div>
<!-- Recipient Button -->
<button
v-if="recipientEntityType === 'person'"
class="flex-1 flex items-center gap-2 bg-slate-100 border border-slate-300 rounded-md p-2"
@click="goBackToStep1('recipient')"
>
<div>
<EntityIcon
v-if="receiver?.did"
:contact="receiver"
class="rounded-full bg-white overflow-hidden !size-[2rem] object-cover"
/>
<font-awesome
v-else
icon="circle-question"
class="text-slate-400 text-3xl"
/>
</div>
<div class="text-start min-w-0">
<p class="text-xs text-slate-500 leading-1 -mb-1 uppercase">
Given to:
</p>
<h3
v-if="receiver?.name && receiver.name !== receiver.did"
class="font-semibold truncate"
>
{{ receiver.name }}
</h3>
<h3
v-if="receiver?.name && receiver.name === receiver.did"
class="font-semibold truncate text-slate-400 italic"
>
(No name)
</h3>
<h3
v-else-if="!receiver?.name"
class="font-semibold truncate text-slate-400 italic"
>
(Unnamed)
</h3>
</div>
<p class="ms-auto text-sm text-blue-500 pe-1">
<font-awesome icon="pen" title="Change" />
</p>
</button>
<div
v-else-if="recipientEntityType === 'project'"
class="flex-1 flex items-center gap-2 bg-slate-100 border border-slate-300 rounded-md p-2"
>
<div>
<ProjectIcon
v-if="receiver?.handleId"
:entity-id="receiver.handleId"
:icon-size="32"
:image-url="receiver.image"
class="rounded-full bg-white overflow-hidden !size-[2rem] object-cover"
/>
</div>
<div class="text-start min-w-0">
<p class="text-xs text-slate-500 leading-1 -mb-1 uppercase">
Given to project:
</p>
<h3
v-if="receiver?.name"
class="font-semibold truncate"
>
{{ receiver.name }}
</h3>
<h3
v-else
class="font-semibold truncate text-slate-400 italic"
>
(Unnamed)
</h3>
</div>
<p class="ms-auto text-sm text-slate-400 pe-1">
<font-awesome icon="lock" title="Can't be changed" />
</p>
</div>
</div>
<input
v-model="description"
type="text"
class="block w-full rounded border border-slate-400 px-3 py-2 mb-4 placeholder:italic"
:placeholder="prompt || 'What was given?'"
/>
<div class="flex mb-4">
<button
class="rounded-s border border-e-0 border-slate-400 bg-slate-200 px-4 py-2"
@click="amountInput === '0' ? null : decrement()"
>
<font-awesome icon="chevron-left" />
</button>
<input
id="inputGivenAmount"
v-model="amountInput"
type="number"
class="flex-1 border border-e-0 border-slate-400 px-2 py-2 text-center w-[1px]"
/>
<button
class="rounded-e border border-slate-400 bg-slate-200 px-4 py-2"
@click="increment()"
>
<font-awesome icon="chevron-right" />
</button>
<select
v-model="unitCode"
class="flex-1 rounded border border-slate-400 ms-2 px-3 py-2"
>
<option
v-for="(displayName, code) in unitOptions"
:key="code"
:value="code"
>
{{ displayName }}
</option>
</select>
</div>
<router-link
:to="{
name: 'gifted-details',
query: giftedDetailsQuery,
}"
class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-lg mb-4"
>
Photo &amp; more options&hellip;
</router-link>
<p class="text-center text-sm mb-4">
<b class="font-medium">Sign &amp; Send</b> to publish to the world
<font-awesome
icon="circle-info"
class="fa-fw text-blue-500 text-base cursor-pointer"
@click="explainData()"
/>
</p>
<!-- Conflict warning -->
<div v-if="hasPersonConflict" class="mb-4 p-3 bg-red-50 border border-red-200 rounded-md">
<p class="text-red-700 text-sm text-center">
<font-awesome icon="exclamation-triangle" class="fa-fw mr-1" />
Cannot record: Same person selected as both giver and recipient
</p>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
<button
:disabled="hasPersonConflict"
:class="{
'block w-full text-center text-md uppercase 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 px-1.5 py-2 rounded-lg': !hasPersonConflict,
'block w-full text-center text-md uppercase font-bold bg-gradient-to-b from-slate-300 to-slate-500 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-slate-400 px-1.5 py-2 rounded-lg cursor-not-allowed': hasPersonConflict
}"
@click="confirm"
>
Sign &amp; Send
</button>
<button
class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-lg"
@click="cancel"
>
Cancel
</button>
</div>
</div>
</div> </div>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { Vue, Component, Prop } from "vue-facing-decorator"; import { Vue, Component, Prop, Watch } from "vue-facing-decorator";
import { NotificationIface, USE_DEXIE_DB } from "../constants/app"; import { NotificationIface, USE_DEXIE_DB } from "../constants/app";
import { import {
createAndSubmitGive, createAndSubmitGive,
didInfo, didInfo,
serverMessageForUser, serverMessageForUser,
getHeaders,
} from "../libs/endorserServer"; } from "../libs/endorserServer";
import * as libsUtil from "../libs/util"; import * as libsUtil from "../libs/util";
import { db, retrieveSettingsForActiveAccount } from "../db/index"; import { db, retrieveSettingsForActiveAccount } from "../db/index";
@@ -102,13 +537,38 @@ import * as databaseUtil from "../db/databaseUtil";
import { retrieveAccountDids } from "../libs/util"; import { retrieveAccountDids } from "../libs/util";
import { logger } from "../utils/logger"; import { logger } from "../utils/logger";
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory"; import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
import EntityIcon from "../components/EntityIcon.vue";
import ProjectIcon from "../components/ProjectIcon.vue";
import { PlanData } from "../interfaces/records";
@Component @Component({
components: {
EntityIcon,
ProjectIcon,
},
})
export default class GiftedDialog extends Vue { export default class GiftedDialog extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void; $notify!: (notification: NotificationIface, timeout?: number) => void;
@Prop() fromProjectId = ""; @Prop() fromProjectId = "";
@Prop() toProjectId = ""; @Prop() toProjectId = "";
@Prop({ default: false }) showProjects = false;
@Prop() isFromProjectView = false;
@Watch("showProjects")
onShowProjectsChange() {
this.updateEntityTypes();
}
@Watch("fromProjectId")
onFromProjectIdChange() {
this.updateEntityTypes();
}
@Watch("toProjectId")
onToProjectIdChange() {
this.updateEntityTypes();
}
activeDid = ""; activeDid = "";
allContacts: Array<Contact> = []; allContacts: Array<Contact> = [];
@@ -119,6 +579,7 @@ export default class GiftedDialog extends Vue {
callbackOnSuccess?: (amount: number) => void = () => {}; callbackOnSuccess?: (amount: number) => void = () => {};
customTitle?: string; customTitle?: string;
description = ""; description = "";
firstStep = true; // true = Step 1 (giver/recipient selection), false = Step 2 (amount/description)
giver?: libsUtil.GiverReceiverInputInfo; // undefined means no identified giver agent giver?: libsUtil.GiverReceiverInputInfo; // undefined means no identified giver agent
offerId = ""; offerId = "";
prompt = ""; prompt = "";
@@ -128,6 +589,80 @@ export default class GiftedDialog extends Vue {
libsUtil = libsUtil; libsUtil = libsUtil;
projects: PlanData[] = [];
didInfo = didInfo;
// Computed property to help debug template logic
get shouldShowProjects() {
const result =
(this.stepType === "giver" && this.giverEntityType === "project") ||
(this.stepType === "recipient" && this.recipientEntityType === "project");
return result;
}
// Computed property to check if current selection would create a conflict
get hasPersonConflict() {
// Only check for conflicts when both entities are persons
if (this.giverEntityType !== "person" || this.recipientEntityType !== "person") {
return false;
}
// Check if giver and recipient are the same person
if (this.giver?.did && this.receiver?.did && this.giver.did === this.receiver.did) {
return true;
}
return false;
}
// Computed property to check if a contact would create a conflict when selected
wouldCreateConflict(contactDid: string) {
// Only check for conflicts when both entities are persons
if (this.giverEntityType !== "person" || this.recipientEntityType !== "person") {
return false;
}
if (this.stepType === "giver") {
// If selecting as giver, check if it conflicts with current recipient
return this.receiver?.did === contactDid;
} else if (this.stepType === "recipient") {
// If selecting as recipient, check if it conflicts with current giver
return this.giver?.did === contactDid;
}
return false;
}
stepType = "giver";
giverEntityType = "person" as "person" | "project";
recipientEntityType = "person" as "person" | "project";
updateEntityTypes() {
// Reset and set entity types based on current context
this.giverEntityType = "person";
this.recipientEntityType = "person";
// Determine entity types based on current context
if (this.showProjects) {
// HomeView "Project" button or ProjectViewView "Given by This"
this.giverEntityType = "project";
this.recipientEntityType = "person";
} else if (this.fromProjectId) {
// ProjectViewView "Given by This" button (project is giver)
this.giverEntityType = "project";
this.recipientEntityType = "person";
} else if (this.toProjectId) {
// ProjectViewView "Given to This" button (project is recipient)
this.giverEntityType = "person";
this.recipientEntityType = "project";
} else {
// HomeView "Person" button
this.giverEntityType = "person";
this.recipientEntityType = "person";
}
}
async open( async open(
giver?: libsUtil.GiverReceiverInputInfo, giver?: libsUtil.GiverReceiverInputInfo,
receiver?: libsUtil.GiverReceiverInputInfo, receiver?: libsUtil.GiverReceiverInputInfo,
@@ -140,10 +675,14 @@ export default class GiftedDialog extends Vue {
this.giver = giver; this.giver = giver;
this.prompt = prompt || ""; this.prompt = prompt || "";
this.receiver = receiver; this.receiver = receiver;
// if we show "given to user" selection, default checkbox to true
this.amountInput = "0"; this.amountInput = "0";
this.callbackOnSuccess = callbackOnSuccess; this.callbackOnSuccess = callbackOnSuccess;
this.offerId = offerId || ""; this.offerId = offerId || "";
this.firstStep = !giver;
this.stepType = "giver";
// Update entity types based on current props
this.updateEntityTypes();
try { try {
let settings = await databaseUtil.retrieveSettingsForActiveAccount(); let settings = await databaseUtil.retrieveSettingsForActiveAccount();
@@ -174,7 +713,16 @@ export default class GiftedDialog extends Vue {
this.allContacts, this.allContacts,
); );
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if (
this.giverEntityType === "project" ||
this.recipientEntityType === "project"
) {
await this.loadProjects();
} else {
// Clear projects array when not needed
this.projects = [];
}
} catch (err: any) { } catch (err: any) {
logger.error("Error retrieving settings from database:", err); logger.error("Error retrieving settings from database:", err);
this.$notify( this.$notify(
@@ -224,6 +772,7 @@ export default class GiftedDialog extends Vue {
this.amountInput = "0"; this.amountInput = "0";
this.prompt = ""; this.prompt = "";
this.unitCode = "HUR"; this.unitCode = "HUR";
this.firstStep = true;
} }
async confirm() { async confirm() {
@@ -265,6 +814,20 @@ export default class GiftedDialog extends Vue {
); );
return; return;
} }
// Check for person conflict
if (this.hasPersonConflict) {
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "You cannot select the same person as both giver and recipient.",
},
3000,
);
return;
}
this.close(); this.close();
this.$notify( this.$notify(
@@ -304,20 +867,52 @@ export default class GiftedDialog extends Vue {
unitCode: string = "HUR", unitCode: string = "HUR",
) { ) {
try { try {
// Determine the correct parameters based on entity types
let fromDid: string | undefined;
let toDid: string | undefined;
let fulfillsProjectHandleId: string | undefined;
let providerPlanHandleId: string | undefined;
if (this.giverEntityType === "project" && this.recipientEntityType === "person") {
// Project-to-person gift
fromDid = undefined; // No person giver
toDid = recipientDid as string; // Person recipient
fulfillsProjectHandleId = undefined; // No project recipient
providerPlanHandleId = this.giver?.handleId; // Project giver
} else if (this.giverEntityType === "person" && this.recipientEntityType === "project") {
// Person-to-project gift
fromDid = giverDid as string; // Person giver
toDid = undefined; // No person recipient
fulfillsProjectHandleId = this.toProjectId; // Project recipient
providerPlanHandleId = undefined; // No project giver
} else if (this.giverEntityType === "project" && this.recipientEntityType === "project") {
// Project-to-project gift
fromDid = undefined; // No person giver
toDid = undefined; // No person recipient
fulfillsProjectHandleId = this.toProjectId; // Project recipient
providerPlanHandleId = this.giver?.handleId; // Project giver
} else {
// Person-to-person gift
fromDid = giverDid as string;
toDid = recipientDid as string;
fulfillsProjectHandleId = undefined;
providerPlanHandleId = undefined;
}
const result = await createAndSubmitGive( const result = await createAndSubmitGive(
this.axios, this.axios,
this.apiServer, this.apiServer,
this.activeDid, this.activeDid,
giverDid as string, fromDid,
recipientDid as string, toDid,
description, description,
amount, amount,
unitCode, unitCode,
this.toProjectId, fulfillsProjectHandleId,
this.offerId, this.offerId,
false, false,
undefined, undefined,
this.fromProjectId, providerPlanHandleId,
); );
if (!result.success) { if (!result.success) {
@@ -378,6 +973,118 @@ export default class GiftedDialog extends Vue {
-1, -1,
); );
} }
selectGiver(contact?: Contact) {
if (contact) {
this.giver = {
did: contact.did,
name: contact.name || contact.did,
};
} else {
this.giver = {
did: "",
name: "",
};
}
this.firstStep = false;
}
goBackToStep1(step: string) {
this.stepType = step;
this.firstStep = true;
}
async loadProjects() {
try {
const response = await fetch(this.apiServer + "/api/v2/report/plans", {
method: "GET",
headers: await getHeaders(this.activeDid),
});
if (response.status !== 200) {
throw new Error("Failed to load projects");
}
const results = await response.json();
if (results.data) {
this.projects = results.data;
}
} catch (error) {
logger.error("Error loading projects:", error);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "Failed to load projects",
},
3000,
);
}
}
selectProject(project: PlanData) {
this.giver = {
name: project.name,
image: project.image,
handleId: project.handleId,
};
this.receiver = {
did: this.activeDid,
name: "You",
};
this.firstStep = false;
}
selectRecipient(contact?: Contact) {
if (contact) {
this.receiver = {
did: contact.did,
name: contact.name || contact.did,
};
} else {
this.receiver = {
did: "",
name: "",
};
}
this.firstStep = false;
}
selectRecipientProject(project: PlanData) {
this.receiver = {
// no did, because it's a project
name: project.name,
image: project.image,
handleId: project.handleId,
};
this.firstStep = false;
}
// Computed property for the query parameters
get giftedDetailsQuery() {
return {
amountInput: this.amountInput,
description: this.description,
giverDid: this.giverEntityType === "person" ? this.giver?.did : undefined,
giverName: this.giver?.name,
offerId: this.offerId,
fulfillsProjectId: this.giverEntityType === "person" && this.recipientEntityType === "project"
? this.toProjectId
: undefined,
providerProjectId: this.giverEntityType === "project" && this.recipientEntityType === "person"
? this.giver?.handleId
: this.fromProjectId,
recipientDid: this.receiver?.did,
recipientName: this.receiver?.name,
unitCode: this.unitCode,
};
}
// Computed property to get unit options
get unitOptions() {
return this.libsUtil.UNIT_SHORT;
}
} }
</script> </script>

View File

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

View File

@@ -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 = [

View File

@@ -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" ? "/" : "./";
}, },
}); });

View File

@@ -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];

View File

@@ -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";
/** /**
@@ -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) {

View File

@@ -29,6 +29,7 @@ import {
faCircleCheck, faCircleCheck,
faCircleInfo, faCircleInfo,
faCircleQuestion, faCircleQuestion,
faCircleRight,
faCircleUser, faCircleUser,
faClock, faClock,
faCoins, faCoins,
@@ -60,6 +61,7 @@ import {
faLightbulb, faLightbulb,
faLink, faLink,
faLocationDot, faLocationDot,
faLock,
faLongArrowAltLeft, faLongArrowAltLeft,
faLongArrowAltRight, faLongArrowAltRight,
faMagnifyingGlass, faMagnifyingGlass,
@@ -79,6 +81,7 @@ import {
faSquareCaretDown, faSquareCaretDown,
faSquareCaretUp, faSquareCaretUp,
faSquarePlus, faSquarePlus,
faThumbtack,
faTrashCan, faTrashCan,
faTriangleExclamation, faTriangleExclamation,
faUser, faUser,
@@ -111,6 +114,7 @@ library.add(
faCircleCheck, faCircleCheck,
faCircleInfo, faCircleInfo,
faCircleQuestion, faCircleQuestion,
faCircleRight,
faCircleUser, faCircleUser,
faClock, faClock,
faCoins, faCoins,
@@ -142,6 +146,7 @@ library.add(
faLightbulb, faLightbulb,
faLink, faLink,
faLocationDot, faLocationDot,
faLock,
faLongArrowAltLeft, faLongArrowAltLeft,
faLongArrowAltRight, faLongArrowAltRight,
faMagnifyingGlass, faMagnifyingGlass,
@@ -161,6 +166,7 @@ library.add(
faSquareCaretDown, faSquareCaretDown,
faSquareCaretUp, faSquareCaretUp,
faSquarePlus, faSquarePlus,
faThumbtack,
faTrashCan, faTrashCan,
faTriangleExclamation, faTriangleExclamation,
faUser, faUser,

View File

@@ -50,6 +50,8 @@ import { DEFAULT_ROOT_DERIVATION_PATH } from "./crypto";
export interface GiverReceiverInputInfo { export interface GiverReceiverInputInfo {
did?: string; did?: string;
name?: string; name?: string;
image?: string;
handleId?: string;
} }
export enum OnboardPage { export enum OnboardPage {
@@ -605,7 +607,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,

View File

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

View File

@@ -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() {

View File

@@ -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;

View File

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

View File

@@ -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);

View File

@@ -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
@@ -1573,25 +1573,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);
// } }
} }
/** /**

View File

@@ -4,14 +4,14 @@
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto"> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Breadcrumb --> <!-- Breadcrumb -->
<div id="ViewBreadcrumb" class="mb-8"> <div id="ViewBreadcrumb" class="mb-8">
<h1 class="text-lg text-center font-light relative px-7"> <h1 class="text-2xl text-center font-semibold relative px-7">
<!-- Back --> <!-- Back -->
<router-link <router-link
:to="{ name: 'home' }" :to="{ name: 'home' }"
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1" class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
><font-awesome icon="chevron-left" class="fa-fw"></font-awesome> ><font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
</router-link> </router-link>
Given by... {{ stepType === "giver" ? "Given by..." : "Given to..." }}
</h1> </h1>
</div> </div>
@@ -19,19 +19,18 @@
<ul class="border-t border-slate-300"> <ul class="border-t border-slate-300">
<li class="border-b border-slate-300 py-3"> <li class="border-b border-slate-300 py-3">
<h2 class="text-base flex gap-4 items-center"> <h2 class="text-base flex gap-4 items-center">
<span class="grow"> <span class="grow flex gap-2 items-center font-medium">
<img <font-awesome
src="../assets/blank-square.svg" icon="circle-question"
width="32" class="text-slate-400 text-4xl"
class="inline-block align-middle border border-slate-300 rounded-md mr-1"
/> />
Unnamed/Unknown <span class="italic text-slate-400">(Unnamed/Unknown)</span>
</span> </span>
<span class="text-right"> <span class="text-right">
<button <button
type="button" type="button"
class="block w-full text-center text-sm uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-3 py-1.5 rounded-md" class="block w-full text-center text-sm uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-3 py-1.5 rounded-md"
@click="openDialog()" @click="openDialog('Unnamed')"
> >
<font-awesome icon="gift" class="fa-fw"></font-awesome> <font-awesome icon="gift" class="fa-fw"></font-awesome>
</button> </button>
@@ -44,13 +43,14 @@
class="border-b border-slate-300 py-3" class="border-b border-slate-300 py-3"
> >
<h2 class="text-base flex gap-4 items-center"> <h2 class="text-base flex gap-4 items-center">
<span class="grow font-semibold"> <span class="grow flex gap-2 items-center font-medium">
<EntityIcon <EntityIcon
:contact="contact" :contact="contact"
:icon-size="32" :icon-size="34"
class="inline-block align-middle border border-slate-300 rounded-md mr-1" class="inline-block align-middle border border-slate-300 rounded-full overflow-hidden"
/> />
{{ contact.name || "(no name)" }} <span v-if="contact.name">{{ contact.name }}</span>
<span v-else class="italic text-slate-400">(No name)</span>
</span> </span>
<span class="text-right"> <span class="text-right">
<button <button
@@ -65,7 +65,13 @@
</li> </li>
</ul> </ul>
<GiftedDialog ref="customDialog" :to-project-id="projectId" /> <GiftedDialog
ref="customDialog"
:from-project-id="fromProjectId"
:to-project-id="toProjectId"
:show-projects="showProjects"
:is-from-project-view="isFromProjectView"
/>
</section> </section>
</template> </template>
@@ -97,6 +103,24 @@ export default class ContactGiftingView extends Vue {
description = ""; description = "";
projectId = ""; projectId = "";
prompt = ""; prompt = "";
recipientProjectName = "";
recipientProjectImage = "";
recipientProjectHandleId = "";
// New context parameters
stepType = "giver";
giverEntityType = "person" as "person" | "project";
recipientEntityType = "person" as "person" | "project";
giverProjectId = "";
giverProjectName = "";
giverProjectImage = "";
giverProjectHandleId = "";
giverDid = "";
recipientDid = "";
fromProjectId = "";
toProjectId = "";
showProjects = false;
isFromProjectView = false;
async created() { async created() {
try { try {
@@ -124,9 +148,41 @@ export default class ContactGiftingView extends Vue {
); );
} }
this.projectId = (this.$route.query["projectId"] as string) || ""; this.projectId =
(this.$route.query["recipientProjectId"] as string) || "";
this.recipientProjectName =
(this.$route.query["recipientProjectName"] as string) || "";
this.recipientProjectImage =
(this.$route.query["recipientProjectImage"] as string) || "";
this.recipientProjectHandleId =
(this.$route.query["recipientProjectHandleId"] as string) || "";
this.prompt = (this.$route.query["prompt"] as string) ?? this.prompt; this.prompt = (this.$route.query["prompt"] as string) ?? this.prompt;
// Read new context parameters
this.stepType = (this.$route.query["stepType"] as string) || "giver";
this.giverEntityType =
(this.$route.query["giverEntityType"] as "person" | "project") ||
"person";
this.recipientEntityType =
(this.$route.query["recipientEntityType"] as "person" | "project") ||
"person";
this.giverProjectId =
(this.$route.query["giverProjectId"] as string) || "";
this.giverProjectName =
(this.$route.query["giverProjectName"] as string) || "";
this.giverProjectImage =
(this.$route.query["giverProjectImage"] as string) || "";
this.giverProjectHandleId =
(this.$route.query["giverProjectHandleId"] as string) || "";
this.giverDid = (this.$route.query["giverDid"] as string) || "";
this.recipientDid = (this.$route.query["recipientDid"] as string) || "";
this.fromProjectId = (this.$route.query["fromProjectId"] as string) || "";
this.toProjectId = (this.$route.query["toProjectId"] as string) || "";
this.showProjects =
(this.$route.query["showProjects"] as string) === "true";
this.isFromProjectView =
(this.$route.query["isFromProjectView"] as string) === "true";
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) { } catch (err: any) {
logger.error("Error retrieving settings & contacts:", err); logger.error("Error retrieving settings & contacts:", err);
@@ -144,17 +200,108 @@ export default class ContactGiftingView extends Vue {
} }
} }
openDialog(giver?: GiverReceiverInputInfo) { openDialog(contact?: GiverReceiverInputInfo | "Unnamed") {
const recipient = this.projectId if (contact === "Unnamed") {
? undefined // Special case: Pass undefined to trigger Step 1, but with "Unnamed" pre-selected
: { did: this.activeDid, name: "you" }; let recipient: GiverReceiverInputInfo;
(this.$refs.customDialog as GiftedDialog).open( let giver: GiverReceiverInputInfo | undefined;
giver,
recipient, if (this.stepType === "giver") {
undefined, // We're selecting a giver, so recipient is either a project or the current user
"Given by " + (giver?.name || "someone not named"), if (this.recipientEntityType === "project") {
this.prompt, recipient = {
); did: this.recipientProjectHandleId,
name: this.recipientProjectName,
image: this.recipientProjectImage,
handleId: this.recipientProjectHandleId,
};
} else {
recipient = { did: this.activeDid, name: "You" };
}
giver = undefined; // Will be set to "Unnamed" in GiftedDialog
} else {
// We're selecting a recipient, so recipient is "Unnamed" and giver is preserved from context
recipient = { did: "", name: "Unnamed" };
// Preserve the existing giver from the context
if (this.giverEntityType === "project") {
giver = {
// no did, because it's a project
name: this.giverProjectName,
image: this.giverProjectImage,
handleId: this.giverProjectHandleId,
};
} else if (this.giverDid) {
giver = {
did: this.giverDid,
name: this.giverProjectName || "Someone",
};
} else {
giver = { did: this.activeDid, name: "You" };
}
}
(this.$refs.customDialog as GiftedDialog).open(
giver,
recipient,
undefined,
this.stepType === "giver" ? "Given by Unnamed" : "Given to Unnamed",
this.prompt,
);
// Immediately select "Unnamed" and move to Step 2
(this.$refs.customDialog as GiftedDialog).selectGiver();
} else {
// Regular case: contact is a GiverReceiverInputInfo
let giver: GiverReceiverInputInfo;
let recipient: GiverReceiverInputInfo;
if (this.stepType === "giver") {
// We're selecting a giver, so the contact becomes the giver
giver = contact as GiverReceiverInputInfo; // Safe because we know contact is not "Unnamed" or undefined
// Recipient is either a project or the current user
if (this.recipientEntityType === "project") {
recipient = {
did: this.recipientProjectHandleId,
name: this.recipientProjectName,
image: this.recipientProjectImage,
handleId: this.recipientProjectHandleId,
};
} else {
recipient = { did: this.activeDid, name: "You" };
}
} else {
// We're selecting a recipient, so the contact becomes the recipient
recipient = contact as GiverReceiverInputInfo; // Safe because we know contact is not "Unnamed" or undefined
// Preserve the existing giver from the context
if (this.giverEntityType === "project") {
giver = {
did: this.giverProjectHandleId,
name: this.giverProjectName,
image: this.giverProjectImage,
handleId: this.giverProjectHandleId,
};
} else if (this.giverDid) {
giver = {
did: this.giverDid,
name: this.giverProjectName || "Someone",
};
} else {
giver = { did: this.activeDid, name: "You" };
}
}
(this.$refs.customDialog as GiftedDialog).open(
giver,
recipient,
undefined,
this.stepType === "giver"
? "Given by " + (contact?.name || "someone not named")
: "Given to " + (contact?.name || "someone not named"),
this.prompt,
);
}
} }
} }
</script> </script>

View File

@@ -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();

View File

@@ -4,84 +4,91 @@
<!-- CONTENT --> <!-- CONTENT -->
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto"> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Back --> <!-- Breadcrumb -->
<div <div id="ViewBreadcrumb" class="mb-8">
v-if="!hideBackButton" <h1 class="text-2xl text-center font-semibold relative px-7 mb-2">
class="text-lg text-center font-light relative px-7" <!-- Back -->
> <div
<h1 v-if="!hideBackButton"
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1" class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="cancelBack()" @click="cancelBack()"
> >
<font-awesome icon="chevron-left" class="fa-fw"></font-awesome> <font-awesome icon="chevron-left" class="fa-fw" />
</div>
What Was Given
</h1> </h1>
<h2 class="text-lg font-normal text-center overflow-hidden">
<div class="truncate">
From
{{
providedByProject
? providerProjectName
: providedByGiver
? giverName
: "someone not named"
}}
</div>
<div class="truncate">
to
{{
givenToProject
? fulfillsProjectName
: givenToRecipient
? recipientName
: "someone not named"
}}
</div>
</h2>
</div> </div>
<!-- Heading -->
<h1 class="text-4xl text-center font-light px-4 mb-4">What Was Given</h1>
<h1 class="text-xl font-bold text-center mb-4">
<span>
From
{{
providedByProject
? providerProjectName
: providedByGiver
? giverName
: "someone not named"
}}
</span>
<br />
<span>
to
{{
givenToProject
? fulfillsProjectName
: givenToRecipient
? recipientName
: "someone not named"
}}</span
>
</h1>
<textarea <textarea
v-model="description" v-model="description"
class="block w-full rounded border border-slate-400 mb-2 px-3 py-2" class="block w-full rounded border border-slate-400 mb-2 px-3 py-2"
placeholder="What was received" placeholder="What was received"
/> />
<div class="flex flex-row justify-center"> <div class="flex mb-4">
<span <button
class="rounded-l border border-r-0 border-slate-400 bg-slate-200 text-center text-blue-500 px-2 py-2 w-20" class="rounded-s border border-e-0 border-slate-400 bg-slate-200 px-4 py-2"
@click="changeUnitCode()"
>
{{ libsUtil.UNIT_SHORT[unitCode] || unitCode }}
</span>
<div
class="border border-r-0 border-slate-400 bg-slate-200 px-4 py-2"
@click="amountInput === '0' ? null : decrement()" @click="amountInput === '0' ? null : decrement()"
> >
<font-awesome icon="chevron-left" /> <font-awesome icon="chevron-left" />
</div> </button>
<input <input
id="inputGivenAmount"
v-model="amountInput" v-model="amountInput"
type="number" type="number"
class="border border-r-0 border-slate-400 px-2 py-2 text-center w-20" class="flex-1 border border-e-0 border-slate-400 px-2 py-2 text-center w-[1px]"
/> />
<div <button
class="rounded-r border border-slate-400 bg-slate-200 px-4 py-2" class="rounded-e border border-slate-400 bg-slate-200 px-4 py-2"
@click="increment()" @click="increment()"
> >
<font-awesome icon="chevron-right" /> <font-awesome icon="chevron-right" />
</div> </button>
<select
v-model="unitCode"
class="flex-1 rounded border border-slate-400 ms-2 px-3 py-2"
>
<option
v-for="(displayName, code) in unitOptions"
:key="code"
:value="code"
>
{{ displayName }}
</option>
</select>
</div> </div>
<div class="flex justify-center mt-4" data-testId="imagery"> <div class="flex justify-center mt-4" data-testId="imagery">
<span v-if="imageUrl" class="flex justify-between"> <span v-if="imageUrl" class="flex items-end gap-3">
<a :href="imageUrl" target="_blank"> <a :href="imageUrl" target="_blank">
<img :src="imageUrl" class="h-24 rounded-xl" /> <img :src="imageUrl" class="h-36 rounded-lg" />
</a> </a>
<font-awesome <font-awesome
icon="trash-can" icon="trash-can"
class="text-red-500 fa-fw ml-8 mt-10" class="text-red-500 fa-fw cursor-pointer"
@click="confirmDeleteImage" @click="confirmDeleteImage"
/> />
</span> </span>
@@ -95,22 +102,22 @@
</div> </div>
<ImageMethodDialog ref="imageDialog" default-camera-mode="environment" /> <ImageMethodDialog ref="imageDialog" default-camera-mode="environment" />
<div class="mt-4 flex justify-between gap-2"> <div class="mt-4 sm:flex justify-between gap-2">
<!-- First Column for Giver --> <!-- First Column for Giver -->
<div class="flex-grow border border-slate-400 p-2 rounded-md"> <div class="sm:flex-grow sm:w-1/2 border border-slate-400 p-2 rounded-md overflow-hidden">
<div class="flex"> <div class="flex items-center">
<input <input
v-if="giverDid && !providedByProject" v-if="giverDid && !providedByProject"
v-model="providedByGiver" v-model="providedByGiver"
type="checkbox" type="checkbox"
class="h-6 w-6 mr-2" class="flex-shrink-0 h-6 w-6 mr-2"
/> />
<font-awesome <font-awesome
v-else v-else
icon="square" icon="square"
class="mr-2 bg-white text-white h-5 w-5 px-0.5 py-0.5 rounded-sm" class="mr-2 bg-white text-white h-5 w-5 px-0.5 py-0.5 rounded-sm"
/> />
<label class="text-sm mt-1"> <label class="text-sm truncate">
{{ {{
giverDid giverDid
? "This was provided by " + giverName + "." ? "This was provided by " + giverName + "."
@@ -120,24 +127,24 @@
<font-awesome <font-awesome
v-if="!giverDid || providedByProject" v-if="!giverDid || providedByProject"
icon="info-circle" icon="info-circle"
class="-mt-1 bg-white text-slate-500 h-5 w-5 px-0.5 py-0.5 rounded-sm" class="text-base cursor-pointer bg-white text-slate-500 ms-1"
@click="notifyUserOfGiver()" @click="notifyUserOfGiver()"
/> />
</div> </div>
<div class="flex"> <div class="flex items-center">
<input <input
v-if="providerProjectId && !providedByGiver" v-if="providerProjectId && !providedByGiver"
v-model="providedByProject" v-model="providedByProject"
type="checkbox" type="checkbox"
class="h-6 w-6 mr-2" class="flex-shrink-0 h-6 w-6 mr-2"
/> />
<font-awesome <font-awesome
v-else v-else
icon="square" icon="square"
class="mr-2 bg-white text-white h-5 w-5 px-0.5 py-0.5 rounded-sm" class="mr-2 bg-white text-white h-5 w-5 px-0.5 py-0.5 rounded-sm"
/> />
<label class="text-sm mt-1"> <label class="text-sm truncate">
{{ {{
providerProjectId providerProjectId
? "This was provided by " + providerProjectName + "." ? "This was provided by " + providerProjectName + "."
@@ -147,31 +154,31 @@
<font-awesome <font-awesome
v-if="!providerProjectId || providedByGiver" v-if="!providerProjectId || providedByGiver"
icon="info-circle" icon="info-circle"
class="-mt-1 bg-white text-slate-500 h-5 w-5 px-0.5 py-0.5 rounded-sm" class="text-base cursor-pointer bg-white text-slate-500 ms-1"
@click="notifyUserOfProvidingProject()" @click="notifyUserOfProvidingProject()"
/> />
</div> </div>
</div> </div>
<div class="flex-shrink flex justify-center items-center"> <div class="sm:flex-shrink flex justify-center items-center my-1 sm:my-0">
<font-awesome icon="arrow-right" class="fa-fw h-7" /> <font-awesome icon="arrow-right" class="fa-fw h-7 rotate-90 sm:rotate-0" />
</div> </div>
<!-- Third Column for Recipient --> <!-- Third Column for Recipient -->
<div class="flex-grow border border-slate-400 p-2 rounded-md"> <div class="sm:flex-grow sm:w-1/2 border border-slate-400 p-2 rounded-md overflow-hidden">
<div class="flex"> <div class="flex items-center">
<input <input
v-if="recipientDid && !givenToProject" v-if="recipientDid && !givenToProject"
v-model="givenToRecipient" v-model="givenToRecipient"
type="checkbox" type="checkbox"
class="h-6 w-6 mr-2" class="flex-shrink-0 h-6 w-6 mr-2"
/> />
<font-awesome <font-awesome
v-else v-else
icon="square" icon="square"
class="mr-2 bg-white text-white h-5 w-5 px-0.5 py-0.5 rounded-sm" class="mr-2 bg-white text-white h-5 w-5 px-0.5 py-0.5 rounded-sm"
/> />
<label class="text-sm mt-1"> <label class="text-sm truncate">
{{ {{
recipientDid recipientDid
? "This was given to " + recipientName + "." ? "This was given to " + recipientName + "."
@@ -181,24 +188,24 @@
<font-awesome <font-awesome
v-if="!recipientDid || givenToProject" v-if="!recipientDid || givenToProject"
icon="info-circle" icon="info-circle"
class="-mt-1 bg-white text-slate-500 h-5 w-5 px-0.5 py-0.5 rounded-sm" class="text-base cursor-pointer bg-white text-slate-500 ms-1"
@click="notifyUserOfRecipient()" @click="notifyUserOfRecipient()"
/> />
</div> </div>
<div class="flex"> <div class="flex items-center">
<input <input
v-if="fulfillsProjectId && !givenToRecipient" v-if="fulfillsProjectId && !givenToRecipient"
v-model="givenToProject" v-model="givenToProject"
type="checkbox" type="checkbox"
class="h-6 w-6 mr-2" class="flex-shrink-0 h-6 w-6 mr-2"
/> />
<font-awesome <font-awesome
v-else v-else
icon="square" icon="square"
class="mr-2 bg-white text-white h-5 w-5 px-0.5 py-0.5 rounded-sm" class="mr-2 bg-white text-white h-5 w-5 px-0.5 py-0.5 rounded-sm"
/> />
<label class="text-sm mt-1"> <label class="text-sm truncate">
{{ {{
fulfillsProjectId fulfillsProjectId
? "This was given to " + fulfillsProjectName + ". " ? "This was given to " + fulfillsProjectName + ". "
@@ -208,7 +215,7 @@
<font-awesome <font-awesome
v-if="!fulfillsProjectId || givenToRecipient" v-if="!fulfillsProjectId || givenToRecipient"
icon="info-circle" icon="info-circle"
class="-mt-1 bg-white text-slate-500 h-5 w-5 px-0.5 py-0.5 rounded-sm" class="text-base cursor-pointer bg-white text-slate-500 ms-1"
@click="notifyUserFulfillsProject()" @click="notifyUserFulfillsProject()"
/> />
</div> </div>
@@ -229,11 +236,11 @@
</router-link> </router-link>
</div> </div>
<p class="text-center mb-2 mt-6 italic"> <p class="text-center text-sm my-4">
Sign & Send to publish to the world <b class="font-medium">Sign &amp; Send</b> to publish to the world
<font-awesome <font-awesome
icon="circle-info" icon="circle-info"
class="pl-2 text-blue-500 cursor-pointer" class="fa-fw text-blue-500 text-base cursor-pointer"
@click="explainData()" @click="explainData()"
/> />
</p> </p>
@@ -910,5 +917,10 @@ export default class GiftedDetails extends Vue {
7000, 7000,
); );
} }
// Computed property to get unit options
get unitOptions() {
return this.libsUtil.UNIT_SHORT;
}
} }
</script> </script>

View File

@@ -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" />
@@ -117,101 +118,73 @@ Raymer * @version 1.0.0 */
</div> </div>
<div v-else id="sectionRecordSomethingGiven"> <div v-else id="sectionRecordSomethingGiven">
<!-- !isCreatingIdentifier && isRegistered --> <!-- Record Quick-Action -->
<div class="mb-6">
<div class="flex gap-2 items-center mb-2">
<h2 class="text-xl font-bold">Record something given by:</h2>
<button
class="block ms-auto text-center text-white bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-2 rounded-full"
@click="openGiftedPrompts()"
>
<font-awesome
icon="lightbulb"
class="block text-center w-[1em]"
/>
</button>
</div>
<!-- show the actions for recognizing a give --> <div class="grid grid-cols-2 gap-2">
<div class="flex"> <button
<h2 class="text-xl font-bold">What have you seen someone do?</h2> type="button"
<button class="text-center text-base uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-3 py-2 rounded-lg"
class="ml-2 block text-xs text-center bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1 rounded-md" @click="openDialogPerson()"
@click="openGiftedPrompts()" >
> <font-awesome icon="user" />
<font-awesome icon="lightbulb" class="fa-fw" /> Person
</button> </button>
<button
type="button"
class="text-center text-base uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-3 py-2 rounded-lg"
@click="openProjectDialog()"
>
<font-awesome icon="folder-open" />
Project
</button>
</div>
</div> </div>
<ul
class="grid grid-cols-4 sm:grid-cols-5 md:grid-cols-6 gap-x-3 gap-y-5 text-center mt-4"
>
<li @click="openDialog()">
<img
src="../assets/blank-square.svg"
class="mx-auto border border-blue-500 rounded-md mb-1 cursor-pointer"
/>
<h3
class="text-xs text-blue-500 italic font-medium text-ellipsis whitespace-nowrap overflow-hidden cursor-pointer"
>
Unnamed/Unknown
</h3>
</li>
<li v-if="allContacts.length === 0" class="text-sm">
(Add friends to see more people worthy of recognition.)
</li>
<li
v-for="contact in allContacts.slice(0, 6)"
:key="contact.did"
@click="openDialog(contact)"
>
<EntityIcon
:contact="contact"
:icon-size="64"
class="mx-auto border border-blue-500 rounded-md mb-1 cursor-pointer"
/>
<h3
class="text-xs text-blue-500 font-medium text-ellipsis whitespace-nowrap overflow-hidden cursor-pointer"
>
{{ contact.name || contact.did }}
</h3>
</li>
<li>
<router-link
v-if="allContacts.length >= 6"
:to="{ name: 'contact-gift' }"
class="flex align-bottom text-xs text-blue-500 mt-12 cursor-pointer"
>
... or someone else...
</router-link>
</li>
</ul>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<GiftedDialog ref="customDialog" /> <GiftedDialog ref="customDialog" :show-projects="showProjectsDialog" />
<GiftedPrompts ref="giftedPrompts" /> <GiftedPrompts ref="giftedPrompts" />
<FeedFilters ref="feedFilters" /> <FeedFilters ref="feedFilters" />
<div class="relative">
<button
v-if="isRegistered"
class="absolute right-6 bottom-0 transform translate-y-1/2 text-center text-4xl leading-none bg-green-600 text-white w-14 py-2.5 rounded-full"
@click="openDialog()"
>
<font-awesome icon="plus" class="fa-fw" />
</button>
</div>
<!-- Results List --> <!-- Results List -->
<div class="mt-4 mb-4"> <div class="mt-4 mb-4">
<div class="flex items-center mb-4"> <div class="flex gap-2 items-center mb-3">
<h2 class="text-xl font-bold flex items-center gap-4"> <h2 class="text-xl font-bold">Latest Activity</h2>
Latest Activity <button
<button v-if="resultsAreFiltered()"
v-if="resultsAreFiltered()" class="block ms-auto text-center text-white bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-2 rounded-full"
class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] px-3 py-1.5 rounded-md text-xs text-white" @click="openFeedFilters()"
@click="openFeedFilters()" >
> <font-awesome
<font-awesome icon="filter" class="fa-fw" /> icon="filter"
</button> class="block text-center w-[1em] translate-y-[0.05em]"
<button />
v-else </button>
class="bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] px-3 py-1.5 rounded-md text-xs text-white" <button
@click="openFeedFilters()" v-else
> class="block ms-auto text-center text-white bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] p-2 rounded-full"
<font-awesome icon="filter" class="fa-fw" /> @click="openFeedFilters()"
</button> >
</h2> <font-awesome
icon="filter"
class="block text-center w-[1em] translate-y-[0.05em]"
/>
</button>
</div> </div>
<div <div
@@ -477,6 +450,7 @@ export default class HomeView extends Vue {
selectedImageData: Blob | null = null; selectedImageData: Blob | null = null;
isImageViewerOpen = false; isImageViewerOpen = false;
imageCache: Map<string, Blob | null> = new Map(); imageCache: Map<string, Blob | null> = new Map();
showProjectsDialog = false;
/** /**
* Initializes the component on mount * Initializes the component on mount
@@ -682,7 +656,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,
); );
@@ -1625,17 +1599,33 @@ export default class HomeView extends Vue {
* @param giver Optional contact info for giver * @param giver Optional contact info for giver
* @param description Optional gift description * @param description Optional gift description
*/ */
openDialog(giver?: GiverReceiverInputInfo, description?: string) { openDialog(giver?: GiverReceiverInputInfo | "Unnamed", description?: string) {
(this.$refs.customDialog as GiftedDialog).open( if (giver === "Unnamed") {
giver, // Special case: Pass undefined to trigger Step 1, but with "Unnamed" pre-selected
{ (this.$refs.customDialog as GiftedDialog).open(
did: this.activeDid, undefined,
name: "you", {
} as GiverReceiverInputInfo, did: this.activeDid,
undefined, name: "You",
"Given by " + (giver?.name || "someone not named"), } as GiverReceiverInputInfo,
description, undefined,
); "Given by Unnamed",
description,
);
// Immediately select "Unnamed" and move to Step 2
(this.$refs.customDialog as GiftedDialog).selectGiver();
} else {
(this.$refs.customDialog as GiftedDialog).open(
giver,
{
did: this.activeDid,
name: "You",
} as GiverReceiverInputInfo,
undefined,
"Given by " + (giver?.name || "someone not named"),
description,
);
}
} }
/** /**
@@ -1869,5 +1859,18 @@ export default class HomeView extends Vue {
this.$router.push({ name: "contact-qr" }); this.$router.push({ name: "contact-qr" });
} }
} }
openDialogPerson(
giver?: GiverReceiverInputInfo | "Unnamed",
description?: string,
) {
this.showProjectsDialog = false;
this.openDialog(giver, description);
}
openProjectDialog() {
this.showProjectsDialog = true;
(this.$refs.customDialog as any).open();
}
} }
</script> </script>

View File

@@ -214,63 +214,11 @@
</div> </div>
</div> </div>
<div v-if="activeDid && isRegistered"> <GiftedDialog
<div class="text-center"> ref="giveDialogToThis"
<p class="mt-2 mt-4 text-center">Record a contribution from:</p> :to-project-id="projectId"
</div> :is-from-project-view="true"
<ul />
class="grid grid-cols-4 sm:grid-cols-5 md:grid-cols-6 gap-x-3 gap-y-5 text-center mb-5 mt-2"
>
<li @click="openGiftDialogToProject({ name: 'you', did: activeDid })">
<font-awesome
icon="hand"
class="fa-fw text-blue-500 text-5xl cursor-pointer"
/>
<h3
class="mt-5 text-xs text-blue-500 font-medium text-ellipsis whitespace-nowrap overflow-hidden cursor-pointer"
>
You
</h3>
</li>
<li @click="openGiftDialogToProject()">
<img
src="../assets/blank-square.svg"
class="mx-auto border border-blue-300 rounded-md mb-1 cursor-pointer"
/>
<h3
class="text-xs text-blue-500 italic font-medium text-ellipsis whitespace-nowrap overflow-hidden cursor-pointer"
>
Unnamed/Unknown
</h3>
</li>
<li
v-for="contact in allContacts.slice(0, 5)"
:key="contact.did"
@click="openGiftDialogToProject(contact)"
>
<EntityIcon
:contact="contact"
:icon-size="64"
class="mx-auto border border-blue-300 rounded-md mb-1 cursor-pointer"
/>
<h3
class="text-xs text-blue-500 font-medium text-ellipsis whitespace-nowrap overflow-hidden cursor-pointer"
>
{{ contact.name || "(no name)" }}
</h3>
</li>
<li>
<span
v-if="allContacts.length >= 5"
class="flex align-bottom text-xs text-blue-500 mt-12 cursor-pointer"
@click="onClickAllContactsGifting()"
>
... or someone else...
</span>
</li>
</ul>
</div>
<GiftedDialog ref="giveDialogToThis" :to-project-id="projectId" />
<!-- Offers & Gifts to & from this --> <!-- Offers & Gifts to & from this -->
<div class="grid items-start grid-cols-1 sm:grid-cols-3 gap-4 mt-4"> <div class="grid items-start grid-cols-1 sm:grid-cols-3 gap-4 mt-4">
@@ -536,7 +484,12 @@
</button> </button>
</div> </div>
</div> </div>
<GiftedDialog ref="giveDialogFromThis" :from-project-id="projectId" /> <GiftedDialog
ref="giveDialogFromThis"
:from-project-id="projectId"
:show-projects="true"
:is-from-project-view="true"
/>
<h3 class="text-lg font-bold mb-3 mt-4"> <h3 class="text-lg font-bold mb-3 mt-4">
Benefitted From This Project Benefitted From This Project
@@ -1270,21 +1223,52 @@ export default class ProjectViewView extends Vue {
); );
} }
openGiftDialogToProject(contact?: libsUtil.GiverReceiverInputInfo) { openGiftDialogToProject(
(this.$refs.giveDialogToThis as GiftedDialog).open( contact?: libsUtil.GiverReceiverInputInfo | "Unnamed",
contact, ) {
undefined, if (contact === "Unnamed") {
undefined, // Special case: Pass undefined to trigger Step 1, but with "Unnamed" pre-selected
(contact?.name || "Someone not named") + ` gave to this project`, (this.$refs.giveDialogToThis as GiftedDialog).open(
); undefined,
undefined,
undefined,
"Given by Unnamed to this project",
);
// Immediately select "Unnamed" and move to Step 2
(this.$refs.giveDialogToThis as GiftedDialog).selectGiver();
} else {
// Open straight to Step 2 with current user as giver and current project as recipient
(this.$refs.giveDialogToThis as GiftedDialog).open(
{
did: this.activeDid,
name: "You",
},
{
did: this.issuer,
name: this.name,
handleId: this.projectId,
image: this.imageUrl,
},
undefined,
`Given to ${this.name}`,
);
}
} }
openGiftDialogFromProject() { openGiftDialogFromProject() {
// Set the project as giver and the current user as recipient
(this.$refs.giveDialogFromThis as GiftedDialog).open( (this.$refs.giveDialogFromThis as GiftedDialog).open(
undefined, {
did: undefined,
name: this.name,
handleId: this.projectId,
image: this.imageUrl,
},
{ did: this.activeDid, name: "You" }, { did: this.activeDid, name: "You" },
undefined, undefined,
`This project gave to you`, `${this.name} gave to you`,
undefined,
undefined,
); );
} }

View File

@@ -144,7 +144,7 @@ export default class QuickActionBvcBeginView extends Vue {
"HUR", "HUR",
BVC_MEETUPS_PROJECT_CLAIM_ID, BVC_MEETUPS_PROJECT_CLAIM_ID,
); );
if (timeResult.success) { if (timeResult.type === "success") {
timeSuccess = true; timeSuccess = true;
} else { } else {
logger.error("Error sending time:", timeResult); logger.error("Error sending time:", timeResult);
@@ -154,7 +154,7 @@ export default class QuickActionBvcBeginView extends Vue {
type: "danger", type: "danger",
title: "Error", title: "Error",
text: text:
timeResult?.error || timeResult?.error?.userMessage ||
"There was an error sending the time.", "There was an error sending the time.",
}, },
5000, 5000,
@@ -171,7 +171,7 @@ export default class QuickActionBvcBeginView extends Vue {
apiServer, apiServer,
axios, axios,
); );
if (attendResult.success) { if (attendResult.type === "success") {
attendedSuccess = true; attendedSuccess = true;
} else { } else {
logger.error("Error sending attendance:", attendResult); logger.error("Error sending attendance:", attendResult);
@@ -181,7 +181,7 @@ export default class QuickActionBvcBeginView extends Vue {
type: "danger", type: "danger",
title: "Error", title: "Error",
text: text:
attendResult?.error || attendResult?.error?.userMessage ||
"There was an error sending the attendance.", "There was an error sending the attendance.",
}, },
5000, 5000,

55
src/vite.config.utils.js Normal file
View 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);
}

View File

@@ -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');

View File

@@ -8,5 +8,5 @@
"allowImportingTsExtensions": true, "allowImportingTsExtensions": true,
"noEmit": true "noEmit": true
}, },
"include": ["vite.config.*", "./src/interfaces/build.ts"] "include": ["vite.config.*"]
} }

View File

@@ -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'));

View File

@@ -1,45 +1,31 @@
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; dotenv.config();
if (process.env.NODE_ENV === NodeEnv.Dev) {
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 +56,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 +78,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 +108,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
View File

@@ -0,0 +1,4 @@
import { defineConfig } from "vite";
import { createBuildConfig } from "./vite.config.common.mts";
export default defineConfig(async () => createBuildConfig('development'));

View File

@@ -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
View 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'
}
}
}
}
});

View File

@@ -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
View 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']
});

View File

@@ -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/*"],
},
],
},
},
},
}; };
} }

View File

@@ -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
}, },