Compare commits

..

16 Commits

Author SHA1 Message Date
8a2966a13e Enhance commentary and logging. 2025-07-07 11:17:10 -06:00
bfa75484f3 Fix issue showing the actions on contact screen. (Test still fails later.) 2025-07-07 11:16:18 -06:00
f71bd9fb42 Fix webapp-run command for test:web. 2025-07-06 19:02:09 -06:00
2bd191e255 Move remaining strings for Environment & Platform to type-checked values. 2025-07-06 14:41:20 -06:00
5905ae871a Refactor build files, separating & consolidating & renaming as needed. 2025-07-06 13:13:35 -06:00
65877627c7 refactor documentation 2025-07-06 09:28:42 -06:00
cbe8c2e427 order build commands alphabetically in package.json 2025-07-06 09:24:37 -06:00
538c2a4369 change each package.json build command to be consistent, with action first, followed by platform 2025-07-06 09:22:49 -06:00
36469a7fd2 remove unused deploy-specific file (now handled by NODE_ENV) 2025-07-05 21:55:11 -06:00
43c5a28153 remove vite.config files that are split into platform files 2025-07-05 21:51:09 -06:00
b6c932b22c rename env names to match proposed build names 2025-07-05 21:45:05 -06:00
322c785119 begin corrections for builds in specific environments (dev/test/prod) 2025-07-05 21:38:46 -06:00
a96cc8155c fix incorrect checks for success 2025-07-04 16:58:18 -06:00
1b283a0045 Merge pull request 'Lock to Portrait Mode (iOS and Android)' (#141) from app-portrait-mode-lock into master
Reviewed-on: #141
2025-06-27 21:47:11 -04:00
afd407e178 add portrait-mode camera to CHANGELOG 2025-06-27 19:46:30 -06:00
Jose Olarte III
59b13823c8 Feature: lock orientation mode 2025-06-23 17:39:21 +08:00
46 changed files with 642 additions and 1635 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, with the JWT ID on this environment (not production).
# This is the claim ID for actions in the BVC project.
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,6 +8,7 @@ signature.bin
# generated during `npm run build`
sw_scripts-combined.js
*.pem
tsconfig.node.tsbuildinfo
verified.txt
myenv
@@ -40,19 +41,15 @@ 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

@@ -6,6 +6,14 @@ 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,49 +3,22 @@
[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 [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.)
See [ClickUp](https://sharing.clickup.com/9014278710/l/h/8cmnyhp-174/10573fec74e2ba0) for current priorities.
## 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 & 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 & doc/BUILDING.md files.
```bash
npm install
npm run dev
NODE_ENV=dev npm run start:web
```
See [BUILDING.md](BUILDING.md) for more details.
See [BUILDING.md](doc/BUILDING.md) for more details.
## Tests
@@ -97,9 +70,6 @@ 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,6 +13,7 @@
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

@@ -11,17 +11,6 @@ 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:
@@ -33,7 +22,7 @@ Install dependencies:
## Web Dev Locally
```bash
npm run dev
NODE_ENV=dev npm run start:web
```
## Web Build for Server
@@ -42,7 +31,7 @@ Install dependencies:
```bash
rm -rf dist
npm run build:web
NODE_ENV=prod npm run build:web
```
The built files will be in the `dist` directory.
@@ -52,7 +41,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
npm run serve
NODE_ENV=dev npm run serve:web
```
### Compile and minify for test & production
@@ -63,7 +52,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`
* Run a build to make sure package-lock version is updated, linting works, etc: `npm install && npm run build:web`
* Commit everything (since the commit hash is used the app).
@@ -74,7 +63,7 @@ Install dependencies:
* For test, build the app (because test server is not yet set up to build):
```bash
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
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
```
... and transfer to the test server:
@@ -216,10 +205,10 @@ docker run -d \
```bash
# For AppImage (recommended)
npm run electron:build-linux
npm run build:electron-linux
# For .deb package
npm run electron:build-linux-deb
npm run build:electron-linux-deb
```
3. The packaged applications will be in `dist-electron-packages/`:
@@ -231,19 +220,19 @@ docker run -d \
1. Build the electron app in production mode:
```bash
npm run build:web
NODE_ENV=prod npm run build:web
npm run build:electron
npm run electron:build-mac
npm run build:electron-mac
```
2. Package the Electron app for macOS:
```bash
# For Intel Macs
npm run electron:build-mac
npm run build:electron-mac
# For Universal build (Intel + Apple Silicon)
npm run electron:build-mac-universal
npm run build:electron-mac-universal
```
3. The packaged applications will be in `dist-electron-packages/`:
@@ -265,7 +254,7 @@ For public distribution on macOS, you need to code sign and notarize your app:
2. Build with signing:
```bash
npm run electron:build-mac
npm run build:electron-mac
```
### Running the Packaged App
@@ -304,10 +293,10 @@ For testing the Electron build before packaging:
```bash
# Build and run in development mode (includes DevTools)
npm run electron:dev
npm run build:electron
# Build in production mode and test
npm run build:electron-prod && npm run electron:start
npm run build:electron-prod && npm run start:electron
```
## Mobile Builds (Capacitor)
@@ -479,3 +468,12 @@ 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

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

View File

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

View File

@@ -6,44 +6,37 @@
"name": "Time Safari Team"
},
"scripts": {
"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",
"prebuild": "eslint --ext .js,.ts,.vue --ignore-path .gitignore src && node sw_combine.js && node scripts/copy-wasm.js",
"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",
"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: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"
"clean:electron": "rimraf dist-electron",
"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: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"
},
"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 dev",
* command: "cd ../endorser-ch; NODE_ENV=test-local npm run start:web",
* url: 'http://localhost:3000',
* reuseExistingServer: !process.env.CI,
* },
@@ -112,7 +112,7 @@ export default defineConfig({
*/
webServer: {
command:
"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",
"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",
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 dev",
// "VITE_PASSKEYS_ENABLED=true VITE_DEFAULT_ENDORSER_API_SERVER=http://localhost:3000 npm run start:web",
// 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 a location to save your
backup file.
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.
</li>
</ul>
</div>

View File

@@ -1,534 +1,99 @@
<template>
<div v-if="visible" class="dialog-overlay">
<div class="dialog">
<!-- 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'
"
<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()"
>
<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>
{{ 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">
<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"
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"
@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, Watch } from "vue-facing-decorator";
import { Vue, Component, Prop } 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";
@@ -537,38 +102,13 @@ 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({
components: {
EntityIcon,
ProjectIcon,
},
})
@Component
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> = [];
@@ -579,7 +119,6 @@ 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 = "";
@@ -589,80 +128,6 @@ 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,
@@ -675,14 +140,10 @@ 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();
@@ -713,16 +174,7 @@ export default class GiftedDialog extends Vue {
this.allContacts,
);
}
if (
this.giverEntityType === "project" ||
this.recipientEntityType === "project"
) {
await this.loadProjects();
} else {
// Clear projects array when not needed
this.projects = [];
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) {
logger.error("Error retrieving settings from database:", err);
this.$notify(
@@ -772,7 +224,6 @@ export default class GiftedDialog extends Vue {
this.amountInput = "0";
this.prompt = "";
this.unitCode = "HUR";
this.firstStep = true;
}
async confirm() {
@@ -814,20 +265,6 @@ 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(
@@ -867,52 +304,20 @@ 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,
fromDid,
toDid,
giverDid as string,
recipientDid as string,
description,
amount,
unitCode,
fulfillsProjectHandleId,
this.toProjectId,
this.offerId,
false,
undefined,
providerPlanHandleId,
this.fromProjectId,
);
if (!result.success) {
@@ -973,118 +378,6 @@ 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,6 +7,7 @@ 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,6 +29,7 @@ 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,9 +1,10 @@
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 !== "production") {
if (process.env.NODE_ENV !== NodeEnv.Prod) {
/* eslint-disable no-console */
console.log(`[Preload] ${message}`, ...args);
/* eslint-enable no-console */
@@ -23,7 +24,7 @@ const logger = {
},
info: (message, ...args) => {
// Always log info in development, log with context in production
if (process.env.NODE_ENV !== "production") {
if (process.env.NODE_ENV !== NodeEnv.Prod) {
/* eslint-disable no-console */
console.info(`[Preload] ${message}`, ...args);
/* eslint-enable no-console */
@@ -53,7 +54,7 @@ const getPath = (pathType) => {
logger.info("Preload script starting...");
// Force electron platform in the renderer process
window.process = { env: { VITE_PLATFORM: "electron" } };
window.process = { env: { VITE_PLATFORM: BuildPlatform.Electron } };
try {
contextBridge.exposeInMainWorld("electronAPI", {
@@ -76,12 +77,12 @@ try {
// Environment info
env: {
isElectron: true,
isDev: process.env.NODE_ENV === "development",
platform: "electron", // Explicitly set platform
isDev: process.env.NODE_ENV === NodeEnv.Dev,
platform: BuildPlatform.Electron, // Explicitly set platform
},
// Path utilities
getBasePath: () => {
return process.env.NODE_ENV === "development" ? "/" : "./";
return process.env.NODE_ENV === NodeEnv.Dev ? "/" : "./";
},
});

21
src/interfaces/build.ts Normal file
View File

@@ -0,0 +1,21 @@
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 } from "../utils/logger";
import { logger, safeStringify } from "../utils/logger";
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
/**
@@ -437,19 +437,23 @@ 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. But 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 ideally let them know.
// 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.
// 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.
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,
error + " - " + safeStringify(error),
true,
);
if ($notify) {

View File

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

View File

@@ -50,8 +50,6 @@ import { DEFAULT_ROOT_DERIVATION_PATH } from "./crypto";
export interface GiverReceiverInputInfo {
did?: string;
name?: string;
image?: string;
handleId?: string;
}
export enum OnboardPage {
@@ -607,7 +605,7 @@ export const retrieveFullyDecryptedAccount = async (
dbAccount.values.length === 0 ||
dbAccount.values[0].length === 0
) {
throw new Error("Account not found.");
throw new Error("Account not found for did: " + activeDid);
}
const fullAccountData = databaseUtil.mapQueryResultToValues(
dbAccount,

View File

@@ -1,12 +1,14 @@
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 !== "electron" && pwa_enabled) {
if (platform !== BuildPlatform.Electron && pwa_enabled) {
import("./registerServiceWorker"); // Web PWA support
}
@@ -25,7 +27,7 @@ function sqlInit() {
// workers through the main thread
initBackend(worker);
}
if (platform === "web" || platform === "development") {
if (platform === BuildPlatform.Web) {
sqlInit();
} else {
logger.warn("[Web] SQL not initialized for platform", { platform });

View File

@@ -1,10 +1,11 @@
/* 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 === "electron" ||
process.env.VITE_PLATFORM === BuildPlatform.Electron ||
process.env.VITE_DISABLE_PWA === "true" ||
window.navigator.userAgent.toLowerCase().includes("electron");
@@ -15,7 +16,7 @@ const isElectron =
if (
!isElectron &&
process.env.VITE_PWA_ENABLED === "true" &&
process.env.NODE_ENV === "production"
process.env.NODE_ENV === NodeEnv.Prod
) {
register(`${process.env.BASE_URL}sw.js`, {
ready() {

View File

@@ -3,6 +3,7 @@ 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.
@@ -35,19 +36,19 @@ export class PlatformServiceFactory {
return PlatformServiceFactory.instance;
}
const platform = process.env.VITE_PLATFORM || "web";
const platform = process.env.VITE_PLATFORM || BuildPlatform.Web;
switch (platform) {
case "capacitor":
case BuildPlatform.Capacitor:
PlatformServiceFactory.instance = new CapacitorPlatformService();
break;
case "electron":
case BuildPlatform.Electron:
PlatformServiceFactory.instance = new ElectronPlatformService();
break;
case "pywebview":
case BuildPlatform.PyWebView:
PlatformServiceFactory.instance = new PyWebViewPlatformService();
break;
case "web":
case BuildPlatform.Web:
default:
PlatformServiceFactory.instance = new WebPlatformService();
break;

View File

@@ -7,6 +7,7 @@
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.
@@ -36,7 +37,7 @@ import { logger, safeStringify } from "../utils/logger";
* ```
*/
export const handleApiError = (error: AxiosError, endpoint: string) => {
if (process.env.VITE_PLATFORM === "capacitor") {
if (process.env.VITE_PLATFORM === BuildPlatform.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,4 +1,5 @@
import { logToDb } from "../db/databaseUtil";
import { BuildPlatform, NodeEnv } from "@/interfaces/build";
export function safeStringify(obj: unknown) {
const seen = new WeakSet();
@@ -21,7 +22,7 @@ export function safeStringify(obj: unknown) {
export const logger = {
debug: (message: string, ...args: unknown[]) => {
if (process.env.NODE_ENV !== "production") {
if (process.env.NODE_ENV !== NodeEnv.Prod) {
// eslint-disable-next-line no-console
console.debug(message, ...args);
// const argsString = args.length > 0 ? " - " + safeStringify(args) : "";
@@ -30,8 +31,8 @@ export const logger = {
},
log: (message: string, ...args: unknown[]) => {
if (
process.env.NODE_ENV !== "production" ||
process.env.VITE_PLATFORM === "capacitor"
process.env.NODE_ENV !== NodeEnv.Prod ||
process.env.VITE_PLATFORM === BuildPlatform.Capacitor
) {
// eslint-disable-next-line no-console
console.log(message, ...args);
@@ -41,9 +42,9 @@ export const logger = {
},
info: (message: string, ...args: unknown[]) => {
if (
process.env.NODE_ENV !== "production" ||
process.env.VITE_PLATFORM === "capacitor" ||
process.env.VITE_PLATFORM === "electron"
process.env.NODE_ENV !== NodeEnv.Prod ||
process.env.VITE_PLATFORM === BuildPlatform.Capacitor ||
process.env.VITE_PLATFORM === BuildPlatform.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 your activity):
(if you've let them see which posts are yours):
</div>
<div class="flex justify-center">
<EntityIcon
@@ -1573,24 +1573,25 @@ export default class AccountViewView extends Vue {
* @throws Will notify the user if there is an export error.
*/
public async exportDatabase() {
try {
// Generate the blob from the database
const blob = await this.generateDatabaseBlob();
throw new Error("Not implemented");
// 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-2xl text-center font-semibold relative px-7">
<h1 class="text-lg text-center font-light 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>
{{ stepType === "giver" ? "Given by..." : "Given to..." }}
Given by...
</h1>
</div>
@@ -19,18 +19,19 @@
<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 flex gap-2 items-center font-medium">
<font-awesome
icon="circle-question"
class="text-slate-400 text-4xl"
<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="italic text-slate-400">(Unnamed/Unknown)</span>
Unnamed/Unknown
</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('Unnamed')"
@click="openDialog()"
>
<font-awesome icon="gift" class="fa-fw"></font-awesome>
</button>
@@ -43,14 +44,13 @@
class="border-b border-slate-300 py-3"
>
<h2 class="text-base flex gap-4 items-center">
<span class="grow flex gap-2 items-center font-medium">
<span class="grow font-semibold">
<EntityIcon
:contact="contact"
:icon-size="34"
class="inline-block align-middle border border-slate-300 rounded-full overflow-hidden"
:icon-size="32"
class="inline-block align-middle border border-slate-300 rounded-md mr-1"
/>
<span v-if="contact.name">{{ contact.name }}</span>
<span v-else class="italic text-slate-400">(No name)</span>
{{ contact.name || "(no name)" }}
</span>
<span class="text-right">
<button
@@ -65,13 +65,7 @@
</li>
</ul>
<GiftedDialog
ref="customDialog"
:from-project-id="fromProjectId"
:to-project-id="toProjectId"
:show-projects="showProjects"
:is-from-project-view="isFromProjectView"
/>
<GiftedDialog ref="customDialog" :to-project-id="projectId" />
</section>
</template>
@@ -103,24 +97,6 @@ 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 {
@@ -148,41 +124,9 @@ export default class ContactGiftingView extends Vue {
);
}
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.projectId = (this.$route.query["projectId"] 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);
@@ -200,108 +144,17 @@ export default class ContactGiftingView extends Vue {
}
}
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,
);
}
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,
);
}
}
</script>

View File

@@ -100,6 +100,7 @@ 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";
@@ -148,7 +149,7 @@ export default class DeepLinkRedirectView extends Vue {
this.deepLinkUrl = `timesafari://${fullPathWithQuery}`;
this.webUrl = `${APP_SERVER}/${fullPathWithQuery}`;
this.isDevelopment = process.env.NODE_ENV !== "production";
this.isDevelopment = process.env.NODE_ENV !== NodeEnv.Prod;
this.userAgent = navigator.userAgent;
this.openDeepLink();

View File

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

View File

@@ -102,8 +102,7 @@ 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 {{ PASSKEYS_ENABLED ? "default" : "your" }} identifier
info
Show them your identification info
</button>
</div>
<UserNameDialog ref="userNameDialog" />
@@ -118,73 +117,101 @@ Raymer * @version 1.0.0 */
</div>
<div v-else id="sectionRecordSomethingGiven">
<!-- 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>
<!-- !isCreatingIdentifier && isRegistered -->
<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>
<!-- 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>
<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" :show-projects="showProjectsDialog" />
<GiftedDialog ref="customDialog" />
<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 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 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>
<div
@@ -450,7 +477,6 @@ 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
@@ -656,7 +682,7 @@ export default class HomeView extends Vue {
group: "alert",
type: "warning",
title: "Feed Loading Issue",
text: "Some feed data may be unavailable. Pull to refresh.",
text: "Some feed data may be unavailable. Try refreshing the page.",
},
5000,
);
@@ -1599,33 +1625,17 @@ export default class HomeView extends Vue {
* @param giver Optional contact info for giver
* @param description Optional gift 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,
);
}
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,
);
}
/**
@@ -1859,18 +1869,5 @@ 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,11 +214,63 @@
</div>
</div>
<GiftedDialog
ref="giveDialogToThis"
:to-project-id="projectId"
:is-from-project-view="true"
/>
<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" />
<!-- Offers & Gifts to & from this -->
<div class="grid items-start grid-cols-1 sm:grid-cols-3 gap-4 mt-4">
@@ -484,12 +536,7 @@
</button>
</div>
</div>
<GiftedDialog
ref="giveDialogFromThis"
:from-project-id="projectId"
:show-projects="true"
:is-from-project-view="true"
/>
<GiftedDialog ref="giveDialogFromThis" :from-project-id="projectId" />
<h3 class="text-lg font-bold mb-3 mt-4">
Benefitted From This Project
@@ -1223,52 +1270,21 @@ export default class ProjectViewView extends Vue {
);
}
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}`,
);
}
openGiftDialogToProject(contact?: libsUtil.GiverReceiverInputInfo) {
(this.$refs.giveDialogToThis as GiftedDialog).open(
contact,
undefined,
undefined,
(contact?.name || "Someone not named") + ` gave to this project`,
);
}
openGiftDialogFromProject() {
// Set the project as giver and the current user as recipient
(this.$refs.giveDialogFromThis as GiftedDialog).open(
{
did: undefined,
name: this.name,
handleId: this.projectId,
image: this.imageUrl,
},
undefined,
{ did: this.activeDid, name: "You" },
undefined,
`${this.name} gave to you`,
undefined,
undefined,
`This project gave to you`,
);
}

View File

@@ -144,7 +144,7 @@ export default class QuickActionBvcBeginView extends Vue {
"HUR",
BVC_MEETUPS_PROJECT_CLAIM_ID,
);
if (timeResult.type === "success") {
if (timeResult.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?.userMessage ||
timeResult?.error ||
"There was an error sending the time.",
},
5000,
@@ -171,7 +171,7 @@ export default class QuickActionBvcBeginView extends Vue {
apiServer,
axios,
);
if (attendResult.type === "success") {
if (attendResult.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?.userMessage ||
attendResult?.error ||
"There was an error sending the attendance.",
},
5000,

View File

@@ -1,55 +0,0 @@
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 user01Did = await generateNewEthrUser(page);
const newUserDid = 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(user01Did + ', A Friend');
await page.getByPlaceholder('URL or DID, Name, Public Key').fill(newUserDid + ', 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 Hours/i }).click();
await page.getByRole('button').filter({ hasText: /See Actions/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, user01Did);
await switchToUser(page, newUserDid);
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.*"]
"include": ["vite.config.*", "./src/interfaces/build.ts"]
}

View File

@@ -1,4 +1,5 @@
import { defineConfig } from "vite";
import { createBuildConfig } from "./vite.config.common.mts";
import { BuildPlatform } from "./src/interfaces/build.ts";
export default defineConfig(async () => createBuildConfig('capacitor'));
export default defineConfig(async () => createBuildConfig(BuildPlatform.Capacitor));

View File

@@ -45,71 +45,73 @@ 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,31 +1,45 @@
import { defineConfig, UserConfig, Plugin } from "vite";
import { defineConfig, UserConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import dotenv from "dotenv";
import { loadAppConfig } from "./vite.config.utils.mts";
import { loadAppConfig } from "./vite.config.common-utils.mts";
import path from "path";
import { fileURLToPath } from 'url';
import { NodeEnv, BuildEnv, BuildPlatform } from "./src/interfaces/build.ts";
// Load environment variables
dotenv.config();
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}` });
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export async function createBuildConfig(mode: string): Promise<UserConfig> {
export async function createBuildConfig(platform: BuildPlatform): Promise<UserConfig> {
const appConfig = await loadAppConfig();
const isElectron = mode === "electron";
const isCapacitor = mode === "capacitor";
const isPyWebView = mode === "pywebview";
console.log(`Platform: ${platform}`);
const isElectron = platform === BuildPlatform.Electron;
const isCapacitor = platform === BuildPlatform.Capacitor;
const isPyWebView = platform === BuildPlatform.PyWebView;
// Explicitly set platform and disable PWA for Electron
process.env.VITE_PLATFORM = mode;
process.env.VITE_PWA_ENABLED = isElectron ? 'false' : 'true';
process.env.VITE_PLATFORM = platform;
process.env.VITE_PWA_ENABLED = (isElectron || isPyWebView || isCapacitor)
? '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()],
@@ -56,7 +70,7 @@ export async function createBuildConfig(mode: string): Promise<UserConfig> {
},
define: {
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
'process.env.VITE_PLATFORM': JSON.stringify(mode),
'process.env.VITE_PLATFORM': JSON.stringify(platform),
'process.env.VITE_PWA_ENABLED': JSON.stringify(!isElectron),
'process.env.VITE_DISABLE_PWA': JSON.stringify(isElectron),
__dirname: isElectron ? JSON.stringify(process.cwd()) : '""',
@@ -78,10 +92,10 @@ export async function createBuildConfig(mode: string): Promise<UserConfig> {
'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': mode === 'development'
'nostr-tools/nip06': buildEnv === BuildEnv.Development
? 'nostr-tools/nip06'
: path.resolve(__dirname, 'node_modules/nostr-tools/nip06'),
'nostr-tools/core': mode === 'development'
'nostr-tools/core': buildEnv === BuildEnv.Development
? 'nostr-tools'
: path.resolve(__dirname, 'node_modules/nostr-tools'),
'nostr-tools': path.resolve(__dirname, 'node_modules/nostr-tools'),
@@ -108,4 +122,4 @@ export async function createBuildConfig(mode: string): Promise<UserConfig> {
};
}
export default defineConfig(async () => createBuildConfig('web'));
export default defineConfig(async () => createBuildConfig(BuildPlatform.Web));

View File

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

View File

@@ -1,9 +1,10 @@
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('electron');
const baseConfig = await createBuildConfig(BuildPlatform.Electron);
return mergeConfig(baseConfig, {
build: {

View File

@@ -1,71 +0,0 @@
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,4 +1,5 @@
import { defineConfig } from "vite";
import { createBuildConfig } from "./vite.config.common.mts";
import { BuildPlatform } from "./src/interfaces/build.ts";
export default defineConfig(async () => createBuildConfig('pywebview'));
export default defineConfig(async () => createBuildConfig(BuildPlatform.PyWebView));

View File

@@ -1,53 +0,0 @@
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

@@ -1,17 +1,18 @@
import { defineConfig, mergeConfig } from "vite";
import { VitePWA } from "vite-plugin-pwa";
import { createBuildConfig } from "./vite.config.common.mts";
import { loadAppConfig } from "./vite.config.utils.mts";
import { loadPwaConfig } from "./vite.config.common-utils.mts";
import { BuildPlatform } from "./src/interfaces/build.ts";
export default defineConfig(async () => {
const baseConfig = await createBuildConfig('web');
const appConfig = await loadAppConfig();
const baseConfig = await createBuildConfig(BuildPlatform.Web);
const pwaConfig = await loadPwaConfig();
return mergeConfig(baseConfig, {
plugins: [
VitePWA({
registerType: 'autoUpdate',
manifest: appConfig.pwaConfig?.manifest,
manifest: pwaConfig.manifest,
devOptions: {
enabled: false
},