Compare commits

...

25 Commits

Author SHA1 Message Date
Matthew Raymer
02bf0b3f1a docs: add comprehensive JSDoc documentation to views
Changes:
- Add detailed JSDoc headers to ContactImportView
- Add component-level documentation to ProjectViewView
- Document state management and data flow
- Add security considerations and usage examples
- Improve test script documentation and organization
- Add interface documentation for deep linking

This improves code maintainability by documenting component
architecture, workflows and integration points.
2025-02-28 12:45:21 +00:00
Matthew Raymer
89d970da1d docs: improve endorserServer.ts documentation and types
Changes:
- Add comprehensive JSDoc headers with examples
- Improve function documentation with param/return types
- Add module-level documentation explaining purpose
- Clean up testRecursivelyOnStrings implementation
- Add type annotations to cache functions
- Simplify serverMessageForUser implementation

This improves code maintainability by adding clear documentation
and improving type safety throughout the endorser server module.
2025-02-27 13:00:53 +00:00
Matthew Raymer
cb03df9240 fix: linkage of capacitor core to use relative links 2025-02-26 11:17:46 +00:00
Matthew Raymer
20620c3aae refactor: reorganize deep linking types and interfaces
Changes:
- Move deep link types from types/ to interfaces/
- Export baseUrlSchema for external use
- Add trailing commas for better git diffs
- Fix type inference for deepLinkSchemas
- Add deepLinks export to interfaces/index.ts
- Remove duplicate SuccessResult interface
- Update import paths in services/deepLinks.ts

This improves code organization by centralizing interface definitions
and fixing type inference issues.
2025-02-26 10:28:55 +00:00
Matthew Raymer
9d04db4a71 docs: add comprehensive deep linking documentation
Changes:
- Add detailed JSDoc headers to deep linking files
- Document type system and validation strategy
- Add architecture overview and error handling docs
- Include usage examples and integration points
- Improve code organization comments

This improves maintainability by documenting the deep linking
system's architecture, type safety, and integration points.
2025-02-26 09:45:08 +00:00
Matthew Raymer
1a9c97fe88 feat(deepLinks): implement comprehensive deep linking system
- Add type-safe deep link parameter validation using Zod
- Implement consistent error handling across all deep link routes
- Add support for query parameters in deep links
- Create comprehensive deep linking documentation
- Add logging for deep link operations

Security:
- Validate all deep link parameters before processing
- Sanitize and type-check query parameters
- Add error boundaries around deep link handling
- Implement route-specific parameter validation

Testing:
- Add parameter validation tests
- Add error handling tests
- Test query parameter support
2025-02-26 09:35:04 +00:00
Matthew Raymer
3b4f4dc125 style: reorder v-model and v-bind directives
Changes:
- Move v-model directives before other attributes
- Move v-bind directives before event handlers
- Reorder attributes for better readability
- Fix template attribute ordering across components
- Improve eslint rules
- add default vite config for testing (handles nostr error too)
This follows Vue.js style guide recommendations for attribute
ordering and improves template consistency.
2025-02-26 09:27:04 +00:00
Matthew Raymer
f6802cd160 refactor: improve router type safety and usage
- Add explicit Router type imports across views
- Replace $router type casting with proper typing
- Use $router.back() instead of $router.go(-1) for consistency
- Add proper route and router typings to components
- Clean up router navigation methods
- Fix router push/back method calls

This commit improves type safety and consistency in router usage across
the application's view components.
2025-02-26 06:50:08 +00:00
Matthew Raymer
a2e19d7e9a fix: improve TypeScript type safety across views
Changes:
- Add proper type annotations for component properties
- Fix null checks with optional chaining
- Add missing interface properties
- Replace any with proper types where possible
- Move interfaces from endorserServer to interfaces/
- Add proper Router and Route typing
- Add default empty string for optional text fields

This improves type safety and reduces TypeScript errors across views.
2025-02-25 11:36:24 +00:00
Matthew Raymer
42055a2d66 fix: update component and import paths
Changes:
- Update font-awesome component closing tag to match naming
- Change @capacitor/app import to use local wrapper
- Fix component self-closing tags in ContactScanView.vue

This improves consistency in component usage and centralizes
capacitor imports through our wrapper layer.
2025-02-25 09:58:31 +00:00
Matthew Raymer
dc16cb393e refactor: move ProviderInfo interface to claims-result.ts
- Move ProviderInfo interface from ClaimView.vue to claims-result.ts
- Add JSDoc documentation to interface properties
- Update imports in ClaimView.vue
- Clean up route handling using vue-router types
- Remove outdated browser compatibility comment

This improves type organization and documentation while reducing
component-level interface definitions.
2025-02-25 09:03:18 +00:00
Matthew Raymer
c708716675 refactor: migrate interfaces to dedicated directory
Reorganizes TypeScript interfaces into a modular structure:
- Create dedicated interfaces directory with specialized files
- Split interfaces by domain (claims, common, limits, records, user)
- Update imports in endorserServer.ts to use new interface locations
- Replace 'any' types with 'unknown' for better type safety
- Add proper type imports and exports

This improves code organization and maintainability by:
- Centralizing interface definitions
- Reducing file size of endorserServer.ts
- Making interface relationships more explicit
- Improving type safety with stricter types
2025-02-24 11:21:08 +00:00
Matthew Raymer
fbb9fba347 docs(capacitor): improve main.capacitor.ts documentation and error handling
- Add comprehensive JSDoc header with process flow
- Document supported deep link routes
- Improve error type handling in deep link handler
- Add debug logging for initialization steps
- Update version number to 0.4.4

This improves code maintainability and debugging capabilities for
the Capacitor platform entry point.
2025-02-24 09:37:30 +00:00
Matthew Raymer
3b7a872ae1 refactor: update nostr-tools imports for better tree shaking
Changes:
- Import specific functions from nostr-tools instead of full module
- Replace nip06.accountFromExtendedKey with direct import
- Update related function calls to use imported version

This change reduces bundle size by enabling better tree shaking
of unused nostr-tools functionality.
2025-02-21 11:53:21 +00:00
Matthew Raymer
a8e15804a6 WIP: certificate view and dependency updates
- Update certificate view canvas rendering and QR code generation
- Upgrade dependencies (expo-file-system, expo-font, expo-keep-awake)
- Fix type imports for nostr-tools and dexie-export-import
- Update vite config for better dependency resolution
- Clean up main entry points (capacitor, electron, pywebview)
- Improve error handling in API and plan services
- Add type safety to API error handling
- Update build configuration for platform-specific builds

This is a work in progress commit focusing on certificate view improvements
and dependency maintenance. Some type definitions and build configurations
may need further refinement.
2025-02-21 09:47:24 +00:00
Matthew Raymer
cee7a6ded3 feat(logging): enhance debug logging across app
Improves application logging and error tracking:
- Add structured logging in main.common.ts for app initialization
- Enhance API error handling with detailed context in services
- Add deep link debugging in Capacitor platform
- Improve plan service logging with retry information
- Update endorser server logs for better cache debugging

Technical changes:
- Replace console.error with info for non-critical cache misses
- Add component context to global error handler
- Add detailed logging for plan loading and retries
- Improve deep link route matching logs
- Add mount state logging for Capacitor

This improves debugging capabilities across web and mobile platforms.
2025-02-20 10:36:47 +00:00
Matthew Raymer
d2157a7d8c feat(mobile): add deep linking support for Capacitor apps
Adds native deep linking capabilities:
- Configure timesafari:// URL scheme for iOS and Android
- Add @capacitor/app dependency and configuration
- Implement deep link handler with improved error logging
- Support parameterized routes like claim/:id
- Add debug logging for native platforms
- Handle app mounting state for deep links

Technical changes:
- Update AndroidManifest.xml with intent filters
- Add URL scheme to iOS Info.plist
- Add @capacitor/app to Podfile and Gradle
- Enhance main.capacitor.ts with robust deep link handling
2025-02-19 13:07:08 +00:00
Matthew Raymer
fbdf72557c fix: disable PWA for Capacitor builds
Updates PWA configuration to:
- Disable PWA features for Capacitor builds
- Add @capacitor/app dependency
- Update environment variable handling in build config

This prevents conflicts between PWA and native app functionality
when building for mobile platforms.

Technical changes:
- Add isCapacitor check to PWA disable logic
- Update VITE_PWA_ENABLED environment variable definition
- Add @capacitor/app to package dependencies
2025-02-18 11:54:42 +00:00
Matthew Raymer
74a412745a refactor: reorganize Vite config into modular files
Split monolithic vite.config.mjs into separate config files:
- vite.config.web.mts
- vite.config.electron.mts
- vite.config.capacitor.mts
- vite.config.pywebview.mts
- vite.config.common.mts
- vite.config.utils.mts

Updates:
- Modify package.json scripts to use specific config files
- Add electron-builder as dev dependency
- Update electron build configuration
- Fix electron resource paths
- Remove old vite.config.mjs and utils.js

This change improves maintainability by:
- Separating concerns for different build targets
- Making build configurations more explicit
- Reducing complexity in individual config files
2025-02-18 11:44:06 +00:00
Matthew Raymer
eaf0b76e9e chore: cleanup console logs and test directories
- Remove commented console.log statements from main.ts
- Add test output directories to .gitignore:
  - playwright-tests/
  - test-playwright/
  - test-playwright-results/

Keeps repository clean by excluding test artifacts and removing
unused logging statements.
2025-02-18 09:04:01 +00:00
eabe2b9448 fix problem when going directly to people-map where the search results disappear 2025-02-17 20:35:05 -07:00
5eaaf32043 bump version and add "-beta" (was 0.4.3 now 0.4.4-beta) 2025-02-17 19:22:03 -07:00
1e9c3f3101 add Discover query param searchPeople to go straight to people map 2025-02-17 19:19:38 -07:00
2e60e2bba9 bump version and add "-beta" 2025-02-17 08:55:07 -07:00
78d7f38aa3 Merge pull request 'new build process to allow building native apps' (#126) from split_build_process into master
Reviewed-on: #126
2025-02-17 10:50:47 -05:00
118 changed files with 8097 additions and 2638 deletions

View File

@@ -5,30 +5,27 @@ module.exports = {
es2022: true,
},
extends: [
"plugin:vue/vue3-essential",
"plugin:vue/vue3-recommended",
"eslint:recommended",
"@vue/typescript/recommended",
"plugin:prettier/recommended",
"plugin:prettier/recommended"
],
// parserOptions: {
// ecmaVersion: 2020,
// },
rules: {
"max-len": [
"warn",
{
code: 120,
ignoreComments: true, // why does this not make it allow comment of any length?
ignorePattern: '^\\s*class="[^"]*"$',
ignoreStrings: true,
ignoreTemplateLiterals: true,
ignoreTrailingComments: true,
ignoreUrls: true,
},
],
"no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
"no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off",
// "prettier/prettier": ["warn", { printWidth: 120 }], // removes errors but adds thousands of warnings
"@typescript-eslint/no-unnecessary-type-constraint": "off",
"max-len": ["warn", {
code: 100,
ignoreComments: true,
ignorePattern: '^\\s*class="[^"]*"$',
ignoreStrings: true,
ignoreTemplateLiterals: true,
ignoreUrls: true,
}],
"no-console": process.env.NODE_ENV === "production" ? "error" : "warn",
"no-debugger": process.env.NODE_ENV === "production" ? "error" : "warn",
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-unnecessary-type-constraint": "off"
},
};

5
.gitignore vendored
View File

@@ -36,4 +36,7 @@ pnpm-debug.log*
/playwright/.cache/
/dist-electron-build/
/dist-capacitor/
/test-playwright-results/
/test-playwright-results/
playwright-tests
test-playwright
dist-electron-packages

View File

@@ -14,12 +14,14 @@ This guide explains how to build TimeSafari for different platforms.
## Initial Setup
1. Clone the repository:
```bash
git clone [repository-url]
cd TimeSafari
```
2. Install dependencies:
```bash
npm install
```
@@ -29,6 +31,7 @@ This guide explains how to build TimeSafari for different platforms.
To build for web deployment:
1. Run the production build:
```bash
npm run build
```
@@ -36,6 +39,7 @@ To build for web deployment:
2. The built files will be in the `dist` directory.
3. To test the production build locally:
```bash
npm run serve
```
@@ -45,11 +49,13 @@ To build for web deployment:
### Building for Linux
1. Build the electron app in production mode:
```bash
npm run build:electron-prod
```
2. Package the Electron app for Linux:
```bash
# For AppImage (recommended)
npm run electron:build-linux
@@ -65,12 +71,14 @@ To build for web deployment:
### Running the Packaged App
- AppImage: Make executable and run
```bash
chmod +x dist-electron-packages/TimeSafari-*.AppImage
./dist-electron-packages/TimeSafari-*.AppImage
```
- DEB: Install and run
```bash
sudo dpkg -i dist-electron-packages/timesafari_*_amd64.deb
timesafari
@@ -95,21 +103,25 @@ npm run build:electron-prod && npm run electron:start
Prerequisites: macOS with Xcode installed
1. Build the web assets:
```bash
npm run build -- --mode capacitor
```
2. Add iOS platform if not already added:
```bash
npx cap add ios
```
3. Update iOS project with latest build:
```bash
npx cap sync ios
```
4. Open the project in Xcode:
```bash
npx cap open ios
```
@@ -121,21 +133,25 @@ Prerequisites: macOS with Xcode installed
Prerequisites: Android Studio with SDK installed
1. Build the web assets:
```bash
npm run build -- --mode capacitor
```
2. Add Android platform if not already added:
```bash
npx cap add android
```
3. Update Android project with latest build:
```bash
npx cap sync android
```
4. Open the project in Android Studio:
```bash
npx cap open android
```
@@ -147,25 +163,29 @@ Prerequisites: Android Studio with SDK installed
To run the application in development mode:
1. Start the development server:
```bash
npm run dev
```
## PyWebView Desktop Build
### Prerequisites
### Prerequisites for PyWebView
- Python 3.8 or higher
- pip (Python package manager)
- virtualenv (recommended)
- System dependencies:
```bash
# For Ubuntu/Debian
sudo apt-get install python3-webview
# or
sudo apt-get install python3-gi python3-gi-cairo gir1.2-gtk-3.0 gir1.2-webkit2-4.0
# For Arch Linux
sudo pacman -S webkit2gtk python-gobject python-cairo
# For Fedora
sudo dnf install python3-webview
# or
@@ -173,7 +193,9 @@ To run the application in development mode:
```
### Setup
1. Create and activate a virtual environment (recommended):
```bash
python -m venv .venv
source .venv/bin/activate # On Linux/macOS
@@ -182,6 +204,7 @@ To run the application in development mode:
```
2. Install Python dependencies:
```bash
pip install -r requirements.txt
```
@@ -189,13 +212,16 @@ To run the application in development mode:
### Troubleshooting
If encountering PyInstaller version errors:
```bash
# Try installing the latest stable version
pip install --upgrade pyinstaller
```
### Development
### Development of PyWebView
1. Start the PyWebView development build:
```bash
npm run pywebview:dev
```
@@ -203,31 +229,39 @@ pip install --upgrade pyinstaller
### Building for Distribution
#### Linux
```bash
npm run pywebview:package-linux
```
The packaged application will be in `dist/TimeSafari`
#### Windows
```bash
npm run pywebview:package-win
```
The packaged application will be in `dist/TimeSafari`
#### macOS
#### macOS
```bash
npm run pywebview:package-mac
```
The packaged application will be in `dist/TimeSafari`
## Testing
Run local tests:
```bash
npm run test-local
```
Run all tests (includes building):
```bash
npm run test-all
```
@@ -235,11 +269,13 @@ npm run test-all
## Linting
Check code style:
```bash
npm run lint
```
Fix code style issues:
```bash
npm run lint-fix
```
@@ -259,42 +295,46 @@ See `.env.*` files for configuration.
## Deployment
### Version Management
1. Update CHANGELOG.md with new changes
2. Update version in package.json
3. Commit changes and tag release:
```bash
git tag <VERSION_TAG>
git push origin <VERSION_TAG>
```
4. After deployment, update package.json with next version + "-beta"
### Test Server
```bash
# Build using staging environment
npm run build -- --mode staging
# Deploy to test server
rsync -azvu -e "ssh -i ~/.ssh/your_key" dist ubuntutest@test.timesafari.app:time-safari/
rsync -azvu -e "ssh -i ~/.ssh/<YOUR_KEY>" dist ubuntutest@test.timesafari.app:time-safari/
```
### Production Server
```bash
# On the production server:
pkgx +npm sh
cd crowd-funder-for-time-pwa
git checkout master && git pull
git checkout <version_tag>
git checkout <VERSION_TAG>
npm install
npm run build
cd -
# Backup and deploy
mv time-safari/dist time-safari-dist-prev.0
mv crowd-funder-for-time-pwa/dist time-safari/
mv time-safari/dist time-safari-dist-prev.0 && mv crowd-funder-for-time-pwa/dist time-safari/
```
### Version Management
1. Update CHANGELOG.md with new changes
2. Update version in package.json
3. Commit changes and tag release:
```bash
git tag <version>
git push origin <version>
```
4. After deployment, update package.json with next version + "-beta"
## Troubleshooting
## Troubleshooting Builds
### Common Build Issues
@@ -311,4 +351,3 @@ mv crowd-funder-for-time-pwa/dist time-safari/
- For iOS: Xcode command line tools must be installed
- For Android: Correct SDK version must be installed
- Check Capacitor configuration in capacitor.config.ts

View File

@@ -5,262 +5,348 @@ All notable changes to this project will be documented in this file.
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).
## [0.4.4] - 2025.02.17
== [0.4.2] - 2025.02.17
### Merged
- Master to split_process_build
- fixed path issues
- all tests passing
- capacitor build to Android working
### Fixed in 0.4.4
- On production (due to data?) the search results would disappear after scrolling down. Now we don't show any results when going to the people map with a shortcut.
## [0.4.3] - 2025.02.17
### Added in 0.4.3
- Discover query parameter searchPeople to go directly to the people map
## [0.4.2] - 2025.02.17
### Added
- Capacitor on iOS and Android
### Fixed
- Path issues
## [0.4.1] - 2025.02.16
### Fixed
### Fixed in 0.4.1
- nostr build issue
- Linting
## [0.4.0] - 2025.02.14
### Changed
- Images in the home feed now take up the full width of the card.
- Clicking the image previously, would open the image in a new tab. Now, clicking the image opens the image in a lightbox view.
### Added
### Added in 0.4.0
- Clicking an image also now displays an in-app lightbox view of the image.
- The lightbox view includes a download button for the image in mobile view.
## [0.3.57] - 2025.02.11
### Added
### Added in 0.3.57
- Automatic user creation in onboarding meetings
## [0.3.55] - 2025.02.07
### Added
### Added in 0.3.55
- End time for projects
## [0.3.54] - 2025.02.06
### Added
### Added in 0.3.54
- Group onboarding meetings
## [0.3.53] - 2025.01.30
### Added
### Added in 0.3.53
- Hints for contacting the creator of a project
## [0.3.52] - 2025.01.22
### Fixed
### Fixed in 0.3.52
- User profile endpoint server for map was broken.
## [0.3.51] - 2025.01.22
### Fixed
### Fixed in 0.3.51
- User profile map jumped on first zoom.
## [0.3.50] - 2025.01.20 - b9fedcd3fd3e34c3fb0fc79150d1a81a76eaeb40
### Added
### Added in 0.3.50
- User public profiles
## [0.3.49] - 2025.01.09 - 36301ed238ff84df25bb11a8d44a295ee7eaf0f8
### Changed
### Changed in 0.3.49
- Make all external contact links direct to the contact-import page.
- Handle all new-single-contact JWTs in the contacts page, and multiple-contact JWTs in the contacts-import page.
## [0.3.48] - 2025.01.08 - 398f3e64a376789f7eb1c400cd886f5a2cacd588 (but app shows 07c4e58)
### Added
### Added in 0.3.48
- More sanity-checks on contact-import JWT
## [0.3.47] - 2025.01.06 - 5bf6dd1ee32ca7cc46d39bd7afca58365b422f93
### Added
### Added in 0.3.47
- Notes on contacts page with new contact-edit page
- Contact methods (only on contact-edit page and under DID details)
- DID view with no DID shows user's info.
### Changed
### Changed in 0.3.47
- URL for user's contact info is now URL to this app (not endorser.ch).
- Extended details (eg. full claim) is beneath details link on claim page.
## [0.3.46] - 2025.01.03 - 9e7056616b5e5acc51e5a8cf7354d408029fefb3
### Added
### Added in 0.3.46
- More action-oriented questions for the gift prompts
### Fixed
### Fixed in 0.3.46
- Contact-list import set visibility for all, even if not chosen.
## [0.3.45] - 2025.01.01 - 65402dc68ce69ccc6cb9aa8d2e7a9249bf4298e0
### Fixed
### Fixed in 0.3.45
- Previous project links stayed when following a link.
## [0.3.44] - 2024.12.31 - 694b22987b05482e4527c2478bbe15e6b6f3b532
### Added
### Added in 0.3.44
- Project counts on a map
## [0.3.42] - 2024.12.27 - 9751934bc24a1040415a8cfeacbae59ed91f92a5
### Added
### Added in 0.3.42
- Link from certificate page to the claim
### Changed
### Changed in 0.3.42
- Contact data sharing is now a verified JWT.
- Feed pictures are larger.
## [0.3.41] - 2024.12.21 - ff6d14138f26daea6216b051562f0a04681f69fc
### Added
### Added in 0.3.41
- Link from certificate page to the claim
## [0.3.40] - 2024.12.20 - 77290d9fed3c364243793dc3e9bfe2e994a016b8
### Added
### Added in 0.3.40
- Only show issuer on certificate if it's not the agent.
## [0.3.39] - 2024.12.20 - d8819155e2acd2b57fdab523168fa5d1d09e80cc
### Added
### Added in 0.3.39
- Page for a framed claim certificate
## [0.3.38] - 2024.12.14 - f8cae5ad4fee1f114320dcce052299eab12108b2
### Fixed
### Fixed in 0.3.38
- Error on BVC confirmation screen (from IndexedDB refactor)
## [0.3.37] - 2024.12.13 - 4d805b43cd25eed73cdd6651f36ad1ec8c109555
### Added
### Added in 0.3.37
- Record a give from a project on the project page.
- New button on home page opens the gifted dialog.
- On confirmation buttons on the project page gives, mark when unavailable and explain why.
### Changed
### Changed in 0.3.37
- Moved the secret into IndexedDB (and out of localStorage) for more reliability.
- New "invite" destination page helps troubleshoot when JWT link doesn't come through.
### Fixed
### Fixed in 0.3.37
- Problem showing claim issuer name
- Problem going "back" from a project page
## [0.3.36] - 2024.11.24 - c8d23647d165016f8a8f575e13d32583242e53ac
### Changed
### Changed in 0.3.36
- More friendly default reminder message
- Blue borders around people to indicate clickability
## [0.3.35] - 2024.11.24 - bff7d0a6320b70349185e26bfac72e3bb17f76df
### Added
### Added in 0.3.35
- Daily reliable, hard-coded notification message
- Setting to change the partner API server
## [0.3.33] - 2024.11.07 - adb7b16ecf1343c39cba71a7d6bb0e7a973e1102
### Fixed
### Fixed in 0.3.33
- Affirm Delivery button on offer claim page didn't work.
- Plans were not showing by default on project page.
## [0.3.32] - 2024.11.06 - 9a3fa38a3fd28f977e06f0265fc39e635c9c5ccd
### Added
### Added in 0.3.32
- Highlight in green new offers to user & to user's projects on the front page.
## [0.3.31] - 2024.10.25 - 07c02ab98a09d293dd90d9289a7872e7d681d296
### Changed
### Changed in 0.3.31
- Onboarding messages about offers
## [0.3.30]
### Added
### Added in 0.3.30
- Onboarding messages
## [0.3.29] - 2024.10.09 - babd3832bdfe0c40eaa3869de1b41399a51713c1
### Added
### Added in 0.3.29
- Invite for a contact to join immediately
### Changed
### Changed in 0.3.29
- Send signed data to nostr endpoints to verify public key ownership.
- Enhanced help & help onboarding.
### Changed in DB or environment
- Uses Endorser.ch version 4.1.1
## [0.3.28] - 2024.09.30 - 84720b94049d29cc0ddd99c50cef2e7176130133
### Added
### Added in 0.3.28
- Posting to nostr apps Trustroots & TripHopping
- Display of providers on claim view page
### Changed
### Changed in 0.3.28
- Switched BVC-meeting-ending gift to be a gift from the group.
### Changed in DB or environment
### Changed in DB or environment in 0.3.28
- Requires Endorser.ch version 4.1.0
## [0.3.27] - 2024.09.22 - ee23e6f005e47f5bd6f04d804599f6395371b0e4
### Fixed
### Fixed in 0.3.27
- Error loading BVC claims to confirm
- Really allow visibility of bulk-imported contacts
## [0.3.26] - 2024.09.16 - 8263ed2b29947b3ccc6f3133bbc9454c222bce28
### Added
### Added in 0.3.26
- Separate 'isRegistered' flag for each account
### Fixed
### Fixed in 0.3.26
- Failure to assign offers to their project
- Alert when looking at one's own activity if not in contacts.
## [0.3.25] - 2024.08.30 - dcbe02d877aecb4cdef2643d90e6595d246a9f82
### Added
### Added in 0.3.25
- "Ideas" now jumps directly to giving prompt or contact list.
### Fixed
### Fixed in 0.3.25
- Empty giver name on gifted-details view
- Previously visited project would show up on the giving-details page.
### Removed
### Removed in 0.3.25
- All unnecessary localStorage for project IDs
## [0.3.23] - 2024.08.30
### Added
### Added in 0.3.23
- Sections in Help for different kinds of users
- Discovery page parameters so that links with search text work
- Message when no projects are found
## [0.3.21] - 2024.08.24 - a7b89f4bb6da928d56daeffaae7741fa74cc80bf
### Added
### Added in 0.3.21
- Send list of contacts to someone, and move individual contact actions to detail page.
- Prompt for name in pop-up, and send to different contact-sharing screens.
### Changed
### Changed in 0.3.21
- Moved contact actions from list onto detail page
## [0.3.20] - 2024.08.18 - 4064eb75a9743ca268bf00016fa0a5fc5dec4e30
### Fixed
### Fixed in 0.3.20
- Bad "give" verbiage on offer page
- Failing offer test
## [0.3.19] - 2024.08.18 - ee9c14942ceba993bf21a11249601f205158ec71
### Added
### Added in 0.3.19
- Update of an offer
- Recipient description in offer list
### Fixed
### Fixed in 0.3.19
- List of offers wasn't showing.
- Destination page after sharing photo was wrong.
## [0.3.17] - 2024.07.11 - cefa384ff1a2d922848c370640c096c529920fab
### Added
### Added in 0.3.17
- Photos on more screens
### Fixed
### Fixed in 0.3.17
- Share of a photo, including sharing a photo from webkit/Safari which never worked
### Changed in DB or environment
### Changed in DB or environment in 0.3.17
- Nothing (though there's a new temp field in IndexedDB)
## [0.3.15] - 2024.08.04 - c8f0f2c2b16b9f0b4b47d40f7bf29058c7baa68e
### Added
### Added in 0.3.15
- Edit gives
- Page to edit claim JSON before submitting
- Update of imported contacts
@@ -271,263 +357,364 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Cache signatures for reports for passkey-signed requests
- Refactor: consolidate alternative signing, eg. for passkeys & did:peer
- Playwright tests
### Changed
- Linked projects display below description (instead of at bottom)
### Fixed
- Visibility toggle appearance
### Changed in DB or environment
- Nothing
### Changed in 0.3.15
- Linked projects display below description (instead of at bottom)
### Fixed in 0.3.15
- Visibility toggle appearance
### Changed in DB or environment in 0.3.15
- Nothing
## [0.3.14] - 2024.06.22 - 1611d22892f683f43856d2503eee7f391b6bbce8
### Added
- Clearer give-confirmation screen
- BX currency https://thebx.medium.com/
- Deselection of project on gifted details page
### Fixed
- Don't show registration pop-up for a new contact that is registered
### Changed in DB or environment
- Nothing
### Added in 0.3.14
- Clearer give-confirmation screen
- BX currency <https://thebx.medium.com/>
- Deselection of project on gifted details page
### Fixed in 0.3.14
- Don't show registration pop-up for a new contact that is registered
### Changed in DB or environment in 0.3.14
- Nothing
## [0.3.13] - 2024.05.24 - 08b67984e443c58d9178ad3776013b0bce7afddc
### Added
- Photos on projects
### Changed in DB or environment
- Nothing
### Added in 0.3.13
- Photos on projects
### Changed in DB or environment in 0.3.13
- Nothing
## [0.3.12] - 2024.05.19 - 141fb39ad19c44d82fe1a33bf85115beacf50870
### Fixed
- Photo share (share_target) failed because requests were sent to server
### Changed in DB or environment
- Nothing
### Fixed in 0.3.12
- Photo share (share_target) failed because requests were sent to server
### Changed in DB or environment in 0.3.12
- Nothing
## [0.3.11] - 2024.05.19 - 567bcad88dfb7e9ac8fea72530d1163985e4a7cc
### Added
- Choose a file for gifts, and a URL for gifts & profiles
### Fixed
- Multiple button pushes were required to switch camera
### Changed in DB or environment
- Nothing
### Added in 0.3.11
- Choose a file for gifts, and a URL for gifts & profiles
### Fixed in 0.3.11
- Multiple button pushes were required to switch camera
### Changed in DB or environment in 0.3.11
- Nothing
## [0.3.10] - 2024.05.11 - 03ac31d98110f7828cf9acb366db8d01b185f64c
### Added
### Added in 0.3.10
- Share an image
- Choose a file on the device for a profile image
### Changed in DB or environment
### Changed in DB or environment in 0.3.10
- Nothing
## [0.3.9] - 2024.04.28 - 874e717e698b93a1ace9f588e675b8a3dccd7617
### Added
### Added in 0.3.9
- Offers on contacts page
- Checks on front page until they show as registered
### Changed
### Changed in 0.3.9
- Scanned contacts now add immediately and prompt for registration.
- Better UI for gives on contact page
- Better UI for all confirmation messages
### Fixed
- Repeated elements at top of main feed
### Changed in DB or environment
- Nothing
### Fixed in 0.3.9
- Repeated elements at top of main feed
### Changed in DB or environment in 0.3.9
- Nothing
## [0.3.8] - 2024.04.20 - 15c026c80ce03a26cae3ff80b0888934c101c7e2
### Added
### Added in 0.3.8
- Profile image for user
### Fixed
### Fixed in 0.3.8
- Slow loading of home page feed
### Changed in DB or environment
### Changed in DB or environment in 0.3.8
- Nothing
## [0.3.7] - 2024.04.10 - cf18f1543a700d62a5f9e764905a4aafe1fb229b
### Added
### Added in 0.3.7
- Filter on home page feed
- Ability to set time of daily notification
- Jump to app on click of notification
### Changed
### Changed in 0.3.7
- Built with vite
- Descriptions on home page to include projects
### Changed in DB or environment
### Changed in DB or environment in 0.3.7
- Nothing
## [0.3.6] - 2024.03.24 - 3a07e31d6313ab95711265562d9023c42916e141
### Added
### Added in 0.3.6
- Button to mirror photo during video
- More detailed onboarding help screen
- Public-data blurb
### Changed in DB or environment
### Changed in DB or environment in 0.3.6
- Nothing
## [0.3.5] - 2024.03.23 - 28754bdfb1e11aa221dd49a5dce4219b69cf6a9d
### Added
### Added in 0.3.5
- Photo on gift records
### Fixed
### Fixed in 0.3.5
- Environment variable for BVC meetings project
- Environment variables and build enhancements for test vs prod
### Changed in DB or environment
### Changed in DB or environment in 0.3.5
- New environment variable for image API server
- Test that a new browser session will get the right default APIs.
- Test that a new browser session will send the right BVC meetings project.
## [0.2.17] - 2024.03.01 - 3612ea42240c5e1b7d7eff29a39ff18f1b869b36
### Added
- Shortcut page for Bountiful Voluntaryist Community
### Changed
- More readable, targeted summaries in home-page feed items
### Changed in DB
- Nothing
### Added in 0.2.17
- Shortcut page for Bountiful Voluntaryist Community
### Changed in 0.2.17
- More readable, targeted summaries in home-page feed items
### Changed in DB
- Nothing
## [0.2.14] - 2024.02.14 - 5f9edea1167dbfb64e16648764eed8c09b24eaeb
### Changed
- Combine all service worker scripts into a single file.
### Changed in DB
- Nothing
### Changed in 0.2.14
- Combine all service worker scripts into a single file.
### Changed in DB in 0.2.14
- Nothing
## [0.2.13] - 2024.02.07
### Added
### Added in 0.2.13
- Display of user's offers
- Check for valid DIDs
### Fixed
### Fixed in 0.2.13
- Name display on give prompt
- Non-numbers on number input & autocapitalize on URL input
### Changed in DB
### Changed in DB in 0.2.13
- Nothing
## [0.2.12] - 2024.02.01
### Added
### Added in 0.2.12
- Prompts for gratitude
## [0.2.11] - 2024.01.28
### Added
### Added in 0.2.11
- Actions to share claim data with contacts
- Bulk CSV import from Endorser Mobile export
- Dates on give summaries
## [0.2.10] - 2024.01.18 - 667e1e8890b42de59cd939caca1a01c7a7a702be
### Added
### Added in 0.2.10
- Person identicons for contacts
- Confirmation & delivery directly from project page
- Offer dialog now allows units
- Links from claim detail page to the fulfilled project or offer
- Link to project from home feed
- Copy to clipboard in more places
### Fixed
### Fixed in 0.2.10
- "More Contacts" for give on project page now links correctly.
## [0.2.9] - 2024.01.15 - e5e702f8a5a53a6efbed48d35f0bc3cee63024a0
### Fixed
### Fixed in 0.2.9
- Set visibility for new contact.
## [0.2.8] - 2024.01.14
### Added
### Added in 0.2.8
- Automatic ID creation from home page
- Agent who can also edit a project
### Fixed
### Fixed in 0.2.8
- Cannot declare anonymous gift
## [0.2.7] - 2024.01.12
### Added
### Added in 0.2.7
- Give to fulfill a particular offer
- Give as part of a trade as opposed to a donation
- Error notifications on import
### Changed
### Changed in 0.2.7
- Library security updates
- Visibility of actions & confirmations on claim page
### Fixed
### Fixed in 0.2.7
- Name of offerer
## [0.2.2] - 2024.01.05
### Added
### Added in 0.2.2
- Check for notification capability on front screen
- Contact next-public-key-hash in manual textual input
- Confirmation for contact visibility change
- YAML rendering of full claim details
- Hints for onboarding on the contact screen
## [0.2.0] - 2024.01.04
### Added
### Added in 0.2.0
- Contact next-public-key-hash
- Icon for Android
- More thorough messaging and testing for notifications
## [0.1.9] - 2024.01.01
### Added
### Added in 0.1.9
- Import for contacts and settings
- Second download button for DuckDuckGo
### Changed
### Changed in 0.1.9
- Removed some keys from Dexie's IndexedDB declarations
## [0.1.8] - 2023.12.27- d26d1d360152a7d0e559b68486e85b72b88bd9ff
### Added
### Added in 0.1.8
- DB logging for service-worker events
- Help page for notifications
- Test notification & web-push triggers inside app
- Check that the app is installed
### Fixed
### Fixed in 0.1.8
- Project issuer display name
## [0.1.7] - 2023.12.19 - 91c6c7c11c71f96006cc876fc946f1f98a274ba2
### Changed
### Changed in 0.1.7
- Icons
### Fixed
### Fixed in 0.1.7
- Notification switch now shows message
- Prod/test server warning message at top of page
## [0.1.6] - 2023.12.17 - b445b1234fbfcf6b37d695373f259aab0eda1118
### Added
### Added in 0.1.6
- Infinite scroll on home page
### Changed
### Changed in 0.1.6
- UI improvements
- Show web-push subscription info
- Icon
## [0.1.5] - 2023.12.09 - 9c36bb509a9bae9bb3306d3bd9eeb144b67aa8ad
### Added
### Added in 0.1.5
- Web push notifications (though not finalized)
- Credentials details page
- See more data without an ID
- Change units of a give
## [0.1.4] - 2023.11.20 - 7311d36726f3667ec4c68f241f91d404273ad4db
### Added
### Added in 0.1.4
- Offer on a project
### Changed
### Changed in 0.1.4
- Automatically set as visible when importing a contact
## [0.1.3] - 2023.11.08 - 910f57ec7d2e50803ae3d04f4b927e0f5219fbde
### Added
### Added in 0.1.3
- Contact name editing
### Changed
### Changed in 0.1.3
- Don't show actions on front page if not registered.
### Removed
### Removed in 0.1.3
- Home page Notiwind test buttons
## [0.1.2] - 2023.11.01 - 7f6c93802911a030a89fe3706e18b5c17151e5bb
### Added
### Added in 0.1.2
- Basics: create ID, record a give, declare a project, search, and get notifications.

View File

@@ -8,8 +8,6 @@ and expand to crowd-fund with time & money, then record and see the impact of co
See [project.task.yaml](project.task.yaml) for current priorities.
(Numbers at the beginning of lines are estimated hours. See [taskyaml.org](https://taskyaml.org/) for details.)
## Setup & Building
Quick start:
@@ -19,15 +17,17 @@ npm install
npm run dev
```
See the test locations for "IMAGE_API_SERVER" or "PARTNER_API_SERVER" below, or use http://localhost:3000 for local endorser.ch
See the test locations for "IMAGE_API_SERVER" or "PARTNER_API_SERVER" below, or use <http://localhost:3000> for local endorser.ch
### Build the test & production app
```
```bash
npm run serve
```
### Lint and fix files
```
```bash
npm run lint
```
@@ -35,7 +35,6 @@ npm run lint
Look below for the "test-all" instructions.
### Compile and minify for test & production
* If there are DB changes: before updating the test server, open browser(s) with current version to test DB migrations.
@@ -52,15 +51,19 @@ Look below for the "test-all" instructions.
* 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_PASSKEYS_ENABLED=true npm run build
```
... and transfer to the test server: `rsync -azvu -e "ssh -i ~/.ssh/..." dist ubuntutest@test.timesafari.app:time-safari`
... and transfer to the test server:
(Let's replace that with a .env.development or .env.staging file.)
```bash
rsync -azvu -e "ssh -i ~/.ssh/..." dist ubuntutest@test.timesafari.app:time-safari
```
(Note: The test BVC_MEETUPS_PROJECT_CLAIM_ID does not resolve as a URL because it's only in the test DB and the prod redirect won't redirect there.)
(Let's replace that with a .env.development or .env.staging file.)
(Note: The test BVC_MEETUPS_PROJECT_CLAIM_ID does not resolve as a URL because it's only in the test DB and the prod redirect won't redirect there.)
* For prod, get on the server and run the correct build:
@@ -76,23 +79,14 @@ TIME_SAFARI_APP_TITLE="TimeSafari_Test" VITE_APP_SERVER=https://test.timesafari.
* Record the new hash in the changelog. Edit package.json to increment version & add "-beta", `npm install`, and commit. Also record what version is on production.
## Tests
See [TESTING.md](test-playwright/TESTING.md) for detailed test instructions.
## Icons
To add an icon, add to main.ts and reference with `fa` element and `icon` attribute with the hyphenated name.
## Other
### Reference Material
@@ -104,7 +98,6 @@ To add an icon, add to main.ts and reference with `fa` element and `icon` attrib
* If you are deploying in a subdirectory, add it to `publicPath` in vue.config.js, eg: `publicPath: "/app/time-tracker/",`
### Kudos
Gifts make the world go 'round!

View File

@@ -9,7 +9,7 @@ android {
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
dependencies {
implementation project(':capacitor-app')
}

View File

@@ -22,6 +22,13 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="timesafari" />
</intent-filter>
</activity>
<provider

View File

@@ -1,3 +1,6 @@
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
include ':capacitor-android'
project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor')
include ':capacitor-app'
project(':capacitor-app').projectDir = new File('../node_modules/@capacitor/app/android')

View File

@@ -8,6 +8,18 @@ const config: CapacitorConfig = {
server: {
cleartext: true,
},
plugins: {
App: {
appUrlOpen: {
handlers: [
{
url: "timesafari://*",
autoVerify: true
}
]
}
}
}
};
export default config;

91
docs/DEEP_LINKS.md Normal file
View File

@@ -0,0 +1,91 @@
# TimeSafari Deep Linking Documentation
## Type System Overview
The deep linking system uses a multi-layered type safety approach:
1. **Runtime Validation (Zod Schemas)**
- Validates URL structure
- Enforces parameter requirements
- Sanitizes input data
- Provides detailed validation errors
2. **TypeScript Types**
- Generated from Zod schemas
- Ensures compile-time type safety
- Provides IDE autocompletion
- Catches type errors during development
3. **Router Integration**
- Type-safe parameter passing
- Route-specific parameter validation
- Query parameter type checking
## Implementation Files
- `src/types/deepLinks.ts`: Type definitions and validation schemas
- `src/services/deepLinks.ts`: Deep link processing service
- `src/main.capacitor.ts`: Capacitor integration
## Type Safety Examples
```typescript
// Parameter type safety
type ClaimParams = DeepLinkParams["claim"];
// TypeScript knows this has:
// - id: string
// - view?: "details" | "certificate" | "raw"
// Runtime validation
const result = deepLinkSchemas.claim.safeParse({
id: "123",
view: "details"
});
// Validates at runtime with detailed error messages
```
## Supported URL Schemes
All deep links follow the format: `timesafari://<route>/<param>?<query>`
### Claim Routes
- `timesafari://claim/:id`
- Query params:
- `view`: "details" | "certificate" | "raw"
- `timesafari://claim-cert/:id`
- `timesafari://claim-add-raw/:id`
- Query params:
- `claim`: JSON string of claim data
- `claimJwtId`: JWT ID for claim
### Contact Routes
- `timesafari://contact-edit/:did`
- `timesafari://contact-import/:jwt`
- Query params:
- `contacts`: JSON array of contacts
### Project Routes
- `timesafari://project/:id`
- Query params:
- `view`: "details" | "edit"
### Invite Routes
- `timesafari://invite-one-accept/:jwt`
- Query params:
- `type`: "one" | "many"
### Gift Routes
- `timesafari://confirm-gift/:id`
- Query params:
- `action`: "confirm" | "details"
### Offer Routes
- `timesafari://offer-details/:id`
- Query params:
- `view`: "details"

View File

@@ -12,6 +12,21 @@
<strong>We're sorry but TimeSafari doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
<script type="module">
const platform = process.env.VITE_PLATFORM;
switch (platform) {
case 'capacitor':
import('./src/main.capacitor.ts');
break;
case 'electron':
import('./src/main.electron.ts');
break;
case 'pywebview':
import('./src/main.pywebview.ts');
break;
default:
import('./src/main.web.ts');
}
</script>
</body>
</html>

View File

@@ -45,5 +45,14 @@
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<true/>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>timesafari</string>
</array>
</dict>
</array>
</dict>
</plist>

View File

@@ -11,7 +11,7 @@ install! 'cocoapods', :disable_input_output_paths => true
def capacitor_pods
pod 'Capacitor', :path => '../../node_modules/@capacitor/ios'
pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios'
pod 'CapacitorApp', :path => '../../node_modules/@capacitor/app'
end
target 'App' do

29
main.js Normal file
View File

@@ -0,0 +1,29 @@
const { app, BrowserWindow } = require('electron');
const path = require('path');
function createWindow() {
const win = new BrowserWindow({
width: 1200,
height: 800,
webPreferences: {
nodeIntegration: true,
contextIsolation: false
}
});
win.loadFile(path.join(__dirname, 'dist-electron/www/index.html'));
}
app.whenReady().then(createWindow);
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});

4622
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,12 @@
{
"name": "timesafari",
"version": "0.4.2",
"version": "0.4.4",
"description": "TimeSafari Desktop Application",
"author": {
"name": "TimeSafari Team"
},
"scripts": {
"dev": "vite",
"dev": "vite --config vite.config.dev.mts",
"serve": "vite preview",
"build": "VITE_GIT_HASH=`git log -1 --pretty=format:%h` vite build",
"lint": "eslint --ext .js,.ts,.vue --ignore-path .gitignore src",
@@ -15,23 +15,25 @@
"test-local": "npx playwright test -c playwright.config-local.ts --trace on",
"test-all": "npm run build && npx playwright test -c playwright.config-local.ts --trace on",
"clean:electron": "rimraf dist-electron",
"build:electron": "npm run clean:electron && vite build --mode electron && node scripts/build-electron.js",
"build:capacitor": "vite build --mode capacitor",
"build:web": "vite build",
"build:pywebview": "vite build --config vite.config.pywebview.mts",
"build:electron": "npm run clean:electron && vite build --config vite.config.electron.mts && node scripts/build-electron.js",
"build:capacitor": "vite build --config vite.config.capacitor.mts",
"build:web": "vite build --config vite.config.web.mts",
"electron:dev": "npm run build && electron dist-electron",
"electron:start": "electron dist-electron",
"electron:build-linux": "electron-builder --linux AppImage",
"electron:build-linux-deb": "electron-builder --linux deb",
"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",
"electron:build-linux-prod": "npm run build:electron-prod && electron-builder --linux AppImage",
"pywebview:dev": "vite build --mode pywebview && .venv/bin/python src/pywebview/main.py",
"pywebview:build": "vite build --mode pywebview && .venv/bin/python src/pywebview/main.py",
"pywebview:package-linux": "vite build --mode pywebview && .venv/bin/python -m PyInstaller --name TimeSafari --add-data 'dist:www' src/pywebview/main.py",
"pywebview:package-win": "vite build --mode pywebview && .venv/Scripts/python -m PyInstaller --name TimeSafari --add-data 'dist;www' src/pywebview/main.py",
"pywebview:package-mac": "vite build --mode pywebview && .venv/bin/python -m PyInstaller --name TimeSafari --add-data 'dist:www' src/pywebview/main.py"
"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"
},
"dependencies": {
"@capacitor/android": "^6.2.0",
"@capacitor/app": "^6.0.0",
"@capacitor/cli": "^6.2.0",
"@capacitor/core": "^6.2.0",
"@capacitor/ios": "^6.2.0",
@@ -64,7 +66,7 @@
"cbor-x": "^1.5.9",
"class-transformer": "^0.5.1",
"dexie": "^3.2.7",
"dexie-export-import": "^4.1.1",
"dexie-export-import": "^4.1.4",
"did-jwt": "^7.4.7",
"did-resolver": "^4.1.0",
"ethereum-cryptography": "^2.1.3",
@@ -89,16 +91,17 @@
"reflect-metadata": "^0.1.14",
"register-service-worker": "^1.7.2",
"simple-vue-camera": "^1.1.3",
"stream-browserify": "^3.0.0",
"three": "^0.156.1",
"ua-parser-js": "^1.0.37",
"util": "^0.12.5",
"vue": "^3.5.13",
"vue-axios": "^3.5.2",
"vue-facing-decorator": "^3.0.4",
"vue-picture-cropper": "^0.7.0",
"vue-qrcode-reader": "^5.5.3",
"vue-router": "^4.5.0",
"web-did-resolver": "^2.0.27"
"web-did-resolver": "^2.0.27",
"zod": "^3.24.2"
},
"devDependencies": {
"@playwright/test": "^1.45.2",
@@ -116,11 +119,14 @@
"autoprefixer": "^10.4.19",
"concurrently": "^8.2.2",
"electron": "^33.2.1",
"electron-builder": "^25.1.8",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-vue": "^9.32.0",
"fs-extra": "^11.3.0",
"markdownlint": "^0.37.4",
"markdownlint-cli": "^0.44.0",
"npm-check-updates": "^17.1.13",
"postcss": "^8.4.38",
"prettier": "^3.2.5",
@@ -139,12 +145,13 @@
},
"files": [
"dist-electron/**/*",
"src/electron/**/*"
"src/electron/**/*",
"main.js"
],
"extraResources": [
{
"from": "dist-electron/www",
"to": "www"
"from": "dist-electron",
"to": "."
}
],
"linux": {

View File

@@ -40,7 +40,10 @@
<div
class="flex items-center justify-center w-12 bg-slate-600 text-slate-100"
>
<fa icon="circle-info" class="fa-fw fa-xl"></fa>
<font-awesome
icon="circle-info"
class="fa-fw fa-xl"
></font-awesome>
</div>
<div class="relative w-full pl-4 pr-8 py-2 text-slate-900">
@@ -48,10 +51,10 @@
<p class="text-sm">{{ truncateLongWords(notification.text) }}</p>
<button
@click="close(notification.id)"
class="absolute top-2 right-2 px-0.5 py-0 rounded-full bg-slate-200 text-slate-600"
@click="close(notification.id)"
>
<fa icon="xmark" class="fa-fw"></fa>
<font-awesome icon="xmark" class="fa-fw"></font-awesome>
</button>
</div>
</div>
@@ -63,7 +66,10 @@
<div
class="flex items-center justify-center w-12 bg-emerald-600 text-emerald-100"
>
<fa icon="circle-info" class="fa-fw fa-xl"></fa>
<font-awesome
icon="circle-info"
class="fa-fw fa-xl"
></font-awesome>
</div>
<div class="relative w-full pl-4 pr-8 py-2 text-emerald-900">
@@ -71,10 +77,10 @@
<p class="text-sm">{{ truncateLongWords(notification.text) }}</p>
<button
@click="close(notification.id)"
class="absolute top-2 right-2 px-0.5 py-0 rounded-full bg-emerald-200 text-emerald-600"
@click="close(notification.id)"
>
<fa icon="xmark" class="fa-fw"></fa>
<font-awesome icon="xmark" class="fa-fw"></font-awesome>
</button>
</div>
</div>
@@ -86,7 +92,10 @@
<div
class="flex items-center justify-center w-12 bg-amber-600 text-amber-100"
>
<fa icon="triangle-exclamation" class="fa-fw fa-xl"></fa>
<font-awesome
icon="triangle-exclamation"
class="fa-fw fa-xl"
></font-awesome>
</div>
<div class="relative w-full pl-4 pr-8 py-2 text-amber-900">
@@ -94,10 +103,10 @@
<p class="text-sm">{{ truncateLongWords(notification.text) }}</p>
<button
@click="close(notification.id)"
class="absolute top-2 right-2 px-0.5 py-0 rounded-full bg-amber-200 text-amber-600"
@click="close(notification.id)"
>
<fa icon="xmark" class="fa-fw"></fa>
<font-awesome icon="xmark" class="fa-fw"></font-awesome>
</button>
</div>
</div>
@@ -109,7 +118,10 @@
<div
class="flex items-center justify-center w-12 bg-rose-600 text-rose-100"
>
<fa icon="triangle-exclamation" class="fa-fw fa-xl"></fa>
<font-awesome
icon="triangle-exclamation"
class="fa-fw fa-xl"
></font-awesome>
</div>
<div class="relative w-full pl-4 pr-8 py-2 text-rose-900">
@@ -117,10 +129,10 @@
<p class="text-sm">{{ truncateLongWords(notification.text) }}</p>
<button
@click="close(notification.id)"
class="absolute top-2 right-2 px-0.5 py-0 rounded-full bg-rose-200 text-rose-600"
@click="close(notification.id)"
>
<fa icon="xmark" class="fa-fw"></fa>
<font-awesome icon="xmark" class="fa-fw"></font-awesome>
</button>
</div>
</div>
@@ -174,11 +186,11 @@
<button
v-if="notification.onYes"
class="block w-full text-center text-md font-bold uppercase bg-blue-600 text-white px-2 py-2 rounded-md mb-2"
@click="
notification.onYes();
close(notification.id);
"
class="block w-full text-center text-md font-bold uppercase bg-blue-600 text-white px-2 py-2 rounded-md mb-2"
>
Yes{{
notification.yesText ? ", " + notification.yesText : ""
@@ -187,12 +199,12 @@
<button
v-if="notification.onNo"
class="block w-full text-center text-md font-bold uppercase bg-yellow-600 text-white px-2 py-2 rounded-md mb-2"
@click="
notification.onNo(stopAsking);
close(notification.id);
stopAsking = false; // reset value
"
class="block w-full text-center text-md font-bold uppercase bg-yellow-600 text-white px-2 py-2 rounded-md mb-2"
>
No{{ notification.noText ? ", " + notification.noText : "" }}
</button>
@@ -209,8 +221,8 @@
<div class="relative ml-2">
<!-- input -->
<input
type="checkbox"
v-model="stopAsking"
type="checkbox"
name="stopAsking"
class="sr-only"
/>
@@ -224,6 +236,7 @@
</label>
<button
class="block w-full text-center text-md font-bold uppercase bg-slate-600 text-white px-2 py-2 rounded-md"
@click="
notification.onCancel
? notification.onCancel(stopAsking)
@@ -231,7 +244,6 @@
close(notification.id);
stopAsking = false; // reset value for next time they open this modal
"
class="block w-full text-center text-md font-bold uppercase bg-slate-600 text-white px-2 py-2 rounded-md"
>
{{ notification.onYes ? "Cancel" : "Close" }}
</button>
@@ -270,8 +282,8 @@
Until I turn it back on
</button>
<button
@click="close(notification.id)"
class="block w-full text-center text-md font-bold uppercase bg-slate-600 text-white px-2 py-2 rounded-md"
@click="close(notification.id)"
>
Cancel
</button>
@@ -292,17 +304,17 @@
</p>
<button
class="block w-full text-center text-md font-bold uppercase bg-rose-600 text-white px-2 py-2 rounded-md mb-2"
@click="
close(notification.id);
turnOffNotifications(notification);
"
class="block w-full text-center text-md font-bold uppercase bg-rose-600 text-white px-2 py-2 rounded-md mb-2"
>
Turn Off Notification
</button>
<button
@click="close(notification.id)"
class="block w-full text-center text-md font-bold uppercase bg-slate-600 text-white px-2 py-2 rounded-md"
@click="close(notification.id)"
>
Leave it On
</button>
@@ -315,8 +327,6 @@
</NotificationGroup>
</template>
<style></style>
<script lang="ts">
import { Vue, Component } from "vue-facing-decorator";
import { logConsoleAndDb, retrieveSettingsForActiveAccount } from "./db/index";
@@ -526,3 +536,5 @@ export default class App extends Vue {
}
}
</script>
<style></style>

View File

@@ -29,29 +29,29 @@
<p class="text-sm mb-2">{{ text }}</p>
<button
@click="handleOption1(close)"
class="block w-full text-center text-md font-bold capitalize bg-blue-800 text-white px-2 py-2 rounded-md mb-2"
@click="handleOption1(close)"
>
{{ option1Text }}
</button>
<button
@click="handleOption2(close)"
class="block w-full text-center text-md font-bold capitalize bg-blue-700 text-white px-2 py-2 rounded-md mb-2"
@click="handleOption2(close)"
>
{{ option2Text }}
</button>
<button
@click="handleOption3(close)"
class="block w-full text-center text-md font-bold capitalize bg-blue-600 text-white px-2 py-2 rounded-md mb-2"
@click="handleOption3(close)"
>
{{ option3Text }}
</button>
<button
@click="handleCancel(close)"
class="block w-full text-center text-md font-bold capitalize bg-slate-600 text-white px-2 py-2 rounded-md"
@click="handleCancel(close)"
>
Cancel
</button>

View File

@@ -6,10 +6,10 @@
{{ message }}
Note that their name is only stored on this device.
<input
v-model="newText"
type="text"
placeholder="Name"
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
v-model="newText"
/>
<div class="mt-8">

View File

@@ -1,5 +1,5 @@
<template>
<div v-html="generateIcon()" class="w-fit"></div>
<div class="w-fit" v-html="generateIcon()"></div>
</template>
<script lang="ts">
import { createAvatar, StyleOptions } from "@dicebear/core";

View File

@@ -16,8 +16,8 @@
<div class="relative ml-2">
<!-- input -->
<input
type="checkbox"
v-model="hasVisibleDid"
type="checkbox"
name="toggleFilterFromMyContacts"
class="sr-only"
/>
@@ -46,8 +46,8 @@
<div v-if="hasSearchBox" class="relative ml-2">
<!-- input -->
<input
type="checkbox"
v-model="isNearby"
type="checkbox"
name="toggleFilterNearby"
class="sr-only"
/>
@@ -98,7 +98,7 @@ import {
LRectangle,
LTileLayer,
} from "@vue-leaflet/vue-leaflet";
import { Router } from "vue-router";
import { MASTER_SETTINGS_KEY } from "../db/tables/settings";
import { db, retrieveSettingsForActiveAccount } from "../db/index";
@@ -111,6 +111,7 @@ import { db, retrieveSettingsForActiveAccount } from "../db/index";
},
})
export default class FeedFilters extends Vue {
$router!: Router;
onCloseIfChanged = () => {};
hasSearchBox = false;
hasVisibleDid = false;

View File

@@ -5,10 +5,10 @@
{{ 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?'"
v-model="description"
/>
<div class="flex flex-row justify-center">
<span
@@ -21,19 +21,19 @@
class="border border-r-0 border-slate-400 bg-slate-200 px-4 py-2"
@click="amountInput === '0' ? null : decrement()"
>
<fa icon="chevron-left" />
<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"
v-model="amountInput"
/>
<div
class="rounded-r border border-slate-400 bg-slate-200 px-4 py-2"
@click="increment()"
>
<fa icon="chevron-right" />
<font-awesome icon="chevron-right" />
</div>
</div>
<div class="mt-4 flex justify-center">
@@ -62,7 +62,7 @@
</div>
<p class="text-center mb-2 mt-6 italic">
Sign & Send to publish to the world
<fa
<font-awesome
icon="circle-info"
class="pl-2 text-blue-500 cursor-pointer"
@click="explainData()"

View File

@@ -7,7 +7,7 @@
class="text-lg text-center p-2 leading-none absolute right-0 -top-1"
@click="cancel"
>
<fa icon="xmark" class="w-[1em]"></fa>
<font-awesome icon="xmark" class="w-[1em]"></font-awesome>
</div>
</h1>
<span class="mt-2 flex justify-between">
@@ -16,7 +16,7 @@
class="rounded-l border border-slate-400 bg-slate-200 px-4 py-2 flex"
@click="prevIdea()"
>
<fa icon="chevron-left" class="m-auto" />
<font-awesome icon="chevron-left" class="m-auto" />
</span>
<div class="m-2">
@@ -45,7 +45,7 @@
class="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.5 py-2 rounded-md mt-4"
@click="nextIdeaPastContacts()"
>
Skip Contacts <fa icon="forward" />
Skip Contacts <font-awesome icon="forward" />
</button>
</span>
</span>
@@ -57,7 +57,7 @@
class="rounded-r border border-slate-400 bg-slate-200 px-4 py-2 flex"
@click="nextIdea()"
>
<fa icon="chevron-right" class="m-auto" />
<font-awesome icon="chevron-right" class="m-auto" />
</span>
</span>
<button
@@ -82,6 +82,7 @@ import { GiverReceiverInputInfo } from "../libs/util";
@Component
export default class GivenPrompts extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
$router!: Router;
CATEGORY_CONTACTS = 1;
CATEGORY_IDEAS = 0;
@@ -145,7 +146,7 @@ export default class GivenPrompts extends Vue {
// proceed with logic but don't change values (just in case some actions are added later)
this.visible = false;
if (this.currentCategory === this.CATEGORY_IDEAS) {
(this.$router as Router).push({
this.$router.push({
name: "contact-gift",
query: {
prompt: this.IDEAS[this.currentIdeaIndex],

View File

@@ -7,8 +7,8 @@
<!-- Header -->
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-bold capitalize">{{ roleName }} Details</h2>
<button @click="close" class="text-gray-500 hover:text-gray-700">
<fa icon="times" />
<button class="text-gray-500 hover:text-gray-700" @click="close">
<font-awesome icon="times" />
</button>
</div>
@@ -53,7 +53,10 @@
target="_blank"
class="text-blue-500"
>
<fa icon="arrow-up-right-from-square" class="fa-fw" />
<font-awesome
icon="arrow-up-right-from-square"
class="fa-fw"
/>
</a>
</span>
</span>
@@ -66,7 +69,7 @@
<div class="mt-4">
<span v-if="canShare">
If you'd like an introduction,
<a @click="onClickShareClaim()" class="text-blue-500"
<a class="text-blue-500" @click="onClickShareClaim()"
>click here to share the information with them and ask if they'll
tell you more about the {{ roleName }}.</a
>
@@ -74,8 +77,8 @@
<span v-else>
If you'd like an introduction,
<a
@click="copyToClipboard('A link to this page', windowLocation)"
class="text-blue-500"
@click="copyToClipboard('A link to this page', windowLocation)"
>click here to copy this page, paste it into a message, and ask if
they'll tell you more about the {{ roleName }}.</a
>
@@ -86,8 +89,8 @@
<!-- Footer -->
<div class="flex justify-end">
<button
@click="close"
class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600"
@click="close"
>
Close
</button>

View File

@@ -12,14 +12,14 @@
class="text-lg text-center px-2 py-0.5 leading-none absolute right-0 top-0 text-white"
@click="close()"
>
<fa icon="xmark" class="w-[1em]"></fa>
<font-awesome icon="xmark" class="w-[1em]"></font-awesome>
</div>
</div>
<div>
<div class="text-center mt-8">
<div>
<fa
<font-awesome
icon="camera"
class="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-2 rounded-md"
@click="openPhotoDialog()"
@@ -31,17 +31,21 @@
<div class="mt-4">
<span class="mt-2">
... or paste a URL:
<input type="text" v-model="imageUrl" class="border-2" />
<input v-model="imageUrl" type="text" class="border-2" />
</span>
<span class="ml-2">
<fa
<font-awesome
v-if="imageUrl"
icon="check"
class="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-2 rounded-md cursor-pointer"
@click="acceptUrl"
/>
<!-- so that there's no shifting when it becomes visible -->
<fa v-else icon="check" class="text-white bg-white px-2 py-2" />
<font-awesome
v-else
icon="check"
class="text-white bg-white px-2 py-2"
/>
</span>
</div>
</div>

View File

@@ -8,7 +8,7 @@
class="text-white text-2xl p-2 rounded-full hover:bg-white/10"
@click="close"
>
<fa icon="xmark" />
<font-awesome icon="xmark" />
</button>
<!-- Mobile share button -->
@@ -17,7 +17,7 @@
class="text-white text-xl p-2 rounded-full hover:bg-white/10"
@click="handleShare"
>
<fa icon="ellipsis" />
<font-awesome icon="ellipsis" />
</button>
</div>
@@ -27,8 +27,8 @@
<img
:src="imageUrl"
class="max-h-[calc(100vh-5rem)] w-full h-full object-contain"
@click.stop
alt="expanded shared content"
@click.stop
/>
</div>
</div>

View File

@@ -8,18 +8,18 @@
If you want to store your own way, the invitation ID is:
{{ inviteIdentifier }}
<input
v-model="text"
type="text"
placeholder="Notes"
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
v-model="text"
/>
<!-- Add date selection element -->
Expiration
<input
v-model="expiresAt"
type="date"
class="block rounded border border-slate-400 mb-4 px-3 py-2"
v-model="expiresAt"
/>
<div class="mt-8">

View File

@@ -5,7 +5,7 @@
v-if="isLoading"
class="mt-16 text-center text-4xl bg-slate-400 text-white w-14 py-2.5 rounded-full mx-auto"
>
<fa icon="spinner" class="fa-spin-pulse" />
<font-awesome icon="spinner" class="fa-spin-pulse" />
</div>
<!-- Members List -->
@@ -33,13 +33,13 @@
<span
class="mx-2 min-w-[24px] min-h-[24px] w-6 h-6 flex items-center justify-center rounded-full bg-blue-100 text-blue-600"
>
<fa icon="plus" class="text-sm" />
<font-awesome icon="plus" class="text-sm" />
</span>
/
<span
class="mx-2 min-w-[24px] min-h-[24px] w-6 h-6 flex items-center justify-center rounded-full bg-blue-100 text-blue-600"
>
<fa icon="minus" class="text-sm" />
<font-awesome icon="minus" class="text-sm" />
</span>
to add/remove them to/from the meeting.
</span>
@@ -54,7 +54,7 @@
<span
class="mx-2 w-8 h-8 flex items-center justify-center rounded-full bg-green-100 text-green-600"
>
<fa icon="circle-user" class="text-xl" />
<font-awesome icon="circle-user" class="text-xl" />
</span>
to add them to your contacts.
</span>
@@ -63,11 +63,11 @@
<div class="flex justify-center">
<!-- always have at least one refresh button even without members in case the organizer changes the password -->
<button
@click="fetchMembers"
class="w-8 h-8 flex items-center justify-center rounded-full bg-blue-100 text-blue-600 hover:bg-blue-200 hover:text-blue-800 transition-colors"
title="Refresh members list"
@click="fetchMembers"
>
<fa icon="rotate" :class="{ 'fa-spin': isLoading }" />
<font-awesome icon="rotate" :class="{ 'fa-spin': isLoading }" />
</button>
</div>
<div
@@ -83,24 +83,24 @@
class="flex justify-end"
>
<button
@click="addAsContact(member)"
class="ml-2 w-8 h-8 flex items-center justify-center rounded-full bg-green-100 text-green-600 hover:bg-green-200 hover:text-green-800 transition-colors"
title="Add as contact"
@click="addAsContact(member)"
>
<fa icon="circle-user" class="text-xl" />
<font-awesome icon="circle-user" class="text-xl" />
</button>
</div>
<button
v-if="member.did !== activeDid"
class="ml-2 mb-2 w-6 h-6 flex items-center justify-center rounded-full bg-slate-100 text-slate-500 hover:bg-slate-200 hover:text-slate-800 transition-colors"
title="Contact info"
@click="
informAboutAddingContact(
getContactFor(member.did) !== undefined,
)
"
class="ml-2 mb-2 w-6 h-6 flex items-center justify-center rounded-full bg-slate-100 text-slate-500 hover:bg-slate-200 hover:text-slate-800 transition-colors"
title="Contact info"
>
<fa icon="circle-info" class="text-base" />
<font-awesome icon="circle-info" class="text-base" />
</button>
</div>
<div class="flex">
@@ -111,23 +111,23 @@
class="flex items-center"
>
<button
@click="checkWhetherContactBeforeAdmitting(member)"
class="mr-2 w-6 h-6 flex items-center justify-center rounded-full bg-blue-100 text-blue-600 hover:bg-blue-200 hover:text-blue-800 transition-colors"
:title="
member.member.admitted ? 'Remove member' : 'Admit member'
"
@click="checkWhetherContactBeforeAdmitting(member)"
>
<fa
<font-awesome
:icon="member.member.admitted ? 'minus' : 'plus'"
class="text-sm"
/>
</button>
<button
@click="informAboutAdmission()"
class="mr-2 mb-2 w-6 h-6 flex items-center justify-center rounded-full bg-slate-100 text-slate-500 hover:bg-slate-200 hover:text-slate-800 transition-colors"
title="Admission info"
@click="informAboutAdmission()"
>
<fa icon="circle-info" class="text-base" />
<font-awesome icon="circle-info" class="text-base" />
</button>
</span>
</div>
@@ -138,11 +138,11 @@
</div>
<div v-if="membersToShow().length > 0" class="flex justify-center mt-4">
<button
@click="fetchMembers"
class="w-8 h-8 flex items-center justify-center rounded-full bg-blue-100 text-blue-600 hover:bg-blue-200 hover:text-blue-800 transition-colors"
title="Refresh members list"
@click="fetchMembers"
>
<fa icon="rotate" :class="{ 'fa-spin': isLoading }" />
<font-awesome icon="rotate" :class="{ 'fa-spin': isLoading }" />
</button>
</div>

View File

@@ -3,11 +3,11 @@
<div class="dialog">
<h1 class="text-xl font-bold text-center mb-4">Offer Help</h1>
<input
v-model="description"
type="text"
data-testId="inputDescription"
class="block w-full rounded border border-slate-400 mb-2 px-3 py-2"
placeholder="Description of what is offered"
v-model="description"
/>
<div class="flex flex-row mt-2">
<span
@@ -17,23 +17,23 @@
{{ libsUtil.UNIT_SHORT[amountUnitCode] }}
</span>
<div
v-if="amountInput !== '0'"
class="border border-r-0 border-slate-400 bg-slate-200 px-4 py-2"
@click="decrement()"
v-if="amountInput !== '0'"
>
<fa icon="chevron-left" />
<font-awesome icon="chevron-left" />
</div>
<input
v-model="amountInput"
data-testId="inputOfferAmount"
type="number"
class="w-full border border-r-0 border-slate-400 px-2 py-2 text-center"
v-model="amountInput"
/>
<div
class="rounded-r border border-slate-400 bg-slate-200 px-4 py-2"
@click="increment()"
>
<fa icon="chevron-right" />
<font-awesome icon="chevron-right" />
</div>
</div>
<div class="mt-4 flex justify-center">

View File

@@ -10,7 +10,7 @@
class="text-lg text-center leading-none absolute right-0 -top-1"
@click="onClickClose(true)"
>
<fa icon="xmark" class="w-[1em]" />
<font-awesome icon="xmark" class="w-[1em]" />
</div>
</h1>
@@ -21,7 +21,7 @@
</span>
click on the
<span class="bg-green-600 text-white rounded-full">
<fa icon="plus" class="fa-fw" />
<font-awesome icon="plus" class="fa-fw" />
</span>
button to express your appreciation for... whatever -- maybe thanks for
showing you all these fascinating stories of
@@ -40,7 +40,7 @@
<p class="mt-4 flex items-center">
The
<fa
<font-awesome
icon="house-chimney"
class="ml-1 mr-1 text-lg text-white bg-slate-400 px-2 py-2 rounded"
/>
@@ -84,7 +84,7 @@
class="text-lg text-center leading-none absolute right-0 -top-1"
@click="onClickClose(true)"
>
<fa icon="xmark" class="w-[1em]" />
<font-awesome icon="xmark" class="w-[1em]" />
</div>
</h1>
@@ -106,7 +106,7 @@
<p class="mt-4 flex items-center">
The
<fa
<font-awesome
icon="magnifying-glass"
class="ml-1 mr-1 text-lg text-white bg-slate-400 px-2 py-2 rounded"
/>
@@ -141,14 +141,14 @@
class="text-lg text-center leading-none absolute right-0 -top-1"
@click="onClickClose(true)"
>
<fa icon="xmark" class="w-[1em]" />
<font-awesome icon="xmark" class="w-[1em]" />
</div>
</h1>
<p class="relative">
Now you can take a turn: click on the
<span class="bg-green-600 text-white rounded-full">
<fa icon="plus" class="fa-fw" />
<font-awesome icon="plus" class="fa-fw" />
</span>
button to throw out projects of your own... anything you'd like to see
happen. If your first idea doesn't catch anyone, try, try again... and
@@ -157,7 +157,7 @@
<p class="mt-4 flex items-center">
The
<fa
<font-awesome
icon="hand"
class="ml-1 mr-1 text-lg text-white bg-slate-400 px-2 py-2 rounded"
/>
@@ -168,7 +168,7 @@
By the way, one good way to get to know your neighbors and their
interests is to offer time directly to them. You can do this on the
contacts screen
<fa icon="users" class="text-slate-500" />
<font-awesome icon="users" class="text-slate-500" />
which is a great way to get to know a neighbor's interests.
</p>
@@ -219,6 +219,7 @@ import { OnboardPage } from "../libs/util";
})
export default class OnboardingDialog extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
$router!: Router;
activeDid = "";
firstContactName = null;
@@ -254,7 +255,7 @@ export default class OnboardingDialog extends Vue {
finishedOnboarding: true,
});
if (goHome) {
(this.$router as Router).push({ name: "home" });
this.$router.push({ name: "home" });
}
}
}

View File

@@ -15,12 +15,12 @@
class="text-lg text-center px-2 py-0.5 leading-none absolute right-0 top-0 text-white"
@click="close()"
>
<fa icon="xmark" class="w-[1em]"></fa>
<font-awesome icon="xmark" class="w-[1em]"></font-awesome>
</div>
</div>
<div v-if="uploading" class="flex justify-center">
<fa
<font-awesome
icon="spinner"
class="fa-spin fa-3x text-center block px-12 py-12"
/>
@@ -28,7 +28,7 @@
<div v-else-if="blob">
<div v-if="crop">
<VuePictureCropper
:boxStyle="{
:box-style="{
backgroundColor: '#f8f8f8',
margin: 'auto',
}"
@@ -56,8 +56,8 @@
</div>
<div class="absolute bottom-[1rem] left-[1rem] px-2 py-1">
<button
@click="uploadImage"
class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white py-1 px-2 rounded-md"
@click="uploadImage"
>
<span>Upload</span>
</button>
@@ -67,8 +67,8 @@
class="absolute bottom-[1rem] right-[1rem] px-2 py-1"
>
<button
@click="retryImage"
class="bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white py-1 px-2 rounded-md"
@click="retryImage"
>
<span>Retry</span>
</button>
@@ -81,37 +81,37 @@
:resolution="{ width: 375, height: 812 }"
-->
<camera
facingMode="environment"
autoplay
ref="camera"
facing-mode="environment"
autoplay
@started="cameraStarted()"
>
<div
class="absolute portrait:bottom-0 portrait:left-0 portrait:right-0 portrait:pb-2 landscape:right-0 landscape:top-0 landscape:bottom-0 landscape:pr-4 flex landscape:flex-row justify-center items-center"
>
<button
@click="takeImage()"
class="bg-blue-500 hover:bg-blue-700 text-white font-bold p-3 rounded-full text-2xl leading-none"
@click="takeImage()"
>
<fa icon="camera" class="w-[1em]"></fa>
<font-awesome icon="camera" class="w-[1em]"></font-awesome>
</button>
</div>
<div
class="absolute portrait:bottom-2 portrait:right-16 landscape:right-0 landscape:bottom-16 landscape:pr-4 flex justify-center items-center"
>
<button
@click="swapMirrorClass()"
class="bg-blue-500 hover:bg-blue-700 text-white font-bold p-3 rounded-full text-2xl leading-none"
@click="swapMirrorClass()"
>
<fa icon="left-right" class="w-[1em]"></fa>
<font-awesome icon="left-right" class="w-[1em]"></font-awesome>
</button>
</div>
<div v-if="numDevices > 1" class="absolute bottom-2 right-4">
<button
@click="switchCamera()"
class="bg-blue-500 hover:bg-blue-700 text-white font-bold p-3 rounded-full text-2xl leading-none"
@click="switchCamera()"
>
<fa icon="rotate" class="w-[1em]"></fa>
<font-awesome icon="rotate" class="w-[1em]"></font-awesome>
</button>
</div>
</camera>

View File

@@ -5,12 +5,12 @@
target="_blank"
class="h-full w-full object-contain"
>
<div v-html="generateIdenticon()" class="h-full w-full object-contain" />
<div class="h-full w-full object-contain" v-html="generateIdenticon()" />
</a>
<div
v-else
v-html="generateIdenticon()"
class="h-full w-full object-contain"
v-html="generateIdenticon()"
/>
</template>
<script lang="ts">

View File

@@ -25,7 +25,7 @@
</p>
<p v-else class="text-lg mb-4">
Waiting for system initialization, which may take up to 5 seconds...
<fa icon="spinner" spin />
<font-awesome icon="spinner" spin />
</p>
<div v-if="serviceWorkerReady && vapidKey">
@@ -54,23 +54,25 @@
<span class="flex flex-row justify-center">
<span class="mt-2">... at: </span>
<input
type="number"
@change="checkHourInput"
class="rounded-l border border-r-0 border-slate-400 ml-2 mt-2 px-2 py-2 text-center w-20"
v-model="hourInput"
type="number"
class="rounded-l border border-r-0 border-slate-400 ml-2 mt-2 px-2 py-2 text-center w-20"
@change="checkHourInput"
/>
<input
type="number"
@change="checkMinuteInput"
class="border border-slate-400 mt-2 px-2 py-2 text-center w-20"
v-model="minuteInput"
type="number"
class="border border-slate-400 mt-2 px-2 py-2 text-center w-20"
@change="checkMinuteInput"
/>
<span
class="rounded-r border border-slate-400 bg-slate-200 text-center text-blue-500 mt-2 px-2 py-2 w-20"
@click="hourAm = !hourAm"
>
<span v-if="hourAm"> AM <fa icon="chevron-down" /> </span>
<span v-else> PM <fa icon="chevron-up" /> </span>
<span v-if="hourAm">
AM <font-awesome icon="chevron-down" />
</span>
<span v-else> PM <font-awesome icon="chevron-up" /> </span>
</span>
</span>
</div>
@@ -86,8 +88,8 @@
</div>
<button
@click="close()"
class="block w-full text-center text-md font-bold uppercase bg-slate-600 text-white mt-4 px-2 py-2 rounded-md"
@click="close()"
>
No, Not Now
</button>

View File

@@ -13,7 +13,7 @@
>
<router-link :to="{ name: 'home' }" class="block text-center py-2 px-1">
<div class="flex flex-col items-center">
<fa icon="house-chimney" class="fa-fw" />
<font-awesome icon="house-chimney" class="fa-fw" />
<span class="text-xs mt-1">feed</span>
</div>
</router-link>
@@ -32,7 +32,7 @@
class="block text-center py-2 px-1"
>
<div class="flex flex-col items-center">
<fa icon="magnifying-glass" class="fa-fw" />
<font-awesome icon="magnifying-glass" class="fa-fw" />
<span class="text-xs mt-1">search</span>
</div>
</router-link>
@@ -51,7 +51,7 @@
class="block text-center py-2 px-1"
>
<div class="flex flex-col items-center">
<fa icon="hand" class="fa-fw" />
<font-awesome icon="hand" class="fa-fw" />
<span class="text-xs mt-1">your work</span>
</div>
</router-link>
@@ -70,7 +70,7 @@
class="block text-center py-2 px-1"
>
<div class="flex flex-col items-center">
<fa icon="users" class="fa-fw" />
<font-awesome icon="users" class="fa-fw" />
<span class="text-xs mt-1">contacts</span>
</div>
</router-link>
@@ -89,7 +89,7 @@
class="block text-center py-2 px-1"
>
<div class="flex flex-col items-center">
<fa icon="circle-user" class="fa-fw" />
<font-awesome icon="circle-user" class="fa-fw" />
<!--
We used to say "account", so we'll keep that in the code,
but it isn't accurate because we don't hold anything for them.

View File

@@ -6,10 +6,10 @@
{{ sharingExplanation }}
<input
v-model="givenName"
type="text"
placeholder="Name"
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
v-model="givenName"
/>
<div class="mt-8">

View File

@@ -0,0 +1,58 @@
import { AxiosResponse } from "axios";
import { GiverReceiverInputInfo } from "../libs/util";
import { ErrorResult, ResultWithType } from "./common";
export interface GiverOutputInfo {
action: string;
giver?: GiverReceiverInputInfo;
description?: string;
amount?: number;
unitCode?: string;
}
export interface ClaimResult {
success: { claimId: string; handleId: string };
error: { code: string; message: string };
}
export interface VerifiableCredential {
exp?: number;
iat: number;
iss: string;
vc: {
"@context": string[];
type: string[];
credentialSubject: VerifiableCredentialSubject;
};
}
export interface VerifiableCredentialSubject {
"@context": string;
"@type": string;
[key: string]: unknown;
}
export interface WorldProperties {
startTime?: string;
endTime?: string;
}
export interface ProviderInfo {
/**
* Could be a DID or a handleId that identifies the provider
*/
identifier: string;
/**
* Indicates if the provider link has been confirmed
*/
linkConfirmed: boolean;
}
// Type for createAndSubmitClaim result
export type CreateAndSubmitClaimResult = SuccessResult | ErrorResult;
// Update SuccessResult to use ClaimResult
export interface SuccessResult extends ResultWithType {
type: "success";
response: AxiosResponse<ClaimResult>;
}

68
src/interfaces/claims.ts Normal file
View File

@@ -0,0 +1,68 @@
import { GenericVerifiableCredential } from "./common";
export interface AgreeVerifiableCredential {
"@context": string;
"@type": string;
object: Record<string, unknown>;
}
// Note that previous VCs may have additional fields.
// https://endorser.ch/doc/html/transactions.html#id4
export interface GiveVerifiableCredential extends GenericVerifiableCredential {
"@context"?: string;
"@type": "GiveAction";
agent?: { identifier: string };
description?: string;
fulfills?: { "@type": string; identifier?: string; lastClaimId?: string }[];
identifier?: string;
image?: string;
object?: { amountOfThisGood: number; unitCode: string };
provider?: GenericVerifiableCredential;
recipient?: { identifier: string };
}
// Note that previous VCs may have additional fields.
// https://endorser.ch/doc/html/transactions.html#id8
export interface OfferVerifiableCredential extends GenericVerifiableCredential {
"@context"?: string;
"@type": "Offer";
description?: string;
includesObject?: { amountOfThisGood: number; unitCode: string };
itemOffered?: {
description?: string;
isPartOf?: {
identifier?: string;
lastClaimId?: string;
"@type"?: string;
name?: string;
};
};
offeredBy?: { identifier: string };
recipient?: { identifier: string };
validThrough?: string;
}
// Note that previous VCs may have additional fields.
// https://endorser.ch/doc/html/transactions.html#id7
export interface PlanVerifiableCredential extends GenericVerifiableCredential {
"@context": "https://schema.org";
"@type": "PlanAction";
name: string;
agent?: { identifier: string };
description?: string;
identifier?: string;
lastClaimId?: string;
location?: {
geo: { "@type": "GeoCoordinates"; latitude: number; longitude: number };
};
}
// AKA Registration & RegisterAction
export interface RegisterVerifiableCredential {
"@context": string;
"@type": "RegisterAction";
agent: { identifier: string };
identifier?: string;
object: string;
participant?: { identifier: string };
}

36
src/interfaces/common.ts Normal file
View File

@@ -0,0 +1,36 @@
// similar to VerifiableCredentialSubject... maybe rename this
export interface GenericVerifiableCredential {
"@context"?: string;
"@type": string;
[key: string]: unknown;
}
export interface GenericCredWrapper<T extends GenericVerifiableCredential> {
claim: T;
claimType?: string;
handleId: string;
id: string;
issuedAt: string;
issuer: string;
publicUrls?: Record<string, string>;
}
export interface ResultWithType {
type: string;
}
export interface ErrorResponse {
error?: {
message?: string;
};
}
export interface InternalError {
error: string;
userMessage?: string;
}
export interface ErrorResult extends ResultWithType {
type: "error";
error: InternalError;
}

View File

@@ -0,0 +1,13 @@
/**
* @file Deep Link Interface Definitions
* @author Matthew Raymer
*
* Defines the core interfaces for the deep linking system.
* These interfaces are used across the deep linking implementation
* to ensure type safety and consistent error handling.
*/
export interface DeepLinkError extends Error {
code: string;
details?: unknown;
}

7
src/interfaces/index.ts Normal file
View File

@@ -0,0 +1,7 @@
export * from "./claims";
export * from "./claims-result";
export * from "./common";
export * from "./limits";
export * from "./records";
export * from "./user";
export * from "./deepLinks";

14
src/interfaces/limits.ts Normal file
View File

@@ -0,0 +1,14 @@
export interface EndorserRateLimits {
doneClaimsThisWeek: string;
doneRegistrationsThisMonth: string;
maxClaimsPerWeek: string;
maxRegistrationsPerMonth: string;
nextMonthBeginDateTime: string;
nextWeekBeginDateTime: string;
}
export interface ImageRateLimits {
doneImagesThisWeek: string;
maxImagesPerWeek: string;
nextWeekBeginDateTime: string;
}

92
src/interfaces/records.ts Normal file
View File

@@ -0,0 +1,92 @@
import { GiveVerifiableCredential, OfferVerifiableCredential } from "./claims";
// a summary record; the VC is found the fullClaim field
export interface GiveSummaryRecord {
[x: string]: PropertyKey | undefined | GiveVerifiableCredential;
type?: string;
agentDid: string;
amount: number;
amountConfirmed: number;
description: string;
fullClaim: GiveVerifiableCredential;
fulfillsHandleId: string;
fulfillsPlanHandleId?: string;
fulfillsType?: string;
handleId: string;
issuedAt: string;
issuerDid: string;
jwtId: string;
providerPlanHandleId?: string;
recipientDid: string;
unit: string;
}
// a summary record; the VC is found the fullClaim field
export interface OfferSummaryRecord {
amount: number;
amountGiven: number;
amountGivenConfirmed: number;
fullClaim: OfferVerifiableCredential;
fulfillsPlanHandleId: string;
handleId: string;
issuerDid: string;
jwtId: string;
nonAmountGivenConfirmed: number;
objectDescription: string;
offeredByDid: string;
recipientDid: string;
requirementsMet: boolean;
unit: string;
validThrough: string;
}
export interface OfferToPlanSummaryRecord extends OfferSummaryRecord {
planName: string;
}
// a summary record; the VC is not currently part of this record
export interface PlanSummaryRecord {
agentDid?: string;
description: string;
endTime?: string;
fulfillsPlanHandleId: string;
handleId: string;
image?: string;
issuerDid: string;
locLat?: number;
locLon?: number;
name?: string;
startTime?: string;
url?: string;
}
/**
* Represents data about a project
*
* @deprecated
* (Maybe we should use PlanSummaryRecord instead, either by adding rowId or by iterating with jwtId.)
**/
export interface PlanData {
/**
* Description of the project
**/
description: string;
/**
* URL referencing information about the project
**/
handleId: string;
image?: string;
/**
* The DID of the issuer
*/
issuerDid: string;
/**
* Name of the project
**/
name: string;
/**
* The identifier of the project record -- different from jwtId
* (Maybe we should use the jwtId to iterate through the records instead.)
**/
rowId?: string;
}

8
src/interfaces/user.ts Normal file
View File

@@ -0,0 +1,8 @@
export interface UserInfo {
did: string;
name: string;
publicEncKey: string;
registered: boolean;
profileImageUrl?: string;
nextPublicEncKeyHash?: string;
}

59
src/lib/capacitor/app.ts Normal file
View File

@@ -0,0 +1,59 @@
// Import from node_modules using relative path
import {
App as CapacitorApp,
AppLaunchUrl,
BackButtonListener,
} from '../../../node_modules/@capacitor/app';
import type { PluginListenerHandle } from "@capacitor/core";
/**
* Interface defining the app event listener functionality
* Supports 'backButton' and 'appUrlOpen' events from Capacitor
*/
interface AppInterface {
/**
* Add listener for back button events
* @param eventName - Must be 'backButton'
* @param listenerFunc - Callback function for back button events
* @returns Promise that resolves with a removable listener handle
*/
addListener(
eventName: "backButton",
listenerFunc: BackButtonListener,
): Promise<PluginListenerHandle> & PluginListenerHandle;
/**
* Add listener for app URL open events
* @param eventName - Must be 'appUrlOpen'
* @param listenerFunc - Callback function for URL open events
* @returns Promise that resolves with a removable listener handle
*/
addListener(
eventName: "appUrlOpen",
listenerFunc: (data: AppLaunchUrl) => void,
): Promise<PluginListenerHandle> & PluginListenerHandle;
}
/**
* App wrapper for Capacitor functionality
* Provides type-safe event listeners for back button and URL open events
*/
export const App: AppInterface = {
addListener(
eventName: "backButton" | "appUrlOpen",
listenerFunc: BackButtonListener | ((data: AppLaunchUrl) => void),
): Promise<PluginListenerHandle> & PluginListenerHandle {
if (eventName === "backButton") {
return CapacitorApp.addListener(
eventName,
listenerFunc as BackButtonListener,
) as Promise<PluginListenerHandle> & PluginListenerHandle;
} else {
return CapacitorApp.addListener(
eventName,
listenerFunc as (data: AppLaunchUrl) => void,
) as Promise<PluginListenerHandle> & PluginListenerHandle;
}
},
};

168
src/lib/fontawesome.ts Normal file
View File

@@ -0,0 +1,168 @@
/**
* @file Font Awesome Icon Library Configuration
* @description Centralizes Font Awesome icon imports and library configuration
* @author Matthew Raymer
*/
import { library } from "@fortawesome/fontawesome-svg-core";
import {
faArrowDown,
faArrowLeft,
faArrowRight,
faArrowRotateBackward,
faArrowUpRightFromSquare,
faArrowUp,
faBan,
faBitcoinSign,
faBurst,
faCalendar,
faCamera,
faCaretDown,
faChair,
faCheck,
faChevronDown,
faChevronLeft,
faChevronRight,
faChevronUp,
faCircle,
faCircleCheck,
faCircleInfo,
faCircleQuestion,
faCircleUser,
faClock,
faCoins,
faComment,
faCopy,
faDollar,
faEllipsis,
faEllipsisVertical,
faEnvelopeOpenText,
faEraser,
faEye,
faEyeSlash,
faFileContract,
faFileLines,
faFilter,
faFloppyDisk,
faFolderOpen,
faForward,
faGift,
faGlobe,
faHammer,
faHand,
faHandHoldingDollar,
faHandHoldingHeart,
faHouseChimney,
faImagePortrait,
faLeftRight,
faLightbulb,
faLink,
faLocationDot,
faLongArrowAltLeft,
faLongArrowAltRight,
faMagnifyingGlass,
faMessage,
faMinus,
faPen,
faPersonCircleCheck,
faPersonCircleQuestion,
faPlus,
faQuestion,
faQrcode,
faRightFromBracket,
faRotate,
faShareNodes,
faSpinner,
faSquare,
faSquareCaretDown,
faSquareCaretUp,
faSquarePlus,
faTrashCan,
faTriangleExclamation,
faUser,
faUsers,
faXmark,
} from "@fortawesome/free-solid-svg-icons";
// Initialize Font Awesome library with all required icons
library.add(
faArrowDown,
faArrowLeft,
faArrowRight,
faArrowRotateBackward,
faArrowUpRightFromSquare,
faArrowUp,
faBan,
faBitcoinSign,
faBurst,
faCalendar,
faCamera,
faCaretDown,
faChair,
faCheck,
faChevronDown,
faChevronLeft,
faChevronRight,
faChevronUp,
faCircle,
faCircleCheck,
faCircleInfo,
faCircleQuestion,
faCircleUser,
faClock,
faCoins,
faComment,
faCopy,
faDollar,
faEllipsis,
faEllipsisVertical,
faEnvelopeOpenText,
faEraser,
faEye,
faEyeSlash,
faFileContract,
faFileLines,
faFilter,
faFloppyDisk,
faFolderOpen,
faForward,
faGift,
faGlobe,
faHammer,
faHand,
faHandHoldingDollar,
faHandHoldingHeart,
faHouseChimney,
faImagePortrait,
faLeftRight,
faLightbulb,
faLink,
faLocationDot,
faLongArrowAltLeft,
faLongArrowAltRight,
faMagnifyingGlass,
faMessage,
faMinus,
faPen,
faPersonCircleCheck,
faPersonCircleQuestion,
faPlus,
faQrcode,
faQuestion,
faRotate,
faRightFromBracket,
faShareNodes,
faSpinner,
faSquare,
faSquareCaretDown,
faSquareCaretUp,
faSquarePlus,
faTrashCan,
faTriangleExclamation,
faUser,
faUsers,
faXmark,
);
// Export the FontAwesomeIcon component for use in other files
export { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";

View File

@@ -1,4 +1,22 @@
import { Axios, AxiosRequestConfig, AxiosResponse } from "axios";
/**
* @fileoverview Endorser Server Interface and Utilities
* @author Matthew Raymer
*
* This module provides the interface and utilities for interacting with the Endorser server.
* It handles authentication, data validation, and server communication for claims, contacts,
* and other core functionality.
*
* Key Features:
* - Deep link URL path constants
* - DID validation and handling
* - Contact management utilities
* - Server authentication
* - Plan caching
*
* @module endorserServer
*/
import { Axios, AxiosRequestConfig } from "axios";
import { Buffer } from "buffer";
import { sha256 } from "ethereum-cryptography/sha256";
import { LRUCache } from "lru-cache";
@@ -17,63 +35,62 @@ import {
retrieveAccountMetadata,
retrieveFullyDecryptedAccount,
getPasskeyExpirationSeconds,
GiverReceiverInputInfo,
} from "../libs/util";
import { createEndorserJwtForKey, KeyMeta } from "../libs/crypto/vc";
import {
GiveVerifiableCredential,
OfferVerifiableCredential,
RegisterVerifiableCredential,
GenericVerifiableCredential,
GenericCredWrapper,
PlanSummaryRecord,
UserInfo,
CreateAndSubmitClaimResult,
} from "../interfaces";
/**
* Standard context for schema.org data
* @constant {string}
*/
export const SCHEMA_ORG_CONTEXT = "https://schema.org";
// the object in RegisterAction claims
/**
* Service identifier for RegisterAction claims
* @constant {string}
*/
export const SERVICE_ID = "endorser.ch";
// the header line for contacts exported via Endorser Mobile
/**
* Header line format for contacts exported via Endorser Mobile
* @constant {string}
*/
export const CONTACT_CSV_HEADER = "name,did,pubKeyBase64,seesMe,registered";
// the suffix for the contact URL in this app where they are confirmed before import
/**
* URL path suffix for contact confirmation before import
* @constant {string}
*/
export const CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI = "/contact-import/";
// the suffix for the contact URL in this app where a single one gets imported automatically
/**
* URL path suffix for the contact URL in this app where a single one gets imported automatically
* @constant {string}
*/
export const CONTACT_IMPORT_ONE_URL_PATH_TIME_SAFARI = "/contacts?contactJwt=";
// the suffix for the old contact URL -- deprecated Jan 2025, though "endorser.ch/contact?jwt=" shows data on endorser.ch server
/**
* URL path suffix for the old contact URL -- deprecated Jan 2025, though "endorser.ch/contact?jwt=" shows data on endorser.ch server
* @constant {string}
*/
export const CONTACT_URL_PATH_ENDORSER_CH_OLD = "/contact?jwt=";
// unused now that we match on the URL path; just note that it was used for a while to create URLs that showed at endorser.ch
//export const CONTACT_URL_PREFIX_ENDORSER_CH_OLD = "https://endorser.ch";
// the prefix for handle IDs, the permanent ID for claims on Endorser
/**
* The prefix for handle IDs, the permanent ID for claims on Endorser
* @constant {string}
*/
export const ENDORSER_CH_HANDLE_PREFIX = "https://endorser.ch/entity/";
export interface AgreeVerifiableCredential {
"@context": string;
"@type": string;
// "any" because arbitrary objects can be subject of agreement
// eslint-disable-next-line @typescript-eslint/no-explicit-any
object: Record<string, any>;
}
export interface GiverOutputInfo {
action: string;
giver?: GiverReceiverInputInfo;
description?: string;
amount?: number;
unitCode?: string;
}
export interface ClaimResult {
success: { claimId: string; handleId: string };
error: { code: string; message: string };
}
// similar to VerifiableCredentialSubject... maybe rename this
export interface GenericVerifiableCredential {
"@context"?: string; // optional when embedded, eg. in an Agree
"@type": string;
[key: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any
}
export interface GenericCredWrapper<T extends GenericVerifiableCredential> {
claim: T;
claimType?: string;
handleId: string;
id: string;
issuedAt: string;
issuer: string;
publicUrls?: Record<string, string>; // only for IDs that want to be public
}
export const BLANK_GENERIC_SERVER_RECORD: GenericCredWrapper<GenericVerifiableCredential> =
{
claim: { "@type": "" },
@@ -83,266 +100,98 @@ export const BLANK_GENERIC_SERVER_RECORD: GenericCredWrapper<GenericVerifiableCr
issuer: "",
};
// a summary record; the VC is found the fullClaim field
export interface GiveSummaryRecord {
agentDid: string;
amount: number;
amountConfirmed: number;
description: string;
fullClaim: GiveVerifiableCredential;
fulfillsHandleId: string;
fulfillsPlanHandleId?: string;
fulfillsType?: string;
handleId: string;
issuedAt: string;
issuerDid: string;
jwtId: string;
providerPlanHandleId?: string;
recipientDid: string;
unit: string;
}
// a summary record; the VC is found the fullClaim field
export interface OfferSummaryRecord {
amount: number;
amountGiven: number;
amountGivenConfirmed: number;
fullClaim: OfferVerifiableCredential;
fulfillsPlanHandleId: string;
handleId: string;
issuerDid: string;
jwtId: string;
nonAmountGivenConfirmed: number;
objectDescription: string;
offeredByDid: string;
recipientDid: string;
requirementsMet: boolean;
unit: string;
validThrough: string;
}
export interface OfferToPlanSummaryRecord extends OfferSummaryRecord {
planName: string;
}
// a summary record; the VC is not currently part of this record
export interface PlanSummaryRecord {
agentDid?: string; // optional, if the issuer wants someone else to manage as well
description: string;
endTime?: string;
fulfillsPlanHandleId: string;
handleId: string;
image?: string;
issuerDid: string;
locLat?: number;
locLon?: number;
name?: string;
startTime?: string;
url?: string;
}
// Note that previous VCs may have additional fields.
// https://endorser.ch/doc/html/transactions.html#id4
export interface GiveVerifiableCredential extends GenericVerifiableCredential {
"@context"?: string; // optional when embedded, eg. in an Agree
"@type": "GiveAction";
agent?: { identifier: string };
description?: string;
fulfills?: { "@type": string; identifier?: string; lastClaimId?: string }[];
identifier?: string;
image?: string;
object?: { amountOfThisGood: number; unitCode: string };
provider?: GenericVerifiableCredential; // typically @type & identifier
recipient?: { identifier: string };
}
// Note that previous VCs may have additional fields.
// https://endorser.ch/doc/html/transactions.html#id8
export interface OfferVerifiableCredential extends GenericVerifiableCredential {
"@context"?: string; // optional when embedded... though it doesn't make sense to agree to an offer
"@type": "Offer";
description?: string; // conditions for the offer
includesObject?: { amountOfThisGood: number; unitCode: string };
itemOffered?: {
description?: string; // description of the item
isPartOf?: { identifier?: string; lastClaimId?: string; "@type"?: string };
};
offeredBy?: { identifier: string };
recipient?: { identifier: string };
validThrough?: string;
}
// Note that previous VCs may have additional fields.
// https://endorser.ch/doc/html/transactions.html#id7
export interface PlanVerifiableCredential extends GenericVerifiableCredential {
"@context": "https://schema.org";
"@type": "PlanAction";
name: string;
agent?: { identifier: string };
description?: string;
identifier?: string;
lastClaimId?: string;
location?: {
geo: { "@type": "GeoCoordinates"; latitude: number; longitude: number };
};
}
/**
* Represents data about a project
*
* @deprecated
* (Maybe we should use PlanSummaryRecord instead, either by adding rowId or by iterating with jwtId.)
**/
export interface PlanData {
/**
* Description of the project
**/
description: string;
/**
* URL referencing information about the project
**/
handleId: string;
image?: string;
/**
* The DID of the issuer
*/
issuerDid: string;
/**
* Name of the project
**/
name: string;
/**
* The identifier of the project record -- different from jwtId
* (Maybe we should use the jwtId to iterate through the records instead.)
**/
rowId?: string;
}
export interface EndorserRateLimits {
doneClaimsThisWeek: string;
doneRegistrationsThisMonth: string;
maxClaimsPerWeek: string;
maxRegistrationsPerMonth: string;
nextMonthBeginDateTime: string;
nextWeekBeginDateTime: string;
}
export interface ImageRateLimits {
doneImagesThisWeek: string;
maxImagesPerWeek: string;
nextWeekBeginDateTime: string;
}
export interface VerifiableCredential {
exp?: number;
iat: number;
iss: string;
vc: {
"@context": string[];
type: string[];
credentialSubject: VerifiableCredentialSubject;
};
}
// similar to GenericVerifiableCredential... maybe replace that one
export interface VerifiableCredentialSubject {
"@context": string;
"@type": string;
[key: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any
}
export interface WorldProperties {
startTime?: string;
endTime?: string;
}
// AKA Registration & RegisterAction
export interface RegisterVerifiableCredential {
"@context": typeof SCHEMA_ORG_CONTEXT;
"@type": "RegisterAction";
agent: { identifier: string };
identifier?: string; // used for invites (when participant ID isn't known)
object: string;
participant?: { identifier: string }; // used when person is known (not an invite)
}
// now for some of the error & other wrapper types
export interface ResultWithType {
type: string;
}
export interface SuccessResult extends ResultWithType {
type: "success";
response: AxiosResponse<ClaimResult>;
}
export interface ErrorResponse {
error?: {
message?: string;
};
}
export interface InternalError {
error: string; // for system logging
userMessage?: string; // for user display
}
export interface ErrorResult extends ResultWithType {
type: "error";
error: InternalError;
}
export type CreateAndSubmitClaimResult = SuccessResult | ErrorResult;
/**
* This is similar to Contact but it grew up in different logic paths.
* We may want to change this to be a Contact.
*/
export interface UserInfo {
did: string;
name: string;
publicEncKey: string;
registered: boolean;
profileImageUrl?: string;
nextPublicEncKeyHash?: string;
}
// This is used to check for hidden info.
// See https://github.com/trentlarson/endorser-ch/blob/0cb626f803028e7d9c67f095858a9fc8542e3dbd/server/api/services/util.js#L6
const HIDDEN_DID = "did:none:HIDDEN";
export function isDid(did: string) {
/**
* Validates if a string is a valid DID
* @param {string} did - The DID string to validate
* @returns {boolean} True if string is a valid DID format
*/
export function isDid(did: string): boolean {
return did.startsWith("did:");
}
export function isHiddenDid(did: string) {
/**
* Checks if a DID is the special hidden DID value
* @param {string} did - The DID to check
* @returns {boolean} True if DID is hidden
*/
export function isHiddenDid(did: string): boolean {
return did === HIDDEN_DID;
}
export function isEmptyOrHiddenDid(did?: string) {
return !did || did === HIDDEN_DID; // catching empty string as well
/**
* Checks if a DID is empty or hidden
* @param {string} [did] - The DID to check
* @returns {boolean} True if DID is empty or hidden
*/
export function isEmptyOrHiddenDid(did?: string): boolean {
return !did || did === HIDDEN_DID;
}
/**
* @return true for any string within this primitive/object/array where func(input) === true
*
* Similar logic is found in endorser-mobile.
* Recursively tests strings within an object/array against a test function
* @param {Function} func - Test function to apply to strings
* @param {any} input - Object/array to recursively test
* @returns {boolean} True if any string passes the test function
*
* @example
* testRecursivelyOnStrings(isDid, { user: { id: "did:example:123" } })
* // Returns: true
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function testRecursivelyOnStrings(func: (arg0: any) => boolean, input: any) {
/**
* Recursively tests strings within a nested object/array structure against a test function
*
* This function traverses through objects and arrays to find all string values and applies
* a test function to each string found. It handles:
* - Direct string values
* - Strings in objects (at any depth)
* - Strings in arrays (at any depth)
* - Mixed nested structures (objects containing arrays containing objects, etc)
*
* @param {Function} func - Test function that takes a string and returns boolean
* @param {any} input - Value to recursively search (can be string, object, array, or other)
* @returns {boolean} True if any string in the structure passes the test function
*
* @example
* // Test if any string is a DID
* const obj = {
* user: {
* id: "did:example:123",
* details: ["name", "did:example:456"]
* }
* };
* testRecursivelyOnStrings(isDid, obj); // Returns: true
*
* @example
* // Test for hidden DIDs
* const obj = {
* visible: "did:example:123",
* hidden: ["did:none:HIDDEN"]
* };
* testRecursivelyOnStrings(isHiddenDid, obj); // Returns: true
*/
function testRecursivelyOnStrings(
func: (arg0: any) => boolean,
input: any
): boolean {
// Test direct string values
if (Object.prototype.toString.call(input) === "[object String]") {
return func(input);
} else if (input instanceof Object) {
}
// Recursively test objects and arrays
else if (input instanceof Object) {
if (!Array.isArray(input)) {
// it's an object
// Handle plain objects
for (const key in input) {
if (testRecursivelyOnStrings(func, input[key])) {
return true;
}
}
} else {
// it's an array
// Handle arrays
for (const value of input) {
if (testRecursivelyOnStrings(func, value)) {
return true;
@@ -351,6 +200,7 @@ function testRecursivelyOnStrings(func: (arg0: any) => boolean, input: any) {
}
return false;
} else {
// Non-string, non-object values can't contain strings
return false;
}
}
@@ -617,15 +467,23 @@ export async function getHeaders(
return headers;
}
/**
* Cache for storing plan data
* @constant {LRUCache}
*/
const planCache: LRUCache<string, PlanSummaryRecord> = new LRUCache({
max: 500,
});
/**
* @param handleId nullable, in which case "undefined" will be returned
* @param requesterDid optional, in which case no private info will be returned
* @param axios
* @param apiServer
* Retrieves plan data from cache or server
* @param {string} handleId - Plan handle ID
* @param {Axios} axios - Axios instance
* @param {string} apiServer - API server URL
* @param {string} [requesterDid] - Optional requester DID for private info
* @returns {Promise<PlanSummaryRecord|undefined>} Plan data or undefined if not found
*
* @throws {Error} If server request fails
*/
export async function getPlanFromCache(
handleId: string | undefined,
@@ -649,44 +507,44 @@ export async function getPlanFromCache(
cred = resp.data.data[0];
planCache.set(handleId, cred);
} else {
console.error(
"Failed to load plan with handle",
console.info(
"[EndorserServer] Plan cache is empty for handle",
handleId,
" Got data:",
resp.data,
JSON.stringify(resp.data),
);
}
} catch (error) {
console.error(
"Failed to load plan with handle",
"[EndorserServer] Failed to load plan with handle",
handleId,
" Got error:",
error,
JSON.stringify(error),
);
}
}
return cred;
}
/**
* Updates plan data in cache
* @param {string} handleId - Plan handle ID
* @param {PlanSummaryRecord} planSummary - Plan data to cache
*/
export async function setPlanInCache(
handleId: string,
planSummary: PlanSummaryRecord,
) {
): Promise<void> {
planCache.set(handleId, planSummary);
}
/**
*
* @param error that is thrown from an Endorser server call by Axios
* @returns user-friendly message, or undefined if none found
* Extracts user-friendly message from server error
* @param {any} error - Error thrown from Endorser server call
* @returns {string|undefined} User-friendly message or undefined if none found
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function serverMessageForUser(error: any) {
return (
// this is how most user messages are returned
error?.response?.data?.error?.message
// some are returned as "error" with a string, but those are more for devs and are less helpful to the user
);
export function serverMessageForUser(error: any): string | undefined {
return error?.response?.data?.error?.message;
}
/**

89
src/main.capacitor.ts Normal file
View File

@@ -0,0 +1,89 @@
/**
* @file Capacitor Main Entry Point
* @author Matthew Raymer
*
* This file initializes the deep linking system for the TimeSafari app.
* It sets up the connection between Capacitor's URL handling and our deep link processor.
*
* Deep Linking Flow:
* 1. Capacitor receives URL open event
* 2. Event is passed to DeepLinkHandler
* 3. URL is validated and processed
* 4. Router navigates to appropriate view
*
* Integration Points:
* - Capacitor App plugin for URL handling
* - Vue Router for navigation
* - Error handling system
* - Logging system
*
* Type Safety:
* - Uses DeepLinkHandler for type-safe parameter processing
* - Ensures type safety between Capacitor events and app routing
* - Maintains type checking through the entire deep link flow
*
* @example
* // URL open event from OS
* timesafari://claim/123?view=details
* // Processed and routed to appropriate view with type-safe parameters
*/
import { initializeApp } from "./main.common";
import { App } from "./lib/capacitor/app";
import router from "./router";
import { handleApiError } from "./services/api";
import { AxiosError } from "axios";
import { DeepLinkHandler } from "./services/deepLinks";
import { logConsoleAndDb } from "./db";
console.log("[Capacitor] Starting initialization");
console.log("[Capacitor] Platform:", process.env.VITE_PLATFORM);
const app = initializeApp();
// Initialize API error handling for unhandled promise rejections
window.addEventListener("unhandledrejection", (event) => {
if (event.reason?.response) {
handleApiError(event.reason, event.reason.config?.url || "unknown");
}
});
const deepLinkHandler = new DeepLinkHandler(router);
/**
* Handles deep link routing for the application
* Processes URLs in the format timesafari://<route>/<param>
* Maps incoming deep links to corresponding router paths with parameters
*
* @param {Object} data - Deep link data object
* @param {string} data.url - The full deep link URL to process
* @returns {Promise<void>}
*
* @example
* // Handles URLs like:
* // timesafari://claim/01JMAAFZRNSRTQ0EBSD70A8E1H
* // timesafari://project/abc123
*
* @throws {Error} If URL format is invalid
*/
const handleDeepLink = async (data: { url: string }) => {
try {
await router.isReady();
await deepLinkHandler.handleDeepLink(data.url);
} catch (error) {
logConsoleAndDb("[DeepLink] Error handling deep link: " + error, true);
handleApiError(
{
message: error instanceof Error ? error.message : String(error),
} as AxiosError,
"deep-link",
);
}
};
// Register deep link handler with Capacitor
App.addListener("appUrlOpen", handleDeepLink);
console.log("[Capacitor] Mounting app");
app.mount("#app");
console.log("[Capacitor] App mounted");

60
src/main.common.ts Normal file
View File

@@ -0,0 +1,60 @@
import { createPinia } from "pinia";
import { App as VueApp, ComponentPublicInstance, createApp } from "vue";
import App from "./App.vue";
import router from "./router";
import axios from "axios";
import VueAxios from "vue-axios";
import Notifications from "notiwind";
import "./assets/styles/tailwind.css";
import { FontAwesomeIcon } from "./lib/fontawesome";
import Camera from "simple-vue-camera";
// Global Error Handler
function setupGlobalErrorHandler(app: VueApp) {
console.log("[App Init] Setting up global error handler");
app.config.errorHandler = (
err: unknown,
instance: ComponentPublicInstance | null,
info: string,
) => {
console.error("[App Error] Global Error Handler:", {
error: err,
info,
component: instance?.$options.name || "unknown",
});
alert(
(err instanceof Error ? err.message : "Something bad happened") +
" - Try reloading or restarting the app.",
);
};
}
// Function to initialize the app
export function initializeApp() {
console.log("[App Init] Starting app initialization");
console.log("[App Init] Platform:", process.env.VITE_PLATFORM);
const app = createApp(App);
console.log("[App Init] Vue app created");
app.component("FontAwesome", FontAwesomeIcon).component("camera", Camera);
console.log("[App Init] Components registered");
const pinia = createPinia();
app.use(pinia);
console.log("[App Init] Pinia store initialized");
app.use(VueAxios, axios);
console.log("[App Init] Axios initialized");
app.use(router);
console.log("[App Init] Router initialized");
app.use(Notifications);
console.log("[App Init] Notifications initialized");
setupGlobalErrorHandler(app);
console.log("[App Init] App initialization complete");
return app;
}

4
src/main.electron.ts Normal file
View File

@@ -0,0 +1,4 @@
import { initializeApp } from "./main.common";
const app = initializeApp();
app.mount("#app");

4
src/main.pywebview.ts Normal file
View File

@@ -0,0 +1,4 @@
import { initializeApp } from "./main.common";
const app = initializeApp();
app.mount("#app");

View File

@@ -6,169 +6,8 @@ import router from "./router";
import axios from "axios";
import VueAxios from "vue-axios";
import Notifications from "notiwind";
import "./assets/styles/tailwind.css";
import { library } from "@fortawesome/fontawesome-svg-core";
import {
faArrowDown,
faArrowLeft,
faArrowRight,
faArrowRotateBackward,
faArrowUpRightFromSquare,
faArrowUp,
faBan,
faBitcoinSign,
faBurst,
faCalendar,
faCamera,
faCaretDown,
faChair,
faCheck,
faChevronDown,
faChevronLeft,
faChevronRight,
faChevronUp,
faCircle,
faCircleCheck,
faCircleInfo,
faCircleQuestion,
faCircleUser,
faClock,
faCoins,
faComment,
faCopy,
faDollar,
faEllipsis,
faEllipsisVertical,
faEnvelopeOpenText,
faEraser,
faEye,
faEyeSlash,
faFileContract,
faFileLines,
faFilter,
faFloppyDisk,
faFolderOpen,
faForward,
faGift,
faGlobe,
faHammer,
faHand,
faHandHoldingDollar,
faHandHoldingHeart,
faHouseChimney,
faImagePortrait,
faLeftRight,
faLightbulb,
faLink,
faLocationDot,
faLongArrowAltLeft,
faLongArrowAltRight,
faMagnifyingGlass,
faMessage,
faMinus,
faPen,
faPersonCircleCheck,
faPersonCircleQuestion,
faPlus,
faQuestion,
faQrcode,
faRightFromBracket,
faRotate,
faShareNodes,
faSpinner,
faSquare,
faSquareCaretDown,
faSquareCaretUp,
faSquarePlus,
faTrashCan,
faTriangleExclamation,
faUser,
faUsers,
faXmark,
} from "@fortawesome/free-solid-svg-icons";
library.add(
faArrowDown,
faArrowLeft,
faArrowRight,
faArrowRotateBackward,
faArrowUpRightFromSquare,
faArrowUp,
faBan,
faBitcoinSign,
faBurst,
faCalendar,
faCamera,
faCaretDown,
faChair,
faCheck,
faChevronDown,
faChevronLeft,
faChevronRight,
faChevronUp,
faCircle,
faCircleCheck,
faCircleInfo,
faCircleQuestion,
faCircleUser,
faClock,
faCoins,
faComment,
faCopy,
faDollar,
faEllipsis,
faEllipsisVertical,
faEnvelopeOpenText,
faEraser,
faEye,
faEyeSlash,
faFileContract,
faFileLines,
faFilter,
faFloppyDisk,
faFolderOpen,
faForward,
faGift,
faGlobe,
faHammer,
faHand,
faHandHoldingDollar,
faHandHoldingHeart,
faHouseChimney,
faImagePortrait,
faLeftRight,
faLightbulb,
faLink,
faLocationDot,
faLongArrowAltLeft,
faLongArrowAltRight,
faMagnifyingGlass,
faMessage,
faMinus,
faPen,
faPersonCircleCheck,
faPersonCircleQuestion,
faPlus,
faQrcode,
faQuestion,
faRotate,
faRightFromBracket,
faShareNodes,
faSpinner,
faSquare,
faSquareCaretDown,
faSquareCaretUp,
faSquarePlus,
faTrashCan,
faTriangleExclamation,
faUser,
faUsers,
faXmark,
);
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { FontAwesomeIcon } from "./lib/fontawesome";
import Camera from "simple-vue-camera";
// Can trigger this with a 'throw' inside some top-level function, eg. on the HomeView
@@ -197,7 +36,7 @@ function setupGlobalErrorHandler(app: VueApp) {
);
};
}
// console.log("Bootstrapping Vue app...");
const app = createApp(App)
.component("fa", FontAwesomeIcon)
.component("camera", Camera)
@@ -209,4 +48,3 @@ const app = createApp(App)
setupGlobalErrorHandler(app);
app.mount("#app");
// console.log("Vue app mounted.");

5
src/main.web.ts Normal file
View File

@@ -0,0 +1,5 @@
import { initializeApp } from "./main.common";
import "./registerServiceWorker"; // Web PWA support
const app = initializeApp();
app.mount("#app");

View File

@@ -276,8 +276,8 @@ const routes: Array<RouteRecordRaw> = [
component: () => import("../views/TestView.vue"),
},
{
path: "/userProfile/:id?",
name: "userProfile",
path: "/user-profile/:id?",
name: "user-profile",
component: () => import("../views/UserProfileView.vue"),
},
];

24
src/services/api.ts Normal file
View File

@@ -0,0 +1,24 @@
import { AxiosError } from "axios";
export const handleApiError = (error: AxiosError, endpoint: string) => {
if (process.env.VITE_PLATFORM === "capacitor") {
console.error(`[Capacitor API Error] ${endpoint}:`, {
message: error.message,
status: error.response?.status,
data: error.response?.data,
config: {
url: error.config?.url,
method: error.config?.method,
headers: error.config?.headers,
},
});
}
// Specific handling for rate limits
if (error.response?.status === 400) {
console.warn(`[Rate Limit] ${endpoint}`);
return null;
}
throw error;
};

145
src/services/deepLinks.ts Normal file
View File

@@ -0,0 +1,145 @@
/**
* @file Deep Link Handler Service
* @author Matthew Raymer
*
* This service handles the processing and routing of deep links in the TimeSafari app.
* It provides a type-safe interface between the raw deep links and the application router.
*
* Architecture:
* 1. DeepLinkHandler class encapsulates all deep link processing logic
* 2. Uses Zod schemas from types/deepLinks for parameter validation
* 3. Provides consistent error handling and logging
* 4. Maps validated parameters to Vue router calls
*
* Error Handling Strategy:
* - All errors are wrapped in DeepLinkError interface
* - Errors include error codes for systematic handling
* - Detailed error information is logged for debugging
* - Errors are propagated to the global error handler
*
* Validation Strategy:
* - URL structure validation
* - Route-specific parameter validation using Zod schemas
* - Query parameter validation and sanitization
* - Type-safe parameter passing to router
*
* @example
* const handler = new DeepLinkHandler(router);
* await handler.handleDeepLink("timesafari://claim/123?view=details");
*/
import { Router } from "vue-router";
import { deepLinkSchemas, baseUrlSchema } from "../types/deepLinks";
import { logConsoleAndDb } from "../db";
import type { DeepLinkError } from "../interfaces/deepLinks";
export class DeepLinkHandler {
private router: Router;
constructor(router: Router) {
this.router = router;
}
/**
* Parses deep link URL into path, params and query components
*/
private parseDeepLink(url: string) {
const parts = url.split("://");
if (parts.length !== 2) {
throw { code: "INVALID_URL", message: "Invalid URL format" };
}
// Validate base URL structure
baseUrlSchema.parse({
scheme: parts[0],
path: parts[1],
queryParams: {}, // Will be populated below
});
const [path, queryString] = parts[1].split("?");
const [routePath, param] = path.split("/");
const query: Record<string, string> = {};
if (queryString) {
new URLSearchParams(queryString).forEach((value, key) => {
query[key] = value;
});
}
return {
path: routePath,
params: param ? { id: param } : {},
query,
};
}
/**
* Processes incoming deep links and routes them appropriately
* @param url The deep link URL to process
*/
async handleDeepLink(url: string): Promise<void> {
try {
logConsoleAndDb("[DeepLink] Processing URL: " + url, false);
const { path, params, query } = this.parseDeepLink(url);
// Ensure params is always a Record<string,string> by converting undefined to empty string
const sanitizedParams = Object.fromEntries(
Object.entries(params).map(([key, value]) => [key, value ?? ""]),
);
await this.validateAndRoute(path, sanitizedParams, query);
} catch (error) {
const deepLinkError = error as DeepLinkError;
logConsoleAndDb(
`[DeepLink] Error (${deepLinkError.code}): ${deepLinkError.message}`,
true,
);
throw {
code: deepLinkError.code || "UNKNOWN_ERROR",
message: deepLinkError.message,
details: deepLinkError.details,
};
}
}
/**
* Routes the deep link to appropriate view with validated parameters
*/
private async validateAndRoute(
path: string,
params: Record<string, string>,
query: Record<string, string>,
): Promise<void> {
const routeMap: Record<string, string> = {
claim: "claim",
"claim-cert": "claim-cert",
"claim-add-raw": "claim-add-raw",
"contact-edit": "contact-edit",
"contact-import": "contact-import",
project: "project",
"invite-one-accept": "invite-one-accept",
"offer-details": "offer-details",
"confirm-gift": "confirm-gift",
};
const routeName = routeMap[path];
if (!routeName) {
throw {
code: "INVALID_ROUTE",
message: `Unsupported route: ${path}`,
};
}
// Validate parameters based on route type
const schema = deepLinkSchemas[path as keyof typeof deepLinkSchemas];
const validatedParams = await schema.parseAsync({
...params,
...query,
});
await this.router.replace({
name: routeName,
params: validatedParams,
query,
});
}
}

80
src/services/plan.ts Normal file
View File

@@ -0,0 +1,80 @@
import axios from "axios";
interface PlanResponse {
data?: unknown;
status?: number;
error?: string;
}
export const loadPlanWithRetry = async (
handle: string,
retries = 3,
): Promise<PlanResponse> => {
try {
console.log(`[Plan Service] Loading plan ${handle}, attempt 1/${retries}`);
console.log(
`[Plan Service] Context: Deep link handle=${handle}, isClaimFlow=${handle.includes("claim")}`,
);
// Different endpoint if this is a claim flow
const response = await loadPlan(handle);
console.log(`[Plan Service] Plan ${handle} loaded successfully:`, {
status: response?.status,
headers: response?.headers,
data: response?.data,
});
return response;
} catch (error: unknown) {
console.error(`[Plan Service] Error loading plan ${handle}:`, {
message: (error as Error).message,
status: (error as { response?: { status?: number } })?.response?.status,
statusText: (error as { response?: { statusText?: string } })?.response
?.statusText,
data: (error as { response?: { data?: unknown } })?.response?.data,
headers: (error as { response?: { headers?: unknown } })?.response
?.headers,
config: {
url: (error as { config?: { url?: string } })?.config?.url,
method: (error as { config?: { method?: string } })?.config?.method,
baseURL: (error as { config?: { baseURL?: string } })?.config?.baseURL,
headers: (error as { config?: { headers?: unknown } })?.config?.headers,
},
});
if (retries > 1) {
console.log(
`[Plan Service] Retrying plan ${handle}, ${retries - 1} attempts remaining`,
);
await new Promise((resolve) => setTimeout(resolve, 1000));
return loadPlanWithRetry(handle, retries - 1);
}
return {
error: `Failed to load plan ${handle} after ${4 - retries} attempts: ${(error as Error).message}`,
status: (error as { response?: { status?: number } })?.response?.status,
};
}
};
export const loadPlan = async (handle: string): Promise<PlanResponse> => {
console.log(`[Plan Service] Making API request for plan ${handle}`);
const endpoint = handle.includes("claim")
? `/api/claims/${handle}`
: `/api/plans/${handle}`;
console.log(`[Plan Service] Using endpoint: ${endpoint}`);
try {
const response = await axios.get(endpoint);
return response;
} catch (error: unknown) {
console.error(`[Plan Service] API request failed for ${handle}:`, {
endpoint,
error: (error as Error).message,
response: (error as { response?: { data?: unknown } })?.response?.data,
});
throw error;
}
};

73
src/types/deepLinks.ts Normal file
View File

@@ -0,0 +1,73 @@
/**
* @file Deep Link Type Definitions and Validation Schemas
* @author Matthew Raymer
*
* This file defines the type system and validation schemas for deep linking in the TimeSafari app.
* It uses Zod for runtime validation while providing TypeScript types for compile-time checking.
*
* Type Strategy:
* 1. Define base URL schema to validate the fundamental deep link structure
* 2. Define route-specific parameter schemas with exact validation rules
* 3. Generate TypeScript types from Zod schemas for type safety
* 4. Export both schemas and types for use in deep link handling
*
* Usage:
* - Import schemas for runtime validation in deep link handlers
* - Import types for type-safe parameter handling in components
* - Use DeepLinkParams type for type-safe access to route parameters
*
* @example
* // Runtime validation
* const params = deepLinkSchemas.claim.parse({ id: "123", view: "details" });
*
* // Type-safe parameter access
* function handleClaimParams(params: DeepLinkParams["claim"]) {
* // TypeScript knows params.id exists and params.view is optional
* }
*/
import { z } from "zod";
// Base URL validation schema
export const baseUrlSchema = z.object({
scheme: z.literal("timesafari"),
path: z.string(),
queryParams: z.record(z.string()).optional(),
});
// Parameter validation schemas for each route type
export const deepLinkSchemas = {
claim: z.object({
id: z.string().min(1),
view: z.enum(["details", "certificate", "raw"]).optional(),
}),
contact: z.object({
did: z.string().regex(/^did:/),
action: z.enum(["edit", "import"]).optional(),
jwt: z.string().optional(),
}),
project: z.object({
id: z.string().min(1),
view: z.enum(["details", "edit"]).optional(),
}),
invite: z.object({
jwt: z.string().min(1),
type: z.enum(["one", "many"]).optional(),
}),
gift: z.object({
id: z.string().min(1),
action: z.enum(["confirm", "details"]).optional(),
}),
offer: z.object({
id: z.string().min(1),
view: z.enum(["details"]).optional(),
}),
};
export type DeepLinkParams = {
[K in keyof typeof deepLinkSchemas]: z.infer<(typeof deepLinkSchemas)[K]>;
};

View File

@@ -39,12 +39,15 @@
:to="{ name: 'contact-qr' }"
class="bg-slate-500 text-white px-1.5 py-1 rounded-md"
>
<fa icon="qrcode" class="fa-fw text-xl"></fa>
<font-awesome icon="qrcode" class="fa-fw text-xl"></font-awesome>
</router-link>
</span>
{{ givenName }}
<router-link :to="{ name: 'new-edit-account' }">
<fa icon="pen" class="text-xs text-blue-500 ml-2 mb-1"></fa>
<font-awesome
icon="pen"
class="text-xs text-blue-500 ml-2 mb-1"
></font-awesome>
</router-link>
</h2>
</div>
@@ -53,13 +56,13 @@
class="block w-full text-center text-md bg-amber-200 border border-dashed border-slate-400 px-1.5 py-2 rounded-md mb-2"
>
<button
class="inline-block text-md 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-4 py-2 rounded-md"
@click="
() =>
(this.$refs.userNameDialog as UserNameDialog).open(
(name) => (this.givenName = name),
($refs.userNameDialog as UserNameDialog).open(
(name) => (givenName = name),
)
"
class="inline-block text-md 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-4 py-2 rounded-md"
>
Set Your Name
</button>
@@ -69,23 +72,23 @@
<span v-if="profileImageUrl" class="flex justify-between">
<EntityIcon
:icon-size="96"
:profileImageUrl="profileImageUrl"
:profile-image-url="profileImageUrl"
class="inline-block align-text-bottom border border-slate-300 rounded"
@click="showLargeIdenticonUrl = profileImageUrl"
/>
<fa
<font-awesome
icon="trash-can"
@click="confirmDeleteImage"
class="text-red-500 fa-fw ml-8 mt-8 w-12 h-12"
@click="confirmDeleteImage"
/>
</span>
<div v-else class="text-center">
<div class @click="openImageDialog()">
<fa
<font-awesome
icon="image-portrait"
class="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-2 rounded-l"
/>
<fa
<font-awesome
icon="camera"
class="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-2 rounded-r"
/>
@@ -101,8 +104,8 @@
</div>
<div class="flex justify-center">
<EntityIcon
:entityId="activeDid"
:iconSize="64"
:entity-id="activeDid"
:icon-size="64"
class="inline-block align-middle border border-slate-300 rounded-md mr-1"
@click="showLargeIdenticonId = activeDid"
/>
@@ -116,9 +119,9 @@
class="absolute inset-0 h-screen flex flex-col items-center justify-center bg-slate-900/50"
>
<EntityIcon
:entityId="showLargeIdenticonId"
:iconSize="512"
:profileImageUrl="showLargeIdenticonUrl"
:entity-id="showLargeIdenticonId"
:icon-size="512"
:profile-image-url="showLargeIdenticonUrl"
class="flex w-11/12 max-w-sm mx-auto mb-3 overflow-hidden bg-white rounded-lg shadow-lg"
@click="
showLargeIdenticonId = undefined;
@@ -135,12 +138,12 @@
>
<code class="truncate">{{ activeDid }}</code>
<button
class="ml-2"
@click="
doCopyTwoSecRedo(activeDid, () => (showDidCopy = !showDidCopy))
"
class="ml-2"
>
<fa icon="copy" class="text-slate-400 fa-fw"></fa>
<font-awesome icon="copy" class="text-slate-400 fa-fw"></font-awesome>
</button>
<span v-show="showDidCopy">Copied</span>
</div>
@@ -185,7 +188,7 @@
<!-- label -->
<div>
Reminder Notification
<fa
<font-awesome
icon="question-circle"
class="text-slate-400 fa-fw ml-2 cursor-pointer"
@click.stop="showReminderNotificationInfo"
@@ -197,7 +200,7 @@
@click="showReminderNotificationChoice()"
>
<!-- input -->
<input type="checkbox" v-model="notifyingReminder" class="sr-only" />
<input v-model="notifyingReminder" type="checkbox" class="sr-only" />
<!-- line -->
<div class="block bg-slate-500 w-14 h-8 rounded-full"></div>
<!-- dot -->
@@ -214,7 +217,7 @@
<!-- label -->
<div>
New Activity Notification
<fa
<font-awesome
icon="question-circle"
class="text-slate-400 fa-fw ml-2 cursor-pointer"
@click.stop="showNewActivityNotificationInfo"
@@ -227,8 +230,8 @@
>
<!-- input -->
<input
type="checkbox"
v-model="notifyingNewActivity"
type="checkbox"
class="sr-only"
/>
<!-- line -->
@@ -268,12 +271,15 @@
class="bg-slate-100 rounded-md overflow-hidden px-4 py-4 mt-8 mb-8"
>
<div v-if="loadingProfile" class="text-center mb-2">
<fa icon="spinner" class="fa-spin text-slate-400"></fa> Loading
profile...
<font-awesome
icon="spinner"
class="fa-spin text-slate-400"
></font-awesome>
Loading profile...
</div>
<div v-else class="flex items-center mb-2">
<span class="font-bold">Public Profile</span>
<fa
<font-awesome
icon="circle-info"
class="text-slate-400 fa-fw ml-2 cursor-pointer"
@click="showProfileInfo"
@@ -289,9 +295,9 @@
<div class="flex items-center mb-4" @click="toggleUserProfileLocation">
<input
v-model="includeUserProfileLocation"
type="checkbox"
class="mr-2"
v-model="includeUserProfileLocation"
/>
<label for="includeUserProfileLocation">Include Location</label>
</div>
@@ -327,17 +333,16 @@
<div v-if="!loadingProfile && !savingProfile">
<div class="flex justify-between items-center">
<button
@click="saveProfile"
class="mt-2 px-4 py-2 bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white rounded-md"
:disabled="loadingProfile || savingProfile"
:class="{
'opacity-50 cursor-not-allowed': loadingProfile || savingProfile,
}"
@click="saveProfile"
>
Save Profile
</button>
<button
@click="confirmDeleteProfile"
class="mt-2 px-4 py-2 bg-gradient-to-b from-red-400 to-red-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white rounded-md"
:disabled="loadingProfile || savingProfile"
:class="{
@@ -346,6 +351,7 @@
savingProfile ||
(!userProfileDesc && !includeUserProfileLocation),
}"
@click="confirmDeleteProfile"
>
Delete Profile
</button>
@@ -363,7 +369,8 @@
<div class="mb-2 font-bold">Usage Limits</div>
<!-- show spinner if loading limits -->
<div v-if="loadingLimits" class="text-center">
Checking&hellip; <fa icon="spinner" class="fa-spin"></fa>
Checking&hellip;
<font-awesome icon="spinner" class="fa-spin"></font-awesome>
</div>
<div class="mb-4 text-center">
{{ limitsMessage }}
@@ -419,15 +426,15 @@
>
<div class="mb-2 font-bold">Data Export</div>
<router-link
:to="{ name: 'seed-backup' }"
v-if="activeDid"
:to="{ name: 'seed-backup' }"
class="block w-full text-center text-md 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-md mb-2 mt-2"
>
Backup Identifier Seed
</router-link>
<button
v-bind:class="computedStartDownloadLinkClassNames()"
:class="computedStartDownloadLinkClassNames()"
class="block w-full text-center text-md 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-md"
@click="exportDatabase()"
>
@@ -437,7 +444,7 @@
</button>
<a
ref="downloadLink"
v-bind:class="computedDownloadLinkClassNames()"
:class="computedDownloadLinkClassNames()"
class="block w-full text-center text-md bg-gradient-to-b from-green-500 to-green-800 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-6"
>
If no download happened yet, click again here to download now.
@@ -454,7 +461,7 @@
</li>
<li class="list-disc list-outside ml-4">
On Android: Choose "Open" and then share
<fa icon="share-nodes" class="fa-fw" />
<font-awesome icon="share-nodes" class="fa-fw" />
to your prefered place.
</li>
</ul>
@@ -469,7 +476,7 @@
>
Advanced
</h3>
<div id="sectionAdvanced" v-if="showAdvanced || showGeneralAdvanced">
<div v-if="showAdvanced || showGeneralAdvanced" id="sectionAdvanced">
<p class="text-rose-600 mb-8">
Beware: the features here can be confusing and even change data in ways
you do not expect. But we support your freedom!
@@ -489,12 +496,15 @@
>
<code class="truncate">{{ publicBase64 }}</code>
<button
class="ml-2"
@click="
doCopyTwoSecRedo(publicBase64, () => (showB64Copy = !showB64Copy))
"
class="ml-2"
>
<fa icon="copy" class="text-slate-400 fa-fw"></fa>
<font-awesome
icon="copy"
class="text-slate-400 fa-fw"
></font-awesome>
</button>
<span v-show="showB64Copy">Copied</span>
</div>
@@ -505,12 +515,15 @@
>
<code class="truncate">{{ publicHex }}</code>
<button
class="ml-2"
@click="
doCopyTwoSecRedo(publicHex, () => (showPubCopy = !showPubCopy))
"
class="ml-2"
>
<fa icon="copy" class="text-slate-400 fa-fw"></fa>
<font-awesome
icon="copy"
class="text-slate-400 fa-fw"
></font-awesome>
</button>
<span v-show="showPubCopy">Copied</span>
</div>
@@ -522,15 +535,18 @@
>
<code class="truncate">{{ derivationPath }}</code>
<button
class="ml-2"
@click="
doCopyTwoSecRedo(
derivationPath,
() => (showDerCopy = !showDerCopy),
)
"
class="ml-2"
>
<fa icon="copy" class="text-slate-400 fa-fw"></fa>
<font-awesome
icon="copy"
class="text-slate-400 fa-fw"
></font-awesome>
</button>
<span v-show="showDerCopy">Copied</span>
</div>
@@ -557,7 +573,7 @@
</h2>
<div class="ml-4 mt-2">
<input type="file" @change="uploadImportFile" class="ml-2" />
<input type="file" class="ml-2" @change="uploadImportFile" />
<transition
enter-active-class="transform ease-out duration-300 transition"
enter-from-class="translate-y-2 opacity-0 sm:translate-y-4"
@@ -604,8 +620,8 @@
<div class="relative ml-2">
<!-- input -->
<input
type="checkbox"
v-model="showContactGives"
type="checkbox"
name="showContactGives"
class="sr-only"
/>
@@ -622,16 +638,20 @@
<h2 class="text-slate-500 text-sm font-bold mt-4">Claim Server</h2>
<div class="px-4 py-4">
<input
v-model="apiServerInput"
type="text"
class="block w-full rounded border border-slate-400 px-4 py-2"
v-model="apiServerInput"
/>
<button
v-if="apiServerInput != apiServer"
class="w-full px-4 rounded bg-yellow-500 border border-slate-400"
@click="onClickSaveApiServer()"
>
<fa icon="floppy-disk" class="fa-fw" color="white"></fa>
<font-awesome
icon="floppy-disk"
class="fa-fw"
color="white"
></font-awesome>
</button>
<button
class="px-3 rounded bg-slate-200 border border-slate-400"
@@ -663,7 +683,7 @@
<!-- toggle -->
<div class="relative ml-2">
<!-- input -->
<input type="checkbox" v-model="warnIfProdServer" class="sr-only" />
<input v-model="warnIfProdServer" type="checkbox" class="sr-only" />
<!-- line -->
<div class="block bg-slate-500 w-14 h-8 rounded-full"></div>
<!-- dot -->
@@ -683,7 +703,7 @@
<!-- toggle -->
<div class="relative ml-2">
<!-- input -->
<input type="checkbox" v-model="warnIfTestServer" class="sr-only" />
<input v-model="warnIfTestServer" type="checkbox" class="sr-only" />
<!-- line -->
<div class="block bg-slate-500 w-14 h-8 rounded-full"></div>
<!-- dot -->
@@ -699,16 +719,20 @@
</h2>
<div class="px-3 py-4">
<input
v-model="webPushServerInput"
type="text"
class="block w-full rounded border border-slate-400 px-3 py-2"
v-model="webPushServerInput"
/>
<button
v-if="webPushServerInput != webPushServer"
class="w-full px-4 rounded bg-yellow-500 border border-slate-400"
@click="onClickSavePushServer()"
>
<fa icon="floppy-disk" class="fa-fw" color="white"></fa>
<font-awesome
icon="floppy-disk"
class="fa-fw"
color="white"
></font-awesome>
</button>
<button
class="px-3 rounded bg-slate-200 border border-slate-400"
@@ -729,7 +753,7 @@
Use Test 2
</button>
</div>
<span class="px-4 text-sm" v-if="!webPushServerInput">
<span v-if="!webPushServerInput" class="px-4 text-sm">
When that setting is blank, this app will use the default web push
server URL:
{{ DEFAULT_PUSH_SERVER }}
@@ -738,16 +762,20 @@
<h2 class="text-slate-500 text-sm font-bold mb-2">Partner Server URL</h2>
<div class="px-3 py-4">
<input
v-model="partnerApiServerInput"
type="text"
class="block w-full rounded border border-slate-400 px-3 py-2"
v-model="partnerApiServerInput"
/>
<button
v-if="partnerApiServerInput != partnerApiServer"
class="w-full px-4 rounded bg-yellow-500 border border-slate-400"
@click="onClickSavePartnerServer()"
>
<fa icon="floppy-disk" class="fa-fw" color="white"></fa>
<font-awesome
icon="floppy-disk"
class="fa-fw"
color="white"
></font-awesome>
</button>
<button
class="px-3 rounded bg-slate-200 border border-slate-400"
@@ -768,7 +796,7 @@
Use Local
</button>
</div>
<span class="px-4 text-sm" v-if="!partnerApiServerInput">
<span v-if="!partnerApiServerInput" class="px-4 text-sm">
When that setting is blank, this app will use the default partner server
URL:
{{ DEFAULT_PARTNER_API_SERVER }}
@@ -793,8 +821,8 @@
<div class="relative ml-2">
<!-- input -->
<input
type="checkbox"
v-model="hideRegisterPromptOnNewContact"
type="checkbox"
class="sr-only"
/>
<!-- line -->
@@ -818,7 +846,7 @@
<!-- toggle -->
<div class="relative ml-2">
<!-- input -->
<input type="checkbox" v-model="showShortcutBvc" class="sr-only" />
<input v-model="showShortcutBvc" type="checkbox" class="sr-only" />
<!-- line -->
<div class="block bg-slate-500 w-14 h-8 rounded-full" />
<!-- dot -->
@@ -851,9 +879,9 @@
</span>
<div class="relative ml-2">
<input
v-model="passkeyExpirationMinutes"
type="number"
class="border border-slate-400 rounded px-2 py-2 text-center w-20"
v-model="passkeyExpirationMinutes"
@change="updatePasskeyExpiration"
/>
</div>
@@ -872,8 +900,8 @@
<div class="relative ml-2">
<!-- input -->
<input
type="checkbox"
v-model="showGeneralAdvanced"
type="checkbox"
class="sr-only"
/>
<!-- line -->
@@ -895,13 +923,13 @@ import { AxiosError } from "axios";
import { Buffer } from "buffer/";
import Dexie from "dexie";
import "dexie-export-import";
import { ImportProgress } from "dexie-export-import/dist/import";
import { ImportProgress } from "dexie-export-import";
import { LeafletMouseEvent } from "leaflet";
import * as R from "ramda";
import { IIdentifier } from "@veramo/core";
import { ref } from "vue";
import { Component, Vue } from "vue-facing-decorator";
import { Router } from "vue-router";
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
import { useClipboard } from "@vueuse/core";
import { LMap, LMarker, LTileLayer } from "@vue-leaflet/vue-leaflet";
@@ -947,6 +975,7 @@ import {
DIRECT_PUSH_TITLE,
retrieveAccountMetadata,
} from "../libs/util";
import { UserProfile } from "@/libs/partnerServer";
const inputImportFileNameRef = ref<Blob>();
@@ -966,6 +995,8 @@ const inputImportFileNameRef = ref<Blob>();
})
export default class AccountViewView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
$route!: RouteLocationNormalizedLoaded;
$router!: Router;
AppConstants = AppString;
DEFAULT_PUSH_SERVER = DEFAULT_PUSH_SERVER;

View File

@@ -7,17 +7,17 @@
<h1 class="text-lg text-center font-light relative px-7">
<!-- Back -->
<button
@click="$router.go(-1)"
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.go(-1)"
>
<fa icon="chevron-left" class="fa-fw" />
<font-awesome icon="chevron-left" class="fa-fw" />
</button>
Raw Claim
</h1>
</div>
<div class="flex">
<textarea rows="20" class="border-2 w-full" v-model="claimStr"></textarea>
<textarea v-model="claimStr" rows="20" class="border-2 w-full"></textarea>
</div>
<button
class="block w-full text-center text-lg font-bold uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md"
@@ -30,7 +30,6 @@
<script lang="ts">
import { Component, Vue } from "vue-facing-decorator";
import { Router } from "vue-router";
import QuickNav from "../components/QuickNav.vue";
import { NotificationIface } from "../constants/app";
@@ -38,12 +37,15 @@ import { logConsoleAndDb, retrieveSettingsForActiveAccount } from "../db/index";
import * as serverUtil from "../libs/endorserServer";
import * as libsUtil from "../libs/util";
import { errorStringForLog } from "../libs/endorserServer";
import { Router, RouteLocationNormalizedLoaded } from "vue-router";
@Component({
components: { QuickNav },
})
export default class ClaimAddRawView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
$route!: RouteLocationNormalizedLoaded;
$router!: Router;
accountIdentityStr: string = "null";
activeDid = "";
@@ -55,7 +57,7 @@ export default class ClaimAddRawView extends Vue {
this.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || "";
this.claimStr = (this.$route as Router).query["claim"];
this.claimStr = (this.$route.query["claim"] as string) || "";
if (this.claimStr) {
try {
const veriClaim = JSON.parse(this.claimStr);
@@ -65,7 +67,7 @@ export default class ClaimAddRawView extends Vue {
}
} else {
// there may be no link that uses this, meaning you'd have to enter it in a browser
const claimJwtId = (this.$route as Router).query["claimJwtId"];
const claimJwtId = (this.$route.query["claimJwtId"] as string) || "";
if (claimJwtId) {
const urlPath = libsUtil.isGlobalUri(claimJwtId)
? "/api/claim/byHandle/"

View File

@@ -2,8 +2,8 @@
<section id="Content">
<div class="flex items-center justify-center h-screen">
<div v-if="claimData">
<router-link :to="'/claim/' + this.claimId">
<canvas class="w-full block mx-auto" ref="claimCanvas"></canvas>
<router-link :to="'/claim/' + claimId">
<canvas ref="claimCanvas" class="w-full block mx-auto"></canvas>
</router-link>
</div>
</div>
@@ -14,11 +14,10 @@
import { Component, Vue } from "vue-facing-decorator";
import { nextTick } from "vue";
import QRCode from "qrcode";
import { APP_SERVER, NotificationIface } from "../constants/app";
import { db, retrieveSettingsForActiveAccount } from "../db/index";
import * as serverUtil from "../libs/endorserServer";
import { GenericCredWrapper, GenericVerifiableCredential } from "../interfaces";
@Component
export default class ClaimCertificateView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
@@ -81,7 +80,7 @@ export default class ClaimCertificateView extends Vue {
}
async drawCanvas(
claimData: serverUtil.GenericCredWrapper<serverUtil.GenericVerifiableCredential>,
claimData: GenericCredWrapper<GenericVerifiableCredential>,
confirmerIds: Array<string>,
) {
await db.open();

View File

@@ -6,16 +6,6 @@
</section>
</template>
<style scoped>
canvas {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
</style>
<script lang="ts">
import { Component, Vue } from "vue-facing-decorator";
import { nextTick } from "vue";
@@ -188,3 +178,13 @@ export default class ClaimReportCertificateView extends Vue {
}
}
</script>
<style scoped>
canvas {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
</style>

View File

@@ -7,10 +7,10 @@
<h1 class="text-lg text-center font-light relative px-7">
<!-- Back -->
<button
@click="$router.go(-1)"
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.go(-1)"
>
<fa icon="chevron-left" class="fa-fw" />
<font-awesome icon="chevron-left" class="fa-fw" />
</button>
Verifiable Claim Details
</h1>
@@ -22,7 +22,9 @@
<div class="w-full">
<div class="flex columns-3">
<h2 class="text-md font-bold w-full">
{{ capitalizeAndInsertSpacesBeforeCaps(veriClaim.claimType) }}
{{
capitalizeAndInsertSpacesBeforeCaps(veriClaim.claimType || "")
}}
<button
v-if="
['GiveAction', 'Offer', 'PlanAction'].includes(
@@ -32,11 +34,14 @@
// but rather than add more Plan-specific logic to detect the agent
// we'll let them click the Project link and edit from there
"
@click="onClickEditClaim"
title="Edit"
data-testId="editClaimButton"
@click="onClickEditClaim"
>
<fa icon="pen" class="text-sm text-blue-500 ml-2 mb-1" />
<font-awesome
icon="pen"
class="text-sm text-blue-500 ml-2 mb-1"
/>
</button>
</h2>
<div class="flex justify-center w-full">
@@ -45,7 +50,10 @@
class="text-blue-500 mt-2"
title="Printable Certificate"
>
<fa icon="square" class="text-white bg-yellow-500 p-1" />
<font-awesome
icon="square"
class="text-white bg-yellow-500 p-1"
/>
</router-link>
</div>
<!-- show link icon to copy this URL to the clipboard -->
@@ -56,30 +64,37 @@
copyToClipboard('A link to this page', window.location.href)
"
>
<fa icon="link" class="text-slate-500" />
<font-awesome icon="link" class="text-slate-500" />
</button>
</div>
</div>
<div class="text-sm">
<div data-testId="description">
<fa icon="message" class="fa-fw text-slate-400" />
<font-awesome icon="message" class="fa-fw text-slate-400" />
{{
veriClaim.claim?.itemOffered?.description ||
veriClaim.claim?.description
(veriClaim.claim?.itemOffered as any)?.description ||
(veriClaim.claim as any)?.description ||
""
}}
</div>
<div>
<fa icon="user" class="fa-fw text-slate-400" />
<font-awesome icon="user" class="fa-fw text-slate-400" />
{{ didInfo(veriClaim.issuer) }}
</div>
<div>
<fa icon="calendar" class="fa-fw text-slate-400" />
<font-awesome icon="calendar" class="fa-fw text-slate-400" />
Recorded
{{ veriClaim.issuedAt?.replace(/T/, " ").replace(/Z/, " UTC") }}
</div>
<div v-if="veriClaim.claim.image" class="flex justify-center">
<a :href="veriClaim.claim.image" target="_blank">
<img :src="veriClaim.claim.image" class="h-24 rounded-xl" />
<div
v-if="(veriClaim.claim as any).image"
class="flex justify-center"
>
<a :href="(veriClaim.claim as any).image" target="_blank">
<img
:src="(veriClaim.claim as any).image"
class="h-24 rounded-xl"
/>
</a>
</div>
@@ -116,10 +131,10 @@
>
<!-- router-link to /claim/ only changes URL path -->
<a
class="text-blue-500 mt-4 cursor-pointer"
@click="
showDifferentClaimPage(detailsForGive?.fulfillsHandleId)
"
class="text-blue-500 mt-4 cursor-pointer"
>
Fulfills
{{
@@ -155,15 +170,15 @@
<div class="flex gap-4">
<div class="grow overflow-hidden">
<a
class="text-blue-500 mt-4 cursor-pointer"
@click="
provider.identifier.startsWith('did:')
? this.$router.push(
? $router.push(
'/did/' +
encodeURIComponent(provider.identifier),
)
: showDifferentClaimPage(provider.identifier)
"
class="text-blue-500 mt-4 cursor-pointer"
>
an activity...
</a>
@@ -177,13 +192,13 @@
</div>
</div>
<div class="mt-2">
<fa icon="comment" class="text-slate-400" />
<font-awesome icon="comment" class="text-slate-400" />
{{ issuerName }} posted that.
</div>
<!--
<div>
<router-link :to="'/claim-cert/' + encodeURIComponent(veriClaim.id)">
<fa icon="file-contract" class="text-slate-400" />
<font-awesome icon="file-contract" class="text-slate-400" />
<span class="ml-2 text-blue-500">Printable Certificate</span>
</router-link>
</div>
@@ -192,11 +207,14 @@
<div class="mt-8">
<button
v-if="libsUtil.canFulfillOffer(veriClaim)"
@click="openFulfillGiftDialog()"
class="col-span-1 block w-fit text-center text-md 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-md"
@click="openFulfillGiftDialog()"
>
Affirm Delivery
<fa icon="hand-holding-heart" class="ml-2 text-white cursor-pointer" />
<font-awesome
icon="hand-holding-heart"
class="ml-2 text-white cursor-pointer"
/>
</button>
</div>
<GiftedDialog ref="customGiveDialog" />
@@ -204,7 +222,6 @@
<div v-if="libsUtil.isGiveAction(veriClaim)">
<div class="flex columns-3">
<button
class="col-span-1 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-4 py-2 rounded-md"
v-if="
libsUtil.isGiveRecordTheUserCanConfirm(
isRegistered,
@@ -213,10 +230,14 @@
confirmerIdList,
)
"
class="col-span-1 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-4 py-2 rounded-md"
@click="confirmConfirmClaim()"
>
Confirm
<fa icon="circle-check" class="ml-2 text-white cursor-pointer" />
<font-awesome
icon="circle-check"
class="ml-2 text-white cursor-pointer"
/>
</button>
<h2 v-else class="font-bold uppercase text-xl mt-2">Confirmations</h2>
@@ -276,7 +297,10 @@
target="_blank"
class="text-blue-500"
>
<fa icon="arrow-up-right-from-square" class="fa-fw" />
<font-awesome
icon="arrow-up-right-from-square"
class="fa-fw"
/>
</a>
</span>
</div>
@@ -314,7 +338,10 @@
target="_blank"
class="text-blue-500"
>
<fa icon="arrow-up-right-from-square" class="fa-fw" />
<font-awesome
icon="arrow-up-right-from-square"
class="fa-fw"
/>
</a>
</span>
</div>
@@ -345,8 +372,8 @@
@click="showVeriClaimDump = !showVeriClaimDump"
>
Details
<fa v-if="showVeriClaimDump" icon="chevron-up" />
<fa v-else icon="chevron-right" />
<font-awesome v-if="showVeriClaimDump" icon="chevron-up" />
<font-awesome v-else icon="chevron-right" />
</h2>
<div v-if="showVeriClaimDump">
<div
@@ -361,7 +388,7 @@
<span v-if="canShare">
You can ask one of your contacts to take a look and see if their
contacts can see more details:
<a @click="onClickShareClaim()" class="text-blue-500"
<a class="text-blue-500" @click="onClickShareClaim()"
>click to send them this page info</a
>
and see if they can make an introduction. Someone is connected to
@@ -372,8 +399,8 @@
You can ask one of your contacts to take a look and see if their
contacts can see more details:
<a
@click="copyToClipboard('A link to this page', windowLocation)"
class="text-blue-500"
@click="copyToClipboard('A link to this page', windowLocation)"
>click to copy this page info</a
>
and see if they can make an introduction. Someone is connected to
@@ -387,7 +414,7 @@
of your contacts.
<span v-if="canShare">
If you'd like an introduction,
<a @click="onClickShareClaim()" class="text-blue-500"
<a class="text-blue-500" @click="onClickShareClaim()"
>click to share the information with them and ask if they'll tell
you more about the participants.</a
>
@@ -395,8 +422,8 @@
<span v-else>
If you'd like an introduction,
<a
@click="copyToClipboard('A link to this page', windowLocation)"
class="text-blue-500"
@click="copyToClipboard('A link to this page', windowLocation)"
>share this page with them and ask if they'll tell you more about
about the participants.</a
>
@@ -408,7 +435,7 @@
class="list-disc p-4"
>
<div class="text-sm">
<fa icon="minus" class="fa-fw" />
<font-awesome icon="minus" class="fa-fw" />
The {{ visibleDidPath }} is visible to:
</div>
<div class="ml-12 p-1">
@@ -427,7 +454,10 @@
target="_blank"
class="text-blue-500"
>
<fa icon="arrow-up-right-from-square" class="fa-fw" />
<font-awesome
icon="arrow-up-right-from-square"
class="fa-fw"
/>
</a>
</span>
<span v-if="veriClaim.publicUrls?.[visDid]"
@@ -436,7 +466,7 @@
target="_blank"
class="text-blue-500"
>
<fa icon="globe" class="fa-fw" />
<font-awesome icon="globe" class="fa-fw" />
{{
veriClaim.publicUrls[visDid].substring(
veriClaim.publicUrls[visDid].indexOf("//") + 2,
@@ -476,7 +506,7 @@
class="text-blue-500 cursor-pointer"
@click="showFullClaim(veriClaim.id as string)"
>
<fa icon="file-lines" class="fa-fw" />
<font-awesome icon="file-lines" class="fa-fw" />
Load Full Claim Details
</button>
</div>
@@ -492,8 +522,8 @@
target="_blank"
class="text-blue-500 cursor-pointer"
>
<fa icon="file-lines" class="fa-fw" />
<fa icon="arrow-up-right-from-square" class="ml-1 fa-fw" />
<font-awesome icon="file-lines" class="fa-fw" />
<font-awesome icon="arrow-up-right-from-square" class="ml-1 fa-fw" />
View on the Public Server
</a>
</div>
@@ -505,9 +535,9 @@ import { AxiosError } from "axios";
import * as yaml from "js-yaml";
import * as R from "ramda";
import { Component, Vue } from "vue-facing-decorator";
import { Router } from "vue-router";
import { Router, RouteLocationNormalizedLoaded } from "vue-router";
import { useClipboard } from "@vueuse/core";
import { GenericVerifiableCredential } from "../interfaces";
import GiftedDialog from "../components/GiftedDialog.vue";
import QuickNav from "../components/QuickNav.vue";
import { NotificationIface } from "../constants/app";
@@ -521,19 +551,17 @@ import * as serverUtil from "../libs/endorserServer";
import {
GenericCredWrapper,
OfferVerifiableCredential,
} from "../libs/endorserServer";
ProviderInfo,
} from "../interfaces";
import * as libsUtil from "../libs/util";
interface ProviderInfo {
identifier: string; // could be a DID or a handleId
linkConfirmed: boolean;
}
@Component({
components: { GiftedDialog, QuickNav },
})
export default class ClaimView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
$route!: RouteLocationNormalizedLoaded;
$router!: Router;
activeDid = "";
allMyDids: Array<string> = [];
@@ -544,8 +572,12 @@ export default class ClaimView extends Vue {
confirmerIdList: string[] = []; // list of DIDs that have confirmed this claim excluding the issuer
confsVisibleErrorMessage = "";
confsVisibleToIdList: string[] = []; // list of DIDs that can see any confirmer
detailsForGive = null;
detailsForOffer = null;
detailsForGive: {
fulfillsPlanHandleId?: string;
fulfillsType?: string;
fulfillsHandleId?: string;
} | null = null;
detailsForOffer: { fulfillsPlanHandleId?: string } | null = null;
fullClaim = null;
fullClaimDump = "";
fullClaimMessage = "";
@@ -558,7 +590,7 @@ export default class ClaimView extends Vue {
showVeriClaimDump = false;
veriClaim = serverUtil.BLANK_GENERIC_SERVER_RECORD;
veriClaimDump = "";
veriClaimDidsVisible = {};
veriClaimDidsVisible: { [key: string]: string[] } = {};
windowLocation = window.location.href;
R = R;
@@ -585,6 +617,7 @@ export default class ClaimView extends Vue {
}
async created() {
console.log("ClaimView created");
const settings = await retrieveSettingsForActiveAccount();
this.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || "";
@@ -594,7 +627,6 @@ export default class ClaimView extends Vue {
try {
this.allMyDids = await libsUtil.retrieveAccountDids();
} catch (error) {
// continue because we want to see claims, even anonymously
logConsoleAndDb(
"Error retrieving all account DIDs on home page:" + error,
true,
@@ -610,10 +642,8 @@ export default class ClaimView extends Vue {
);
}
const pathParam = window.location.pathname.substring("/claim/".length);
let claimId;
if (pathParam) {
claimId = decodeURIComponent(pathParam);
const claimId = this.$route.params.id as string;
if (claimId) {
await this.loadClaim(claimId, this.activeDid);
} else {
this.$notify(
@@ -627,8 +657,6 @@ export default class ClaimView extends Vue {
);
}
// When Chrome compatibility is fixed https://developer.mozilla.org/en-US/docs/Web/API/Web_Share_API#api.navigator.canshare
// then use this truer check: navigator.canShare && navigator.canShare()
this.canShare = !!navigator.share;
}
@@ -659,6 +687,7 @@ export default class ClaimView extends Vue {
}
async loadClaim(claimId: string, userDid: string) {
console.log("[ClaimView] loadClaim called with claimId:", claimId);
const urlPath = libsUtil.isGlobalUri(claimId)
? "/api/claim/byHandle/"
: "/api/claim/";
@@ -666,6 +695,7 @@ export default class ClaimView extends Vue {
const headers = await serverUtil.getHeaders(userDid);
try {
console.log("[ClaimView] Making API request to:", url);
const resp = await this.axios.get(url, { headers });
if (resp.status === 200) {
this.veriClaim = resp.data;
@@ -887,7 +917,7 @@ export default class ClaimView extends Vue {
),
),
);
const confirmationClaim: serverUtil.GenericVerifiableCredential = {
const confirmationClaim: GenericVerifiableCredential = {
"@context": "https://schema.org",
"@type": "AgreeAction",
object: goodClaim,

View File

@@ -8,8 +8,8 @@
<router-link
:to="{ name: 'account' }"
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
><fa icon="chevron-left" class="fa-fw"></fa
></router-link>
><font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
</router-link>
Confirm Contact
</h1>

View File

@@ -8,10 +8,10 @@
<h1 class="text-lg text-center font-light relative px-7">
<!-- Back -->
<button
@click="$router.go(-1)"
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.go(-1)"
>
<fa icon="chevron-left" class="fa-fw" />
<font-awesome icon="chevron-left" class="fa-fw" />
</button>
<span
v-if="
@@ -32,7 +32,6 @@
<div v-if="giveDetails && !isLoading">
<div class="flex justify-center">
<button
class="col-span-1 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-4 py-2 rounded-md"
v-if="
libsUtil.isGiveRecordTheUserCanConfirm(
isRegistered,
@@ -41,18 +40,25 @@
confirmerIdList,
)
"
class="col-span-1 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-4 py-2 rounded-md"
@click="confirmConfirmClaim()"
>
Confirm
<fa icon="circle-check" class="ml-2 text-white cursor-pointer" />
<font-awesome
icon="circle-check"
class="ml-2 text-white cursor-pointer"
/>
</button>
<button
v-else
@click="notifyWhyCannotConfirm()"
class="col-span-1 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-4 py-2 rounded-md"
@click="notifyWhyCannotConfirm()"
>
Confirm
<fa icon="circle-check" class="ml-2 text-white cursor-pointer" />
<font-awesome
icon="circle-check"
class="ml-2 text-white cursor-pointer"
/>
</button>
</div>
@@ -62,26 +68,29 @@
<div class="overflow-hidden">
<div class="text-sm">
<div>
<fa icon="arrow-left" class="fa-fw text-slate-400" />
<font-awesome icon="arrow-left" class="fa-fw text-slate-400" />
{{ giverName }}
</div>
<div class="ml-6">gave</div>
<div v-if="giveDetails.amount">
<fa icon="hand-holding-dollar" class="fa-fw text-slate-400" />
<font-awesome
icon="hand-holding-dollar"
class="fa-fw text-slate-400"
/>
{{ displayAmount(giveDetails.unit, giveDetails.amount) }}
</div>
<div v-if="giveDetails.description">
<fa icon="message" class="fa-fw text-slate-400" />
<font-awesome icon="message" class="fa-fw text-slate-400" />
{{ giveDetails.amount ? "and:" : "" }}
{{ giveDetails.description }}
</div>
<div class="ml-6">to</div>
<div>
<fa icon="arrow-right" class="fa-fw text-slate-400" />
<font-awesome icon="arrow-right" class="fa-fw text-slate-400" />
{{ recipientName }}
</div>
<div>
<fa icon="calendar" class="fa-fw text-slate-400" />
<font-awesome icon="calendar" class="fa-fw text-slate-400" />
on
{{ giveDetails.issuedAt.substring(0, 10) }}
</div>
@@ -89,7 +98,7 @@
<!-- Fullfills Links -->
<!-- fullfills links for a give -->
<div class="mt-2" v-if="giveDetails?.fulfillsPlanHandleId">
<div v-if="giveDetails?.fulfillsPlanHandleId" class="mt-2">
<router-link
:to="
'/project/' +
@@ -99,7 +108,10 @@
target="_blank"
>
This fulfills a bigger plan
<fa icon="arrow-up-right-from-square" class="fa-fw" />
<font-awesome
icon="arrow-up-right-from-square"
class="fa-fw"
/>
</router-link>
</div>
<!-- if there's another, it's probably fulfilling an offer, too -->
@@ -125,7 +137,10 @@
giveDetails?.fulfillsType || "",
)
}}
<fa icon="arrow-up-right-from-square" class="fa-fw" />
<font-awesome
icon="arrow-up-right-from-square"
class="fa-fw"
/>
</router-link>
</div>
</div>
@@ -133,7 +148,7 @@
</div>
</div>
<div class="mt-2">
<fa icon="comment" class="text-slate-400" />
<font-awesome icon="comment" class="text-slate-400" />
{{ issuerName }} posted that.
</div>
@@ -185,7 +200,10 @@
)
"
>
<fa icon="copy" class="text-slate-400 fa-fw" />
<font-awesome
icon="copy"
class="text-slate-400 fa-fw"
/>
</button>
</span>
</div>
@@ -228,7 +246,10 @@
)
"
>
<fa icon="copy" class="text-slate-400 fa-fw" />
<font-awesome
icon="copy"
class="text-slate-400 fa-fw"
/>
</button>
</span>
</div>
@@ -260,8 +281,8 @@
@click="showVeriClaimDump = !showVeriClaimDump"
>
Details
<fa v-if="showVeriClaimDump" icon="chevron-up" />
<fa v-else icon="chevron-right" />
<font-awesome v-if="showVeriClaimDump" icon="chevron-up" />
<font-awesome v-else icon="chevron-right" />
</h2>
<div v-if="showVeriClaimDump">
<div
@@ -276,7 +297,7 @@
<span v-if="canShare">
You can ask one of your contacts to take a look and see if their
contacts can see more details:
<a @click="onClickShareClaim()" class="text-blue-500"
<a class="text-blue-500" @click="onClickShareClaim()"
>click to send them this page info</a
>
and see if they can make an introduction. Someone is connected to
@@ -287,8 +308,8 @@
You can ask one of your contacts to take a look and see if their
contacts can see more details:
<a
@click="copyToClipboard('A link to this page', windowLocation)"
class="text-blue-500"
@click="copyToClipboard('A link to this page', windowLocation)"
>click to copy this page info</a
>
and see if they can make an introduction. Someone is connected to
@@ -302,7 +323,7 @@
some of your contacts.
<span v-if="canShare">
If you'd like an introduction,
<a @click="onClickShareClaim()" class="text-blue-500"
<a class="text-blue-500" @click="onClickShareClaim()"
>click to share the information with them and ask if they'll tell
you more about the participants.</a
>
@@ -310,8 +331,8 @@
<span v-else>
If you'd like an introduction,
<a
@click="copyToClipboard('A link to this page', windowLocation)"
class="text-blue-500"
@click="copyToClipboard('A link to this page', windowLocation)"
>share this page with them and ask if they'll tell you more about
about the participants.</a
>
@@ -323,7 +344,7 @@
class="list-disc p-4"
>
<div class="text-sm">
<fa icon="minus" class="fa-fw" />
<font-awesome icon="minus" class="fa-fw" />
The {{ visibleDidPath }} is visible to:
</div>
<div class="ml-12 p-1">
@@ -342,12 +363,18 @@
copyToClipboard('The DID of ' + visDid, visDid)
"
>
<fa icon="copy" class="text-slate-400 fa-fw" />
<font-awesome
icon="copy"
class="text-slate-400 fa-fw"
/>
</button>
</span>
<span v-if="veriClaim.publicUrls?.[visDid]"
>, found at
<fa icon="globe" class="fa-fw text-slate-400" />
<font-awesome
icon="globe"
class="fa-fw text-slate-400"
/>
<a
:href="veriClaim.publicUrls?.[visDid]"
class="text-blue-500"
@@ -372,10 +399,10 @@
>
<div class="mt-2 ml-2">
<a
@click="showClaimPage(veriClaim.id)"
class="text-blue-500 cursor-pointer"
@click="showClaimPage(veriClaim.id)"
>
<fa icon="file-lines" />
<font-awesome icon="file-lines" />
See All Generic Info
</a>
</div>
@@ -385,7 +412,7 @@
class="text-blue-500 cursor-pointer"
:href="urlForNewGive"
>
<fa icon="file-lines" />
<font-awesome icon="file-lines" />
Record a Give Similar to the Original
</a>
</div>
@@ -394,10 +421,10 @@
<div v-else-if="!isLoading">This does not have details to confirm.</div>
<div
class="fixed left-6 bottom-24 text-center text-4xl leading-none bg-slate-400 text-white w-14 py-2.5 rounded-full"
v-if="isLoading"
class="fixed left-6 bottom-24 text-center text-4xl leading-none bg-slate-400 text-white w-14 py-2.5 rounded-full"
>
<fa icon="spinner" class="fa-spin-pulse"></fa>
<font-awesome icon="spinner" class="fa-spin-pulse"></font-awesome>
</div>
</section>
</template>
@@ -408,24 +435,26 @@ import * as yaml from "js-yaml";
import * as R from "ramda";
import { Component, Vue } from "vue-facing-decorator";
import { useClipboard } from "@vueuse/core";
import { Router } from "vue-router";
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
import { GenericVerifiableCredential } from "../interfaces";
import QuickNav from "../components/QuickNav.vue";
import { NotificationIface } from "../constants/app";
import { db, retrieveSettingsForActiveAccount } from "../db/index";
import { Contact } from "../db/tables/contacts";
import * as serverUtil from "../libs/endorserServer";
import { displayAmount, GiveSummaryRecord } from "../libs/endorserServer";
import { GiveSummaryRecord } from "../interfaces";
import { displayAmount } from "../libs/endorserServer";
import * as libsUtil from "../libs/util";
import { isGiveAction, retrieveAccountDids } from "../libs/util";
import TopMessage from "../components/TopMessage.vue";
@Component({
methods: { displayAmount },
components: { TopMessage, QuickNav },
})
export default class ClaimView extends Vue {
export default class ConfirmGiftView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
$route!: RouteLocationNormalizedLoaded;
$router!: Router;
activeDid = "";
allMyDids: Array<string> = [];
@@ -447,13 +476,14 @@ export default class ClaimView extends Vue {
urlForNewGive = "";
veriClaim = serverUtil.BLANK_GENERIC_SERVER_RECORD;
veriClaimDump = "";
veriClaimDidsVisible = {};
veriClaimDidsVisible: { [key: string]: string[] } = {};
windowLocation = window.location.href;
R = R;
yaml = yaml;
libsUtil = libsUtil;
serverUtil = serverUtil;
displayAmount = displayAmount;
resetThisValues() {
this.confirmerIdList = [];
@@ -719,7 +749,7 @@ export default class ClaimView extends Vue {
),
),
);
const confirmationClaim: serverUtil.GenericVerifiableCredential = {
const confirmationClaim: GenericVerifiableCredential = {
"@context": "https://schema.org",
"@type": "AgreeAction",
object: goodClaim,

View File

@@ -11,8 +11,8 @@
<router-link
:to="{ name: 'contacts' }"
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
><fa icon="chevron-left" class="fa-fw"></fa
></router-link>
><font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
</router-link>
</h1>
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4">
@@ -59,10 +59,13 @@
<div class="font-bold">
{{ displayAmount(record.unit, record.amount) }}
<span v-if="record.amountConfirmed" title="Confirmed">
<fa icon="circle-check" class="text-green-600 fa-fw" />
<font-awesome
icon="circle-check"
class="text-green-600 fa-fw"
/>
</span>
<button v-else @click="confirm(record)" title="Unconfirmed">
<fa icon="circle" class="text-blue-600 fa-fw" />
<button v-else title="Unconfirmed" @click="confirm(record)">
<font-awesome icon="circle" class="text-blue-600 fa-fw" />
</button>
</div>
<div class="italic text-xs sm:text-sm text-slate-500">
@@ -72,10 +75,10 @@
</td>
<td class="p-1">
<span v-if="record.agentDid == contact?.did">
<fa icon="arrow-left" class="text-slate-400 fa-fw" />
<font-awesome icon="arrow-left" class="text-slate-400 fa-fw" />
</span>
<span v-else>
<fa icon="arrow-right" class="text-slate-400 fa-fw" />
<font-awesome icon="arrow-right" class="text-slate-400 fa-fw" />
</span>
</td>
<td class="p-1">
@@ -83,14 +86,17 @@
<div class="font-bold">
{{ displayAmount(record.unit, record.amount) }}
<span v-if="record.amountConfirmed" title="Confirmed">
<fa icon="circle-check" class="text-green-600 fa-fw" />
<font-awesome
icon="circle-check"
class="text-green-600 fa-fw"
/>
</span>
<button
v-else
@click="cannotConfirmMessage()"
title="Unconfirmed"
@click="cannotConfirmMessage()"
>
<fa icon="circle" class="text-slate-600 fa-fw" />
<font-awesome icon="circle" class="text-slate-600 fa-fw" />
</button>
</div>
<div class="italic text-xs sm:text-sm text-slate-500">
@@ -108,7 +114,7 @@
import { AxiosError, AxiosRequestHeaders } from "axios";
import * as R from "ramda";
import { Component, Vue } from "vue-facing-decorator";
import { Router } from "vue-router";
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
import QuickNav from "../components/QuickNav.vue";
import { NotificationIface } from "../constants/app";
@@ -116,11 +122,13 @@ import { db, retrieveSettingsForActiveAccount } from "../db/index";
import { Contact } from "../db/tables/contacts";
import {
AgreeVerifiableCredential,
GiveSummaryRecord,
GiveVerifiableCredential,
} from "../interfaces";
import {
createEndorserJwtVcFromClaim,
displayAmount,
getHeaders,
GiveSummaryRecord,
GiveVerifiableCredential,
SCHEMA_ORG_CONTEXT,
} from "../libs/endorserServer";
import { retrieveAccountCount } from "../libs/util";
@@ -128,6 +136,8 @@ import { retrieveAccountCount } from "../libs/util";
@Component({ components: { QuickNav } })
export default class ContactAmountssView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
$route!: RouteLocationNormalizedLoaded;
$router!: Router;
activeDid = "";
apiServer = "";
@@ -143,7 +153,7 @@ export default class ContactAmountssView extends Vue {
async created() {
try {
const contactDid = (this.$route as Router).query["contactDid"] as string;
const contactDid = this.$route.query["contactDid"] as string;
this.contact = (await db.contacts.get(contactDid)) || null;
const settings = await retrieveSettingsForActiveAccount();

View File

@@ -7,10 +7,10 @@
<h1 class="text-4xl text-center font-light relative px-7">
<!-- Back -->
<button
@click="$router.go(-1)"
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.go(-1)"
>
<fa icon="chevron-left" class="fa-fw" />
<font-awesome icon="chevron-left" class="fa-fw" />
</button>
{{ contact.name || AppString.NO_CONTACT_NAME }}
</h1>
@@ -25,9 +25,9 @@
Name
</label>
<input
v-model="contactName"
type="text"
class="block w-full ml-2 mt-1 border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500"
v-model="contactName"
/>
</div>
@@ -38,9 +38,9 @@
</label>
<textarea
id="contactNotes"
v-model="contactNotes"
rows="4"
class="block w-full mt-1 border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500"
v-model="contactNotes"
></textarea>
</div>
@@ -53,60 +53,60 @@
class="flex mt-2"
>
<input
type="text"
v-model="method.label"
type="text"
class="block w-1/4 border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500"
placeholder="Label"
/>
<input
type="text"
v-model="method.type"
type="text"
class="block ml-2 w-1/4 border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500"
placeholder="Type"
/>
<div class="relative">
<button
@click="toggleDropdown(index)"
class="px-2 py-1 bg-gray-200 rounded-md"
@click="toggleDropdown(index)"
>
<fa icon="caret-down" class="fa-fw" />
<font-awesome icon="caret-down" class="fa-fw" />
</button>
<div
v-if="dropdownIndex === index"
class="absolute bg-white border border-gray-300 rounded-md mt-1"
>
<div
@click="setMethodType(index, 'CELL')"
class="px-4 py-2 hover:bg-gray-100 cursor-pointer"
@click="setMethodType(index, 'CELL')"
>
CELL
</div>
<div
@click="setMethodType(index, 'EMAIL')"
class="px-4 py-2 hover:bg-gray-100 cursor-pointer"
@click="setMethodType(index, 'EMAIL')"
>
EMAIL
</div>
<div
@click="setMethodType(index, 'WHATSAPP')"
class="px-4 py-2 hover:bg-gray-100 cursor-pointer"
@click="setMethodType(index, 'WHATSAPP')"
>
WHATSAPP
</div>
</div>
</div>
<input
type="text"
v-model="method.value"
type="text"
class="block ml-2 w-1/2 border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500"
placeholder="Number, email, etc."
/>
<button @click="removeContactMethod(index)" class="ml-2 text-red-500">
<fa icon="trash-can" class="fa-fw" />
<button class="ml-2 text-red-500" @click="removeContactMethod(index)">
<font-awesome icon="trash-can" class="fa-fw" />
</button>
</div>
<button @click="addContactMethod" class="mt-2">
<fa
<button class="mt-2" @click="addContactMethod">
<font-awesome
icon="plus"
class="fa-fw px-2 py-2.5 bg-green-500 text-green-100 rounded-full"
/>
@@ -134,7 +134,7 @@
<script lang="ts">
import * as R from "ramda";
import { Component, Vue } from "vue-facing-decorator";
import { RouteLocation, Router } from "vue-router";
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
import QuickNav from "../components/QuickNav.vue";
import TopMessage from "../components/TopMessage.vue";
@@ -142,6 +142,37 @@ import { AppString, NotificationIface } from "../constants/app";
import { db } from "../db/index";
import { Contact, ContactMethod } from "../db/tables/contacts";
/**
* Contact Edit View Component
* @author Matthew Raymer
*
* This component provides a full-featured contact editing interface with support for:
* - Basic contact information (name, notes)
* - Multiple contact methods with type selection
* - Data validation and persistence
*
* Workflow:
* 1. Component loads with DID from route params
* 2. Fetches existing contact data from IndexedDB
* 3. Presents editable form with current values
* 4. Validates and saves updates back to database
*
* Contact Method Types:
* - CELL: Mobile phone numbers
* - EMAIL: Email addresses
* - WHATSAPP: WhatsApp contact info
*
* State Management:
* - Maintains separate state for form fields to prevent direct mutation
* - Handles array cloning for contact methods to prevent reference issues
* - Manages dropdown state for method type selection
*
* Navigation:
* - Back button returns to previous view
* - Save redirects to contact detail view
* - Cancel returns to previous view
* - Invalid DID redirects to contacts list
*/
@Component({
components: {
QuickNav,
@@ -149,22 +180,46 @@ import { Contact, ContactMethod } from "../db/tables/contacts";
},
})
export default class ContactEditView extends Vue {
/** Notification function injected by Vue */
$notify!: (notification: NotificationIface, timeout?: number) => void;
/** Current route instance */
$route!: RouteLocationNormalizedLoaded;
/** Router instance for navigation */
$router!: Router;
/** Current contact data */
contact: Contact = {
did: "",
name: "",
notes: "",
};
/** Editable contact name field */
contactName = "";
/** Editable contact notes field */
contactNotes = "";
/** Array of editable contact methods */
contactMethods: Array<ContactMethod> = [];
/** Currently open dropdown index, null if none open */
dropdownIndex: number | null = null;
/** App string constants */
AppString = AppString;
/**
* Component lifecycle hook that initializes the contact edit form
*
* Workflow:
* 1. Extracts DID from route parameters
* 2. Queries database for existing contact
* 3. Populates form fields with contact data
* 4. Handles missing contact error case
*
* @throws Will not throw but redirects on error
* @emits Notification on contact not found
* @emits Router navigation on error
*/
async created() {
const contactDid = (this.$route as RouteLocation).params.did;
const contactDid = this.$route.params.did;
const contact = await db.contacts.get(contactDid || "");
if (contact) {
this.contact = contact;
@@ -183,29 +238,75 @@ export default class ContactEditView extends Vue {
}
}
/**
* Adds a new empty contact method to the methods array
*
* Creates a new method object with empty fields for:
* - label: Custom label for the method
* - type: Communication type (CELL, EMAIL, WHATSAPP)
* - value: The contact information value
*/
addContactMethod() {
this.contactMethods.push({ label: "", type: "", value: "" });
}
/**
* Removes a contact method at the specified index
*
* @param index The array index of the method to remove
*/
removeContactMethod(index: number) {
this.contactMethods.splice(index, 1);
}
/**
* Toggles the type selection dropdown for a contact method
*
* If the clicked dropdown is already open, closes it.
* If another dropdown is open, closes it and opens the clicked one.
*
* @param index The array index of the method whose dropdown to toggle
*/
toggleDropdown(index: number) {
this.dropdownIndex = this.dropdownIndex === index ? null : index;
}
/**
* Sets the type for a contact method and closes the dropdown
*
* @param index The array index of the method to update
* @param type The new type value (CELL, EMAIL, WHATSAPP)
*/
setMethodType(index: number, type: string) {
this.contactMethods[index].type = type;
this.dropdownIndex = null;
}
/**
* Saves the edited contact information to the database
*
* Workflow:
* 1. Clones contact methods array to prevent reference issues
* 2. Normalizes method types to uppercase
* 3. Checks for changes in method types
* 4. Updates database with new values
* 5. Notifies user of success
* 6. Redirects to contact detail view
*
* @throws Will not throw but notifies on validation errors
* @emits Notification on type changes or success
* @emits Router navigation on success
*/
async saveEdit() {
// without this conversion, "Failed to execute 'put' on 'IDBObjectStore': [object Array] could not be cloned."
const contactMethodsObj = JSON.parse(JSON.stringify(this.contactMethods));
// Normalize method types to uppercase
const contactMethods = contactMethodsObj.map((method: ContactMethod) =>
R.set(R.lensProp("type"), method.type.toUpperCase(), method),
);
// Check for type changes
if (!R.equals(contactMethodsObj, contactMethods)) {
this.contactMethods = contactMethods;
this.$notify(
@@ -219,11 +320,15 @@ export default class ContactEditView extends Vue {
);
return;
}
// Save to database
await db.contacts.update(this.contact.did, {
name: this.contactName,
notes: this.contactNotes,
contactMethods: contactMethods,
});
// Notify success and redirect
this.$notify({
group: "alert",
type: "success",

View File

@@ -9,8 +9,8 @@
<router-link
:to="{ name: 'home' }"
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
><fa icon="chevron-left" class="fa-fw"></fa
></router-link>
><font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
</router-link>
Given by...
</h1>
</div>
@@ -30,10 +30,10 @@
<span class="text-right">
<button
type="button"
@click="openDialog()"
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()"
>
<fa icon="gift" class="fa-fw"></fa>
<font-awesome icon="gift" class="fa-fw"></font-awesome>
</button>
</span>
</h2>
@@ -47,7 +47,7 @@
<span class="grow font-semibold">
<EntityIcon
:contact="contact"
:iconSize="32"
:icon-size="32"
class="inline-block align-middle border border-slate-300 rounded-md mr-1"
/>
{{ contact.name || "(no name)" }}
@@ -55,23 +55,23 @@
<span class="text-right">
<button
type="button"
@click="openDialog(contact)"
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(contact)"
>
<fa icon="gift" class="fa-fw"></fa>
<font-awesome icon="gift" class="fa-fw"></font-awesome>
</button>
</span>
</h2>
</li>
</ul>
<GiftedDialog ref="customDialog" :toProjectId="projectId" />
<GiftedDialog ref="customDialog" :to-project-id="projectId" />
</section>
</template>
<script lang="ts">
import { Component, Vue } from "vue-facing-decorator";
import { Router } from "vue-router";
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
import GiftedDialog from "../components/GiftedDialog.vue";
import QuickNav from "../components/QuickNav.vue";
@@ -86,6 +86,8 @@ import { GiverReceiverInputInfo } from "../libs/util";
})
export default class ContactGiftingView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
$route!: RouteLocationNormalizedLoaded;
$router!: Router;
activeDid = "";
allContacts: Array<Contact> = [];
@@ -107,9 +109,8 @@ export default class ContactGiftingView extends Vue {
(a.name || "").localeCompare(b.name || ""),
);
this.projectId = (this.$route as Router).query["projectId"] || "";
this.prompt = (this.$route as Router).query["prompt"] ?? this.prompt;
this.projectId = (this.$route.query["projectId"] as string) || "";
this.prompt = (this.$route.query["prompt"] as string) ?? this.prompt;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) {

View File

@@ -3,11 +3,8 @@
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Back -->
<div 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="$router.back()"
>
<fa icon="chevron-left" class="fa-fw"></fa>
<h1 class="text-lg text-center px-2 py-1 absolute -left-2 -top-1" @click="$router.back()">
<font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
</h1>
</div>
@@ -17,46 +14,31 @@
</h1>
<div v-if="checkingImports" class="text-center">
<fa icon="spinner" class="animate-spin" />
<font-awesome icon="spinner" class="animate-spin" />
</div>
<div v-else>
<span
v-if="contactsImporting.length > sameCount"
class="flex justify-center"
>
<input type="checkbox" v-model="makeVisible" class="mr-2" />
<span v-if="contactsImporting.length > sameCount" class="flex justify-center">
<input v-model="makeVisible" type="checkbox" class="mr-2" />
Make my activity visible to these contacts.
</span>
<div v-if="sameCount > 0">
<span v-if="sameCount == 1"
>One contact is the same as an existing contact</span
>
<span v-else
>{{ sameCount }} contacts are the same as existing contacts</span
>
<span v-if="sameCount == 1">One contact is the same as an existing contact</span>
<span v-else>{{ sameCount }} contacts are the same as existing contacts</span>
</div>
<!-- Results List -->
<ul
v-if="contactsImporting.length > sameCount"
class="border-t border-slate-300"
>
<ul v-if="contactsImporting.length > sameCount" class="border-t border-slate-300">
<li v-for="(contact, index) in contactsImporting" :key="contact.did">
<div
v-if="
!contactsExisting[contact.did] ||
!R.isEmpty(contactDifferences[contact.did])
"
class="grow overflow-hidden border-b border-slate-300 pt-2.5 pb-4"
>
<div v-if="
!contactsExisting[contact.did] ||
!R.isEmpty(contactDifferences[contact.did])
" class="grow overflow-hidden border-b border-slate-300 pt-2.5 pb-4">
<h2 class="text-base font-semibold">
<input type="checkbox" v-model="contactsSelected[index]" />
<input v-model="contactsSelected[index]" type="checkbox" />
{{ contact.name || AppString.NO_CONTACT_NAME }}
-
<span v-if="contactsExisting[contact.did]" class="text-orange-500"
>Existing</span
>
<span v-if="contactsExisting[contact.did]" class="text-orange-500">Existing</span>
<span v-else class="text-green-500">New</span>
</h2>
<div class="text-sm truncate">
@@ -69,13 +51,9 @@
<div class="font-bold">Old Value</div>
<div class="font-bold">New Value</div>
</div>
<div
v-for="(value, contactField) in contactDifferences[
contact.did
]"
:key="contactField"
class="grid grid-cols-3 border"
>
<div v-for="(value, contactField) in contactDifferences[
contact.did
]" :key="contactField" class="grid grid-cols-3 border">
<div class="border font-bold p-1">
{{ capitalizeAndInsertSpacesBeforeCaps(contactField) }}
</div>
@@ -88,8 +66,7 @@
</li>
<button
class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-sm text-white mt-2 px-2 py-1.5 rounded"
@click="importContacts"
>
@click="importContacts">
Import Selected Contacts
</button>
</ul>
@@ -101,18 +78,10 @@
get the full text and paste it. (Note that iOS cuts off data in text
messages.) Ask the person to send the data a different way, eg. email.
<div class="mt-4 text-center">
<textarea
v-model="inputJwt"
placeholder="Contact-import data"
class="mt-4 border-2 border-gray-300 p-2 rounded"
cols="30"
@input="() => checkContactJwt(inputJwt)"
/>
<textarea v-model="inputJwt" placeholder="Contact-import data"
class="mt-4 border-2 border-gray-300 p-2 rounded" cols="30" @input="() => checkContactJwt(inputJwt)" />
<br />
<button
@click="() => processContactJwt(inputJwt)"
class="ml-2 p-2 bg-blue-500 text-white rounded"
>
<button class="ml-2 p-2 bg-blue-500 text-white rounded" @click="() => processContactJwt(inputJwt)">
Check Import
</button>
</div>
@@ -122,6 +91,77 @@
</template>
<script lang="ts">
/**
* @file Contact Import View Component
* @author Matthew Raymer
*
* This component handles the import of contacts into the TimeSafari app.
* It supports multiple import methods and handles duplicate detection,
* contact validation, and visibility settings.
*
* Import Methods:
* 1. Direct URL Query Parameters:
* Example: /contact-import?contacts=[{"did":"did:example:123","name":"Alice"}]
*
* 2. JWT in URL Path:
* Example: /contact-import/eyJhbGciOiJFUzI1NksifQ...
* - Supports both single and bulk imports
* - JWT payload can be either:
* a) Array format: { contacts: [{did: "...", name: "..."}, ...] }
* b) Single contact: { own: true, did: "...", name: "..." }
*
* 3. Manual JWT Input:
* - Accepts pasted JWT strings
* - Validates format and content before processing
*
* URL Examples:
* ```
* # Bulk import via query params
* /contact-import?contacts=[
* {"did":"did:example:123","name":"Alice"},
* {"did":"did:example:456","name":"Bob"}
* ]
*
* # Single contact via JWT
* /contact-import/eyJhbGciOiJFUzI1NksifQ.eyJvd24iOnRydWUsImRpZCI6ImRpZDpleGFtcGxlOjEyMyJ9...
*
* # Bulk import via JWT
* /contact-import/eyJhbGciOiJFUzI1NksifQ.eyJjb250YWN0cyI6W3siZGlkIjoiZGlkOmV4YW1wbGU6MTIzIn1dfQ...
*
* # Redirect to contacts page (single contact)
* /contacts?contactJwt=eyJhbGciOiJFUzI1NksifQ...
* ```
*
* Features:
* - Automatic duplicate detection
* - Field-by-field comparison for existing contacts
* - Batch visibility settings
* - Auto-import for single new contacts
* - Error handling and validation
*
* State Management:
* - Tracks existing contacts
* - Maintains selection state for bulk imports
* - Records differences for duplicate contacts
* - Manages visibility settings
*
* Security Considerations:
* - JWT validation for imported contacts
* - Visibility control per contact
* - Error handling for malformed data
*
* @example
* // Component usage in router
* {
* path: "/contact-import/:jwt?",
* name: "contact-import",
* component: ContactImportView
* }
*
* @see {@link Contact} for contact data structure
* @see {@link setVisibilityUtil} for visibility management
*/
import * as R from "ramda";
import { Component, Vue } from "vue-facing-decorator";
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
@@ -145,22 +185,75 @@ import {
import { getContactJwtFromJwtUrl } from "../libs/crypto";
import { decodeEndorserJwt } from "../libs/crypto/vc";
/**
* Contact Import View Component
* @author Matthew Raymer
*
* This component handles the secure import of contacts into TimeSafari via JWT tokens.
* It supports both single and multiple contact imports with validation and duplicate detection.
*
* Import Workflows:
* 1. JWT in URL Path (/contact-import/[JWT])
* - Extracts JWT from path
* - Decodes and validates contact data
* - Handles both single and multiple contacts
*
* 2. JWT in Query Parameter (/contacts?contactJwt=[JWT])
* - Used for single contact redirects
* - Processes JWT from query parameter
* - Redirects to appropriate view
*
* JWT Payload Structure:
* ```json
* {
* "iat": 1740740453,
* "contacts": [{
* "did": "did:ethr:0x...",
* "name": "Optional Name",
* "nextPubKeyHashB64": "base64 string",
* "publicKeyBase64": "base64 string"
* }],
* "iss": "did:ethr:0x..."
* }
* ```
*
* Security Features:
* - JWT validation
* - Issuer verification
* - Duplicate detection
* - Contact data validation
*
* @component
*/
@Component({
components: { EntityIcon, OfferDialog, QuickNav },
})
export default class ContactImportView extends Vue {
/** Notification function injected by Vue */
$notify!: (notification: NotificationIface, timeout?: number) => void;
/** Current route instance */
$route!: RouteLocationNormalizedLoaded;
/** Router instance for navigation */
$router!: Router;
// Constants
AppString = AppString;
capitalizeAndInsertSpacesBeforeCaps = capitalizeAndInsertSpacesBeforeCaps;
libsUtil = libsUtil;
R = R;
// Component state
/** Active user's DID for authentication and visibility settings */
activeDid = "";
/** API server URL for backend communication */
apiServer = "";
contactsExisting: Record<string, Contact> = {}; // user's contacts already in the system, keyed by DID
contactsImporting: Array<Contact> = []; // contacts from the import
contactsSelected: Array<boolean> = []; // whether each contact in contactsImporting is selected
/** Map of existing contacts keyed by DID for duplicate detection */
contactsExisting: Record<string, Contact> = {};
/** Array of contacts being imported from JWT */
contactsImporting: Array<Contact> = [];
/** Selection state for each importing contact */
contactsSelected: Array<boolean> = [];
/** Differences between existing and importing contacts */
contactDifferences: Record<
string,
Record<
@@ -170,69 +263,117 @@ export default class ContactImportView extends Vue {
old: string | boolean | Array<ContactMethod> | undefined;
}
>
> = {}; // for existing contacts, it shows the difference between imported and existing contacts for each key
> = {};
/** Loading state for import operations */
checkingImports = false;
/** JWT input for manual contact import */
inputJwt: string = "";
/** Visibility setting for imported contacts */
makeVisible = true;
/** Count of duplicate contacts found */
sameCount = 0;
/**
* Component lifecycle hook that initializes the contact import process
*
* This method handles three distinct import scenarios:
* 1. Query Parameter Import:
* - Checks for contacts in URL query parameters
* - Parses JSON array of contacts if present
*
* 2. JWT URL Import:
* - Extracts JWT from URL path using regex pattern '/contact-import/(ey.+)$'
* - Decodes JWT without validation (supports future-dated QR codes)
* - Handles two JWT payload formats:
* a. Array format: payload.contacts or direct array
* b. Single contact format: redirects to contacts page with JWT
*
* 3. Auto-Import Logic:
* - Automatically imports if exactly one new contact is present
* - Only triggers if no existing contacts match
*
* @throws Will not throw but logs errors during JWT processing
* @emits router.push when redirecting for single contact import
*/
async created() {
await this.initializeSettings();
await this.processQueryParams();
await this.processJwtFromPath();
await this.handleAutoImport();
}
/**
* Initializes component settings from active account
*/
private async initializeSettings() {
const settings = await retrieveSettingsForActiveAccount();
this.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || "";
}
// look for any imported contact array from the query parameter
const importedContacts = (this.$route as RouteLocationNormalizedLoaded)
.query["contacts"] as string;
/**
* Processes contacts from URL query parameters
*/
private async processQueryParams() {
const importedContacts = this.$route.query["contacts"] as string;
if (importedContacts) {
await this.setContactsSelected(JSON.parse(importedContacts));
}
}
// look for a JWT after /contact-import/ in the window.location.pathname
const jwt = window.location.pathname.match(
/\/contact-import\/(ey.+)$/,
)?.[1];
/**
* Processes JWT from URL path and handles different JWT formats
*/
private async processJwtFromPath() {
// JWT tokens always start with 'ey' (base64url encoded header)
const JWT_PATTERN = /\/contact-import\/(ey.+)$/;
const jwt = window.location.pathname.match(JWT_PATTERN)?.[1];
if (jwt) {
// would prefer to validate but we've got an error with JWTs on QR codes generated in the future
// eslint-disable-next-line prettier/prettier
// const parsedJwt: Omit<JWTVerified, "didResolutionResult" | "signer" | "jwt"> = await decodeAndVerifyJwt(jwt);
// decode the JWT
const parsedJwt = decodeEndorserJwt(jwt);
const contacts: Array<Contact> =
parsedJwt.payload.contacts || // someday this will be the only payload sent to this page
parsedJwt.payload.contacts ||
(Array.isArray(parsedJwt.payload) ? parsedJwt.payload : undefined);
if (!contacts && parsedJwt.payload.own) {
// handle this single-contact JWT in the contacts page, better suited to single additions
(this.$router as Router).push({
this.$router.push({
name: "contacts",
query: { contactJwt: jwt },
});
return;
}
if (contacts) {
await this.setContactsSelected(contacts);
} else {
// no contacts found so default message should be OK
}
}
}
/**
* Handles automatic import for single new contacts
*/
private async handleAutoImport() {
if (
this.contactsImporting.length === 1 &&
R.isEmpty(this.contactsExisting)
) {
// if there is only one contact and it's new, then we will automatically import it
this.contactsSelected[0] = true;
this.importContacts(); // ... which routes to the contacts list
await this.importContacts();
}
}
/**
* Processes contacts for import and checks for duplicates
* @param contacts Array of contacts to process
*/
async setContactsSelected(contacts: Array<Contact>) {
this.contactsImporting = contacts;
this.contactsSelected = new Array(this.contactsImporting.length).fill(true);
await db.open();
const baseContacts = await db.contacts.toArray();
// set the existing contacts, keyed by DID, if they exist in contactsImporting
// Check for existing contacts and differences
for (let i = 0; i < this.contactsImporting.length; i++) {
const contactIn = this.contactsImporting[i];
const existingContact = baseContacts.find(
@@ -241,6 +382,7 @@ export default class ContactImportView extends Vue {
if (existingContact) {
this.contactsExisting[contactIn.did] = existingContact;
// Compare contact fields for differences
const differences: Record<
string,
{
@@ -249,7 +391,6 @@ export default class ContactImportView extends Vue {
}
> = {};
Object.keys(contactIn).forEach((key) => {
// eslint-disable-next-line prettier/prettier
if (!R.equals(contactIn[key as keyof Contact], existingContact[key as keyof Contact])) {
differences[key] = {
old: existingContact[key as keyof Contact],
@@ -262,13 +403,16 @@ export default class ContactImportView extends Vue {
this.sameCount++;
}
// don't automatically import previous data
// Don't auto-select duplicates
this.contactsSelected[i] = false;
}
}
}
// check the contact-import JWT
/**
* Validates contact import JWT format
* @param jwtInput JWT string to validate
*/
async checkContactJwt(jwtInput: string) {
if (
jwtInput.endsWith(APP_SERVER) ||
@@ -288,14 +432,15 @@ export default class ContactImportView extends Vue {
}
}
// process the invite JWT and/or text message containing the URL with the JWT
/**
* Processes contact import JWT and updates contacts
* @param jwtInput JWT string containing contact data
*/
async processContactJwt(jwtInput: string) {
this.checkingImports = true;
try {
// (For another approach used with invites, see InviteOneAcceptView.processInvite)
const jwt: string = getContactJwtFromJwtUrl(jwtInput);
// JWT format: { header, payload, signature, data }
const payload = decodeEndorserJwt(jwt).payload;
if (Array.isArray(payload.contacts)) {
@@ -319,10 +464,16 @@ export default class ContactImportView extends Vue {
this.checkingImports = false;
}
/**
* Imports selected contacts and sets visibility if requested
* Updates existing contacts or adds new ones
*/
async importContacts() {
this.checkingImports = true;
let importedCount = 0,
updatedCount = 0;
// Process selected contacts
for (let i = 0; i < this.contactsImporting.length; i++) {
if (this.contactsSelected[i]) {
const contact = this.contactsImporting[i];
@@ -338,6 +489,8 @@ export default class ContactImportView extends Vue {
}
}
}
// Set visibility if requested
if (this.makeVisible) {
const failedVisibileToContacts = [];
for (let i = 0; i < this.contactsImporting.length; i++) {
@@ -364,9 +517,8 @@ export default class ContactImportView extends Vue {
group: "alert",
type: "danger",
title: "Visibility Error",
text: `Failed to set visibility for ${failedVisibileToContacts.length} contact${
failedVisibileToContacts.length == 1 ? "" : "s"
}. You must set them individually: ${failedVisibileToContacts.map((c) => c.name).join(", ")}`,
text: `Failed to set visibility for ${failedVisibileToContacts.length} contact${failedVisibileToContacts.length == 1 ? "" : "s"
}. You must set them individually: ${failedVisibileToContacts.map((c) => c.name).join(", ")}`,
},
-1,
);
@@ -375,6 +527,7 @@ export default class ContactImportView extends Vue {
this.checkingImports = false;
// Show success notification
this.$notify(
{
group: "alert",
@@ -386,7 +539,7 @@ export default class ContactImportView extends Vue {
},
3000,
);
(this.$router as Router).push({ name: "contacts" });
this.$router.push({ name: "contacts" });
}
}
</script>

View File

@@ -10,7 +10,7 @@
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.back()"
>
<fa icon="chevron-left" class="fa-fw" />
<font-awesome icon="chevron-left" class="fa-fw" />
</h1>
</div>
@@ -26,10 +26,8 @@
You aren't sharing your name, so quickly
<br />
<span
@click="
() => $refs.userNameDialog.open((name) => (this.givenName = name))
"
class="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-1 rounded-md"
@click="() => $refs.userNameDialog.open((name) => (givenName = name))"
>
click here to set it for them.
</span>
@@ -38,18 +36,18 @@
<UserNameDialog ref="userNameDialog" />
<div
@click="onCopyUrlToClipboard()"
v-if="activeDid && activeDid.startsWith(ETHR_DID_PREFIX)"
class="text-center"
@click="onCopyUrlToClipboard()"
>
<!--
Play with display options: https://qr-code-styling.com/
See docs: https://www.npmjs.com/package/qr-code-generator-vue3
-->
<QRCodeVue3
:value="this.qrValue"
:cornersSquareOptions="{ type: 'extra-rounded' }"
:dotsOptions="{ type: 'square' }"
:value="qrValue"
:corners-square-options="{ type: 'extra-rounded' }"
:dots-options="{ type: 'square' }"
class="flex justify-center"
/>
<span>
@@ -58,14 +56,14 @@
</div>
<div v-else-if="activeDid" class="text-center">
<!-- Not an ETHR DID so force them to paste it. (Passkey Peer DIDs are too big.) -->
<span @click="onCopyDidToClipboard()" class="text-blue-500">
<span class="text-blue-500" @click="onCopyDidToClipboard()">
Click here to copy your DID to your clipboard.
</span>
<span>
Then give it to them so they can paste it in their list of People.
</span>
</div>
<div class="text-center" v-else>
<div v-else class="text-center">
You have no identitifiers yet, so
<router-link
:to="{ name: 'start' }"
@@ -110,6 +108,7 @@ import {
} from "../libs/endorserServer";
import { decodeEndorserJwt, ETHR_DID_PREFIX } from "../libs/crypto/vc";
import { retrieveAccountMetadata } from "../libs/util";
import { Router } from "vue-router";
@Component({
components: {
@@ -121,6 +120,7 @@ import { retrieveAccountMetadata } from "../libs/util";
})
export default class ContactQRScanShow extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
$router!: Router;
activeDid = "";
apiServer = "";

View File

@@ -7,8 +7,8 @@
<router-link
:to="{ name: 'account' }"
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
><fa icon="chevron-left" class="fa-fw"></fa
></router-link>
><font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
</router-link>
Scan Contact
</h1>

View File

@@ -23,26 +23,26 @@
<!-- New Contact -->
<div id="formAddNewContact" class="mt-4 mb-4 flex items-stretch">
<span class="flex" v-if="isRegistered">
<span v-if="isRegistered" class="flex">
<router-link
:to="{ name: 'invite-one' }"
class="flex items-center bg-gradient-to-b from-green-400 to-green-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-1 mr-1 rounded-md"
>
<fa icon="envelope-open-text" class="fa-fw text-2xl" />
<font-awesome icon="envelope-open-text" class="fa-fw text-2xl" />
</router-link>
<button
@click="showOnboardMeetingDialog()"
class="flex items-center bg-gradient-to-b from-green-400 to-green-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-1 mr-1 rounded-md"
@click="showOnboardMeetingDialog()"
>
<fa icon="chair" class="fa-fw text-2xl" />
<font-awesome icon="chair" class="fa-fw text-2xl" />
</button>
</span>
<span v-else class="flex">
<span
class="flex items-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.5 py-1 mr-1 rounded-md"
>
<fa
<font-awesome
icon="envelope-open-text"
class="fa-fw text-2xl"
@click="
@@ -56,7 +56,7 @@
<span
class="flex items-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.5 py-1 mr-1 rounded-md"
>
<fa
<font-awesome
icon="chair"
class="fa-fw text-2xl"
@click="
@@ -73,38 +73,39 @@
:to="{ name: 'contact-qr' }"
class="flex items-center bg-gradient-to-b from-green-400 to-green-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-1 mr-1 rounded-md"
>
<fa icon="qrcode" class="fa-fw text-2xl" />
<font-awesome icon="qrcode" class="fa-fw text-2xl" />
</router-link>
<textarea
v-model="contactInput"
type="text"
placeholder="New URL or DID, Name, Public Key, Next Public Key Hash"
class="block w-full rounded-l border border-r-0 border-slate-400 px-3 py-2 h-10"
v-model="contactInput"
/>
<button
class="px-4 rounded-r bg-green-200 border border-l-0 border-green-400"
@click="onClickNewContact()"
>
<fa icon="plus" class="fa-fw" />
<font-awesome icon="plus" class="fa-fw" />
</button>
</div>
<div class="flex justify-between" v-if="contacts.length > 0">
<div v-if="contacts.length > 0" class="flex justify-between">
<div class="w-full text-left">
<div v-if="!showGiveNumbers">
<input
type="checkbox"
:checked="contactsSelected.length === contacts.length"
class="align-middle ml-2 h-6 w-6"
data-testId="contactCheckAllTop"
@click="
contactsSelected.length === contacts.length
? (contactsSelected = [])
: (contactsSelected = contacts.map((contact) => contact.did))
"
class="align-middle ml-2 h-6 w-6"
data-testId="contactCheckAllTop"
/>
<button
v-if="!showGiveNumbers"
href=""
class="text-md bg-gradient-to-b shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white ml-2 px-1 py-1 rounded-md"
:style="
@@ -112,14 +113,16 @@
? 'background-image: linear-gradient(to bottom, #3b82f6, #1e40af);'
: 'background-image: linear-gradient(to bottom, #94a3b8, #374151);'
"
@click="copySelectedContacts()"
v-if="!showGiveNumbers"
data-testId="copySelectedContactsButtonTop"
@click="copySelectedContacts()"
>
Copy Selections
</button>
<button @click="showCopySelectionsInfo()">
<fa icon="circle-info" class="text-xl text-blue-500 ml-4" />
<font-awesome
icon="circle-info"
class="text-xl text-blue-500 ml-4"
/>
</button>
</div>
</div>
@@ -136,20 +139,20 @@
</button>
</div>
</div>
<div class="flex justify-between mt-1" v-if="showGiveNumbers">
<div v-if="showGiveNumbers" class="flex justify-between mt-1">
<div class="w-full text-right">
In the following, only the most recent hours are included. To see more,
click
<span
class="text-sm 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 py-1 rounded-md"
>
<fa icon="file-lines" class="fa-fw" />
<font-awesome icon="file-lines" class="fa-fw" />
</span>
<br />
<button
href=""
class="text-md bg-gradient-to-b shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-1 rounded-md mt-1"
v-bind:class="showGiveAmountsClassNames()"
:class="showGiveAmountsClassNames()"
@click="toggleShowGiveTotals()"
>
{{
@@ -159,36 +162,38 @@
? "Confirmed Amounts"
: "Unconfirmed Amounts"
}}
<fa icon="left-right" class="fa-fw" />
<font-awesome icon="left-right" class="fa-fw" />
</button>
</div>
</div>
<!-- Results List -->
<ul
id="listContacts"
v-if="contacts.length > 0"
id="listContacts"
class="border-t border-slate-300 mt-1"
>
<li
class="border-b border-slate-300 pt-1 pb-1"
v-for="contact in filteredContacts()"
:key="contact.did"
class="border-b border-slate-300 pt-1 pb-1"
data-testId="contactListItem"
>
<div class="grow overflow-hidden">
<div class="flex items-center">
<EntityIcon
:contact="contact"
:iconSize="24"
:icon-size="24"
class="inline-block align-text-bottom border border-slate-300 rounded cursor-pointer"
@click="showLargeIdenticon = contact"
/>
<input
type="checkbox"
v-if="!showGiveNumbers"
type="checkbox"
:checked="contactsSelected.includes(contact.did)"
class="ml-2 h-6 w-6 flex-shrink-0"
data-testId="contactCheckOne"
@click="
contactsSelected.includes(contact.did)
? contactsSelected.splice(
@@ -197,8 +202,6 @@
)
: contactsSelected.push(contact.did)
"
class="ml-2 h-6 w-6 flex-shrink-0"
data-testId="contactCheckOne"
/>
<h2
@@ -215,7 +218,10 @@
}"
title="See more about this person"
>
<fa icon="circle-info" class="text-xl text-blue-500 ml-4" />
<font-awesome
icon="circle-info"
class="text-xl text-blue-500 ml-4"
/>
</router-link>
<span class="ml-4 text-sm overflow-hidden">{{
@@ -234,17 +240,17 @@
>
<button
class="text-sm 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-1.5 rounded-l-md"
@click="confirmShowGiftedDialog(contact.did, activeDid)"
:title="givenToMeDescriptions[contact.did] || ''"
@click="confirmShowGiftedDialog(contact.did, activeDid)"
>
From:
<br />
{{
/* eslint-disable prettier/prettier */
this.showGiveTotals
showGiveTotals
? ((givenToMeConfirmed[contact.did] || 0)
+ (givenToMeUnconfirmed[contact.did] || 0))
: this.showGiveConfirmed
: showGiveConfirmed
? (givenToMeConfirmed[contact.did] || 0)
: (givenToMeUnconfirmed[contact.did] || 0)
/* eslint-enable prettier/prettier */
@@ -253,17 +259,17 @@
<button
class="text-sm bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white -ml-1.5 px-2 py-1.5 rounded-r-md border-l"
@click="confirmShowGiftedDialog(activeDid, contact.did)"
:title="givenByMeDescriptions[contact.did] || ''"
@click="confirmShowGiftedDialog(activeDid, contact.did)"
>
To:
<br />
{{
/* eslint-disable prettier/prettier */
this.showGiveTotals
showGiveTotals
? ((givenByMeConfirmed[contact.did] || 0)
+ (givenByMeUnconfirmed[contact.did] || 0))
: this.showGiveConfirmed
: showGiveConfirmed
? (givenByMeConfirmed[contact.did] || 0)
: (givenByMeUnconfirmed[contact.did] || 0)
/* eslint-enable prettier/prettier */
@@ -272,8 +278,8 @@
<button
class="text-sm 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-1.5 rounded-md border border-blue-400"
@click="openOfferDialog(contact.did, contact.name)"
data-testId="offerButton"
@click="openOfferDialog(contact.did, contact.name)"
>
Offer
</button>
@@ -286,7 +292,7 @@
class="text-sm 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-2 py-1.5 rounded-md border border-slate-400"
title="See more given activity"
>
<fa icon="file-lines" class="fa-fw" />
<font-awesome icon="file-lines" class="fa-fw" />
</router-link>
</div>
</div>
@@ -295,20 +301,21 @@
</ul>
<p v-else>There are no contacts.</p>
<div class="mt-2 w-full text-left" v-if="contacts.length > 0">
<div v-if="contacts.length > 0" class="mt-2 w-full text-left">
<input
type="checkbox"
v-if="!showGiveNumbers"
type="checkbox"
:checked="contactsSelected.length === contacts.length"
class="align-middle ml-2 h-6 w-6"
data-testId="contactCheckAllBottom"
@click="
contactsSelected.length === contacts.length
? (contactsSelected = [])
: (contactsSelected = contacts.map((contact) => contact.did))
"
class="align-middle ml-2 h-6 w-6"
data-testId="contactCheckAllBottom"
/>
<button
v-if="!showGiveNumbers"
href=""
class="text-md bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white ml-2 px-1 py-1 rounded-md"
:style="
@@ -317,7 +324,6 @@
: 'background-image: linear-gradient(to bottom, #94a3b8, #374151);'
"
@click="copySelectedContacts()"
v-if="!showGiveNumbers"
>
Copy Selections
</button>
@@ -333,7 +339,7 @@
>
<EntityIcon
:contact="showLargeIdenticon"
:iconSize="512"
:icon-size="512"
class="flex w-11/12 max-w-sm mx-auto mb-3 overflow-hidden bg-white rounded-lg shadow-lg"
@click="showLargeIdenticon = undefined"
/>
@@ -399,6 +405,8 @@ import { generateSaveAndActivateIdentity } from "../libs/util";
})
export default class ContactsView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
$route!: RouteLocationNormalizedLoaded;
$router!: Router;
activeDid = "";
apiServer = "";
@@ -465,8 +473,7 @@ export default class ContactsView extends Vue {
//
// For external links, use /contact-import/:jwt with a JWT that has an array of contacts
// because that will do better error checking for things like missing data on iOS platforms.
const importedContactJwt = (this.$route as RouteLocationNormalizedLoaded)
.query["contactJwt"] as string;
const importedContactJwt = this.$route.query["contactJwt"] as string;
if (importedContactJwt) {
// really should fully verify contents
const { payload } = decodeEndorserJwt(importedContactJwt);
@@ -481,14 +488,13 @@ export default class ContactsView extends Vue {
} as Contact;
await this.addContact(newContact);
// if we're here, they haven't redirected anywhere, so we'll redirect here without a query parameter
(this.$router as Router).push({ path: "/contacts" });
this.$router.push({ path: "/contacts" });
}
}
private async processInviteJwt() {
// handle an invite JWT sent via URL
const importedInviteJwt = (this.$route as RouteLocationNormalizedLoaded)
.query["inviteJwt"] as string;
const importedInviteJwt = this.$route.query["inviteJwt"] as string;
if (importedInviteJwt === "") {
// this happens when a platform (eg iOS) doesn't include anything after the "=" in a shared link.
this.$notify(
@@ -590,7 +596,7 @@ export default class ContactsView extends Vue {
);
}
// if we're here, they haven't redirected anywhere, so we'll redirect here without a query parameter
(this.$router as Router).push({ path: "/contacts" });
this.$router.push({ path: "/contacts" });
}
}
@@ -630,7 +636,7 @@ export default class ContactsView extends Vue {
title: "They're Added To Your List",
text: "Would you like to go to the main page now?",
onYes: async () => {
(this.$router as Router).push({ name: "home" });
this.$router.push({ name: "home" });
},
},
-1,
@@ -767,9 +773,7 @@ export default class ContactsView extends Vue {
if (contactInput.includes(CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI)) {
const jwt = getContactJwtFromJwtUrl(contactInput);
(this.$router as Router).push({
path: "/contact-import/" + jwt,
});
this.$router.push({ path: "/contact-import/" + jwt });
return;
}
@@ -877,7 +881,7 @@ export default class ContactsView extends Vue {
);
try {
const contacts = JSON.parse(jsonContactInput);
(this.$router as Router).push({
this.$router.push({
name: "contact-import",
query: { contacts: JSON.stringify(contacts) },
});
@@ -1203,7 +1207,7 @@ export default class ContactsView extends Vue {
this.showGiftedDialog(giverDid, recipientDid);
},
onYes: async () => {
(this.$router as Router).push({
this.$router.push({
name: "contact-amounts",
query: { contactDid: giverDid },
});
@@ -1403,10 +1407,10 @@ export default class ContactsView extends Vue {
if (hostResponse.data.data) {
// They're the host, take them to setup
(this.$router as Router).push({ name: "onboard-meeting-setup" });
this.$router.push({ name: "onboard-meeting-setup" });
} else {
// They're not the host, take them to list
(this.$router as Router).push({ name: "onboard-meeting-list" });
this.$router.push({ name: "onboard-meeting-list" });
}
} else {
// They're not in a meeting, show the dialog
@@ -1417,11 +1421,11 @@ export default class ContactsView extends Vue {
title: "Onboarding Meeting",
text: "Would you like to start a new meeting?",
onYes: async () => {
(this.$router as Router).push({ name: "onboard-meeting-setup" });
this.$router.push({ name: "onboard-meeting-setup" });
},
yesText: "Start New Meeting",
onNo: async () => {
(this.$router as Router).push({ name: "onboard-meeting-list" });
this.$router.push({ name: "onboard-meeting-list" });
},
noText: "Join Existing Meeting",
},

View File

@@ -9,10 +9,10 @@
<h1 id="ViewHeading" class="text-lg text-center font-light relative px-7">
<!-- Back -->
<button
@click="$router.go(-1)"
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.go(-1)"
>
<fa icon="chevron-left" class="fa-fw"></fa>
<font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
</button>
Identifier Details
</h1>
@@ -29,16 +29,20 @@
<router-link
:to="{ name: 'contact-edit', params: { did: contactFromDid?.did } }"
>
<fa icon="pen" class="text-sm text-blue-500 ml-2 mb-1" />
<font-awesome icon="pen" class="text-sm text-blue-500 ml-2 mb-1" />
</router-link>
</h2>
<button
@click="showDidDetails = !showDidDetails"
class="ml-2 mr-2 mt-4"
@click="showDidDetails = !showDidDetails"
>
Details
<fa v-if="showDidDetails" icon="chevron-down" class="text-blue-400" />
<fa v-else icon="chevron-right" class="text-blue-400" />
<font-awesome
v-if="showDidDetails"
icon="chevron-down"
class="text-blue-400"
/>
<font-awesome v-else icon="chevron-right" class="text-blue-400" />
</button>
<!-- Keep the dump contents directly between > and < to avoid weird spacing. -->
<pre
@@ -54,7 +58,7 @@
>
<EntityIcon
:icon-size="96"
:profileImageUrl="contactFromDid?.profileImageUrl"
:profile-image-url="contactFromDid?.profileImageUrl"
class="inline-block align-text-bottom border border-slate-300 rounded"
@click="showLargeIdenticonUrl = contactFromDid?.profileImageUrl"
/>
@@ -69,61 +73,65 @@
contactFromDid?.seesMe && contactFromDid.did !== activeDid
"
class="text-sm 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 mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
@click="confirmSetVisibility(contactFromDid, false)"
title="They can see you"
@click="confirmSetVisibility(contactFromDid, false)"
>
<fa icon="eye" class="fa-fw" />
<font-awesome icon="eye" class="fa-fw" />
</button>
<button
v-else-if="
!contactFromDid?.seesMe && contactFromDid?.did !== activeDid
"
class="text-sm 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 mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
@click="confirmSetVisibility(contactFromDid, true)"
title="They cannot see you"
@click="confirmSetVisibility(contactFromDid, true)"
>
<fa icon="eye-slash" class="fa-fw" />
<font-awesome icon="eye-slash" class="fa-fw" />
</button>
<button
class="text-sm 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 mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
@click="checkVisibility(contactFromDid)"
title="Check Visibility"
v-if="contactFromDid?.did !== activeDid"
class="text-sm 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 mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
title="Check Visibility"
@click="checkVisibility(contactFromDid)"
>
<fa icon="rotate" class="fa-fw" />
<font-awesome icon="rotate" class="fa-fw" />
</button>
</div>
<button
@click="confirmRegister(contactFromDid)"
class="text-sm 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 ml-6 mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
v-if="contactFromDid?.did !== activeDid"
class="text-sm 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 ml-6 mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
title="Registration"
@click="confirmRegister(contactFromDid)"
>
<fa
<font-awesome
v-if="contactFromDid?.registered"
icon="person-circle-check"
class="fa-fw"
/>
<fa v-else icon="person-circle-question" class="fa-fw" />
<font-awesome
v-else
icon="person-circle-question"
class="fa-fw"
/>
</button>
</div>
<button
@click="confirmDeleteContact(contactFromDid)"
class="text-sm uppercase bg-gradient-to-b from-rose-500 to-rose-800 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white ml-6 mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
title="Delete"
@click="confirmDeleteContact(contactFromDid)"
>
<fa icon="trash-can" class="fa-fw" />
<font-awesome icon="trash-can" class="fa-fw" />
</button>
</div>
<div v-if="!contactFromDid?.profileImageUrl">
<div>Auto-Generated Icon</div>
<div class="flex justify-center">
<EntityIcon
:entityId="viewingDid"
:iconSize="64"
:entity-id="viewingDid"
:icon-size="64"
class="inline-block align-middle border border-slate-300 rounded-md mr-1"
@click="showLargeIdenticonId = viewingDid"
/>
@@ -138,9 +146,9 @@
class="absolute inset-0 h-screen flex flex-col items-center justify-center bg-slate-900/50"
>
<EntityIcon
:entityId="showLargeIdenticonId"
:iconSize="512"
:profileImageUrl="showLargeIdenticonUrl"
:entity-id="showLargeIdenticonId"
:icon-size="512"
:profile-image-url="showLargeIdenticonUrl"
class="flex w-11/12 max-w-sm mx-auto mb-3 overflow-hidden bg-white rounded-lg shadow-lg"
@click="
showLargeIdenticonId = undefined;
@@ -161,10 +169,10 @@
<!-- Loading Animation -->
<div
class="fixed left-6 bottom-24 text-center text-4xl leading-none bg-slate-400 text-white w-14 py-2.5 rounded-full"
v-if="isLoading"
class="fixed left-6 bottom-24 text-center text-4xl leading-none bg-slate-400 text-white w-14 py-2.5 rounded-full"
>
<fa icon="spinner" class="fa-spin-pulse"></fa>
<font-awesome icon="spinner" class="fa-spin-pulse"></font-awesome>
</div>
<!-- Results List -->
<div v-if="claims.length > 0" class="mt-4">
@@ -175,9 +183,9 @@
<InfiniteScroll @reached-bottom="loadMoreData">
<ul>
<li
class="border-b border-slate-300"
v-for="claim in claims"
:key="claim.handleId"
class="border-b border-slate-300"
>
<div class="grid grid-cols-12 gap-4">
<span class="col-span-2">
@@ -193,8 +201,11 @@
{{ claimDescription(claim) }}
</span>
<span class="col-span-1">
<a @click="onClickLoadClaim(claim.id)" class="cursor-pointer">
<fa icon="file-lines" class="pl-2 pt-1 text-blue-500" />
<a class="cursor-pointer" @click="onClickLoadClaim(claim.id)">
<font-awesome
icon="file-lines"
class="pl-2 pt-1 text-blue-500"
/>
</a>
</span>
</div>
@@ -216,7 +227,7 @@
import { AxiosError } from "axios";
import * as yaml from "js-yaml";
import { Component, Vue } from "vue-facing-decorator";
import { Router } from "vue-router";
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
import QuickNav from "../components/QuickNav.vue";
import InfiniteScroll from "../components/InfiniteScroll.vue";
@@ -226,14 +237,16 @@ import { db, retrieveSettingsForActiveAccount } from "../db/index";
import { Contact } from "../db/tables/contacts";
import { BoundingBox } from "../db/tables/settings";
import {
capitalizeAndInsertSpacesBeforeCaps,
didInfoForContact,
displayAmount,
getHeaders,
GenericCredWrapper,
GenericVerifiableCredential,
GiveVerifiableCredential,
OfferVerifiableCredential,
} from "../interfaces";
import {
capitalizeAndInsertSpacesBeforeCaps,
didInfoForContact,
displayAmount,
getHeaders,
register,
setVisibilityUtil,
} from "../libs/endorserServer";
@@ -250,6 +263,8 @@ import EntityIcon from "../components/EntityIcon.vue";
})
export default class DIDView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
$route!: RouteLocationNormalizedLoaded;
$router!: Router;
libsUtil = libsUtil;
yaml = yaml;
@@ -352,7 +367,7 @@ export default class DIDView extends Vue {
},
3000,
);
(this.$router as Router).push({ name: "contacts" });
this.$router.push({ name: "contacts" });
}
// confirm to register a new contact
@@ -505,7 +520,7 @@ export default class DIDView extends Vue {
const route = {
path: "/claim/" + encodeURIComponent(jwtId),
};
(this.$router as Router).push(route);
this.$router.push(route);
}
public claimAmount(claim: GenericVerifiableCredential) {

View File

@@ -18,17 +18,17 @@
:style="{ visibility: isSearchVisible ? 'visible' : 'hidden' }"
>
<input
type="text"
v-model="searchTerms"
type="text"
placeholder="Search…"
class="block w-full rounded-l border border-r-0 border-slate-400 px-3 py-2"
v-on:keyup.enter="searchSelected()"
@keyup.enter="searchSelected()"
/>
<button
class="px-4 rounded-r bg-slate-200 border border-l-0 border-slate-400"
@click="searchSelected()"
>
<fa icon="magnifying-glass" class="fa-fw"></fa>
<font-awesome icon="magnifying-glass" class="fa-fw"></font-awesome>
</button>
</div>
@@ -39,6 +39,7 @@
<li>
<a
href="#"
:class="computedProjectsTabStyleClassNames()"
@click="
projects = [];
userProfiles = [];
@@ -46,7 +47,6 @@
isPeopleActive = false;
searchSelected();
"
v-bind:class="computedProjectsTabStyleClassNames()"
>
Projects
</a>
@@ -54,6 +54,7 @@
<li>
<a
href="#"
:class="computedPeopleTabStyleClassNames()"
@click="
projects = [];
userProfiles = [];
@@ -61,7 +62,6 @@
isPeopleActive = true;
searchSelected();
"
v-bind:class="computedPeopleTabStyleClassNames()"
>
People
</a>
@@ -75,6 +75,7 @@
<li>
<a
href="#"
:class="computedLocalTabStyleClassNames()"
@click="
projects = [];
userProfiles = [];
@@ -85,7 +86,6 @@
tempSearchBox = null;
searchLocal();
"
v-bind:class="computedLocalTabStyleClassNames()"
>
Nearby
<!-- restore when the links don't jump around for different numbers
@@ -101,6 +101,7 @@
<li>
<a
href="#"
:class="computedMappedTabStyleClassNames()"
@click="
projects = [];
userProfiles = [];
@@ -111,7 +112,6 @@
searchTerms = '';
tempSearchBox = null;
"
v-bind:class="computedMappedTabStyleClassNames()"
>
<!-- search is triggered when map component gets to "ready" state -->
Mapped
@@ -120,6 +120,7 @@
<li>
<a
href="#"
:class="computedRemoteTabStyleClassNames()"
@click="
projects = [];
userProfiles = [];
@@ -130,7 +131,6 @@
tempSearchBox = null;
searchAll();
"
v-bind:class="computedRemoteTabStyleClassNames()"
>
Anywhere
<!-- restore when the links don't jump around for different numbers
@@ -152,7 +152,7 @@
class="ml-2 mt-2 px-4 py-2 rounded-md bg-blue-200 text-blue-500"
@click="$router.push({ name: 'search-area' })"
>
<fa icon="location-dot" class="fa-fw" />
<font-awesome icon="location-dot" class="fa-fw" />
Select a {{ searchBox ? "Different" : "" }} Location for Nearby Search
</button>
</div>
@@ -179,10 +179,10 @@
<!-- Loading Animation -->
<div
class="fixed left-6 bottom-24 text-center text-4xl leading-none bg-slate-400 text-white w-14 py-2.5 rounded-full"
v-if="isLoading"
class="fixed left-6 bottom-24 text-center text-4xl leading-none bg-slate-400 text-white w-14 py-2.5 rounded-full"
>
<fa icon="spinner" class="fa-spin-pulse"></fa>
<font-awesome icon="spinner" class="fa-spin-pulse"></font-awesome>
</div>
<div
v-else-if="projects.length === 0 && userProfiles.length === 0"
@@ -205,19 +205,19 @@
<!-- Projects List -->
<template v-if="isProjectsActive">
<li
class="border-b border-slate-300"
v-for="project in projects"
:key="project.handleId"
class="border-b border-slate-300"
>
<a
@click="onClickLoadItem(project.handleId)"
class="block py-4 flex gap-4 cursor-pointer"
@click="onClickLoadItem(project.handleId)"
>
<div>
<ProjectIcon
:entityId="project.handleId"
:iconSize="48"
:imageUrl="project.image"
:entity-id="project.handleId"
:icon-size="48"
:image-url="project.image"
class="block border border-slate-300 rounded-md max-h-12 max-w-12"
/>
</div>
@@ -225,7 +225,10 @@
<div class="grow">
<h2 class="text-base font-semibold">{{ project.name }}</h2>
<div class="text-sm">
<fa icon="user" class="fa-fw text-slate-400"></fa>
<font-awesome
icon="user"
class="fa-fw text-slate-400"
></font-awesome>
{{
didInfo(
project.issuerDid,
@@ -243,17 +246,20 @@
<!-- Profiles List -->
<template v-else>
<li
class="border-b border-slate-300"
v-for="profile in userProfiles"
:key="profile.issuerDid"
class="border-b border-slate-300"
>
<a
@click="onClickLoadItem(profile?.rowId || '')"
class="block py-4 flex gap-4 cursor-pointer"
@click="onClickLoadItem(profile?.rowId || '')"
>
<div class="grow">
<div class="text-sm">
<fa icon="user" class="fa-fw text-slate-400"></fa>
<font-awesome
icon="user"
class="fa-fw text-slate-400"
></font-awesome>
{{
didInfo(
profile.issuerDid,
@@ -273,7 +279,10 @@
v-if="isAnywhereActive && profile.locLat && profile.locLon"
class="mt-1 text-xs text-slate-500"
>
<fa icon="location-dot" class="fa-fw"></fa>
<font-awesome
icon="location-dot"
class="fa-fw"
></font-awesome>
{{
(profile.locLat > 0 ? "North" : "South") +
" in " +
@@ -313,11 +322,11 @@ import {
} from "../db/index";
import { Contact } from "../db/tables/contacts";
import { BoundingBox } from "../db/tables/settings";
import { PlanData } from "../interfaces";
import {
didInfo,
errorStringForLog,
getHeaders,
PlanData,
} from "../libs/endorserServer";
import { OnboardPage, retrieveAccountDids } from "../libs/util";
@@ -352,9 +361,9 @@ export default class DiscoverView extends Vue {
allMyDids: Array<string> = [];
apiServer = "";
isLoading = false;
isLocalActive = true;
isLocalActive = false;
isMappedActive = false;
isAnywhereActive = false;
isAnywhereActive = true;
isProjectsActive = true;
isPeopleActive = false;
isSearchVisible = true;
@@ -375,6 +384,11 @@ export default class DiscoverView extends Vue {
didInfo = didInfo;
async mounted() {
this.searchTerms = this.$route.query["searchText"]?.toString() || "";
const searchPeople = !!this.$route.query["searchPeople"];
const settings = await retrieveSettingsForActiveAccount();
this.activeDid = (settings.activeDid as string) || "";
this.apiServer = (settings.apiServer as string) || "";
@@ -386,25 +400,35 @@ export default class DiscoverView extends Vue {
this.allMyDids = await retrieveAccountDids();
this.searchTerms = this.$route.query["searchText"]?.toString() || "";
if (!settings.finishedOnboarding) {
(this.$refs.onboardingDialog as OnboardingDialog).open(
OnboardPage.Discover,
);
}
if (this.searchBox) {
await this.searchLocal();
// Someday we'll have enough people that we can default to their local area.
// if (this.searchBox) {
// this.isLocalActive = true;
// this.isAnywhereActive = false;
// await this.searchLocal();
//
// const bbox = this.searchBox.bbox;
// this.localCenterLat = (bbox.maxLat + bbox.minLat) / 2;
// this.localCenterLong = (bbox.eastLong + bbox.westLong) / 2;
// } else {
const bbox = this.searchBox.bbox;
this.localCenterLat = (bbox.maxLat + bbox.minLat) / 2;
this.localCenterLong = (bbox.eastLong + bbox.westLong) / 2;
if (searchPeople) {
this.isPeopleActive = true;
this.isProjectsActive = false;
this.isMappedActive = true;
this.isAnywhereActive = false;
}
if (this.isMappedActive) {
// The map will be loaded when it's ready
// and if we try to do it here before the map is ready then we get errors.
} else {
this.isLocalActive = false;
this.isMappedActive = false;
this.isAnywhereActive = true;
await this.searchAll();
await this.searchSelected();
}
}
@@ -466,7 +490,7 @@ export default class DiscoverView extends Vue {
} else {
throw JSON.stringify(results);
}
} else {
} else { // people search must be active
this.projects = [];
const profiles: UserProfile[] = results.data;
if (profiles) {

View File

@@ -13,7 +13,7 @@
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="cancelBack()"
>
<fa icon="chevron-left" class="fa-fw"></fa>
<font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
</h1>
</div>
@@ -44,9 +44,9 @@
>
</h1>
<textarea
v-model="description"
class="block w-full rounded border border-slate-400 mb-2 px-3 py-2"
placeholder="What was received"
v-model="description"
/>
<div class="flex flex-row justify-center">
<span
@@ -59,18 +59,18 @@
class="border border-r-0 border-slate-400 bg-slate-200 px-4 py-2"
@click="amountInput === '0' ? null : decrement()"
>
<fa icon="chevron-left" />
<font-awesome icon="chevron-left" />
</div>
<input
v-model="amountInput"
type="number"
class="border border-r-0 border-slate-400 px-2 py-2 text-center w-20"
v-model="amountInput"
/>
<div
class="rounded-r border border-slate-400 bg-slate-200 px-4 py-2"
@click="increment()"
>
<fa icon="chevron-right" />
<font-awesome icon="chevron-right" />
</div>
</div>
@@ -79,14 +79,14 @@
<a :href="imageUrl" target="_blank">
<img :src="imageUrl" class="h-24 rounded-xl" />
</a>
<fa
<font-awesome
icon="trash-can"
@click="confirmDeleteImage"
class="text-red-500 fa-fw ml-8 mt-10"
@click="confirmDeleteImage"
/>
</span>
<span v-else>
<fa
<font-awesome
icon="camera"
class="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-2 rounded-md"
@click="openImageDialog"
@@ -101,11 +101,11 @@
<div class="flex">
<input
v-if="giverDid && !providedByProject"
v-model="providedByGiver"
type="checkbox"
class="h-6 w-6 mr-2"
v-model="providedByGiver"
/>
<fa
<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"
@@ -117,7 +117,7 @@
: "No named individual gave."
}}
</label>
<fa
<font-awesome
v-if="!giverDid || providedByProject"
icon="info-circle"
class="-mt-1 bg-white text-slate-500 h-5 w-5 px-0.5 py-0.5 rounded-sm"
@@ -128,11 +128,11 @@
<div class="flex">
<input
v-if="providerProjectId && !providedByGiver"
v-model="providedByProject"
type="checkbox"
class="h-6 w-6 mr-2"
v-model="providedByProject"
/>
<fa
<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"
@@ -144,7 +144,7 @@
: "This was not provided by a project."
}}
</label>
<fa
<font-awesome
v-if="!providerProjectId || providedByGiver"
icon="info-circle"
class="-mt-1 bg-white text-slate-500 h-5 w-5 px-0.5 py-0.5 rounded-sm"
@@ -154,7 +154,7 @@
</div>
<div class="flex-shrink flex justify-center items-center">
<fa icon="arrow-right" class="fa-fw h-7" />
<font-awesome icon="arrow-right" class="fa-fw h-7" />
</div>
<!-- Third Column for Recipient -->
@@ -162,11 +162,11 @@
<div class="flex">
<input
v-if="recipientDid && !givenToProject"
v-model="givenToRecipient"
type="checkbox"
class="h-6 w-6 mr-2"
v-model="givenToRecipient"
/>
<fa
<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"
@@ -178,7 +178,7 @@
: "No individual benefitted."
}}
</label>
<fa
<font-awesome
v-if="!recipientDid || givenToProject"
icon="info-circle"
class="-mt-1 bg-white text-slate-500 h-5 w-5 px-0.5 py-0.5 rounded-sm"
@@ -189,11 +189,11 @@
<div class="flex">
<input
v-if="fulfillsProjectId && !givenToRecipient"
v-model="givenToProject"
type="checkbox"
class="h-6 w-6 mr-2"
v-model="givenToProject"
/>
<fa
<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"
@@ -205,7 +205,7 @@
: "No project benefitted."
}}
</label>
<fa
<font-awesome
v-if="!fulfillsProjectId || givenToRecipient"
icon="info-circle"
class="-mt-1 bg-white text-slate-500 h-5 w-5 px-0.5 py-0.5 rounded-sm"
@@ -216,7 +216,7 @@
</div>
<div class="mt-8 flex">
<input type="checkbox" class="h-6 w-6 mr-2" v-model="isTrade" />
<input v-model="isTrade" type="checkbox" class="h-6 w-6 mr-2" />
<label class="text-sm mt-1">This was a trade (not a gift)</label>
</div>
@@ -236,7 +236,7 @@
<p class="text-center mb-2 mt-6 italic">
Sign & Send to publish to the world
<fa
<font-awesome
icon="circle-info"
class="pl-2 text-blue-500 cursor-pointer"
@click="explainData()"
@@ -261,21 +261,20 @@
<script lang="ts">
import { Component, Vue } from "vue-facing-decorator";
import { Router } from "vue-router";
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
import ImageMethodDialog from "../components/ImageMethodDialog.vue";
import QuickNav from "../components/QuickNav.vue";
import TopMessage from "../components/TopMessage.vue";
import { DEFAULT_IMAGE_API_SERVER, NotificationIface } from "../constants/app";
import { db, retrieveSettingsForActiveAccount } from "../db/index";
import { GenericCredWrapper, GiveVerifiableCredential } from "../interfaces";
import {
createAndSubmitGive,
didInfo,
editAndSubmitGive,
GenericCredWrapper,
getHeaders,
getPlanFromCache,
GiveVerifiableCredential,
hydrateGive,
} from "../libs/endorserServer";
import * as libsUtil from "../libs/util";
@@ -290,6 +289,8 @@ import { retrieveAccountDids } from "../libs/util";
})
export default class GiftedDetails extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
$route!: RouteLocationNormalizedLoaded;
$router!: Router;
activeDid = "";
apiServer = "";
@@ -322,9 +323,9 @@ export default class GiftedDetails extends Vue {
async mounted() {
try {
this.prevCredToEdit = (this.$route as Router).query["prevCredToEdit"]
this.prevCredToEdit = (this.$route.query["prevCredToEdit"] as string)
? (JSON.parse(
(this.$route as Router).query["prevCredToEdit"],
this.$route.query["prevCredToEdit"] as string,
) as GenericCredWrapper<GiveVerifiableCredential>)
: undefined;
} catch (error) {
@@ -341,24 +342,23 @@ export default class GiftedDetails extends Vue {
const prevAmount = this.prevCredToEdit?.claim?.object?.amountOfThisGood;
this.amountInput =
(this.$route as Router).query["amountInput"] ||
(this.$route.query["amountInput"] as string) ||
(prevAmount ? String(prevAmount) : "") ||
this.amountInput;
this.description =
(this.$route as Router).query["description"] ||
(this.$route.query["description"] as string) ||
this.prevCredToEdit?.claim?.description ||
this.description;
this.destinationPathAfter = (this.$route as Router).query[
"destinationPathAfter"
];
this.giverDid = ((this.$route as Router).query["giverDid"] ||
this.prevCredToEdit?.claim?.agent?.identifier ||
this.destinationPathAfter =
(this.$route.query["destinationPathAfter"] as string) || "";
this.giverDid = ((this.$route.query["giverDid"] as string) ||
(this.prevCredToEdit?.claim?.agent as unknown as { identifier: string })
?.identifier ||
this.giverDid) as string;
this.giverName =
((this.$route as Router).query["giverName"] as string) || "";
this.giverName = (this.$route.query["giverName"] as string) || "";
this.hideBackButton =
(this.$route as Router).query["hideBackButton"] === "true";
this.message = ((this.$route as Router).query["message"] as string) || "";
(this.$route.query["hideBackButton"] as string) === "true";
this.message = (this.$route.query["message"] as string) || "";
// find any offer ID
const fulfills = this.prevCredToEdit?.claim?.fulfills;
@@ -368,7 +368,7 @@ export default class GiftedDetails extends Vue {
? [fulfills]
: [];
const offer = fulfillsArray.find((rec) => rec["@type"] === "Offer");
this.offerId = ((this.$route as Router).query["offerId"] ||
this.offerId = ((this.$route.query["offerId"] as string) ||
offer?.identifier ||
this.offerId) as string;
@@ -378,7 +378,7 @@ export default class GiftedDetails extends Vue {
);
// eslint-disable-next-line prettier/prettier
this.fulfillsProjectId =
((this.$route as Router).query["fulfillsProjectId"] ||
((this.$route.query["fulfillsProjectId"] as string) ||
fulfillsProject?.identifier ||
this.fulfillsProjectId) as string;
@@ -392,40 +392,38 @@ export default class GiftedDetails extends Vue {
const providerProject = providerArray.find(
(rec) => rec["@type"] === "PlanAction",
);
this.providerProjectId = ((this.$route as Router).query[
this.providerProjectId = ((this.$route.query[
"providerProjectId"
] ||
] as string) ||
providerProject?.identifier ||
this.providerProjectId) as string;
this.recipientDid = ((this.$route as Router).query["recipientDid"] ||
this.recipientDid = ((this.$route.query["recipientDid"] as string) ||
this.prevCredToEdit?.claim?.recipient?.identifier) as string;
this.recipientName =
((this.$route as Router).query["recipientName"] as string) || "";
this.unitCode = ((this.$route as Router).query["unitCode"] ||
this.recipientName = (this.$route.query["recipientName"] as string) || "";
this.unitCode = ((this.$route.query["unitCode"] as string) ||
this.prevCredToEdit?.claim?.object?.unitCode ||
this.unitCode) as string;
this.imageUrl =
((this.$route as Router).query["imageUrl"] as string) ||
this.imageUrl = ((this.$route.query["imageUrl"] as string) ||
this.prevCredToEdit?.claim?.image ||
localStorage.getItem("imageUrl") ||
this.imageUrl;
this.imageUrl) as string;
// this is an endpoint for sharing project info to highlight something given
// https://developer.mozilla.org/en-US/docs/Web/Manifest/share_target
if ((this.$route as Router).query["shareTitle"]) {
if (this.$route.query["shareTitle"] as string) {
this.description =
((this.$route as Router).query["shareTitle"] as string) +
((this.$route.query["shareTitle"] as string) || "") +
(this.description ? "\n" + this.description : "");
}
if ((this.$route as Router).query["shareText"]) {
if (this.$route.query["shareText"] as string) {
this.description =
(this.description ? this.description + "\n" : "") +
((this.$route as Router).query["shareText"] as string);
((this.$route.query["shareText"] as string) || "");
}
if ((this.$route as Router).query["shareUrl"]) {
this.imageUrl = (this.$route as Router).query["shareUrl"] as string;
if (this.$route.query["shareUrl"] as string) {
this.imageUrl = this.$route.query["shareUrl"] as string;
}
const settings = await retrieveSettingsForActiveAccount();

View File

@@ -11,7 +11,7 @@
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.back()"
>
<fa icon="chevron-left" class="fa-fw"></fa>
<font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
</h1>
</div>

View File

@@ -11,7 +11,7 @@
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.back()"
>
<fa icon="chevron-left" class="fa-fw"></fa>
<font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
</h1>
</div>
@@ -30,8 +30,8 @@
<p>
If this works then you're all set.
<button
@click="sendTestWebPushMessage(true)"
class="block w-full text-center text-md 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 mb-2"
@click="sendTestWebPushMessage(true)"
>
Send Yourself a Test Web Push Message (Through Push Server but
Skipping Client Filter)
@@ -140,7 +140,7 @@
class="text-blue-500"
target="_blank"
>
here <fa icon="arrow-up-right-from-square" class="fa-fw" />
here <font-awesome icon="arrow-up-right-from-square" class="fa-fw" />
</a>
</div>
@@ -186,7 +186,7 @@
class="text-blue-500"
target="_blank"
>
here <fa icon="arrow-up-right-from-square" class="fa-fw" />
here <font-awesome icon="arrow-up-right-from-square" class="fa-fw" />
</a>
</div>
</div>
@@ -199,7 +199,7 @@
<p>
Of course, you'll want to back up all your data first -- all seeds as
well as the contacts & settings -- on the Profile
<fa icon="circle-user" /> page.
<font-awesome icon="circle-user" /> page.
</p>
<p>
Here are instructions to uninstall the app and clear out caches and storage.
@@ -246,8 +246,8 @@
<h2 class="text-xl font-semibold mt-4">Tests</h2>
<button
@click="showTestNotification()"
class="block w-full text-center text-md 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 mt-4 mb-2"
@click="showTestNotification()"
>
Send Test Notification Directly to Device (Not Through Push Server)
</button>
@@ -259,8 +259,8 @@
</p>
<button
@click="alertWebPushSubscription()"
class="block w-full text-center text-md 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 mt-4 mb-2"
@click="alertWebPushSubscription()"
>
Show Web Push Subscription Info
</button>
@@ -272,8 +272,8 @@
</p>
<button
@click="sendTestWebPushMessage(true)"
class="block w-full text-center text-md 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 mt-4 mb-2"
@click="sendTestWebPushMessage(true)"
>
Send Yourself a Test Web Push Message (Through Push Server but Skipping
Client Filter)
@@ -285,8 +285,8 @@
</p>
<button
@click="sendTestWebPushMessage()"
class="block w-full text-center text-md 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 mt-4 mb-2"
@click="sendTestWebPushMessage()"
>
Send Yourself a Test Web Push Message (Through Push Server and Client
Filter)
@@ -313,11 +313,12 @@ import { DIRECT_PUSH_TITLE, sendTestThroughPushServer } from "../libs/util";
import PushNotificationPermission from "../components/PushNotificationPermission.vue";
import { db } from "../db/index";
import { MASTER_SETTINGS_KEY } from "../db/tables/settings";
import { Router } from "vue-router";
@Component({ components: { PushNotificationPermission, QuickNav } })
export default class HelpNotificationsView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
$router!: Router;
subscriptionJSON?: PushSubscriptionJSON;
async mounted() {

View File

@@ -19,13 +19,14 @@
:to="{ name: 'invite-one' }"
class="bg-gradient-to-b from-green-400 to-green-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md"
>
<fa icon="envelope-open-text" class="fa-fw text-xl"
<font-awesome icon="envelope-open-text" class="fa-fw text-xl"
/></router-link>
</p>
<p>Then watch that page to see when they accept their invite.</p>
<p>
(That page is also reachable from the Contacts <fa icon="users" /> page
though the invitation <fa icon="envelope-open-text" /> icon.)
(That page is also reachable from the Contacts
<font-awesome icon="users" /> page though the invitation
<font-awesome icon="envelope-open-text" /> icon.)
</p>
<h1 class="mt-4 font-bold text-xl">Next Steps</h1>
@@ -35,12 +36,13 @@
<h1 class="font-bold text-xl">Without a backup, you can lose data.</h1>
<div>
<p>
Exporting backups (from the Account <fa icon="circle-user" /> screen)
is important for the case where they lose their device. This is
especially true for the Identifier Seed: that is theirs and and theirs
alone, and currently nobody else can recover it if they lose it. The
good thing is that anyone can create a new account and simply inform
their network of their new ID.
Exporting backups (from the Account
<font-awesome icon="circle-user" /> screen) is important for the case
where they lose their device. This is especially true for the
Identifier Seed: that is theirs and and theirs alone, and currently
nobody else can recover it if they lose it. The good thing is that
anyone can create a new account and simply inform their network of
their new ID.
</p>
</div>
</div>
@@ -54,7 +56,7 @@
<h1 class="font-bold text-xl">Add Contact & Register</h1>
<p>
You share even more information such as your picture and name when
you share with your QR code at these links: <fa icon="qrcode" />
you share with your QR code at these links: <font-awesome icon="qrcode" />
</p>
<p>
Scanning
@@ -70,14 +72,14 @@
</p>
<p>
2) Scan their QR, or have them tap on it to copy their info and send it to you.
Then you can add them to your Contacts <fa icon="users" />
Then you can add them to your Contacts <font-awesome icon="users" />
</p>
<p>
3) You can register them at their info page <fa icon="circle-info" />
and click on the register button <fa icon="person-circle-question" />
3) You can register them at their info page <font-awesome icon="circle-info" />
and click on the register button <font-awesome icon="person-circle-question" />
</p>
<p>
4) Add yourself to their Contacts <fa icon="users" />
4) Add yourself to their Contacts <font-awesome icon="users" />
</p>
</div>
@@ -94,7 +96,7 @@
<h1 class="font-bold text-xl">Enable Notifications</h1>
<div>
<p>
Enable notifications from the Account page <fa icon="circle-user" />.
Enable notifications from the Account page <font-awesome icon="circle-user" />.
Those notifications might show up on the device depending on your settings.
For the most reliable habits, set an alarm or do some other ritual to record gratitude every day.
</p>

View File

@@ -11,7 +11,7 @@
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.back()"
>
<fa icon="chevron-left" class="fa-fw" />
<font-awesome icon="chevron-left" class="fa-fw" />
</h1>
</div>
@@ -30,8 +30,8 @@
<p class="ml-4">
If you'd like to see the page-by-page help,
<span
@click="unsetFinishedOnboarding()"
class="text-blue-500 cursor-pointer"
@click="unsetFinishedOnboarding()"
>click here</span>.
</p>
@@ -61,7 +61,7 @@
<h2 class="text-xl font-semibold">I want to know more because...</h2>
<ul class="list-disc list-outside ml-4">
<li class="p-2">
<div @click="showAlpha = !showAlpha" class="text-blue-500">... I'm a member of Alpha chat.</div>
<div class="text-blue-500" @click="showAlpha = !showAlpha">... I'm a member of Alpha chat.</div>
<div v-if="showAlpha">
<p>
This is a project for public benefit. You are invited to add your gratitude
@@ -98,7 +98,7 @@
</div>
</li>
<li class="p-2">
<div @click="showGroup = !showGroup" class="text-blue-500">... I want to find a group I'll enjoy working with.</div>
<div class="text-blue-500" @click="showGroup = !showGroup">... I want to find a group I'll enjoy working with.</div>
<div v-if="showGroup">
<p>
This app encourages people to offer small bits of time to one another. It's a way to
@@ -114,7 +114,7 @@
</div>
</li>
<li class="p-2">
<div @click="showCommunity = !showCommunity" class="text-blue-500">... I want to participate in community projects.</div>
<div class="text-blue-500" @click="showCommunity = !showCommunity">... I want to participate in community projects.</div>
<div v-if="showCommunity">
<p>
These are mostly at the beginning stages, so any of them will appreciate your offers that show interest.
@@ -127,7 +127,7 @@
</div>
</li>
<li class="p-2">
<div @click="showVerifiable = !showVerifiable" class="text-blue-500">... I want to build with verifiable, private data.</div>
<div class="text-blue-500" @click="showVerifiable = !showVerifiable">... I want to build with verifiable, private data.</div>
<div v-if="showVerifiable">
<p>
Make your claims and get others to confirm them. Then you can use the API to pull your copy of all that
@@ -153,7 +153,7 @@
</div>
</li>
<li class="p-2">
<div @click="showGovernance = !showGovernance" class="text-blue-500">... I want to build governance organically.</div>
<div class="text-blue-500" @click="showGovernance = !showGovernance">... I want to build governance organically.</div>
<div v-if="showGovernance">
<p>
This requires motivated, dedicated citizens. The good thing is that dedication the primary ingredient;
@@ -172,7 +172,7 @@
</div>
</li>
<li class="p-2">
<div @click="showBasics = !showBasics" class="text-blue-500">... I want to supply life's basics freely.</div>
<div class="text-blue-500" @click="showBasics = !showBasics">... I want to supply life's basics freely.</div>
<div v-if="showBasics">
<p>
This platform is not optimal for balancing needs and resources at this point,
@@ -191,7 +191,7 @@
<h2 class="text-xl font-semibold">How do I get started?</h2>
<p>
Someone -- like the person who told you about this app -- needs to register you
on the Contacts <fa icon="users" class="fa-fw" /> page.
on the Contacts <font-awesome icon="users" class="fa-fw" /> page.
If you heard about this from our outreach, feel free to contact us (below) for a chat.
After someone registers you, you can register others.
</p>
@@ -219,7 +219,7 @@
</p>
<p>
If they are not nearby to scan QR codes, you each can tap on the QR code
and paste it into the text box on the Contacts <fa icon="users" class="fa-fw" /> page.
and paste it into the text box on the Contacts <font-awesome icon="users" class="fa-fw" /> page.
</p>
<h2 class="text-xl font-semibold">
@@ -244,7 +244,7 @@
</h2>
<ul class="list-disc list-outside ml-4">
<li>
Go to Your Identity <fa icon="circle-user" class="fa-fw" /> page.
Go to Your Identity <font-awesome icon="circle-user" class="fa-fw" /> page.
</li>
<li>
Click on "Backup Identifier Seed" and follow the instructions.
@@ -260,7 +260,7 @@
</h2>
<ul class="list-disc list-outside ml-4">
<li>
Go to Your Identity <fa icon="circle-user" class="fa-fw" /> page.
Go to Your Identity <font-awesome icon="circle-user" class="fa-fw" /> page.
</li>
<li>
Click on "Download Settings...". That will save a file to your
@@ -274,7 +274,7 @@
</h2>
<ul class="list-disc list-outside ml-4">
<li>
Go to Your Identity <fa icon="circle-user" class="fa-fw" /> page,
Go to Your Identity <font-awesome icon="circle-user" class="fa-fw" /> page,
tap on your image, and save it.
</li>
</ul>
@@ -315,7 +315,7 @@
</h2>
<ul class="list-disc list-outside ml-4">
<li>
Go to Your Identity <fa icon="circle-user" class="fa-fw" /> page,
Go to Your Identity <font-awesome icon="circle-user" class="fa-fw" /> page,
click Advanced, and follow the instructions for the Contacts & Settings Database "Import".
Beware that this will erase your existing contact & settings.
</li>
@@ -384,7 +384,7 @@
</h2>
<p>
There is an "Advanced" section at the bottom of the Profile
<fa icon="circle-user" /> page.
<font-awesome icon="circle-user" /> page.
</p>
<p>
There is even more functionality in a mobile app (and more
@@ -402,8 +402,8 @@
because they have not given you permission to see their information. Ask
them to add you to their contact list, and ask specifically to make sure
the eye next to your name is open like this
<fa icon="eye" class="fa-fw" /> and not closed like this
<fa icon="eye-slash" class="fa-fw" />.
<font-awesome icon="eye" class="fa-fw" /> and not closed like this
<font-awesome icon="eye-slash" class="fa-fw" />.
</p>
<p>
Sometimes the reason you don't see something is because the search
@@ -444,7 +444,7 @@
</li>
<li>
There may be a problem with your identity. Go to the Identity
<fa icon="circle-user" class="fa-fw" /> page, then "Advanced", and "Switch Identifier"
<font-awesome icon="circle-user" class="fa-fw" /> page, then "Advanced", and "Switch Identifier"
and you may see helpful info there. If it shows a problem, try adding your identifier again.
</li>
<li>
@@ -505,7 +505,7 @@
<ul class="list-disc list-outside ml-4">
<li>
If using notifications, a server stores push token data. That can be revoked at any time
by disabling notifications on the Profile <fa icon="circle-user" class="fa-fw" /> page.
by disabling notifications on the Profile <font-awesome icon="circle-user" class="fa-fw" /> page.
</li>
<li>
If sending images, a server stores them, too. They can be removed by editing the claim
@@ -529,17 +529,17 @@
If you have skills, contact us below.
If you have Bitcoin, donate to
<button
class="text-blue-500 ml-2"
@click="
doCopyTwoSecRedo(
'bc1q90v4ted6cpt63tjfh2lvd5xzfc67sd4g9w8xma',
() => (showDidCopy = !showDidCopy)
)
"
class="text-blue-500 ml-2"
>
bc1q90v4ted6cpt63tjfh2lvd5xzfc67sd4g9w8xma
<fa v-show="!showDidCopy" icon="copy" class="text-sm text-slate-400 fa-fw" />
<fa v-show="showDidCopy" icon="circle-check" class="text-sm text-green-500 fa-fw"/>
<font-awesome v-show="!showDidCopy" icon="copy" class="text-sm text-slate-400 fa-fw" />
<font-awesome v-show="showDidCopy" icon="circle-check" class="text-sm text-green-500 fa-fw"/>
</button>
You can donate online via
<a href="https://www.patreon.com/TimeSafari" target="_blank" class="text-blue-500">Patreon here</a>.
@@ -588,6 +588,7 @@ import {
@Component({ components: { QuickNav } })
export default class Help extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
$router!: Router;
package = Package;
commitHash = import.meta.env.VITE_GIT_HASH;
@@ -614,7 +615,7 @@ export default class Help extends Vue {
finishedOnboarding: false,
});
}
(this.$router as Router).push({ name: "home" });
this.$router.push({ name: "home" });
}
}
</script>

View File

@@ -28,7 +28,7 @@
width="30"
style="display: inline; margin: 0 5px; vertical-align: middle"
/>and then "Add to Home Screen"
<fa icon="square-plus" title="Apple 'Add' icon" />
<font-awesome icon="square-plus" title="Apple 'Add' icon" />
and go click on that new app.
</span>
<span
@@ -36,12 +36,8 @@
>
You should see a prompt to install, or you can click on the
top-right dots
<fa
icon="ellipsis-vertical"
title="vertical ellipsis"
class="fa-fw"
/>
and then "Install"<img
<font-awesome icon="ellipsis-vertical" title="vertical ellipsis" />
/> and then "Install"<img
src="../assets/help/install-android-chrome.png"
alt="Android 'install' icon"
width="30"
@@ -73,7 +69,7 @@
<div class="mb-8">
<div v-if="isCreatingIdentifier">
<p class="text-slate-500 text-center italic mt-4 mb-4">
<fa icon="spinner" class="fa-spin-pulse" />
<font-awesome icon="spinner" class="fa-spin-pulse" />
Loading&hellip;
</p>
</div>
@@ -91,8 +87,8 @@
To share, someone must register you.
<div class="block text-center">
<button
@click="showNameThenIdDialog()"
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
@@ -116,10 +112,10 @@
<div class="flex">
<h2 class="text-xl font-bold">What have you seen someone do?</h2>
<button
@click="openGiftedPrompts()"
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()"
>
<fa icon="lightbulb" class="fa-fw" />
<font-awesome icon="lightbulb" class="fa-fw" />
</button>
</div>
@@ -147,7 +143,7 @@
>
<EntityIcon
:contact="contact"
:iconSize="64"
:icon-size="64"
class="mx-auto border border-blue-500 rounded-md mb-1 cursor-pointer"
/>
<h3
@@ -181,7 +177,7 @@
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()"
>
<fa icon="plus" class="fa-fw" />
<font-awesome icon="plus" class="fa-fw" />
</button>
</div>
@@ -192,12 +188,12 @@
Latest Activity
<button @click="openFeedFilters()">
<span class="text-xs text-white">
<fa
<font-awesome
v-if="resultsAreFiltered()"
icon="filter"
class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] px-1 py-1.5 rounded-md"
/>
<fa
<font-awesome
v-else
icon="filter"
class="bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] px-1 py-1.5 rounded-md"
@@ -208,8 +204,8 @@
</div>
<div
@click="goToActivityToUserPage()"
class="border-t p-2 border-slate-300"
@click="goToActivityToUserPage()"
>
<div class="flex justify-center">
<div
@@ -251,13 +247,13 @@
<InfiniteScroll @reached-bottom="loadMoreGives">
<ul id="listLatestActivity" class="border-t border-slate-300">
<li
class="border-b border-slate-300 py-2"
v-for="record in feedData"
:key="record.jwtId"
class="border-b border-slate-300 py-2"
>
<div
class="border-b border-slate-300 text-orange-400 pb-2 mb-2 font-bold text-sm"
v-if="record.jwtId == feedLastViewedClaimId"
class="border-b border-slate-300 text-orange-400 pb-2 mb-2 font-bold text-sm"
>
You've already seen all the following
</div>
@@ -265,7 +261,7 @@
<div class="grid grid-cols-12">
<span class="pt-1 col-span-1 justify-self-start">
<span>
<fa
<font-awesome
icon="circle-user"
:class="
computeKnownPersonIconStyleClassNames(
@@ -274,7 +270,7 @@
"
@click="toastUser('This involves your contacts.')"
/>
<fa
<font-awesome
icon="gift"
class="pl-3 text-slate-500"
@click="toastUser('This is a gift.')"
@@ -295,7 +291,7 @@
:profile-image-url="record.giver.profileImageUrl"
class="inline-block align-middle border border-slate-300 rounded-md mr-1"
/>
<fa
<font-awesome
v-if="
record.agentDid !== activeDid &&
record.recipientDid !== activeDid &&
@@ -319,7 +315,7 @@
{{ giveDescription(record) }}
</span>
<a @click="onClickLoadClaim(record.jwtId)">
<fa
<font-awesome
icon="file-lines"
class="pl-2 text-slate-500 cursor-pointer"
/>
@@ -333,7 +329,7 @@
encodeURIComponent(record.fulfillsPlanHandleId)
"
>
<fa icon="hammer" class="text-blue-500" />
<font-awesome icon="hammer" class="text-blue-500" />
</router-link>
<router-link
v-if="record.providerPlanHandleId"
@@ -342,7 +338,7 @@
encodeURIComponent(record.providerPlanHandleId)
"
>
<fa icon="hammer" class="text-blue-500" />
<font-awesome icon="hammer" class="text-blue-500" />
</router-link>
</span>
</div>
@@ -364,7 +360,7 @@
</InfiniteScroll>
<div v-if="isFeedLoading">
<p class="text-slate-500 text-center italic mt-4 mb-4">
<fa icon="spinner" class="fa-spin-pulse" /> Loading&hellip;
<font-awesome icon="spinner" class="fa-spin-pulse" /> Loading&hellip;
</p>
</div>
<div v-if="!isFeedLoading && feedData.length === 0">
@@ -378,9 +374,9 @@
<ChoiceButtonDialog ref="choiceButtonDialog" />
<ImageViewer
v-model:is-open="isImageViewerOpen"
:image-url="selectedImage"
:image-data="selectedImageData"
v-model:is-open="isImageViewerOpen"
/>
</template>
@@ -438,6 +434,7 @@ import {
} from "../libs/util";
interface GiveRecordWithContactInfo extends GiveSummaryRecord {
jwtId: string;
giver: {
displayName: string;
known: boolean;
@@ -731,6 +728,7 @@ export default class HomeView extends Vue {
const newRecord: GiveRecordWithContactInfo = {
...record,
jwtId: record.jwtId,
giver: didInfoForContact(
giverDid,
this.activeDid,

View File

@@ -8,7 +8,7 @@
<router-link
:to="{ name: 'account' }"
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
><fa icon="chevron-left" class="fa-fw"></fa>
><font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
</router-link>
Switch Identity
@@ -22,7 +22,10 @@
v-if="activeDid && !activeDidInIdentities"
class="block bg-slate-100 rounded-md flex items-center px-4 py-3 mb-4"
>
<fa icon="circle-check" class="fa-fw text-red-600 text-xl mr-3"></fa>
<font-awesome
icon="circle-check"
class="fa-fw text-red-600 text-xl mr-3"
></font-awesome>
<div class="text-sm text-slate-500">
<div class="overflow-hidden truncate">
<b>ID:</b> <code>{{ activeDid }}</code>
@@ -45,12 +48,12 @@
class="flex flex-grow items-center bg-slate-100 rounded-md px-4 py-3 mb-2 truncate cursor-pointer"
@click="switchAccount(ident.did)"
>
<fa
<font-awesome
v-if="ident.did === activeDid"
icon="circle-check"
class="fa-fw text-blue-600 text-xl mr-3"
/>
<fa
<font-awesome
v-else
icon="circle"
class="fa-fw text-slate-400 text-xl mr-3"
@@ -62,13 +65,13 @@
</span>
</div>
<div>
<fa
<font-awesome
v-if="ident.did === activeDid"
icon="trash-can"
class="text-slate-400 text-xl ml-2 mr-2 cursor-pointer"
@click="notifyCannotDelete()"
/>
<fa
<font-awesome
v-else
icon="trash-can"
class="text-red-600 text-xl ml-2 mr-2 cursor-pointer"
@@ -114,6 +117,7 @@ import { retrieveAllAccountsMetadata } from "../libs/util";
@Component({ components: { QuickNav } })
export default class IdentitySwitcherView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
$router!: Router;
public activeDid = "";
public activeDidInIdentities = false;
@@ -131,7 +135,10 @@ export default class IdentitySwitcherView extends Vue {
const accounts = await retrieveAllAccountsMetadata();
for (let n = 0; n < accounts.length; n++) {
const acct = accounts[n];
this.otherIdentities.push({ id: acct.id as string, did: acct.did });
this.otherIdentities.push({
id: (acct.id ?? 0).toString(),
did: acct.did,
});
if (acct.did && this.activeDid === acct.did) {
this.activeDidInIdentities = true;
}
@@ -159,7 +166,7 @@ export default class IdentitySwitcherView extends Vue {
await db.settings.update(MASTER_SETTINGS_KEY, {
activeDid: did,
});
(this.$router as Router).push({ name: "account" });
this.$router.push({ name: "account" });
}
async deleteAccount(id: string) {

View File

@@ -5,10 +5,10 @@
<h1 class="text-lg text-center font-light relative px-7">
<!-- Cancel -->
<button
@click="$router.go(-1)"
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.go(-1)"
>
<fa icon="chevron-left"></fa>
<font-awesome icon="chevron-left"></font-awesome>
</button>
Import Existing Identifier
</h1>
@@ -20,10 +20,10 @@
<!-- id used by puppeteer test script -->
<textarea
id="seed-input"
v-model="mnemonic"
type="text"
placeholder="Seed Phrase"
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
v-model="mnemonic"
/>
<h3
@@ -35,22 +35,22 @@
<div v-if="showAdvanced">
Enter a custom derivation path
<input
v-model="derivationPath"
type="text"
class="block w-full rounded border border-slate-400 mb-2 px-3 py-2"
v-model="derivationPath"
/>
<span class="ml-4">
For previous uPort or Endorser users,
<a
@click="derivationPath = UPORT_DERIVATION_PATH"
class="text-blue-500"
@click="derivationPath = UPORT_DERIVATION_PATH"
>
click here to use that value.
</a>
</span>
<div class="mt-4" v-if="numAccounts == 1">
<input type="checkbox" class="mr-2" v-model="shouldErase" />
<div v-if="numAccounts == 1" class="mt-4">
<input v-model="shouldErase" type="checkbox" class="mr-2" />
<label>Erase the previous identifier.</label>
</div>
@@ -65,15 +65,15 @@
<div class="mt-8">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
<button
@click="fromMnemonic()"
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="fromMnemonic()"
>
Import
</button>
<button
@click="onCancelClick()"
type="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="onCancelClick()"
>
Cancel
</button>
@@ -111,6 +111,7 @@ export default class ImportAccountView extends Vue {
AppString = AppString;
$notify!: (notification: NotificationIface, timeout?: number) => void;
$router!: Router;
apiServer = "";
address = "";
@@ -130,7 +131,7 @@ export default class ImportAccountView extends Vue {
}
public onCancelClick() {
(this.$router as Router).back();
this.$router.back();
}
public isNotProdServer() {
@@ -170,7 +171,7 @@ export default class ImportAccountView extends Vue {
await db.settings.update(MASTER_SETTINGS_KEY, {
activeDid: newId.did,
});
(this.$router as Router).push({ name: "account" });
this.$router.push({ name: "account" });
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) {
console.error("Error saving mnemonic & updating settings:", err);

View File

@@ -5,10 +5,10 @@
<h1 class="text-lg text-center font-light relative px-7">
<!-- Cancel -->
<button
@click="$router.go(-1)"
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.go(-1)"
>
<fa icon="chevron-left"></fa>
<font-awesome icon="chevron-left"></font-awesome>
</button>
Derive from Existing Identity
</h1>
@@ -25,21 +25,21 @@
</p>
<ul class="mb-4">
<li
class="block bg-slate-100 rounded-md flex items-center px-4 py-3 mb-2"
v-for="dids in didArrays"
:key="dids[0]"
class="block bg-slate-100 rounded-md flex items-center px-4 py-3 mb-2"
@click="switchAccount(dids[0])"
>
<fa
<font-awesome
v-if="dids[0] == selectedArrayFirstDid"
icon="circle"
class="fa-fw text-blue-500 text-xl mr-3"
></fa>
<fa
></font-awesome>
<font-awesome
v-else
icon="circle"
class="fa-fw text-slate-400 text-xl mr-3"
></fa>
></font-awesome>
<span class="overflow-hidden">
<div class="text-sm text-slate-500 truncate">
<code>{{ dids.join(",") }}</code>
@@ -51,15 +51,15 @@
<div class="mt-8">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
<button
@click="incrementDerivation()"
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="incrementDerivation()"
>
Increment and Import
</button>
<button
@click="onCancelClick()"
type="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="onCancelClick()"
>
Cancel
</button>
@@ -70,7 +70,7 @@
<script lang="ts">
import { Component, Vue } from "vue-facing-decorator";
import { Router } from "vue-router";
import { Router, RouteLocationNormalizedLoaded } from "vue-router";
import {
DEFAULT_ROOT_DERIVATION_PATH,
@@ -86,6 +86,9 @@ import { retrieveAllFullyDecryptedAccounts } from "../libs/util";
components: {},
})
export default class ImportAccountView extends Vue {
$route!: RouteLocationNormalizedLoaded;
$router!: Router;
derivationPath = DEFAULT_ROOT_DERIVATION_PATH;
didArrays: Array<Array<string>> = [];
selectedArrayFirstDid = "";
@@ -102,7 +105,7 @@ export default class ImportAccountView extends Vue {
}
public onCancelClick() {
(this.$router as Router).back();
this.$router.back();
}
public switchAccount(did: string) {
@@ -151,7 +154,7 @@ export default class ImportAccountView extends Vue {
await db.settings.update(MASTER_SETTINGS_KEY, {
activeDid: newId.did,
});
(this.$router as Router).push({ name: "account" });
this.$router.push({ name: "account" });
} catch (err) {
console.error("Error saving mnemonic & updating settings:", err);
}

View File

@@ -5,7 +5,7 @@
v-if="checkingInvite"
class="text-lg text-center font-light relative px-7"
>
<fa icon="spinner" class="fa-spin-pulse" />
<font-awesome icon="spinner" class="fa-spin-pulse" />
</div>
<div v-else class="text-center mt-4">
<p>That invitation did not work.</p>
@@ -28,8 +28,8 @@
/>
<br />
<button
@click="() => processInvite(inputJwt, true)"
class="ml-2 p-2 bg-blue-500 text-white rounded"
@click="() => processInvite(inputJwt, true)"
>
Accept
</button>
@@ -55,6 +55,7 @@ import { generateSaveAndActivateIdentity } from "../libs/util";
@Component({ components: { QuickNav } })
export default class InviteOneAcceptView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
$router!: Router;
activeDid: string = "";
apiServer: string = "";
@@ -123,7 +124,7 @@ export default class InviteOneAcceptView extends Vue {
// That's good enough for an initial check.
// Send them to the contacts page to finish, with inviteJwt in the query string.
(this.$router as Router).push({
this.$router.push({
name: "contacts",
query: { inviteJwt: jwt },
});

View File

@@ -9,7 +9,7 @@
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.back()"
>
<fa icon="chevron-left" class="fa-fw"></fa>
<font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
</h1>
</div>
@@ -40,7 +40,7 @@
class="fixed right-6 top-12 text-center text-4xl leading-none bg-green-600 text-white w-14 py-2.5 rounded-full"
@click="createInvite()"
>
<fa icon="plus" class="fa-fw"></fa>
<font-awesome icon="plus" class="fa-fw"></font-awesome>
</button>
<InviteDialog ref="inviteDialog" />
@@ -72,16 +72,18 @@
!invite.redeemedAt &&
invite.expiresAt > new Date().toISOString()
"
class="text-center text-blue-500 cursor-pointer"
:title="inviteLink(invite.jwt)"
@click="
copyInviteAndNotify(invite.inviteIdentifier, invite.jwt)
"
class="text-center text-blue-500 cursor-pointer"
:title="inviteLink(invite.jwt)"
>
{{ getTruncatedInviteId(invite.inviteIdentifier) }}
</span>
<span
v-else
class="text-center text-slate-500 cursor-pointer"
:title="inviteLink(invite.jwt)"
@click="
showInvite(
invite.inviteIdentifier,
@@ -89,8 +91,6 @@
invite.expiresAt < new Date().toISOString(),
)
"
class="text-center text-slate-500 cursor-pointer"
:title="inviteLink(invite.jwt)"
>
{{ getTruncatedInviteId(invite.inviteIdentifier) }}
</span>
@@ -106,7 +106,7 @@
<br />
{{ getTruncatedRedeemedBy(invite.redeemedBy) }}
<br />
<fa
<font-awesome
v-if="invite.redeemedBy && !contactsRedeemed[invite.redeemedBy]"
icon="plus"
class="bg-green-600 text-white px-1 py-1 rounded-full cursor-pointer"
@@ -114,7 +114,7 @@
/>
</td>
<td>
<fa
<font-awesome
icon="trash-can"
class="text-red-600 text-xl ml-2 mr-2 cursor-pointer"
@click="deleteInvite(invite.inviteIdentifier, invite.notes)"
@@ -132,6 +132,7 @@
import axios from "axios";
import { Component, Vue } from "vue-facing-decorator";
import { useClipboard } from "@vueuse/core";
import { Router } from "vue-router";
import ContactNameDialog from "../components/ContactNameDialog.vue";
import QuickNav from "../components/QuickNav.vue";
@@ -156,6 +157,7 @@ interface Invite {
})
export default class InviteOneView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
$router!: Router;
invites: Invite[] = [];
activeDid: string = "";

View File

@@ -6,10 +6,10 @@
<div id="ViewBreadcrumb" class="mb-8">
<h1 class="text-lg text-center font-light relative px-7">
<!-- Back -->
<fa
<font-awesome
icon="chevron-left"
@click="$router.back()"
class="fa-fw text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.back()"
/>
New Activity For You
</h1>
@@ -25,7 +25,7 @@
<span class="text-lg font-medium ml-4"
>New Offer{{ newOffersToUser.length === 1 ? "" : "s" }} To You</span
>
<fa
<font-awesome
v-if="newOffersToUser.length > 0"
:icon="showOffersDetails ? 'chevron-down' : 'chevron-right'"
class="cursor-pointer ml-4 mr-4 text-lg"
@@ -59,12 +59,15 @@
:to="{ path: '/claim/' + encodeURIComponent(offer.jwtId) }"
class="text-blue-500"
>
<fa icon="file-lines" class="pl-2 text-blue-500 cursor-pointer" />
<font-awesome
icon="file-lines"
class="pl-2 text-blue-500 cursor-pointer"
/>
</router-link>
<!-- New line that appears on hover or when the offer is clicked -->
<div
@click="markOffersAsReadStartingWith(offer.jwtId)"
class="absolute left-0 w-full text-left text-gray-500 text-sm hidden group-hover:flex cursor-pointer items-center"
@click="markOffersAsReadStartingWith(offer.jwtId)"
>
<span class="inline-block w-8 h-px bg-gray-500 mr-2" />
Click to keep all above as new offers
@@ -87,7 +90,7 @@
>New Offer{{ newOffersToUserProjects.length === 1 ? "" : "s" }} To
Your Projects</span
>
<fa
<font-awesome
v-if="newOffersToUserProjects.length > 0"
:icon="
showOffersToUserProjectsDetails ? 'chevron-down' : 'chevron-right'
@@ -125,12 +128,15 @@
:to="{ path: '/claim/' + encodeURIComponent(offer.jwtId) }"
class="text-blue-500"
>
<fa icon="file-lines" class="pl-2 text-blue-500 cursor-pointer" />
<font-awesome
icon="file-lines"
class="pl-2 text-blue-500 cursor-pointer"
/>
</router-link>
<!-- New line that appears on hover -->
<div
@click="markOffersToUserProjectsAsReadStartingWith(offer.jwtId)"
class="absolute left-0 w-full text-left text-gray-500 text-sm hidden group-hover:flex cursor-pointer items-center"
@click="markOffersToUserProjectsAsReadStartingWith(offer.jwtId)"
>
<span class="inline-block w-8 h-px bg-gray-500 mr-2" />
Click to keep all above as new offers
@@ -154,13 +160,13 @@ import {
updateAccountSettings,
} from "../db/index";
import { Contact } from "../db/tables/contacts";
import { Router } from "vue-router";
import { OfferSummaryRecord, OfferToPlanSummaryRecord } from "../interfaces";
import {
didInfo,
displayAmount,
getNewOffersToUser,
getNewOffersToUserProjects,
OfferSummaryRecord,
OfferToPlanSummaryRecord,
} from "../libs/endorserServer";
import { retrieveAccountDids } from "../libs/util";
@@ -169,7 +175,7 @@ import { retrieveAccountDids } from "../libs/util";
})
export default class NewActivityView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
$router!: Router;
activeDid = "";
allContacts: Array<Contact> = [];
allMyDids: string[] = [];

View File

@@ -5,20 +5,20 @@
<h1 class="text-lg text-center font-light relative px-7">
<!-- Cancel -->
<button
@click="$router.go(-1)"
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.back()"
>
<fa icon="chevron-left" class="fa-fw"></fa>
<font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
</button>
Edit Identity
</h1>
</div>
<input
v-model="givenName"
type="text"
placeholder="Name"
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
v-model="givenName"
/>
<div class="mt-8">
@@ -54,6 +54,8 @@ import { MASTER_SETTINGS_KEY } from "../db/tables/settings";
components: {},
})
export default class NewEditAccountView extends Vue {
$router!: Router;
givenName = "";
// 'created' hook runs when the Vue instance is first created
@@ -69,11 +71,11 @@ export default class NewEditAccountView extends Vue {
firstName: this.givenName,
lastName: "", // deprecated, pre v 0.1.3
});
(this.$router as Router).back();
this.$router.back();
}
onClickCancel() {
(this.$router as Router).back();
this.$router.back();
}
}
</script>

View File

@@ -8,10 +8,10 @@
<!-- Cancel -->
<!-- Back -->
<button
@click="$router.go(-1)"
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.back()"
>
<fa icon="chevron-left" class="fa-fw"></fa>
<font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
</button>
Edit Project Idea
</h1>
@@ -25,10 +25,10 @@
</div>
<input
v-model="fullClaim.name"
type="text"
placeholder="Idea Name"
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
v-model="fullClaim.name"
/>
<div class="flex justify-center mt-4">
@@ -36,14 +36,14 @@
<a :href="imageUrl" target="_blank" class="text-blue-500 ml-4">
<img :src="imageUrl" class="h-24 rounded-xl" />
</a>
<fa
<font-awesome
icon="trash-can"
@click="confirmDeleteImage"
class="text-red-500 fa-fw ml-8 mt-10"
@click="confirmDeleteImage"
/>
</span>
<span v-else>
<fa
<font-awesome
icon="camera"
class="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-2 rounded-md"
@click="openImageDialog"
@@ -53,27 +53,27 @@
<ImageMethodDialog ref="imageDialog" />
<input
v-model="agentDid"
type="text"
placeholder="Other Authorized Representative"
class="mt-4 block w-full rounded border border-slate-400 px-3 py-2"
v-model="agentDid"
/>
<div class="mb-4">
<p v-if="activeDid != projectIssuerDid && agentDid != projectIssuerDid">
<span class="text-red-500">Beware!</span>
If you save this, the original project owner will no longer be able to
edit it.
<button @click="agentDid = projectIssuerDid" class="text-blue-500">
<button class="text-blue-500" @click="agentDid = projectIssuerDid">
Click here to make the original owner an authorized representative.
</button>
</p>
</div>
<textarea
v-model="fullClaim.description"
placeholder="Description"
class="block w-full rounded border border-slate-400 px-3 py-2"
rows="5"
v-model="fullClaim.description"
maxlength="5000"
></textarea>
<div class="text-xs text-slate-500 italic">
@@ -102,9 +102,9 @@
class="rounded border border-slate-400 px-3 py-2"
/>
<input
v-model="startTimeInput"
:disabled="!startDateInput"
placeholder="Start Time"
v-model="startTimeInput"
type="time"
class="rounded border border-slate-400 ml-2 px-3 py-2"
/>
@@ -127,9 +127,9 @@
class="ml-2 rounded border border-slate-400 px-3 py-2"
/>
<input
v-model="endTimeInput"
:disabled="!endDateInput"
placeholder="End Time"
v-model="endTimeInput"
type="time"
class="rounded border border-slate-400 ml-2 px-3 py-2"
/>
@@ -140,7 +140,7 @@
class="flex items-center mt-4"
@click="includeLocation = !includeLocation"
>
<input type="checkbox" class="mr-2" v-model="includeLocation" />
<input v-model="includeLocation" type="checkbox" class="mr-2" />
<label for="includeLocation">Include Location</label>
</div>
<div v-if="includeLocation" class="mb-4 aspect-video">
@@ -179,9 +179,9 @@
class="items-center mb-4"
>
<div class="flex" @click="sendToTrustroots = !sendToTrustroots">
<input type="checkbox" class="mr-2" v-model="sendToTrustroots" />
<input v-model="sendToTrustroots" type="checkbox" class="mr-2" />
<label>Send to Trustroots</label>
<fa
<font-awesome
icon="circle-info"
class="text-blue-500 ml-2 cursor-pointer"
@click.stop="showNostrPartnerInfo"
@@ -191,7 +191,7 @@
<div class="flex" @click="sendToTripHopping = !sendToTripHopping">
<input type="checkbox" class="mr-2" v-model="sendToTripHopping" />
<label>Send to TripHopping</label>
<fa icon="circle-info" class="text-blue-500 ml-2 cursor-pointer" @click.stop="showNostrPartnerInfo" />
<font-awesome icon="circle-info" class="text-blue-500 ml-2 cursor-pointer" @click.stop="showNostrPartnerInfo" />
</div>
-->
</div>
@@ -229,10 +229,11 @@
import "leaflet/dist/leaflet.css";
import { AxiosError, AxiosRequestHeaders } from "axios";
import { DateTime } from "luxon";
import { finalizeEvent, serializeEvent } from "nostr-tools";
// these core imports could also be included as "import type ..."
import { EventTemplate, UnsignedEvent, VerifiedEvent } from "nostr-tools/core";
import * as nip06 from "nostr-tools/nip06";
import { finalizeEvent } from "nostr-tools/lib/esm/index.js";
import {
accountFromExtendedKey,
extendedKeysFromSeedWords,
} from "nostr-tools/lib/esm/nip06.js";
import { Component, Vue } from "vue-facing-decorator";
import { LMap, LMarker, LTileLayer } from "@vue-leaflet/vue-leaflet";
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
@@ -245,21 +246,30 @@ import {
NotificationIface,
} from "../constants/app";
import { retrieveSettingsForActiveAccount } from "../db/index";
import { PlanVerifiableCredential } from "../interfaces";
import {
createEndorserJwtVcFromClaim,
getHeaders,
PlanVerifiableCredential,
} from "../libs/endorserServer";
import {
retrieveAccountCount,
retrieveFullyDecryptedAccount,
} from "../libs/util";
import {
EventTemplate,
UnsignedEvent,
VerifiedEvent,
} from "nostr-tools/lib/esm/index.js";
import { serializeEvent } from "nostr-tools/lib/esm/index.js";
@Component({
components: { ImageMethodDialog, LMap, LMarker, LTileLayer, QuickNav },
})
export default class NewEditProjectView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
$route!: RouteLocationNormalizedLoaded;
$router!: Router;
errNote(message: string) {
this.$notify(
{ group: "alert", type: "danger", title: "Error", text: message },
@@ -305,8 +315,7 @@ export default class NewEditProjectView extends Vue {
this.apiServer = settings.apiServer || "";
this.showGeneralAdvanced = !!settings.showGeneralAdvanced;
this.projectId =
(this.$route as RouteLocationNormalizedLoaded).query["projectId"] || "";
this.projectId = (this.$route.query["projectId"] as string) || "";
if (this.projectId) {
if (this.numAccounts === 0) {
@@ -591,7 +600,7 @@ export default class NewEditProjectView extends Vue {
}
}
(this.$router as Router).push({ path: "/project/" + projectPath });
this.$router.push({ path: "/project/" + projectPath });
} else {
console.error(
"Got unexpected 'data' inside response from server",
@@ -668,7 +677,7 @@ export default class NewEditProjectView extends Vue {
// remove any trailing '
const finalDerNumNoApostrophe = finalDerNum?.replace(/'/g, "");
const accountNum = Number(finalDerNumNoApostrophe || 0);
const extPubPri = nip06.extendedKeysFromSeedWords(
const extPubPri = extendedKeysFromSeedWords(
account?.mnemonic as string,
"",
accountNum,
@@ -676,7 +685,7 @@ export default class NewEditProjectView extends Vue {
const publicExtendedKey: string = extPubPri?.publicExtendedKey;
const privateExtendedKey = extPubPri?.privateExtendedKey;
const privateBytes: Uint8Array =
nip06.accountFromExtendedKey(privateExtendedKey).privateKey;
accountFromExtendedKey(privateExtendedKey).privateKey;
// No real content is necessary, we just want something signed,
// so we might as well use nostr libs for nostr functions.
// Besides: someday we may create real content that we can relay.
@@ -710,8 +719,7 @@ export default class NewEditProjectView extends Vue {
const endorserPartnerUrl = partnerServer + "/api/partner/link";
const timeSafariUrl = window.location.origin + "/claim/" + jwtId;
const content = this.fullClaim.name + " - see " + timeSafariUrl;
const publicKeyHex =
nip06.accountFromExtendedKey(publicExtendedKey).publicKey;
const publicKeyHex = accountFromExtendedKey(publicExtendedKey).publicKey;
const unsignedPayload: UnsignedEvent = {
// why doesn't "...signedPayload" work?
kind: signedPayload.kind,
@@ -810,7 +818,7 @@ export default class NewEditProjectView extends Vue {
}
public onCancelClick() {
(this.$router as Router).back();
this.$router.back();
}
public showNostrPartnerInfo() {

View File

@@ -11,7 +11,7 @@
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.back()"
>
<fa icon="chevron-left" class="fa-fw"></fa>
<font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
</h1>
</div>
@@ -25,16 +25,16 @@
<div />
<div v-if="loading">
<span class="text-xl">Creating...&nbsp;</span>
<fa
<font-awesome
icon="spinner"
class="fa-spin fa-spin-pulse"
color="green"
size="128"
></fa>
></font-awesome>
</div>
<div v-else>
<span class="text-xl">Created!</span>
<fa
<font-awesome
icon="burst"
class="fa-beat px-12"
color="green"
@@ -44,7 +44,7 @@
--fa-animation-iteration-count: 1;
--fa-beat-scale: 6;
"
></fa>
></font-awesome>
</div>
<div />
</div>
@@ -62,12 +62,13 @@ import QuickNav from "../components/QuickNav.vue";
@Component({ components: { QuickNav } })
export default class NewIdentifierView extends Vue {
loading = true;
$router!: Router;
async mounted() {
await generateSaveAndActivateIdentity();
this.loading = false;
setTimeout(() => {
(this.$router as Router).push({ name: "home" });
this.$router.push({ name: "home" });
}, 1000);
}
}

View File

@@ -13,7 +13,7 @@
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="cancelBack()"
>
<fa icon="chevron-left" class="fa-fw"></fa>
<font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
</h1>
</div>
@@ -33,9 +33,9 @@
>
</h1>
<textarea
v-model="descriptionOfItem"
class="block w-full rounded border border-slate-400 mb-2 px-3 py-2"
placeholder="What is offered"
v-model="descriptionOfItem"
data-testId="itemDescription"
/>
<div class="flex flex-row justify-center">
@@ -49,19 +49,19 @@
class="border border-r-0 border-slate-400 bg-slate-200 px-4 py-2"
@click="amountInput === '0' ? null : decrement()"
>
<fa icon="chevron-left" />
<font-awesome icon="chevron-left" />
</div>
<input
v-model="amountInput"
type="number"
class="border border-r-0 border-slate-400 px-2 py-2 text-center w-20"
v-model="amountInput"
data-testId="inputOfferAmount"
/>
<div
class="rounded-r border border-slate-400 bg-slate-200 px-4 py-2"
@click="increment()"
>
<fa icon="chevron-right" />
<font-awesome icon="chevron-right" />
</div>
</div>
@@ -72,9 +72,9 @@
Conditions
</span>
<textarea
v-model="descriptionOfCondition"
class="w-full border border-slate-400 px-3 py-2 rounded-r"
placeholder="Prerequisites, other people to include, etc."
v-model="descriptionOfCondition"
/>
</div>
@@ -94,11 +94,11 @@
<div class="h-7 mt-4 flex">
<input
v-if="projectId && !offeredToRecipient"
v-model="offeredToProject"
type="checkbox"
class="h-6 w-6 mr-2"
v-model="offeredToProject"
/>
<fa
<font-awesome
v-else
icon="square"
class="bg-slate-500 text-slate-500 h-5 w-5 px-0.5 py-0.5 mr-2 rounded"
@@ -116,11 +116,11 @@
<div class="h-7 mt-4 flex">
<input
v-if="recipientDid && !offeredToProject"
v-model="offeredToRecipient"
type="checkbox"
class="h-6 w-6 mr-2"
v-model="offeredToRecipient"
/>
<fa
<font-awesome
v-else
icon="square"
class="bg-slate-500 text-slate-500 h-5 w-5 px-0.5 py-0.5 mr-2 rounded"
@@ -151,7 +151,7 @@
<p class="text-center mb-2 mt-6 italic">
Sign & Send to publish to the world
<fa
<font-awesome
icon="circle-info"
class="pl-2 text-blue-500 cursor-pointer"
@click="explainData()"
@@ -176,20 +176,19 @@
<script lang="ts">
import { Component, Vue } from "vue-facing-decorator";
import { Router } from "vue-router";
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
import QuickNav from "../components/QuickNav.vue";
import TopMessage from "../components/TopMessage.vue";
import { NotificationIface } from "../constants/app";
import { db, retrieveSettingsForActiveAccount } from "../db/index";
import { GenericCredWrapper, OfferVerifiableCredential } from "../interfaces";
import {
createAndSubmitOffer,
didInfo,
editAndSubmitOffer,
GenericCredWrapper,
getPlanFromCache,
hydrateOffer,
OfferVerifiableCredential,
} from "../libs/endorserServer";
import * as libsUtil from "../libs/util";
import { retrieveAccountDids } from "../libs/util";
@@ -202,6 +201,8 @@ import { retrieveAccountDids } from "../libs/util";
})
export default class OfferDetailsView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
$route!: RouteLocationNormalizedLoaded;
$router!: Router;
activeDid = "";
apiServer = "";
@@ -229,9 +230,9 @@ export default class OfferDetailsView extends Vue {
async mounted() {
try {
this.prevCredToEdit = (this.$route as Router).query["prevCredToEdit"]
this.prevCredToEdit = (this.$route.query["prevCredToEdit"] as string)
? (JSON.parse(
(this.$route as Router).query["prevCredToEdit"],
this.$route.query["prevCredToEdit"] as string,
) as GenericCredWrapper<OfferVerifiableCredential>)
: undefined;
} catch (error) {
@@ -249,28 +250,28 @@ export default class OfferDetailsView extends Vue {
const prevAmount =
this.prevCredToEdit?.claim?.includesObject?.amountOfThisGood;
this.amountInput =
(this.$route as Router).query["amountInput"] ||
(this.$route.query["amountInput"] as string) ||
(prevAmount ? String(prevAmount) : "") ||
this.amountInput;
this.unitCode = ((this.$route as Router).query["unitCode"] ||
this.unitCode = ((this.$route.query["unitCode"] as string) ||
this.prevCredToEdit?.claim?.includesObject?.unitCode ||
this.unitCode) as string;
this.descriptionOfCondition =
this.prevCredToEdit?.claim?.description || this.descriptionOfCondition;
this.descriptionOfItem =
(this.$route as Router).query["description"] ||
(this.$route.query["description"] as string) ||
this.prevCredToEdit?.claim?.itemOffered?.description ||
this.descriptionOfItem;
this.destinationPathAfter = (this.$route as Router).query[
"destinationPathAfter"
];
this.offererDid = ((this.$route as Router).query["offererDid"] ||
this.prevCredToEdit?.claim?.agent?.identifier ||
this.destinationPathAfter =
(this.$route.query["destinationPathAfter"] as string) || "";
this.offererDid = ((this.$route.query["offererDid"] as string) ||
(this.prevCredToEdit?.claim?.agent as unknown as { identifier: string })
?.identifier ||
this.offererDid) as string;
this.hideBackButton =
(this.$route as Router).query["hideBackButton"] === "true";
this.message = ((this.$route as Router).query["message"] as string) || "";
(this.$route.query["hideBackButton"] as string) === "true";
this.message = (this.$route.query["message"] as string) || "";
// find any project ID
let project;
@@ -280,17 +281,16 @@ export default class OfferDetailsView extends Vue {
) {
project = this.prevCredToEdit?.claim?.itemOffered?.isPartOf;
}
this.projectId = ((this.$route as Router).query["projectId"] ||
this.projectId = ((this.$route.query["projectId"] as string) ||
project?.identifier ||
this.projectId) as string;
this.projectName = ((this.$route as Router).query["projectName"] ||
this.projectName = ((this.$route.query["projectName"] as string) ||
project?.name ||
this.projectName) as string;
this.recipientDid = ((this.$route as Router).query["recipientDid"] ||
this.recipientDid = ((this.$route.query["recipientDid"] as string) ||
this.prevCredToEdit?.claim?.recipient?.identifier) as string;
this.recipientName =
((this.$route as Router).query["recipientName"] as string) || "";
this.recipientName = (this.$route.query["recipientName"] as string) || "";
this.validThroughDateInput =
this.prevCredToEdit?.claim?.validThrough || this.validThroughDateInput;

View File

@@ -10,7 +10,7 @@
<!-- Loading State -->
<div v-if="isLoading" class="flex justify-center items-center py-8">
<fa icon="spinner" class="fa-spin-pulse" />
<font-awesome icon="spinner" class="fa-spin-pulse" />
</div>
<div v-else-if="attendingMeeting">
@@ -22,11 +22,11 @@
<div class="flex justify-between items-center">
<h2 class="text-xl font-medium">{{ attendingMeeting.name }}</h2>
<button
@click.stop="leaveMeeting"
class="text-red-600 hover:text-red-700 p-2"
title="Leave Meeting"
@click.stop="leaveMeeting"
>
<fa icon="right-from-bracket" />
<font-awesome icon="right-from-bracket" />
</button>
</div>
</div>
@@ -65,14 +65,14 @@
/>
<div class="flex justify-end space-x-4">
<button
@click="cancelPasswordDialog"
class="px-4 py-2 bg-gray-200 rounded hover:bg-gray-300"
@click="cancelPasswordDialog"
>
Cancel
</button>
<button
@click="submitPassword"
class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
@click="submitPassword"
>
Submit
</button>
@@ -120,6 +120,7 @@ export default class OnboardMeetingListView extends Vue {
},
timeout?: number,
) => void;
$router!: Router;
activeDid = "";
apiServer = "";
@@ -257,7 +258,7 @@ export default class OnboardMeetingListView extends Vue {
if (postResult.data && postResult.data.success) {
// Navigate to members view with password and groupId
(this.$router as Router).push({
this.$router.push({
name: "onboard-meeting-members",
params: {
groupId: this.selectedMeeting.groupId.toString(),

View File

@@ -10,10 +10,10 @@
<!-- Loading Animation -->
<div
class="mt-16 text-center text-4xl bg-slate-400 text-white w-14 py-2.5 rounded-full mx-auto"
v-if="isLoading"
class="mt-16 text-center text-4xl bg-slate-400 text-white w-14 py-2.5 rounded-full mx-auto"
>
<fa icon="spinner" class="fa-spin-pulse"></fa>
<font-awesome icon="spinner" class="fa-spin-pulse"></font-awesome>
</div>
<!-- Error State -->
@@ -39,7 +39,7 @@
<script lang="ts">
import { Component, Vue } from "vue-facing-decorator";
import { RouteLocation } from "vue-router";
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
import QuickNav from "../components/QuickNav.vue";
import TopMessage from "../components/TopMessage.vue";
@@ -69,17 +69,17 @@ export default class OnboardMeetingMembersView extends Vue {
firstName = "";
isRegistered = false;
isLoading = true;
$route!: RouteLocationNormalizedLoaded;
$router!: Router;
$refs!: {
userNameDialog: InstanceType<typeof UserNameDialog>;
};
userNameDialog!: InstanceType<typeof UserNameDialog>;
get groupId(): string {
return (this.$route as RouteLocation).params.groupId as string;
return (this.$route.params.groupId as string) || "";
}
get password(): string {
return (this.$route as RouteLocation).query.password as string;
return (this.$route.query.password as string) || "";
}
async created() {

View File

@@ -17,24 +17,24 @@
<div class="flex items-center">
<h2 class="text-2xl">Current Meeting</h2>
<button
@click="startEditing"
class="mb-4 text-blue-600 hover:text-blue-800 transition-colors duration-200 ml-2"
title="Edit Meeting"
@click="startEditing"
>
<fa icon="pen" class="fa-fw" />
<font-awesome icon="pen" class="fa-fw" />
<span class="sr-only">{{
isInCreateMode() ? "Create Meeting" : "Edit Meeting"
}}</span>
</button>
</div>
<button
@click="confirmDelete"
class="text-red-600 hover:text-red-800 transition-colors duration-200"
:disabled="isDeleting"
:class="{ 'opacity-50 cursor-not-allowed': isDeleting }"
title="Delete Meeting"
@click="confirmDelete"
>
<fa icon="trash-can" class="fa-fw" />
<font-awesome icon="trash-can" class="fa-fw" />
<span class="sr-only">{{
isDeleting ? "Deleting..." : "Delete Meeting"
}}</span>
@@ -72,14 +72,14 @@
</p>
<div class="flex justify-between space-x-4">
<button
@click="showDeleteConfirm = false"
class="px-4 py-2 bg-slate-500 text-white rounded hover:bg-slate-700"
@click="showDeleteConfirm = false"
>
Cancel
</button>
<button
@click="deleteMeeting"
class="px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700"
@click="deleteMeeting"
>
Delete
</button>
@@ -101,8 +101,8 @@
</h2>
<!-- This is my first form. Not sure if I like it; will see if the browser benefits extend to the native app. -->
<form
@submit.prevent="isInCreateMode() ? createMeeting() : updateMeeting()"
class="space-y-4"
@submit.prevent="isInCreateMode() ? createMeeting() : updateMeeting()"
>
<div>
<label
@@ -182,8 +182,8 @@
<button
v-if="isInEditOrCreateMode()"
type="button"
@click="cancelEditing"
class="w-full bg-slate-500 text-white px-4 py-2 rounded-md hover:bg-slate-600"
@click="cancelEditing"
>
Cancel
</button>
@@ -204,20 +204,21 @@
class="inline-block text-blue-600"
target="_blank"
>
&bull; Open shortcut page for members <fa icon="external-link" />
&bull; Open shortcut page for members
<font-awesome icon="external-link" />
</router-link>
<MembersList
:password="currentMeeting.password || ''"
:show-organizer-tools="true"
@error="handleMembersError"
class="mt-4"
@error="handleMembersError"
/>
</div>
<div v-else-if="isLoading">
<div class="flex justify-center items-center h-full">
<fa icon="spinner" class="fa-spin-pulse" />
<font-awesome icon="spinner" class="fa-spin-pulse" />
</div>
</div>
</section>

View File

@@ -10,10 +10,10 @@
<h1 class="text-center text-lg font-light relative px-7">
<!-- Back -->
<button
@click="$router.go(-1)"
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.back()"
>
<fa icon="chevron-left" class="fa-fw"></fa>
<font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
</button>
Project Idea
</h1>
@@ -21,11 +21,11 @@
{{ name }}
<button
v-if="activeDid === issuer || activeDid === agentDid"
@click="onEditClick()"
title="Edit"
data-testId="editClaimButton"
@click="onEditClick()"
>
<fa icon="pen" class="text-sm text-blue-500 ml-2 mb-1" />
<font-awesome icon="pen" class="text-sm text-blue-500 ml-2 mb-1" />
</button>
</h2>
</div>
@@ -37,10 +37,10 @@
<div class="pb-4 flex gap-4">
<div class="pt-1">
<ProjectIcon
:entityId="projectId"
:iconSize="64"
:imageUrl="imageUrl"
:linkToFull="true"
:entity-id="projectId"
:icon-size="64"
:image-url="imageUrl"
:link-to-full="true"
class="block border border-slate-300 rounded-md max-h-16 max-w-16"
/>
</div>
@@ -48,7 +48,10 @@
<div class="overflow-hidden">
<div class="text-sm mb-3">
<div class="truncate">
<fa icon="user" class="fa-fw text-slate-400"></fa>
<font-awesome
icon="user"
class="fa-fw text-slate-400"
></font-awesome>
{{ issuerInfoObject?.displayName }}
<span v-if="!serverUtil.isEmptyOrHiddenDid(issuer)">
<a
@@ -56,11 +59,14 @@
target="_blank"
class="text-blue-500"
>
<fa icon="arrow-up-right-from-square" class="fa-fw" />
<font-awesome
icon="arrow-up-right-from-square"
class="fa-fw"
/>
</a>
</span>
<span v-else-if="serverUtil.isHiddenDid(issuer)">
<fa
<font-awesome
icon="info-circle"
class="fa-fw text-blue-500 cursor-pointer"
@click="openHiddenDidDialog()"
@@ -68,35 +74,50 @@
</span>
</div>
<div v-if="startTime">
<fa icon="calendar" class="fa-fw text-slate-400"></fa>
<font-awesome
icon="calendar"
class="fa-fw text-slate-400"
></font-awesome>
Starts {{ startTime }}
</div>
<div v-if="endTime">
<fa icon="calendar" class="fa-fw text-slate-400"></fa>
<font-awesome
icon="calendar"
class="fa-fw text-slate-400"
></font-awesome>
Ends {{ endTime }}
</div>
<div v-if="latitude || longitude">
<fa icon="location-dot" class="fa-fw text-slate-400"></fa>
<font-awesome
icon="location-dot"
class="fa-fw text-slate-400"
></font-awesome>
<a
:href="getOpenStreetMapUrl()"
target="_blank"
class="underline text-blue-500"
>Map View
<fa
<font-awesome
icon="arrow-up-right-from-square"
class="fa-fw text-blue-500"
/>
</a>
</div>
<div v-if="url">
<fa icon="globe" class="fa-fw text-slate-400"></fa>
<font-awesome
icon="globe"
class="fa-fw text-slate-400"
></font-awesome>
<a
:href="addScheme(url)"
target="_blank"
class="underline text-blue-500"
>
{{ domainForWebsite(this.url) }}
<fa icon="arrow-up-right-from-square" class="fa-fw" />
{{ domainForWebsite(url) }}
<font-awesome
icon="arrow-up-right-from-square"
class="fa-fw"
/>
</a>
</div>
</div>
@@ -108,23 +129,23 @@
{{ truncatedDesc }}
<a
v-if="description.length >= truncateLength"
@click="expandText"
class="uppercase text-xs font-semibold text-slate-700"
@click="expandText"
>... Read More</a
>
</div>
<div v-else>
{{ description }}
<a
@click="collapseText"
class="uppercase text-xs font-semibold text-slate-700"
@click="collapseText"
>- Read Less</a
>
</div>
</div>
<a @click="onClickLoadClaim(projectId)" class="cursor-pointer">
<fa icon="file-lines" class="pl-2 pt-1 text-blue-500" />
<a class="cursor-pointer" @click="onClickLoadClaim(projectId)">
<font-awesome icon="file-lines" class="pl-2 pt-1 text-blue-500" />
</a>
</div>
</div>
@@ -142,8 +163,8 @@
<div class="text-center">
<div v-for="plan in fulfillersToThis" :key="plan.handleId">
<button
@click="onClickLoadProject(plan.handleId)"
class="text-blue-500"
@click="onClickLoadProject(plan.handleId)"
>
{{ plan.name }}
</button>
@@ -163,8 +184,8 @@
<!-- centering because long, wrapped project names didn't left align with blank or "text-left" -->
<div class="text-center">
<button
@click="onClickLoadProject(fulfilledByThis.handleId)"
class="text-blue-500"
@click="onClickLoadProject(fulfilledByThis.handleId)"
>
{{ fulfilledByThis.name }}
</button>
@@ -181,7 +202,10 @@
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 })">
<fa icon="hand" class="fa-fw text-blue-500 text-5xl cursor-pointer" />
<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"
>
@@ -206,7 +230,7 @@
>
<EntityIcon
:contact="contact"
:iconSize="64"
:icon-size="64"
class="mx-auto border border-blue-300 rounded-md mb-1 cursor-pointer"
/>
<h3
@@ -218,14 +242,14 @@
<li>
<span
v-if="allContacts.length >= 5"
@click="onClickAllContactsGifting()"
class="flex align-bottom text-xs text-blue-500 mt-12 cursor-pointer"
@click="onClickAllContactsGifting()"
>
... or someone else...
</span>
</li>
</ul>
<GiftedDialog ref="giveDialogToThis" :toProjectId="this.projectId" />
<GiftedDialog ref="giveDialogToThis" :to-project-id="projectId" />
</div>
<!-- Offers & Gifts to & from this -->
@@ -236,8 +260,8 @@
<div class="text-center">
<button
data-testId="offerButton"
@click="openOfferDialog()"
class="block w-full 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 rounded-md"
@click="openOfferDialog()"
>
Offer to this (maybe with conditions)...
</button>
@@ -245,15 +269,15 @@
</div>
<OfferDialog
ref="customOfferDialog"
:projectId="this.projectId"
:projectName="this.name"
:project-id="projectId"
:project-name="name"
/>
<h3 class="text-lg font-bold mb-3 mt-4">Offered To This Idea</h3>
<div v-if="offersToThis.length === 0">
(None yet. Wanna
<span @click="openOfferDialog()" class="cursor-pointer text-blue-500"
<span class="cursor-pointer text-blue-500" @click="openOfferDialog()"
>offer something... especially if others join you</span
>?)
</div>
@@ -266,7 +290,10 @@
>
<div class="flex justify-between gap-4">
<span>
<fa icon="user" class="fa-fw text-slate-400"></fa>
<font-awesome
icon="user"
class="fa-fw text-slate-400"
></font-awesome>
{{
serverUtil.didInfo(
offer.offeredByDid,
@@ -277,28 +304,31 @@
}}
</span>
<span v-if="offer.amount" class="whitespace-nowrap">
<fa
<font-awesome
:icon="libsUtil.iconForUnitCode(offer.unit)"
class="fa-fw text-slate-400"
/>{{ offer.amount }}
</span>
</div>
<div v-if="offer.objectDescription" class="text-slate-500">
<fa icon="comment" class="fa-fw text-slate-400" />
<font-awesome icon="comment" class="fa-fw text-slate-400" />
{{ offer.objectDescription }}
</div>
<div class="flex justify-between">
<a
@click="onClickLoadClaim(offer.jwtId as string)"
class="cursor-pointer"
@click="onClickLoadClaim(offer.jwtId as string)"
>
<fa icon="file-lines" class="pl-2 pt-1 text-blue-500" />
<font-awesome
icon="file-lines"
class="pl-2 pt-1 text-blue-500"
/>
</a>
<a
v-if="checkIsFulfillable(offer)"
@click="onClickFulfillGiveToOffer(offer)"
>
<fa
<font-awesome
icon="hand-holding-heart"
class="text-blue-500 cursor-pointer"
/>
@@ -317,8 +347,8 @@
<div v-if="activeDid && isRegistered">
<div class="text-center">
<button
@click="openGiftDialogToProject()"
class="block w-full 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-1rounded-md"
@click="openGiftDialogToProject()"
>
Given To This...
</button>
@@ -340,7 +370,7 @@
>
<div class="flex justify-between gap-4">
<span>
<fa icon="user" class="fa-fw text-slate-400" />
<font-awesome icon="user" class="fa-fw text-slate-400" />
{{
serverUtil.didInfo(
give.agentDid,
@@ -351,23 +381,26 @@
}}
</span>
<span v-if="give.amount" class="whitespace-nowrap">
<fa
<font-awesome
:icon="libsUtil.iconForUnitCode(give.unit)"
class="fa-fw text-slate-400"
/>{{ give.amount }}
</span>
</div>
<div class="text-slate-500">
<fa icon="calendar" class="fa-fw text-slate-400" />
<font-awesome icon="calendar" class="fa-fw text-slate-400" />
{{ give.issuedAt?.substring(0, 10) }}
</div>
<div v-if="give.description" class="text-slate-500">
<fa icon="comment" class="fa-fw text-slate-400" />
<font-awesome icon="comment" class="fa-fw text-slate-400" />
{{ give.description }}
</div>
<div class="flex justify-between">
<a @click="onClickLoadClaim(give.jwtId)">
<fa icon="file-lines" class="text-blue-500 cursor-pointer" />
<font-awesome
icon="file-lines"
class="text-blue-500 cursor-pointer"
/>
</a>
<a
@@ -377,13 +410,19 @@
"
@click="deepCheckConfirmable(give)"
>
<fa icon="circle-check" class="text-blue-500 cursor-pointer" />
<font-awesome
icon="circle-check"
class="text-blue-500 cursor-pointer"
/>
</a>
<a v-else-if="checkingConfirmationForJwtId === give.jwtId">
<fa icon="spinner" class="fa-spin-pulse" />
<font-awesome icon="spinner" class="fa-spin-pulse" />
</a>
<a v-else @click="shallowNotifyWhyCannotConfirm(give)">
<fa icon="circle-check" class="text-slate-500 cursor-pointer" />
<font-awesome
icon="circle-check"
class="text-slate-500 cursor-pointer"
/>
</a>
</div>
<div v-if="give.fullClaim.image" class="flex justify-center">
@@ -404,17 +443,14 @@
<div v-if="activeDid && isRegistered">
<div class="text-center">
<button
@click="openGiftDialogFromProject()"
class="block w-full 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 rounded-md"
@click="openGiftDialogFromProject()"
>
Given By This...
</button>
</div>
</div>
<GiftedDialog
ref="giveDialogFromThis"
:fromProjectId="this.projectId"
/>
<GiftedDialog ref="giveDialogFromThis" :from-project-id="projectId" />
<h3 class="text-lg font-bold mb-3 mt-4">
Benefitted From This Project
@@ -440,23 +476,26 @@
}}
</span>
<span v-if="give.amount" class="whitespace-nowrap">
<fa
<font-awesome
:icon="libsUtil.iconForUnitCode(give.unit)"
class="fa-fw text-slate-400"
/>{{ give.amount }}
</span>
</div>
<div class="text-slate-500">
<fa icon="calendar" class="fa-fw text-slate-400" />
<font-awesome icon="calendar" class="fa-fw text-slate-400" />
{{ give.issuedAt?.substring(0, 10) }}
</div>
<div v-if="give.description" class="text-slate-500">
<fa icon="comment" class="fa-fw text-slate-400" />
<font-awesome icon="comment" class="fa-fw text-slate-400" />
{{ give.description }}
</div>
<div class="flex justify-between">
<a @click="onClickLoadClaim(give.jwtId)">
<fa icon="file-lines" class="text-blue-500 cursor-pointer" />
<font-awesome
icon="file-lines"
class="text-blue-500 cursor-pointer"
/>
</a>
<a
@@ -466,13 +505,19 @@
"
@click="deepCheckConfirmable(give)"
>
<fa icon="circle-check" class="text-blue-500 cursor-pointer" />
<font-awesome
icon="circle-check"
class="text-blue-500 cursor-pointer"
/>
</a>
<a v-else-if="checkingConfirmationForJwtId === give.jwtId">
<fa icon="spinner" class="fa-spin-pulse" />
<font-awesome icon="spinner" class="fa-spin-pulse" />
</a>
<a v-else @click="shallowNotifyWhyCannotConfirm(give)">
<fa icon="circle-check" class="text-slate-500 cursor-pointer" />
<font-awesome
icon="circle-check"
class="text-slate-500 cursor-pointer"
/>
</a>
</div>
<div v-if="give.fullClaim.image" class="flex justify-center">
@@ -496,7 +541,15 @@
import { AxiosError } from "axios";
import { Component, Vue } from "vue-facing-decorator";
import { Router } from "vue-router";
import {
GenericVerifiableCredential,
GenericCredWrapper,
GiveSummaryRecord,
GiveVerifiableCredential,
OfferSummaryRecord,
OfferVerifiableCredential,
PlanSummaryRecord,
} from "../interfaces";
import GiftedDialog from "../components/GiftedDialog.vue";
import OfferDialog from "../components/OfferDialog.vue";
import TopMessage from "../components/TopMessage.vue";
@@ -511,18 +564,41 @@ import {
} from "../db/index";
import { Contact } from "../db/tables/contacts";
import * as libsUtil from "../libs/util";
import {
GenericCredWrapper,
GiveSummaryRecord,
GiveVerifiableCredential,
OfferSummaryRecord,
OfferVerifiableCredential,
PlanSummaryRecord,
} from "../libs/endorserServer";
import * as serverUtil from "../libs/endorserServer";
import { retrieveAccountDids } from "../libs/util";
import HiddenDidDialog from "../components/HiddenDidDialog.vue";
/**
* Project View Component
* @author Matthew Raymer
*
* This component displays detailed project information and manages interactions including:
* - Project metadata (name, description, dates, location)
* - Issuer information and verification
* - Project contributions and fulfillments
* - Offers and gifts tracking
* - Contact interactions
*
* Data Flow:
* 1. Component loads with project ID from route
* 2. Fetches project data, contacts, and account settings
* 3. Loads related data (offers, gifts, fulfillments)
* 4. Updates UI with paginated results
*
* Security Features:
* - DID visibility controls
* - JWT validation for imports
* - Permission checks for actions
*
* State Management:
* - Maintains separate loading states for different data types
* - Handles pagination limits
* - Tracks confirmation states
*
* @see GiftedDialog for gift creation
* @see OfferDialog for offer creation
* @see HiddenDidDialog for DID privacy explanations
*/
@Component({
components: {
EntityIcon,
@@ -535,49 +611,103 @@ import HiddenDidDialog from "../components/HiddenDidDialog.vue";
},
})
export default class ProjectViewView extends Vue {
/** Notification function injected by Vue */
$notify!: (notification: NotificationIface, timeout?: number) => void;
/** Router instance for navigation */
$router!: Router;
// Account and Settings State
/** Currently active DID */
activeDid = "";
/** Project agent DID */
agentDid = "";
/** DIDs that can see the agent DID */
agentDidVisibleToDids: Array<string> = [];
/** All DIDs associated with current account */
allMyDids: Array<string> = [];
/** All known contacts */
allContacts: Array<Contact> = [];
/** API server endpoint */
apiServer = "";
checkingConfirmationForJwtId = "";
description = "";
endTime = "";
expanded = false;
fulfilledByThis: PlanSummaryRecord | null = null;
fulfillersToThis: Array<PlanSummaryRecord> = [];
fulfillersToHitLimit = false;
givesToThis: Array<GiveSummaryRecord> = [];
givesHitLimit = false;
givesProvidedByThis: Array<GiveSummaryRecord> = [];
givesProvidedByHitLimit = false;
imageUrl = "";
/** Registration status of current user */
isRegistered = false;
// Project Data
/** Project description */
description = "";
/** Project end time */
endTime = "";
/** Text expansion state */
expanded = false;
/** Project fulfilled by this project */
fulfilledByThis: PlanSummaryRecord | null = null;
/** Projects fulfilling this project */
fulfillersToThis: Array<PlanSummaryRecord> = [];
/** Flag for fulfiller pagination */
fulfillersToHitLimit = false;
/** Project image URL */
imageUrl = "";
/** Project issuer DID */
issuer = "";
/** Cached issuer information */
issuerInfoObject: {
known: boolean;
displayName: string;
profileImageUrl?: string;
} | null = null;
/** DIDs that can see issuer information */
issuerVisibleToDids: Array<string> = [];
/** Project location data */
latitude = 0;
longitude = 0;
/** Project name */
name = "";
offersToThis: Array<OfferSummaryRecord> = [];
offersHitLimit = false;
projectId = ""; // handle ID
recentlyCheckedAndUnconfirmableJwts: string[] = [];
/** Project ID (handle) */
projectId = "";
/** Project start time */
startTime = "";
truncatedDesc = "";
truncateLength = 40;
/** Project URL */
url = "";
// Interaction Data
/** Gifts to this project */
givesToThis: Array<GiveSummaryRecord> = [];
/** Flag for gifts pagination */
givesHitLimit = false;
/** Gifts from this project */
givesProvidedByThis: Array<GiveSummaryRecord> = [];
/** Flag for provided gifts pagination */
givesProvidedByHitLimit = false;
/** Offers to this project */
offersToThis: Array<OfferSummaryRecord> = [];
/** Flag for offers pagination */
offersHitLimit = false;
// UI State
/** JWT being checked for confirmation */
checkingConfirmationForJwtId = "";
/** Recently checked unconfirmable JWTs */
recentlyCheckedAndUnconfirmableJwts: string[] = [];
truncatedDesc = "";
/** Truncation length */
truncateLength = 40;
// Utility References
libsUtil = libsUtil;
serverUtil = serverUtil;
/**
* Component lifecycle hook that initializes the project view
*
* Workflow:
* 1. Loads account settings and contacts
* 2. Retrieves all account DIDs
* 3. Extracts project ID from URL
* 4. Initializes project data loading
*
* @throws Logs errors but continues loading
* @emits Notification on profile loading errors
*/
async created() {
const settings = await retrieveSettingsForActiveAccount();
this.activeDid = settings.activeDid || "";
@@ -611,23 +741,19 @@ export default class ProjectViewView extends Vue {
this.loadProject(this.projectId, this.activeDid);
}
onEditClick() {
const route = {
name: "new-edit-project",
query: { projectId: this.projectId },
};
(this.$router as Router).push(route);
}
// Isn't there a better way to make this available to the template?
expandText() {
this.expanded = true;
}
collapseText() {
this.expanded = false;
}
/**
* Loads project data and related information
*
* Workflow:
* 1. Fetches project details from API
* 2. Updates component state with project data
* 3. Initializes related data loading (gifts, offers, fulfillments)
*
* @param projectId Project handle ID
* @param userDid Active user's DID
* @throws Logs errors and notifies user
* @emits Notification on loading errors
*/
async loadProject(projectId: string, userDid: string) {
this.projectId = projectId;
@@ -714,6 +840,15 @@ export default class ProjectViewView extends Vue {
this.loadPlanFulfilledBy();
}
/**
* Loads gifts made to this project
*
* Handles pagination and updates component state with results.
* Uses beforeId for pagination based on last loaded gift.
*
* @throws Logs errors and notifies user
* @emits Notification on loading errors
*/
async loadGives() {
const givesUrl =
this.apiServer +
@@ -761,6 +896,15 @@ export default class ProjectViewView extends Vue {
}
}
/**
* Loads gifts provided by this project
*
* Similar to loadGives but for outgoing gifts.
* Maintains separate pagination state.
*
* @throws Logs errors and notifies user
* @emits Notification on loading errors
*/
async loadGivesProvidedBy() {
const providedByUrl =
this.apiServer +
@@ -811,6 +955,15 @@ export default class ProjectViewView extends Vue {
}
}
/**
* Loads offers made to this project
*
* Handles pagination and filtering of valid offers.
* Updates component state with results.
*
* @throws Logs errors and notifies user
* @emits Notification on loading errors
*/
async loadOffers() {
const offersUrl =
this.apiServer +
@@ -858,6 +1011,14 @@ export default class ProjectViewView extends Vue {
}
}
/**
* Loads projects that fulfill this project
*
* Manages pagination state and updates component with results.
*
* @throws Logs errors and notifies user
* @emits Notification on loading errors
*/
async loadPlanFulfillersTo() {
const fulfillsUrl =
this.apiServer +
@@ -906,6 +1067,14 @@ export default class ProjectViewView extends Vue {
}
}
/**
* Loads project that this project fulfills
*
* Updates fulfilledByThis state with result.
*
* @throws Logs errors and notifies user
* @emits Notification on loading errors
*/
async loadPlanFulfilledBy() {
const fulfilledByUrl =
this.apiServer +
@@ -945,6 +1114,23 @@ export default class ProjectViewView extends Vue {
}
}
onEditClick() {
const route = {
name: "new-edit-project",
query: { projectId: this.projectId },
};
this.$router.push(route);
}
// Isn't there a better way to make this available to the template?
expandText() {
this.expanded = true;
}
collapseText() {
this.expanded = false;
}
/**
* Handle clicking on a project entry found in the list
* @param id of the project
@@ -953,7 +1139,7 @@ export default class ProjectViewView extends Vue {
const route = {
path: "/project/" + encodeURIComponent(projectId),
};
(this.$router as Router).push(route);
this.$router.push(route);
this.loadProject(projectId, this.activeDid);
}
@@ -1000,14 +1186,14 @@ export default class ProjectViewView extends Vue {
projectId: this.projectId,
},
};
(this.$router as Router).push(route);
this.$router.push(route);
}
onClickLoadClaim(jwtId: string) {
const route = {
path: "/claim/" + encodeURIComponent(jwtId),
};
(this.$router as Router).push(route);
this.$router.push(route);
}
checkIsFulfillable(offer: OfferSummaryRecord) {
@@ -1156,7 +1342,7 @@ export default class ProjectViewView extends Vue {
),
),
);
const confirmationClaim: serverUtil.GenericVerifiableCredential = {
const confirmationClaim: GenericVerifiableCredential = {
"@context": "https://schema.org",
"@type": "AgreeAction",
object: goodClaim,

View File

@@ -16,6 +16,7 @@
<li>
<a
href="#"
:class="computedOfferTabClassNames()"
@click="
offers = [];
projects = [];
@@ -23,7 +24,6 @@
showProjects = false;
loadOffers();
"
v-bind:class="computedOfferTabClassNames()"
>
Offers
</a>
@@ -31,6 +31,7 @@
<li>
<a
href="#"
:class="computedProjectTabClassNames()"
@click="
offers = [];
projects = [];
@@ -38,7 +39,6 @@
showProjects = true;
loadProjects();
"
v-bind:class="computedProjectTabClassNames()"
>
Projects
</a>
@@ -57,7 +57,7 @@
<button
class="px-4 rounded-r bg-slate-200 border border-l-0 border-slate-400"
>
<fa icon="magnifying-glass" class="fa-fw"></fa>
<font-awesome icon="magnifying-glass" class="fa-fw"></font-awesome>
</button>
</div>
-->
@@ -68,15 +68,15 @@
class="fixed right-6 top-24 text-center text-4xl leading-none bg-green-600 text-white w-14 py-2.5 rounded-full"
@click="onClickNewProject()"
>
<fa icon="plus" class="fa-fw"></fa>
<font-awesome icon="plus" class="fa-fw"></font-awesome>
</button>
<!-- Loading Animation -->
<div
class="fixed left-6 bottom-24 text-center text-4xl leading-none bg-slate-400 text-white w-14 py-2.5 rounded-full"
v-if="isLoading"
class="fixed left-6 bottom-24 text-center text-4xl leading-none bg-slate-400 text-white w-14 py-2.5 rounded-full"
>
<fa icon="spinner" class="fa-spin-pulse"></fa>
<font-awesome icon="spinner" class="fa-spin-pulse"></font-awesome>
</div>
<!-- Offer Results List -->
@@ -90,22 +90,22 @@
</div>
<ul id="listOffers" class="border-t border-slate-300">
<li
class="border-b border-slate-300"
v-for="offer in offers"
:key="offer.handleId"
class="border-b border-slate-300"
>
<div class="block py-4 flex gap-4">
<div v-if="offer.fulfillsPlanHandleId" class="flex-none">
<ProjectIcon
:entityId="offer.fulfillsPlanHandleId"
:iconSize="48"
:entity-id="offer.fulfillsPlanHandleId"
:icon-size="48"
class="inline-block align-middle border border-slate-300 rounded-md max-h-12 max-w-12"
/>
</div>
<div v-if="offer.recipientDid" class="flex-none w-12">
<EntityIcon
:entityId="offer.recipientDid"
:iconSize="48"
:entity-id="offer.recipientDid"
:icon-size="48"
class="inline-block align-middle border border-slate-300 rounded-md"
/>
</div>
@@ -130,17 +130,20 @@
<span class="text-sm">
<span v-if="offer.amount">
<fa
<font-awesome
:icon="libsUtil.iconForUnitCode(offer.unit)"
class="fa-fw text-slate-400"
/>
<span v-if="offer.amountGiven >= offer.amount">
<fa icon="check-circle" class="fa-fw text-green-500" />
<font-awesome
icon="check-circle"
class="fa-fw text-green-500"
/>
All {{ offer.amount }} given
</span>
<span v-else>
<fa
<font-awesome
icon="triangle-exclamation"
class="fa-fw text-yellow-500"
/>
@@ -162,7 +165,7 @@
</span>
<span v-else>
<!-- only show icon if there's not already a warning -->
<fa
<font-awesome
v-if="offer.amountGiven >= offer.amount"
icon="triangle-exclamation"
class="fa-fw text-yellow-300"
@@ -176,13 +179,16 @@
<span v-else>
<!-- Non-amount offer -->
<span v-if="offer.nonAmountGivenConfirmed">
<fa icon="check-circle" class="fa-fw text-green-500" />
<font-awesome
icon="check-circle"
class="fa-fw text-green-500"
/>
{{ offer.nonAmountGivenConfirmed }}
{{ offer.nonAmountGivenConfirmed == 1 ? "give" : "gives" }}
are confirmed.
</span>
<span v-else>
<fa
<font-awesome
icon="triangle-exclamation"
class="fa-fw text-yellow-500"
/>
@@ -191,10 +197,10 @@
</span>
<a @click="onClickLoadClaim(offer.jwtId)">
<fa
<font-awesome
icon="file-lines"
class="pl-2 text-blue-500 cursor-pointer"
></fa>
></font-awesome>
</a>
</span>
</div>
@@ -209,7 +215,7 @@
You have not announced any projects.
<div v-if="isRegistered">
Hit the big
<fa
<font-awesome
icon="plus"
class="bg-green-600 text-white px-1.5 py-1 rounded-full"
/>
@@ -217,8 +223,8 @@
</div>
<div v-else>
<button
@click="showNameThenIdDialog()"
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()"
>
Get someone to onboard you.
</button>
@@ -227,19 +233,19 @@
</div>
<ul id="listProjects" class="border-t border-slate-300">
<li
class="border-b border-slate-300"
v-for="project in projects"
:key="project.handleId"
class="border-b border-slate-300"
>
<a
@click="onClickLoadProject(project.handleId)"
class="block py-4 flex gap-4"
@click="onClickLoadProject(project.handleId)"
>
<div class="flex-none">
<ProjectIcon
:entityId="project.handleId"
:iconSize="48"
:imageUrl="project.image"
:entity-id="project.handleId"
:icon-size="48"
:image-url="project.image"
class="inline-block align-middle border border-slate-300 rounded-md max-h-12 max-w-12"
/>
</div>
@@ -295,7 +301,9 @@ import { OnboardPage } from "../libs/util";
})
export default class ProjectsView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
errNote(message) {
$router!: Router;
errNote(message: string) {
this.$notify(
{ group: "alert", type: "danger", title: "Error", text: message },
5000,
@@ -417,7 +425,7 @@ export default class ProjectsView extends Vue {
const route = {
path: "/project/" + encodeURIComponent(id),
};
(this.$router as Router).push(route);
this.$router.push(route);
}
/**
@@ -427,14 +435,14 @@ export default class ProjectsView extends Vue {
const route = {
name: "new-edit-project",
};
(this.$router as Router).push(route);
this.$router.push(route);
}
onClickLoadClaim(jwtId: string) {
const route = {
path: "/claim/" + encodeURIComponent(jwtId),
};
(this.$router as Router).push(route);
this.$router.push(route);
}
/**
@@ -537,10 +545,10 @@ export default class ProjectsView extends Vue {
text: "If so, we'll use those with QR codes to share.",
onCancel: async () => {},
onNo: async () => {
(this.$router as Router).push({ name: "share-my-contact-info" });
this.$router.push({ name: "share-my-contact-info" });
},
onYes: async () => {
(this.$router as Router).push({ name: "contact-qr" });
this.$router.push({ name: "contact-qr" });
},
noText: "we will share another way",
yesText: "we are nearby with cameras",

View File

@@ -10,7 +10,7 @@
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.back()"
>
<fa icon="chevron-left" class="fa-fw"></fa>
<font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
</h1>
</div>
@@ -22,17 +22,17 @@
<div>
<h2 class="text-2xl m-2">You're Here</h2>
<div class="m-2 flex">
<input type="checkbox" v-model="attended" class="h-6 w-6" />
<input v-model="attended" type="checkbox" class="h-6 w-6" />
<span class="pb-2 pl-2 pr-2">Attended</span>
</div>
<div class="m-2 flex">
<input type="checkbox" v-model="gaveTime" class="h-6 w-6" />
<input v-model="gaveTime" type="checkbox" class="h-6 w-6" />
<span class="pb-2 pl-2 pr-2">Spent Time</span>
<span v-if="gaveTime">
<input
v-model="hoursStr"
type="text"
placeholder="How much time"
v-model="hoursStr"
size="1"
class="border border-slate-400 h-6 px-2"
/>
@@ -48,8 +48,8 @@
class="flex justify-center mt-4"
>
<button
@click="record()"
class="block text-center 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 px-2 py-3 rounded-md w-56"
@click="record()"
>
Sign & Send
</button>
@@ -90,7 +90,7 @@ import * as libsUtil from "../libs/util";
})
export default class QuickActionBvcBeginView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
$router!: Router;
attended = true;
gaveTime = true;
hoursStr = "1";
@@ -201,7 +201,7 @@ export default class QuickActionBvcBeginView extends Vue {
},
3000,
);
(this.$router as Router).push({ path: "/quick-action-bvc" });
this.$router.push({ path: "/quick-action-bvc" });
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any

View File

@@ -10,7 +10,7 @@
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.back()"
>
<fa icon="chevron-left" class="fa-fw"></fa>
<font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
</h1>
</div>
@@ -22,16 +22,16 @@
<div>
<h2 class="text-2xl m-2">Confirm</h2>
<div v-if="loadingConfirms" class="flex justify-center">
<fa icon="spinner" class="fa-spin-pulse" />
<font-awesome icon="spinner" class="fa-spin-pulse" />
</div>
<div v-else-if="claimsToConfirm.length === 0">
There are no claims yet today for you to confirm.
</div>
<ul class="border-t border-slate-300 m-2">
<li
class="border-b border-slate-300 py-2"
v-for="record in claimsToConfirm"
:key="record.id"
class="border-b border-slate-300 py-2"
>
<div class="grid grid-cols-12">
<span class="col-span-11 justify-self-start">
@@ -39,6 +39,7 @@
<input
type="checkbox"
:checked="claimsToConfirmSelected.includes(record.id)"
class="mr-2 h-6 w-6"
@click="
claimsToConfirmSelected.includes(record.id)
? claimsToConfirmSelected.splice(
@@ -47,7 +48,6 @@
)
: claimsToConfirmSelected.push(record.id)
"
class="mr-2 h-6 w-6"
/>
</span>
{{
@@ -59,7 +59,7 @@
)
}}
<a @click="onClickLoadClaim(record.id)">
<fa
<font-awesome
icon="file-lines"
class="pl-2 text-blue-500 cursor-pointer"
/>
@@ -78,7 +78,7 @@
}}
so if you expected but do not see details from someone then ask them to
check that their activity is visible to you on their Contacts
<fa icon="users" class="text-slate-500" />
<font-awesome icon="users" class="text-slate-500" />
page.
</span>
</div>
@@ -96,18 +96,18 @@
<div>
<h2 class="text-2xl m-2">Anything else?</h2>
<div class="m-2 flex">
<input type="checkbox" v-model="someoneGave" class="h-6 w-6" />
<input v-model="someoneGave" type="checkbox" class="h-6 w-6" />
<span class="pb-2 pl-2 pr-2">The group provided</span>
<span v-if="someoneGave">
<input
type="text"
v-model="description"
type="text"
size="20"
class="border border-slate-400 h-6 px-2"
/>
<br />
(Everyone likes personalized messages! 😁 ... and for a pic:
<input type="checkbox" v-model="supplyGiftDetails" />)
<input v-model="supplyGiftDetails" type="checkbox" />)
</span>
<!-- This is to match input height to avoid shifting when hiding & showing. -->
<span v-else class="h-6">...</span>
@@ -119,8 +119,8 @@
class="flex justify-center mt-4"
>
<button
@click="record()"
class="block text-center 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 px-2 py-3 rounded-md w-56"
@click="record()"
>
Sign & Send
</button>
@@ -151,16 +151,18 @@ import {
retrieveSettingsForActiveAccount,
} from "../db/index";
import { Contact } from "../db/tables/contacts";
import {
GenericCredWrapper,
GenericVerifiableCredential,
ErrorResult,
} from "../interfaces";
import {
BVC_MEETUPS_PROJECT_CLAIM_ID,
claimSpecialDescription,
containsHiddenDid,
createAndSubmitConfirmation,
createAndSubmitGive,
GenericCredWrapper,
GenericVerifiableCredential,
getHeaders,
ErrorResult,
} from "../libs/endorserServer";
@Component({

View File

@@ -10,7 +10,7 @@
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.back()"
>
<fa icon="chevron-left" class="fa-fw"></fa>
<font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
</h1>
</div>
@@ -41,12 +41,14 @@ import { Component, Vue } from "vue-facing-decorator";
import QuickNav from "../components/QuickNav.vue";
import TopMessage from "../components/TopMessage.vue";
import { Router } from "vue-router";
@Component({
components: {
QuickNav,
TopMessage,
},
})
export default class QuickActionBvcView extends Vue {}
export default class QuickActionBvcView extends Vue {
$router!: Router;
}
</script>

View File

@@ -6,10 +6,10 @@
<div id="ViewBreadcrumb" class="mb-8">
<h1 class="text-lg text-center font-light relative px-7">
<!-- Back -->
<fa
<font-awesome
icon="chevron-left"
@click="$router.back()"
class="fa-fw text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.back()"
/>
Offers to Your Projects
</h1>
@@ -20,13 +20,13 @@
<p class="mt-2">
Maybe there are already some projects you can help on the
<router-link to="/discover" class="text-blue-500">
Discover page <fa icon="search" />
Discover page <font-awesome icon="search" />
</router-link>
</p>
<p class="mt-2">
You can announce more of your own on
<router-link to="/contacts" class="text-blue-500">
Your Ideas page <fa icon="hand" />
Your Ideas page <font-awesome icon="hand" />
</router-link>
</p>
</div>
@@ -42,8 +42,8 @@
class="mt-4 relative group"
>
<div
class="border-b border-slate-300 text-orange-400 pb-2 mb-2 font-bold text-sm"
v-if="offer.jwtId == lastAckedOfferToUserProjectsJwtId"
class="border-b border-slate-300 text-orange-400 pb-2 mb-2 font-bold text-sm"
>
You've already seen all the following
</div>
@@ -65,7 +65,10 @@
:to="{ path: '/claim/' + encodeURIComponent(offer.jwtId) }"
class="text-blue-500"
>
<fa icon="file-lines" class="pl-2 text-blue-500 cursor-pointer" />
<font-awesome
icon="file-lines"
class="pl-2 text-blue-500 cursor-pointer"
/>
</router-link>
</li>
</ul>
@@ -83,11 +86,12 @@ import QuickNav from "../components/QuickNav.vue";
import { NotificationIface } from "../constants/app";
import { db, retrieveSettingsForActiveAccount } from "../db/index";
import { Contact } from "../db/tables/contacts";
import { Router } from "vue-router";
import { OfferToPlanSummaryRecord } from "../interfaces";
import {
didInfo,
displayAmount,
getNewOffersToUserProjects,
OfferToPlanSummaryRecord,
} from "../libs/endorserServer";
import { retrieveAccountDids } from "../libs/util";
@@ -96,7 +100,7 @@ import { retrieveAccountDids } from "../libs/util";
})
export default class RecentOffersToUserView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
$router!: Router;
activeDid = "";
allContacts: Array<Contact> = [];
allMyDids: string[] = [];

View File

@@ -6,10 +6,10 @@
<div id="ViewBreadcrumb" class="mb-8">
<h1 class="text-lg text-center font-light relative px-7">
<!-- Back -->
<fa
<font-awesome
icon="chevron-left"
@click="$router.back()"
class="fa-fw text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.back()"
/>
Offers to You
</h1>
@@ -20,7 +20,7 @@
<p class="mt-2">
You can start the cycle on the
<router-link to="/contacts" class="text-blue-500">
Contacts page <fa icon="users" />
Contacts page <font-awesome icon="users" />
</router-link>
with an "Offer" directly to someone. Hopefully you'll find a common
interest!
@@ -37,8 +37,8 @@
class="mt-4 relative group"
>
<div
class="border-b border-slate-300 text-orange-400 pb-2 mb-2 font-bold text-sm"
v-if="offer.jwtId == lastAckedOfferToUserJwtId"
class="border-b border-slate-300 text-orange-400 pb-2 mb-2 font-bold text-sm"
>
You've already seen all the following
</div>
@@ -58,7 +58,10 @@
:to="{ path: '/claim/' + encodeURIComponent(offer.jwtId) }"
class="text-blue-500"
>
<fa icon="file-lines" class="pl-2 text-blue-500 cursor-pointer" />
<font-awesome
icon="file-lines"
class="pl-2 text-blue-500 cursor-pointer"
/>
</router-link>
</li>
</ul>
@@ -68,7 +71,7 @@
<script lang="ts">
import { Component, Vue } from "vue-facing-decorator";
import { Router } from "vue-router";
import GiftedDialog from "../components/GiftedDialog.vue";
import EntityIcon from "../components/EntityIcon.vue";
import InfiniteScroll from "../components/InfiniteScroll.vue";
@@ -76,11 +79,11 @@ import QuickNav from "../components/QuickNav.vue";
import { NotificationIface } from "../constants/app";
import { db, retrieveSettingsForActiveAccount } from "../db/index";
import { Contact } from "../db/tables/contacts";
import { OfferSummaryRecord } from "../interfaces";
import {
didInfo,
displayAmount,
getNewOffersToUser,
OfferSummaryRecord,
} from "../libs/endorserServer";
import { retrieveAccountDids } from "../libs/util";
@@ -89,7 +92,7 @@ import { retrieveAccountDids } from "../libs/util";
})
export default class RecentOffersToUserView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
$router!: Router;
activeDid = "";
allContacts: Array<Contact> = [];
allMyDids: string[] = [];

View File

@@ -11,7 +11,7 @@
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.back()"
>
<fa icon="chevron-left" class="fa-fw"></fa>
<font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
</h1>
</div>
@@ -35,7 +35,7 @@
class="m-4 px-4 py-2 rounded-md bg-blue-200 text-blue-500"
@click="storeSearchBox"
>
<fa icon="save" class="fa-fw" />
<font-awesome icon="save" class="fa-fw" />
Store This Location for Nearby Search
</button>
<button
@@ -43,7 +43,7 @@
class="m-4 px-4 py-2 rounded-md bg-blue-200 text-blue-500"
@click="forgetSearchBox"
>
<fa icon="trash-can" class="fa-fw" />
<font-awesome icon="trash-can" class="fa-fw" />
Delete Stored Location
</button>
<button
@@ -51,7 +51,7 @@
class="m-4 px-4 py-2 rounded-md bg-blue-200 text-blue-500"
@click="resetLatLong"
>
<fa icon="rotate" class="fa-fw" />
<font-awesome icon="rotate" class="fa-fw" />
Reset To Original
</button>
<button
@@ -59,7 +59,7 @@
class="m-4 px-4 py-2 rounded-md bg-blue-200 text-blue-500"
@click="isNewMarkerSet = false"
>
<fa icon="eraser" class="fa-fw" />
<font-awesome icon="eraser" class="fa-fw" />
Erase Marker
</button>
<div v-if="isNewMarkerSet">
@@ -71,9 +71,9 @@
<div class="aspect-video">
<l-map
ref="map"
v-model:zoom="localZoom"
:center="[localCenterLat, localCenterLong]"
class="!z-40 rounded-md"
v-model:zoom="localZoom"
@click="setMapPoint"
>
<l-tile-layer
@@ -131,6 +131,7 @@ const DEFAULT_ZOOM = 2;
})
export default class SearchAreaView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
$router!: Router;
isChoosingSearchBox = false;
isNewMarkerSet = false;
@@ -219,7 +220,7 @@ export default class SearchAreaView extends Vue {
},
7000,
);
(this.$router as Router).back();
this.$router.back();
} catch (err) {
this.$notify(
{

Some files were not shown because too many files have changed in this diff Show More