Compare commits

..

22 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
118 changed files with 8055 additions and 2608 deletions

View File

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

5
.gitignore vendored
View File

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

View File

@@ -5,266 +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/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.4.4] - 2025.02.17
### 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 ## [0.4.3] - 2025.02.17
### Added
### Added in 0.4.3
- Discover query parameter searchPeople to go directly to the people map - Discover query parameter searchPeople to go directly to the people map
## [0.4.2] - 2025.02.17 ## [0.4.2] - 2025.02.17
### Added ### Added
- Capacitor build to Android
- Capacitor on iOS and Android
### Fixed ### Fixed
- Path issues - Path issues
## [0.4.1] - 2025.02.16 ## [0.4.1] - 2025.02.16
### Fixed
### Fixed in 0.4.1
- nostr build issue - nostr build issue
- Linting - Linting
## [0.4.0] - 2025.02.14 ## [0.4.0] - 2025.02.14
### Changed ### Changed
- Images in the home feed now take up the full width of the card. - 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. - 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. - 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. - The lightbox view includes a download button for the image in mobile view.
## [0.3.57] - 2025.02.11 ## [0.3.57] - 2025.02.11
### Added
### Added in 0.3.57
- Automatic user creation in onboarding meetings - Automatic user creation in onboarding meetings
## [0.3.55] - 2025.02.07 ## [0.3.55] - 2025.02.07
### Added
### Added in 0.3.55
- End time for projects - End time for projects
## [0.3.54] - 2025.02.06 ## [0.3.54] - 2025.02.06
### Added
### Added in 0.3.54
- Group onboarding meetings - Group onboarding meetings
## [0.3.53] - 2025.01.30 ## [0.3.53] - 2025.01.30
### Added
### Added in 0.3.53
- Hints for contacting the creator of a project - Hints for contacting the creator of a project
## [0.3.52] - 2025.01.22 ## [0.3.52] - 2025.01.22
### Fixed
### Fixed in 0.3.52
- User profile endpoint server for map was broken. - User profile endpoint server for map was broken.
## [0.3.51] - 2025.01.22 ## [0.3.51] - 2025.01.22
### Fixed
### Fixed in 0.3.51
- User profile map jumped on first zoom. - User profile map jumped on first zoom.
## [0.3.50] - 2025.01.20 - b9fedcd3fd3e34c3fb0fc79150d1a81a76eaeb40 ## [0.3.50] - 2025.01.20 - b9fedcd3fd3e34c3fb0fc79150d1a81a76eaeb40
### Added
### Added in 0.3.50
- User public profiles - User public profiles
## [0.3.49] - 2025.01.09 - 36301ed238ff84df25bb11a8d44a295ee7eaf0f8 ## [0.3.49] - 2025.01.09 - 36301ed238ff84df25bb11a8d44a295ee7eaf0f8
### Changed
### Changed in 0.3.49
- Make all external contact links direct to the contact-import page. - 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. - 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) ## [0.3.48] - 2025.01.08 - 398f3e64a376789f7eb1c400cd886f5a2cacd588 (but app shows 07c4e58)
### Added
### Added in 0.3.48
- More sanity-checks on contact-import JWT - More sanity-checks on contact-import JWT
## [0.3.47] - 2025.01.06 - 5bf6dd1ee32ca7cc46d39bd7afca58365b422f93 ## [0.3.47] - 2025.01.06 - 5bf6dd1ee32ca7cc46d39bd7afca58365b422f93
### Added
### Added in 0.3.47
- Notes on contacts page with new contact-edit page - Notes on contacts page with new contact-edit page
- Contact methods (only on contact-edit page and under DID details) - Contact methods (only on contact-edit page and under DID details)
- DID view with no DID shows user's info. - 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). - 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. - Extended details (eg. full claim) is beneath details link on claim page.
## [0.3.46] - 2025.01.03 - 9e7056616b5e5acc51e5a8cf7354d408029fefb3 ## [0.3.46] - 2025.01.03 - 9e7056616b5e5acc51e5a8cf7354d408029fefb3
### Added
### Added in 0.3.46
- More action-oriented questions for the gift prompts - 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. - Contact-list import set visibility for all, even if not chosen.
## [0.3.45] - 2025.01.01 - 65402dc68ce69ccc6cb9aa8d2e7a9249bf4298e0 ## [0.3.45] - 2025.01.01 - 65402dc68ce69ccc6cb9aa8d2e7a9249bf4298e0
### Fixed
### Fixed in 0.3.45
- Previous project links stayed when following a link. - Previous project links stayed when following a link.
## [0.3.44] - 2024.12.31 - 694b22987b05482e4527c2478bbe15e6b6f3b532 ## [0.3.44] - 2024.12.31 - 694b22987b05482e4527c2478bbe15e6b6f3b532
### Added
### Added in 0.3.44
- Project counts on a map - Project counts on a map
## [0.3.42] - 2024.12.27 - 9751934bc24a1040415a8cfeacbae59ed91f92a5 ## [0.3.42] - 2024.12.27 - 9751934bc24a1040415a8cfeacbae59ed91f92a5
### Added
### Added in 0.3.42
- Link from certificate page to the claim - Link from certificate page to the claim
### Changed
### Changed in 0.3.42
- Contact data sharing is now a verified JWT. - Contact data sharing is now a verified JWT.
- Feed pictures are larger. - Feed pictures are larger.
## [0.3.41] - 2024.12.21 - ff6d14138f26daea6216b051562f0a04681f69fc ## [0.3.41] - 2024.12.21 - ff6d14138f26daea6216b051562f0a04681f69fc
### Added
### Added in 0.3.41
- Link from certificate page to the claim - Link from certificate page to the claim
## [0.3.40] - 2024.12.20 - 77290d9fed3c364243793dc3e9bfe2e994a016b8 ## [0.3.40] - 2024.12.20 - 77290d9fed3c364243793dc3e9bfe2e994a016b8
### Added
### Added in 0.3.40
- Only show issuer on certificate if it's not the agent. - Only show issuer on certificate if it's not the agent.
## [0.3.39] - 2024.12.20 - d8819155e2acd2b57fdab523168fa5d1d09e80cc ## [0.3.39] - 2024.12.20 - d8819155e2acd2b57fdab523168fa5d1d09e80cc
### Added
### Added in 0.3.39
- Page for a framed claim certificate - Page for a framed claim certificate
## [0.3.38] - 2024.12.14 - f8cae5ad4fee1f114320dcce052299eab12108b2 ## [0.3.38] - 2024.12.14 - f8cae5ad4fee1f114320dcce052299eab12108b2
### Fixed
### Fixed in 0.3.38
- Error on BVC confirmation screen (from IndexedDB refactor) - Error on BVC confirmation screen (from IndexedDB refactor)
## [0.3.37] - 2024.12.13 - 4d805b43cd25eed73cdd6651f36ad1ec8c109555 ## [0.3.37] - 2024.12.13 - 4d805b43cd25eed73cdd6651f36ad1ec8c109555
### Added
### Added in 0.3.37
- Record a give from a project on the project page. - Record a give from a project on the project page.
- New button on home page opens the gifted dialog. - New button on home page opens the gifted dialog.
- On confirmation buttons on the project page gives, mark when unavailable and explain why. - 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. - 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. - 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 showing claim issuer name
- Problem going "back" from a project page - Problem going "back" from a project page
## [0.3.36] - 2024.11.24 - c8d23647d165016f8a8f575e13d32583242e53ac ## [0.3.36] - 2024.11.24 - c8d23647d165016f8a8f575e13d32583242e53ac
### Changed
### Changed in 0.3.36
- More friendly default reminder message - More friendly default reminder message
- Blue borders around people to indicate clickability - Blue borders around people to indicate clickability
## [0.3.35] - 2024.11.24 - bff7d0a6320b70349185e26bfac72e3bb17f76df ## [0.3.35] - 2024.11.24 - bff7d0a6320b70349185e26bfac72e3bb17f76df
### Added
### Added in 0.3.35
- Daily reliable, hard-coded notification message - Daily reliable, hard-coded notification message
- Setting to change the partner API server - Setting to change the partner API server
## [0.3.33] - 2024.11.07 - adb7b16ecf1343c39cba71a7d6bb0e7a973e1102 ## [0.3.33] - 2024.11.07 - adb7b16ecf1343c39cba71a7d6bb0e7a973e1102
### Fixed
### Fixed in 0.3.33
- Affirm Delivery button on offer claim page didn't work. - Affirm Delivery button on offer claim page didn't work.
- Plans were not showing by default on project page. - Plans were not showing by default on project page.
## [0.3.32] - 2024.11.06 - 9a3fa38a3fd28f977e06f0265fc39e635c9c5ccd ## [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. - Highlight in green new offers to user & to user's projects on the front page.
## [0.3.31] - 2024.10.25 - 07c02ab98a09d293dd90d9289a7872e7d681d296 ## [0.3.31] - 2024.10.25 - 07c02ab98a09d293dd90d9289a7872e7d681d296
### Changed
### Changed in 0.3.31
- Onboarding messages about offers - Onboarding messages about offers
## [0.3.30] ## [0.3.30]
### Added
### Added in 0.3.30
- Onboarding messages - Onboarding messages
## [0.3.29] - 2024.10.09 - babd3832bdfe0c40eaa3869de1b41399a51713c1 ## [0.3.29] - 2024.10.09 - babd3832bdfe0c40eaa3869de1b41399a51713c1
### Added
### Added in 0.3.29
- Invite for a contact to join immediately - Invite for a contact to join immediately
### Changed
### Changed in 0.3.29
- Send signed data to nostr endpoints to verify public key ownership. - Send signed data to nostr endpoints to verify public key ownership.
- Enhanced help & help onboarding. - Enhanced help & help onboarding.
### Changed in DB or environment ### Changed in DB or environment
- Uses Endorser.ch version 4.1.1 - Uses Endorser.ch version 4.1.1
## [0.3.28] - 2024.09.30 - 84720b94049d29cc0ddd99c50cef2e7176130133 ## [0.3.28] - 2024.09.30 - 84720b94049d29cc0ddd99c50cef2e7176130133
### Added
### Added in 0.3.28
- Posting to nostr apps Trustroots & TripHopping - Posting to nostr apps Trustroots & TripHopping
- Display of providers on claim view page - 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. - 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 - Requires Endorser.ch version 4.1.0
## [0.3.27] - 2024.09.22 - ee23e6f005e47f5bd6f04d804599f6395371b0e4 ## [0.3.27] - 2024.09.22 - ee23e6f005e47f5bd6f04d804599f6395371b0e4
### Fixed
### Fixed in 0.3.27
- Error loading BVC claims to confirm - Error loading BVC claims to confirm
- Really allow visibility of bulk-imported contacts - Really allow visibility of bulk-imported contacts
## [0.3.26] - 2024.09.16 - 8263ed2b29947b3ccc6f3133bbc9454c222bce28 ## [0.3.26] - 2024.09.16 - 8263ed2b29947b3ccc6f3133bbc9454c222bce28
### Added
### Added in 0.3.26
- Separate 'isRegistered' flag for each account - Separate 'isRegistered' flag for each account
### Fixed
### Fixed in 0.3.26
- Failure to assign offers to their project - Failure to assign offers to their project
- Alert when looking at one's own activity if not in contacts. - Alert when looking at one's own activity if not in contacts.
## [0.3.25] - 2024.08.30 - dcbe02d877aecb4cdef2643d90e6595d246a9f82 ## [0.3.25] - 2024.08.30 - dcbe02d877aecb4cdef2643d90e6595d246a9f82
### Added
### Added in 0.3.25
- "Ideas" now jumps directly to giving prompt or contact list. - "Ideas" now jumps directly to giving prompt or contact list.
### Fixed
### Fixed in 0.3.25
- Empty giver name on gifted-details view - Empty giver name on gifted-details view
- Previously visited project would show up on the giving-details page. - Previously visited project would show up on the giving-details page.
### Removed
### Removed in 0.3.25
- All unnecessary localStorage for project IDs - All unnecessary localStorage for project IDs
## [0.3.23] - 2024.08.30 ## [0.3.23] - 2024.08.30
### Added
### Added in 0.3.23
- Sections in Help for different kinds of users - Sections in Help for different kinds of users
- Discovery page parameters so that links with search text work - Discovery page parameters so that links with search text work
- Message when no projects are found - Message when no projects are found
## [0.3.21] - 2024.08.24 - a7b89f4bb6da928d56daeffaae7741fa74cc80bf ## [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. - 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. - 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 - Moved contact actions from list onto detail page
## [0.3.20] - 2024.08.18 - 4064eb75a9743ca268bf00016fa0a5fc5dec4e30 ## [0.3.20] - 2024.08.18 - 4064eb75a9743ca268bf00016fa0a5fc5dec4e30
### Fixed
### Fixed in 0.3.20
- Bad "give" verbiage on offer page - Bad "give" verbiage on offer page
- Failing offer test - Failing offer test
## [0.3.19] - 2024.08.18 - ee9c14942ceba993bf21a11249601f205158ec71 ## [0.3.19] - 2024.08.18 - ee9c14942ceba993bf21a11249601f205158ec71
### Added
### Added in 0.3.19
- Update of an offer - Update of an offer
- Recipient description in offer list - Recipient description in offer list
### Fixed
### Fixed in 0.3.19
- List of offers wasn't showing. - List of offers wasn't showing.
- Destination page after sharing photo was wrong. - Destination page after sharing photo was wrong.
## [0.3.17] - 2024.07.11 - cefa384ff1a2d922848c370640c096c529920fab ## [0.3.17] - 2024.07.11 - cefa384ff1a2d922848c370640c096c529920fab
### Added
### Added in 0.3.17
- Photos on more screens - Photos on more screens
### Fixed
### Fixed in 0.3.17
- Share of a photo, including sharing a photo from webkit/Safari which never worked - 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) - Nothing (though there's a new temp field in IndexedDB)
## [0.3.15] - 2024.08.04 - c8f0f2c2b16b9f0b4b47d40f7bf29058c7baa68e ## [0.3.15] - 2024.08.04 - c8f0f2c2b16b9f0b4b47d40f7bf29058c7baa68e
### Added
### Added in 0.3.15
- Edit gives - Edit gives
- Page to edit claim JSON before submitting - Page to edit claim JSON before submitting
- Update of imported contacts - Update of imported contacts
@@ -275,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 - Cache signatures for reports for passkey-signed requests
- Refactor: consolidate alternative signing, eg. for passkeys & did:peer - Refactor: consolidate alternative signing, eg. for passkeys & did:peer
- Playwright tests - 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 ## [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 ## [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 ## [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 ## [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 ## [0.3.10] - 2024.05.11 - 03ac31d98110f7828cf9acb366db8d01b185f64c
### Added
### Added in 0.3.10
- Share an image - Share an image
- Choose a file on the device for a profile 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 - Nothing
## [0.3.9] - 2024.04.28 - 874e717e698b93a1ace9f588e675b8a3dccd7617 ## [0.3.9] - 2024.04.28 - 874e717e698b93a1ace9f588e675b8a3dccd7617
### Added
### Added in 0.3.9
- Offers on contacts page - Offers on contacts page
- Checks on front page until they show as registered - Checks on front page until they show as registered
### Changed
### Changed in 0.3.9
- Scanned contacts now add immediately and prompt for registration. - Scanned contacts now add immediately and prompt for registration.
- Better UI for gives on contact page - Better UI for gives on contact page
- Better UI for all confirmation messages - 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 ## [0.3.8] - 2024.04.20 - 15c026c80ce03a26cae3ff80b0888934c101c7e2
### Added
### Added in 0.3.8
- Profile image for user - Profile image for user
### Fixed
### Fixed in 0.3.8
- Slow loading of home page feed - Slow loading of home page feed
### Changed in DB or environment
### Changed in DB or environment in 0.3.8
- Nothing - Nothing
## [0.3.7] - 2024.04.10 - cf18f1543a700d62a5f9e764905a4aafe1fb229b ## [0.3.7] - 2024.04.10 - cf18f1543a700d62a5f9e764905a4aafe1fb229b
### Added
### Added in 0.3.7
- Filter on home page feed - Filter on home page feed
- Ability to set time of daily notification - Ability to set time of daily notification
- Jump to app on click of notification - Jump to app on click of notification
### Changed
### Changed in 0.3.7
- Built with vite - Built with vite
- Descriptions on home page to include projects - Descriptions on home page to include projects
### Changed in DB or environment
### Changed in DB or environment in 0.3.7
- Nothing - Nothing
## [0.3.6] - 2024.03.24 - 3a07e31d6313ab95711265562d9023c42916e141 ## [0.3.6] - 2024.03.24 - 3a07e31d6313ab95711265562d9023c42916e141
### Added
### Added in 0.3.6
- Button to mirror photo during video - Button to mirror photo during video
- More detailed onboarding help screen - More detailed onboarding help screen
- Public-data blurb - Public-data blurb
### Changed in DB or environment
### Changed in DB or environment in 0.3.6
- Nothing - Nothing
## [0.3.5] - 2024.03.23 - 28754bdfb1e11aa221dd49a5dce4219b69cf6a9d ## [0.3.5] - 2024.03.23 - 28754bdfb1e11aa221dd49a5dce4219b69cf6a9d
### Added
### Added in 0.3.5
- Photo on gift records - Photo on gift records
### Fixed
### Fixed in 0.3.5
- Environment variable for BVC meetings project - Environment variable for BVC meetings project
- Environment variables and build enhancements for test vs prod - 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 - 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 get the right default APIs.
- Test that a new browser session will send the right BVC meetings project. - Test that a new browser session will send the right BVC meetings project.
## [0.2.17] - 2024.03.01 - 3612ea42240c5e1b7d7eff29a39ff18f1b869b36 ## [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 ## [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 ## [0.2.13] - 2024.02.07
### Added
### Added in 0.2.13
- Display of user's offers - Display of user's offers
- Check for valid DIDs - Check for valid DIDs
### Fixed
### Fixed in 0.2.13
- Name display on give prompt - Name display on give prompt
- Non-numbers on number input & autocapitalize on URL input - Non-numbers on number input & autocapitalize on URL input
### Changed in DB
### Changed in DB in 0.2.13
- Nothing - Nothing
## [0.2.12] - 2024.02.01 ## [0.2.12] - 2024.02.01
### Added
### Added in 0.2.12
- Prompts for gratitude - Prompts for gratitude
## [0.2.11] - 2024.01.28 ## [0.2.11] - 2024.01.28
### Added
### Added in 0.2.11
- Actions to share claim data with contacts - Actions to share claim data with contacts
- Bulk CSV import from Endorser Mobile export - Bulk CSV import from Endorser Mobile export
- Dates on give summaries - Dates on give summaries
## [0.2.10] - 2024.01.18 - 667e1e8890b42de59cd939caca1a01c7a7a702be ## [0.2.10] - 2024.01.18 - 667e1e8890b42de59cd939caca1a01c7a7a702be
### Added
### Added in 0.2.10
- Person identicons for contacts - Person identicons for contacts
- Confirmation & delivery directly from project page - Confirmation & delivery directly from project page
- Offer dialog now allows units - Offer dialog now allows units
- Links from claim detail page to the fulfilled project or offer - Links from claim detail page to the fulfilled project or offer
- Link to project from home feed - Link to project from home feed
- Copy to clipboard in more places - Copy to clipboard in more places
### Fixed
### Fixed in 0.2.10
- "More Contacts" for give on project page now links correctly. - "More Contacts" for give on project page now links correctly.
## [0.2.9] - 2024.01.15 - e5e702f8a5a53a6efbed48d35f0bc3cee63024a0 ## [0.2.9] - 2024.01.15 - e5e702f8a5a53a6efbed48d35f0bc3cee63024a0
### Fixed
### Fixed in 0.2.9
- Set visibility for new contact. - Set visibility for new contact.
## [0.2.8] - 2024.01.14 ## [0.2.8] - 2024.01.14
### Added
### Added in 0.2.8
- Automatic ID creation from home page - Automatic ID creation from home page
- Agent who can also edit a project - Agent who can also edit a project
### Fixed
### Fixed in 0.2.8
- Cannot declare anonymous gift - Cannot declare anonymous gift
## [0.2.7] - 2024.01.12 ## [0.2.7] - 2024.01.12
### Added
### Added in 0.2.7
- Give to fulfill a particular offer - Give to fulfill a particular offer
- Give as part of a trade as opposed to a donation - Give as part of a trade as opposed to a donation
- Error notifications on import - Error notifications on import
### Changed
### Changed in 0.2.7
- Library security updates - Library security updates
- Visibility of actions & confirmations on claim page - Visibility of actions & confirmations on claim page
### Fixed
### Fixed in 0.2.7
- Name of offerer - Name of offerer
## [0.2.2] - 2024.01.05 ## [0.2.2] - 2024.01.05
### Added
### Added in 0.2.2
- Check for notification capability on front screen - Check for notification capability on front screen
- Contact next-public-key-hash in manual textual input - Contact next-public-key-hash in manual textual input
- Confirmation for contact visibility change - Confirmation for contact visibility change
- YAML rendering of full claim details - YAML rendering of full claim details
- Hints for onboarding on the contact screen - Hints for onboarding on the contact screen
## [0.2.0] - 2024.01.04 ## [0.2.0] - 2024.01.04
### Added
### Added in 0.2.0
- Contact next-public-key-hash - Contact next-public-key-hash
- Icon for Android - Icon for Android
- More thorough messaging and testing for notifications - More thorough messaging and testing for notifications
## [0.1.9] - 2024.01.01 ## [0.1.9] - 2024.01.01
### Added
### Added in 0.1.9
- Import for contacts and settings - Import for contacts and settings
- Second download button for DuckDuckGo - Second download button for DuckDuckGo
### Changed
### Changed in 0.1.9
- Removed some keys from Dexie's IndexedDB declarations - Removed some keys from Dexie's IndexedDB declarations
## [0.1.8] - 2023.12.27- d26d1d360152a7d0e559b68486e85b72b88bd9ff ## [0.1.8] - 2023.12.27- d26d1d360152a7d0e559b68486e85b72b88bd9ff
### Added
### Added in 0.1.8
- DB logging for service-worker events - DB logging for service-worker events
- Help page for notifications - Help page for notifications
- Test notification & web-push triggers inside app - Test notification & web-push triggers inside app
- Check that the app is installed - Check that the app is installed
### Fixed
### Fixed in 0.1.8
- Project issuer display name - Project issuer display name
## [0.1.7] - 2023.12.19 - 91c6c7c11c71f96006cc876fc946f1f98a274ba2 ## [0.1.7] - 2023.12.19 - 91c6c7c11c71f96006cc876fc946f1f98a274ba2
### Changed
### Changed in 0.1.7
- Icons - Icons
### Fixed
### Fixed in 0.1.7
- Notification switch now shows message - Notification switch now shows message
- Prod/test server warning message at top of page - Prod/test server warning message at top of page
## [0.1.6] - 2023.12.17 - b445b1234fbfcf6b37d695373f259aab0eda1118 ## [0.1.6] - 2023.12.17 - b445b1234fbfcf6b37d695373f259aab0eda1118
### Added
### Added in 0.1.6
- Infinite scroll on home page - Infinite scroll on home page
### Changed
### Changed in 0.1.6
- UI improvements - UI improvements
- Show web-push subscription info - Show web-push subscription info
- Icon - Icon
## [0.1.5] - 2023.12.09 - 9c36bb509a9bae9bb3306d3bd9eeb144b67aa8ad ## [0.1.5] - 2023.12.09 - 9c36bb509a9bae9bb3306d3bd9eeb144b67aa8ad
### Added
### Added in 0.1.5
- Web push notifications (though not finalized) - Web push notifications (though not finalized)
- Credentials details page - Credentials details page
- See more data without an ID - See more data without an ID
- Change units of a give - Change units of a give
## [0.1.4] - 2023.11.20 - 7311d36726f3667ec4c68f241f91d404273ad4db ## [0.1.4] - 2023.11.20 - 7311d36726f3667ec4c68f241f91d404273ad4db
### Added
### Added in 0.1.4
- Offer on a project - Offer on a project
### Changed
### Changed in 0.1.4
- Automatically set as visible when importing a contact - Automatically set as visible when importing a contact
## [0.1.3] - 2023.11.08 - 910f57ec7d2e50803ae3d04f4b927e0f5219fbde ## [0.1.3] - 2023.11.08 - 910f57ec7d2e50803ae3d04f4b927e0f5219fbde
### Added
### Added in 0.1.3
- Contact name editing - Contact name editing
### Changed
### Changed in 0.1.3
- Don't show actions on front page if not registered. - Don't show actions on front page if not registered.
### Removed
### Removed in 0.1.3
- Home page Notiwind test buttons - Home page Notiwind test buttons
## [0.1.2] - 2023.11.01 - 7f6c93802911a030a89fe3706e18b5c17151e5bb ## [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. - 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. 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.) (Numbers at the beginning of lines are estimated hours. See [taskyaml.org](https://taskyaml.org/) for details.)
## Setup & Building ## Setup & Building
Quick start: Quick start:
@@ -19,15 +17,17 @@ npm install
npm run dev 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 ### Build the test & production app
```
```bash
npm run serve npm run serve
``` ```
### Lint and fix files ### Lint and fix files
```
```bash
npm run lint npm run lint
``` ```
@@ -35,7 +35,6 @@ npm run lint
Look below for the "test-all" instructions. Look below for the "test-all" instructions.
### Compile and minify for test & production ### 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. * 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): * 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 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: * 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. * 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 ## Tests
See [TESTING.md](test-playwright/TESTING.md) for detailed test instructions. See [TESTING.md](test-playwright/TESTING.md) for detailed test instructions.
## Icons ## Icons
To add an icon, add to main.ts and reference with `fa` element and `icon` attribute with the hyphenated name. To add an icon, add to main.ts and reference with `fa` element and `icon` attribute with the hyphenated name.
## Other ## Other
### Reference Material ### 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/",` * If you are deploying in a subdirectory, add it to `publicPath` in vue.config.js, eg: `publicPath: "/app/time-tracker/",`
### Kudos ### Kudos
Gifts make the world go 'round! Gifts make the world go 'round!

View File

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

View File

@@ -22,6 +22,13 @@
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </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> </activity>
<provider <provider

View File

@@ -1,3 +1,6 @@
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN // DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
include ':capacitor-android' include ':capacitor-android'
project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor') 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: { server: {
cleartext: true, cleartext: true,
}, },
plugins: {
App: {
appUrlOpen: {
handlers: [
{
url: "timesafari://*",
autoVerify: true
}
]
}
}
}
}; };
export default config; 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> <strong>We're sorry but TimeSafari doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript> </noscript>
<div id="app"></div> <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> </body>
</html> </html>

View File

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

View File

@@ -11,7 +11,7 @@ install! 'cocoapods', :disable_input_output_paths => true
def capacitor_pods def capacitor_pods
pod 'Capacitor', :path => '../../node_modules/@capacitor/ios' pod 'Capacitor', :path => '../../node_modules/@capacitor/ios'
pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios' pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios'
pod 'CapacitorApp', :path => '../../node_modules/@capacitor/app'
end end
target 'App' do 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();
}
});

4618
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -29,29 +29,29 @@
<p class="text-sm mb-2">{{ text }}</p> <p class="text-sm mb-2">{{ text }}</p>
<button <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" 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 }} {{ option1Text }}
</button> </button>
<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" 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 }} {{ option2Text }}
</button> </button>
<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" 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 }} {{ option3Text }}
</button> </button>
<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" 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 Cancel
</button> </button>

View File

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

View File

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

View File

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

View File

@@ -5,10 +5,10 @@
{{ customTitle }} {{ customTitle }}
</h1> </h1>
<input <input
v-model="description"
type="text" type="text"
class="block w-full rounded border border-slate-400 mb-2 px-3 py-2" class="block w-full rounded border border-slate-400 mb-2 px-3 py-2"
:placeholder="prompt || 'What was given?'" :placeholder="prompt || 'What was given?'"
v-model="description"
/> />
<div class="flex flex-row justify-center"> <div class="flex flex-row justify-center">
<span <span
@@ -21,19 +21,19 @@
class="border border-r-0 border-slate-400 bg-slate-200 px-4 py-2" class="border border-r-0 border-slate-400 bg-slate-200 px-4 py-2"
@click="amountInput === '0' ? null : decrement()" @click="amountInput === '0' ? null : decrement()"
> >
<fa icon="chevron-left" /> <font-awesome icon="chevron-left" />
</div> </div>
<input <input
id="inputGivenAmount" id="inputGivenAmount"
v-model="amountInput"
type="number" type="number"
class="border border-r-0 border-slate-400 px-2 py-2 text-center w-20" class="border border-r-0 border-slate-400 px-2 py-2 text-center w-20"
v-model="amountInput"
/> />
<div <div
class="rounded-r border border-slate-400 bg-slate-200 px-4 py-2" class="rounded-r border border-slate-400 bg-slate-200 px-4 py-2"
@click="increment()" @click="increment()"
> >
<fa icon="chevron-right" /> <font-awesome icon="chevron-right" />
</div> </div>
</div> </div>
<div class="mt-4 flex justify-center"> <div class="mt-4 flex justify-center">
@@ -62,7 +62,7 @@
</div> </div>
<p class="text-center mb-2 mt-6 italic"> <p class="text-center mb-2 mt-6 italic">
Sign & Send to publish to the world Sign & Send to publish to the world
<fa <font-awesome
icon="circle-info" icon="circle-info"
class="pl-2 text-blue-500 cursor-pointer" class="pl-2 text-blue-500 cursor-pointer"
@click="explainData()" @click="explainData()"

View File

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

View File

@@ -7,8 +7,8 @@
<!-- Header --> <!-- Header -->
<div class="flex justify-between items-center mb-4"> <div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-bold capitalize">{{ roleName }} Details</h2> <h2 class="text-xl font-bold capitalize">{{ roleName }} Details</h2>
<button @click="close" class="text-gray-500 hover:text-gray-700"> <button class="text-gray-500 hover:text-gray-700" @click="close">
<fa icon="times" /> <font-awesome icon="times" />
</button> </button>
</div> </div>
@@ -53,7 +53,10 @@
target="_blank" target="_blank"
class="text-blue-500" 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> </a>
</span> </span>
</span> </span>
@@ -66,7 +69,7 @@
<div class="mt-4"> <div class="mt-4">
<span v-if="canShare"> <span v-if="canShare">
If you'd like an introduction, 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 >click here to share the information with them and ask if they'll
tell you more about the {{ roleName }}.</a tell you more about the {{ roleName }}.</a
> >
@@ -74,8 +77,8 @@
<span v-else> <span v-else>
If you'd like an introduction, If you'd like an introduction,
<a <a
@click="copyToClipboard('A link to this page', windowLocation)"
class="text-blue-500" 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 >click here to copy this page, paste it into a message, and ask if
they'll tell you more about the {{ roleName }}.</a they'll tell you more about the {{ roleName }}.</a
> >
@@ -86,8 +89,8 @@
<!-- Footer --> <!-- Footer -->
<div class="flex justify-end"> <div class="flex justify-end">
<button <button
@click="close"
class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600" class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600"
@click="close"
> >
Close Close
</button> </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" class="text-lg text-center px-2 py-0.5 leading-none absolute right-0 top-0 text-white"
@click="close()" @click="close()"
> >
<fa icon="xmark" class="w-[1em]"></fa> <font-awesome icon="xmark" class="w-[1em]"></font-awesome>
</div> </div>
</div> </div>
<div> <div>
<div class="text-center mt-8"> <div class="text-center mt-8">
<div> <div>
<fa <font-awesome
icon="camera" 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" 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()" @click="openPhotoDialog()"
@@ -31,17 +31,21 @@
<div class="mt-4"> <div class="mt-4">
<span class="mt-2"> <span class="mt-2">
... or paste a URL: ... or paste a URL:
<input type="text" v-model="imageUrl" class="border-2" /> <input v-model="imageUrl" type="text" class="border-2" />
</span> </span>
<span class="ml-2"> <span class="ml-2">
<fa <font-awesome
v-if="imageUrl" v-if="imageUrl"
icon="check" 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" 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" @click="acceptUrl"
/> />
<!-- so that there's no shifting when it becomes visible --> <!-- 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> </span>
</div> </div>
</div> </div>

View File

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

View File

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

View File

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

View File

@@ -3,11 +3,11 @@
<div class="dialog"> <div class="dialog">
<h1 class="text-xl font-bold text-center mb-4">Offer Help</h1> <h1 class="text-xl font-bold text-center mb-4">Offer Help</h1>
<input <input
v-model="description"
type="text" type="text"
data-testId="inputDescription" data-testId="inputDescription"
class="block w-full rounded border border-slate-400 mb-2 px-3 py-2" class="block w-full rounded border border-slate-400 mb-2 px-3 py-2"
placeholder="Description of what is offered" placeholder="Description of what is offered"
v-model="description"
/> />
<div class="flex flex-row mt-2"> <div class="flex flex-row mt-2">
<span <span
@@ -17,23 +17,23 @@
{{ libsUtil.UNIT_SHORT[amountUnitCode] }} {{ libsUtil.UNIT_SHORT[amountUnitCode] }}
</span> </span>
<div <div
v-if="amountInput !== '0'"
class="border border-r-0 border-slate-400 bg-slate-200 px-4 py-2" class="border border-r-0 border-slate-400 bg-slate-200 px-4 py-2"
@click="decrement()" @click="decrement()"
v-if="amountInput !== '0'"
> >
<fa icon="chevron-left" /> <font-awesome icon="chevron-left" />
</div> </div>
<input <input
v-model="amountInput"
data-testId="inputOfferAmount" data-testId="inputOfferAmount"
type="number" type="number"
class="w-full border border-r-0 border-slate-400 px-2 py-2 text-center" class="w-full border border-r-0 border-slate-400 px-2 py-2 text-center"
v-model="amountInput"
/> />
<div <div
class="rounded-r border border-slate-400 bg-slate-200 px-4 py-2" class="rounded-r border border-slate-400 bg-slate-200 px-4 py-2"
@click="increment()" @click="increment()"
> >
<fa icon="chevron-right" /> <font-awesome icon="chevron-right" />
</div> </div>
</div> </div>
<div class="mt-4 flex justify-center"> <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" class="text-lg text-center leading-none absolute right-0 -top-1"
@click="onClickClose(true)" @click="onClickClose(true)"
> >
<fa icon="xmark" class="w-[1em]" /> <font-awesome icon="xmark" class="w-[1em]" />
</div> </div>
</h1> </h1>
@@ -21,7 +21,7 @@
</span> </span>
click on the click on the
<span class="bg-green-600 text-white rounded-full"> <span class="bg-green-600 text-white rounded-full">
<fa icon="plus" class="fa-fw" /> <font-awesome icon="plus" class="fa-fw" />
</span> </span>
button to express your appreciation for... whatever -- maybe thanks for button to express your appreciation for... whatever -- maybe thanks for
showing you all these fascinating stories of showing you all these fascinating stories of
@@ -40,7 +40,7 @@
<p class="mt-4 flex items-center"> <p class="mt-4 flex items-center">
The The
<fa <font-awesome
icon="house-chimney" icon="house-chimney"
class="ml-1 mr-1 text-lg text-white bg-slate-400 px-2 py-2 rounded" 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" class="text-lg text-center leading-none absolute right-0 -top-1"
@click="onClickClose(true)" @click="onClickClose(true)"
> >
<fa icon="xmark" class="w-[1em]" /> <font-awesome icon="xmark" class="w-[1em]" />
</div> </div>
</h1> </h1>
@@ -106,7 +106,7 @@
<p class="mt-4 flex items-center"> <p class="mt-4 flex items-center">
The The
<fa <font-awesome
icon="magnifying-glass" icon="magnifying-glass"
class="ml-1 mr-1 text-lg text-white bg-slate-400 px-2 py-2 rounded" 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" class="text-lg text-center leading-none absolute right-0 -top-1"
@click="onClickClose(true)" @click="onClickClose(true)"
> >
<fa icon="xmark" class="w-[1em]" /> <font-awesome icon="xmark" class="w-[1em]" />
</div> </div>
</h1> </h1>
<p class="relative"> <p class="relative">
Now you can take a turn: click on the Now you can take a turn: click on the
<span class="bg-green-600 text-white rounded-full"> <span class="bg-green-600 text-white rounded-full">
<fa icon="plus" class="fa-fw" /> <font-awesome icon="plus" class="fa-fw" />
</span> </span>
button to throw out projects of your own... anything you'd like to see 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 happen. If your first idea doesn't catch anyone, try, try again... and
@@ -157,7 +157,7 @@
<p class="mt-4 flex items-center"> <p class="mt-4 flex items-center">
The The
<fa <font-awesome
icon="hand" icon="hand"
class="ml-1 mr-1 text-lg text-white bg-slate-400 px-2 py-2 rounded" 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 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 interests is to offer time directly to them. You can do this on the
contacts screen 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. which is a great way to get to know a neighbor's interests.
</p> </p>
@@ -219,6 +219,7 @@ import { OnboardPage } from "../libs/util";
}) })
export default class OnboardingDialog extends Vue { export default class OnboardingDialog extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void; $notify!: (notification: NotificationIface, timeout?: number) => void;
$router!: Router;
activeDid = ""; activeDid = "";
firstContactName = null; firstContactName = null;
@@ -254,7 +255,7 @@ export default class OnboardingDialog extends Vue {
finishedOnboarding: true, finishedOnboarding: true,
}); });
if (goHome) { 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" class="text-lg text-center px-2 py-0.5 leading-none absolute right-0 top-0 text-white"
@click="close()" @click="close()"
> >
<fa icon="xmark" class="w-[1em]"></fa> <font-awesome icon="xmark" class="w-[1em]"></font-awesome>
</div> </div>
</div> </div>
<div v-if="uploading" class="flex justify-center"> <div v-if="uploading" class="flex justify-center">
<fa <font-awesome
icon="spinner" icon="spinner"
class="fa-spin fa-3x text-center block px-12 py-12" class="fa-spin fa-3x text-center block px-12 py-12"
/> />
@@ -28,7 +28,7 @@
<div v-else-if="blob"> <div v-else-if="blob">
<div v-if="crop"> <div v-if="crop">
<VuePictureCropper <VuePictureCropper
:boxStyle="{ :box-style="{
backgroundColor: '#f8f8f8', backgroundColor: '#f8f8f8',
margin: 'auto', margin: 'auto',
}" }"
@@ -56,8 +56,8 @@
</div> </div>
<div class="absolute bottom-[1rem] left-[1rem] px-2 py-1"> <div class="absolute bottom-[1rem] left-[1rem] px-2 py-1">
<button <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" 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> <span>Upload</span>
</button> </button>
@@ -67,8 +67,8 @@
class="absolute bottom-[1rem] right-[1rem] px-2 py-1" class="absolute bottom-[1rem] right-[1rem] px-2 py-1"
> >
<button <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" 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> <span>Retry</span>
</button> </button>
@@ -81,37 +81,37 @@
:resolution="{ width: 375, height: 812 }" :resolution="{ width: 375, height: 812 }"
--> -->
<camera <camera
facingMode="environment"
autoplay
ref="camera" ref="camera"
facing-mode="environment"
autoplay
@started="cameraStarted()" @started="cameraStarted()"
> >
<div <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" 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 <button
@click="takeImage()"
class="bg-blue-500 hover:bg-blue-700 text-white font-bold p-3 rounded-full text-2xl leading-none" 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> </button>
</div> </div>
<div <div
class="absolute portrait:bottom-2 portrait:right-16 landscape:right-0 landscape:bottom-16 landscape:pr-4 flex justify-center items-center" class="absolute portrait:bottom-2 portrait:right-16 landscape:right-0 landscape:bottom-16 landscape:pr-4 flex justify-center items-center"
> >
<button <button
@click="swapMirrorClass()"
class="bg-blue-500 hover:bg-blue-700 text-white font-bold p-3 rounded-full text-2xl leading-none" 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> </button>
</div> </div>
<div v-if="numDevices > 1" class="absolute bottom-2 right-4"> <div v-if="numDevices > 1" class="absolute bottom-2 right-4">
<button <button
@click="switchCamera()"
class="bg-blue-500 hover:bg-blue-700 text-white font-bold p-3 rounded-full text-2xl leading-none" 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> </button>
</div> </div>
</camera> </camera>

View File

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

View File

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

View File

@@ -13,7 +13,7 @@
> >
<router-link :to="{ name: 'home' }" class="block text-center py-2 px-1"> <router-link :to="{ name: 'home' }" class="block text-center py-2 px-1">
<div class="flex flex-col items-center"> <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> <span class="text-xs mt-1">feed</span>
</div> </div>
</router-link> </router-link>
@@ -32,7 +32,7 @@
class="block text-center py-2 px-1" class="block text-center py-2 px-1"
> >
<div class="flex flex-col items-center"> <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> <span class="text-xs mt-1">search</span>
</div> </div>
</router-link> </router-link>
@@ -51,7 +51,7 @@
class="block text-center py-2 px-1" class="block text-center py-2 px-1"
> >
<div class="flex flex-col items-center"> <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> <span class="text-xs mt-1">your work</span>
</div> </div>
</router-link> </router-link>
@@ -70,7 +70,7 @@
class="block text-center py-2 px-1" class="block text-center py-2 px-1"
> >
<div class="flex flex-col items-center"> <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> <span class="text-xs mt-1">contacts</span>
</div> </div>
</router-link> </router-link>
@@ -89,7 +89,7 @@
class="block text-center py-2 px-1" class="block text-center py-2 px-1"
> >
<div class="flex flex-col items-center"> <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, 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. but it isn't accurate because we don't hold anything for them.

View File

@@ -6,10 +6,10 @@
{{ sharingExplanation }} {{ sharingExplanation }}
<input <input
v-model="givenName"
type="text" type="text"
placeholder="Name" placeholder="Name"
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2" class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
v-model="givenName"
/> />
<div class="mt-8"> <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 { Buffer } from "buffer";
import { sha256 } from "ethereum-cryptography/sha256"; import { sha256 } from "ethereum-cryptography/sha256";
import { LRUCache } from "lru-cache"; import { LRUCache } from "lru-cache";
@@ -17,63 +35,62 @@ import {
retrieveAccountMetadata, retrieveAccountMetadata,
retrieveFullyDecryptedAccount, retrieveFullyDecryptedAccount,
getPasskeyExpirationSeconds, getPasskeyExpirationSeconds,
GiverReceiverInputInfo,
} from "../libs/util"; } from "../libs/util";
import { createEndorserJwtForKey, KeyMeta } from "../libs/crypto/vc"; 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"; 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"; 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"; 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/"; 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="; 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="; 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 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> = export const BLANK_GENERIC_SERVER_RECORD: GenericCredWrapper<GenericVerifiableCredential> =
{ {
claim: { "@type": "" }, claim: { "@type": "" },
@@ -83,266 +100,98 @@ export const BLANK_GENERIC_SERVER_RECORD: GenericCredWrapper<GenericVerifiableCr
issuer: "", 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. // This is used to check for hidden info.
// See https://github.com/trentlarson/endorser-ch/blob/0cb626f803028e7d9c67f095858a9fc8542e3dbd/server/api/services/util.js#L6 // See https://github.com/trentlarson/endorser-ch/blob/0cb626f803028e7d9c67f095858a9fc8542e3dbd/server/api/services/util.js#L6
const HIDDEN_DID = "did:none:HIDDEN"; 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:"); 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; 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 * Recursively tests strings within an object/array against a test function
* * @param {Function} func - Test function to apply to strings
* Similar logic is found in endorser-mobile. * @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]") { if (Object.prototype.toString.call(input) === "[object String]") {
return func(input); return func(input);
} else if (input instanceof Object) { }
// Recursively test objects and arrays
else if (input instanceof Object) {
if (!Array.isArray(input)) { if (!Array.isArray(input)) {
// it's an object // Handle plain objects
for (const key in input) { for (const key in input) {
if (testRecursivelyOnStrings(func, input[key])) { if (testRecursivelyOnStrings(func, input[key])) {
return true; return true;
} }
} }
} else { } else {
// it's an array // Handle arrays
for (const value of input) { for (const value of input) {
if (testRecursivelyOnStrings(func, value)) { if (testRecursivelyOnStrings(func, value)) {
return true; return true;
@@ -351,6 +200,7 @@ function testRecursivelyOnStrings(func: (arg0: any) => boolean, input: any) {
} }
return false; return false;
} else { } else {
// Non-string, non-object values can't contain strings
return false; return false;
} }
} }
@@ -617,15 +467,23 @@ export async function getHeaders(
return headers; return headers;
} }
/**
* Cache for storing plan data
* @constant {LRUCache}
*/
const planCache: LRUCache<string, PlanSummaryRecord> = new LRUCache({ const planCache: LRUCache<string, PlanSummaryRecord> = new LRUCache({
max: 500, max: 500,
}); });
/** /**
* @param handleId nullable, in which case "undefined" will be returned * Retrieves plan data from cache or server
* @param requesterDid optional, in which case no private info will be returned * @param {string} handleId - Plan handle ID
* @param axios * @param {Axios} axios - Axios instance
* @param apiServer * @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( export async function getPlanFromCache(
handleId: string | undefined, handleId: string | undefined,
@@ -649,44 +507,44 @@ export async function getPlanFromCache(
cred = resp.data.data[0]; cred = resp.data.data[0];
planCache.set(handleId, cred); planCache.set(handleId, cred);
} else { } else {
console.error( console.info(
"Failed to load plan with handle", "[EndorserServer] Plan cache is empty for handle",
handleId, handleId,
" Got data:", " Got data:",
resp.data, JSON.stringify(resp.data),
); );
} }
} catch (error) { } catch (error) {
console.error( console.error(
"Failed to load plan with handle", "[EndorserServer] Failed to load plan with handle",
handleId, handleId,
" Got error:", " Got error:",
error, JSON.stringify(error),
); );
} }
} }
return cred; return cred;
} }
/**
* Updates plan data in cache
* @param {string} handleId - Plan handle ID
* @param {PlanSummaryRecord} planSummary - Plan data to cache
*/
export async function setPlanInCache( export async function setPlanInCache(
handleId: string, handleId: string,
planSummary: PlanSummaryRecord, planSummary: PlanSummaryRecord,
) { ): Promise<void> {
planCache.set(handleId, planSummary); planCache.set(handleId, planSummary);
} }
/** /**
* * Extracts user-friendly message from server error
* @param error that is thrown from an Endorser server call by Axios * @param {any} error - Error thrown from Endorser server call
* @returns user-friendly message, or undefined if none found * @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): string | undefined {
export function serverMessageForUser(error: any) { return error?.response?.data?.error?.message;
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
);
} }
/** /**

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,10 +7,10 @@
<h1 class="text-4xl text-center font-light relative px-7"> <h1 class="text-4xl text-center font-light relative px-7">
<!-- Back --> <!-- Back -->
<button <button
@click="$router.go(-1)"
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1" class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.go(-1)"
> >
<fa icon="chevron-left" class="fa-fw" /> <font-awesome icon="chevron-left" class="fa-fw" />
</button> </button>
{{ contact.name || AppString.NO_CONTACT_NAME }} {{ contact.name || AppString.NO_CONTACT_NAME }}
</h1> </h1>
@@ -25,9 +25,9 @@
Name Name
</label> </label>
<input <input
v-model="contactName"
type="text" 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" 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> </div>
@@ -38,9 +38,9 @@
</label> </label>
<textarea <textarea
id="contactNotes" id="contactNotes"
v-model="contactNotes"
rows="4" rows="4"
class="block w-full mt-1 border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500" 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> ></textarea>
</div> </div>
@@ -53,60 +53,60 @@
class="flex mt-2" class="flex mt-2"
> >
<input <input
type="text"
v-model="method.label" 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" class="block w-1/4 border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500"
placeholder="Label" placeholder="Label"
/> />
<input <input
type="text"
v-model="method.type" 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" 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" placeholder="Type"
/> />
<div class="relative"> <div class="relative">
<button <button
@click="toggleDropdown(index)"
class="px-2 py-1 bg-gray-200 rounded-md" 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> </button>
<div <div
v-if="dropdownIndex === index" v-if="dropdownIndex === index"
class="absolute bg-white border border-gray-300 rounded-md mt-1" class="absolute bg-white border border-gray-300 rounded-md mt-1"
> >
<div <div
@click="setMethodType(index, 'CELL')"
class="px-4 py-2 hover:bg-gray-100 cursor-pointer" class="px-4 py-2 hover:bg-gray-100 cursor-pointer"
@click="setMethodType(index, 'CELL')"
> >
CELL CELL
</div> </div>
<div <div
@click="setMethodType(index, 'EMAIL')"
class="px-4 py-2 hover:bg-gray-100 cursor-pointer" class="px-4 py-2 hover:bg-gray-100 cursor-pointer"
@click="setMethodType(index, 'EMAIL')"
> >
EMAIL EMAIL
</div> </div>
<div <div
@click="setMethodType(index, 'WHATSAPP')"
class="px-4 py-2 hover:bg-gray-100 cursor-pointer" class="px-4 py-2 hover:bg-gray-100 cursor-pointer"
@click="setMethodType(index, 'WHATSAPP')"
> >
WHATSAPP WHATSAPP
</div> </div>
</div> </div>
</div> </div>
<input <input
type="text"
v-model="method.value" 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" 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." placeholder="Number, email, etc."
/> />
<button @click="removeContactMethod(index)" class="ml-2 text-red-500"> <button class="ml-2 text-red-500" @click="removeContactMethod(index)">
<fa icon="trash-can" class="fa-fw" /> <font-awesome icon="trash-can" class="fa-fw" />
</button> </button>
</div> </div>
<button @click="addContactMethod" class="mt-2"> <button class="mt-2" @click="addContactMethod">
<fa <font-awesome
icon="plus" icon="plus"
class="fa-fw px-2 py-2.5 bg-green-500 text-green-100 rounded-full" class="fa-fw px-2 py-2.5 bg-green-500 text-green-100 rounded-full"
/> />
@@ -134,7 +134,7 @@
<script lang="ts"> <script lang="ts">
import * as R from "ramda"; import * as R from "ramda";
import { Component, Vue } from "vue-facing-decorator"; 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 QuickNav from "../components/QuickNav.vue";
import TopMessage from "../components/TopMessage.vue"; import TopMessage from "../components/TopMessage.vue";
@@ -142,6 +142,37 @@ import { AppString, NotificationIface } from "../constants/app";
import { db } from "../db/index"; import { db } from "../db/index";
import { Contact, ContactMethod } from "../db/tables/contacts"; 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({ @Component({
components: { components: {
QuickNav, QuickNav,
@@ -149,22 +180,46 @@ import { Contact, ContactMethod } from "../db/tables/contacts";
}, },
}) })
export default class ContactEditView extends Vue { export default class ContactEditView extends Vue {
/** Notification function injected by Vue */
$notify!: (notification: NotificationIface, timeout?: number) => void; $notify!: (notification: NotificationIface, timeout?: number) => void;
/** Current route instance */
$route!: RouteLocationNormalizedLoaded;
/** Router instance for navigation */
$router!: Router;
/** Current contact data */
contact: Contact = { contact: Contact = {
did: "", did: "",
name: "", name: "",
notes: "", notes: "",
}; };
/** Editable contact name field */
contactName = ""; contactName = "";
/** Editable contact notes field */
contactNotes = ""; contactNotes = "";
/** Array of editable contact methods */
contactMethods: Array<ContactMethod> = []; contactMethods: Array<ContactMethod> = [];
/** Currently open dropdown index, null if none open */
dropdownIndex: number | null = null; dropdownIndex: number | null = null;
/** App string constants */
AppString = AppString; 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() { async created() {
const contactDid = (this.$route as RouteLocation).params.did; const contactDid = this.$route.params.did;
const contact = await db.contacts.get(contactDid || ""); const contact = await db.contacts.get(contactDid || "");
if (contact) { if (contact) {
this.contact = 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() { addContactMethod() {
this.contactMethods.push({ label: "", type: "", value: "" }); 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) { removeContactMethod(index: number) {
this.contactMethods.splice(index, 1); 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) { toggleDropdown(index: number) {
this.dropdownIndex = this.dropdownIndex === index ? null : index; 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) { setMethodType(index: number, type: string) {
this.contactMethods[index].type = type; this.contactMethods[index].type = type;
this.dropdownIndex = null; 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() { async saveEdit() {
// without this conversion, "Failed to execute 'put' on 'IDBObjectStore': [object Array] could not be cloned." // without this conversion, "Failed to execute 'put' on 'IDBObjectStore': [object Array] could not be cloned."
const contactMethodsObj = JSON.parse(JSON.stringify(this.contactMethods)); const contactMethodsObj = JSON.parse(JSON.stringify(this.contactMethods));
// Normalize method types to uppercase
const contactMethods = contactMethodsObj.map((method: ContactMethod) => const contactMethods = contactMethodsObj.map((method: ContactMethod) =>
R.set(R.lensProp("type"), method.type.toUpperCase(), method), R.set(R.lensProp("type"), method.type.toUpperCase(), method),
); );
// Check for type changes
if (!R.equals(contactMethodsObj, contactMethods)) { if (!R.equals(contactMethodsObj, contactMethods)) {
this.contactMethods = contactMethods; this.contactMethods = contactMethods;
this.$notify( this.$notify(
@@ -219,11 +320,15 @@ export default class ContactEditView extends Vue {
); );
return; return;
} }
// Save to database
await db.contacts.update(this.contact.did, { await db.contacts.update(this.contact.did, {
name: this.contactName, name: this.contactName,
notes: this.contactNotes, notes: this.contactNotes,
contactMethods: contactMethods, contactMethods: contactMethods,
}); });
// Notify success and redirect
this.$notify({ this.$notify({
group: "alert", group: "alert",
type: "success", type: "success",

View File

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

View File

@@ -3,11 +3,8 @@
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto"> <section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Back --> <!-- Back -->
<div class="text-lg text-center font-light relative px-7"> <div class="text-lg text-center font-light relative px-7">
<h1 <h1 class="text-lg text-center px-2 py-1 absolute -left-2 -top-1" @click="$router.back()">
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1" <font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
@click="$router.back()"
>
<fa icon="chevron-left" class="fa-fw"></fa>
</h1> </h1>
</div> </div>
@@ -17,46 +14,31 @@
</h1> </h1>
<div v-if="checkingImports" class="text-center"> <div v-if="checkingImports" class="text-center">
<fa icon="spinner" class="animate-spin" /> <font-awesome icon="spinner" class="animate-spin" />
</div> </div>
<div v-else> <div v-else>
<span <span v-if="contactsImporting.length > sameCount" class="flex justify-center">
v-if="contactsImporting.length > sameCount" <input v-model="makeVisible" type="checkbox" class="mr-2" />
class="flex justify-center"
>
<input type="checkbox" v-model="makeVisible" class="mr-2" />
Make my activity visible to these contacts. Make my activity visible to these contacts.
</span> </span>
<div v-if="sameCount > 0"> <div v-if="sameCount > 0">
<span v-if="sameCount == 1" <span v-if="sameCount == 1">One contact is the same as an existing contact</span>
>One contact is the same as an existing contact</span <span v-else>{{ sameCount }} contacts are the same as existing contacts</span>
>
<span v-else
>{{ sameCount }} contacts are the same as existing contacts</span
>
</div> </div>
<!-- Results List --> <!-- Results List -->
<ul <ul v-if="contactsImporting.length > sameCount" class="border-t border-slate-300">
v-if="contactsImporting.length > sameCount"
class="border-t border-slate-300"
>
<li v-for="(contact, index) in contactsImporting" :key="contact.did"> <li v-for="(contact, index) in contactsImporting" :key="contact.did">
<div <div v-if="
v-if=" !contactsExisting[contact.did] ||
!contactsExisting[contact.did] || !R.isEmpty(contactDifferences[contact.did])
!R.isEmpty(contactDifferences[contact.did]) " class="grow overflow-hidden border-b border-slate-300 pt-2.5 pb-4">
"
class="grow overflow-hidden border-b border-slate-300 pt-2.5 pb-4"
>
<h2 class="text-base font-semibold"> <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 }} {{ contact.name || AppString.NO_CONTACT_NAME }}
- -
<span v-if="contactsExisting[contact.did]" class="text-orange-500" <span v-if="contactsExisting[contact.did]" class="text-orange-500">Existing</span>
>Existing</span
>
<span v-else class="text-green-500">New</span> <span v-else class="text-green-500">New</span>
</h2> </h2>
<div class="text-sm truncate"> <div class="text-sm truncate">
@@ -69,13 +51,9 @@
<div class="font-bold">Old Value</div> <div class="font-bold">Old Value</div>
<div class="font-bold">New Value</div> <div class="font-bold">New Value</div>
</div> </div>
<div <div v-for="(value, contactField) in contactDifferences[
v-for="(value, contactField) in contactDifferences[ contact.did
contact.did ]" :key="contactField" class="grid grid-cols-3 border">
]"
:key="contactField"
class="grid grid-cols-3 border"
>
<div class="border font-bold p-1"> <div class="border font-bold p-1">
{{ capitalizeAndInsertSpacesBeforeCaps(contactField) }} {{ capitalizeAndInsertSpacesBeforeCaps(contactField) }}
</div> </div>
@@ -88,8 +66,7 @@
</li> </li>
<button <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" 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 Import Selected Contacts
</button> </button>
</ul> </ul>
@@ -101,18 +78,10 @@
get the full text and paste it. (Note that iOS cuts off data in text 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. messages.) Ask the person to send the data a different way, eg. email.
<div class="mt-4 text-center"> <div class="mt-4 text-center">
<textarea <textarea v-model="inputJwt" placeholder="Contact-import data"
v-model="inputJwt" class="mt-4 border-2 border-gray-300 p-2 rounded" cols="30" @input="() => checkContactJwt(inputJwt)" />
placeholder="Contact-import data"
class="mt-4 border-2 border-gray-300 p-2 rounded"
cols="30"
@input="() => checkContactJwt(inputJwt)"
/>
<br /> <br />
<button <button class="ml-2 p-2 bg-blue-500 text-white rounded" @click="() => processContactJwt(inputJwt)">
@click="() => processContactJwt(inputJwt)"
class="ml-2 p-2 bg-blue-500 text-white rounded"
>
Check Import Check Import
</button> </button>
</div> </div>
@@ -122,6 +91,77 @@
</template> </template>
<script lang="ts"> <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 * as R from "ramda";
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { RouteLocationNormalizedLoaded, Router } from "vue-router"; import { RouteLocationNormalizedLoaded, Router } from "vue-router";
@@ -145,22 +185,75 @@ import {
import { getContactJwtFromJwtUrl } from "../libs/crypto"; import { getContactJwtFromJwtUrl } from "../libs/crypto";
import { decodeEndorserJwt } from "../libs/crypto/vc"; 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({ @Component({
components: { EntityIcon, OfferDialog, QuickNav }, components: { EntityIcon, OfferDialog, QuickNav },
}) })
export default class ContactImportView extends Vue { export default class ContactImportView extends Vue {
/** Notification function injected by Vue */
$notify!: (notification: NotificationIface, timeout?: number) => void; $notify!: (notification: NotificationIface, timeout?: number) => void;
/** Current route instance */
$route!: RouteLocationNormalizedLoaded;
/** Router instance for navigation */
$router!: Router;
// Constants
AppString = AppString; AppString = AppString;
capitalizeAndInsertSpacesBeforeCaps = capitalizeAndInsertSpacesBeforeCaps; capitalizeAndInsertSpacesBeforeCaps = capitalizeAndInsertSpacesBeforeCaps;
libsUtil = libsUtil; libsUtil = libsUtil;
R = R; R = R;
// Component state
/** Active user's DID for authentication and visibility settings */
activeDid = ""; activeDid = "";
/** API server URL for backend communication */
apiServer = ""; apiServer = "";
contactsExisting: Record<string, Contact> = {}; // user's contacts already in the system, keyed by DID /** Map of existing contacts keyed by DID for duplicate detection */
contactsImporting: Array<Contact> = []; // contacts from the import contactsExisting: Record<string, Contact> = {};
contactsSelected: Array<boolean> = []; // whether each contact in contactsImporting is selected /** 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< contactDifferences: Record<
string, string,
Record< Record<
@@ -170,69 +263,117 @@ export default class ContactImportView extends Vue {
old: string | boolean | Array<ContactMethod> | undefined; 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; checkingImports = false;
/** JWT input for manual contact import */
inputJwt: string = ""; inputJwt: string = "";
/** Visibility setting for imported contacts */
makeVisible = true; makeVisible = true;
/** Count of duplicate contacts found */
sameCount = 0; 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() { 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(); const settings = await retrieveSettingsForActiveAccount();
this.activeDid = settings.activeDid || ""; this.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || ""; this.apiServer = settings.apiServer || "";
}
// look for any imported contact array from the query parameter /**
const importedContacts = (this.$route as RouteLocationNormalizedLoaded) * Processes contacts from URL query parameters
.query["contacts"] as string; */
private async processQueryParams() {
const importedContacts = this.$route.query["contacts"] as string;
if (importedContacts) { if (importedContacts) {
await this.setContactsSelected(JSON.parse(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( * Processes JWT from URL path and handles different JWT formats
/\/contact-import\/(ey.+)$/, */
)?.[1]; 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) { 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 parsedJwt = decodeEndorserJwt(jwt);
const contacts: Array<Contact> = 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); (Array.isArray(parsedJwt.payload) ? parsedJwt.payload : undefined);
if (!contacts && parsedJwt.payload.own) { if (!contacts && parsedJwt.payload.own) {
// handle this single-contact JWT in the contacts page, better suited to single additions this.$router.push({
(this.$router as Router).push({
name: "contacts", name: "contacts",
query: { contactJwt: jwt }, query: { contactJwt: jwt },
}); });
return;
} }
if (contacts) { if (contacts) {
await this.setContactsSelected(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 ( if (
this.contactsImporting.length === 1 && this.contactsImporting.length === 1 &&
R.isEmpty(this.contactsExisting) 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.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>) { async setContactsSelected(contacts: Array<Contact>) {
this.contactsImporting = contacts; this.contactsImporting = contacts;
this.contactsSelected = new Array(this.contactsImporting.length).fill(true); this.contactsSelected = new Array(this.contactsImporting.length).fill(true);
await db.open(); await db.open();
const baseContacts = await db.contacts.toArray(); 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++) { for (let i = 0; i < this.contactsImporting.length; i++) {
const contactIn = this.contactsImporting[i]; const contactIn = this.contactsImporting[i];
const existingContact = baseContacts.find( const existingContact = baseContacts.find(
@@ -241,6 +382,7 @@ export default class ContactImportView extends Vue {
if (existingContact) { if (existingContact) {
this.contactsExisting[contactIn.did] = existingContact; this.contactsExisting[contactIn.did] = existingContact;
// Compare contact fields for differences
const differences: Record< const differences: Record<
string, string,
{ {
@@ -249,7 +391,6 @@ export default class ContactImportView extends Vue {
} }
> = {}; > = {};
Object.keys(contactIn).forEach((key) => { Object.keys(contactIn).forEach((key) => {
// eslint-disable-next-line prettier/prettier
if (!R.equals(contactIn[key as keyof Contact], existingContact[key as keyof Contact])) { if (!R.equals(contactIn[key as keyof Contact], existingContact[key as keyof Contact])) {
differences[key] = { differences[key] = {
old: existingContact[key as keyof Contact], old: existingContact[key as keyof Contact],
@@ -262,13 +403,16 @@ export default class ContactImportView extends Vue {
this.sameCount++; this.sameCount++;
} }
// don't automatically import previous data // Don't auto-select duplicates
this.contactsSelected[i] = false; this.contactsSelected[i] = false;
} }
} }
} }
// check the contact-import JWT /**
* Validates contact import JWT format
* @param jwtInput JWT string to validate
*/
async checkContactJwt(jwtInput: string) { async checkContactJwt(jwtInput: string) {
if ( if (
jwtInput.endsWith(APP_SERVER) || 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) { async processContactJwt(jwtInput: string) {
this.checkingImports = true; this.checkingImports = true;
try { try {
// (For another approach used with invites, see InviteOneAcceptView.processInvite)
const jwt: string = getContactJwtFromJwtUrl(jwtInput); const jwt: string = getContactJwtFromJwtUrl(jwtInput);
// JWT format: { header, payload, signature, data }
const payload = decodeEndorserJwt(jwt).payload; const payload = decodeEndorserJwt(jwt).payload;
if (Array.isArray(payload.contacts)) { if (Array.isArray(payload.contacts)) {
@@ -319,10 +464,16 @@ export default class ContactImportView extends Vue {
this.checkingImports = false; this.checkingImports = false;
} }
/**
* Imports selected contacts and sets visibility if requested
* Updates existing contacts or adds new ones
*/
async importContacts() { async importContacts() {
this.checkingImports = true; this.checkingImports = true;
let importedCount = 0, let importedCount = 0,
updatedCount = 0; updatedCount = 0;
// Process selected contacts
for (let i = 0; i < this.contactsImporting.length; i++) { for (let i = 0; i < this.contactsImporting.length; i++) {
if (this.contactsSelected[i]) { if (this.contactsSelected[i]) {
const contact = this.contactsImporting[i]; const contact = this.contactsImporting[i];
@@ -338,6 +489,8 @@ export default class ContactImportView extends Vue {
} }
} }
} }
// Set visibility if requested
if (this.makeVisible) { if (this.makeVisible) {
const failedVisibileToContacts = []; const failedVisibileToContacts = [];
for (let i = 0; i < this.contactsImporting.length; i++) { for (let i = 0; i < this.contactsImporting.length; i++) {
@@ -364,9 +517,8 @@ export default class ContactImportView extends Vue {
group: "alert", group: "alert",
type: "danger", type: "danger",
title: "Visibility Error", title: "Visibility Error",
text: `Failed to set visibility for ${failedVisibileToContacts.length} contact${ text: `Failed to set visibility for ${failedVisibileToContacts.length} contact${failedVisibileToContacts.length == 1 ? "" : "s"
failedVisibileToContacts.length == 1 ? "" : "s" }. You must set them individually: ${failedVisibileToContacts.map((c) => c.name).join(", ")}`,
}. You must set them individually: ${failedVisibileToContacts.map((c) => c.name).join(", ")}`,
}, },
-1, -1,
); );
@@ -375,6 +527,7 @@ export default class ContactImportView extends Vue {
this.checkingImports = false; this.checkingImports = false;
// Show success notification
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@@ -386,7 +539,7 @@ export default class ContactImportView extends Vue {
}, },
3000, 3000,
); );
(this.$router as Router).push({ name: "contacts" }); this.$router.push({ name: "contacts" });
} }
} }
</script> </script>

View File

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

View File

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

View File

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

View File

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

View File

@@ -18,17 +18,17 @@
:style="{ visibility: isSearchVisible ? 'visible' : 'hidden' }" :style="{ visibility: isSearchVisible ? 'visible' : 'hidden' }"
> >
<input <input
type="text"
v-model="searchTerms" v-model="searchTerms"
type="text"
placeholder="Search…" placeholder="Search…"
class="block w-full rounded-l border border-r-0 border-slate-400 px-3 py-2" 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 <button
class="px-4 rounded-r bg-slate-200 border border-l-0 border-slate-400" class="px-4 rounded-r bg-slate-200 border border-l-0 border-slate-400"
@click="searchSelected()" @click="searchSelected()"
> >
<fa icon="magnifying-glass" class="fa-fw"></fa> <font-awesome icon="magnifying-glass" class="fa-fw"></font-awesome>
</button> </button>
</div> </div>
@@ -39,6 +39,7 @@
<li> <li>
<a <a
href="#" href="#"
:class="computedProjectsTabStyleClassNames()"
@click=" @click="
projects = []; projects = [];
userProfiles = []; userProfiles = [];
@@ -46,7 +47,6 @@
isPeopleActive = false; isPeopleActive = false;
searchSelected(); searchSelected();
" "
v-bind:class="computedProjectsTabStyleClassNames()"
> >
Projects Projects
</a> </a>
@@ -54,6 +54,7 @@
<li> <li>
<a <a
href="#" href="#"
:class="computedPeopleTabStyleClassNames()"
@click=" @click="
projects = []; projects = [];
userProfiles = []; userProfiles = [];
@@ -61,7 +62,6 @@
isPeopleActive = true; isPeopleActive = true;
searchSelected(); searchSelected();
" "
v-bind:class="computedPeopleTabStyleClassNames()"
> >
People People
</a> </a>
@@ -75,6 +75,7 @@
<li> <li>
<a <a
href="#" href="#"
:class="computedLocalTabStyleClassNames()"
@click=" @click="
projects = []; projects = [];
userProfiles = []; userProfiles = [];
@@ -85,7 +86,6 @@
tempSearchBox = null; tempSearchBox = null;
searchLocal(); searchLocal();
" "
v-bind:class="computedLocalTabStyleClassNames()"
> >
Nearby Nearby
<!-- restore when the links don't jump around for different numbers <!-- restore when the links don't jump around for different numbers
@@ -101,6 +101,7 @@
<li> <li>
<a <a
href="#" href="#"
:class="computedMappedTabStyleClassNames()"
@click=" @click="
projects = []; projects = [];
userProfiles = []; userProfiles = [];
@@ -111,7 +112,6 @@
searchTerms = ''; searchTerms = '';
tempSearchBox = null; tempSearchBox = null;
" "
v-bind:class="computedMappedTabStyleClassNames()"
> >
<!-- search is triggered when map component gets to "ready" state --> <!-- search is triggered when map component gets to "ready" state -->
Mapped Mapped
@@ -120,6 +120,7 @@
<li> <li>
<a <a
href="#" href="#"
:class="computedRemoteTabStyleClassNames()"
@click=" @click="
projects = []; projects = [];
userProfiles = []; userProfiles = [];
@@ -130,7 +131,6 @@
tempSearchBox = null; tempSearchBox = null;
searchAll(); searchAll();
" "
v-bind:class="computedRemoteTabStyleClassNames()"
> >
Anywhere Anywhere
<!-- restore when the links don't jump around for different numbers <!-- 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" class="ml-2 mt-2 px-4 py-2 rounded-md bg-blue-200 text-blue-500"
@click="$router.push({ name: 'search-area' })" @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 Select a {{ searchBox ? "Different" : "" }} Location for Nearby Search
</button> </button>
</div> </div>
@@ -179,10 +179,10 @@
<!-- Loading Animation --> <!-- Loading Animation -->
<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" 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>
<div <div
v-else-if="projects.length === 0 && userProfiles.length === 0" v-else-if="projects.length === 0 && userProfiles.length === 0"
@@ -205,19 +205,19 @@
<!-- Projects List --> <!-- Projects List -->
<template v-if="isProjectsActive"> <template v-if="isProjectsActive">
<li <li
class="border-b border-slate-300"
v-for="project in projects" v-for="project in projects"
:key="project.handleId" :key="project.handleId"
class="border-b border-slate-300"
> >
<a <a
@click="onClickLoadItem(project.handleId)"
class="block py-4 flex gap-4 cursor-pointer" class="block py-4 flex gap-4 cursor-pointer"
@click="onClickLoadItem(project.handleId)"
> >
<div> <div>
<ProjectIcon <ProjectIcon
:entityId="project.handleId" :entity-id="project.handleId"
:iconSize="48" :icon-size="48"
:imageUrl="project.image" :image-url="project.image"
class="block border border-slate-300 rounded-md max-h-12 max-w-12" class="block border border-slate-300 rounded-md max-h-12 max-w-12"
/> />
</div> </div>
@@ -225,7 +225,10 @@
<div class="grow"> <div class="grow">
<h2 class="text-base font-semibold">{{ project.name }}</h2> <h2 class="text-base font-semibold">{{ project.name }}</h2>
<div class="text-sm"> <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( didInfo(
project.issuerDid, project.issuerDid,
@@ -243,17 +246,20 @@
<!-- Profiles List --> <!-- Profiles List -->
<template v-else> <template v-else>
<li <li
class="border-b border-slate-300"
v-for="profile in userProfiles" v-for="profile in userProfiles"
:key="profile.issuerDid" :key="profile.issuerDid"
class="border-b border-slate-300"
> >
<a <a
@click="onClickLoadItem(profile?.rowId || '')"
class="block py-4 flex gap-4 cursor-pointer" class="block py-4 flex gap-4 cursor-pointer"
@click="onClickLoadItem(profile?.rowId || '')"
> >
<div class="grow"> <div class="grow">
<div class="text-sm"> <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( didInfo(
profile.issuerDid, profile.issuerDid,
@@ -273,7 +279,10 @@
v-if="isAnywhereActive && profile.locLat && profile.locLon" v-if="isAnywhereActive && profile.locLat && profile.locLon"
class="mt-1 text-xs text-slate-500" 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") + (profile.locLat > 0 ? "North" : "South") +
" in " + " in " +
@@ -313,11 +322,11 @@ import {
} from "../db/index"; } from "../db/index";
import { Contact } from "../db/tables/contacts"; import { Contact } from "../db/tables/contacts";
import { BoundingBox } from "../db/tables/settings"; import { BoundingBox } from "../db/tables/settings";
import { PlanData } from "../interfaces";
import { import {
didInfo, didInfo,
errorStringForLog, errorStringForLog,
getHeaders, getHeaders,
PlanData,
} from "../libs/endorserServer"; } from "../libs/endorserServer";
import { OnboardPage, retrieveAccountDids } from "../libs/util"; import { OnboardPage, retrieveAccountDids } from "../libs/util";
@@ -414,7 +423,13 @@ export default class DiscoverView extends Vue {
this.isMappedActive = true; this.isMappedActive = true;
this.isAnywhereActive = false; this.isAnywhereActive = false;
} }
await this.searchAll();
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 {
await this.searchSelected();
}
} }
public resetCounts() { public resetCounts() {
@@ -475,7 +490,7 @@ export default class DiscoverView extends Vue {
} else { } else {
throw JSON.stringify(results); throw JSON.stringify(results);
} }
} else { } else { // people search must be active
this.projects = []; this.projects = [];
const profiles: UserProfile[] = results.data; const profiles: UserProfile[] = results.data;
if (profiles) { if (profiles) {

View File

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

View File

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

View File

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

View File

@@ -19,13 +19,14 @@
:to="{ name: 'invite-one' }" :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" 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> /></router-link>
</p> </p>
<p>Then watch that page to see when they accept their invite.</p> <p>Then watch that page to see when they accept their invite.</p>
<p> <p>
(That page is also reachable from the Contacts <fa icon="users" /> page (That page is also reachable from the Contacts
though the invitation <fa icon="envelope-open-text" /> icon.) <font-awesome icon="users" /> page though the invitation
<font-awesome icon="envelope-open-text" /> icon.)
</p> </p>
<h1 class="mt-4 font-bold text-xl">Next Steps</h1> <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> <h1 class="font-bold text-xl">Without a backup, you can lose data.</h1>
<div> <div>
<p> <p>
Exporting backups (from the Account <fa icon="circle-user" /> screen) Exporting backups (from the Account
is important for the case where they lose their device. This is <font-awesome icon="circle-user" /> screen) is important for the case
especially true for the Identifier Seed: that is theirs and and theirs where they lose their device. This is especially true for the
alone, and currently nobody else can recover it if they lose it. The Identifier Seed: that is theirs and and theirs alone, and currently
good thing is that anyone can create a new account and simply inform nobody else can recover it if they lose it. The good thing is that
their network of their new ID. anyone can create a new account and simply inform their network of
their new ID.
</p> </p>
</div> </div>
</div> </div>
@@ -54,7 +56,7 @@
<h1 class="font-bold text-xl">Add Contact & Register</h1> <h1 class="font-bold text-xl">Add Contact & Register</h1>
<p> <p>
You share even more information such as your picture and name when 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>
<p> <p>
Scanning Scanning
@@ -70,14 +72,14 @@
</p> </p>
<p> <p>
2) Scan their QR, or have them tap on it to copy their info and send it to you. 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>
<p> <p>
3) You can register them at their info page <fa icon="circle-info" /> 3) You can register them at their info page <font-awesome icon="circle-info" />
and click on the register button <fa icon="person-circle-question" /> and click on the register button <font-awesome icon="person-circle-question" />
</p> </p>
<p> <p>
4) Add yourself to their Contacts <fa icon="users" /> 4) Add yourself to their Contacts <font-awesome icon="users" />
</p> </p>
</div> </div>
@@ -94,7 +96,7 @@
<h1 class="font-bold text-xl">Enable Notifications</h1> <h1 class="font-bold text-xl">Enable Notifications</h1>
<div> <div>
<p> <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. 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. For the most reliable habits, set an alarm or do some other ritual to record gratitude every day.
</p> </p>

View File

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

View File

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

View File

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

View File

@@ -5,10 +5,10 @@
<h1 class="text-lg text-center font-light relative px-7"> <h1 class="text-lg text-center font-light relative px-7">
<!-- Cancel --> <!-- Cancel -->
<button <button
@click="$router.go(-1)"
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1" class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.go(-1)"
> >
<fa icon="chevron-left"></fa> <font-awesome icon="chevron-left"></font-awesome>
</button> </button>
Import Existing Identifier Import Existing Identifier
</h1> </h1>
@@ -20,10 +20,10 @@
<!-- id used by puppeteer test script --> <!-- id used by puppeteer test script -->
<textarea <textarea
id="seed-input" id="seed-input"
v-model="mnemonic"
type="text" type="text"
placeholder="Seed Phrase" placeholder="Seed Phrase"
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2" class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
v-model="mnemonic"
/> />
<h3 <h3
@@ -35,22 +35,22 @@
<div v-if="showAdvanced"> <div v-if="showAdvanced">
Enter a custom derivation path Enter a custom derivation path
<input <input
v-model="derivationPath"
type="text" type="text"
class="block w-full rounded border border-slate-400 mb-2 px-3 py-2" class="block w-full rounded border border-slate-400 mb-2 px-3 py-2"
v-model="derivationPath"
/> />
<span class="ml-4"> <span class="ml-4">
For previous uPort or Endorser users, For previous uPort or Endorser users,
<a <a
@click="derivationPath = UPORT_DERIVATION_PATH"
class="text-blue-500" class="text-blue-500"
@click="derivationPath = UPORT_DERIVATION_PATH"
> >
click here to use that value. click here to use that value.
</a> </a>
</span> </span>
<div class="mt-4" v-if="numAccounts == 1"> <div v-if="numAccounts == 1" class="mt-4">
<input type="checkbox" class="mr-2" v-model="shouldErase" /> <input v-model="shouldErase" type="checkbox" class="mr-2" />
<label>Erase the previous identifier.</label> <label>Erase the previous identifier.</label>
</div> </div>
@@ -65,15 +65,15 @@
<div class="mt-8"> <div class="mt-8">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2"> <div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
<button <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" 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 Import
</button> </button>
<button <button
@click="onCancelClick()"
type="button" 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" 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 Cancel
</button> </button>
@@ -111,6 +111,7 @@ export default class ImportAccountView extends Vue {
AppString = AppString; AppString = AppString;
$notify!: (notification: NotificationIface, timeout?: number) => void; $notify!: (notification: NotificationIface, timeout?: number) => void;
$router!: Router;
apiServer = ""; apiServer = "";
address = ""; address = "";
@@ -130,7 +131,7 @@ export default class ImportAccountView extends Vue {
} }
public onCancelClick() { public onCancelClick() {
(this.$router as Router).back(); this.$router.back();
} }
public isNotProdServer() { public isNotProdServer() {
@@ -170,7 +171,7 @@ export default class ImportAccountView extends Vue {
await db.settings.update(MASTER_SETTINGS_KEY, { await db.settings.update(MASTER_SETTINGS_KEY, {
activeDid: newId.did, activeDid: newId.did,
}); });
(this.$router as Router).push({ name: "account" }); this.$router.push({ name: "account" });
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) { } catch (err: any) {
console.error("Error saving mnemonic & updating settings:", err); 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"> <h1 class="text-lg text-center font-light relative px-7">
<!-- Cancel --> <!-- Cancel -->
<button <button
@click="$router.go(-1)"
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1" class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.go(-1)"
> >
<fa icon="chevron-left"></fa> <font-awesome icon="chevron-left"></font-awesome>
</button> </button>
Derive from Existing Identity Derive from Existing Identity
</h1> </h1>
@@ -25,21 +25,21 @@
</p> </p>
<ul class="mb-4"> <ul class="mb-4">
<li <li
class="block bg-slate-100 rounded-md flex items-center px-4 py-3 mb-2"
v-for="dids in didArrays" v-for="dids in didArrays"
:key="dids[0]" :key="dids[0]"
class="block bg-slate-100 rounded-md flex items-center px-4 py-3 mb-2"
@click="switchAccount(dids[0])" @click="switchAccount(dids[0])"
> >
<fa <font-awesome
v-if="dids[0] == selectedArrayFirstDid" v-if="dids[0] == selectedArrayFirstDid"
icon="circle" icon="circle"
class="fa-fw text-blue-500 text-xl mr-3" class="fa-fw text-blue-500 text-xl mr-3"
></fa> ></font-awesome>
<fa <font-awesome
v-else v-else
icon="circle" icon="circle"
class="fa-fw text-slate-400 text-xl mr-3" class="fa-fw text-slate-400 text-xl mr-3"
></fa> ></font-awesome>
<span class="overflow-hidden"> <span class="overflow-hidden">
<div class="text-sm text-slate-500 truncate"> <div class="text-sm text-slate-500 truncate">
<code>{{ dids.join(",") }}</code> <code>{{ dids.join(",") }}</code>
@@ -51,15 +51,15 @@
<div class="mt-8"> <div class="mt-8">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2"> <div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
<button <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" 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 Increment and Import
</button> </button>
<button <button
@click="onCancelClick()"
type="button" 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" 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 Cancel
</button> </button>
@@ -70,7 +70,7 @@
<script lang="ts"> <script lang="ts">
import { Component, Vue } from "vue-facing-decorator"; import { Component, Vue } from "vue-facing-decorator";
import { Router } from "vue-router"; import { Router, RouteLocationNormalizedLoaded } from "vue-router";
import { import {
DEFAULT_ROOT_DERIVATION_PATH, DEFAULT_ROOT_DERIVATION_PATH,
@@ -86,6 +86,9 @@ import { retrieveAllFullyDecryptedAccounts } from "../libs/util";
components: {}, components: {},
}) })
export default class ImportAccountView extends Vue { export default class ImportAccountView extends Vue {
$route!: RouteLocationNormalizedLoaded;
$router!: Router;
derivationPath = DEFAULT_ROOT_DERIVATION_PATH; derivationPath = DEFAULT_ROOT_DERIVATION_PATH;
didArrays: Array<Array<string>> = []; didArrays: Array<Array<string>> = [];
selectedArrayFirstDid = ""; selectedArrayFirstDid = "";
@@ -102,7 +105,7 @@ export default class ImportAccountView extends Vue {
} }
public onCancelClick() { public onCancelClick() {
(this.$router as Router).back(); this.$router.back();
} }
public switchAccount(did: string) { public switchAccount(did: string) {
@@ -151,7 +154,7 @@ export default class ImportAccountView extends Vue {
await db.settings.update(MASTER_SETTINGS_KEY, { await db.settings.update(MASTER_SETTINGS_KEY, {
activeDid: newId.did, activeDid: newId.did,
}); });
(this.$router as Router).push({ name: "account" }); this.$router.push({ name: "account" });
} catch (err) { } catch (err) {
console.error("Error saving mnemonic & updating settings:", err); console.error("Error saving mnemonic & updating settings:", err);
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,10 +10,10 @@
<!-- Loading Animation --> <!-- Loading Animation -->
<div <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" 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> </div>
<!-- Error State --> <!-- Error State -->
@@ -39,7 +39,7 @@
<script lang="ts"> <script lang="ts">
import { Component, Vue } from "vue-facing-decorator"; 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 QuickNav from "../components/QuickNav.vue";
import TopMessage from "../components/TopMessage.vue"; import TopMessage from "../components/TopMessage.vue";
@@ -69,17 +69,17 @@ export default class OnboardMeetingMembersView extends Vue {
firstName = ""; firstName = "";
isRegistered = false; isRegistered = false;
isLoading = true; isLoading = true;
$route!: RouteLocationNormalizedLoaded;
$router!: Router;
$refs!: { userNameDialog!: InstanceType<typeof UserNameDialog>;
userNameDialog: InstanceType<typeof UserNameDialog>;
};
get groupId(): string { get groupId(): string {
return (this.$route as RouteLocation).params.groupId as string; return (this.$route.params.groupId as string) || "";
} }
get password(): string { get password(): string {
return (this.$route as RouteLocation).query.password as string; return (this.$route.query.password as string) || "";
} }
async created() { async created() {

View File

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

View File

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

View File

@@ -16,6 +16,7 @@
<li> <li>
<a <a
href="#" href="#"
:class="computedOfferTabClassNames()"
@click=" @click="
offers = []; offers = [];
projects = []; projects = [];
@@ -23,7 +24,6 @@
showProjects = false; showProjects = false;
loadOffers(); loadOffers();
" "
v-bind:class="computedOfferTabClassNames()"
> >
Offers Offers
</a> </a>
@@ -31,6 +31,7 @@
<li> <li>
<a <a
href="#" href="#"
:class="computedProjectTabClassNames()"
@click=" @click="
offers = []; offers = [];
projects = []; projects = [];
@@ -38,7 +39,6 @@
showProjects = true; showProjects = true;
loadProjects(); loadProjects();
" "
v-bind:class="computedProjectTabClassNames()"
> >
Projects Projects
</a> </a>
@@ -57,7 +57,7 @@
<button <button
class="px-4 rounded-r bg-slate-200 border border-l-0 border-slate-400" 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> </button>
</div> </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" 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()" @click="onClickNewProject()"
> >
<fa icon="plus" class="fa-fw"></fa> <font-awesome icon="plus" class="fa-fw"></font-awesome>
</button> </button>
<!-- Loading Animation --> <!-- Loading Animation -->
<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" 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>
<!-- Offer Results List --> <!-- Offer Results List -->
@@ -90,22 +90,22 @@
</div> </div>
<ul id="listOffers" class="border-t border-slate-300"> <ul id="listOffers" class="border-t border-slate-300">
<li <li
class="border-b border-slate-300"
v-for="offer in offers" v-for="offer in offers"
:key="offer.handleId" :key="offer.handleId"
class="border-b border-slate-300"
> >
<div class="block py-4 flex gap-4"> <div class="block py-4 flex gap-4">
<div v-if="offer.fulfillsPlanHandleId" class="flex-none"> <div v-if="offer.fulfillsPlanHandleId" class="flex-none">
<ProjectIcon <ProjectIcon
:entityId="offer.fulfillsPlanHandleId" :entity-id="offer.fulfillsPlanHandleId"
:iconSize="48" :icon-size="48"
class="inline-block align-middle border border-slate-300 rounded-md max-h-12 max-w-12" class="inline-block align-middle border border-slate-300 rounded-md max-h-12 max-w-12"
/> />
</div> </div>
<div v-if="offer.recipientDid" class="flex-none w-12"> <div v-if="offer.recipientDid" class="flex-none w-12">
<EntityIcon <EntityIcon
:entityId="offer.recipientDid" :entity-id="offer.recipientDid"
:iconSize="48" :icon-size="48"
class="inline-block align-middle border border-slate-300 rounded-md" class="inline-block align-middle border border-slate-300 rounded-md"
/> />
</div> </div>
@@ -130,17 +130,20 @@
<span class="text-sm"> <span class="text-sm">
<span v-if="offer.amount"> <span v-if="offer.amount">
<fa <font-awesome
:icon="libsUtil.iconForUnitCode(offer.unit)" :icon="libsUtil.iconForUnitCode(offer.unit)"
class="fa-fw text-slate-400" class="fa-fw text-slate-400"
/> />
<span v-if="offer.amountGiven >= offer.amount"> <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 All {{ offer.amount }} given
</span> </span>
<span v-else> <span v-else>
<fa <font-awesome
icon="triangle-exclamation" icon="triangle-exclamation"
class="fa-fw text-yellow-500" class="fa-fw text-yellow-500"
/> />
@@ -162,7 +165,7 @@
</span> </span>
<span v-else> <span v-else>
<!-- only show icon if there's not already a warning --> <!-- only show icon if there's not already a warning -->
<fa <font-awesome
v-if="offer.amountGiven >= offer.amount" v-if="offer.amountGiven >= offer.amount"
icon="triangle-exclamation" icon="triangle-exclamation"
class="fa-fw text-yellow-300" class="fa-fw text-yellow-300"
@@ -176,13 +179,16 @@
<span v-else> <span v-else>
<!-- Non-amount offer --> <!-- Non-amount offer -->
<span v-if="offer.nonAmountGivenConfirmed"> <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 }}
{{ offer.nonAmountGivenConfirmed == 1 ? "give" : "gives" }} {{ offer.nonAmountGivenConfirmed == 1 ? "give" : "gives" }}
are confirmed. are confirmed.
</span> </span>
<span v-else> <span v-else>
<fa <font-awesome
icon="triangle-exclamation" icon="triangle-exclamation"
class="fa-fw text-yellow-500" class="fa-fw text-yellow-500"
/> />
@@ -191,10 +197,10 @@
</span> </span>
<a @click="onClickLoadClaim(offer.jwtId)"> <a @click="onClickLoadClaim(offer.jwtId)">
<fa <font-awesome
icon="file-lines" icon="file-lines"
class="pl-2 text-blue-500 cursor-pointer" class="pl-2 text-blue-500 cursor-pointer"
></fa> ></font-awesome>
</a> </a>
</span> </span>
</div> </div>
@@ -209,7 +215,7 @@
You have not announced any projects. You have not announced any projects.
<div v-if="isRegistered"> <div v-if="isRegistered">
Hit the big Hit the big
<fa <font-awesome
icon="plus" icon="plus"
class="bg-green-600 text-white px-1.5 py-1 rounded-full" class="bg-green-600 text-white px-1.5 py-1 rounded-full"
/> />
@@ -217,8 +223,8 @@
</div> </div>
<div v-else> <div v-else>
<button <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" 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. Get someone to onboard you.
</button> </button>
@@ -227,19 +233,19 @@
</div> </div>
<ul id="listProjects" class="border-t border-slate-300"> <ul id="listProjects" class="border-t border-slate-300">
<li <li
class="border-b border-slate-300"
v-for="project in projects" v-for="project in projects"
:key="project.handleId" :key="project.handleId"
class="border-b border-slate-300"
> >
<a <a
@click="onClickLoadProject(project.handleId)"
class="block py-4 flex gap-4" class="block py-4 flex gap-4"
@click="onClickLoadProject(project.handleId)"
> >
<div class="flex-none"> <div class="flex-none">
<ProjectIcon <ProjectIcon
:entityId="project.handleId" :entity-id="project.handleId"
:iconSize="48" :icon-size="48"
:imageUrl="project.image" :image-url="project.image"
class="inline-block align-middle border border-slate-300 rounded-md max-h-12 max-w-12" class="inline-block align-middle border border-slate-300 rounded-md max-h-12 max-w-12"
/> />
</div> </div>
@@ -295,7 +301,9 @@ import { OnboardPage } from "../libs/util";
}) })
export default class ProjectsView extends Vue { export default class ProjectsView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void; $notify!: (notification: NotificationIface, timeout?: number) => void;
errNote(message) { $router!: Router;
errNote(message: string) {
this.$notify( this.$notify(
{ group: "alert", type: "danger", title: "Error", text: message }, { group: "alert", type: "danger", title: "Error", text: message },
5000, 5000,
@@ -417,7 +425,7 @@ export default class ProjectsView extends Vue {
const route = { const route = {
path: "/project/" + encodeURIComponent(id), 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 = { const route = {
name: "new-edit-project", name: "new-edit-project",
}; };
(this.$router as Router).push(route); this.$router.push(route);
} }
onClickLoadClaim(jwtId: string) { onClickLoadClaim(jwtId: string) {
const route = { const route = {
path: "/claim/" + encodeURIComponent(jwtId), 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.", text: "If so, we'll use those with QR codes to share.",
onCancel: async () => {}, onCancel: async () => {},
onNo: async () => { onNo: async () => {
(this.$router as Router).push({ name: "share-my-contact-info" }); this.$router.push({ name: "share-my-contact-info" });
}, },
onYes: async () => { onYes: async () => {
(this.$router as Router).push({ name: "contact-qr" }); this.$router.push({ name: "contact-qr" });
}, },
noText: "we will share another way", noText: "we will share another way",
yesText: "we are nearby with cameras", 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" class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.back()" @click="$router.back()"
> >
<fa icon="chevron-left" class="fa-fw"></fa> <font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
</h1> </h1>
</div> </div>
@@ -22,17 +22,17 @@
<div> <div>
<h2 class="text-2xl m-2">You're Here</h2> <h2 class="text-2xl m-2">You're Here</h2>
<div class="m-2 flex"> <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> <span class="pb-2 pl-2 pr-2">Attended</span>
</div> </div>
<div class="m-2 flex"> <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 class="pb-2 pl-2 pr-2">Spent Time</span>
<span v-if="gaveTime"> <span v-if="gaveTime">
<input <input
v-model="hoursStr"
type="text" type="text"
placeholder="How much time" placeholder="How much time"
v-model="hoursStr"
size="1" size="1"
class="border border-slate-400 h-6 px-2" class="border border-slate-400 h-6 px-2"
/> />
@@ -48,8 +48,8 @@
class="flex justify-center mt-4" class="flex justify-center mt-4"
> >
<button <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" 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 Sign & Send
</button> </button>
@@ -90,7 +90,7 @@ import * as libsUtil from "../libs/util";
}) })
export default class QuickActionBvcBeginView extends Vue { export default class QuickActionBvcBeginView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void; $notify!: (notification: NotificationIface, timeout?: number) => void;
$router!: Router;
attended = true; attended = true;
gaveTime = true; gaveTime = true;
hoursStr = "1"; hoursStr = "1";
@@ -201,7 +201,7 @@ export default class QuickActionBvcBeginView extends Vue {
}, },
3000, 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 // 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" class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.back()" @click="$router.back()"
> >
<fa icon="chevron-left" class="fa-fw"></fa> <font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
</h1> </h1>
</div> </div>
@@ -22,16 +22,16 @@
<div> <div>
<h2 class="text-2xl m-2">Confirm</h2> <h2 class="text-2xl m-2">Confirm</h2>
<div v-if="loadingConfirms" class="flex justify-center"> <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>
<div v-else-if="claimsToConfirm.length === 0"> <div v-else-if="claimsToConfirm.length === 0">
There are no claims yet today for you to confirm. There are no claims yet today for you to confirm.
</div> </div>
<ul class="border-t border-slate-300 m-2"> <ul class="border-t border-slate-300 m-2">
<li <li
class="border-b border-slate-300 py-2"
v-for="record in claimsToConfirm" v-for="record in claimsToConfirm"
:key="record.id" :key="record.id"
class="border-b border-slate-300 py-2"
> >
<div class="grid grid-cols-12"> <div class="grid grid-cols-12">
<span class="col-span-11 justify-self-start"> <span class="col-span-11 justify-self-start">
@@ -39,6 +39,7 @@
<input <input
type="checkbox" type="checkbox"
:checked="claimsToConfirmSelected.includes(record.id)" :checked="claimsToConfirmSelected.includes(record.id)"
class="mr-2 h-6 w-6"
@click=" @click="
claimsToConfirmSelected.includes(record.id) claimsToConfirmSelected.includes(record.id)
? claimsToConfirmSelected.splice( ? claimsToConfirmSelected.splice(
@@ -47,7 +48,6 @@
) )
: claimsToConfirmSelected.push(record.id) : claimsToConfirmSelected.push(record.id)
" "
class="mr-2 h-6 w-6"
/> />
</span> </span>
{{ {{
@@ -59,7 +59,7 @@
) )
}} }}
<a @click="onClickLoadClaim(record.id)"> <a @click="onClickLoadClaim(record.id)">
<fa <font-awesome
icon="file-lines" icon="file-lines"
class="pl-2 text-blue-500 cursor-pointer" 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 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 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. page.
</span> </span>
</div> </div>
@@ -96,18 +96,18 @@
<div> <div>
<h2 class="text-2xl m-2">Anything else?</h2> <h2 class="text-2xl m-2">Anything else?</h2>
<div class="m-2 flex"> <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 class="pb-2 pl-2 pr-2">The group provided</span>
<span v-if="someoneGave"> <span v-if="someoneGave">
<input <input
type="text"
v-model="description" v-model="description"
type="text"
size="20" size="20"
class="border border-slate-400 h-6 px-2" class="border border-slate-400 h-6 px-2"
/> />
<br /> <br />
(Everyone likes personalized messages! 😁 ... and for a pic: (Everyone likes personalized messages! 😁 ... and for a pic:
<input type="checkbox" v-model="supplyGiftDetails" />) <input v-model="supplyGiftDetails" type="checkbox" />)
</span> </span>
<!-- This is to match input height to avoid shifting when hiding & showing. --> <!-- This is to match input height to avoid shifting when hiding & showing. -->
<span v-else class="h-6">...</span> <span v-else class="h-6">...</span>
@@ -119,8 +119,8 @@
class="flex justify-center mt-4" class="flex justify-center mt-4"
> >
<button <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" 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 Sign & Send
</button> </button>
@@ -151,16 +151,18 @@ import {
retrieveSettingsForActiveAccount, retrieveSettingsForActiveAccount,
} from "../db/index"; } from "../db/index";
import { Contact } from "../db/tables/contacts"; import { Contact } from "../db/tables/contacts";
import {
GenericCredWrapper,
GenericVerifiableCredential,
ErrorResult,
} from "../interfaces";
import { import {
BVC_MEETUPS_PROJECT_CLAIM_ID, BVC_MEETUPS_PROJECT_CLAIM_ID,
claimSpecialDescription, claimSpecialDescription,
containsHiddenDid, containsHiddenDid,
createAndSubmitConfirmation, createAndSubmitConfirmation,
createAndSubmitGive, createAndSubmitGive,
GenericCredWrapper,
GenericVerifiableCredential,
getHeaders, getHeaders,
ErrorResult,
} from "../libs/endorserServer"; } from "../libs/endorserServer";
@Component({ @Component({

View File

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

View File

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

View File

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

View File

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

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