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.
TIME_SAFARI_APP_TITLE="TimeSafari_Test"
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_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`
sw_scripts-combined.js
*.pem
tsconfig.node.tsbuildinfo
verified.txt
myenv
@@ -41,15 +40,19 @@ pnpm-debug.log*
playwright-tests
dist-electron-packages
.ruby-version
+.env
# Test files generated by scripts test-ios.js & test-android.js
.generated/
.env.default
vendor/
# Build logs
build_logs/
# PWA icon files generated by capacitor-assets
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
- 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
Install dependencies:
@@ -22,7 +33,7 @@ Install dependencies:
## Web Dev Locally
```bash
NODE_ENV=dev npm run start:web
npm run dev
```
## Web Build for Server
@@ -31,7 +42,7 @@ Install dependencies:
```bash
rm -rf dist
NODE_ENV=prod npm run build:web
npm run build:web
```
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.
```bash
NODE_ENV=dev npm run serve:web
npm run serve
```
### 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`.
* 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).
@@ -63,7 +74,7 @@ Install dependencies:
* For test, build the app (because test server is not yet set up to build):
```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:
@@ -205,10 +216,10 @@ docker run -d \
```bash
# For AppImage (recommended)
npm run build:electron-linux
npm run electron:build-linux
# 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/`:
@@ -220,19 +231,19 @@ docker run -d \
1. Build the electron app in production mode:
```bash
NODE_ENV=prod npm run build:web
npm run build:web
npm run build:electron
npm run build:electron-mac
npm run electron:build-mac
```
2. Package the Electron app for macOS:
```bash
# For Intel Macs
npm run build:electron-mac
npm run electron:build-mac
# 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/`:
@@ -254,7 +265,7 @@ For public distribution on macOS, you need to code sign and notarize your app:
2. Build with signing:
```bash
npm run build:electron-mac
npm run electron:build-mac
```
### Running the Packaged App
@@ -293,10 +304,10 @@ For testing the Electron build before packaging:
```bash
# Build and run in development mode (includes DevTools)
npm run build:electron
npm run electron:dev
# 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)
@@ -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]
## 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).
## [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
### Added
- 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
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
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
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
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
@@ -70,6 +97,9 @@ The application uses a platform-agnostic database layer:
**Development Guidelines**:
- 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

View File

@@ -13,7 +13,6 @@
android:exported="true"
android:label="@string/title_activity_main"
android:launchMode="singleTask"
android:screenOrientation="portrait"
android:theme="@style/AppTheme.NoActionBarLaunch">
<intent-filter>
<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
- 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
### Enable Dexie Database

View File

@@ -15,21 +15,17 @@
<script type="module">
const platform = process.env.VITE_PLATFORM;
switch (platform) {
case 'capacitor': // BuildPlatform.Capacitor
case 'capacitor':
import('./src/main.capacitor.ts');
break;
case 'electron': // BuildPlatform.Electron
case 'electron':
import('./src/main.electron.ts');
break;
case 'pywebview': // BuildPlatform.PyWebView
case 'pywebview':
import('./src/main.pywebview.ts');
break;
case 'web': // BuildPlatform.Web
import('./src/main.web.ts');
break;
default:
console.error(`Unknown platform: ${platform}`);
throw new Error(`Unknown platform: ${platform}`);
import('./src/main.web.ts');
}
</script>
</body>

View File

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

View File

@@ -6,37 +6,44 @@
"name": "Time Safari Team"
},
"scripts": {
"build-start:pywebview": "vite build --config vite.config.pywebview.mts && .venv/bin/python src/pywebview/main.py",
"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",
"build:capacitor": "vite build --config vite.config.capacitor.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",
"dev": "vite --config vite.config.dev.mts --host",
"serve": "vite preview",
"build": "VITE_GIT_HASH=`git log -1 --pretty=format:%h` vite build --config vite.config.mts",
"lint": "eslint --ext .js,.ts,.vue --ignore-path .gitignore 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",
"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: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:ios": "node scripts/test-ios.js",
"test:mobile": "npm run build:capacitor && npm run test:android && npm run test:ios",
"test:prerequisites": "node scripts/check-prerequisites.js",
"test:web": "npx playwright test -c playwright.config-local.ts --trace on"
"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: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": {
"@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:
* {
* 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',
* reuseExistingServer: !process.env.CI,
* },
@@ -112,7 +112,7 @@ export default defineConfig({
*/
webServer: {
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",
reuseExistingServer: !process.env.CI,
},

View File

@@ -74,7 +74,7 @@ export default defineConfig({
/* Run your local dev server before starting the tests */
// webServer: {
// 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",
// 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"
class="list-disc list-outside ml-4"
>
On Android: You will be prompted to choose an app for sharing your
backup file. To save on your phone, you will need a file manager app.
On Android: You will be prompted to choose a location to save your
backup file.
</li>
</ul>
</div>

View File

@@ -1,99 +1,534 @@
<template>
<div v-if="visible" class="dialog-overlay">
<div class="dialog">
<h1 class="text-xl font-bold text-center mb-4">
{{ customTitle }}
</h1>
<input
v-model="description"
type="text"
class="block w-full rounded border border-slate-400 mb-2 px-3 py-2"
:placeholder="prompt || 'What was given?'"
/>
<div class="flex flex-row justify-center">
<span
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()"
<!-- Step 1: Giver -->
<div v-show="firstStep" id="sectionGiftedGiver">
<label class="block font-bold mb-4">
{{
stepType === "recipient"
? "Choose who received the gift:"
: showProjects
? "Choose a project benefitted from:"
: "Choose a person received from:"
}}
</label>
<!-- 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 }}
</span>
<div
class="border border-r-0 border-slate-400 bg-slate-200 px-4 py-2"
@click="amountInput === '0' ? null : decrement()"
>
<font-awesome icon="chevron-left" />
</div>
<input
id="inputGivenAmount"
v-model="amountInput"
type="number"
class="border border-r-0 border-slate-400 px-2 py-2 text-center w-20"
/>
<div
class="rounded-r border border-slate-400 bg-slate-200 px-4 py-2"
@click="increment()"
>
<font-awesome icon="chevron-right" />
</div>
</div>
<div class="mt-4 flex justify-center">
<span>
<router-link
:to="{
name: 'gifted-details',
query: {
amountInput,
description,
giverDid: giver?.did,
giverName: giver?.name,
offerId,
fulfillsProjectId: toProjectId,
providerProjectId: fromProjectId,
recipientDid: receiver?.did,
recipientName: receiver?.name,
unitCode,
},
}"
class="text-blue-500"
>
Photo & more options ...
</router-link>
</span>
</div>
<p class="text-center mb-2 mt-6 italic">
Sign & Send to publish to the world
<font-awesome
icon="circle-info"
class="pl-2 text-blue-500 cursor-pointer"
@click="explainData()"
/>
</p>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
<template v-if="shouldShowProjects">
<!-- show projects -->
<li
v-for="project in projects.slice(0, 7)"
:key="project.handleId"
class="cursor-pointer"
@click="
stepType === 'recipient'
? selectRecipientProject(project)
: selectProject(project)
"
>
<div class="relative w-fit mx-auto mb-1">
<ProjectIcon
:entity-id="project.handleId"
:icon-size="48"
:image-url="project.image"
class="!size-[3rem] mx-auto border border-slate-300 bg-white overflow-hidden rounded-full"
/>
</div>
<h3
class="text-xs font-medium text-ellipsis whitespace-nowrap overflow-hidden"
>
{{ project.name }}
</h3>
<div class="text-xs text-slate-500 truncate">
<font-awesome icon="user" class="fa-fw text-slate-400" />
{{
didInfo(project.issuerDid, activeDid, allMyDids, allContacts)
}}
</div>
</li>
<li
v-if="projects.length === 0"
class="text-xs text-slate-500 italic col-span-full"
>
(No projects found.)
</li>
<li v-if="projects.length > 0">
<router-link :to="{ name: 'discover' }" class="cursor-pointer">
<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>
<template v-else>
<!-- 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
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"
@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"
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>
<!-- 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>
</template>
<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 {
createAndSubmitGive,
didInfo,
serverMessageForUser,
getHeaders,
} from "../libs/endorserServer";
import * as libsUtil from "../libs/util";
import { db, retrieveSettingsForActiveAccount } from "../db/index";
@@ -102,13 +537,38 @@ import * as databaseUtil from "../db/databaseUtil";
import { retrieveAccountDids } from "../libs/util";
import { logger } from "../utils/logger";
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 {
$notify!: (notification: NotificationIface, timeout?: number) => void;
@Prop() fromProjectId = "";
@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 = "";
allContacts: Array<Contact> = [];
@@ -119,6 +579,7 @@ export default class GiftedDialog extends Vue {
callbackOnSuccess?: (amount: number) => void = () => {};
customTitle?: string;
description = "";
firstStep = true; // true = Step 1 (giver/recipient selection), false = Step 2 (amount/description)
giver?: libsUtil.GiverReceiverInputInfo; // undefined means no identified giver agent
offerId = "";
prompt = "";
@@ -128,6 +589,80 @@ export default class GiftedDialog extends Vue {
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(
giver?: libsUtil.GiverReceiverInputInfo,
receiver?: libsUtil.GiverReceiverInputInfo,
@@ -140,10 +675,14 @@ export default class GiftedDialog extends Vue {
this.giver = giver;
this.prompt = prompt || "";
this.receiver = receiver;
// if we show "given to user" selection, default checkbox to true
this.amountInput = "0";
this.callbackOnSuccess = callbackOnSuccess;
this.offerId = offerId || "";
this.firstStep = !giver;
this.stepType = "giver";
// Update entity types based on current props
this.updateEntityTypes();
try {
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
@@ -174,7 +713,16 @@ export default class GiftedDialog extends Vue {
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) {
logger.error("Error retrieving settings from database:", err);
this.$notify(
@@ -224,6 +772,7 @@ export default class GiftedDialog extends Vue {
this.amountInput = "0";
this.prompt = "";
this.unitCode = "HUR";
this.firstStep = true;
}
async confirm() {
@@ -265,6 +814,20 @@ export default class GiftedDialog extends Vue {
);
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.$notify(
@@ -304,20 +867,52 @@ export default class GiftedDialog extends Vue {
unitCode: string = "HUR",
) {
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(
this.axios,
this.apiServer,
this.activeDid,
giverDid as string,
recipientDid as string,
fromDid,
toDid,
description,
amount,
unitCode,
this.toProjectId,
fulfillsProjectHandleId,
this.offerId,
false,
undefined,
this.fromProjectId,
providerPlanHandleId,
);
if (!result.success) {
@@ -378,6 +973,118 @@ export default class GiftedDialog extends Vue {
-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>

View File

@@ -7,7 +7,6 @@ export enum AppString {
// 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.
APP_NAME = "Time Safari",
// iOS doesn't like spaces in the app title.
APP_NAME_NO_SPACES = "TimeSafari",
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 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)
const MIGRATIONS = [

View File

@@ -1,10 +1,9 @@
const { contextBridge, ipcRenderer } = require("electron");
import { NodeEnv, BuildPlatform } from "@/interfaces/build";
const logger = {
log: (message, ...args) => {
// 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 */
console.log(`[Preload] ${message}`, ...args);
/* eslint-enable no-console */
@@ -24,7 +23,7 @@ const logger = {
},
info: (message, ...args) => {
// 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 */
console.info(`[Preload] ${message}`, ...args);
/* eslint-enable no-console */
@@ -54,7 +53,7 @@ const getPath = (pathType) => {
logger.info("Preload script starting...");
// Force electron platform in the renderer process
window.process = { env: { VITE_PLATFORM: BuildPlatform.Electron } };
window.process = { env: { VITE_PLATFORM: "electron" } };
try {
contextBridge.exposeInMainWorld("electronAPI", {
@@ -77,12 +76,12 @@ try {
// Environment info
env: {
isElectron: true,
isDev: process.env.NODE_ENV === NodeEnv.Dev,
platform: BuildPlatform.Electron, // Explicitly set platform
isDev: process.env.NODE_ENV === "development",
platform: "electron", // Explicitly set platform
},
// Path utilities
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,
} from "../interfaces/common";
import { PlanSummaryRecord } from "../interfaces/records";
import { logger, safeStringify } from "../utils/logger";
import { logger } from "../utils/logger";
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
/**
@@ -437,23 +437,19 @@ export async function getHeaders(
}
headers["Authorization"] = "Bearer " + token;
} catch (error) {
// This rarely happens: we've seen it when they have account info but the
// encryption secret got lost.
// Replicate this in Chrome: go to Storage and hit 'Clear site data'.
// Check the util.ts retrieveFullyDecryptedAccount method where it calls simpleDecrypt.
// encryption secret got lost. But in most cases we want users to at
// least see their feed -- and anything else that returns results for
// anonymous users.
// In most cases we want users to at least see their feed -- and anything
// 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.
// We'll continue with an anonymous request... still want to show feed and other things, but ideally let them know.
logConsoleAndDb(
"Something failed in getHeaders call (will proceed anonymously" +
($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'.
//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,
);
if ($notify) {

View File

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

View File

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

View File

@@ -1,14 +1,12 @@
import { initBackend } from "absurd-sql/dist/indexeddb-main-thread";
import { initializeApp } from "./main.common";
import { logger } from "./utils/logger";
import { BuildPlatform } from "@/interfaces/build";
const platform = process.env.VITE_PLATFORM;
const pwa_enabled = process.env.VITE_PWA_ENABLED === "true";
// Only import service worker for web builds
if (platform !== BuildPlatform.Electron && pwa_enabled) {
if (platform !== "electron" && pwa_enabled) {
import("./registerServiceWorker"); // Web PWA support
}
@@ -27,7 +25,7 @@ function sqlInit() {
// workers through the main thread
initBackend(worker);
}
if (platform === BuildPlatform.Web) {
if (platform === "web" || platform === "development") {
sqlInit();
} else {
logger.warn("[Web] SQL not initialized for platform", { platform });

View File

@@ -1,11 +1,10 @@
/* eslint-disable no-console */
import { register } from "register-service-worker";
import { NodeEnv, BuildPlatform } from "@/interfaces/build";
// Check if we're in an Electron environment
const isElectron =
process.env.VITE_PLATFORM === BuildPlatform.Electron ||
process.env.VITE_PLATFORM === "electron" ||
process.env.VITE_DISABLE_PWA === "true" ||
window.navigator.userAgent.toLowerCase().includes("electron");
@@ -16,7 +15,7 @@ const isElectron =
if (
!isElectron &&
process.env.VITE_PWA_ENABLED === "true" &&
process.env.NODE_ENV === NodeEnv.Prod
process.env.NODE_ENV === "production"
) {
register(`${process.env.BASE_URL}sw.js`, {
ready() {

View File

@@ -3,7 +3,6 @@ import { WebPlatformService } from "./platforms/WebPlatformService";
import { CapacitorPlatformService } from "./platforms/CapacitorPlatformService";
import { ElectronPlatformService } from "./platforms/ElectronPlatformService";
import { PyWebViewPlatformService } from "./platforms/PyWebViewPlatformService";
import { BuildPlatform } from "@/interfaces/build";
/**
* Factory class for creating platform-specific service implementations.
@@ -36,19 +35,19 @@ export class PlatformServiceFactory {
return PlatformServiceFactory.instance;
}
const platform = process.env.VITE_PLATFORM || BuildPlatform.Web;
const platform = process.env.VITE_PLATFORM || "web";
switch (platform) {
case BuildPlatform.Capacitor:
case "capacitor":
PlatformServiceFactory.instance = new CapacitorPlatformService();
break;
case BuildPlatform.Electron:
case "electron":
PlatformServiceFactory.instance = new ElectronPlatformService();
break;
case BuildPlatform.PyWebView:
case "pywebview":
PlatformServiceFactory.instance = new PyWebViewPlatformService();
break;
case BuildPlatform.Web:
case "web":
default:
PlatformServiceFactory.instance = new WebPlatformService();
break;

View File

@@ -7,7 +7,6 @@
import { AxiosError } from "axios";
import { logger, safeStringify } from "../utils/logger";
import { BuildPlatform } from "@/interfaces/build";
/**
* 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) => {
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
logger.error(`[Capacitor API Error] ${endpointStr}:`, {
message: error.message,

View File

@@ -1,5 +1,4 @@
import { logToDb } from "../db/databaseUtil";
import { BuildPlatform, NodeEnv } from "@/interfaces/build";
export function safeStringify(obj: unknown) {
const seen = new WeakSet();
@@ -22,7 +21,7 @@ export function safeStringify(obj: unknown) {
export const logger = {
debug: (message: string, ...args: unknown[]) => {
if (process.env.NODE_ENV !== NodeEnv.Prod) {
if (process.env.NODE_ENV !== "production") {
// eslint-disable-next-line no-console
console.debug(message, ...args);
// const argsString = args.length > 0 ? " - " + safeStringify(args) : "";
@@ -31,8 +30,8 @@ export const logger = {
},
log: (message: string, ...args: unknown[]) => {
if (
process.env.NODE_ENV !== NodeEnv.Prod ||
process.env.VITE_PLATFORM === BuildPlatform.Capacitor
process.env.NODE_ENV !== "production" ||
process.env.VITE_PLATFORM === "capacitor"
) {
// eslint-disable-next-line no-console
console.log(message, ...args);
@@ -42,9 +41,9 @@ export const logger = {
},
info: (message: string, ...args: unknown[]) => {
if (
process.env.NODE_ENV !== NodeEnv.Prod ||
process.env.VITE_PLATFORM === BuildPlatform.Capacitor ||
process.env.VITE_PLATFORM === BuildPlatform.Electron
process.env.NODE_ENV !== "production" ||
process.env.VITE_PLATFORM === "capacitor" ||
process.env.VITE_PLATFORM === "electron"
) {
// eslint-disable-next-line no-console
console.info(message, ...args);

View File

@@ -126,7 +126,7 @@
<div class="flex justify-center text-center text-sm leading-tight mb-1">
People {{ profileImageUrl ? "without your image" : "" }} see this
<br />
(if you've let them see which posts are yours):
(if you've let them see your activity):
</div>
<div class="flex justify-center">
<EntityIcon
@@ -1573,25 +1573,24 @@ export default class AccountViewView extends Vue {
* @throws Will notify the user if there is an export error.
*/
public async exportDatabase() {
throw new Error("Not implemented");
// try {
// // Generate the blob from the database
// const blob = await this.generateDatabaseBlob();
try {
// Generate the blob from the database
const blob = await this.generateDatabaseBlob();
// // Create a temporary URL for the blob
// this.downloadUrl = this.createBlobURL(blob);
// Create a temporary URL for the blob
this.downloadUrl = this.createBlobURL(blob);
// // Trigger the download
// this.downloadDatabaseBackup(this.downloadUrl);
// Trigger the download
this.downloadDatabaseBackup(this.downloadUrl);
// // Notify the user that the download has started
// this.notifyDownloadStarted();
// Notify the user that the download has started
this.notifyDownloadStarted();
// // Revoke the temporary URL -- after a pause to avoid DuckDuckGo download failure
// setTimeout(() => URL.revokeObjectURL(this.downloadUrl), 1000);
// } catch (error) {
// this.handleExportError(error);
// }
// Revoke the temporary URL -- after a pause to avoid DuckDuckGo download failure
setTimeout(() => URL.revokeObjectURL(this.downloadUrl), 1000);
} catch (error) {
this.handleExportError(error);
}
}
/**

View File

@@ -4,14 +4,14 @@
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Breadcrumb -->
<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 -->
<router-link
:to="{ name: 'home' }"
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
><font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
</router-link>
Given by...
{{ stepType === "giver" ? "Given by..." : "Given to..." }}
</h1>
</div>
@@ -19,19 +19,18 @@
<ul class="border-t border-slate-300">
<li class="border-b border-slate-300 py-3">
<h2 class="text-base flex gap-4 items-center">
<span class="grow">
<img
src="../assets/blank-square.svg"
width="32"
class="inline-block align-middle border border-slate-300 rounded-md mr-1"
<span class="grow flex gap-2 items-center font-medium">
<font-awesome
icon="circle-question"
class="text-slate-400 text-4xl"
/>
Unnamed/Unknown
<span class="italic text-slate-400">(Unnamed/Unknown)</span>
</span>
<span class="text-right">
<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"
@click="openDialog()"
@click="openDialog('Unnamed')"
>
<font-awesome icon="gift" class="fa-fw"></font-awesome>
</button>
@@ -44,13 +43,14 @@
class="border-b border-slate-300 py-3"
>
<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
:contact="contact"
:icon-size="32"
class="inline-block align-middle border border-slate-300 rounded-md mr-1"
:icon-size="34"
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 class="text-right">
<button
@@ -65,7 +65,13 @@
</li>
</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>
</template>
@@ -97,6 +103,24 @@ export default class ContactGiftingView extends Vue {
description = "";
projectId = "";
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() {
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;
// 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
} catch (err: any) {
logger.error("Error retrieving settings & contacts:", err);
@@ -144,17 +200,108 @@ export default class ContactGiftingView extends Vue {
}
}
openDialog(giver?: GiverReceiverInputInfo) {
const recipient = this.projectId
? undefined
: { did: this.activeDid, name: "you" };
(this.$refs.customDialog as GiftedDialog).open(
giver,
recipient,
undefined,
"Given by " + (giver?.name || "someone not named"),
this.prompt,
);
openDialog(contact?: GiverReceiverInputInfo | "Unnamed") {
if (contact === "Unnamed") {
// Special case: Pass undefined to trigger Step 1, but with "Unnamed" pre-selected
let recipient: GiverReceiverInputInfo;
let giver: GiverReceiverInputInfo | undefined;
if (this.stepType === "giver") {
// We're selecting a giver, so 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" };
}
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>

View File

@@ -100,7 +100,6 @@ import { Component, Vue } from "vue-facing-decorator";
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
import { APP_SERVER } from "@/constants/app";
import { NodeEnv } from "@/interfaces/build";
import { logger } from "@/utils/logger";
import { errorStringForLog } from "@/libs/endorserServer";
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
@@ -149,7 +148,7 @@ export default class DeepLinkRedirectView extends Vue {
this.deepLinkUrl = `timesafari://${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.openDeepLink();

View File

@@ -4,84 +4,91 @@
<!-- CONTENT -->
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Back -->
<div
v-if="!hideBackButton"
class="text-lg text-center font-light relative px-7"
>
<h1
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="cancelBack()"
>
<font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
<!-- Breadcrumb -->
<div id="ViewBreadcrumb" class="mb-8">
<h1 class="text-2xl text-center font-semibold relative px-7 mb-2">
<!-- Back -->
<div
v-if="!hideBackButton"
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="cancelBack()"
>
<font-awesome icon="chevron-left" class="fa-fw" />
</div>
What Was Given
</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>
<!-- 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
v-model="description"
class="block w-full rounded border border-slate-400 mb-2 px-3 py-2"
placeholder="What was received"
/>
<div class="flex flex-row justify-center">
<span
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()"
>
{{ libsUtil.UNIT_SHORT[unitCode] || unitCode }}
</span>
<div
class="border border-r-0 border-slate-400 bg-slate-200 px-4 py-2"
<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" />
</div>
</button>
<input
id="inputGivenAmount"
v-model="amountInput"
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
class="rounded-r border border-slate-400 bg-slate-200 px-4 py-2"
<button
class="rounded-e border border-slate-400 bg-slate-200 px-4 py-2"
@click="increment()"
>
<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 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">
<img :src="imageUrl" class="h-24 rounded-xl" />
<img :src="imageUrl" class="h-36 rounded-lg" />
</a>
<font-awesome
icon="trash-can"
class="text-red-500 fa-fw ml-8 mt-10"
class="text-red-500 fa-fw cursor-pointer"
@click="confirmDeleteImage"
/>
</span>
@@ -95,22 +102,22 @@
</div>
<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 -->
<div class="flex-grow border border-slate-400 p-2 rounded-md">
<div class="flex">
<div class="sm:flex-grow sm:w-1/2 border border-slate-400 p-2 rounded-md overflow-hidden">
<div class="flex items-center">
<input
v-if="giverDid && !providedByProject"
v-model="providedByGiver"
type="checkbox"
class="h-6 w-6 mr-2"
class="flex-shrink-0 h-6 w-6 mr-2"
/>
<font-awesome
v-else
icon="square"
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
? "This was provided by " + giverName + "."
@@ -120,24 +127,24 @@
<font-awesome
v-if="!giverDid || providedByProject"
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()"
/>
</div>
<div class="flex">
<div class="flex items-center">
<input
v-if="providerProjectId && !providedByGiver"
v-model="providedByProject"
type="checkbox"
class="h-6 w-6 mr-2"
class="flex-shrink-0 h-6 w-6 mr-2"
/>
<font-awesome
v-else
icon="square"
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
? "This was provided by " + providerProjectName + "."
@@ -147,31 +154,31 @@
<font-awesome
v-if="!providerProjectId || providedByGiver"
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()"
/>
</div>
</div>
<div class="flex-shrink flex justify-center items-center">
<font-awesome icon="arrow-right" class="fa-fw h-7" />
<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 rotate-90 sm:rotate-0" />
</div>
<!-- Third Column for Recipient -->
<div class="flex-grow border border-slate-400 p-2 rounded-md">
<div class="flex">
<div class="sm:flex-grow sm:w-1/2 border border-slate-400 p-2 rounded-md overflow-hidden">
<div class="flex items-center">
<input
v-if="recipientDid && !givenToProject"
v-model="givenToRecipient"
type="checkbox"
class="h-6 w-6 mr-2"
class="flex-shrink-0 h-6 w-6 mr-2"
/>
<font-awesome
v-else
icon="square"
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
? "This was given to " + recipientName + "."
@@ -181,24 +188,24 @@
<font-awesome
v-if="!recipientDid || givenToProject"
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()"
/>
</div>
<div class="flex">
<div class="flex items-center">
<input
v-if="fulfillsProjectId && !givenToRecipient"
v-model="givenToProject"
type="checkbox"
class="h-6 w-6 mr-2"
class="flex-shrink-0 h-6 w-6 mr-2"
/>
<font-awesome
v-else
icon="square"
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
? "This was given to " + fulfillsProjectName + ". "
@@ -208,7 +215,7 @@
<font-awesome
v-if="!fulfillsProjectId || givenToRecipient"
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()"
/>
</div>
@@ -229,11 +236,11 @@
</router-link>
</div>
<p class="text-center mb-2 mt-6 italic">
Sign & Send to publish to the world
<p class="text-center text-sm my-4">
<b class="font-medium">Sign &amp; Send</b> to publish to the world
<font-awesome
icon="circle-info"
class="pl-2 text-blue-500 cursor-pointer"
class="fa-fw text-blue-500 text-base cursor-pointer"
@click="explainData()"
/>
</p>
@@ -910,5 +917,10 @@ export default class GiftedDetails extends Vue {
7000,
);
}
// Computed property to get unit options
get unitOptions() {
return this.libsUtil.UNIT_SHORT;
}
}
</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"
@click="showNameThenIdDialog()"
>
Show them your identification info
Show them {{ PASSKEYS_ENABLED ? "default" : "your" }} identifier
info
</button>
</div>
<UserNameDialog ref="userNameDialog" />
@@ -117,101 +118,73 @@ Raymer * @version 1.0.0 */
</div>
<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="flex">
<h2 class="text-xl font-bold">What have you seen someone do?</h2>
<button
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="openGiftedPrompts()"
>
<font-awesome icon="lightbulb" class="fa-fw" />
</button>
<div class="grid grid-cols-2 gap-2">
<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="openDialogPerson()"
>
<font-awesome icon="user" />
Person
</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>
<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>
<GiftedDialog ref="customDialog" />
<GiftedDialog ref="customDialog" :show-projects="showProjectsDialog" />
<GiftedPrompts ref="giftedPrompts" />
<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 -->
<div class="mt-4 mb-4">
<div class="flex items-center mb-4">
<h2 class="text-xl font-bold flex items-center gap-4">
Latest Activity
<button
v-if="resultsAreFiltered()"
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()"
>
<font-awesome icon="filter" class="fa-fw" />
</button>
<button
v-else
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"
@click="openFeedFilters()"
>
<font-awesome icon="filter" class="fa-fw" />
</button>
</h2>
<div class="flex gap-2 items-center mb-3">
<h2 class="text-xl font-bold">Latest Activity</h2>
<button
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"
@click="openFeedFilters()"
>
<font-awesome
icon="filter"
class="block text-center w-[1em] translate-y-[0.05em]"
/>
</button>
<button
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"
@click="openFeedFilters()"
>
<font-awesome
icon="filter"
class="block text-center w-[1em] translate-y-[0.05em]"
/>
</button>
</div>
<div
@@ -477,6 +450,7 @@ export default class HomeView extends Vue {
selectedImageData: Blob | null = null;
isImageViewerOpen = false;
imageCache: Map<string, Blob | null> = new Map();
showProjectsDialog = false;
/**
* Initializes the component on mount
@@ -682,7 +656,7 @@ export default class HomeView extends Vue {
group: "alert",
type: "warning",
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,
);
@@ -1625,17 +1599,33 @@ export default class HomeView extends Vue {
* @param giver Optional contact info for giver
* @param description Optional gift description
*/
openDialog(giver?: GiverReceiverInputInfo, description?: string) {
(this.$refs.customDialog as GiftedDialog).open(
giver,
{
did: this.activeDid,
name: "you",
} as GiverReceiverInputInfo,
undefined,
"Given by " + (giver?.name || "someone not named"),
description,
);
openDialog(giver?: GiverReceiverInputInfo | "Unnamed", description?: string) {
if (giver === "Unnamed") {
// Special case: Pass undefined to trigger Step 1, but with "Unnamed" pre-selected
(this.$refs.customDialog as GiftedDialog).open(
undefined,
{
did: this.activeDid,
name: "You",
} as GiverReceiverInputInfo,
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" });
}
}
openDialogPerson(
giver?: GiverReceiverInputInfo | "Unnamed",
description?: string,
) {
this.showProjectsDialog = false;
this.openDialog(giver, description);
}
openProjectDialog() {
this.showProjectsDialog = true;
(this.$refs.customDialog as any).open();
}
}
</script>

View File

@@ -214,63 +214,11 @@
</div>
</div>
<div v-if="activeDid && isRegistered">
<div class="text-center">
<p class="mt-2 mt-4 text-center">Record a contribution from:</p>
</div>
<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" />
<GiftedDialog
ref="giveDialogToThis"
:to-project-id="projectId"
:is-from-project-view="true"
/>
<!-- Offers & Gifts to & from this -->
<div class="grid items-start grid-cols-1 sm:grid-cols-3 gap-4 mt-4">
@@ -536,7 +484,12 @@
</button>
</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">
Benefitted From This Project
@@ -1270,21 +1223,52 @@ export default class ProjectViewView extends Vue {
);
}
openGiftDialogToProject(contact?: libsUtil.GiverReceiverInputInfo) {
(this.$refs.giveDialogToThis as GiftedDialog).open(
contact,
undefined,
undefined,
(contact?.name || "Someone not named") + ` gave to this project`,
);
openGiftDialogToProject(
contact?: libsUtil.GiverReceiverInputInfo | "Unnamed",
) {
if (contact === "Unnamed") {
// Special case: Pass undefined to trigger Step 1, but with "Unnamed" pre-selected
(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() {
// Set the project as giver and the current user as recipient
(this.$refs.giveDialogFromThis as GiftedDialog).open(
undefined,
{
did: undefined,
name: this.name,
handleId: this.projectId,
image: this.imageUrl,
},
{ did: this.activeDid, name: "You" },
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",
BVC_MEETUPS_PROJECT_CLAIM_ID,
);
if (timeResult.success) {
if (timeResult.type === "success") {
timeSuccess = true;
} else {
logger.error("Error sending time:", timeResult);
@@ -154,7 +154,7 @@ export default class QuickActionBvcBeginView extends Vue {
type: "danger",
title: "Error",
text:
timeResult?.error ||
timeResult?.error?.userMessage ||
"There was an error sending the time.",
},
5000,
@@ -171,7 +171,7 @@ export default class QuickActionBvcBeginView extends Vue {
apiServer,
axios,
);
if (attendResult.success) {
if (attendResult.type === "success") {
attendedSuccess = true;
} else {
logger.error("Error sending attendance:", attendResult);
@@ -181,7 +181,7 @@ export default class QuickActionBvcBeginView extends Vue {
type: "danger",
title: "Error",
text:
attendResult?.error ||
attendResult?.error?.userMessage ||
"There was an error sending the attendance.",
},
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';
test('New offers for another user', async ({ page }) => {
const newUserDid = await generateNewEthrUser(page);
const user01Did = await generateNewEthrUser(page);
await page.goto('./');
await page.getByTestId('closeOnboardingAndFinish').click();
await expect(page.getByTestId('newDirectOffersActivityNumber')).toBeHidden();
await importUser(page, '00');
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 page.locator('button > svg.fa-plus').click();
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
// 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
// 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
// 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('./');
let offerNumElem = page.getByTestId('newDirectOffersActivityNumber');
await expect(offerNumElem).toHaveText('2');

View File

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

View File

@@ -1,5 +1,4 @@
import { defineConfig } from "vite";
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 dotenv from "dotenv";
import { loadAppConfig } from "./vite.config.common-utils.mts";
import { loadAppConfig } from "./vite.config.utils.mts";
import path from "path";
import { fileURLToPath } from 'url';
import { NodeEnv, BuildEnv, BuildPlatform } from "./src/interfaces/build.ts";
// Load environment variables
let buildEnv: BuildEnv;
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}` });
dotenv.config();
const __filename = fileURLToPath(import.meta.url);
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();
console.log(`Platform: ${platform}`);
const isElectron = platform === BuildPlatform.Electron;
const isCapacitor = platform === BuildPlatform.Capacitor;
const isPyWebView = platform === BuildPlatform.PyWebView;
const isElectron = mode === "electron";
const isCapacitor = mode === "capacitor";
const isPyWebView = mode === "pywebview";
// Explicitly set platform and disable PWA for Electron
process.env.VITE_PLATFORM = platform;
process.env.VITE_PWA_ENABLED = (isElectron || isPyWebView || isCapacitor)
? 'false'
: 'true';
process.env.VITE_PLATFORM = mode;
process.env.VITE_PWA_ENABLED = isElectron ? 'false' : 'true';
process.env.VITE_DISABLE_PWA = isElectron ? 'true' : 'false';
if (isElectron || isPyWebView || isCapacitor) {
process.env.VITE_PWA_ENABLED = 'false';
}
return {
base: isElectron || isPyWebView ? "./" : "/",
plugins: [vue()],
@@ -70,7 +56,7 @@ export async function createBuildConfig(platform: BuildPlatform): Promise<UserCo
},
define: {
'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_DISABLE_PWA': JSON.stringify(isElectron),
__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'),
'fs': path.resolve(__dirname, './src/utils/node-modules/fs.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'
: path.resolve(__dirname, 'node_modules/nostr-tools/nip06'),
'nostr-tools/core': buildEnv === BuildEnv.Development
'nostr-tools/core': mode === 'development'
? '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 { createBuildConfig } from "./vite.config.common.mts";
import path from 'path';
import { BuildPlatform } from "./src/interfaces/build.ts";
export default defineConfig(async () => {
const baseConfig = await createBuildConfig(BuildPlatform.Electron);
const baseConfig = await createBuildConfig('electron');
return mergeConfig(baseConfig, {
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 { 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 {
pwaConfig: PWAConfig;
aliasConfig: {
[key: string]: string;
};
}
export async function loadAppConfig(): Promise<AppConfig> {
const packageJson = await loadPackageJson();
const appName = process.env.TIME_SAFARI_APP_TITLE || packageJson.name;
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: {
"@": path.resolve(__dirname, "src"),
buffer: path.resolve(__dirname, "node_modules", "buffer"),
"dexie-export-import/dist/import":
"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 { VitePWA } from "vite-plugin-pwa";
import { createBuildConfig } from "./vite.config.common.mts";
import { loadPwaConfig } from "./vite.config.common-utils.mts";
import { BuildPlatform } from "./src/interfaces/build.ts";
import { loadAppConfig } from "./vite.config.utils.mts";
export default defineConfig(async () => {
const baseConfig = await createBuildConfig(BuildPlatform.Web);
const pwaConfig = await loadPwaConfig();
const baseConfig = await createBuildConfig('web');
const appConfig = await loadAppConfig();
return mergeConfig(baseConfig, {
plugins: [
VitePWA({
registerType: 'autoUpdate',
manifest: pwaConfig.manifest,
manifest: appConfig.pwaConfig?.manifest,
devOptions: {
enabled: false
},