Compare commits

..
Author SHA1 Message Date
Jose Olarte III f71c76fcd3 Fix: removed links and elements
- Removed links from icons and giver name
- Removed confirm button
- Lint fixes
2025-03-21 15:52:45 +08:00
Matthew Raymer d024db2258 feature: move amount and wire it up 2025-03-21 07:03:31 +00:00
Jose Olarte III b40604f8a6 Amount above arrow
- Needs wiring up
2025-03-19 20:47:10 +08:00
Jose Olarte III 436f40813c Source-destination compacted
- Narrower max-width
- Element sizes adjusted
- Switched to a more controllable unit for widths and heights
2025-03-17 21:03:58 +08:00
Jose Olarte III 77b296b606 Identicon responsive size fix + lint-fix 2025-03-17 17:59:59 +08:00
Matthew Raymer 8f7d794962 fix: improve image handling and icon support
- Fix image load event handler signature
- Add alt text for accessibility
- Add building icon to FontAwesome library

Technical Changes:
- Update cacheImage event to pass only image URL
- Add proper alt text for activity images
- Add faBuilding icon to FontAwesome library

This improves image handling and accessibility while adding
needed icon support for the activity feed interface.
2025-03-07 12:58:14 +00:00
Matthew Raymer fa7d6317b9 feat: add claim confirmation functionality to activity feed
- Add confirm button functionality to ActivityListItem
- Implement confirmation logic in HomeView
- Add proper button state handling and validation

Technical Changes:
- Add canConfirm computed property to validate confirmation ability
- Add handleConfirmClick method with proper error handling
- Pass required props (isRegistered, activeDid, confirmerIdList)
- Add confirmation dialog with user verification
- Implement claim submission with proper cleanup
- Add visual feedback for button states
- Update feed after successful confirmation

UI/UX Improvements:
- Add disabled state styling for confirm button
- Show proper error messages for invalid confirmation attempts
- Add loading and success notifications
- Improve button accessibility with proper states

Bug Fixes:
- Make apiServer optional in settings type
- Fix settings update during registration
- Add proper type checking for claim confirmation

This adds the ability to confirm claims directly from the
activity feed with proper validation, error handling, and
user feedback. The confirmation flow matches the existing
claim view confirmation functionality.
2025-03-07 10:22:53 +00:00
Jose Olarte III 4a75cdf20e Homeview changes
- Moved activity image further up the frame
- Added placeholder icon for projects
- Minor fixes
2025-03-04 20:38:14 +08:00
Jose Olarte III 79fdb9e570 Homeview design adjustments
- Added markup for poster info (needs wiring)
- Repositioned giver and receiver type icons to beside their labels
- Added "Confirm" button to bottom right of item card (timestamp repositioned to poster info as a result)
- Spacing tweaks
2025-03-03 19:34:09 +08:00
Jose Olarte III aa09827317 Type fixes 2025-02-28 21:00:03 +08:00
Matthew Raymer cc1780bd01 refactor: extract ActivityListItem into separate component
- Move activity list item markup from HomeView to new component
- Improve code organization and reusability
- Pass required props for claim handling and image viewing
- Maintain existing functionality while reducing component complexity
- Clean up unused commented code in HomeView

This refactor improves code maintainability by extracting the activity
feed item logic into its own component.
2025-02-28 09:34:59 +00:00
Jose Olarte III e5d9c25ad4 Comments 2025-02-27 21:21:01 +08:00
Jose Olarte III ef8c2e6093 In-progress: homeview design refresh
I had to comment out line 544 because it was causing errors (and seemed redundant?)
2025-02-27 21:12:29 +08:00
trentlarson 61afba3bca show totals of gives to a project 2025-02-22 20:34:23 -07:00
121 changed files with 3206 additions and 8186 deletions
+18 -15
View File
@@ -5,27 +5,30 @@ module.exports = {
es2022: true,
},
extends: [
"plugin:vue/vue3-recommended",
"plugin:vue/vue3-essential",
"eslint:recommended",
"@vue/typescript/recommended",
"plugin:prettier/recommended"
"plugin:prettier/recommended",
],
// parserOptions: {
// ecmaVersion: 2020,
// },
rules: {
"max-len": ["warn", {
code: 100,
ignoreComments: true,
ignorePattern: '^\\s*class="[^"]*"$',
ignoreStrings: true,
ignoreTemplateLiterals: true,
ignoreUrls: true,
}],
"no-console": process.env.NODE_ENV === "production" ? "error" : "warn",
"no-debugger": process.env.NODE_ENV === "production" ? "error" : "warn",
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-unnecessary-type-constraint": "off"
"max-len": [
"warn",
{
code: 120,
ignoreComments: true, // why does this not make it allow comment of any length?
ignorePattern: '^\\s*class="[^"]*"$',
ignoreStrings: true,
ignoreTemplateLiterals: true,
ignoreTrailingComments: true,
ignoreUrls: true,
},
],
"no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
"no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off",
// "prettier/prettier": ["warn", { printWidth: 120 }], // removes errors but adds thousands of warnings
"@typescript-eslint/no-unnecessary-type-constraint": "off",
},
};
+1 -4
View File
@@ -36,7 +36,4 @@ pnpm-debug.log*
/playwright/.cache/
/dist-electron-build/
/dist-capacitor/
/test-playwright-results/
playwright-tests
test-playwright
dist-electron-packages
/test-playwright-results/
+7 -47
View File
@@ -14,14 +14,12 @@ This guide explains how to build TimeSafari for different platforms.
## Initial Setup
1. Clone the repository:
```bash
git clone [repository-url]
cd TimeSafari
```
2. Install dependencies:
```bash
npm install
```
@@ -31,7 +29,6 @@ This guide explains how to build TimeSafari for different platforms.
To build for web deployment:
1. Run the production build:
```bash
npm run build
```
@@ -39,7 +36,6 @@ To build for web deployment:
2. The built files will be in the `dist` directory.
3. To test the production build locally:
```bash
npm run serve
```
@@ -49,13 +45,11 @@ To build for web deployment:
### Building for Linux
1. Build the electron app in production mode:
```bash
npm run build:electron-prod
```
2. Package the Electron app for Linux:
```bash
# For AppImage (recommended)
npm run electron:build-linux
@@ -71,14 +65,12 @@ To build for web deployment:
### Running the Packaged App
- AppImage: Make executable and run
```bash
chmod +x dist-electron-packages/TimeSafari-*.AppImage
./dist-electron-packages/TimeSafari-*.AppImage
```
- DEB: Install and run
```bash
sudo dpkg -i dist-electron-packages/timesafari_*_amd64.deb
timesafari
@@ -103,25 +95,21 @@ npm run build:electron-prod && npm run electron:start
Prerequisites: macOS with Xcode installed
1. Build the web assets:
```bash
npm run build -- --mode capacitor
```
2. Add iOS platform if not already added:
```bash
npx cap add ios
```
3. Update iOS project with latest build:
```bash
npx cap sync ios
```
4. Open the project in Xcode:
```bash
npx cap open ios
```
@@ -133,25 +121,21 @@ Prerequisites: macOS with Xcode installed
Prerequisites: Android Studio with SDK installed
1. Build the web assets:
```bash
npm run build -- --mode capacitor
```
2. Add Android platform if not already added:
```bash
npx cap add android
```
3. Update Android project with latest build:
```bash
npx cap sync android
```
4. Open the project in Android Studio:
```bash
npx cap open android
```
@@ -163,29 +147,25 @@ Prerequisites: Android Studio with SDK installed
To run the application in development mode:
1. Start the development server:
```bash
npm run dev
```
## PyWebView Desktop Build
### Prerequisites for PyWebView
### Prerequisites
- Python 3.8 or higher
- pip (Python package manager)
- virtualenv (recommended)
- System dependencies:
```bash
# For Ubuntu/Debian
sudo apt-get install python3-webview
# or
sudo apt-get install python3-gi python3-gi-cairo gir1.2-gtk-3.0 gir1.2-webkit2-4.0
# For Arch Linux
sudo pacman -S webkit2gtk python-gobject python-cairo
# For Fedora
sudo dnf install python3-webview
# or
@@ -193,9 +173,7 @@ To run the application in development mode:
```
### Setup
1. Create and activate a virtual environment (recommended):
```bash
python -m venv .venv
source .venv/bin/activate # On Linux/macOS
@@ -204,7 +182,6 @@ To run the application in development mode:
```
2. Install Python dependencies:
```bash
pip install -r requirements.txt
```
@@ -212,16 +189,13 @@ To run the application in development mode:
### Troubleshooting
If encountering PyInstaller version errors:
```bash
# Try installing the latest stable version
pip install --upgrade pyinstaller
```
### Development of PyWebView
### Development
1. Start the PyWebView development build:
```bash
npm run pywebview:dev
```
@@ -229,39 +203,31 @@ pip install --upgrade pyinstaller
### Building for Distribution
#### Linux
```bash
npm run pywebview:package-linux
```
The packaged application will be in `dist/TimeSafari`
#### Windows
```bash
npm run pywebview:package-win
```
The packaged application will be in `dist/TimeSafari`
#### macOS
#### macOS
```bash
npm run pywebview:package-mac
```
The packaged application will be in `dist/TimeSafari`
## Testing
Run local tests:
```bash
npm run test-local
```
Run all tests (includes building):
```bash
npm run test-all
```
@@ -269,13 +235,11 @@ npm run test-all
## Linting
Check code style:
```bash
npm run lint
```
Fix code style issues:
```bash
npm run lint-fix
```
@@ -296,20 +260,16 @@ See `.env.*` files for configuration.
## Deployment
### Version Management
1. Update CHANGELOG.md with new changes
2. Update version in package.json
3. Commit changes and tag release:
```bash
git tag <VERSION_TAG>
git push origin <VERSION_TAG>
```
4. After deployment, update package.json with next version + "-beta"
### Test Server
```bash
# Build using staging environment
npm run build -- --mode staging
@@ -319,7 +279,6 @@ rsync -azvu -e "ssh -i ~/.ssh/<YOUR_KEY>" dist ubuntutest@test.timesafari.app:ti
```
### Production Server
```bash
# On the production server:
pkgx +npm sh
@@ -334,7 +293,7 @@ cd -
mv time-safari/dist time-safari-dist-prev.0 && mv crowd-funder-for-time-pwa/dist time-safari/
```
## Troubleshooting Builds
## Troubleshooting
### Common Build Issues
@@ -351,3 +310,4 @@ 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 Android: Correct SDK version must be installed
- Check Capacitor configuration in capacitor.config.ts
+199 -369
View File
@@ -5,348 +5,279 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.4.5] - 2025.02.23
### Added
- Total amounts of gives on project page
### Changed in DB or environment
- Requires Endorser.ch version 4.2.6+
## [0.4.4] - 2025.02.17
### Fixed in 0.4.4
### Fixed
- On production (due to data?) the search results would disappear after scrolling down. Now we don't show any results when going to the people map with a shortcut.
## [0.4.3] - 2025.02.17
### Added in 0.4.3
### Added
- Discover query parameter searchPeople to go directly to the people map
## [0.4.2] - 2025.02.17
### Added
- Capacitor on iOS and Android
- Capacitor build to Android
### Fixed
- Path issues
## [0.4.1] - 2025.02.16
### Fixed in 0.4.1
### Fixed
- nostr build issue
- Linting
## [0.4.0] - 2025.02.14
### Changed
- Images in the home feed now take up the full width of the card.
- Clicking the image previously, would open the image in a new tab. Now, clicking the image opens the image in a lightbox view.
### Added in 0.4.0
### Added
- Clicking an image also now displays an in-app lightbox view of the image.
- The lightbox view includes a download button for the image in mobile view.
## [0.3.57] - 2025.02.11
### Added in 0.3.57
### Added
- Automatic user creation in onboarding meetings
## [0.3.55] - 2025.02.07
### Added in 0.3.55
### Added
- End time for projects
## [0.3.54] - 2025.02.06
### Added in 0.3.54
### Added
- Group onboarding meetings
## [0.3.53] - 2025.01.30
### Added in 0.3.53
### Added
- Hints for contacting the creator of a project
## [0.3.52] - 2025.01.22
### Fixed in 0.3.52
### Fixed
- User profile endpoint server for map was broken.
## [0.3.51] - 2025.01.22
### Fixed in 0.3.51
### Fixed
- User profile map jumped on first zoom.
## [0.3.50] - 2025.01.20 - b9fedcd3fd3e34c3fb0fc79150d1a81a76eaeb40
### Added in 0.3.50
### Added
- User public profiles
## [0.3.49] - 2025.01.09 - 36301ed238ff84df25bb11a8d44a295ee7eaf0f8
### Changed in 0.3.49
### Changed
- Make all external contact links direct to the contact-import page.
- Handle all new-single-contact JWTs in the contacts page, and multiple-contact JWTs in the contacts-import page.
## [0.3.48] - 2025.01.08 - 398f3e64a376789f7eb1c400cd886f5a2cacd588 (but app shows 07c4e58)
### Added in 0.3.48
### Added
- More sanity-checks on contact-import JWT
## [0.3.47] - 2025.01.06 - 5bf6dd1ee32ca7cc46d39bd7afca58365b422f93
### Added in 0.3.47
### Added
- Notes on contacts page with new contact-edit page
- Contact methods (only on contact-edit page and under DID details)
- DID view with no DID shows user's info.
### Changed in 0.3.47
### Changed
- URL for user's contact info is now URL to this app (not endorser.ch).
- Extended details (eg. full claim) is beneath details link on claim page.
## [0.3.46] - 2025.01.03 - 9e7056616b5e5acc51e5a8cf7354d408029fefb3
### Added in 0.3.46
### Added
- More action-oriented questions for the gift prompts
### Fixed in 0.3.46
### Fixed
- Contact-list import set visibility for all, even if not chosen.
## [0.3.45] - 2025.01.01 - 65402dc68ce69ccc6cb9aa8d2e7a9249bf4298e0
### Fixed in 0.3.45
### Fixed
- Previous project links stayed when following a link.
## [0.3.44] - 2024.12.31 - 694b22987b05482e4527c2478bbe15e6b6f3b532
### Added in 0.3.44
### Added
- Project counts on a map
## [0.3.42] - 2024.12.27 - 9751934bc24a1040415a8cfeacbae59ed91f92a5
### Added in 0.3.42
### Added
- Link from certificate page to the claim
### Changed in 0.3.42
### Changed
- Contact data sharing is now a verified JWT.
- Feed pictures are larger.
## [0.3.41] - 2024.12.21 - ff6d14138f26daea6216b051562f0a04681f69fc
### Added in 0.3.41
### Added
- Link from certificate page to the claim
## [0.3.40] - 2024.12.20 - 77290d9fed3c364243793dc3e9bfe2e994a016b8
### Added in 0.3.40
### Added
- Only show issuer on certificate if it's not the agent.
## [0.3.39] - 2024.12.20 - d8819155e2acd2b57fdab523168fa5d1d09e80cc
### Added in 0.3.39
### Added
- Page for a framed claim certificate
## [0.3.38] - 2024.12.14 - f8cae5ad4fee1f114320dcce052299eab12108b2
### Fixed in 0.3.38
### Fixed
- Error on BVC confirmation screen (from IndexedDB refactor)
## [0.3.37] - 2024.12.13 - 4d805b43cd25eed73cdd6651f36ad1ec8c109555
### Added in 0.3.37
### Added
- Record a give from a project on the project page.
- New button on home page opens the gifted dialog.
- On confirmation buttons on the project page gives, mark when unavailable and explain why.
### Changed in 0.3.37
### Changed
- Moved the secret into IndexedDB (and out of localStorage) for more reliability.
- New "invite" destination page helps troubleshoot when JWT link doesn't come through.
### Fixed in 0.3.37
### Fixed
- Problem showing claim issuer name
- Problem going "back" from a project page
## [0.3.36] - 2024.11.24 - c8d23647d165016f8a8f575e13d32583242e53ac
### Changed in 0.3.36
### Changed
- More friendly default reminder message
- Blue borders around people to indicate clickability
## [0.3.35] - 2024.11.24 - bff7d0a6320b70349185e26bfac72e3bb17f76df
### Added in 0.3.35
### Added
- Daily reliable, hard-coded notification message
- Setting to change the partner API server
## [0.3.33] - 2024.11.07 - adb7b16ecf1343c39cba71a7d6bb0e7a973e1102
### Fixed in 0.3.33
### Fixed
- Affirm Delivery button on offer claim page didn't work.
- Plans were not showing by default on project page.
## [0.3.32] - 2024.11.06 - 9a3fa38a3fd28f977e06f0265fc39e635c9c5ccd
### Added in 0.3.32
### Added
- Highlight in green new offers to user & to user's projects on the front page.
## [0.3.31] - 2024.10.25 - 07c02ab98a09d293dd90d9289a7872e7d681d296
### Changed in 0.3.31
### Changed
- Onboarding messages about offers
## [0.3.30]
### Added in 0.3.30
### Added
- Onboarding messages
## [0.3.29] - 2024.10.09 - babd3832bdfe0c40eaa3869de1b41399a51713c1
### Added in 0.3.29
### Added
- Invite for a contact to join immediately
### Changed in 0.3.29
### Changed
- Send signed data to nostr endpoints to verify public key ownership.
- Enhanced help & help onboarding.
### Changed in DB or environment
- Uses Endorser.ch version 4.1.1
## [0.3.28] - 2024.09.30 - 84720b94049d29cc0ddd99c50cef2e7176130133
### Added in 0.3.28
### Added
- Posting to nostr apps Trustroots & TripHopping
- Display of providers on claim view page
### Changed in 0.3.28
### Changed
- Switched BVC-meeting-ending gift to be a gift from the group.
### Changed in DB or environment in 0.3.28
### Changed in DB or environment
- Requires Endorser.ch version 4.1.0
## [0.3.27] - 2024.09.22 - ee23e6f005e47f5bd6f04d804599f6395371b0e4
### Fixed in 0.3.27
### Fixed
- Error loading BVC claims to confirm
- Really allow visibility of bulk-imported contacts
## [0.3.26] - 2024.09.16 - 8263ed2b29947b3ccc6f3133bbc9454c222bce28
### Added in 0.3.26
### Added
- Separate 'isRegistered' flag for each account
### Fixed in 0.3.26
### Fixed
- Failure to assign offers to their project
- Alert when looking at one's own activity if not in contacts.
## [0.3.25] - 2024.08.30 - dcbe02d877aecb4cdef2643d90e6595d246a9f82
### Added in 0.3.25
### Added
- "Ideas" now jumps directly to giving prompt or contact list.
### Fixed in 0.3.25
### Fixed
- Empty giver name on gifted-details view
- Previously visited project would show up on the giving-details page.
### Removed in 0.3.25
### Removed
- All unnecessary localStorage for project IDs
## [0.3.23] - 2024.08.30
### Added in 0.3.23
### Added
- Sections in Help for different kinds of users
- Discovery page parameters so that links with search text work
- Message when no projects are found
## [0.3.21] - 2024.08.24 - a7b89f4bb6da928d56daeffaae7741fa74cc80bf
### Added in 0.3.21
### Added
- Send list of contacts to someone, and move individual contact actions to detail page.
- Prompt for name in pop-up, and send to different contact-sharing screens.
### Changed in 0.3.21
### Changed
- Moved contact actions from list onto detail page
## [0.3.20] - 2024.08.18 - 4064eb75a9743ca268bf00016fa0a5fc5dec4e30
### Fixed in 0.3.20
### Fixed
- Bad "give" verbiage on offer page
- Failing offer test
## [0.3.19] - 2024.08.18 - ee9c14942ceba993bf21a11249601f205158ec71
### Added in 0.3.19
### Added
- Update of an offer
- Recipient description in offer list
### Fixed in 0.3.19
### Fixed
- List of offers wasn't showing.
- Destination page after sharing photo was wrong.
## [0.3.17] - 2024.07.11 - cefa384ff1a2d922848c370640c096c529920fab
### Added in 0.3.17
### Added
- Photos on more screens
### Fixed in 0.3.17
### Fixed
- Share of a photo, including sharing a photo from webkit/Safari which never worked
### Changed in DB or environment in 0.3.17
### Changed in DB or environment
- Nothing (though there's a new temp field in IndexedDB)
## [0.3.15] - 2024.08.04 - c8f0f2c2b16b9f0b4b47d40f7bf29058c7baa68e
### Added in 0.3.15
### Added
- Edit gives
- Page to edit claim JSON before submitting
- Update of imported contacts
@@ -357,364 +288,263 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Cache signatures for reports for passkey-signed requests
- Refactor: consolidate alternative signing, eg. for passkeys & did:peer
- Playwright tests
### Changed in 0.3.15
### Changed
- Linked projects display below description (instead of at bottom)
### Fixed in 0.3.15
### Fixed
- Visibility toggle appearance
### Changed in DB or environment in 0.3.15
### Changed in DB or environment
- Nothing
## [0.3.14] - 2024.06.22 - 1611d22892f683f43856d2503eee7f391b6bbce8
### Added in 0.3.14
### Added
- Clearer give-confirmation screen
- BX currency <https://thebx.medium.com/>
- BX currency https://thebx.medium.com/
- Deselection of project on gifted details page
### Fixed in 0.3.14
### Fixed
- Don't show registration pop-up for a new contact that is registered
### Changed in DB or environment in 0.3.14
### Changed in DB or environment
- Nothing
## [0.3.13] - 2024.05.24 - 08b67984e443c58d9178ad3776013b0bce7afddc
### Added in 0.3.13
### Added
- Photos on projects
### Changed in DB or environment in 0.3.13
### Changed in DB or environment
- Nothing
## [0.3.12] - 2024.05.19 - 141fb39ad19c44d82fe1a33bf85115beacf50870
### Fixed in 0.3.12
### Fixed
- Photo share (share_target) failed because requests were sent to server
### Changed in DB or environment in 0.3.12
### Changed in DB or environment
- Nothing
## [0.3.11] - 2024.05.19 - 567bcad88dfb7e9ac8fea72530d1163985e4a7cc
### Added in 0.3.11
### Added
- Choose a file for gifts, and a URL for gifts & profiles
### Fixed in 0.3.11
### Fixed
- Multiple button pushes were required to switch camera
### Changed in DB or environment in 0.3.11
### Changed in DB or environment
- Nothing
## [0.3.10] - 2024.05.11 - 03ac31d98110f7828cf9acb366db8d01b185f64c
### Added in 0.3.10
### Added
- Share an image
- Choose a file on the device for a profile image
### Changed in DB or environment in 0.3.10
### Changed in DB or environment
- Nothing
## [0.3.9] - 2024.04.28 - 874e717e698b93a1ace9f588e675b8a3dccd7617
### Added in 0.3.9
### Added
- Offers on contacts page
- Checks on front page until they show as registered
### Changed in 0.3.9
### Changed
- Scanned contacts now add immediately and prompt for registration.
- Better UI for gives on contact page
- Better UI for all confirmation messages
### Fixed in 0.3.9
### Fixed
- Repeated elements at top of main feed
### Changed in DB or environment in 0.3.9
### Changed in DB or environment
- Nothing
## [0.3.8] - 2024.04.20 - 15c026c80ce03a26cae3ff80b0888934c101c7e2
### Added in 0.3.8
### Added
- Profile image for user
### Fixed in 0.3.8
### Fixed
- Slow loading of home page feed
### Changed in DB or environment in 0.3.8
### Changed in DB or environment
- Nothing
## [0.3.7] - 2024.04.10 - cf18f1543a700d62a5f9e764905a4aafe1fb229b
### Added in 0.3.7
### Added
- Filter on home page feed
- Ability to set time of daily notification
- Jump to app on click of notification
### Changed in 0.3.7
### Changed
- Built with vite
- Descriptions on home page to include projects
### Changed in DB or environment in 0.3.7
### Changed in DB or environment
- Nothing
## [0.3.6] - 2024.03.24 - 3a07e31d6313ab95711265562d9023c42916e141
### Added in 0.3.6
### Added
- Button to mirror photo during video
- More detailed onboarding help screen
- Public-data blurb
### Changed in DB or environment in 0.3.6
### Changed in DB or environment
- Nothing
## [0.3.5] - 2024.03.23 - 28754bdfb1e11aa221dd49a5dce4219b69cf6a9d
### Added in 0.3.5
### Added
- Photo on gift records
### Fixed in 0.3.5
### Fixed
- Environment variable for BVC meetings project
- Environment variables and build enhancements for test vs prod
### Changed in DB or environment in 0.3.5
### Changed in DB or environment
- New environment variable for image API server
- Test that a new browser session will get the right default APIs.
- Test that a new browser session will send the right BVC meetings project.
## [0.2.17] - 2024.03.01 - 3612ea42240c5e1b7d7eff29a39ff18f1b869b36
### Added in 0.2.17
### Added
- Shortcut page for Bountiful Voluntaryist Community
### Changed in 0.2.17
### Changed
- More readable, targeted summaries in home-page feed items
### Changed in DB
- Nothing
## [0.2.14] - 2024.02.14 - 5f9edea1167dbfb64e16648764eed8c09b24eaeb
### Changed in 0.2.14
### Changed
- Combine all service worker scripts into a single file.
### Changed in DB in 0.2.14
### Changed in DB
- Nothing
## [0.2.13] - 2024.02.07
### Added in 0.2.13
### Added
- Display of user's offers
- Check for valid DIDs
### Fixed in 0.2.13
### Fixed
- Name display on give prompt
- Non-numbers on number input & autocapitalize on URL input
### Changed in DB in 0.2.13
### Changed in DB
- Nothing
## [0.2.12] - 2024.02.01
### Added in 0.2.12
### Added
- Prompts for gratitude
## [0.2.11] - 2024.01.28
### Added in 0.2.11
### Added
- Actions to share claim data with contacts
- Bulk CSV import from Endorser Mobile export
- Dates on give summaries
## [0.2.10] - 2024.01.18 - 667e1e8890b42de59cd939caca1a01c7a7a702be
### Added in 0.2.10
### Added
- Person identicons for contacts
- Confirmation & delivery directly from project page
- Offer dialog now allows units
- Links from claim detail page to the fulfilled project or offer
- Link to project from home feed
- Copy to clipboard in more places
### Fixed in 0.2.10
### Fixed
- "More Contacts" for give on project page now links correctly.
## [0.2.9] - 2024.01.15 - e5e702f8a5a53a6efbed48d35f0bc3cee63024a0
### Fixed in 0.2.9
### Fixed
- Set visibility for new contact.
## [0.2.8] - 2024.01.14
### Added in 0.2.8
### Added
- Automatic ID creation from home page
- Agent who can also edit a project
### Fixed in 0.2.8
### Fixed
- Cannot declare anonymous gift
## [0.2.7] - 2024.01.12
### Added in 0.2.7
### Added
- Give to fulfill a particular offer
- Give as part of a trade as opposed to a donation
- Error notifications on import
### Changed in 0.2.7
### Changed
- Library security updates
- Visibility of actions & confirmations on claim page
### Fixed in 0.2.7
### Fixed
- Name of offerer
## [0.2.2] - 2024.01.05
### Added in 0.2.2
### Added
- Check for notification capability on front screen
- Contact next-public-key-hash in manual textual input
- Confirmation for contact visibility change
- YAML rendering of full claim details
- Hints for onboarding on the contact screen
## [0.2.0] - 2024.01.04
### Added in 0.2.0
### Added
- Contact next-public-key-hash
- Icon for Android
- More thorough messaging and testing for notifications
## [0.1.9] - 2024.01.01
### Added in 0.1.9
### Added
- Import for contacts and settings
- Second download button for DuckDuckGo
### Changed in 0.1.9
### Changed
- Removed some keys from Dexie's IndexedDB declarations
## [0.1.8] - 2023.12.27- d26d1d360152a7d0e559b68486e85b72b88bd9ff
### Added in 0.1.8
### Added
- DB logging for service-worker events
- Help page for notifications
- Test notification & web-push triggers inside app
- Check that the app is installed
### Fixed in 0.1.8
### Fixed
- Project issuer display name
## [0.1.7] - 2023.12.19 - 91c6c7c11c71f96006cc876fc946f1f98a274ba2
### Changed in 0.1.7
### Changed
- Icons
### Fixed in 0.1.7
### Fixed
- Notification switch now shows message
- Prod/test server warning message at top of page
## [0.1.6] - 2023.12.17 - b445b1234fbfcf6b37d695373f259aab0eda1118
### Added in 0.1.6
### Added
- Infinite scroll on home page
### Changed in 0.1.6
### Changed
- UI improvements
- Show web-push subscription info
- Icon
## [0.1.5] - 2023.12.09 - 9c36bb509a9bae9bb3306d3bd9eeb144b67aa8ad
### Added in 0.1.5
### Added
- Web push notifications (though not finalized)
- Credentials details page
- See more data without an ID
- Change units of a give
## [0.1.4] - 2023.11.20 - 7311d36726f3667ec4c68f241f91d404273ad4db
### Added in 0.1.4
### Added
- Offer on a project
### Changed in 0.1.4
### Changed
- Automatically set as visible when importing a contact
## [0.1.3] - 2023.11.08 - 910f57ec7d2e50803ae3d04f4b927e0f5219fbde
### Added in 0.1.3
### Added
- Contact name editing
### Changed in 0.1.3
### Changed
- Don't show actions on front page if not registered.
### Removed in 0.1.3
### Removed
- Home page Notiwind test buttons
## [0.1.2] - 2023.11.01 - 7f6c93802911a030a89fe3706e18b5c17151e5bb
### Added in 0.1.2
### Added
- Basics: create ID, record a give, declare a project, search, and get notifications.
+20 -13
View File
@@ -8,6 +8,8 @@ and expand to crowd-fund with time & money, then record and see the impact of co
See [project.task.yaml](project.task.yaml) for current priorities.
(Numbers at the beginning of lines are estimated hours. See [taskyaml.org](https://taskyaml.org/) for details.)
## Setup & Building
Quick start:
@@ -17,17 +19,15 @@ npm install
npm run dev
```
See the test locations for "IMAGE_API_SERVER" or "PARTNER_API_SERVER" below, or use <http://localhost:3000> for local endorser.ch
See the test locations for "IMAGE_API_SERVER" or "PARTNER_API_SERVER" below, or use http://localhost:3000 for local endorser.ch
### Build the test & production app
```bash
```
npm run serve
```
### Lint and fix files
```bash
```
npm run lint
```
@@ -35,6 +35,7 @@ npm run lint
Look below for the "test-all" instructions.
### Compile and minify for test & production
* If there are DB changes: before updating the test server, open browser(s) with current version to test DB migrations.
@@ -51,19 +52,15 @@ Look below for the "test-all" instructions.
* For test, build the app (because test server is not yet set up to build):
```bash
```
TIME_SAFARI_APP_TITLE="TimeSafari_Test" VITE_APP_SERVER=https://test.timesafari.app VITE_BVC_MEETUPS_PROJECT_CLAIM_ID=https://endorser.ch/entity/01HWE8FWHQ1YGP7GFZYYPS272F VITE_DEFAULT_ENDORSER_API_SERVER=https://test-api.endorser.ch VITE_DEFAULT_IMAGE_API_SERVER=https://test-image-api.timesafari.app VITE_DEFAULT_PARTNER_API_SERVER=https://test-partner-api.endorser.ch VITE_PASSKEYS_ENABLED=true npm run build
```
... and transfer to the test server:
... and transfer to the test server: `rsync -azvu -e "ssh -i ~/.ssh/..." dist ubuntutest@test.timesafari.app:time-safari`
```bash
rsync -azvu -e "ssh -i ~/.ssh/..." dist ubuntutest@test.timesafari.app:time-safari
```
(Let's replace that with a .env.development or .env.staging file.)
(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.)
(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:
@@ -79,14 +76,23 @@ TIME_SAFARI_APP_TITLE="TimeSafari_Test" VITE_APP_SERVER=https://test.timesafari.
* Record the new hash in the changelog. Edit package.json to increment version & add "-beta", `npm install`, and commit. Also record what version is on production.
## Tests
See [TESTING.md](test-playwright/TESTING.md) for detailed test instructions.
## Icons
To add an icon, add to main.ts and reference with `fa` element and `icon` attribute with the hyphenated name.
## Other
### Reference Material
@@ -98,6 +104,7 @@ To add an icon, add to main.ts and reference with `fa` element and `icon` attrib
* If you are deploying in a subdirectory, add it to `publicPath` in vue.config.js, eg: `publicPath: "/app/time-tracker/",`
### Kudos
Gifts make the world go 'round!
+1 -1
View File
@@ -9,7 +9,7 @@ android {
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
dependencies {
implementation project(':capacitor-app')
}
-7
View File
@@ -22,13 +22,6 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="timesafari" />
</intent-filter>
</activity>
<provider
-3
View File
@@ -1,6 +1,3 @@
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
include ':capacitor-android'
project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor')
include ':capacitor-app'
project(':capacitor-app').projectDir = new File('../node_modules/@capacitor/app/android')
-12
View File
@@ -8,18 +8,6 @@ const config: CapacitorConfig = {
server: {
cleartext: true,
},
plugins: {
App: {
appUrlOpen: {
handlers: [
{
url: "timesafari://*",
autoVerify: true
}
]
}
}
}
};
export default config;
-91
View File
@@ -1,91 +0,0 @@
# 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"
+1 -16
View File
@@ -12,21 +12,6 @@
<strong>We're sorry but TimeSafari doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<script type="module">
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>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
-9
View File
@@ -45,14 +45,5 @@
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<true/>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>timesafari</string>
</array>
</dict>
</array>
</dict>
</plist>
+1 -1
View File
@@ -11,7 +11,7 @@ install! 'cocoapods', :disable_input_output_paths => true
def capacitor_pods
pod 'Capacitor', :path => '../../node_modules/@capacitor/ios'
pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios'
pod 'CapacitorApp', :path => '../../node_modules/@capacitor/app'
end
target 'App' do
-29
View File
@@ -1,29 +0,0 @@
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();
}
});
+579 -4033
View File
File diff suppressed because it is too large Load Diff
+18 -25
View File
@@ -6,7 +6,7 @@
"name": "TimeSafari Team"
},
"scripts": {
"dev": "vite --config vite.config.dev.mts",
"dev": "vite",
"serve": "vite preview",
"build": "VITE_GIT_HASH=`git log -1 --pretty=format:%h` vite build",
"lint": "eslint --ext .js,.ts,.vue --ignore-path .gitignore src",
@@ -15,25 +15,23 @@
"test-local": "npx playwright test -c playwright.config-local.ts --trace on",
"test-all": "npm run build && npx playwright test -c playwright.config-local.ts --trace on",
"clean:electron": "rimraf dist-electron",
"build:pywebview": "vite build --config vite.config.pywebview.mts",
"build:electron": "npm run clean:electron && vite build --config vite.config.electron.mts && node scripts/build-electron.js",
"build:capacitor": "vite build --config vite.config.capacitor.mts",
"build:web": "vite build --config vite.config.web.mts",
"build:electron": "npm run clean:electron && vite build --mode electron && node scripts/build-electron.js",
"build:capacitor": "vite build --mode capacitor",
"build:web": "vite build",
"electron:dev": "npm run build && electron dist-electron",
"electron:start": "electron dist-electron",
"electron:build-linux": "npm run build:electron && electron-builder --linux AppImage",
"electron:build-linux-deb": "npm run build:electron && electron-builder --linux deb",
"electron:build-linux-prod": "NODE_ENV=production npm run build:electron && electron-builder --linux AppImage",
"electron:build-linux": "electron-builder --linux AppImage",
"electron:build-linux-deb": "electron-builder --linux deb",
"build:electron-prod": "NODE_ENV=production npm run build:electron",
"pywebview:dev": "vite build --config vite.config.pywebview.mts && .venv/bin/python src/pywebview/main.py",
"pywebview:build": "vite build --config vite.config.pywebview.mts && .venv/bin/python src/pywebview/main.py",
"pywebview:package-linux": "vite build --mode pywebview --config vite.config.pywebview.mts && .venv/bin/python -m PyInstaller --name TimeSafari --add-data 'dist:www' src/pywebview/main.py",
"pywebview:package-win": "vite build --mode pywebview --config vite.config.pywebview.mts && .venv/Scripts/python -m PyInstaller --name TimeSafari --add-data 'dist;www' src/pywebview/main.py",
"pywebview:package-mac": "vite build --mode pywebview --config vite.config.pywebview.mts && .venv/bin/python -m PyInstaller --name TimeSafari --add-data 'dist:www' src/pywebview/main.py"
"electron:build-linux-prod": "npm run build:electron-prod && electron-builder --linux AppImage",
"pywebview:dev": "vite build --mode pywebview && .venv/bin/python src/pywebview/main.py",
"pywebview:build": "vite build --mode pywebview && .venv/bin/python src/pywebview/main.py",
"pywebview:package-linux": "vite build --mode pywebview && .venv/bin/python -m PyInstaller --name TimeSafari --add-data 'dist:www' src/pywebview/main.py",
"pywebview:package-win": "vite build --mode pywebview && .venv/Scripts/python -m PyInstaller --name TimeSafari --add-data 'dist;www' src/pywebview/main.py",
"pywebview:package-mac": "vite build --mode pywebview && .venv/bin/python -m PyInstaller --name TimeSafari --add-data 'dist:www' src/pywebview/main.py"
},
"dependencies": {
"@capacitor/android": "^6.2.0",
"@capacitor/app": "^6.0.0",
"@capacitor/cli": "^6.2.0",
"@capacitor/core": "^6.2.0",
"@capacitor/ios": "^6.2.0",
@@ -66,7 +64,7 @@
"cbor-x": "^1.5.9",
"class-transformer": "^0.5.1",
"dexie": "^3.2.7",
"dexie-export-import": "^4.1.4",
"dexie-export-import": "^4.1.1",
"did-jwt": "^7.4.7",
"did-resolver": "^4.1.0",
"ethereum-cryptography": "^2.1.3",
@@ -91,17 +89,16 @@
"reflect-metadata": "^0.1.14",
"register-service-worker": "^1.7.2",
"simple-vue-camera": "^1.1.3",
"stream-browserify": "^3.0.0",
"three": "^0.156.1",
"ua-parser-js": "^1.0.37",
"util": "^0.12.5",
"vue": "^3.5.13",
"vue-axios": "^3.5.2",
"vue-facing-decorator": "^3.0.4",
"vue-picture-cropper": "^0.7.0",
"vue-qrcode-reader": "^5.5.3",
"vue-router": "^4.5.0",
"web-did-resolver": "^2.0.27",
"zod": "^3.24.2"
"web-did-resolver": "^2.0.27"
},
"devDependencies": {
"@playwright/test": "^1.45.2",
@@ -119,14 +116,11 @@
"autoprefixer": "^10.4.19",
"concurrently": "^8.2.2",
"electron": "^33.2.1",
"electron-builder": "^25.1.8",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-vue": "^9.32.0",
"fs-extra": "^11.3.0",
"markdownlint": "^0.37.4",
"markdownlint-cli": "^0.44.0",
"npm-check-updates": "^17.1.13",
"postcss": "^8.4.38",
"prettier": "^3.2.5",
@@ -145,13 +139,12 @@
},
"files": [
"dist-electron/**/*",
"src/electron/**/*",
"main.js"
"src/electron/**/*"
],
"extraResources": [
{
"from": "dist-electron",
"to": "."
"from": "dist-electron/www",
"to": "www"
}
],
"linux": {
+21 -33
View File
@@ -40,10 +40,7 @@
<div
class="flex items-center justify-center w-12 bg-slate-600 text-slate-100"
>
<font-awesome
icon="circle-info"
class="fa-fw fa-xl"
></font-awesome>
<fa icon="circle-info" class="fa-fw fa-xl"></fa>
</div>
<div class="relative w-full pl-4 pr-8 py-2 text-slate-900">
@@ -51,10 +48,10 @@
<p class="text-sm">{{ truncateLongWords(notification.text) }}</p>
<button
class="absolute top-2 right-2 px-0.5 py-0 rounded-full bg-slate-200 text-slate-600"
@click="close(notification.id)"
class="absolute top-2 right-2 px-0.5 py-0 rounded-full bg-slate-200 text-slate-600"
>
<font-awesome icon="xmark" class="fa-fw"></font-awesome>
<fa icon="xmark" class="fa-fw"></fa>
</button>
</div>
</div>
@@ -66,10 +63,7 @@
<div
class="flex items-center justify-center w-12 bg-emerald-600 text-emerald-100"
>
<font-awesome
icon="circle-info"
class="fa-fw fa-xl"
></font-awesome>
<fa icon="circle-info" class="fa-fw fa-xl"></fa>
</div>
<div class="relative w-full pl-4 pr-8 py-2 text-emerald-900">
@@ -77,10 +71,10 @@
<p class="text-sm">{{ truncateLongWords(notification.text) }}</p>
<button
class="absolute top-2 right-2 px-0.5 py-0 rounded-full bg-emerald-200 text-emerald-600"
@click="close(notification.id)"
class="absolute top-2 right-2 px-0.5 py-0 rounded-full bg-emerald-200 text-emerald-600"
>
<font-awesome icon="xmark" class="fa-fw"></font-awesome>
<fa icon="xmark" class="fa-fw"></fa>
</button>
</div>
</div>
@@ -92,10 +86,7 @@
<div
class="flex items-center justify-center w-12 bg-amber-600 text-amber-100"
>
<font-awesome
icon="triangle-exclamation"
class="fa-fw fa-xl"
></font-awesome>
<fa icon="triangle-exclamation" class="fa-fw fa-xl"></fa>
</div>
<div class="relative w-full pl-4 pr-8 py-2 text-amber-900">
@@ -103,10 +94,10 @@
<p class="text-sm">{{ truncateLongWords(notification.text) }}</p>
<button
class="absolute top-2 right-2 px-0.5 py-0 rounded-full bg-amber-200 text-amber-600"
@click="close(notification.id)"
class="absolute top-2 right-2 px-0.5 py-0 rounded-full bg-amber-200 text-amber-600"
>
<font-awesome icon="xmark" class="fa-fw"></font-awesome>
<fa icon="xmark" class="fa-fw"></fa>
</button>
</div>
</div>
@@ -118,10 +109,7 @@
<div
class="flex items-center justify-center w-12 bg-rose-600 text-rose-100"
>
<font-awesome
icon="triangle-exclamation"
class="fa-fw fa-xl"
></font-awesome>
<fa icon="triangle-exclamation" class="fa-fw fa-xl"></fa>
</div>
<div class="relative w-full pl-4 pr-8 py-2 text-rose-900">
@@ -129,10 +117,10 @@
<p class="text-sm">{{ truncateLongWords(notification.text) }}</p>
<button
class="absolute top-2 right-2 px-0.5 py-0 rounded-full bg-rose-200 text-rose-600"
@click="close(notification.id)"
class="absolute top-2 right-2 px-0.5 py-0 rounded-full bg-rose-200 text-rose-600"
>
<font-awesome icon="xmark" class="fa-fw"></font-awesome>
<fa icon="xmark" class="fa-fw"></fa>
</button>
</div>
</div>
@@ -186,11 +174,11 @@
<button
v-if="notification.onYes"
class="block w-full text-center text-md font-bold uppercase bg-blue-600 text-white px-2 py-2 rounded-md mb-2"
@click="
notification.onYes();
close(notification.id);
"
class="block w-full text-center text-md font-bold uppercase bg-blue-600 text-white px-2 py-2 rounded-md mb-2"
>
Yes{{
notification.yesText ? ", " + notification.yesText : ""
@@ -199,12 +187,12 @@
<button
v-if="notification.onNo"
class="block w-full text-center text-md font-bold uppercase bg-yellow-600 text-white px-2 py-2 rounded-md mb-2"
@click="
notification.onNo(stopAsking);
close(notification.id);
stopAsking = false; // reset value
"
class="block w-full text-center text-md font-bold uppercase bg-yellow-600 text-white px-2 py-2 rounded-md mb-2"
>
No{{ notification.noText ? ", " + notification.noText : "" }}
</button>
@@ -221,8 +209,8 @@
<div class="relative ml-2">
<!-- input -->
<input
v-model="stopAsking"
type="checkbox"
v-model="stopAsking"
name="stopAsking"
class="sr-only"
/>
@@ -236,7 +224,6 @@
</label>
<button
class="block w-full text-center text-md font-bold uppercase bg-slate-600 text-white px-2 py-2 rounded-md"
@click="
notification.onCancel
? notification.onCancel(stopAsking)
@@ -244,6 +231,7 @@
close(notification.id);
stopAsking = false; // reset value for next time they open this modal
"
class="block w-full text-center text-md font-bold uppercase bg-slate-600 text-white px-2 py-2 rounded-md"
>
{{ notification.onYes ? "Cancel" : "Close" }}
</button>
@@ -282,8 +270,8 @@
Until I turn it back on
</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="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"
>
Cancel
</button>
@@ -304,17 +292,17 @@
</p>
<button
class="block w-full text-center text-md font-bold uppercase bg-rose-600 text-white px-2 py-2 rounded-md mb-2"
@click="
close(notification.id);
turnOffNotifications(notification);
"
class="block w-full text-center text-md font-bold uppercase bg-rose-600 text-white px-2 py-2 rounded-md mb-2"
>
Turn Off Notification
</button>
<button
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)"
class="block w-full text-center text-md font-bold uppercase bg-slate-600 text-white px-2 py-2 rounded-md"
>
Leave it On
</button>
@@ -327,6 +315,8 @@
</NotificationGroup>
</template>
<style></style>
<script lang="ts">
import { Vue, Component } from "vue-facing-decorator";
import { logConsoleAndDb, retrieveSettingsForActiveAccount } from "./db/index";
@@ -536,5 +526,3 @@ export default class App extends Vue {
}
}
</script>
<style></style>
+334
View File
@@ -0,0 +1,334 @@
<template>
<li>
<!-- Last viewed separator -->
<div
class="border-b border-dashed border-slate-300 text-orange-400 mt-4 mb-6 font-bold text-sm"
v-if="record.jwtId == lastViewedClaimId"
>
<span class="block w-fit mx-auto -mb-2.5 bg-white px-2">
You've already seen all the following
</span>
</div>
<div class="bg-slate-100 rounded-t-md border border-slate-300 p-3 sm:p-4">
<div class="flex items-center gap-2 mb-6">
<img
src="https://a.fsdn.com/con/app/proj/identicons/screenshots/225506.jpg"
class="size-8 object-cover rounded-full"
/>
<div>
<h3 class="font-semibold">
{{
record.giver.known ? record.giver.displayName : "Anonymous Giver"
}}
</h3>
<p class="ms-auto text-xs text-slate-500 italic">
{{ friendlyDate }}
</p>
</div>
</div>
<!-- Record Image -->
<div
v-if="record.image"
class="bg-cover mb-6 -mx-3 sm:-mx-4"
:style="`background-image: url(${record.image});`"
>
<a
class="block bg-slate-100/50 backdrop-blur-md px-6 py-4 cursor-pointer"
@click="$emit('viewImage', record.image)"
>
<img
class="w-full h-auto max-w-lg max-h-96 object-contain mx-auto drop-shadow-md"
:src="record.image"
@load="$emit('cacheImage', record.image)"
alt="Activity image"
/>
</a>
</div>
<div class="relative flex justify-between gap-4 max-w-lg mx-auto mb-5">
<!-- Source -->
<div
class="w-28 sm:w-40 text-center bg-white border border-slate-200 rounded p-2 sm:p-3"
>
<div class="relative w-fit mx-auto">
<template v-if="record.giver.profileImageUrl">
<EntityIcon
:profile-image-url="record.giver.profileImageUrl"
:class="[
!record.providerPlanName
? 'rounded-full size-[3rem] sm:size-[4rem] object-cover'
: 'rounded size-[3rem] sm:size-[4rem] object-cover',
]"
/>
</template>
<template v-else>
<!-- Project Icon -->
<template v-if="record.providerPlanName">
<ProjectIcon
:entityId="record.providerPlanName"
:iconSize="48"
class="rounded size-[3rem] sm:size-[4rem] *:w-full *:h-full"
/>
</template>
<!-- Identicon for DIDs -->
<template v-else-if="record.giver.did">
<img
:src="`https://a.fsdn.com/con/app/proj/identicons/screenshots/225506.jpg`"
class="rounded-full size-[3rem] sm:size-[4rem]"
alt="Identicon"
/>
</template>
<!-- Unknown Person -->
<template v-else>
<fa
icon="person-circle-question"
class="text-slate-300 text-[3rem] sm:text-[4rem]"
/>
</template>
</template>
</div>
<div class="text-xs mt-2 line-clamp-3 sm:line-clamp-2">
<fa
:icon="record.providerPlanName ? 'building' : 'user'"
class="fa-fw text-slate-400"
/>
{{ record.giver.displayName }}
</div>
</div>
<!-- Arrow -->
<div
class="absolute inset-x-28 sm:inset-x-40 mx-2 top-1/2 -translate-y-1/2"
>
<div class="text-sm text-center leading-none font-semibold">
{{ fetchAmount }}
</div>
<div class="flex items-center">
<hr
class="grow border-t-[18px] sm:border-t-[24px] border-slate-300"
/>
<div
class="shrink-0 w-0 h-0 border border-slate-300 border-t-[20px] sm:border-t-[25px] border-t-transparent border-b-[20px] sm:border-b-[25px] border-b-transparent border-s-[27px] sm:border-s-[34px] border-e-0"
></div>
</div>
</div>
<!-- Destination -->
<div
class="w-28 sm:w-40 text-center bg-white border border-slate-200 rounded p-2 sm:p-3"
>
<div class="relative w-fit mx-auto">
<template v-if="record.receiver.profileImageUrl">
<EntityIcon
:profile-image-url="record.receiver.profileImageUrl"
:class="[
!record.recipientProjectName
? 'rounded-full size-[3rem] sm:size-[4rem] object-cover'
: 'rounded size-[3rem] sm:size-[4rem] object-cover',
]"
/>
</template>
<template v-else>
<!-- Project Icon -->
<template v-if="record.recipientProjectName">
<ProjectIcon
:entityId="record.recipientProjectName"
:iconSize="48"
class="rounded size-[3rem] sm:size-[4rem] *:w-full *:h-full"
/>
</template>
<!-- Identicon for DIDs -->
<template v-else-if="record.receiver.did">
<img
:src="`https://a.fsdn.com/con/app/proj/identicons/screenshots/225506.jpg`"
class="rounded-full size-[3rem] sm:size-[4rem]"
alt="Identicon"
/>
</template>
<!-- Unknown Person -->
<template v-else>
<fa
icon="person-circle-question"
class="text-slate-300 text-[3rem] sm:text-[4rem]"
/>
</template>
</template>
</div>
<div class="text-xs mt-2 line-clamp-3 sm:line-clamp-2">
<fa
:icon="record.recipientProjectName ? 'building' : 'user'"
class="fa-fw text-slate-400"
/>
{{ record.receiver.displayName }}
</div>
</div>
</div>
<!-- Description -->
<p class="font-medium">
<a @click="$emit('loadClaim', record.jwtId)" class="cursor-pointer">
{{ description }}
</a>
</p>
<p class="text-sm">{{ subDescription }}</p>
</div>
<div
class="flex items-center gap-2 text-lg bg-slate-300 rounded-b-md px-3 sm:px-4 py-1 sm:py-2"
>
<a @click="$emit('loadClaim', record.jwtId)" class="cursor-pointer">
<fa icon="circle-info" class="fa-fw text-slate-500" />
</a>
</div>
</li>
</template>
<script lang="ts">
import { Component, Prop, Vue } from "vue-facing-decorator";
import { GiveRecordWithContactInfo } from "../types";
import EntityIcon from "./EntityIcon.vue";
import { isGiveClaimType, notifyWhyCannotConfirm } from "../libs/util";
import { containsHiddenDid } from "../libs/endorserServer";
import ProjectIcon from "./ProjectIcon.vue";
@Component({
components: {
EntityIcon,
ProjectIcon,
},
})
export default class ActivityListItem extends Vue {
@Prop() record!: GiveRecordWithContactInfo;
@Prop() lastViewedClaimId?: string;
@Prop() isRegistered!: boolean;
@Prop() activeDid!: string;
@Prop() confirmerIdList?: string[];
get fetchAmount(): string {
const claim =
(this.record.fullClaim as unknown).claim || this.record.fullClaim;
const amount = claim.object?.amountOfThisGood
? this.displayAmount(claim.object.unitCode, claim.object.amountOfThisGood)
: "";
return amount;
}
private formatParticipantInfo(): string {
const { giver, receiver } = this.record;
// Both participants are known contacts
if (giver.known && receiver.known) {
return `${giver.displayName} gave to ${receiver.displayName}`;
}
// Only giver is known
if (giver.known) {
const recipient = this.record.recipientProjectName
? `the project "${this.record.recipientProjectName}"`
: receiver.displayName;
return `${giver.displayName} gave to ${recipient}`;
}
// Only receiver is known
if (receiver.known) {
const provider = this.record.providerPlanName
? `the project "${this.record.providerPlanName}"`
: giver.displayName;
return `${receiver.displayName} received from ${provider}`;
}
// Neither is known
return this.formatUnknownParticipants();
}
private formatUnknownParticipants(): string {
const { giver, receiver, providerPlanName, recipientProjectName } =
this.record;
if (providerPlanName || recipientProjectName) {
const from = providerPlanName
? `the project "${providerPlanName}"`
: giver.displayName;
const to = recipientProjectName
? `the project "${recipientProjectName}"`
: receiver.displayName;
return `from ${from} to ${to}`;
}
return giver.displayName === receiver.displayName
? `between two who are ${giver.displayName}`
: `from ${giver.displayName} to ${receiver.displayName}`;
}
get description(): string {
const claim =
(this.record.fullClaim as unknown).claim || this.record.fullClaim;
if (!claim.description) {
return "something not described";
}
return `${claim.description}`;
}
get subDescription(): string {
const participants = this.formatParticipantInfo();
return `${participants}`;
}
private displayAmount(code: string, amt: number) {
return `${amt} ${this.currencyShortWordForCode(code, amt === 1)}`;
}
private currencyShortWordForCode(unitCode: string, single: boolean) {
return unitCode === "HUR" ? (single ? "hour" : "hours") : unitCode;
}
get formattedTimestamp() {
// Add your timestamp formatting logic here
return this.record.timestamp;
}
get canConfirm(): boolean {
if (!this.isRegistered) return false;
if (!isGiveClaimType(this.record.fullClaim?.["@type"])) return false;
if (this.confirmerIdList?.includes(this.activeDid)) return false;
if (this.record.issuerDid === this.activeDid) return false;
if (containsHiddenDid(this.record.fullClaim)) return false;
return true;
}
handleConfirmClick() {
if (!this.canConfirm) {
notifyWhyCannotConfirm(
this.$notify,
this.isRegistered,
this.record.fullClaim?.["@type"],
this.record,
this.activeDid,
this.confirmerIdList,
);
return;
}
this.$emit("confirmClaim", this.record);
}
get friendlyDate(): string {
const date = new Date(this.record.issuedAt);
return date.toLocaleDateString(undefined, {
year: "numeric",
month: "short",
day: "numeric",
});
}
}
</script>
+4 -4
View File
@@ -29,29 +29,29 @@
<p class="text-sm mb-2">{{ text }}</p>
<button
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)"
class="block w-full text-center text-md font-bold capitalize bg-blue-800 text-white px-2 py-2 rounded-md mb-2"
>
{{ option1Text }}
</button>
<button
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)"
class="block w-full text-center text-md font-bold capitalize bg-blue-700 text-white px-2 py-2 rounded-md mb-2"
>
{{ option2Text }}
</button>
<button
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)"
class="block w-full text-center text-md font-bold capitalize bg-blue-600 text-white px-2 py-2 rounded-md mb-2"
>
{{ option3Text }}
</button>
<button
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)"
class="block w-full text-center text-md font-bold capitalize bg-slate-600 text-white px-2 py-2 rounded-md"
>
Cancel
</button>
+1 -1
View File
@@ -6,10 +6,10 @@
{{ message }}
Note that their name is only stored on this device.
<input
v-model="newText"
type="text"
placeholder="Name"
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
v-model="newText"
/>
<div class="mt-8">
+1 -1
View File
@@ -1,5 +1,5 @@
<template>
<div class="w-fit" v-html="generateIcon()"></div>
<div v-html="generateIcon()" class="w-fit"></div>
</template>
<script lang="ts">
import { createAvatar, StyleOptions } from "@dicebear/core";
+3 -4
View File
@@ -16,8 +16,8 @@
<div class="relative ml-2">
<!-- input -->
<input
v-model="hasVisibleDid"
type="checkbox"
v-model="hasVisibleDid"
name="toggleFilterFromMyContacts"
class="sr-only"
/>
@@ -46,8 +46,8 @@
<div v-if="hasSearchBox" class="relative ml-2">
<!-- input -->
<input
v-model="isNearby"
type="checkbox"
v-model="isNearby"
name="toggleFilterNearby"
class="sr-only"
/>
@@ -98,7 +98,7 @@ import {
LRectangle,
LTileLayer,
} from "@vue-leaflet/vue-leaflet";
import { Router } from "vue-router";
import { MASTER_SETTINGS_KEY } from "../db/tables/settings";
import { db, retrieveSettingsForActiveAccount } from "../db/index";
@@ -111,7 +111,6 @@ import { db, retrieveSettingsForActiveAccount } from "../db/index";
},
})
export default class FeedFilters extends Vue {
$router!: Router;
onCloseIfChanged = () => {};
hasSearchBox = false;
hasVisibleDid = false;
+5 -5
View File
@@ -5,10 +5,10 @@
{{ customTitle }}
</h1>
<input
v-model="description"
type="text"
class="block w-full rounded border border-slate-400 mb-2 px-3 py-2"
:placeholder="prompt || 'What was given?'"
v-model="description"
/>
<div class="flex flex-row justify-center">
<span
@@ -21,19 +21,19 @@
class="border border-r-0 border-slate-400 bg-slate-200 px-4 py-2"
@click="amountInput === '0' ? null : decrement()"
>
<font-awesome icon="chevron-left" />
<fa icon="chevron-left" />
</div>
<input
id="inputGivenAmount"
v-model="amountInput"
type="number"
class="border border-r-0 border-slate-400 px-2 py-2 text-center w-20"
v-model="amountInput"
/>
<div
class="rounded-r border border-slate-400 bg-slate-200 px-4 py-2"
@click="increment()"
>
<font-awesome icon="chevron-right" />
<fa icon="chevron-right" />
</div>
</div>
<div class="mt-4 flex justify-center">
@@ -62,7 +62,7 @@
</div>
<p class="text-center mb-2 mt-6 italic">
Sign & Send to publish to the world
<font-awesome
<fa
icon="circle-info"
class="pl-2 text-blue-500 cursor-pointer"
@click="explainData()"
+5 -6
View File
@@ -7,7 +7,7 @@
class="text-lg text-center p-2 leading-none absolute right-0 -top-1"
@click="cancel"
>
<font-awesome icon="xmark" class="w-[1em]"></font-awesome>
<fa icon="xmark" class="w-[1em]"></fa>
</div>
</h1>
<span class="mt-2 flex justify-between">
@@ -16,7 +16,7 @@
class="rounded-l border border-slate-400 bg-slate-200 px-4 py-2 flex"
@click="prevIdea()"
>
<font-awesome icon="chevron-left" class="m-auto" />
<fa icon="chevron-left" class="m-auto" />
</span>
<div class="m-2">
@@ -45,7 +45,7 @@
class="text-center bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mt-4"
@click="nextIdeaPastContacts()"
>
Skip Contacts <font-awesome icon="forward" />
Skip Contacts <fa icon="forward" />
</button>
</span>
</span>
@@ -57,7 +57,7 @@
class="rounded-r border border-slate-400 bg-slate-200 px-4 py-2 flex"
@click="nextIdea()"
>
<font-awesome icon="chevron-right" class="m-auto" />
<fa icon="chevron-right" class="m-auto" />
</span>
</span>
<button
@@ -82,7 +82,6 @@ import { GiverReceiverInputInfo } from "../libs/util";
@Component
export default class GivenPrompts extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
$router!: Router;
CATEGORY_CONTACTS = 1;
CATEGORY_IDEAS = 0;
@@ -146,7 +145,7 @@ export default class GivenPrompts extends Vue {
// proceed with logic but don't change values (just in case some actions are added later)
this.visible = false;
if (this.currentCategory === this.CATEGORY_IDEAS) {
this.$router.push({
(this.$router as Router).push({
name: "contact-gift",
query: {
prompt: this.IDEAS[this.currentIdeaIndex],
+6 -9
View File
@@ -7,8 +7,8 @@
<!-- Header -->
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-bold capitalize">{{ roleName }} Details</h2>
<button class="text-gray-500 hover:text-gray-700" @click="close">
<font-awesome icon="times" />
<button @click="close" class="text-gray-500 hover:text-gray-700">
<fa icon="times" />
</button>
</div>
@@ -53,10 +53,7 @@
target="_blank"
class="text-blue-500"
>
<font-awesome
icon="arrow-up-right-from-square"
class="fa-fw"
/>
<fa icon="arrow-up-right-from-square" class="fa-fw" />
</a>
</span>
</span>
@@ -69,7 +66,7 @@
<div class="mt-4">
<span v-if="canShare">
If you'd like an introduction,
<a class="text-blue-500" @click="onClickShareClaim()"
<a @click="onClickShareClaim()" class="text-blue-500"
>click here to share the information with them and ask if they'll
tell you more about the {{ roleName }}.</a
>
@@ -77,8 +74,8 @@
<span v-else>
If you'd like an introduction,
<a
class="text-blue-500"
@click="copyToClipboard('A link to this page', windowLocation)"
class="text-blue-500"
>click here to copy this page, paste it into a message, and ask if
they'll tell you more about the {{ roleName }}.</a
>
@@ -89,8 +86,8 @@
<!-- Footer -->
<div class="flex justify-end">
<button
class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600"
@click="close"
class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600"
>
Close
</button>
+5 -9
View File
@@ -12,14 +12,14 @@
class="text-lg text-center px-2 py-0.5 leading-none absolute right-0 top-0 text-white"
@click="close()"
>
<font-awesome icon="xmark" class="w-[1em]"></font-awesome>
<fa icon="xmark" class="w-[1em]"></fa>
</div>
</div>
<div>
<div class="text-center mt-8">
<div>
<font-awesome
<fa
icon="camera"
class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-2 rounded-md"
@click="openPhotoDialog()"
@@ -31,21 +31,17 @@
<div class="mt-4">
<span class="mt-2">
... or paste a URL:
<input v-model="imageUrl" type="text" class="border-2" />
<input type="text" v-model="imageUrl" class="border-2" />
</span>
<span class="ml-2">
<font-awesome
<fa
v-if="imageUrl"
icon="check"
class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-2 rounded-md cursor-pointer"
@click="acceptUrl"
/>
<!-- so that there's no shifting when it becomes visible -->
<font-awesome
v-else
icon="check"
class="text-white bg-white px-2 py-2"
/>
<fa v-else icon="check" class="text-white bg-white px-2 py-2" />
</span>
</div>
</div>
+3 -3
View File
@@ -8,7 +8,7 @@
class="text-white text-2xl p-2 rounded-full hover:bg-white/10"
@click="close"
>
<font-awesome icon="xmark" />
<fa icon="xmark" />
</button>
<!-- Mobile share button -->
@@ -17,7 +17,7 @@
class="text-white text-xl p-2 rounded-full hover:bg-white/10"
@click="handleShare"
>
<font-awesome icon="ellipsis" />
<fa icon="ellipsis" />
</button>
</div>
@@ -27,8 +27,8 @@
<img
:src="imageUrl"
class="max-h-[calc(100vh-5rem)] w-full h-full object-contain"
alt="expanded shared content"
@click.stop
alt="expanded shared content"
/>
</div>
</div>
+2 -2
View File
@@ -8,18 +8,18 @@
If you want to store your own way, the invitation ID is:
{{ inviteIdentifier }}
<input
v-model="text"
type="text"
placeholder="Notes"
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
v-model="text"
/>
<!-- Add date selection element -->
Expiration
<input
v-model="expiresAt"
type="date"
class="block rounded border border-slate-400 mb-4 px-3 py-2"
v-model="expiresAt"
/>
<div class="mt-8">
+17 -17
View File
@@ -5,7 +5,7 @@
v-if="isLoading"
class="mt-16 text-center text-4xl bg-slate-400 text-white w-14 py-2.5 rounded-full mx-auto"
>
<font-awesome icon="spinner" class="fa-spin-pulse" />
<fa icon="spinner" class="fa-spin-pulse" />
</div>
<!-- Members List -->
@@ -33,13 +33,13 @@
<span
class="mx-2 min-w-[24px] min-h-[24px] w-6 h-6 flex items-center justify-center rounded-full bg-blue-100 text-blue-600"
>
<font-awesome icon="plus" class="text-sm" />
<fa icon="plus" class="text-sm" />
</span>
/
<span
class="mx-2 min-w-[24px] min-h-[24px] w-6 h-6 flex items-center justify-center rounded-full bg-blue-100 text-blue-600"
>
<font-awesome icon="minus" class="text-sm" />
<fa icon="minus" class="text-sm" />
</span>
to add/remove them to/from the meeting.
</span>
@@ -54,7 +54,7 @@
<span
class="mx-2 w-8 h-8 flex items-center justify-center rounded-full bg-green-100 text-green-600"
>
<font-awesome icon="circle-user" class="text-xl" />
<fa icon="circle-user" class="text-xl" />
</span>
to add them to your contacts.
</span>
@@ -63,11 +63,11 @@
<div class="flex justify-center">
<!-- always have at least one refresh button even without members in case the organizer changes the password -->
<button
@click="fetchMembers"
class="w-8 h-8 flex items-center justify-center rounded-full bg-blue-100 text-blue-600 hover:bg-blue-200 hover:text-blue-800 transition-colors"
title="Refresh members list"
@click="fetchMembers"
>
<font-awesome icon="rotate" :class="{ 'fa-spin': isLoading }" />
<fa icon="rotate" :class="{ 'fa-spin': isLoading }" />
</button>
</div>
<div
@@ -83,24 +83,24 @@
class="flex justify-end"
>
<button
@click="addAsContact(member)"
class="ml-2 w-8 h-8 flex items-center justify-center rounded-full bg-green-100 text-green-600 hover:bg-green-200 hover:text-green-800 transition-colors"
title="Add as contact"
@click="addAsContact(member)"
>
<font-awesome icon="circle-user" class="text-xl" />
<fa icon="circle-user" class="text-xl" />
</button>
</div>
<button
v-if="member.did !== activeDid"
class="ml-2 mb-2 w-6 h-6 flex items-center justify-center rounded-full bg-slate-100 text-slate-500 hover:bg-slate-200 hover:text-slate-800 transition-colors"
title="Contact info"
@click="
informAboutAddingContact(
getContactFor(member.did) !== undefined,
)
"
class="ml-2 mb-2 w-6 h-6 flex items-center justify-center rounded-full bg-slate-100 text-slate-500 hover:bg-slate-200 hover:text-slate-800 transition-colors"
title="Contact info"
>
<font-awesome icon="circle-info" class="text-base" />
<fa icon="circle-info" class="text-base" />
</button>
</div>
<div class="flex">
@@ -111,23 +111,23 @@
class="flex items-center"
>
<button
@click="checkWhetherContactBeforeAdmitting(member)"
class="mr-2 w-6 h-6 flex items-center justify-center rounded-full bg-blue-100 text-blue-600 hover:bg-blue-200 hover:text-blue-800 transition-colors"
:title="
member.member.admitted ? 'Remove member' : 'Admit member'
"
@click="checkWhetherContactBeforeAdmitting(member)"
>
<font-awesome
<fa
:icon="member.member.admitted ? 'minus' : 'plus'"
class="text-sm"
/>
</button>
<button
@click="informAboutAdmission()"
class="mr-2 mb-2 w-6 h-6 flex items-center justify-center rounded-full bg-slate-100 text-slate-500 hover:bg-slate-200 hover:text-slate-800 transition-colors"
title="Admission info"
@click="informAboutAdmission()"
>
<font-awesome icon="circle-info" class="text-base" />
<fa icon="circle-info" class="text-base" />
</button>
</span>
</div>
@@ -138,11 +138,11 @@
</div>
<div v-if="membersToShow().length > 0" class="flex justify-center mt-4">
<button
@click="fetchMembers"
class="w-8 h-8 flex items-center justify-center rounded-full bg-blue-100 text-blue-600 hover:bg-blue-200 hover:text-blue-800 transition-colors"
title="Refresh members list"
@click="fetchMembers"
>
<font-awesome icon="rotate" :class="{ 'fa-spin': isLoading }" />
<fa icon="rotate" :class="{ 'fa-spin': isLoading }" />
</button>
</div>
+5 -5
View File
@@ -3,11 +3,11 @@
<div class="dialog">
<h1 class="text-xl font-bold text-center mb-4">Offer Help</h1>
<input
v-model="description"
type="text"
data-testId="inputDescription"
class="block w-full rounded border border-slate-400 mb-2 px-3 py-2"
placeholder="Description of what is offered"
v-model="description"
/>
<div class="flex flex-row mt-2">
<span
@@ -17,23 +17,23 @@
{{ libsUtil.UNIT_SHORT[amountUnitCode] }}
</span>
<div
v-if="amountInput !== '0'"
class="border border-r-0 border-slate-400 bg-slate-200 px-4 py-2"
@click="decrement()"
v-if="amountInput !== '0'"
>
<font-awesome icon="chevron-left" />
<fa icon="chevron-left" />
</div>
<input
v-model="amountInput"
data-testId="inputOfferAmount"
type="number"
class="w-full border border-r-0 border-slate-400 px-2 py-2 text-center"
v-model="amountInput"
/>
<div
class="rounded-r border border-slate-400 bg-slate-200 px-4 py-2"
@click="increment()"
>
<font-awesome icon="chevron-right" />
<fa icon="chevron-right" />
</div>
</div>
<div class="mt-4 flex justify-center">
+10 -11
View File
@@ -10,7 +10,7 @@
class="text-lg text-center leading-none absolute right-0 -top-1"
@click="onClickClose(true)"
>
<font-awesome icon="xmark" class="w-[1em]" />
<fa icon="xmark" class="w-[1em]" />
</div>
</h1>
@@ -21,7 +21,7 @@
</span>
click on the
<span class="bg-green-600 text-white rounded-full">
<font-awesome icon="plus" class="fa-fw" />
<fa icon="plus" class="fa-fw" />
</span>
button to express your appreciation for... whatever -- maybe thanks for
showing you all these fascinating stories of
@@ -40,7 +40,7 @@
<p class="mt-4 flex items-center">
The
<font-awesome
<fa
icon="house-chimney"
class="ml-1 mr-1 text-lg text-white bg-slate-400 px-2 py-2 rounded"
/>
@@ -84,7 +84,7 @@
class="text-lg text-center leading-none absolute right-0 -top-1"
@click="onClickClose(true)"
>
<font-awesome icon="xmark" class="w-[1em]" />
<fa icon="xmark" class="w-[1em]" />
</div>
</h1>
@@ -106,7 +106,7 @@
<p class="mt-4 flex items-center">
The
<font-awesome
<fa
icon="magnifying-glass"
class="ml-1 mr-1 text-lg text-white bg-slate-400 px-2 py-2 rounded"
/>
@@ -141,14 +141,14 @@
class="text-lg text-center leading-none absolute right-0 -top-1"
@click="onClickClose(true)"
>
<font-awesome icon="xmark" class="w-[1em]" />
<fa icon="xmark" class="w-[1em]" />
</div>
</h1>
<p class="relative">
Now you can take a turn: click on the
<span class="bg-green-600 text-white rounded-full">
<font-awesome icon="plus" class="fa-fw" />
<fa icon="plus" class="fa-fw" />
</span>
button to throw out projects of your own... anything you'd like to see
happen. If your first idea doesn't catch anyone, try, try again... and
@@ -157,7 +157,7 @@
<p class="mt-4 flex items-center">
The
<font-awesome
<fa
icon="hand"
class="ml-1 mr-1 text-lg text-white bg-slate-400 px-2 py-2 rounded"
/>
@@ -168,7 +168,7 @@
By the way, one good way to get to know your neighbors and their
interests is to offer time directly to them. You can do this on the
contacts screen
<font-awesome icon="users" class="text-slate-500" />
<fa icon="users" class="text-slate-500" />
which is a great way to get to know a neighbor's interests.
</p>
@@ -219,7 +219,6 @@ import { OnboardPage } from "../libs/util";
})
export default class OnboardingDialog extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
$router!: Router;
activeDid = "";
firstContactName = null;
@@ -255,7 +254,7 @@ export default class OnboardingDialog extends Vue {
finishedOnboarding: true,
});
if (goHome) {
this.$router.push({ name: "home" });
(this.$router as Router).push({ name: "home" });
}
}
}
+13 -13
View File
@@ -15,12 +15,12 @@
class="text-lg text-center px-2 py-0.5 leading-none absolute right-0 top-0 text-white"
@click="close()"
>
<font-awesome icon="xmark" class="w-[1em]"></font-awesome>
<fa icon="xmark" class="w-[1em]"></fa>
</div>
</div>
<div v-if="uploading" class="flex justify-center">
<font-awesome
<fa
icon="spinner"
class="fa-spin fa-3x text-center block px-12 py-12"
/>
@@ -28,7 +28,7 @@
<div v-else-if="blob">
<div v-if="crop">
<VuePictureCropper
:box-style="{
:boxStyle="{
backgroundColor: '#f8f8f8',
margin: 'auto',
}"
@@ -56,8 +56,8 @@
</div>
<div class="absolute bottom-[1rem] left-[1rem] px-2 py-1">
<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-white py-1 px-2 rounded-md"
@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"
>
<span>Upload</span>
</button>
@@ -67,8 +67,8 @@
class="absolute bottom-[1rem] right-[1rem] px-2 py-1"
>
<button
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"
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"
>
<span>Retry</span>
</button>
@@ -81,37 +81,37 @@
:resolution="{ width: 375, height: 812 }"
-->
<camera
ref="camera"
facing-mode="environment"
facingMode="environment"
autoplay
ref="camera"
@started="cameraStarted()"
>
<div
class="absolute portrait:bottom-0 portrait:left-0 portrait:right-0 portrait:pb-2 landscape:right-0 landscape:top-0 landscape:bottom-0 landscape:pr-4 flex landscape:flex-row justify-center items-center"
>
<button
class="bg-blue-500 hover:bg-blue-700 text-white font-bold p-3 rounded-full text-2xl leading-none"
@click="takeImage()"
class="bg-blue-500 hover:bg-blue-700 text-white font-bold p-3 rounded-full text-2xl leading-none"
>
<font-awesome icon="camera" class="w-[1em]"></font-awesome>
<fa icon="camera" class="w-[1em]"></fa>
</button>
</div>
<div
class="absolute portrait:bottom-2 portrait:right-16 landscape:right-0 landscape:bottom-16 landscape:pr-4 flex justify-center items-center"
>
<button
class="bg-blue-500 hover:bg-blue-700 text-white font-bold p-3 rounded-full text-2xl leading-none"
@click="swapMirrorClass()"
class="bg-blue-500 hover:bg-blue-700 text-white font-bold p-3 rounded-full text-2xl leading-none"
>
<font-awesome icon="left-right" class="w-[1em]"></font-awesome>
<fa icon="left-right" class="w-[1em]"></fa>
</button>
</div>
<div v-if="numDevices > 1" class="absolute bottom-2 right-4">
<button
class="bg-blue-500 hover:bg-blue-700 text-white font-bold p-3 rounded-full text-2xl leading-none"
@click="switchCamera()"
class="bg-blue-500 hover:bg-blue-700 text-white font-bold p-3 rounded-full text-2xl leading-none"
>
<font-awesome icon="rotate" class="w-[1em]"></font-awesome>
<fa icon="rotate" class="w-[1em]"></fa>
</button>
</div>
</camera>
+2 -2
View File
@@ -5,12 +5,12 @@
target="_blank"
class="h-full w-full object-contain"
>
<div class="h-full w-full object-contain" v-html="generateIdenticon()" />
<div v-html="generateIdenticon()" class="h-full w-full object-contain" />
</a>
<div
v-else
class="h-full w-full object-contain"
v-html="generateIdenticon()"
class="h-full w-full object-contain"
/>
</template>
<script lang="ts">
+8 -10
View File
@@ -25,7 +25,7 @@
</p>
<p v-else class="text-lg mb-4">
Waiting for system initialization, which may take up to 5 seconds...
<font-awesome icon="spinner" spin />
<fa icon="spinner" spin />
</p>
<div v-if="serviceWorkerReady && vapidKey">
@@ -54,25 +54,23 @@
<span class="flex flex-row justify-center">
<span class="mt-2">... at: </span>
<input
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"
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"
/>
<input
v-model="minuteInput"
type="number"
class="border border-slate-400 mt-2 px-2 py-2 text-center w-20"
@change="checkMinuteInput"
class="border border-slate-400 mt-2 px-2 py-2 text-center w-20"
v-model="minuteInput"
/>
<span
class="rounded-r border border-slate-400 bg-slate-200 text-center text-blue-500 mt-2 px-2 py-2 w-20"
@click="hourAm = !hourAm"
>
<span v-if="hourAm">
AM <font-awesome icon="chevron-down" />
</span>
<span v-else> PM <font-awesome icon="chevron-up" /> </span>
<span v-if="hourAm"> AM <fa icon="chevron-down" /> </span>
<span v-else> PM <fa icon="chevron-up" /> </span>
</span>
</span>
</div>
@@ -88,8 +86,8 @@
</div>
<button
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()"
class="block w-full text-center text-md font-bold uppercase bg-slate-600 text-white mt-4 px-2 py-2 rounded-md"
>
No, Not Now
</button>
+5 -5
View File
@@ -13,7 +13,7 @@
>
<router-link :to="{ name: 'home' }" class="block text-center py-2 px-1">
<div class="flex flex-col items-center">
<font-awesome icon="house-chimney" class="fa-fw" />
<fa icon="house-chimney" class="fa-fw" />
<span class="text-xs mt-1">feed</span>
</div>
</router-link>
@@ -32,7 +32,7 @@
class="block text-center py-2 px-1"
>
<div class="flex flex-col items-center">
<font-awesome icon="magnifying-glass" class="fa-fw" />
<fa icon="magnifying-glass" class="fa-fw" />
<span class="text-xs mt-1">search</span>
</div>
</router-link>
@@ -51,7 +51,7 @@
class="block text-center py-2 px-1"
>
<div class="flex flex-col items-center">
<font-awesome icon="hand" class="fa-fw" />
<fa icon="hand" class="fa-fw" />
<span class="text-xs mt-1">your work</span>
</div>
</router-link>
@@ -70,7 +70,7 @@
class="block text-center py-2 px-1"
>
<div class="flex flex-col items-center">
<font-awesome icon="users" class="fa-fw" />
<fa icon="users" class="fa-fw" />
<span class="text-xs mt-1">contacts</span>
</div>
</router-link>
@@ -89,7 +89,7 @@
class="block text-center py-2 px-1"
>
<div class="flex flex-col items-center">
<font-awesome icon="circle-user" class="fa-fw" />
<fa icon="circle-user" class="fa-fw" />
<!--
We used to say "account", so we'll keep that in the code,
but it isn't accurate because we don't hold anything for them.
+1 -1
View File
@@ -6,10 +6,10 @@
{{ sharingExplanation }}
<input
v-model="givenName"
type="text"
placeholder="Name"
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
v-model="givenName"
/>
<div class="mt-8">
+1 -1
View File
@@ -20,7 +20,7 @@ export type Settings = {
// active Decentralized ID
activeDid?: string; // only used in the MASTER_SETTINGS_KEY entry
apiServer: string; // API server URL
apiServer?: string; // API server URL
filterFeedByNearby?: boolean; // filter by nearby
filterFeedByVisible?: boolean; // filter by visible users ie. anyone not hidden
-58
View File
@@ -1,58 +0,0 @@
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
View File
@@ -1,68 +0,0 @@
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
View File
@@ -1,36 +0,0 @@
// 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;
}
-13
View File
@@ -1,13 +0,0 @@
/**
* @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
View File
@@ -1,7 +0,0 @@
export * from "./claims";
export * from "./claims-result";
export * from "./common";
export * from "./limits";
export * from "./records";
export * from "./user";
export * from "./deepLinks";
-14
View File
@@ -1,14 +0,0 @@
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
View File
@@ -1,92 +0,0 @@
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
View File
@@ -1,8 +0,0 @@
export interface UserInfo {
did: string;
name: string;
publicEncKey: string;
registered: boolean;
profileImageUrl?: string;
nextPublicEncKeyHash?: string;
}
-59
View File
@@ -1,59 +0,0 @@
// 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
View File
@@ -1,168 +0,0 @@
/**
* @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";
+304 -162
View File
@@ -1,22 +1,4 @@
/**
* @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 { Axios, AxiosRequestConfig, AxiosResponse } from "axios";
import { Buffer } from "buffer";
import { sha256 } from "ethereum-cryptography/sha256";
import { LRUCache } from "lru-cache";
@@ -35,62 +17,63 @@ import {
retrieveAccountMetadata,
retrieveFullyDecryptedAccount,
getPasskeyExpirationSeconds,
GiverReceiverInputInfo,
} from "../libs/util";
import { createEndorserJwtForKey, KeyMeta } from "../libs/crypto/vc";
import {
GiveVerifiableCredential,
OfferVerifiableCredential,
RegisterVerifiableCredential,
GenericVerifiableCredential,
GenericCredWrapper,
PlanSummaryRecord,
UserInfo,
CreateAndSubmitClaimResult,
} from "../interfaces";
/**
* Standard context for schema.org data
* @constant {string}
*/
export const SCHEMA_ORG_CONTEXT = "https://schema.org";
/**
* Service identifier for RegisterAction claims
* @constant {string}
*/
// the object in RegisterAction claims
export const SERVICE_ID = "endorser.ch";
/**
* Header line format for contacts exported via Endorser Mobile
* @constant {string}
*/
// the header line for contacts exported via Endorser Mobile
export const CONTACT_CSV_HEADER = "name,did,pubKeyBase64,seesMe,registered";
/**
* URL path suffix for contact confirmation before import
* @constant {string}
*/
// the suffix for the contact URL in this app where they are confirmed before import
export const CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI = "/contact-import/";
/**
* URL path suffix for the contact URL in this app where a single one gets imported automatically
* @constant {string}
*/
// the suffix for the contact URL in this app where a single one gets imported automatically
export const CONTACT_IMPORT_ONE_URL_PATH_TIME_SAFARI = "/contacts?contactJwt=";
/**
* URL path suffix for the old contact URL -- deprecated Jan 2025, though "endorser.ch/contact?jwt=" shows data on endorser.ch server
* @constant {string}
*/
// the suffix for the old contact URL -- deprecated Jan 2025, though "endorser.ch/contact?jwt=" shows data on endorser.ch server
export const CONTACT_URL_PATH_ENDORSER_CH_OLD = "/contact?jwt=";
/**
* The prefix for handle IDs, the permanent ID for claims on Endorser
* @constant {string}
*/
// 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
export const ENDORSER_CH_HANDLE_PREFIX = "https://endorser.ch/entity/";
export interface AgreeVerifiableCredential {
"@context": string;
"@type": string;
// "any" because arbitrary objects can be subject of agreement
// eslint-disable-next-line @typescript-eslint/no-explicit-any
object: Record<string, any>;
}
export interface GiverOutputInfo {
action: string;
giver?: GiverReceiverInputInfo;
description?: string;
amount?: number;
unitCode?: string;
}
export interface ClaimResult {
success: { claimId: string; handleId: string };
error: { code: string; message: string };
}
// similar to VerifiableCredentialSubject... maybe rename this
export interface GenericVerifiableCredential {
"@context"?: string; // optional when embedded, eg. in an Agree
"@type": string;
[key: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any
}
export interface GenericCredWrapper<T extends GenericVerifiableCredential> {
claim: T;
claimType?: string;
handleId: string;
id: string;
issuedAt: string;
issuer: string;
publicUrls?: Record<string, string>; // only for IDs that want to be public
}
export const BLANK_GENERIC_SERVER_RECORD: GenericCredWrapper<GenericVerifiableCredential> =
{
claim: { "@type": "" },
@@ -100,98 +83,266 @@ export const BLANK_GENERIC_SERVER_RECORD: GenericCredWrapper<GenericVerifiableCr
issuer: "",
};
// a summary record; the VC is found the fullClaim field
export interface GiveSummaryRecord {
agentDid: string;
amount: number;
amountConfirmed: number;
description: string;
fullClaim: GiveVerifiableCredential;
fulfillsHandleId: string;
fulfillsPlanHandleId?: string;
fulfillsType?: string;
handleId: string;
issuedAt: string;
issuerDid: string;
jwtId: string;
providerPlanHandleId?: string;
recipientDid: string;
unit: string;
}
// a summary record; the VC is found the fullClaim field
export interface OfferSummaryRecord {
amount: number;
amountGiven: number;
amountGivenConfirmed: number;
fullClaim: OfferVerifiableCredential;
fulfillsPlanHandleId: string;
handleId: string;
issuerDid: string;
jwtId: string;
nonAmountGivenConfirmed: number;
objectDescription: string;
offeredByDid: string;
recipientDid: string;
requirementsMet: boolean;
unit: string;
validThrough: string;
}
export interface OfferToPlanSummaryRecord extends OfferSummaryRecord {
planName: string;
}
// a summary record; the VC is not currently part of this record
export interface PlanSummaryRecord {
agentDid?: string; // optional, if the issuer wants someone else to manage as well
description: string;
endTime?: string;
fulfillsPlanHandleId: string;
handleId: string;
image?: string;
issuerDid: string;
locLat?: number;
locLon?: number;
name?: string;
startTime?: string;
url?: string;
}
// Note that previous VCs may have additional fields.
// https://endorser.ch/doc/html/transactions.html#id4
export interface GiveVerifiableCredential extends GenericVerifiableCredential {
"@context"?: string; // optional when embedded, eg. in an Agree
"@type": "GiveAction";
agent?: { identifier: string };
description?: string;
fulfills?: { "@type": string; identifier?: string; lastClaimId?: string }[];
identifier?: string;
image?: string;
object?: { amountOfThisGood: number; unitCode: string };
provider?: GenericVerifiableCredential; // typically @type & identifier
recipient?: { identifier: string };
}
// Note that previous VCs may have additional fields.
// https://endorser.ch/doc/html/transactions.html#id8
export interface OfferVerifiableCredential extends GenericVerifiableCredential {
"@context"?: string; // optional when embedded... though it doesn't make sense to agree to an offer
"@type": "Offer";
description?: string; // conditions for the offer
includesObject?: { amountOfThisGood: number; unitCode: string };
itemOffered?: {
description?: string; // description of the item
isPartOf?: { identifier?: string; lastClaimId?: string; "@type"?: string };
};
offeredBy?: { identifier: string };
recipient?: { identifier: string };
validThrough?: string;
}
// Note that previous VCs may have additional fields.
// https://endorser.ch/doc/html/transactions.html#id7
export interface PlanVerifiableCredential extends GenericVerifiableCredential {
"@context": "https://schema.org";
"@type": "PlanAction";
name: string;
agent?: { identifier: string };
description?: string;
identifier?: string;
lastClaimId?: string;
location?: {
geo: { "@type": "GeoCoordinates"; latitude: number; longitude: number };
};
}
/**
* Represents data about a project
*
* @deprecated
* (Maybe we should use PlanSummaryRecord instead, either by adding rowId or by iterating with jwtId.)
**/
export interface PlanData {
/**
* Description of the project
**/
description: string;
/**
* URL referencing information about the project
**/
handleId: string;
image?: string;
/**
* The DID of the issuer
*/
issuerDid: string;
/**
* Name of the project
**/
name: string;
/**
* The identifier of the project record -- different from jwtId
* (Maybe we should use the jwtId to iterate through the records instead.)
**/
rowId?: string;
}
export interface EndorserRateLimits {
doneClaimsThisWeek: string;
doneRegistrationsThisMonth: string;
maxClaimsPerWeek: string;
maxRegistrationsPerMonth: string;
nextMonthBeginDateTime: string;
nextWeekBeginDateTime: string;
}
export interface ImageRateLimits {
doneImagesThisWeek: string;
maxImagesPerWeek: string;
nextWeekBeginDateTime: string;
}
export interface VerifiableCredential {
exp?: number;
iat: number;
iss: string;
vc: {
"@context": string[];
type: string[];
credentialSubject: VerifiableCredentialSubject;
};
}
// similar to GenericVerifiableCredential... maybe replace that one
export interface VerifiableCredentialSubject {
"@context": string;
"@type": string;
[key: string]: any; // eslint-disable-line @typescript-eslint/no-explicit-any
}
export interface WorldProperties {
startTime?: string;
endTime?: string;
}
// AKA Registration & RegisterAction
export interface RegisterVerifiableCredential {
"@context": typeof SCHEMA_ORG_CONTEXT;
"@type": "RegisterAction";
agent: { identifier: string };
identifier?: string; // used for invites (when participant ID isn't known)
object: string;
participant?: { identifier: string }; // used when person is known (not an invite)
}
// now for some of the error & other wrapper types
export interface ResultWithType {
type: string;
}
export interface SuccessResult extends ResultWithType {
type: "success";
response: AxiosResponse<ClaimResult>;
}
export interface ErrorResponse {
error?: {
message?: string;
};
}
export interface InternalError {
error: string; // for system logging
userMessage?: string; // for user display
}
export interface ErrorResult extends ResultWithType {
type: "error";
error: InternalError;
}
export type CreateAndSubmitClaimResult = SuccessResult | ErrorResult;
/**
* This is similar to Contact but it grew up in different logic paths.
* We may want to change this to be a Contact.
*/
export interface UserInfo {
did: string;
name: string;
publicEncKey: string;
registered: boolean;
profileImageUrl?: string;
nextPublicEncKeyHash?: string;
}
// This is used to check for hidden info.
// See https://github.com/trentlarson/endorser-ch/blob/0cb626f803028e7d9c67f095858a9fc8542e3dbd/server/api/services/util.js#L6
const HIDDEN_DID = "did:none:HIDDEN";
/**
* 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 {
export function isDid(did: string) {
return did.startsWith("did:");
}
/**
* 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 {
export function isHiddenDid(did: string) {
return did === HIDDEN_DID;
}
/**
* 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;
export function isEmptyOrHiddenDid(did?: string) {
return !did || did === HIDDEN_DID; // catching empty string as well
}
/**
* Recursively tests strings within an object/array against a test function
* @param {Function} func - Test function to apply to strings
* @param {any} input - Object/array to recursively test
* @returns {boolean} True if any string passes the test function
*
* @example
* testRecursivelyOnStrings(isDid, { user: { id: "did:example:123" } })
* // Returns: true
* @return true for any string within this primitive/object/array where func(input) === true
*
* Similar logic is found in endorser-mobile.
*/
/**
* Recursively tests strings within 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
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function testRecursivelyOnStrings(func: (arg0: any) => boolean, input: any) {
if (Object.prototype.toString.call(input) === "[object String]") {
return func(input);
}
// Recursively test objects and arrays
else if (input instanceof Object) {
} else if (input instanceof Object) {
if (!Array.isArray(input)) {
// Handle plain objects
// it's an object
for (const key in input) {
if (testRecursivelyOnStrings(func, input[key])) {
return true;
}
}
} else {
// Handle arrays
// it's an array
for (const value of input) {
if (testRecursivelyOnStrings(func, value)) {
return true;
@@ -200,7 +351,6 @@ function testRecursivelyOnStrings(
}
return false;
} else {
// Non-string, non-object values can't contain strings
return false;
}
}
@@ -467,23 +617,15 @@ export async function getHeaders(
return headers;
}
/**
* Cache for storing plan data
* @constant {LRUCache}
*/
const planCache: LRUCache<string, PlanSummaryRecord> = new LRUCache({
max: 500,
});
/**
* Retrieves plan data from cache or server
* @param {string} handleId - Plan handle ID
* @param {Axios} axios - Axios instance
* @param {string} apiServer - API server URL
* @param {string} [requesterDid] - Optional requester DID for private info
* @returns {Promise<PlanSummaryRecord|undefined>} Plan data or undefined if not found
*
* @throws {Error} If server request fails
* @param handleId nullable, in which case "undefined" will be returned
* @param requesterDid optional, in which case no private info will be returned
* @param axios
* @param apiServer
*/
export async function getPlanFromCache(
handleId: string | undefined,
@@ -507,44 +649,44 @@ export async function getPlanFromCache(
cred = resp.data.data[0];
planCache.set(handleId, cred);
} else {
console.info(
"[EndorserServer] Plan cache is empty for handle",
console.error(
"Failed to load plan with handle",
handleId,
" Got data:",
JSON.stringify(resp.data),
resp.data,
);
}
} catch (error) {
console.error(
"[EndorserServer] Failed to load plan with handle",
"Failed to load plan with handle",
handleId,
" Got error:",
JSON.stringify(error),
error,
);
}
}
return cred;
}
/**
* Updates plan data in cache
* @param {string} handleId - Plan handle ID
* @param {PlanSummaryRecord} planSummary - Plan data to cache
*/
export async function setPlanInCache(
handleId: string,
planSummary: PlanSummaryRecord,
): Promise<void> {
) {
planCache.set(handleId, planSummary);
}
/**
* Extracts user-friendly message from server error
* @param {any} error - Error thrown from Endorser server call
* @returns {string|undefined} User-friendly message or undefined if none found
*
* @param error that is thrown from an Endorser server call by Axios
* @returns user-friendly message, or undefined if none found
*/
export function serverMessageForUser(error: any): string | undefined {
return error?.response?.data?.error?.message;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function serverMessageForUser(error: any) {
return (
// this is how most user messages are returned
error?.response?.data?.error?.message
// some are returned as "error" with a string, but those are more for devs and are less helpful to the user
);
}
/**
-89
View File
@@ -1,89 +0,0 @@
/**
* @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
View File
@@ -1,60 +0,0 @@
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
View File
@@ -1,4 +0,0 @@
import { initializeApp } from "./main.common";
const app = initializeApp();
app.mount("#app");
-4
View File
@@ -1,4 +0,0 @@
import { initializeApp } from "./main.common";
const app = initializeApp();
app.mount("#app");
+168 -2
View File
@@ -6,8 +6,173 @@ 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 { 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,
faImage,
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,
faBuilding,
} 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,
faImage,
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,
faBuilding,
);
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import Camera from "simple-vue-camera";
// Can trigger this with a 'throw' inside some top-level function, eg. on the HomeView
@@ -36,7 +201,7 @@ function setupGlobalErrorHandler(app: VueApp) {
);
};
}
// console.log("Bootstrapping Vue app...");
const app = createApp(App)
.component("fa", FontAwesomeIcon)
.component("camera", Camera)
@@ -48,3 +213,4 @@ const app = createApp(App)
setupGlobalErrorHandler(app);
app.mount("#app");
// console.log("Vue app mounted.");
-5
View File
@@ -1,5 +0,0 @@
import { initializeApp } from "./main.common";
import "./registerServiceWorker"; // Web PWA support
const app = initializeApp();
app.mount("#app");
+2 -2
View File
@@ -276,8 +276,8 @@ const routes: Array<RouteRecordRaw> = [
component: () => import("../views/TestView.vue"),
},
{
path: "/user-profile/:id?",
name: "user-profile",
path: "/userProfile/:id?",
name: "userProfile",
component: () => import("../views/UserProfileView.vue"),
},
];
-24
View File
@@ -1,24 +0,0 @@
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
View File
@@ -1,145 +0,0 @@
/**
* @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
View File
@@ -1,80 +0,0 @@
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
View File
@@ -1,73 +0,0 @@
/**
* @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]>;
};
+20
View File
@@ -0,0 +1,20 @@
export interface GiveRecordWithContactInfo {
jwtId: string;
fullClaim: unknown; // Replace with proper type
giver: {
known: boolean;
displayName: string;
profileImageUrl?: string;
};
receiver: {
known: boolean;
displayName: string;
profileImageUrl?: string;
};
providerPlanName?: string;
recipientProjectName?: string;
description?: string;
subDescription?: string;
image?: string;
timestamp: string;
}
+57 -88
View File
@@ -39,15 +39,12 @@
:to="{ name: 'contact-qr' }"
class="bg-slate-500 text-white px-1.5 py-1 rounded-md"
>
<font-awesome icon="qrcode" class="fa-fw text-xl"></font-awesome>
<fa icon="qrcode" class="fa-fw text-xl"></fa>
</router-link>
</span>
{{ givenName }}
<router-link :to="{ name: 'new-edit-account' }">
<font-awesome
icon="pen"
class="text-xs text-blue-500 ml-2 mb-1"
></font-awesome>
<fa icon="pen" class="text-xs text-blue-500 ml-2 mb-1"></fa>
</router-link>
</h2>
</div>
@@ -56,13 +53,13 @@
class="block w-full text-center text-md bg-amber-200 border border-dashed border-slate-400 px-1.5 py-2 rounded-md mb-2"
>
<button
class="inline-block text-md uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md"
@click="
() =>
($refs.userNameDialog as UserNameDialog).open(
(name) => (givenName = name),
(this.$refs.userNameDialog as UserNameDialog).open(
(name) => (this.givenName = name),
)
"
class="inline-block text-md uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md"
>
Set Your Name
</button>
@@ -72,23 +69,23 @@
<span v-if="profileImageUrl" class="flex justify-between">
<EntityIcon
:icon-size="96"
:profile-image-url="profileImageUrl"
:profileImageUrl="profileImageUrl"
class="inline-block align-text-bottom border border-slate-300 rounded"
@click="showLargeIdenticonUrl = profileImageUrl"
/>
<font-awesome
<fa
icon="trash-can"
class="text-red-500 fa-fw ml-8 mt-8 w-12 h-12"
@click="confirmDeleteImage"
class="text-red-500 fa-fw ml-8 mt-8 w-12 h-12"
/>
</span>
<div v-else class="text-center">
<div class @click="openImageDialog()">
<font-awesome
<fa
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"
/>
<font-awesome
<fa
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"
/>
@@ -104,8 +101,8 @@
</div>
<div class="flex justify-center">
<EntityIcon
:entity-id="activeDid"
:icon-size="64"
:entityId="activeDid"
:iconSize="64"
class="inline-block align-middle border border-slate-300 rounded-md mr-1"
@click="showLargeIdenticonId = activeDid"
/>
@@ -119,9 +116,9 @@
class="absolute inset-0 h-screen flex flex-col items-center justify-center bg-slate-900/50"
>
<EntityIcon
:entity-id="showLargeIdenticonId"
:icon-size="512"
:profile-image-url="showLargeIdenticonUrl"
:entityId="showLargeIdenticonId"
:iconSize="512"
:profileImageUrl="showLargeIdenticonUrl"
class="flex w-11/12 max-w-sm mx-auto mb-3 overflow-hidden bg-white rounded-lg shadow-lg"
@click="
showLargeIdenticonId = undefined;
@@ -138,12 +135,12 @@
>
<code class="truncate">{{ activeDid }}</code>
<button
class="ml-2"
@click="
doCopyTwoSecRedo(activeDid, () => (showDidCopy = !showDidCopy))
"
class="ml-2"
>
<font-awesome icon="copy" class="text-slate-400 fa-fw"></font-awesome>
<fa icon="copy" class="text-slate-400 fa-fw"></fa>
</button>
<span v-show="showDidCopy">Copied</span>
</div>
@@ -188,7 +185,7 @@
<!-- label -->
<div>
Reminder Notification
<font-awesome
<fa
icon="question-circle"
class="text-slate-400 fa-fw ml-2 cursor-pointer"
@click.stop="showReminderNotificationInfo"
@@ -200,7 +197,7 @@
@click="showReminderNotificationChoice()"
>
<!-- input -->
<input v-model="notifyingReminder" type="checkbox" class="sr-only" />
<input type="checkbox" v-model="notifyingReminder" class="sr-only" />
<!-- line -->
<div class="block bg-slate-500 w-14 h-8 rounded-full"></div>
<!-- dot -->
@@ -217,7 +214,7 @@
<!-- label -->
<div>
New Activity Notification
<font-awesome
<fa
icon="question-circle"
class="text-slate-400 fa-fw ml-2 cursor-pointer"
@click.stop="showNewActivityNotificationInfo"
@@ -230,8 +227,8 @@
>
<!-- input -->
<input
v-model="notifyingNewActivity"
type="checkbox"
v-model="notifyingNewActivity"
class="sr-only"
/>
<!-- line -->
@@ -271,15 +268,12 @@
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">
<font-awesome
icon="spinner"
class="fa-spin text-slate-400"
></font-awesome>
Loading profile...
<fa icon="spinner" class="fa-spin text-slate-400"></fa> Loading
profile...
</div>
<div v-else class="flex items-center mb-2">
<span class="font-bold">Public Profile</span>
<font-awesome
<fa
icon="circle-info"
class="text-slate-400 fa-fw ml-2 cursor-pointer"
@click="showProfileInfo"
@@ -295,9 +289,9 @@
<div class="flex items-center mb-4" @click="toggleUserProfileLocation">
<input
v-model="includeUserProfileLocation"
type="checkbox"
class="mr-2"
v-model="includeUserProfileLocation"
/>
<label for="includeUserProfileLocation">Include Location</label>
</div>
@@ -333,16 +327,17 @@
<div v-if="!loadingProfile && !savingProfile">
<div class="flex justify-between items-center">
<button
@click="saveProfile"
class="mt-2 px-4 py-2 bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white rounded-md"
:disabled="loadingProfile || savingProfile"
:class="{
'opacity-50 cursor-not-allowed': loadingProfile || savingProfile,
}"
@click="saveProfile"
>
Save Profile
</button>
<button
@click="confirmDeleteProfile"
class="mt-2 px-4 py-2 bg-gradient-to-b from-red-400 to-red-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white rounded-md"
:disabled="loadingProfile || savingProfile"
:class="{
@@ -351,7 +346,6 @@
savingProfile ||
(!userProfileDesc && !includeUserProfileLocation),
}"
@click="confirmDeleteProfile"
>
Delete Profile
</button>
@@ -369,8 +363,7 @@
<div class="mb-2 font-bold">Usage Limits</div>
<!-- show spinner if loading limits -->
<div v-if="loadingLimits" class="text-center">
Checking&hellip;
<font-awesome icon="spinner" class="fa-spin"></font-awesome>
Checking&hellip; <fa icon="spinner" class="fa-spin"></fa>
</div>
<div class="mb-4 text-center">
{{ limitsMessage }}
@@ -426,15 +419,15 @@
>
<div class="mb-2 font-bold">Data Export</div>
<router-link
v-if="activeDid"
:to="{ name: 'seed-backup' }"
v-if="activeDid"
class="block w-full text-center text-md bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-2 mt-2"
>
Backup Identifier Seed
</router-link>
<button
:class="computedStartDownloadLinkClassNames()"
v-bind:class="computedStartDownloadLinkClassNames()"
class="block w-full text-center text-md bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md"
@click="exportDatabase()"
>
@@ -444,7 +437,7 @@
</button>
<a
ref="downloadLink"
:class="computedDownloadLinkClassNames()"
v-bind:class="computedDownloadLinkClassNames()"
class="block w-full text-center text-md bg-gradient-to-b from-green-500 to-green-800 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-6"
>
If no download happened yet, click again here to download now.
@@ -461,7 +454,7 @@
</li>
<li class="list-disc list-outside ml-4">
On Android: Choose "Open" and then share
<font-awesome icon="share-nodes" class="fa-fw" />
<fa icon="share-nodes" class="fa-fw" />
to your prefered place.
</li>
</ul>
@@ -476,7 +469,7 @@
>
Advanced
</h3>
<div v-if="showAdvanced || showGeneralAdvanced" id="sectionAdvanced">
<div id="sectionAdvanced" v-if="showAdvanced || showGeneralAdvanced">
<p class="text-rose-600 mb-8">
Beware: the features here can be confusing and even change data in ways
you do not expect. But we support your freedom!
@@ -496,15 +489,12 @@
>
<code class="truncate">{{ publicBase64 }}</code>
<button
class="ml-2"
@click="
doCopyTwoSecRedo(publicBase64, () => (showB64Copy = !showB64Copy))
"
class="ml-2"
>
<font-awesome
icon="copy"
class="text-slate-400 fa-fw"
></font-awesome>
<fa icon="copy" class="text-slate-400 fa-fw"></fa>
</button>
<span v-show="showB64Copy">Copied</span>
</div>
@@ -515,15 +505,12 @@
>
<code class="truncate">{{ publicHex }}</code>
<button
class="ml-2"
@click="
doCopyTwoSecRedo(publicHex, () => (showPubCopy = !showPubCopy))
"
class="ml-2"
>
<font-awesome
icon="copy"
class="text-slate-400 fa-fw"
></font-awesome>
<fa icon="copy" class="text-slate-400 fa-fw"></fa>
</button>
<span v-show="showPubCopy">Copied</span>
</div>
@@ -535,18 +522,15 @@
>
<code class="truncate">{{ derivationPath }}</code>
<button
class="ml-2"
@click="
doCopyTwoSecRedo(
derivationPath,
() => (showDerCopy = !showDerCopy),
)
"
class="ml-2"
>
<font-awesome
icon="copy"
class="text-slate-400 fa-fw"
></font-awesome>
<fa icon="copy" class="text-slate-400 fa-fw"></fa>
</button>
<span v-show="showDerCopy">Copied</span>
</div>
@@ -573,7 +557,7 @@
</h2>
<div class="ml-4 mt-2">
<input type="file" class="ml-2" @change="uploadImportFile" />
<input type="file" @change="uploadImportFile" class="ml-2" />
<transition
enter-active-class="transform ease-out duration-300 transition"
enter-from-class="translate-y-2 opacity-0 sm:translate-y-4"
@@ -620,8 +604,8 @@
<div class="relative ml-2">
<!-- input -->
<input
v-model="showContactGives"
type="checkbox"
v-model="showContactGives"
name="showContactGives"
class="sr-only"
/>
@@ -638,20 +622,16 @@
<h2 class="text-slate-500 text-sm font-bold mt-4">Claim Server</h2>
<div class="px-4 py-4">
<input
v-model="apiServerInput"
type="text"
class="block w-full rounded border border-slate-400 px-4 py-2"
v-model="apiServerInput"
/>
<button
v-if="apiServerInput != apiServer"
class="w-full px-4 rounded bg-yellow-500 border border-slate-400"
@click="onClickSaveApiServer()"
>
<font-awesome
icon="floppy-disk"
class="fa-fw"
color="white"
></font-awesome>
<fa icon="floppy-disk" class="fa-fw" color="white"></fa>
</button>
<button
class="px-3 rounded bg-slate-200 border border-slate-400"
@@ -683,7 +663,7 @@
<!-- toggle -->
<div class="relative ml-2">
<!-- input -->
<input v-model="warnIfProdServer" type="checkbox" class="sr-only" />
<input type="checkbox" v-model="warnIfProdServer" class="sr-only" />
<!-- line -->
<div class="block bg-slate-500 w-14 h-8 rounded-full"></div>
<!-- dot -->
@@ -703,7 +683,7 @@
<!-- toggle -->
<div class="relative ml-2">
<!-- input -->
<input v-model="warnIfTestServer" type="checkbox" class="sr-only" />
<input type="checkbox" v-model="warnIfTestServer" class="sr-only" />
<!-- line -->
<div class="block bg-slate-500 w-14 h-8 rounded-full"></div>
<!-- dot -->
@@ -719,20 +699,16 @@
</h2>
<div class="px-3 py-4">
<input
v-model="webPushServerInput"
type="text"
class="block w-full rounded border border-slate-400 px-3 py-2"
v-model="webPushServerInput"
/>
<button
v-if="webPushServerInput != webPushServer"
class="w-full px-4 rounded bg-yellow-500 border border-slate-400"
@click="onClickSavePushServer()"
>
<font-awesome
icon="floppy-disk"
class="fa-fw"
color="white"
></font-awesome>
<fa icon="floppy-disk" class="fa-fw" color="white"></fa>
</button>
<button
class="px-3 rounded bg-slate-200 border border-slate-400"
@@ -753,7 +729,7 @@
Use Test 2
</button>
</div>
<span v-if="!webPushServerInput" class="px-4 text-sm">
<span class="px-4 text-sm" v-if="!webPushServerInput">
When that setting is blank, this app will use the default web push
server URL:
{{ DEFAULT_PUSH_SERVER }}
@@ -762,20 +738,16 @@
<h2 class="text-slate-500 text-sm font-bold mb-2">Partner Server URL</h2>
<div class="px-3 py-4">
<input
v-model="partnerApiServerInput"
type="text"
class="block w-full rounded border border-slate-400 px-3 py-2"
v-model="partnerApiServerInput"
/>
<button
v-if="partnerApiServerInput != partnerApiServer"
class="w-full px-4 rounded bg-yellow-500 border border-slate-400"
@click="onClickSavePartnerServer()"
>
<font-awesome
icon="floppy-disk"
class="fa-fw"
color="white"
></font-awesome>
<fa icon="floppy-disk" class="fa-fw" color="white"></fa>
</button>
<button
class="px-3 rounded bg-slate-200 border border-slate-400"
@@ -796,7 +768,7 @@
Use Local
</button>
</div>
<span v-if="!partnerApiServerInput" class="px-4 text-sm">
<span class="px-4 text-sm" v-if="!partnerApiServerInput">
When that setting is blank, this app will use the default partner server
URL:
{{ DEFAULT_PARTNER_API_SERVER }}
@@ -821,8 +793,8 @@
<div class="relative ml-2">
<!-- input -->
<input
v-model="hideRegisterPromptOnNewContact"
type="checkbox"
v-model="hideRegisterPromptOnNewContact"
class="sr-only"
/>
<!-- line -->
@@ -846,7 +818,7 @@
<!-- toggle -->
<div class="relative ml-2">
<!-- input -->
<input v-model="showShortcutBvc" type="checkbox" class="sr-only" />
<input type="checkbox" v-model="showShortcutBvc" class="sr-only" />
<!-- line -->
<div class="block bg-slate-500 w-14 h-8 rounded-full" />
<!-- dot -->
@@ -879,9 +851,9 @@
</span>
<div class="relative ml-2">
<input
v-model="passkeyExpirationMinutes"
type="number"
class="border border-slate-400 rounded px-2 py-2 text-center w-20"
v-model="passkeyExpirationMinutes"
@change="updatePasskeyExpiration"
/>
</div>
@@ -900,8 +872,8 @@
<div class="relative ml-2">
<!-- input -->
<input
v-model="showGeneralAdvanced"
type="checkbox"
v-model="showGeneralAdvanced"
class="sr-only"
/>
<!-- line -->
@@ -923,13 +895,13 @@ import { AxiosError } from "axios";
import { Buffer } from "buffer/";
import Dexie from "dexie";
import "dexie-export-import";
import { ImportProgress } from "dexie-export-import";
import { ImportProgress } from "dexie-export-import/dist/import";
import { LeafletMouseEvent } from "leaflet";
import * as R from "ramda";
import { IIdentifier } from "@veramo/core";
import { ref } from "vue";
import { Component, Vue } from "vue-facing-decorator";
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
import { Router } from "vue-router";
import { useClipboard } from "@vueuse/core";
import { LMap, LMarker, LTileLayer } from "@vue-leaflet/vue-leaflet";
@@ -975,7 +947,6 @@ import {
DIRECT_PUSH_TITLE,
retrieveAccountMetadata,
} from "../libs/util";
import { UserProfile } from "@/libs/partnerServer";
const inputImportFileNameRef = ref<Blob>();
@@ -995,8 +966,6 @@ const inputImportFileNameRef = ref<Blob>();
})
export default class AccountViewView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
$route!: RouteLocationNormalizedLoaded;
$router!: Router;
AppConstants = AppString;
DEFAULT_PUSH_SERVER = DEFAULT_PUSH_SERVER;
+6 -8
View File
@@ -7,17 +7,17 @@
<h1 class="text-lg text-center font-light relative px-7">
<!-- Back -->
<button
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.go(-1)"
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
>
<font-awesome icon="chevron-left" class="fa-fw" />
<fa icon="chevron-left" class="fa-fw" />
</button>
Raw Claim
</h1>
</div>
<div class="flex">
<textarea v-model="claimStr" rows="20" class="border-2 w-full"></textarea>
<textarea rows="20" class="border-2 w-full" v-model="claimStr"></textarea>
</div>
<button
class="block w-full text-center text-lg font-bold uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-3 rounded-md"
@@ -30,6 +30,7 @@
<script lang="ts">
import { Component, Vue } from "vue-facing-decorator";
import { Router } from "vue-router";
import QuickNav from "../components/QuickNav.vue";
import { NotificationIface } from "../constants/app";
@@ -37,15 +38,12 @@ import { logConsoleAndDb, retrieveSettingsForActiveAccount } from "../db/index";
import * as serverUtil from "../libs/endorserServer";
import * as libsUtil from "../libs/util";
import { errorStringForLog } from "../libs/endorserServer";
import { Router, RouteLocationNormalizedLoaded } from "vue-router";
@Component({
components: { QuickNav },
})
export default class ClaimAddRawView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
$route!: RouteLocationNormalizedLoaded;
$router!: Router;
accountIdentityStr: string = "null";
activeDid = "";
@@ -57,7 +55,7 @@ export default class ClaimAddRawView extends Vue {
this.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || "";
this.claimStr = (this.$route.query["claim"] as string) || "";
this.claimStr = (this.$route as Router).query["claim"];
if (this.claimStr) {
try {
const veriClaim = JSON.parse(this.claimStr);
@@ -67,7 +65,7 @@ export default class ClaimAddRawView extends Vue {
}
} else {
// there may be no link that uses this, meaning you'd have to enter it in a browser
const claimJwtId = (this.$route.query["claimJwtId"] as string) || "";
const claimJwtId = (this.$route as Router).query["claimJwtId"];
if (claimJwtId) {
const urlPath = libsUtil.isGlobalUri(claimJwtId)
? "/api/claim/byHandle/"
+5 -4
View File
@@ -2,8 +2,8 @@
<section id="Content">
<div class="flex items-center justify-center h-screen">
<div v-if="claimData">
<router-link :to="'/claim/' + claimId">
<canvas ref="claimCanvas" class="w-full block mx-auto"></canvas>
<router-link :to="'/claim/' + this.claimId">
<canvas class="w-full block mx-auto" ref="claimCanvas"></canvas>
</router-link>
</div>
</div>
@@ -14,10 +14,11 @@
import { Component, Vue } from "vue-facing-decorator";
import { nextTick } from "vue";
import QRCode from "qrcode";
import { APP_SERVER, NotificationIface } from "../constants/app";
import { db, retrieveSettingsForActiveAccount } from "../db/index";
import * as serverUtil from "../libs/endorserServer";
import { GenericCredWrapper, GenericVerifiableCredential } from "../interfaces";
@Component
export default class ClaimCertificateView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
@@ -80,7 +81,7 @@ export default class ClaimCertificateView extends Vue {
}
async drawCanvas(
claimData: GenericCredWrapper<GenericVerifiableCredential>,
claimData: serverUtil.GenericCredWrapper<serverUtil.GenericVerifiableCredential>,
confirmerIds: Array<string>,
) {
await db.open();
+10 -10
View File
@@ -6,6 +6,16 @@
</section>
</template>
<style scoped>
canvas {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
</style>
<script lang="ts">
import { Component, Vue } from "vue-facing-decorator";
import { nextTick } from "vue";
@@ -178,13 +188,3 @@ export default class ClaimReportCertificateView extends Vue {
}
}
</script>
<style scoped>
canvas {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
</style>
+57 -87
View File
@@ -7,10 +7,10 @@
<h1 class="text-lg text-center font-light relative px-7">
<!-- Back -->
<button
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.go(-1)"
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
>
<font-awesome icon="chevron-left" class="fa-fw" />
<fa icon="chevron-left" class="fa-fw" />
</button>
Verifiable Claim Details
</h1>
@@ -22,9 +22,7 @@
<div class="w-full">
<div class="flex columns-3">
<h2 class="text-md font-bold w-full">
{{
capitalizeAndInsertSpacesBeforeCaps(veriClaim.claimType || "")
}}
{{ capitalizeAndInsertSpacesBeforeCaps(veriClaim.claimType) }}
<button
v-if="
['GiveAction', 'Offer', 'PlanAction'].includes(
@@ -34,14 +32,11 @@
// but rather than add more Plan-specific logic to detect the agent
// we'll let them click the Project link and edit from there
"
@click="onClickEditClaim"
title="Edit"
data-testId="editClaimButton"
@click="onClickEditClaim"
>
<font-awesome
icon="pen"
class="text-sm text-blue-500 ml-2 mb-1"
/>
<fa icon="pen" class="text-sm text-blue-500 ml-2 mb-1" />
</button>
</h2>
<div class="flex justify-center w-full">
@@ -50,10 +45,7 @@
class="text-blue-500 mt-2"
title="Printable Certificate"
>
<font-awesome
icon="square"
class="text-white bg-yellow-500 p-1"
/>
<fa icon="square" class="text-white bg-yellow-500 p-1" />
</router-link>
</div>
<!-- show link icon to copy this URL to the clipboard -->
@@ -64,37 +56,30 @@
copyToClipboard('A link to this page', window.location.href)
"
>
<font-awesome icon="link" class="text-slate-500" />
<fa icon="link" class="text-slate-500" />
</button>
</div>
</div>
<div class="text-sm">
<div data-testId="description">
<font-awesome icon="message" class="fa-fw text-slate-400" />
<fa icon="message" class="fa-fw text-slate-400" />
{{
(veriClaim.claim?.itemOffered as any)?.description ||
(veriClaim.claim as any)?.description ||
""
veriClaim.claim?.itemOffered?.description ||
veriClaim.claim?.description
}}
</div>
<div>
<font-awesome icon="user" class="fa-fw text-slate-400" />
<fa icon="user" class="fa-fw text-slate-400" />
{{ didInfo(veriClaim.issuer) }}
</div>
<div>
<font-awesome icon="calendar" class="fa-fw text-slate-400" />
<fa icon="calendar" class="fa-fw text-slate-400" />
Recorded
{{ veriClaim.issuedAt?.replace(/T/, " ").replace(/Z/, " UTC") }}
</div>
<div
v-if="(veriClaim.claim as any).image"
class="flex justify-center"
>
<a :href="(veriClaim.claim as any).image" target="_blank">
<img
:src="(veriClaim.claim as any).image"
class="h-24 rounded-xl"
/>
<div v-if="veriClaim.claim.image" class="flex justify-center">
<a :href="veriClaim.claim.image" target="_blank">
<img :src="veriClaim.claim.image" class="h-24 rounded-xl" />
</a>
</div>
@@ -131,10 +116,10 @@
>
<!-- router-link to /claim/ only changes URL path -->
<a
class="text-blue-500 mt-4 cursor-pointer"
@click="
showDifferentClaimPage(detailsForGive?.fulfillsHandleId)
"
class="text-blue-500 mt-4 cursor-pointer"
>
Fulfills
{{
@@ -170,15 +155,15 @@
<div class="flex gap-4">
<div class="grow overflow-hidden">
<a
class="text-blue-500 mt-4 cursor-pointer"
@click="
provider.identifier.startsWith('did:')
? $router.push(
? this.$router.push(
'/did/' +
encodeURIComponent(provider.identifier),
)
: showDifferentClaimPage(provider.identifier)
"
class="text-blue-500 mt-4 cursor-pointer"
>
an activity...
</a>
@@ -192,13 +177,13 @@
</div>
</div>
<div class="mt-2">
<font-awesome icon="comment" class="text-slate-400" />
<fa icon="comment" class="text-slate-400" />
{{ issuerName }} posted that.
</div>
<!--
<div>
<router-link :to="'/claim-cert/' + encodeURIComponent(veriClaim.id)">
<font-awesome icon="file-contract" class="text-slate-400" />
<fa icon="file-contract" class="text-slate-400" />
<span class="ml-2 text-blue-500">Printable Certificate</span>
</router-link>
</div>
@@ -207,14 +192,11 @@
<div class="mt-8">
<button
v-if="libsUtil.canFulfillOffer(veriClaim)"
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()"
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"
>
Affirm Delivery
<font-awesome
icon="hand-holding-heart"
class="ml-2 text-white cursor-pointer"
/>
<fa icon="hand-holding-heart" class="ml-2 text-white cursor-pointer" />
</button>
</div>
<GiftedDialog ref="customGiveDialog" />
@@ -222,6 +204,7 @@
<div v-if="libsUtil.isGiveAction(veriClaim)">
<div class="flex columns-3">
<button
class="col-span-1 bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mt-2 px-4 py-2 rounded-md"
v-if="
libsUtil.isGiveRecordTheUserCanConfirm(
isRegistered,
@@ -230,14 +213,10 @@
confirmerIdList,
)
"
class="col-span-1 bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mt-2 px-4 py-2 rounded-md"
@click="confirmConfirmClaim()"
>
Confirm
<font-awesome
icon="circle-check"
class="ml-2 text-white cursor-pointer"
/>
<fa icon="circle-check" class="ml-2 text-white cursor-pointer" />
</button>
<h2 v-else class="font-bold uppercase text-xl mt-2">Confirmations</h2>
@@ -297,10 +276,7 @@
target="_blank"
class="text-blue-500"
>
<font-awesome
icon="arrow-up-right-from-square"
class="fa-fw"
/>
<fa icon="arrow-up-right-from-square" class="fa-fw" />
</a>
</span>
</div>
@@ -338,10 +314,7 @@
target="_blank"
class="text-blue-500"
>
<font-awesome
icon="arrow-up-right-from-square"
class="fa-fw"
/>
<fa icon="arrow-up-right-from-square" class="fa-fw" />
</a>
</span>
</div>
@@ -372,8 +345,8 @@
@click="showVeriClaimDump = !showVeriClaimDump"
>
Details
<font-awesome v-if="showVeriClaimDump" icon="chevron-up" />
<font-awesome v-else icon="chevron-right" />
<fa v-if="showVeriClaimDump" icon="chevron-up" />
<fa v-else icon="chevron-right" />
</h2>
<div v-if="showVeriClaimDump">
<div
@@ -388,7 +361,7 @@
<span v-if="canShare">
You can ask one of your contacts to take a look and see if their
contacts can see more details:
<a class="text-blue-500" @click="onClickShareClaim()"
<a @click="onClickShareClaim()" class="text-blue-500"
>click to send them this page info</a
>
and see if they can make an introduction. Someone is connected to
@@ -399,8 +372,8 @@
You can ask one of your contacts to take a look and see if their
contacts can see more details:
<a
class="text-blue-500"
@click="copyToClipboard('A link to this page', windowLocation)"
class="text-blue-500"
>click to copy this page info</a
>
and see if they can make an introduction. Someone is connected to
@@ -414,7 +387,7 @@
of your contacts.
<span v-if="canShare">
If you'd like an introduction,
<a class="text-blue-500" @click="onClickShareClaim()"
<a @click="onClickShareClaim()" class="text-blue-500"
>click to share the information with them and ask if they'll tell
you more about the participants.</a
>
@@ -422,8 +395,8 @@
<span v-else>
If you'd like an introduction,
<a
class="text-blue-500"
@click="copyToClipboard('A link to this page', windowLocation)"
class="text-blue-500"
>share this page with them and ask if they'll tell you more about
about the participants.</a
>
@@ -435,7 +408,7 @@
class="list-disc p-4"
>
<div class="text-sm">
<font-awesome icon="minus" class="fa-fw" />
<fa icon="minus" class="fa-fw" />
The {{ visibleDidPath }} is visible to:
</div>
<div class="ml-12 p-1">
@@ -454,10 +427,7 @@
target="_blank"
class="text-blue-500"
>
<font-awesome
icon="arrow-up-right-from-square"
class="fa-fw"
/>
<fa icon="arrow-up-right-from-square" class="fa-fw" />
</a>
</span>
<span v-if="veriClaim.publicUrls?.[visDid]"
@@ -466,7 +436,7 @@
target="_blank"
class="text-blue-500"
>
<font-awesome icon="globe" class="fa-fw" />
<fa icon="globe" class="fa-fw" />
{{
veriClaim.publicUrls[visDid].substring(
veriClaim.publicUrls[visDid].indexOf("//") + 2,
@@ -506,7 +476,7 @@
class="text-blue-500 cursor-pointer"
@click="showFullClaim(veriClaim.id as string)"
>
<font-awesome icon="file-lines" class="fa-fw" />
<fa icon="file-lines" class="fa-fw" />
Load Full Claim Details
</button>
</div>
@@ -522,8 +492,8 @@
target="_blank"
class="text-blue-500 cursor-pointer"
>
<font-awesome icon="file-lines" class="fa-fw" />
<font-awesome icon="arrow-up-right-from-square" class="ml-1 fa-fw" />
<fa icon="file-lines" class="fa-fw" />
<fa icon="arrow-up-right-from-square" class="ml-1 fa-fw" />
View on the Public Server
</a>
</div>
@@ -535,9 +505,9 @@ import { AxiosError } from "axios";
import * as yaml from "js-yaml";
import * as R from "ramda";
import { Component, Vue } from "vue-facing-decorator";
import { Router, RouteLocationNormalizedLoaded } from "vue-router";
import { Router } from "vue-router";
import { useClipboard } from "@vueuse/core";
import { GenericVerifiableCredential } from "../interfaces";
import GiftedDialog from "../components/GiftedDialog.vue";
import QuickNav from "../components/QuickNav.vue";
import { NotificationIface } from "../constants/app";
@@ -551,17 +521,19 @@ import * as serverUtil from "../libs/endorserServer";
import {
GenericCredWrapper,
OfferVerifiableCredential,
ProviderInfo,
} from "../interfaces";
} from "../libs/endorserServer";
import * as libsUtil from "../libs/util";
interface ProviderInfo {
identifier: string; // could be a DID or a handleId
linkConfirmed: boolean;
}
@Component({
components: { GiftedDialog, QuickNav },
})
export default class ClaimView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
$route!: RouteLocationNormalizedLoaded;
$router!: Router;
activeDid = "";
allMyDids: Array<string> = [];
@@ -572,12 +544,8 @@ export default class ClaimView extends Vue {
confirmerIdList: string[] = []; // list of DIDs that have confirmed this claim excluding the issuer
confsVisibleErrorMessage = "";
confsVisibleToIdList: string[] = []; // list of DIDs that can see any confirmer
detailsForGive: {
fulfillsPlanHandleId?: string;
fulfillsType?: string;
fulfillsHandleId?: string;
} | null = null;
detailsForOffer: { fulfillsPlanHandleId?: string } | null = null;
detailsForGive = null;
detailsForOffer = null;
fullClaim = null;
fullClaimDump = "";
fullClaimMessage = "";
@@ -590,7 +558,7 @@ export default class ClaimView extends Vue {
showVeriClaimDump = false;
veriClaim = serverUtil.BLANK_GENERIC_SERVER_RECORD;
veriClaimDump = "";
veriClaimDidsVisible: { [key: string]: string[] } = {};
veriClaimDidsVisible = {};
windowLocation = window.location.href;
R = R;
@@ -617,7 +585,6 @@ export default class ClaimView extends Vue {
}
async created() {
console.log("ClaimView created");
const settings = await retrieveSettingsForActiveAccount();
this.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || "";
@@ -627,6 +594,7 @@ export default class ClaimView extends Vue {
try {
this.allMyDids = await libsUtil.retrieveAccountDids();
} catch (error) {
// continue because we want to see claims, even anonymously
logConsoleAndDb(
"Error retrieving all account DIDs on home page:" + error,
true,
@@ -642,8 +610,10 @@ export default class ClaimView extends Vue {
);
}
const claimId = this.$route.params.id as string;
if (claimId) {
const pathParam = window.location.pathname.substring("/claim/".length);
let claimId;
if (pathParam) {
claimId = decodeURIComponent(pathParam);
await this.loadClaim(claimId, this.activeDid);
} else {
this.$notify(
@@ -657,6 +627,8 @@ 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;
}
@@ -687,7 +659,6 @@ export default class ClaimView extends Vue {
}
async loadClaim(claimId: string, userDid: string) {
console.log("[ClaimView] loadClaim called with claimId:", claimId);
const urlPath = libsUtil.isGlobalUri(claimId)
? "/api/claim/byHandle/"
: "/api/claim/";
@@ -695,7 +666,6 @@ export default class ClaimView extends Vue {
const headers = await serverUtil.getHeaders(userDid);
try {
console.log("[ClaimView] Making API request to:", url);
const resp = await this.axios.get(url, { headers });
if (resp.status === 200) {
this.veriClaim = resp.data;
@@ -917,7 +887,7 @@ export default class ClaimView extends Vue {
),
),
);
const confirmationClaim: GenericVerifiableCredential = {
const confirmationClaim: serverUtil.GenericVerifiableCredential = {
"@context": "https://schema.org",
"@type": "AgreeAction",
object: goodClaim,
+2 -2
View File
@@ -8,8 +8,8 @@
<router-link
:to="{ name: 'account' }"
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
><font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
</router-link>
><fa icon="chevron-left" class="fa-fw"></fa
></router-link>
Confirm Contact
</h1>
+38 -68
View File
@@ -8,10 +8,10 @@
<h1 class="text-lg text-center font-light relative px-7">
<!-- Back -->
<button
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.go(-1)"
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
>
<font-awesome icon="chevron-left" class="fa-fw" />
<fa icon="chevron-left" class="fa-fw" />
</button>
<span
v-if="
@@ -32,6 +32,7 @@
<div v-if="giveDetails && !isLoading">
<div class="flex justify-center">
<button
class="col-span-1 bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md"
v-if="
libsUtil.isGiveRecordTheUserCanConfirm(
isRegistered,
@@ -40,25 +41,18 @@
confirmerIdList,
)
"
class="col-span-1 bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-4 py-2 rounded-md"
@click="confirmConfirmClaim()"
>
Confirm
<font-awesome
icon="circle-check"
class="ml-2 text-white cursor-pointer"
/>
<fa icon="circle-check" class="ml-2 text-white cursor-pointer" />
</button>
<button
v-else
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()"
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"
>
Confirm
<font-awesome
icon="circle-check"
class="ml-2 text-white cursor-pointer"
/>
<fa icon="circle-check" class="ml-2 text-white cursor-pointer" />
</button>
</div>
@@ -68,29 +62,26 @@
<div class="overflow-hidden">
<div class="text-sm">
<div>
<font-awesome icon="arrow-left" class="fa-fw text-slate-400" />
<fa icon="arrow-left" class="fa-fw text-slate-400" />
{{ giverName }}
</div>
<div class="ml-6">gave</div>
<div v-if="giveDetails.amount">
<font-awesome
icon="hand-holding-dollar"
class="fa-fw text-slate-400"
/>
<fa icon="hand-holding-dollar" class="fa-fw text-slate-400" />
{{ displayAmount(giveDetails.unit, giveDetails.amount) }}
</div>
<div v-if="giveDetails.description">
<font-awesome icon="message" class="fa-fw text-slate-400" />
<fa icon="message" class="fa-fw text-slate-400" />
{{ giveDetails.amount ? "and:" : "" }}
{{ giveDetails.description }}
</div>
<div class="ml-6">to</div>
<div>
<font-awesome icon="arrow-right" class="fa-fw text-slate-400" />
<fa icon="arrow-right" class="fa-fw text-slate-400" />
{{ recipientName }}
</div>
<div>
<font-awesome icon="calendar" class="fa-fw text-slate-400" />
<fa icon="calendar" class="fa-fw text-slate-400" />
on
{{ giveDetails.issuedAt.substring(0, 10) }}
</div>
@@ -98,7 +89,7 @@
<!-- Fullfills Links -->
<!-- fullfills links for a give -->
<div v-if="giveDetails?.fulfillsPlanHandleId" class="mt-2">
<div class="mt-2" v-if="giveDetails?.fulfillsPlanHandleId">
<router-link
:to="
'/project/' +
@@ -108,10 +99,7 @@
target="_blank"
>
This fulfills a bigger plan
<font-awesome
icon="arrow-up-right-from-square"
class="fa-fw"
/>
<fa icon="arrow-up-right-from-square" class="fa-fw" />
</router-link>
</div>
<!-- if there's another, it's probably fulfilling an offer, too -->
@@ -137,10 +125,7 @@
giveDetails?.fulfillsType || "",
)
}}
<font-awesome
icon="arrow-up-right-from-square"
class="fa-fw"
/>
<fa icon="arrow-up-right-from-square" class="fa-fw" />
</router-link>
</div>
</div>
@@ -148,7 +133,7 @@
</div>
</div>
<div class="mt-2">
<font-awesome icon="comment" class="text-slate-400" />
<fa icon="comment" class="text-slate-400" />
{{ issuerName }} posted that.
</div>
@@ -200,10 +185,7 @@
)
"
>
<font-awesome
icon="copy"
class="text-slate-400 fa-fw"
/>
<fa icon="copy" class="text-slate-400 fa-fw" />
</button>
</span>
</div>
@@ -246,10 +228,7 @@
)
"
>
<font-awesome
icon="copy"
class="text-slate-400 fa-fw"
/>
<fa icon="copy" class="text-slate-400 fa-fw" />
</button>
</span>
</div>
@@ -281,8 +260,8 @@
@click="showVeriClaimDump = !showVeriClaimDump"
>
Details
<font-awesome v-if="showVeriClaimDump" icon="chevron-up" />
<font-awesome v-else icon="chevron-right" />
<fa v-if="showVeriClaimDump" icon="chevron-up" />
<fa v-else icon="chevron-right" />
</h2>
<div v-if="showVeriClaimDump">
<div
@@ -297,7 +276,7 @@
<span v-if="canShare">
You can ask one of your contacts to take a look and see if their
contacts can see more details:
<a class="text-blue-500" @click="onClickShareClaim()"
<a @click="onClickShareClaim()" class="text-blue-500"
>click to send them this page info</a
>
and see if they can make an introduction. Someone is connected to
@@ -308,8 +287,8 @@
You can ask one of your contacts to take a look and see if their
contacts can see more details:
<a
class="text-blue-500"
@click="copyToClipboard('A link to this page', windowLocation)"
class="text-blue-500"
>click to copy this page info</a
>
and see if they can make an introduction. Someone is connected to
@@ -323,7 +302,7 @@
some of your contacts.
<span v-if="canShare">
If you'd like an introduction,
<a class="text-blue-500" @click="onClickShareClaim()"
<a @click="onClickShareClaim()" class="text-blue-500"
>click to share the information with them and ask if they'll tell
you more about the participants.</a
>
@@ -331,8 +310,8 @@
<span v-else>
If you'd like an introduction,
<a
class="text-blue-500"
@click="copyToClipboard('A link to this page', windowLocation)"
class="text-blue-500"
>share this page with them and ask if they'll tell you more about
about the participants.</a
>
@@ -344,7 +323,7 @@
class="list-disc p-4"
>
<div class="text-sm">
<font-awesome icon="minus" class="fa-fw" />
<fa icon="minus" class="fa-fw" />
The {{ visibleDidPath }} is visible to:
</div>
<div class="ml-12 p-1">
@@ -363,18 +342,12 @@
copyToClipboard('The DID of ' + visDid, visDid)
"
>
<font-awesome
icon="copy"
class="text-slate-400 fa-fw"
/>
<fa icon="copy" class="text-slate-400 fa-fw" />
</button>
</span>
<span v-if="veriClaim.publicUrls?.[visDid]"
>, found at
<font-awesome
icon="globe"
class="fa-fw text-slate-400"
/>
<fa icon="globe" class="fa-fw text-slate-400" />
<a
:href="veriClaim.publicUrls?.[visDid]"
class="text-blue-500"
@@ -399,10 +372,10 @@
>
<div class="mt-2 ml-2">
<a
class="text-blue-500 cursor-pointer"
@click="showClaimPage(veriClaim.id)"
class="text-blue-500 cursor-pointer"
>
<font-awesome icon="file-lines" />
<fa icon="file-lines" />
See All Generic Info
</a>
</div>
@@ -412,7 +385,7 @@
class="text-blue-500 cursor-pointer"
:href="urlForNewGive"
>
<font-awesome icon="file-lines" />
<fa icon="file-lines" />
Record a Give Similar to the Original
</a>
</div>
@@ -421,10 +394,10 @@
<div v-else-if="!isLoading">This does not have details to confirm.</div>
<div
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"
v-if="isLoading"
>
<font-awesome icon="spinner" class="fa-spin-pulse"></font-awesome>
<fa icon="spinner" class="fa-spin-pulse"></fa>
</div>
</section>
</template>
@@ -435,26 +408,24 @@ import * as yaml from "js-yaml";
import * as R from "ramda";
import { Component, Vue } from "vue-facing-decorator";
import { useClipboard } from "@vueuse/core";
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
import { GenericVerifiableCredential } from "../interfaces";
import { Router } from "vue-router";
import QuickNav from "../components/QuickNav.vue";
import { NotificationIface } from "../constants/app";
import { db, retrieveSettingsForActiveAccount } from "../db/index";
import { Contact } from "../db/tables/contacts";
import * as serverUtil from "../libs/endorserServer";
import { GiveSummaryRecord } from "../interfaces";
import { displayAmount } from "../libs/endorserServer";
import { displayAmount, GiveSummaryRecord } from "../libs/endorserServer";
import * as libsUtil from "../libs/util";
import { isGiveAction, retrieveAccountDids } from "../libs/util";
import TopMessage from "../components/TopMessage.vue";
@Component({
methods: { displayAmount },
components: { TopMessage, QuickNav },
})
export default class ConfirmGiftView extends Vue {
export default class ClaimView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
$route!: RouteLocationNormalizedLoaded;
$router!: Router;
activeDid = "";
allMyDids: Array<string> = [];
@@ -476,14 +447,13 @@ export default class ConfirmGiftView extends Vue {
urlForNewGive = "";
veriClaim = serverUtil.BLANK_GENERIC_SERVER_RECORD;
veriClaimDump = "";
veriClaimDidsVisible: { [key: string]: string[] } = {};
veriClaimDidsVisible = {};
windowLocation = window.location.href;
R = R;
yaml = yaml;
libsUtil = libsUtil;
serverUtil = serverUtil;
displayAmount = displayAmount;
resetThisValues() {
this.confirmerIdList = [];
@@ -749,7 +719,7 @@ export default class ConfirmGiftView extends Vue {
),
),
);
const confirmationClaim: GenericVerifiableCredential = {
const confirmationClaim: serverUtil.GenericVerifiableCredential = {
"@context": "https://schema.org",
"@type": "AgreeAction",
object: goodClaim,
+14 -24
View File
@@ -11,8 +11,8 @@
<router-link
:to="{ name: 'contacts' }"
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
><font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
</router-link>
><fa icon="chevron-left" class="fa-fw"></fa
></router-link>
</h1>
<h1 id="ViewHeading" class="text-4xl text-center font-light pt-4">
@@ -59,13 +59,10 @@
<div class="font-bold">
{{ displayAmount(record.unit, record.amount) }}
<span v-if="record.amountConfirmed" title="Confirmed">
<font-awesome
icon="circle-check"
class="text-green-600 fa-fw"
/>
<fa icon="circle-check" class="text-green-600 fa-fw" />
</span>
<button v-else title="Unconfirmed" @click="confirm(record)">
<font-awesome icon="circle" class="text-blue-600 fa-fw" />
<button v-else @click="confirm(record)" title="Unconfirmed">
<fa icon="circle" class="text-blue-600 fa-fw" />
</button>
</div>
<div class="italic text-xs sm:text-sm text-slate-500">
@@ -75,10 +72,10 @@
</td>
<td class="p-1">
<span v-if="record.agentDid == contact?.did">
<font-awesome icon="arrow-left" class="text-slate-400 fa-fw" />
<fa icon="arrow-left" class="text-slate-400 fa-fw" />
</span>
<span v-else>
<font-awesome icon="arrow-right" class="text-slate-400 fa-fw" />
<fa icon="arrow-right" class="text-slate-400 fa-fw" />
</span>
</td>
<td class="p-1">
@@ -86,17 +83,14 @@
<div class="font-bold">
{{ displayAmount(record.unit, record.amount) }}
<span v-if="record.amountConfirmed" title="Confirmed">
<font-awesome
icon="circle-check"
class="text-green-600 fa-fw"
/>
<fa icon="circle-check" class="text-green-600 fa-fw" />
</span>
<button
v-else
title="Unconfirmed"
@click="cannotConfirmMessage()"
title="Unconfirmed"
>
<font-awesome icon="circle" class="text-slate-600 fa-fw" />
<fa icon="circle" class="text-slate-600 fa-fw" />
</button>
</div>
<div class="italic text-xs sm:text-sm text-slate-500">
@@ -114,7 +108,7 @@
import { AxiosError, AxiosRequestHeaders } from "axios";
import * as R from "ramda";
import { Component, Vue } from "vue-facing-decorator";
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
import { Router } from "vue-router";
import QuickNav from "../components/QuickNav.vue";
import { NotificationIface } from "../constants/app";
@@ -122,13 +116,11 @@ import { db, retrieveSettingsForActiveAccount } from "../db/index";
import { Contact } from "../db/tables/contacts";
import {
AgreeVerifiableCredential,
GiveSummaryRecord,
GiveVerifiableCredential,
} from "../interfaces";
import {
createEndorserJwtVcFromClaim,
displayAmount,
getHeaders,
GiveSummaryRecord,
GiveVerifiableCredential,
SCHEMA_ORG_CONTEXT,
} from "../libs/endorserServer";
import { retrieveAccountCount } from "../libs/util";
@@ -136,8 +128,6 @@ import { retrieveAccountCount } from "../libs/util";
@Component({ components: { QuickNav } })
export default class ContactAmountssView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
$route!: RouteLocationNormalizedLoaded;
$router!: Router;
activeDid = "";
apiServer = "";
@@ -153,7 +143,7 @@ export default class ContactAmountssView extends Vue {
async created() {
try {
const contactDid = this.$route.query["contactDid"] as string;
const contactDid = (this.$route as Router).query["contactDid"] as string;
this.contact = (await db.contacts.get(contactDid)) || null;
const settings = await retrieveSettingsForActiveAccount();
+18 -123
View File
@@ -7,10 +7,10 @@
<h1 class="text-4xl text-center font-light relative px-7">
<!-- Back -->
<button
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.go(-1)"
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
>
<font-awesome icon="chevron-left" class="fa-fw" />
<fa icon="chevron-left" class="fa-fw" />
</button>
{{ contact.name || AppString.NO_CONTACT_NAME }}
</h1>
@@ -25,9 +25,9 @@
Name
</label>
<input
v-model="contactName"
type="text"
class="block w-full ml-2 mt-1 border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500"
v-model="contactName"
/>
</div>
@@ -38,9 +38,9 @@
</label>
<textarea
id="contactNotes"
v-model="contactNotes"
rows="4"
class="block w-full mt-1 border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500"
v-model="contactNotes"
></textarea>
</div>
@@ -53,60 +53,60 @@
class="flex mt-2"
>
<input
v-model="method.label"
type="text"
v-model="method.label"
class="block w-1/4 border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500"
placeholder="Label"
/>
<input
v-model="method.type"
type="text"
v-model="method.type"
class="block ml-2 w-1/4 border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500"
placeholder="Type"
/>
<div class="relative">
<button
class="px-2 py-1 bg-gray-200 rounded-md"
@click="toggleDropdown(index)"
class="px-2 py-1 bg-gray-200 rounded-md"
>
<font-awesome icon="caret-down" class="fa-fw" />
<fa icon="caret-down" class="fa-fw" />
</button>
<div
v-if="dropdownIndex === index"
class="absolute bg-white border border-gray-300 rounded-md mt-1"
>
<div
class="px-4 py-2 hover:bg-gray-100 cursor-pointer"
@click="setMethodType(index, 'CELL')"
class="px-4 py-2 hover:bg-gray-100 cursor-pointer"
>
CELL
</div>
<div
class="px-4 py-2 hover:bg-gray-100 cursor-pointer"
@click="setMethodType(index, 'EMAIL')"
class="px-4 py-2 hover:bg-gray-100 cursor-pointer"
>
EMAIL
</div>
<div
class="px-4 py-2 hover:bg-gray-100 cursor-pointer"
@click="setMethodType(index, 'WHATSAPP')"
class="px-4 py-2 hover:bg-gray-100 cursor-pointer"
>
WHATSAPP
</div>
</div>
</div>
<input
v-model="method.value"
type="text"
v-model="method.value"
class="block ml-2 w-1/2 border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500"
placeholder="Number, email, etc."
/>
<button class="ml-2 text-red-500" @click="removeContactMethod(index)">
<font-awesome icon="trash-can" class="fa-fw" />
<button @click="removeContactMethod(index)" class="ml-2 text-red-500">
<fa icon="trash-can" class="fa-fw" />
</button>
</div>
<button class="mt-2" @click="addContactMethod">
<font-awesome
<button @click="addContactMethod" class="mt-2">
<fa
icon="plus"
class="fa-fw px-2 py-2.5 bg-green-500 text-green-100 rounded-full"
/>
@@ -134,7 +134,7 @@
<script lang="ts">
import * as R from "ramda";
import { Component, Vue } from "vue-facing-decorator";
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
import { RouteLocation, Router } from "vue-router";
import QuickNav from "../components/QuickNav.vue";
import TopMessage from "../components/TopMessage.vue";
@@ -142,37 +142,6 @@ import { AppString, NotificationIface } from "../constants/app";
import { db } from "../db/index";
import { Contact, ContactMethod } from "../db/tables/contacts";
/**
* Contact Edit View Component
* @author Matthew Raymer
*
* This component provides a full-featured contact editing interface with support for:
* - Basic contact information (name, notes)
* - Multiple contact methods with type selection
* - Data validation and persistence
*
* Workflow:
* 1. Component loads with DID from route params
* 2. Fetches existing contact data from IndexedDB
* 3. Presents editable form with current values
* 4. Validates and saves updates back to database
*
* Contact Method Types:
* - CELL: Mobile phone numbers
* - EMAIL: Email addresses
* - WHATSAPP: WhatsApp contact info
*
* State Management:
* - Maintains separate state for form fields to prevent direct mutation
* - Handles array cloning for contact methods to prevent reference issues
* - Manages dropdown state for method type selection
*
* Navigation:
* - Back button returns to previous view
* - Save redirects to contact detail view
* - Cancel returns to previous view
* - Invalid DID redirects to contacts list
*/
@Component({
components: {
QuickNav,
@@ -180,46 +149,22 @@ import { Contact, ContactMethod } from "../db/tables/contacts";
},
})
export default class ContactEditView extends Vue {
/** Notification function injected by Vue */
$notify!: (notification: NotificationIface, timeout?: number) => void;
/** Current route instance */
$route!: RouteLocationNormalizedLoaded;
/** Router instance for navigation */
$router!: Router;
/** Current contact data */
contact: Contact = {
did: "",
name: "",
notes: "",
};
/** Editable contact name field */
contactName = "";
/** Editable contact notes field */
contactNotes = "";
/** Array of editable contact methods */
contactMethods: Array<ContactMethod> = [];
/** Currently open dropdown index, null if none open */
dropdownIndex: number | null = null;
/** App string constants */
AppString = AppString;
/**
* Component lifecycle hook that initializes the contact edit form
*
* Workflow:
* 1. Extracts DID from route parameters
* 2. Queries database for existing contact
* 3. Populates form fields with contact data
* 4. Handles missing contact error case
*
* @throws Will not throw but redirects on error
* @emits Notification on contact not found
* @emits Router navigation on error
*/
async created() {
const contactDid = this.$route.params.did;
const contactDid = (this.$route as RouteLocation).params.did;
const contact = await db.contacts.get(contactDid || "");
if (contact) {
this.contact = contact;
@@ -238,75 +183,29 @@ export default class ContactEditView extends Vue {
}
}
/**
* Adds a new empty contact method to the methods array
*
* Creates a new method object with empty fields for:
* - label: Custom label for the method
* - type: Communication type (CELL, EMAIL, WHATSAPP)
* - value: The contact information value
*/
addContactMethod() {
this.contactMethods.push({ label: "", type: "", value: "" });
}
/**
* Removes a contact method at the specified index
*
* @param index The array index of the method to remove
*/
removeContactMethod(index: number) {
this.contactMethods.splice(index, 1);
}
/**
* Toggles the type selection dropdown for a contact method
*
* If the clicked dropdown is already open, closes it.
* If another dropdown is open, closes it and opens the clicked one.
*
* @param index The array index of the method whose dropdown to toggle
*/
toggleDropdown(index: number) {
this.dropdownIndex = this.dropdownIndex === index ? null : index;
}
/**
* Sets the type for a contact method and closes the dropdown
*
* @param index The array index of the method to update
* @param type The new type value (CELL, EMAIL, WHATSAPP)
*/
setMethodType(index: number, type: string) {
this.contactMethods[index].type = type;
this.dropdownIndex = null;
}
/**
* Saves the edited contact information to the database
*
* Workflow:
* 1. Clones contact methods array to prevent reference issues
* 2. Normalizes method types to uppercase
* 3. Checks for changes in method types
* 4. Updates database with new values
* 5. Notifies user of success
* 6. Redirects to contact detail view
*
* @throws Will not throw but notifies on validation errors
* @emits Notification on type changes or success
* @emits Router navigation on success
*/
async saveEdit() {
// without this conversion, "Failed to execute 'put' on 'IDBObjectStore': [object Array] could not be cloned."
const contactMethodsObj = JSON.parse(JSON.stringify(this.contactMethods));
// Normalize method types to uppercase
const contactMethods = contactMethodsObj.map((method: ContactMethod) =>
R.set(R.lensProp("type"), method.type.toUpperCase(), method),
);
// Check for type changes
if (!R.equals(contactMethodsObj, contactMethods)) {
this.contactMethods = contactMethods;
this.$notify(
@@ -320,15 +219,11 @@ export default class ContactEditView extends Vue {
);
return;
}
// Save to database
await db.contacts.update(this.contact.did, {
name: this.contactName,
notes: this.contactNotes,
contactMethods: contactMethods,
});
// Notify success and redirect
this.$notify({
group: "alert",
type: "success",
+12 -13
View File
@@ -9,8 +9,8 @@
<router-link
:to="{ name: 'home' }"
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
><font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
</router-link>
><fa icon="chevron-left" class="fa-fw"></fa
></router-link>
Given by...
</h1>
</div>
@@ -30,10 +30,10 @@
<span class="text-right">
<button
type="button"
class="block w-full text-center text-sm uppercase bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-3 py-1.5 rounded-md"
@click="openDialog()"
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"
>
<font-awesome icon="gift" class="fa-fw"></font-awesome>
<fa icon="gift" class="fa-fw"></fa>
</button>
</span>
</h2>
@@ -47,7 +47,7 @@
<span class="grow font-semibold">
<EntityIcon
:contact="contact"
:icon-size="32"
:iconSize="32"
class="inline-block align-middle border border-slate-300 rounded-md mr-1"
/>
{{ contact.name || "(no name)" }}
@@ -55,23 +55,23 @@
<span class="text-right">
<button
type="button"
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)"
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"
>
<font-awesome icon="gift" class="fa-fw"></font-awesome>
<fa icon="gift" class="fa-fw"></fa>
</button>
</span>
</h2>
</li>
</ul>
<GiftedDialog ref="customDialog" :to-project-id="projectId" />
<GiftedDialog ref="customDialog" :toProjectId="projectId" />
</section>
</template>
<script lang="ts">
import { Component, Vue } from "vue-facing-decorator";
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
import { Router } from "vue-router";
import GiftedDialog from "../components/GiftedDialog.vue";
import QuickNav from "../components/QuickNav.vue";
@@ -86,8 +86,6 @@ import { GiverReceiverInputInfo } from "../libs/util";
})
export default class ContactGiftingView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
$route!: RouteLocationNormalizedLoaded;
$router!: Router;
activeDid = "";
allContacts: Array<Contact> = [];
@@ -109,8 +107,9 @@ export default class ContactGiftingView extends Vue {
(a.name || "").localeCompare(b.name || ""),
);
this.projectId = (this.$route.query["projectId"] as string) || "";
this.prompt = (this.$route.query["prompt"] as string) ?? this.prompt;
this.projectId = (this.$route as Router).query["projectId"] || "";
this.prompt = (this.$route as Router).query["prompt"] ?? this.prompt;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) {
+87 -240
View File
@@ -3,8 +3,11 @@
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Back -->
<div class="text-lg text-center font-light relative px-7">
<h1 class="text-lg text-center px-2 py-1 absolute -left-2 -top-1" @click="$router.back()">
<font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
<h1
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.back()"
>
<fa icon="chevron-left" class="fa-fw"></fa>
</h1>
</div>
@@ -14,31 +17,46 @@
</h1>
<div v-if="checkingImports" class="text-center">
<font-awesome icon="spinner" class="animate-spin" />
<fa icon="spinner" class="animate-spin" />
</div>
<div v-else>
<span v-if="contactsImporting.length > sameCount" class="flex justify-center">
<input v-model="makeVisible" type="checkbox" class="mr-2" />
<span
v-if="contactsImporting.length > sameCount"
class="flex justify-center"
>
<input type="checkbox" v-model="makeVisible" class="mr-2" />
Make my activity visible to these contacts.
</span>
<div v-if="sameCount > 0">
<span v-if="sameCount == 1">One contact is the same as an existing contact</span>
<span v-else>{{ sameCount }} contacts are the same as existing contacts</span>
<span v-if="sameCount == 1"
>One contact is the same as an existing contact</span
>
<span v-else
>{{ sameCount }} contacts are the same as existing contacts</span
>
</div>
<!-- Results List -->
<ul v-if="contactsImporting.length > sameCount" class="border-t border-slate-300">
<ul
v-if="contactsImporting.length > sameCount"
class="border-t border-slate-300"
>
<li v-for="(contact, index) in contactsImporting" :key="contact.did">
<div v-if="
!contactsExisting[contact.did] ||
!R.isEmpty(contactDifferences[contact.did])
" class="grow overflow-hidden border-b border-slate-300 pt-2.5 pb-4">
<div
v-if="
!contactsExisting[contact.did] ||
!R.isEmpty(contactDifferences[contact.did])
"
class="grow overflow-hidden border-b border-slate-300 pt-2.5 pb-4"
>
<h2 class="text-base font-semibold">
<input v-model="contactsSelected[index]" type="checkbox" />
<input type="checkbox" v-model="contactsSelected[index]" />
{{ contact.name || AppString.NO_CONTACT_NAME }}
-
<span v-if="contactsExisting[contact.did]" class="text-orange-500">Existing</span>
<span v-if="contactsExisting[contact.did]" class="text-orange-500"
>Existing</span
>
<span v-else class="text-green-500">New</span>
</h2>
<div class="text-sm truncate">
@@ -51,9 +69,13 @@
<div class="font-bold">Old Value</div>
<div class="font-bold">New Value</div>
</div>
<div v-for="(value, contactField) in contactDifferences[
contact.did
]" :key="contactField" class="grid grid-cols-3 border">
<div
v-for="(value, contactField) in contactDifferences[
contact.did
]"
:key="contactField"
class="grid grid-cols-3 border"
>
<div class="border font-bold p-1">
{{ capitalizeAndInsertSpacesBeforeCaps(contactField) }}
</div>
@@ -66,7 +88,8 @@
</li>
<button
class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-sm text-white mt-2 px-2 py-1.5 rounded"
@click="importContacts">
@click="importContacts"
>
Import Selected Contacts
</button>
</ul>
@@ -78,10 +101,18 @@
get the full text and paste it. (Note that iOS cuts off data in text
messages.) Ask the person to send the data a different way, eg. email.
<div class="mt-4 text-center">
<textarea v-model="inputJwt" placeholder="Contact-import data"
class="mt-4 border-2 border-gray-300 p-2 rounded" cols="30" @input="() => checkContactJwt(inputJwt)" />
<textarea
v-model="inputJwt"
placeholder="Contact-import data"
class="mt-4 border-2 border-gray-300 p-2 rounded"
cols="30"
@input="() => checkContactJwt(inputJwt)"
/>
<br />
<button class="ml-2 p-2 bg-blue-500 text-white rounded" @click="() => processContactJwt(inputJwt)">
<button
@click="() => processContactJwt(inputJwt)"
class="ml-2 p-2 bg-blue-500 text-white rounded"
>
Check Import
</button>
</div>
@@ -91,77 +122,6 @@
</template>
<script lang="ts">
/**
* @file Contact Import View Component
* @author Matthew Raymer
*
* This component handles the import of contacts into the TimeSafari app.
* It supports multiple import methods and handles duplicate detection,
* contact validation, and visibility settings.
*
* Import Methods:
* 1. Direct URL Query Parameters:
* Example: /contact-import?contacts=[{"did":"did:example:123","name":"Alice"}]
*
* 2. JWT in URL Path:
* Example: /contact-import/eyJhbGciOiJFUzI1NksifQ...
* - Supports both single and bulk imports
* - JWT payload can be either:
* a) Array format: { contacts: [{did: "...", name: "..."}, ...] }
* b) Single contact: { own: true, did: "...", name: "..." }
*
* 3. Manual JWT Input:
* - Accepts pasted JWT strings
* - Validates format and content before processing
*
* URL Examples:
* ```
* # Bulk import via query params
* /contact-import?contacts=[
* {"did":"did:example:123","name":"Alice"},
* {"did":"did:example:456","name":"Bob"}
* ]
*
* # Single contact via JWT
* /contact-import/eyJhbGciOiJFUzI1NksifQ.eyJvd24iOnRydWUsImRpZCI6ImRpZDpleGFtcGxlOjEyMyJ9...
*
* # Bulk import via JWT
* /contact-import/eyJhbGciOiJFUzI1NksifQ.eyJjb250YWN0cyI6W3siZGlkIjoiZGlkOmV4YW1wbGU6MTIzIn1dfQ...
*
* # Redirect to contacts page (single contact)
* /contacts?contactJwt=eyJhbGciOiJFUzI1NksifQ...
* ```
*
* Features:
* - Automatic duplicate detection
* - Field-by-field comparison for existing contacts
* - Batch visibility settings
* - Auto-import for single new contacts
* - Error handling and validation
*
* State Management:
* - Tracks existing contacts
* - Maintains selection state for bulk imports
* - Records differences for duplicate contacts
* - Manages visibility settings
*
* Security Considerations:
* - JWT validation for imported contacts
* - Visibility control per contact
* - Error handling for malformed data
*
* @example
* // Component usage in router
* {
* path: "/contact-import/:jwt?",
* name: "contact-import",
* component: ContactImportView
* }
*
* @see {@link Contact} for contact data structure
* @see {@link setVisibilityUtil} for visibility management
*/
import * as R from "ramda";
import { Component, Vue } from "vue-facing-decorator";
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
@@ -185,75 +145,22 @@ import {
import { getContactJwtFromJwtUrl } from "../libs/crypto";
import { decodeEndorserJwt } from "../libs/crypto/vc";
/**
* Contact Import View Component
* @author Matthew Raymer
*
* This component handles the secure import of contacts into TimeSafari via JWT tokens.
* It supports both single and multiple contact imports with validation and duplicate detection.
*
* Import Workflows:
* 1. JWT in URL Path (/contact-import/[JWT])
* - Extracts JWT from path
* - Decodes and validates contact data
* - Handles both single and multiple contacts
*
* 2. JWT in Query Parameter (/contacts?contactJwt=[JWT])
* - Used for single contact redirects
* - Processes JWT from query parameter
* - Redirects to appropriate view
*
* JWT Payload Structure:
* ```json
* {
* "iat": 1740740453,
* "contacts": [{
* "did": "did:ethr:0x...",
* "name": "Optional Name",
* "nextPubKeyHashB64": "base64 string",
* "publicKeyBase64": "base64 string"
* }],
* "iss": "did:ethr:0x..."
* }
* ```
*
* Security Features:
* - JWT validation
* - Issuer verification
* - Duplicate detection
* - Contact data validation
*
* @component
*/
@Component({
components: { EntityIcon, OfferDialog, QuickNav },
})
export default class ContactImportView extends Vue {
/** Notification function injected by Vue */
$notify!: (notification: NotificationIface, timeout?: number) => void;
/** Current route instance */
$route!: RouteLocationNormalizedLoaded;
/** Router instance for navigation */
$router!: Router;
// Constants
AppString = AppString;
capitalizeAndInsertSpacesBeforeCaps = capitalizeAndInsertSpacesBeforeCaps;
libsUtil = libsUtil;
R = R;
// Component state
/** Active user's DID for authentication and visibility settings */
activeDid = "";
/** API server URL for backend communication */
apiServer = "";
/** Map of existing contacts keyed by DID for duplicate detection */
contactsExisting: Record<string, Contact> = {};
/** Array of contacts being imported from JWT */
contactsImporting: Array<Contact> = [];
/** Selection state for each importing contact */
contactsSelected: Array<boolean> = [];
/** Differences between existing and importing contacts */
contactsExisting: Record<string, Contact> = {}; // user's contacts already in the system, keyed by DID
contactsImporting: Array<Contact> = []; // contacts from the import
contactsSelected: Array<boolean> = []; // whether each contact in contactsImporting is selected
contactDifferences: Record<
string,
Record<
@@ -263,117 +170,69 @@ export default class ContactImportView extends Vue {
old: string | boolean | Array<ContactMethod> | undefined;
}
>
> = {};
/** Loading state for import operations */
> = {}; // for existing contacts, it shows the difference between imported and existing contacts for each key
checkingImports = false;
/** JWT input for manual contact import */
inputJwt: string = "";
/** Visibility setting for imported contacts */
makeVisible = true;
/** Count of duplicate contacts found */
sameCount = 0;
/**
* Component lifecycle hook that initializes the contact import process
*
* This method handles three distinct import scenarios:
* 1. Query Parameter Import:
* - Checks for contacts in URL query parameters
* - Parses JSON array of contacts if present
*
* 2. JWT URL Import:
* - Extracts JWT from URL path using regex pattern '/contact-import/(ey.+)$'
* - Decodes JWT without validation (supports future-dated QR codes)
* - Handles two JWT payload formats:
* a. Array format: payload.contacts or direct array
* b. Single contact format: redirects to contacts page with JWT
*
* 3. Auto-Import Logic:
* - Automatically imports if exactly one new contact is present
* - Only triggers if no existing contacts match
*
* @throws Will not throw but logs errors during JWT processing
* @emits router.push when redirecting for single contact import
*/
async created() {
await this.initializeSettings();
await this.processQueryParams();
await this.processJwtFromPath();
await this.handleAutoImport();
}
/**
* Initializes component settings from active account
*/
private async initializeSettings() {
const settings = await retrieveSettingsForActiveAccount();
this.activeDid = settings.activeDid || "";
this.apiServer = settings.apiServer || "";
}
/**
* Processes contacts from URL query parameters
*/
private async processQueryParams() {
const importedContacts = this.$route.query["contacts"] as string;
// look for any imported contact array from the query parameter
const importedContacts = (this.$route as RouteLocationNormalizedLoaded)
.query["contacts"] as string;
if (importedContacts) {
await this.setContactsSelected(JSON.parse(importedContacts));
}
}
/**
* Processes JWT from URL path and handles different JWT formats
*/
private async processJwtFromPath() {
// JWT tokens always start with 'ey' (base64url encoded header)
const JWT_PATTERN = /\/contact-import\/(ey.+)$/;
const jwt = window.location.pathname.match(JWT_PATTERN)?.[1];
// look for a JWT after /contact-import/ in the window.location.pathname
const jwt = window.location.pathname.match(
/\/contact-import\/(ey.+)$/,
)?.[1];
if (jwt) {
// would prefer to validate but we've got an error with JWTs on QR codes generated in the future
// eslint-disable-next-line prettier/prettier
// const parsedJwt: Omit<JWTVerified, "didResolutionResult" | "signer" | "jwt"> = await decodeAndVerifyJwt(jwt);
// decode the JWT
const parsedJwt = decodeEndorserJwt(jwt);
const contacts: Array<Contact> =
parsedJwt.payload.contacts ||
(Array.isArray(parsedJwt.payload) ? parsedJwt.payload : undefined);
const contacts: Array<Contact> =
parsedJwt.payload.contacts || // someday this will be the only payload sent to this page
(Array.isArray(parsedJwt.payload) ? parsedJwt.payload : undefined);
if (!contacts && parsedJwt.payload.own) {
this.$router.push({
// handle this single-contact JWT in the contacts page, better suited to single additions
(this.$router as Router).push({
name: "contacts",
query: { contactJwt: jwt },
});
return;
}
if (contacts) {
await this.setContactsSelected(contacts);
} else {
// no contacts found so default message should be OK
}
}
}
/**
* Handles automatic import for single new contacts
*/
private async handleAutoImport() {
if (
this.contactsImporting.length === 1 &&
R.isEmpty(this.contactsExisting)
) {
// if there is only one contact and it's new, then we will automatically import it
this.contactsSelected[0] = true;
await this.importContacts();
this.importContacts(); // ... which routes to the contacts list
}
}
/**
* Processes contacts for import and checks for duplicates
* @param contacts Array of contacts to process
*/
async setContactsSelected(contacts: Array<Contact>) {
this.contactsImporting = contacts;
this.contactsSelected = new Array(this.contactsImporting.length).fill(true);
await db.open();
const baseContacts = await db.contacts.toArray();
// Check for existing contacts and differences
// set the existing contacts, keyed by DID, if they exist in contactsImporting
for (let i = 0; i < this.contactsImporting.length; i++) {
const contactIn = this.contactsImporting[i];
const existingContact = baseContacts.find(
@@ -382,7 +241,6 @@ export default class ContactImportView extends Vue {
if (existingContact) {
this.contactsExisting[contactIn.did] = existingContact;
// Compare contact fields for differences
const differences: Record<
string,
{
@@ -391,6 +249,7 @@ export default class ContactImportView extends Vue {
}
> = {};
Object.keys(contactIn).forEach((key) => {
// eslint-disable-next-line prettier/prettier
if (!R.equals(contactIn[key as keyof Contact], existingContact[key as keyof Contact])) {
differences[key] = {
old: existingContact[key as keyof Contact],
@@ -403,16 +262,13 @@ export default class ContactImportView extends Vue {
this.sameCount++;
}
// Don't auto-select duplicates
// don't automatically import previous data
this.contactsSelected[i] = false;
}
}
}
/**
* Validates contact import JWT format
* @param jwtInput JWT string to validate
*/
// check the contact-import JWT
async checkContactJwt(jwtInput: string) {
if (
jwtInput.endsWith(APP_SERVER) ||
@@ -432,15 +288,14 @@ export default class ContactImportView extends Vue {
}
}
/**
* Processes contact import JWT and updates contacts
* @param jwtInput JWT string containing contact data
*/
// process the invite JWT and/or text message containing the URL with the JWT
async processContactJwt(jwtInput: string) {
this.checkingImports = true;
try {
// (For another approach used with invites, see InviteOneAcceptView.processInvite)
const jwt: string = getContactJwtFromJwtUrl(jwtInput);
// JWT format: { header, payload, signature, data }
const payload = decodeEndorserJwt(jwt).payload;
if (Array.isArray(payload.contacts)) {
@@ -464,16 +319,10 @@ export default class ContactImportView extends Vue {
this.checkingImports = false;
}
/**
* Imports selected contacts and sets visibility if requested
* Updates existing contacts or adds new ones
*/
async importContacts() {
this.checkingImports = true;
let importedCount = 0,
updatedCount = 0;
// Process selected contacts
for (let i = 0; i < this.contactsImporting.length; i++) {
if (this.contactsSelected[i]) {
const contact = this.contactsImporting[i];
@@ -489,8 +338,6 @@ export default class ContactImportView extends Vue {
}
}
}
// Set visibility if requested
if (this.makeVisible) {
const failedVisibileToContacts = [];
for (let i = 0; i < this.contactsImporting.length; i++) {
@@ -517,8 +364,9 @@ export default class ContactImportView extends Vue {
group: "alert",
type: "danger",
title: "Visibility Error",
text: `Failed to set visibility for ${failedVisibileToContacts.length} contact${failedVisibileToContacts.length == 1 ? "" : "s"
}. You must set them individually: ${failedVisibileToContacts.map((c) => c.name).join(", ")}`,
text: `Failed to set visibility for ${failedVisibileToContacts.length} contact${
failedVisibileToContacts.length == 1 ? "" : "s"
}. You must set them individually: ${failedVisibileToContacts.map((c) => c.name).join(", ")}`,
},
-1,
);
@@ -527,7 +375,6 @@ export default class ContactImportView extends Vue {
this.checkingImports = false;
// Show success notification
this.$notify(
{
group: "alert",
@@ -539,7 +386,7 @@ export default class ContactImportView extends Vue {
},
3000,
);
this.$router.push({ name: "contacts" });
(this.$router as Router).push({ name: "contacts" });
}
}
</script>
+10 -10
View File
@@ -10,7 +10,7 @@
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.back()"
>
<font-awesome icon="chevron-left" class="fa-fw" />
<fa icon="chevron-left" class="fa-fw" />
</h1>
</div>
@@ -26,8 +26,10 @@
You aren't sharing your name, so quickly
<br />
<span
@click="
() => $refs.userNameDialog.open((name) => (this.givenName = name))
"
class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-1 rounded-md"
@click="() => $refs.userNameDialog.open((name) => (givenName = name))"
>
click here to set it for them.
</span>
@@ -36,18 +38,18 @@
<UserNameDialog ref="userNameDialog" />
<div
@click="onCopyUrlToClipboard()"
v-if="activeDid && activeDid.startsWith(ETHR_DID_PREFIX)"
class="text-center"
@click="onCopyUrlToClipboard()"
>
<!--
Play with display options: https://qr-code-styling.com/
See docs: https://www.npmjs.com/package/qr-code-generator-vue3
-->
<QRCodeVue3
:value="qrValue"
:corners-square-options="{ type: 'extra-rounded' }"
:dots-options="{ type: 'square' }"
:value="this.qrValue"
:cornersSquareOptions="{ type: 'extra-rounded' }"
:dotsOptions="{ type: 'square' }"
class="flex justify-center"
/>
<span>
@@ -56,14 +58,14 @@
</div>
<div v-else-if="activeDid" class="text-center">
<!-- Not an ETHR DID so force them to paste it. (Passkey Peer DIDs are too big.) -->
<span class="text-blue-500" @click="onCopyDidToClipboard()">
<span @click="onCopyDidToClipboard()" class="text-blue-500">
Click here to copy your DID to your clipboard.
</span>
<span>
Then give it to them so they can paste it in their list of People.
</span>
</div>
<div v-else class="text-center">
<div class="text-center" v-else>
You have no identitifiers yet, so
<router-link
:to="{ name: 'start' }"
@@ -108,7 +110,6 @@ import {
} from "../libs/endorserServer";
import { decodeEndorserJwt, ETHR_DID_PREFIX } from "../libs/crypto/vc";
import { retrieveAccountMetadata } from "../libs/util";
import { Router } from "vue-router";
@Component({
components: {
@@ -120,7 +121,6 @@ import { Router } from "vue-router";
})
export default class ContactQRScanShow extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
$router!: Router;
activeDid = "";
apiServer = "";
+2 -2
View File
@@ -7,8 +7,8 @@
<router-link
:to="{ name: 'account' }"
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
><font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
</router-link>
><fa icon="chevron-left" class="fa-fw"></fa
></router-link>
Scan Contact
</h1>
+56 -60
View File
@@ -23,26 +23,26 @@
<!-- New Contact -->
<div id="formAddNewContact" class="mt-4 mb-4 flex items-stretch">
<span v-if="isRegistered" class="flex">
<span class="flex" v-if="isRegistered">
<router-link
:to="{ name: 'invite-one' }"
class="flex items-center bg-gradient-to-b from-green-400 to-green-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-1 mr-1 rounded-md"
>
<font-awesome icon="envelope-open-text" class="fa-fw text-2xl" />
<fa icon="envelope-open-text" class="fa-fw text-2xl" />
</router-link>
<button
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()"
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"
>
<font-awesome icon="chair" class="fa-fw text-2xl" />
<fa icon="chair" class="fa-fw text-2xl" />
</button>
</span>
<span v-else class="flex">
<span
class="flex items-center bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-1 mr-1 rounded-md"
>
<font-awesome
<fa
icon="envelope-open-text"
class="fa-fw text-2xl"
@click="
@@ -56,7 +56,7 @@
<span
class="flex items-center bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-1 mr-1 rounded-md"
>
<font-awesome
<fa
icon="chair"
class="fa-fw text-2xl"
@click="
@@ -73,39 +73,38 @@
: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"
>
<font-awesome icon="qrcode" class="fa-fw text-2xl" />
<fa icon="qrcode" class="fa-fw text-2xl" />
</router-link>
<textarea
v-model="contactInput"
type="text"
placeholder="New URL or DID, Name, Public Key, Next Public Key Hash"
class="block w-full rounded-l border border-r-0 border-slate-400 px-3 py-2 h-10"
v-model="contactInput"
/>
<button
class="px-4 rounded-r bg-green-200 border border-l-0 border-green-400"
@click="onClickNewContact()"
>
<font-awesome icon="plus" class="fa-fw" />
<fa icon="plus" class="fa-fw" />
</button>
</div>
<div v-if="contacts.length > 0" class="flex justify-between">
<div class="flex justify-between" v-if="contacts.length > 0">
<div class="w-full text-left">
<div v-if="!showGiveNumbers">
<input
type="checkbox"
:checked="contactsSelected.length === contacts.length"
class="align-middle ml-2 h-6 w-6"
data-testId="contactCheckAllTop"
@click="
contactsSelected.length === contacts.length
? (contactsSelected = [])
: (contactsSelected = contacts.map((contact) => contact.did))
"
class="align-middle ml-2 h-6 w-6"
data-testId="contactCheckAllTop"
/>
<button
v-if="!showGiveNumbers"
href=""
class="text-md bg-gradient-to-b shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white ml-2 px-1 py-1 rounded-md"
:style="
@@ -113,16 +112,14 @@
? 'background-image: linear-gradient(to bottom, #3b82f6, #1e40af);'
: 'background-image: linear-gradient(to bottom, #94a3b8, #374151);'
"
data-testId="copySelectedContactsButtonTop"
@click="copySelectedContacts()"
v-if="!showGiveNumbers"
data-testId="copySelectedContactsButtonTop"
>
Copy Selections
</button>
<button @click="showCopySelectionsInfo()">
<font-awesome
icon="circle-info"
class="text-xl text-blue-500 ml-4"
/>
<fa icon="circle-info" class="text-xl text-blue-500 ml-4" />
</button>
</div>
</div>
@@ -139,20 +136,20 @@
</button>
</div>
</div>
<div v-if="showGiveNumbers" class="flex justify-between mt-1">
<div class="flex justify-between mt-1" v-if="showGiveNumbers">
<div class="w-full text-right">
In the following, only the most recent hours are included. To see more,
click
<span
class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1 py-1 rounded-md"
>
<font-awesome icon="file-lines" class="fa-fw" />
<fa icon="file-lines" class="fa-fw" />
</span>
<br />
<button
href=""
class="text-md bg-gradient-to-b shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-1 rounded-md mt-1"
:class="showGiveAmountsClassNames()"
v-bind:class="showGiveAmountsClassNames()"
@click="toggleShowGiveTotals()"
>
{{
@@ -162,38 +159,36 @@
? "Confirmed Amounts"
: "Unconfirmed Amounts"
}}
<font-awesome icon="left-right" class="fa-fw" />
<fa icon="left-right" class="fa-fw" />
</button>
</div>
</div>
<!-- Results List -->
<ul
v-if="contacts.length > 0"
id="listContacts"
v-if="contacts.length > 0"
class="border-t border-slate-300 mt-1"
>
<li
class="border-b border-slate-300 pt-1 pb-1"
v-for="contact in filteredContacts()"
:key="contact.did"
class="border-b border-slate-300 pt-1 pb-1"
data-testId="contactListItem"
>
<div class="grow overflow-hidden">
<div class="flex items-center">
<EntityIcon
:contact="contact"
:icon-size="24"
:iconSize="24"
class="inline-block align-text-bottom border border-slate-300 rounded cursor-pointer"
@click="showLargeIdenticon = contact"
/>
<input
v-if="!showGiveNumbers"
type="checkbox"
v-if="!showGiveNumbers"
:checked="contactsSelected.includes(contact.did)"
class="ml-2 h-6 w-6 flex-shrink-0"
data-testId="contactCheckOne"
@click="
contactsSelected.includes(contact.did)
? contactsSelected.splice(
@@ -202,6 +197,8 @@
)
: contactsSelected.push(contact.did)
"
class="ml-2 h-6 w-6 flex-shrink-0"
data-testId="contactCheckOne"
/>
<h2
@@ -218,10 +215,7 @@
}"
title="See more about this person"
>
<font-awesome
icon="circle-info"
class="text-xl text-blue-500 ml-4"
/>
<fa icon="circle-info" class="text-xl text-blue-500 ml-4" />
</router-link>
<span class="ml-4 text-sm overflow-hidden">{{
@@ -240,17 +234,17 @@
>
<button
class="text-sm bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-1.5 rounded-l-md"
:title="givenToMeDescriptions[contact.did] || ''"
@click="confirmShowGiftedDialog(contact.did, activeDid)"
:title="givenToMeDescriptions[contact.did] || ''"
>
From:
<br />
{{
/* eslint-disable prettier/prettier */
showGiveTotals
this.showGiveTotals
? ((givenToMeConfirmed[contact.did] || 0)
+ (givenToMeUnconfirmed[contact.did] || 0))
: showGiveConfirmed
: this.showGiveConfirmed
? (givenToMeConfirmed[contact.did] || 0)
: (givenToMeUnconfirmed[contact.did] || 0)
/* eslint-enable prettier/prettier */
@@ -259,17 +253,17 @@
<button
class="text-sm bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white -ml-1.5 px-2 py-1.5 rounded-r-md border-l"
:title="givenByMeDescriptions[contact.did] || ''"
@click="confirmShowGiftedDialog(activeDid, contact.did)"
:title="givenByMeDescriptions[contact.did] || ''"
>
To:
<br />
{{
/* eslint-disable prettier/prettier */
showGiveTotals
this.showGiveTotals
? ((givenByMeConfirmed[contact.did] || 0)
+ (givenByMeUnconfirmed[contact.did] || 0))
: showGiveConfirmed
: this.showGiveConfirmed
? (givenByMeConfirmed[contact.did] || 0)
: (givenByMeUnconfirmed[contact.did] || 0)
/* eslint-enable prettier/prettier */
@@ -278,8 +272,8 @@
<button
class="text-sm bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-1.5 rounded-md border border-blue-400"
data-testId="offerButton"
@click="openOfferDialog(contact.did, contact.name)"
data-testId="offerButton"
>
Offer
</button>
@@ -292,7 +286,7 @@
class="text-sm bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-1.5 rounded-md border border-slate-400"
title="See more given activity"
>
<font-awesome icon="file-lines" class="fa-fw" />
<fa icon="file-lines" class="fa-fw" />
</router-link>
</div>
</div>
@@ -301,21 +295,20 @@
</ul>
<p v-else>There are no contacts.</p>
<div v-if="contacts.length > 0" class="mt-2 w-full text-left">
<div class="mt-2 w-full text-left" v-if="contacts.length > 0">
<input
v-if="!showGiveNumbers"
type="checkbox"
v-if="!showGiveNumbers"
:checked="contactsSelected.length === contacts.length"
class="align-middle ml-2 h-6 w-6"
data-testId="contactCheckAllBottom"
@click="
contactsSelected.length === contacts.length
? (contactsSelected = [])
: (contactsSelected = contacts.map((contact) => contact.did))
"
class="align-middle ml-2 h-6 w-6"
data-testId="contactCheckAllBottom"
/>
<button
v-if="!showGiveNumbers"
href=""
class="text-md bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white ml-2 px-1 py-1 rounded-md"
:style="
@@ -324,6 +317,7 @@
: 'background-image: linear-gradient(to bottom, #94a3b8, #374151);'
"
@click="copySelectedContacts()"
v-if="!showGiveNumbers"
>
Copy Selections
</button>
@@ -339,7 +333,7 @@
>
<EntityIcon
:contact="showLargeIdenticon"
:icon-size="512"
:iconSize="512"
class="flex w-11/12 max-w-sm mx-auto mb-3 overflow-hidden bg-white rounded-lg shadow-lg"
@click="showLargeIdenticon = undefined"
/>
@@ -405,8 +399,6 @@ import { generateSaveAndActivateIdentity } from "../libs/util";
})
export default class ContactsView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
$route!: RouteLocationNormalizedLoaded;
$router!: Router;
activeDid = "";
apiServer = "";
@@ -473,7 +465,8 @@ export default class ContactsView extends Vue {
//
// For external links, use /contact-import/:jwt with a JWT that has an array of contacts
// because that will do better error checking for things like missing data on iOS platforms.
const importedContactJwt = this.$route.query["contactJwt"] as string;
const importedContactJwt = (this.$route as RouteLocationNormalizedLoaded)
.query["contactJwt"] as string;
if (importedContactJwt) {
// really should fully verify contents
const { payload } = decodeEndorserJwt(importedContactJwt);
@@ -488,13 +481,14 @@ export default class ContactsView extends Vue {
} as Contact;
await this.addContact(newContact);
// if we're here, they haven't redirected anywhere, so we'll redirect here without a query parameter
this.$router.push({ path: "/contacts" });
(this.$router as Router).push({ path: "/contacts" });
}
}
private async processInviteJwt() {
// handle an invite JWT sent via URL
const importedInviteJwt = this.$route.query["inviteJwt"] as string;
const importedInviteJwt = (this.$route as RouteLocationNormalizedLoaded)
.query["inviteJwt"] as string;
if (importedInviteJwt === "") {
// this happens when a platform (eg iOS) doesn't include anything after the "=" in a shared link.
this.$notify(
@@ -596,7 +590,7 @@ export default class ContactsView extends Vue {
);
}
// if we're here, they haven't redirected anywhere, so we'll redirect here without a query parameter
this.$router.push({ path: "/contacts" });
(this.$router as Router).push({ path: "/contacts" });
}
}
@@ -636,7 +630,7 @@ export default class ContactsView extends Vue {
title: "They're Added To Your List",
text: "Would you like to go to the main page now?",
onYes: async () => {
this.$router.push({ name: "home" });
(this.$router as Router).push({ name: "home" });
},
},
-1,
@@ -773,7 +767,9 @@ export default class ContactsView extends Vue {
if (contactInput.includes(CONTACT_IMPORT_CONFIRM_URL_PATH_TIME_SAFARI)) {
const jwt = getContactJwtFromJwtUrl(contactInput);
this.$router.push({ path: "/contact-import/" + jwt });
(this.$router as Router).push({
path: "/contact-import/" + jwt,
});
return;
}
@@ -881,7 +877,7 @@ export default class ContactsView extends Vue {
);
try {
const contacts = JSON.parse(jsonContactInput);
this.$router.push({
(this.$router as Router).push({
name: "contact-import",
query: { contacts: JSON.stringify(contacts) },
});
@@ -1207,7 +1203,7 @@ export default class ContactsView extends Vue {
this.showGiftedDialog(giverDid, recipientDid);
},
onYes: async () => {
this.$router.push({
(this.$router as Router).push({
name: "contact-amounts",
query: { contactDid: giverDid },
});
@@ -1407,10 +1403,10 @@ export default class ContactsView extends Vue {
if (hostResponse.data.data) {
// They're the host, take them to setup
this.$router.push({ name: "onboard-meeting-setup" });
(this.$router as Router).push({ name: "onboard-meeting-setup" });
} else {
// They're not the host, take them to list
this.$router.push({ name: "onboard-meeting-list" });
(this.$router as Router).push({ name: "onboard-meeting-list" });
}
} else {
// They're not in a meeting, show the dialog
@@ -1421,11 +1417,11 @@ export default class ContactsView extends Vue {
title: "Onboarding Meeting",
text: "Would you like to start a new meeting?",
onYes: async () => {
this.$router.push({ name: "onboard-meeting-setup" });
(this.$router as Router).push({ name: "onboard-meeting-setup" });
},
yesText: "Start New Meeting",
onNo: async () => {
this.$router.push({ name: "onboard-meeting-list" });
(this.$router as Router).push({ name: "onboard-meeting-list" });
},
noText: "Join Existing Meeting",
},
+38 -53
View File
@@ -9,10 +9,10 @@
<h1 id="ViewHeading" class="text-lg text-center font-light relative px-7">
<!-- Back -->
<button
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.go(-1)"
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
>
<font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
<fa icon="chevron-left" class="fa-fw"></fa>
</button>
Identifier Details
</h1>
@@ -29,20 +29,16 @@
<router-link
:to="{ name: 'contact-edit', params: { did: contactFromDid?.did } }"
>
<font-awesome icon="pen" class="text-sm text-blue-500 ml-2 mb-1" />
<fa icon="pen" class="text-sm text-blue-500 ml-2 mb-1" />
</router-link>
</h2>
<button
class="ml-2 mr-2 mt-4"
@click="showDidDetails = !showDidDetails"
class="ml-2 mr-2 mt-4"
>
Details
<font-awesome
v-if="showDidDetails"
icon="chevron-down"
class="text-blue-400"
/>
<font-awesome v-else icon="chevron-right" class="text-blue-400" />
<fa v-if="showDidDetails" icon="chevron-down" class="text-blue-400" />
<fa v-else icon="chevron-right" class="text-blue-400" />
</button>
<!-- Keep the dump contents directly between > and < to avoid weird spacing. -->
<pre
@@ -58,7 +54,7 @@
>
<EntityIcon
:icon-size="96"
:profile-image-url="contactFromDid?.profileImageUrl"
:profileImageUrl="contactFromDid?.profileImageUrl"
class="inline-block align-text-bottom border border-slate-300 rounded"
@click="showLargeIdenticonUrl = contactFromDid?.profileImageUrl"
/>
@@ -73,65 +69,61 @@
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"
title="They can see you"
@click="confirmSetVisibility(contactFromDid, false)"
title="They can see you"
>
<font-awesome icon="eye" class="fa-fw" />
<fa icon="eye" class="fa-fw" />
</button>
<button
v-else-if="
!contactFromDid?.seesMe && contactFromDid?.did !== activeDid
"
class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
title="They cannot see you"
@click="confirmSetVisibility(contactFromDid, true)"
title="They cannot see you"
>
<font-awesome icon="eye-slash" class="fa-fw" />
<fa icon="eye-slash" class="fa-fw" />
</button>
<button
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)"
title="Check Visibility"
v-if="contactFromDid?.did !== activeDid"
>
<font-awesome icon="rotate" class="fa-fw" />
<fa icon="rotate" class="fa-fw" />
</button>
</div>
<button
v-if="contactFromDid?.did !== activeDid"
class="text-sm uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white ml-6 mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
title="Registration"
@click="confirmRegister(contactFromDid)"
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"
title="Registration"
>
<font-awesome
<fa
v-if="contactFromDid?.registered"
icon="person-circle-check"
class="fa-fw"
/>
<font-awesome
v-else
icon="person-circle-question"
class="fa-fw"
/>
<fa v-else icon="person-circle-question" class="fa-fw" />
</button>
</div>
<button
@click="confirmDeleteContact(contactFromDid)"
class="text-sm uppercase bg-gradient-to-b from-rose-500 to-rose-800 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white ml-6 mx-0.5 my-0.5 px-2 py-1.5 rounded-md"
title="Delete"
@click="confirmDeleteContact(contactFromDid)"
>
<font-awesome icon="trash-can" class="fa-fw" />
<fa icon="trash-can" class="fa-fw" />
</button>
</div>
<div v-if="!contactFromDid?.profileImageUrl">
<div>Auto-Generated Icon</div>
<div class="flex justify-center">
<EntityIcon
:entity-id="viewingDid"
:icon-size="64"
:entityId="viewingDid"
:iconSize="64"
class="inline-block align-middle border border-slate-300 rounded-md mr-1"
@click="showLargeIdenticonId = viewingDid"
/>
@@ -146,9 +138,9 @@
class="absolute inset-0 h-screen flex flex-col items-center justify-center bg-slate-900/50"
>
<EntityIcon
:entity-id="showLargeIdenticonId"
:icon-size="512"
:profile-image-url="showLargeIdenticonUrl"
:entityId="showLargeIdenticonId"
:iconSize="512"
:profileImageUrl="showLargeIdenticonUrl"
class="flex w-11/12 max-w-sm mx-auto mb-3 overflow-hidden bg-white rounded-lg shadow-lg"
@click="
showLargeIdenticonId = undefined;
@@ -169,10 +161,10 @@
<!-- Loading Animation -->
<div
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"
v-if="isLoading"
>
<font-awesome icon="spinner" class="fa-spin-pulse"></font-awesome>
<fa icon="spinner" class="fa-spin-pulse"></fa>
</div>
<!-- Results List -->
<div v-if="claims.length > 0" class="mt-4">
@@ -183,9 +175,9 @@
<InfiniteScroll @reached-bottom="loadMoreData">
<ul>
<li
class="border-b border-slate-300"
v-for="claim in claims"
:key="claim.handleId"
class="border-b border-slate-300"
>
<div class="grid grid-cols-12 gap-4">
<span class="col-span-2">
@@ -201,11 +193,8 @@
{{ claimDescription(claim) }}
</span>
<span class="col-span-1">
<a class="cursor-pointer" @click="onClickLoadClaim(claim.id)">
<font-awesome
icon="file-lines"
class="pl-2 pt-1 text-blue-500"
/>
<a @click="onClickLoadClaim(claim.id)" class="cursor-pointer">
<fa icon="file-lines" class="pl-2 pt-1 text-blue-500" />
</a>
</span>
</div>
@@ -227,7 +216,7 @@
import { AxiosError } from "axios";
import * as yaml from "js-yaml";
import { Component, Vue } from "vue-facing-decorator";
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
import { Router } from "vue-router";
import QuickNav from "../components/QuickNav.vue";
import InfiniteScroll from "../components/InfiniteScroll.vue";
@@ -236,17 +225,15 @@ import { NotificationIface } from "../constants/app";
import { db, retrieveSettingsForActiveAccount } from "../db/index";
import { Contact } from "../db/tables/contacts";
import { BoundingBox } from "../db/tables/settings";
import {
GenericCredWrapper,
GenericVerifiableCredential,
GiveVerifiableCredential,
OfferVerifiableCredential,
} from "../interfaces";
import {
capitalizeAndInsertSpacesBeforeCaps,
didInfoForContact,
displayAmount,
getHeaders,
GenericCredWrapper,
GenericVerifiableCredential,
GiveVerifiableCredential,
OfferVerifiableCredential,
register,
setVisibilityUtil,
} from "../libs/endorserServer";
@@ -263,8 +250,6 @@ import EntityIcon from "../components/EntityIcon.vue";
})
export default class DIDView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
$route!: RouteLocationNormalizedLoaded;
$router!: Router;
libsUtil = libsUtil;
yaml = yaml;
@@ -367,7 +352,7 @@ export default class DIDView extends Vue {
},
3000,
);
this.$router.push({ name: "contacts" });
(this.$router as Router).push({ name: "contacts" });
}
// confirm to register a new contact
@@ -520,7 +505,7 @@ export default class DIDView extends Vue {
const route = {
path: "/claim/" + encodeURIComponent(jwtId),
};
this.$router.push(route);
(this.$router as Router).push(route);
}
public claimAmount(claim: GenericVerifiableCredential) {
+22 -31
View File
@@ -18,17 +18,17 @@
:style="{ visibility: isSearchVisible ? 'visible' : 'hidden' }"
>
<input
v-model="searchTerms"
type="text"
v-model="searchTerms"
placeholder="Search…"
class="block w-full rounded-l border border-r-0 border-slate-400 px-3 py-2"
@keyup.enter="searchSelected()"
v-on:keyup.enter="searchSelected()"
/>
<button
class="px-4 rounded-r bg-slate-200 border border-l-0 border-slate-400"
@click="searchSelected()"
>
<font-awesome icon="magnifying-glass" class="fa-fw"></font-awesome>
<fa icon="magnifying-glass" class="fa-fw"></fa>
</button>
</div>
@@ -39,7 +39,6 @@
<li>
<a
href="#"
:class="computedProjectsTabStyleClassNames()"
@click="
projects = [];
userProfiles = [];
@@ -47,6 +46,7 @@
isPeopleActive = false;
searchSelected();
"
v-bind:class="computedProjectsTabStyleClassNames()"
>
Projects
</a>
@@ -54,7 +54,6 @@
<li>
<a
href="#"
:class="computedPeopleTabStyleClassNames()"
@click="
projects = [];
userProfiles = [];
@@ -62,6 +61,7 @@
isPeopleActive = true;
searchSelected();
"
v-bind:class="computedPeopleTabStyleClassNames()"
>
People
</a>
@@ -75,7 +75,6 @@
<li>
<a
href="#"
:class="computedLocalTabStyleClassNames()"
@click="
projects = [];
userProfiles = [];
@@ -86,6 +85,7 @@
tempSearchBox = null;
searchLocal();
"
v-bind:class="computedLocalTabStyleClassNames()"
>
Nearby
<!-- restore when the links don't jump around for different numbers
@@ -101,7 +101,6 @@
<li>
<a
href="#"
:class="computedMappedTabStyleClassNames()"
@click="
projects = [];
userProfiles = [];
@@ -112,6 +111,7 @@
searchTerms = '';
tempSearchBox = null;
"
v-bind:class="computedMappedTabStyleClassNames()"
>
<!-- search is triggered when map component gets to "ready" state -->
Mapped
@@ -120,7 +120,6 @@
<li>
<a
href="#"
:class="computedRemoteTabStyleClassNames()"
@click="
projects = [];
userProfiles = [];
@@ -131,6 +130,7 @@
tempSearchBox = null;
searchAll();
"
v-bind:class="computedRemoteTabStyleClassNames()"
>
Anywhere
<!-- restore when the links don't jump around for different numbers
@@ -152,7 +152,7 @@
class="ml-2 mt-2 px-4 py-2 rounded-md bg-blue-200 text-blue-500"
@click="$router.push({ name: 'search-area' })"
>
<font-awesome icon="location-dot" class="fa-fw" />
<fa icon="location-dot" class="fa-fw" />
Select a {{ searchBox ? "Different" : "" }} Location for Nearby Search
</button>
</div>
@@ -179,10 +179,10 @@
<!-- Loading Animation -->
<div
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"
v-if="isLoading"
>
<font-awesome icon="spinner" class="fa-spin-pulse"></font-awesome>
<fa icon="spinner" class="fa-spin-pulse"></fa>
</div>
<div
v-else-if="projects.length === 0 && userProfiles.length === 0"
@@ -205,19 +205,19 @@
<!-- Projects List -->
<template v-if="isProjectsActive">
<li
class="border-b border-slate-300"
v-for="project in projects"
:key="project.handleId"
class="border-b border-slate-300"
>
<a
class="block py-4 flex gap-4 cursor-pointer"
@click="onClickLoadItem(project.handleId)"
class="block py-4 flex gap-4 cursor-pointer"
>
<div>
<ProjectIcon
:entity-id="project.handleId"
:icon-size="48"
:image-url="project.image"
:entityId="project.handleId"
:iconSize="48"
:imageUrl="project.image"
class="block border border-slate-300 rounded-md max-h-12 max-w-12"
/>
</div>
@@ -225,10 +225,7 @@
<div class="grow">
<h2 class="text-base font-semibold">{{ project.name }}</h2>
<div class="text-sm">
<font-awesome
icon="user"
class="fa-fw text-slate-400"
></font-awesome>
<fa icon="user" class="fa-fw text-slate-400"></fa>
{{
didInfo(
project.issuerDid,
@@ -246,20 +243,17 @@
<!-- Profiles List -->
<template v-else>
<li
class="border-b border-slate-300"
v-for="profile in userProfiles"
:key="profile.issuerDid"
class="border-b border-slate-300"
>
<a
class="block py-4 flex gap-4 cursor-pointer"
@click="onClickLoadItem(profile?.rowId || '')"
class="block py-4 flex gap-4 cursor-pointer"
>
<div class="grow">
<div class="text-sm">
<font-awesome
icon="user"
class="fa-fw text-slate-400"
></font-awesome>
<fa icon="user" class="fa-fw text-slate-400"></fa>
{{
didInfo(
profile.issuerDid,
@@ -279,10 +273,7 @@
v-if="isAnywhereActive && profile.locLat && profile.locLon"
class="mt-1 text-xs text-slate-500"
>
<font-awesome
icon="location-dot"
class="fa-fw"
></font-awesome>
<fa icon="location-dot" class="fa-fw"></fa>
{{
(profile.locLat > 0 ? "North" : "South") +
" in " +
@@ -322,11 +313,11 @@ import {
} from "../db/index";
import { Contact } from "../db/tables/contacts";
import { BoundingBox } from "../db/tables/settings";
import { PlanData } from "../interfaces";
import {
didInfo,
errorStringForLog,
getHeaders,
PlanData,
} from "../libs/endorserServer";
import { OnboardPage, retrieveAccountDids } from "../libs/util";
+56 -54
View File
@@ -13,7 +13,7 @@
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="cancelBack()"
>
<font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
<fa icon="chevron-left" class="fa-fw"></fa>
</h1>
</div>
@@ -44,9 +44,9 @@
>
</h1>
<textarea
v-model="description"
class="block w-full rounded border border-slate-400 mb-2 px-3 py-2"
placeholder="What was received"
v-model="description"
/>
<div class="flex flex-row justify-center">
<span
@@ -59,18 +59,18 @@
class="border border-r-0 border-slate-400 bg-slate-200 px-4 py-2"
@click="amountInput === '0' ? null : decrement()"
>
<font-awesome icon="chevron-left" />
<fa icon="chevron-left" />
</div>
<input
v-model="amountInput"
type="number"
class="border border-r-0 border-slate-400 px-2 py-2 text-center w-20"
v-model="amountInput"
/>
<div
class="rounded-r border border-slate-400 bg-slate-200 px-4 py-2"
@click="increment()"
>
<font-awesome icon="chevron-right" />
<fa icon="chevron-right" />
</div>
</div>
@@ -79,14 +79,14 @@
<a :href="imageUrl" target="_blank">
<img :src="imageUrl" class="h-24 rounded-xl" />
</a>
<font-awesome
<fa
icon="trash-can"
class="text-red-500 fa-fw ml-8 mt-10"
@click="confirmDeleteImage"
class="text-red-500 fa-fw ml-8 mt-10"
/>
</span>
<span v-else>
<font-awesome
<fa
icon="camera"
class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-2 rounded-md"
@click="openImageDialog"
@@ -101,11 +101,11 @@
<div class="flex">
<input
v-if="giverDid && !providedByProject"
v-model="providedByGiver"
type="checkbox"
class="h-6 w-6 mr-2"
v-model="providedByGiver"
/>
<font-awesome
<fa
v-else
icon="square"
class="mr-2 bg-white text-white h-5 w-5 px-0.5 py-0.5 rounded-sm"
@@ -117,7 +117,7 @@
: "No named individual gave."
}}
</label>
<font-awesome
<fa
v-if="!giverDid || providedByProject"
icon="info-circle"
class="-mt-1 bg-white text-slate-500 h-5 w-5 px-0.5 py-0.5 rounded-sm"
@@ -128,11 +128,11 @@
<div class="flex">
<input
v-if="providerProjectId && !providedByGiver"
v-model="providedByProject"
type="checkbox"
class="h-6 w-6 mr-2"
v-model="providedByProject"
/>
<font-awesome
<fa
v-else
icon="square"
class="mr-2 bg-white text-white h-5 w-5 px-0.5 py-0.5 rounded-sm"
@@ -144,7 +144,7 @@
: "This was not provided by a project."
}}
</label>
<font-awesome
<fa
v-if="!providerProjectId || providedByGiver"
icon="info-circle"
class="-mt-1 bg-white text-slate-500 h-5 w-5 px-0.5 py-0.5 rounded-sm"
@@ -154,7 +154,7 @@
</div>
<div class="flex-shrink flex justify-center items-center">
<font-awesome icon="arrow-right" class="fa-fw h-7" />
<fa icon="arrow-right" class="fa-fw h-7" />
</div>
<!-- Third Column for Recipient -->
@@ -162,11 +162,11 @@
<div class="flex">
<input
v-if="recipientDid && !givenToProject"
v-model="givenToRecipient"
type="checkbox"
class="h-6 w-6 mr-2"
v-model="givenToRecipient"
/>
<font-awesome
<fa
v-else
icon="square"
class="mr-2 bg-white text-white h-5 w-5 px-0.5 py-0.5 rounded-sm"
@@ -178,7 +178,7 @@
: "No individual benefitted."
}}
</label>
<font-awesome
<fa
v-if="!recipientDid || givenToProject"
icon="info-circle"
class="-mt-1 bg-white text-slate-500 h-5 w-5 px-0.5 py-0.5 rounded-sm"
@@ -189,11 +189,11 @@
<div class="flex">
<input
v-if="fulfillsProjectId && !givenToRecipient"
v-model="givenToProject"
type="checkbox"
class="h-6 w-6 mr-2"
v-model="givenToProject"
/>
<font-awesome
<fa
v-else
icon="square"
class="mr-2 bg-white text-white h-5 w-5 px-0.5 py-0.5 rounded-sm"
@@ -205,7 +205,7 @@
: "No project benefitted."
}}
</label>
<font-awesome
<fa
v-if="!fulfillsProjectId || givenToRecipient"
icon="info-circle"
class="-mt-1 bg-white text-slate-500 h-5 w-5 px-0.5 py-0.5 rounded-sm"
@@ -216,7 +216,7 @@
</div>
<div class="mt-8 flex">
<input v-model="isTrade" type="checkbox" class="h-6 w-6 mr-2" />
<input type="checkbox" class="h-6 w-6 mr-2" v-model="isTrade" />
<label class="text-sm mt-1">This was a trade (not a gift)</label>
</div>
@@ -236,7 +236,7 @@
<p class="text-center mb-2 mt-6 italic">
Sign & Send to publish to the world
<font-awesome
<fa
icon="circle-info"
class="pl-2 text-blue-500 cursor-pointer"
@click="explainData()"
@@ -261,20 +261,21 @@
<script lang="ts">
import { Component, Vue } from "vue-facing-decorator";
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
import { Router } from "vue-router";
import ImageMethodDialog from "../components/ImageMethodDialog.vue";
import QuickNav from "../components/QuickNav.vue";
import TopMessage from "../components/TopMessage.vue";
import { DEFAULT_IMAGE_API_SERVER, NotificationIface } from "../constants/app";
import { db, retrieveSettingsForActiveAccount } from "../db/index";
import { GenericCredWrapper, GiveVerifiableCredential } from "../interfaces";
import {
createAndSubmitGive,
didInfo,
editAndSubmitGive,
GenericCredWrapper,
getHeaders,
getPlanFromCache,
GiveVerifiableCredential,
hydrateGive,
} from "../libs/endorserServer";
import * as libsUtil from "../libs/util";
@@ -289,8 +290,6 @@ import { retrieveAccountDids } from "../libs/util";
})
export default class GiftedDetails extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
$route!: RouteLocationNormalizedLoaded;
$router!: Router;
activeDid = "";
apiServer = "";
@@ -323,9 +322,9 @@ export default class GiftedDetails extends Vue {
async mounted() {
try {
this.prevCredToEdit = (this.$route.query["prevCredToEdit"] as string)
this.prevCredToEdit = (this.$route as Router).query["prevCredToEdit"]
? (JSON.parse(
this.$route.query["prevCredToEdit"] as string,
(this.$route as Router).query["prevCredToEdit"],
) as GenericCredWrapper<GiveVerifiableCredential>)
: undefined;
} catch (error) {
@@ -342,23 +341,24 @@ export default class GiftedDetails extends Vue {
const prevAmount = this.prevCredToEdit?.claim?.object?.amountOfThisGood;
this.amountInput =
(this.$route.query["amountInput"] as string) ||
(this.$route as Router).query["amountInput"] ||
(prevAmount ? String(prevAmount) : "") ||
this.amountInput;
this.description =
(this.$route.query["description"] as string) ||
(this.$route as Router).query["description"] ||
this.prevCredToEdit?.claim?.description ||
this.description;
this.destinationPathAfter =
(this.$route.query["destinationPathAfter"] as string) || "";
this.giverDid = ((this.$route.query["giverDid"] as string) ||
(this.prevCredToEdit?.claim?.agent as unknown as { identifier: string })
?.identifier ||
this.destinationPathAfter = (this.$route as Router).query[
"destinationPathAfter"
];
this.giverDid = ((this.$route as Router).query["giverDid"] ||
this.prevCredToEdit?.claim?.agent?.identifier ||
this.giverDid) as string;
this.giverName = (this.$route.query["giverName"] as string) || "";
this.giverName =
((this.$route as Router).query["giverName"] as string) || "";
this.hideBackButton =
(this.$route.query["hideBackButton"] as string) === "true";
this.message = (this.$route.query["message"] as string) || "";
(this.$route as Router).query["hideBackButton"] === "true";
this.message = ((this.$route as Router).query["message"] as string) || "";
// find any offer ID
const fulfills = this.prevCredToEdit?.claim?.fulfills;
@@ -368,7 +368,7 @@ export default class GiftedDetails extends Vue {
? [fulfills]
: [];
const offer = fulfillsArray.find((rec) => rec["@type"] === "Offer");
this.offerId = ((this.$route.query["offerId"] as string) ||
this.offerId = ((this.$route as Router).query["offerId"] ||
offer?.identifier ||
this.offerId) as string;
@@ -378,7 +378,7 @@ export default class GiftedDetails extends Vue {
);
// eslint-disable-next-line prettier/prettier
this.fulfillsProjectId =
((this.$route.query["fulfillsProjectId"] as string) ||
((this.$route as Router).query["fulfillsProjectId"] ||
fulfillsProject?.identifier ||
this.fulfillsProjectId) as string;
@@ -392,38 +392,40 @@ export default class GiftedDetails extends Vue {
const providerProject = providerArray.find(
(rec) => rec["@type"] === "PlanAction",
);
this.providerProjectId = ((this.$route.query[
this.providerProjectId = ((this.$route as Router).query[
"providerProjectId"
] as string) ||
] ||
providerProject?.identifier ||
this.providerProjectId) as string;
this.recipientDid = ((this.$route.query["recipientDid"] as string) ||
this.recipientDid = ((this.$route as Router).query["recipientDid"] ||
this.prevCredToEdit?.claim?.recipient?.identifier) as string;
this.recipientName = (this.$route.query["recipientName"] as string) || "";
this.unitCode = ((this.$route.query["unitCode"] as string) ||
this.recipientName =
((this.$route as Router).query["recipientName"] as string) || "";
this.unitCode = ((this.$route as Router).query["unitCode"] ||
this.prevCredToEdit?.claim?.object?.unitCode ||
this.unitCode) as string;
this.imageUrl = ((this.$route.query["imageUrl"] as string) ||
this.imageUrl =
((this.$route as Router).query["imageUrl"] as string) ||
this.prevCredToEdit?.claim?.image ||
localStorage.getItem("imageUrl") ||
this.imageUrl) as string;
this.imageUrl;
// this is an endpoint for sharing project info to highlight something given
// https://developer.mozilla.org/en-US/docs/Web/Manifest/share_target
if (this.$route.query["shareTitle"] as string) {
if ((this.$route as Router).query["shareTitle"]) {
this.description =
((this.$route.query["shareTitle"] as string) || "") +
((this.$route as Router).query["shareTitle"] as string) +
(this.description ? "\n" + this.description : "");
}
if (this.$route.query["shareText"] as string) {
if ((this.$route as Router).query["shareText"]) {
this.description =
(this.description ? this.description + "\n" : "") +
((this.$route.query["shareText"] as string) || "");
((this.$route as Router).query["shareText"] as string);
}
if (this.$route.query["shareUrl"] as string) {
this.imageUrl = this.$route.query["shareUrl"] as string;
if ((this.$route as Router).query["shareUrl"]) {
this.imageUrl = (this.$route as Router).query["shareUrl"] as string;
}
const settings = await retrieveSettingsForActiveAccount();
+1 -1
View File
@@ -11,7 +11,7 @@
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.back()"
>
<font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
<fa icon="chevron-left" class="fa-fw"></fa>
</h1>
</div>
+10 -11
View File
@@ -11,7 +11,7 @@
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.back()"
>
<font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
<fa icon="chevron-left" class="fa-fw"></fa>
</h1>
</div>
@@ -30,8 +30,8 @@
<p>
If this works then you're all set.
<button
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)"
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"
>
Send Yourself a Test Web Push Message (Through Push Server but
Skipping Client Filter)
@@ -140,7 +140,7 @@
class="text-blue-500"
target="_blank"
>
here <font-awesome icon="arrow-up-right-from-square" class="fa-fw" />
here <fa icon="arrow-up-right-from-square" class="fa-fw" />
</a>
</div>
@@ -186,7 +186,7 @@
class="text-blue-500"
target="_blank"
>
here <font-awesome icon="arrow-up-right-from-square" class="fa-fw" />
here <fa icon="arrow-up-right-from-square" class="fa-fw" />
</a>
</div>
</div>
@@ -199,7 +199,7 @@
<p>
Of course, you'll want to back up all your data first -- all seeds as
well as the contacts & settings -- on the Profile
<font-awesome icon="circle-user" /> page.
<fa icon="circle-user" /> page.
</p>
<p>
Here are instructions to uninstall the app and clear out caches and storage.
@@ -246,8 +246,8 @@
<h2 class="text-xl font-semibold mt-4">Tests</h2>
<button
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()"
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"
>
Send Test Notification Directly to Device (Not Through Push Server)
</button>
@@ -259,8 +259,8 @@
</p>
<button
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()"
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"
>
Show Web Push Subscription Info
</button>
@@ -272,8 +272,8 @@
</p>
<button
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)"
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"
>
Send Yourself a Test Web Push Message (Through Push Server but Skipping
Client Filter)
@@ -285,8 +285,8 @@
</p>
<button
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()"
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"
>
Send Yourself a Test Web Push Message (Through Push Server and Client
Filter)
@@ -313,12 +313,11 @@ import { DIRECT_PUSH_TITLE, sendTestThroughPushServer } from "../libs/util";
import PushNotificationPermission from "../components/PushNotificationPermission.vue";
import { db } from "../db/index";
import { MASTER_SETTINGS_KEY } from "../db/tables/settings";
import { Router } from "vue-router";
@Component({ components: { PushNotificationPermission, QuickNav } })
export default class HelpNotificationsView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
$router!: Router;
subscriptionJSON?: PushSubscriptionJSON;
async mounted() {
+15 -17
View File
@@ -19,14 +19,13 @@
: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"
>
<font-awesome icon="envelope-open-text" class="fa-fw text-xl"
<fa icon="envelope-open-text" class="fa-fw text-xl"
/></router-link>
</p>
<p>Then watch that page to see when they accept their invite.</p>
<p>
(That page is also reachable from the Contacts
<font-awesome icon="users" /> page though the invitation
<font-awesome icon="envelope-open-text" /> icon.)
(That page is also reachable from the Contacts <fa icon="users" /> page
though the invitation <fa icon="envelope-open-text" /> icon.)
</p>
<h1 class="mt-4 font-bold text-xl">Next Steps</h1>
@@ -36,13 +35,12 @@
<h1 class="font-bold text-xl">Without a backup, you can lose data.</h1>
<div>
<p>
Exporting backups (from the Account
<font-awesome icon="circle-user" /> screen) is important for the case
where they lose their device. This is especially true for the
Identifier Seed: that is theirs and and theirs alone, and currently
nobody else can recover it if they lose it. The good thing is that
anyone can create a new account and simply inform their network of
their new ID.
Exporting backups (from the Account <fa icon="circle-user" /> screen)
is important for the case where they lose their device. This is
especially true for the Identifier Seed: that is theirs and and theirs
alone, and currently nobody else can recover it if they lose it. The
good thing is that anyone can create a new account and simply inform
their network of their new ID.
</p>
</div>
</div>
@@ -56,7 +54,7 @@
<h1 class="font-bold text-xl">Add Contact & Register</h1>
<p>
You share even more information such as your picture and name when
you share with your QR code at these links: <font-awesome icon="qrcode" />
you share with your QR code at these links: <fa icon="qrcode" />
</p>
<p>
Scanning
@@ -72,14 +70,14 @@
</p>
<p>
2) Scan their QR, or have them tap on it to copy their info and send it to you.
Then you can add them to your Contacts <font-awesome icon="users" />
Then you can add them to your Contacts <fa icon="users" />
</p>
<p>
3) You can register them at their info page <font-awesome icon="circle-info" />
and click on the register button <font-awesome icon="person-circle-question" />
3) You can register them at their info page <fa icon="circle-info" />
and click on the register button <fa icon="person-circle-question" />
</p>
<p>
4) Add yourself to their Contacts <font-awesome icon="users" />
4) Add yourself to their Contacts <fa icon="users" />
</p>
</div>
@@ -96,7 +94,7 @@
<h1 class="font-bold text-xl">Enable Notifications</h1>
<div>
<p>
Enable notifications from the Account page <font-awesome icon="circle-user" />.
Enable notifications from the Account page <fa icon="circle-user" />.
Those notifications might show up on the device depending on your settings.
For the most reliable habits, set an alarm or do some other ritual to record gratitude every day.
</p>
+23 -24
View File
@@ -11,7 +11,7 @@
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.back()"
>
<font-awesome icon="chevron-left" class="fa-fw" />
<fa icon="chevron-left" class="fa-fw" />
</h1>
</div>
@@ -30,8 +30,8 @@
<p class="ml-4">
If you'd like to see the page-by-page help,
<span
class="text-blue-500 cursor-pointer"
@click="unsetFinishedOnboarding()"
class="text-blue-500 cursor-pointer"
>click here</span>.
</p>
@@ -61,7 +61,7 @@
<h2 class="text-xl font-semibold">I want to know more because...</h2>
<ul class="list-disc list-outside ml-4">
<li class="p-2">
<div class="text-blue-500" @click="showAlpha = !showAlpha">... I'm a member of Alpha chat.</div>
<div @click="showAlpha = !showAlpha" class="text-blue-500">... I'm a member of Alpha chat.</div>
<div v-if="showAlpha">
<p>
This is a project for public benefit. You are invited to add your gratitude
@@ -98,7 +98,7 @@
</div>
</li>
<li class="p-2">
<div class="text-blue-500" @click="showGroup = !showGroup">... I want to find a group I'll enjoy working with.</div>
<div @click="showGroup = !showGroup" class="text-blue-500">... I want to find a group I'll enjoy working with.</div>
<div v-if="showGroup">
<p>
This app encourages people to offer small bits of time to one another. It's a way to
@@ -114,7 +114,7 @@
</div>
</li>
<li class="p-2">
<div class="text-blue-500" @click="showCommunity = !showCommunity">... I want to participate in community projects.</div>
<div @click="showCommunity = !showCommunity" class="text-blue-500">... I want to participate in community projects.</div>
<div v-if="showCommunity">
<p>
These are mostly at the beginning stages, so any of them will appreciate your offers that show interest.
@@ -127,7 +127,7 @@
</div>
</li>
<li class="p-2">
<div class="text-blue-500" @click="showVerifiable = !showVerifiable">... I want to build with verifiable, private data.</div>
<div @click="showVerifiable = !showVerifiable" class="text-blue-500">... I want to build with verifiable, private data.</div>
<div v-if="showVerifiable">
<p>
Make your claims and get others to confirm them. Then you can use the API to pull your copy of all that
@@ -153,7 +153,7 @@
</div>
</li>
<li class="p-2">
<div class="text-blue-500" @click="showGovernance = !showGovernance">... I want to build governance organically.</div>
<div @click="showGovernance = !showGovernance" class="text-blue-500">... I want to build governance organically.</div>
<div v-if="showGovernance">
<p>
This requires motivated, dedicated citizens. The good thing is that dedication the primary ingredient;
@@ -172,7 +172,7 @@
</div>
</li>
<li class="p-2">
<div class="text-blue-500" @click="showBasics = !showBasics">... I want to supply life's basics freely.</div>
<div @click="showBasics = !showBasics" class="text-blue-500">... I want to supply life's basics freely.</div>
<div v-if="showBasics">
<p>
This platform is not optimal for balancing needs and resources at this point,
@@ -191,7 +191,7 @@
<h2 class="text-xl font-semibold">How do I get started?</h2>
<p>
Someone -- like the person who told you about this app -- needs to register you
on the Contacts <font-awesome icon="users" class="fa-fw" /> page.
on the Contacts <fa icon="users" class="fa-fw" /> page.
If you heard about this from our outreach, feel free to contact us (below) for a chat.
After someone registers you, you can register others.
</p>
@@ -219,7 +219,7 @@
</p>
<p>
If they are not nearby to scan QR codes, you each can tap on the QR code
and paste it into the text box on the Contacts <font-awesome icon="users" class="fa-fw" /> page.
and paste it into the text box on the Contacts <fa icon="users" class="fa-fw" /> page.
</p>
<h2 class="text-xl font-semibold">
@@ -244,7 +244,7 @@
</h2>
<ul class="list-disc list-outside ml-4">
<li>
Go to Your Identity <font-awesome icon="circle-user" class="fa-fw" /> page.
Go to Your Identity <fa icon="circle-user" class="fa-fw" /> page.
</li>
<li>
Click on "Backup Identifier Seed" and follow the instructions.
@@ -260,7 +260,7 @@
</h2>
<ul class="list-disc list-outside ml-4">
<li>
Go to Your Identity <font-awesome icon="circle-user" class="fa-fw" /> page.
Go to Your Identity <fa icon="circle-user" class="fa-fw" /> page.
</li>
<li>
Click on "Download Settings...". That will save a file to your
@@ -274,7 +274,7 @@
</h2>
<ul class="list-disc list-outside ml-4">
<li>
Go to Your Identity <font-awesome icon="circle-user" class="fa-fw" /> page,
Go to Your Identity <fa icon="circle-user" class="fa-fw" /> page,
tap on your image, and save it.
</li>
</ul>
@@ -315,7 +315,7 @@
</h2>
<ul class="list-disc list-outside ml-4">
<li>
Go to Your Identity <font-awesome icon="circle-user" class="fa-fw" /> page,
Go to Your Identity <fa icon="circle-user" class="fa-fw" /> page,
click Advanced, and follow the instructions for the Contacts & Settings Database "Import".
Beware that this will erase your existing contact & settings.
</li>
@@ -384,7 +384,7 @@
</h2>
<p>
There is an "Advanced" section at the bottom of the Profile
<font-awesome icon="circle-user" /> page.
<fa icon="circle-user" /> page.
</p>
<p>
There is even more functionality in a mobile app (and more
@@ -402,8 +402,8 @@
because they have not given you permission to see their information. Ask
them to add you to their contact list, and ask specifically to make sure
the eye next to your name is open like this
<font-awesome icon="eye" class="fa-fw" /> and not closed like this
<font-awesome icon="eye-slash" class="fa-fw" />.
<fa icon="eye" class="fa-fw" /> and not closed like this
<fa icon="eye-slash" class="fa-fw" />.
</p>
<p>
Sometimes the reason you don't see something is because the search
@@ -444,7 +444,7 @@
</li>
<li>
There may be a problem with your identity. Go to the Identity
<font-awesome icon="circle-user" class="fa-fw" /> page, then "Advanced", and "Switch Identifier"
<fa icon="circle-user" class="fa-fw" /> page, then "Advanced", and "Switch Identifier"
and you may see helpful info there. If it shows a problem, try adding your identifier again.
</li>
<li>
@@ -505,7 +505,7 @@
<ul class="list-disc list-outside ml-4">
<li>
If using notifications, a server stores push token data. That can be revoked at any time
by disabling notifications on the Profile <font-awesome icon="circle-user" class="fa-fw" /> page.
by disabling notifications on the Profile <fa icon="circle-user" class="fa-fw" /> page.
</li>
<li>
If sending images, a server stores them, too. They can be removed by editing the claim
@@ -529,17 +529,17 @@
If you have skills, contact us below.
If you have Bitcoin, donate to
<button
class="text-blue-500 ml-2"
@click="
doCopyTwoSecRedo(
'bc1q90v4ted6cpt63tjfh2lvd5xzfc67sd4g9w8xma',
() => (showDidCopy = !showDidCopy)
)
"
class="text-blue-500 ml-2"
>
bc1q90v4ted6cpt63tjfh2lvd5xzfc67sd4g9w8xma
<font-awesome v-show="!showDidCopy" icon="copy" class="text-sm text-slate-400 fa-fw" />
<font-awesome v-show="showDidCopy" icon="circle-check" class="text-sm text-green-500 fa-fw"/>
<fa v-show="!showDidCopy" icon="copy" class="text-sm text-slate-400 fa-fw" />
<fa v-show="showDidCopy" icon="circle-check" class="text-sm text-green-500 fa-fw"/>
</button>
You can donate online via
<a href="https://www.patreon.com/TimeSafari" target="_blank" class="text-blue-500">Patreon here</a>.
@@ -588,7 +588,6 @@ import {
@Component({ components: { QuickNav } })
export default class Help extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
$router!: Router;
package = Package;
commitHash = import.meta.env.VITE_GIT_HASH;
@@ -615,7 +614,7 @@ export default class Help extends Vue {
finishedOnboarding: false,
});
}
this.$router.push({ name: "home" });
(this.$router as Router).push({ name: "home" });
}
}
</script>
+110 -138
View File
@@ -28,7 +28,7 @@
width="30"
style="display: inline; margin: 0 5px; vertical-align: middle"
/>and then "Add to Home Screen"
<font-awesome icon="square-plus" title="Apple 'Add' icon" />
<fa icon="square-plus" title="Apple 'Add' icon" />
and go click on that new app.
</span>
<span
@@ -36,8 +36,12 @@
>
You should see a prompt to install, or you can click on the
top-right dots
<font-awesome icon="ellipsis-vertical" title="vertical ellipsis" />
/> and then "Install"<img
<fa
icon="ellipsis-vertical"
title="vertical ellipsis"
class="fa-fw"
/>
and then "Install"<img
src="../assets/help/install-android-chrome.png"
alt="Android 'install' icon"
width="30"
@@ -69,7 +73,7 @@
<div class="mb-8">
<div v-if="isCreatingIdentifier">
<p class="text-slate-500 text-center italic mt-4 mb-4">
<font-awesome icon="spinner" class="fa-spin-pulse" />
<fa icon="spinner" class="fa-spin-pulse" />
Loading&hellip;
</p>
</div>
@@ -87,8 +91,8 @@
To share, someone must register you.
<div class="block text-center">
<button
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()"
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"
>
Show them {{ PASSKEYS_ENABLED ? "default" : "your" }} identifier
info
@@ -112,10 +116,10 @@
<div class="flex">
<h2 class="text-xl font-bold">What have you seen someone do?</h2>
<button
class="ml-2 block text-xs text-center bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1 rounded-md"
@click="openGiftedPrompts()"
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"
>
<font-awesome icon="lightbulb" class="fa-fw" />
<fa icon="lightbulb" class="fa-fw" />
</button>
</div>
@@ -143,7 +147,7 @@
>
<EntityIcon
:contact="contact"
:icon-size="64"
:iconSize="64"
class="mx-auto border border-blue-500 rounded-md mb-1 cursor-pointer"
/>
<h3
@@ -177,35 +181,35 @@
class="absolute right-6 bottom-0 transform translate-y-1/2 text-center text-4xl leading-none bg-green-600 text-white w-14 py-2.5 rounded-full"
@click="openDialog()"
>
<font-awesome icon="plus" class="fa-fw" />
<fa icon="plus" class="fa-fw" />
</button>
</div>
<!-- Results List -->
<div class="bg-slate-100 rounded-md px-4 py-3 mt-4 mb-4">
<div class="mt-4 mb-4">
<div class="flex items-center mb-4">
<h2 class="text-xl font-bold">
<h2 class="text-xl font-bold flex items-center gap-4">
Latest Activity
<button @click="openFeedFilters()">
<span class="text-xs text-white">
<font-awesome
v-if="resultsAreFiltered()"
icon="filter"
class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] px-1 py-1.5 rounded-md"
/>
<font-awesome
v-else
icon="filter"
class="bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] px-1 py-1.5 rounded-md"
/>
</span>
<button
v-if="resultsAreFiltered()"
class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] px-3 py-1.5 rounded-md text-xs text-white"
@click="openFeedFilters()"
>
<fa icon="filter" class="fa-fw" />
</button>
<button
v-else
class="bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] px-3 py-1.5 rounded-md text-xs text-white"
@click="openFeedFilters()"
>
<fa icon="filter" class="fa-fw" />
</button>
</h2>
</div>
<div
class="border-t p-2 border-slate-300"
@click="goToActivityToUserPage()"
class="border-t p-2 border-slate-300"
>
<div class="flex justify-center">
<div
@@ -245,122 +249,25 @@
</div>
<InfiniteScroll @reached-bottom="loadMoreGives">
<ul id="listLatestActivity" class="border-t border-slate-300">
<li
<ul id="listLatestActivity" class="space-y-4">
<ActivityListItem
v-for="record in feedData"
:key="record.jwtId"
class="border-b border-slate-300 py-2"
>
<div
v-if="record.jwtId == feedLastViewedClaimId"
class="border-b border-slate-300 text-orange-400 pb-2 mb-2 font-bold text-sm"
>
You've already seen all the following
</div>
<div class="grid grid-cols-12">
<span class="pt-1 col-span-1 justify-self-start">
<span>
<font-awesome
icon="circle-user"
:class="
computeKnownPersonIconStyleClassNames(
record.giver.known || record.receiver.known,
)
"
@click="toastUser('This involves your contacts.')"
/>
<font-awesome
icon="gift"
class="pl-3 text-slate-500"
@click="toastUser('This is a gift.')"
/>
</span>
</span>
<span class="col-span-10 justify-self-stretch overflow-hidden">
<!-- show giver and/or receiver profiles... which seemed like a good idea but actually adds clutter
<span
v-if="
record.giver.profileImageUrl ||
record.receiver.profileImageUrl
"
>
<EntityIcon
v-if="record.agentDid !== activeDid"
:icon-size="32"
:profile-image-url="record.giver.profileImageUrl"
class="inline-block align-middle border border-slate-300 rounded-md mr-1"
/>
<font-awesome
v-if="
record.agentDid !== activeDid &&
record.recipientDid !== activeDid &&
!record.fulfillsPlanHandleId
"
icon="ellipsis"
class="text-slate"
/>
<EntityIcon
v-if="
record.recipientDid !== activeDid &&
!record.fulfillsPlanHandleId
"
:iconSize="32"
:profile-image-url="record.receiver.profileImageUrl"
class="inline-block align-middle border border-slate-300 rounded-md ml-1"
/>
</span>
-->
<span class="pl-2 block break-words">
{{ giveDescription(record) }}
</span>
<a @click="onClickLoadClaim(record.jwtId)">
<font-awesome
icon="file-lines"
class="pl-2 text-slate-500 cursor-pointer"
/>
</a>
</span>
<span class="col-span-1 justify-self-end">
<router-link
v-if="record.fulfillsPlanHandleId"
:to="
'/project/' +
encodeURIComponent(record.fulfillsPlanHandleId)
"
>
<font-awesome icon="hammer" class="text-blue-500" />
</router-link>
<router-link
v-if="record.providerPlanHandleId"
:to="
'/project/' +
encodeURIComponent(record.providerPlanHandleId)
"
>
<font-awesome icon="hammer" class="text-blue-500" />
</router-link>
</span>
</div>
<div v-if="record.image" class="w-full">
<div
class="cursor-pointer"
@click="openImageViewer(record.image)"
>
<img
:src="record.image"
class="w-full aspect-[3/2] object-cover rounded-xl mt-2"
alt="shared content"
@load="cacheImageData($event, record.image)"
/>
</div>
</div>
</li>
:record="record"
:lastViewedClaimId="feedLastViewedClaimId"
:isRegistered="isRegistered"
:activeDid="activeDid"
:confirmerIdList="record.confirmerIdList"
@loadClaim="onClickLoadClaim"
@viewImage="openImageViewer"
@cacheImage="cacheImageData"
@confirmClaim="confirmClaim"
/>
</ul>
</InfiniteScroll>
<div v-if="isFeedLoading">
<p class="text-slate-500 text-center italic mt-4 mb-4">
<font-awesome icon="spinner" class="fa-spin-pulse" /> Loading&hellip;
<fa icon="spinner" class="fa-spin-pulse" /> Loading&hellip;
</p>
</div>
<div v-if="!isFeedLoading && feedData.length === 0">
@@ -374,9 +281,9 @@
<ChoiceButtonDialog ref="choiceButtonDialog" />
<ImageViewer
v-model:is-open="isImageViewerOpen"
:image-url="selectedImage"
:image-data="selectedImageData"
v-model:is-open="isImageViewerOpen"
/>
</template>
@@ -397,6 +304,7 @@ import TopMessage from "../components/TopMessage.vue";
import UserNameDialog from "../components/UserNameDialog.vue";
import ChoiceButtonDialog from "../components/ChoiceButtonDialog.vue";
import ImageViewer from "../components/ImageViewer.vue";
import ActivityListItem from "../components/ActivityListItem.vue";
import {
AppString,
NotificationIface,
@@ -432,9 +340,10 @@ import {
OnboardPage,
registerSaveAndActivatePasskey,
} from "../libs/util";
import * as serverUtil from "../libs/endorserServer";
// import { fa0 } from "@fortawesome/free-solid-svg-icons";
interface GiveRecordWithContactInfo extends GiveSummaryRecord {
jwtId: string;
giver: {
displayName: string;
known: boolean;
@@ -463,6 +372,7 @@ interface GiveRecordWithContactInfo extends GiveSummaryRecord {
TopMessage,
UserNameDialog,
ImageViewer,
ActivityListItem,
},
})
export default class HomeView extends Vue {
@@ -556,6 +466,7 @@ export default class HomeView extends Vue {
if (resp.status === 200) {
await updateAccountSettings(this.activeDid, {
isRegistered: true,
...(await retrieveSettingsForActiveAccount()),
});
this.isRegistered = true;
}
@@ -728,7 +639,6 @@ export default class HomeView extends Vue {
const newRecord: GiveRecordWithContactInfo = {
...record,
jwtId: record.jwtId,
giver: didInfoForContact(
giverDid,
this.activeDid,
@@ -1000,5 +910,67 @@ export default class HomeView extends Vue {
this.selectedImage = imageUrl;
this.isImageViewerOpen = true;
}
async confirmClaim(record: GiveRecordWithContactInfo) {
this.$notify(
{
group: "modal",
type: "confirm",
title: "Confirm",
text: "Do you personally confirm that this is true?",
onYes: async () => {
const goodClaim = serverUtil.removeSchemaContext(
serverUtil.removeVisibleToDids(
serverUtil.addLastClaimOrHandleAsIdIfMissing(
record.fullClaim,
record.jwtId,
record.handleId,
),
),
);
const confirmationClaim = {
"@context": "https://schema.org",
"@type": "AgreeAction",
object: goodClaim,
};
const result = await serverUtil.createAndSubmitClaim(
confirmationClaim,
this.activeDid,
this.apiServer,
this.axios,
);
if (result.type === "success") {
this.$notify(
{
group: "alert",
type: "success",
title: "Success",
text: "Confirmation submitted.",
},
3000,
);
// Refresh the feed to show updated confirmation status
await this.updateAllFeed();
} else {
console.error("Error submitting confirmation:", result);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "There was a problem submitting the confirmation.",
},
5000,
);
}
},
},
-1,
);
}
}
</script>
+8 -15
View File
@@ -8,7 +8,7 @@
<router-link
:to="{ name: 'account' }"
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
><font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
><fa icon="chevron-left" class="fa-fw"></fa>
</router-link>
Switch Identity
@@ -22,10 +22,7 @@
v-if="activeDid && !activeDidInIdentities"
class="block bg-slate-100 rounded-md flex items-center px-4 py-3 mb-4"
>
<font-awesome
icon="circle-check"
class="fa-fw text-red-600 text-xl mr-3"
></font-awesome>
<fa icon="circle-check" class="fa-fw text-red-600 text-xl mr-3"></fa>
<div class="text-sm text-slate-500">
<div class="overflow-hidden truncate">
<b>ID:</b> <code>{{ activeDid }}</code>
@@ -48,12 +45,12 @@
class="flex flex-grow items-center bg-slate-100 rounded-md px-4 py-3 mb-2 truncate cursor-pointer"
@click="switchAccount(ident.did)"
>
<font-awesome
<fa
v-if="ident.did === activeDid"
icon="circle-check"
class="fa-fw text-blue-600 text-xl mr-3"
/>
<font-awesome
<fa
v-else
icon="circle"
class="fa-fw text-slate-400 text-xl mr-3"
@@ -65,13 +62,13 @@
</span>
</div>
<div>
<font-awesome
<fa
v-if="ident.did === activeDid"
icon="trash-can"
class="text-slate-400 text-xl ml-2 mr-2 cursor-pointer"
@click="notifyCannotDelete()"
/>
<font-awesome
<fa
v-else
icon="trash-can"
class="text-red-600 text-xl ml-2 mr-2 cursor-pointer"
@@ -117,7 +114,6 @@ import { retrieveAllAccountsMetadata } from "../libs/util";
@Component({ components: { QuickNav } })
export default class IdentitySwitcherView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
$router!: Router;
public activeDid = "";
public activeDidInIdentities = false;
@@ -135,10 +131,7 @@ export default class IdentitySwitcherView extends Vue {
const accounts = await retrieveAllAccountsMetadata();
for (let n = 0; n < accounts.length; n++) {
const acct = accounts[n];
this.otherIdentities.push({
id: (acct.id ?? 0).toString(),
did: acct.did,
});
this.otherIdentities.push({ id: acct.id as string, did: acct.did });
if (acct.did && this.activeDid === acct.did) {
this.activeDidInIdentities = true;
}
@@ -166,7 +159,7 @@ export default class IdentitySwitcherView extends Vue {
await db.settings.update(MASTER_SETTINGS_KEY, {
activeDid: did,
});
this.$router.push({ name: "account" });
(this.$router as Router).push({ name: "account" });
}
async deleteAccount(id: string) {
+11 -12
View File
@@ -5,10 +5,10 @@
<h1 class="text-lg text-center font-light relative px-7">
<!-- Cancel -->
<button
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.go(-1)"
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
>
<font-awesome icon="chevron-left"></font-awesome>
<fa icon="chevron-left"></fa>
</button>
Import Existing Identifier
</h1>
@@ -20,10 +20,10 @@
<!-- id used by puppeteer test script -->
<textarea
id="seed-input"
v-model="mnemonic"
type="text"
placeholder="Seed Phrase"
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
v-model="mnemonic"
/>
<h3
@@ -35,22 +35,22 @@
<div v-if="showAdvanced">
Enter a custom derivation path
<input
v-model="derivationPath"
type="text"
class="block w-full rounded border border-slate-400 mb-2 px-3 py-2"
v-model="derivationPath"
/>
<span class="ml-4">
For previous uPort or Endorser users,
<a
class="text-blue-500"
@click="derivationPath = UPORT_DERIVATION_PATH"
class="text-blue-500"
>
click here to use that value.
</a>
</span>
<div v-if="numAccounts == 1" class="mt-4">
<input v-model="shouldErase" type="checkbox" class="mr-2" />
<div class="mt-4" v-if="numAccounts == 1">
<input type="checkbox" class="mr-2" v-model="shouldErase" />
<label>Erase the previous identifier.</label>
</div>
@@ -65,15 +65,15 @@
<div class="mt-8">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
<button
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()"
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"
>
Import
</button>
<button
@click="onCancelClick()"
type="button"
class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md"
@click="onCancelClick()"
>
Cancel
</button>
@@ -111,7 +111,6 @@ export default class ImportAccountView extends Vue {
AppString = AppString;
$notify!: (notification: NotificationIface, timeout?: number) => void;
$router!: Router;
apiServer = "";
address = "";
@@ -131,7 +130,7 @@ export default class ImportAccountView extends Vue {
}
public onCancelClick() {
this.$router.back();
(this.$router as Router).back();
}
public isNotProdServer() {
@@ -171,7 +170,7 @@ export default class ImportAccountView extends Vue {
await db.settings.update(MASTER_SETTINGS_KEY, {
activeDid: newId.did,
});
this.$router.push({ name: "account" });
(this.$router as Router).push({ name: "account" });
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (err: any) {
console.error("Error saving mnemonic & updating settings:", err);
+12 -15
View File
@@ -5,10 +5,10 @@
<h1 class="text-lg text-center font-light relative px-7">
<!-- Cancel -->
<button
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.go(-1)"
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
>
<font-awesome icon="chevron-left"></font-awesome>
<fa icon="chevron-left"></fa>
</button>
Derive from Existing Identity
</h1>
@@ -25,21 +25,21 @@
</p>
<ul class="mb-4">
<li
class="block bg-slate-100 rounded-md flex items-center px-4 py-3 mb-2"
v-for="dids in didArrays"
:key="dids[0]"
class="block bg-slate-100 rounded-md flex items-center px-4 py-3 mb-2"
@click="switchAccount(dids[0])"
>
<font-awesome
<fa
v-if="dids[0] == selectedArrayFirstDid"
icon="circle"
class="fa-fw text-blue-500 text-xl mr-3"
></font-awesome>
<font-awesome
></fa>
<fa
v-else
icon="circle"
class="fa-fw text-slate-400 text-xl mr-3"
></font-awesome>
></fa>
<span class="overflow-hidden">
<div class="text-sm text-slate-500 truncate">
<code>{{ dids.join(",") }}</code>
@@ -51,15 +51,15 @@
<div class="mt-8">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
<button
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()"
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"
>
Increment and Import
</button>
<button
@click="onCancelClick()"
type="button"
class="block w-full text-center text-md uppercase bg-gradient-to-b from-slate-400 to-slate-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md"
@click="onCancelClick()"
>
Cancel
</button>
@@ -70,7 +70,7 @@
<script lang="ts">
import { Component, Vue } from "vue-facing-decorator";
import { Router, RouteLocationNormalizedLoaded } from "vue-router";
import { Router } from "vue-router";
import {
DEFAULT_ROOT_DERIVATION_PATH,
@@ -86,9 +86,6 @@ import { retrieveAllFullyDecryptedAccounts } from "../libs/util";
components: {},
})
export default class ImportAccountView extends Vue {
$route!: RouteLocationNormalizedLoaded;
$router!: Router;
derivationPath = DEFAULT_ROOT_DERIVATION_PATH;
didArrays: Array<Array<string>> = [];
selectedArrayFirstDid = "";
@@ -105,7 +102,7 @@ export default class ImportAccountView extends Vue {
}
public onCancelClick() {
this.$router.back();
(this.$router as Router).back();
}
public switchAccount(did: string) {
@@ -154,7 +151,7 @@ export default class ImportAccountView extends Vue {
await db.settings.update(MASTER_SETTINGS_KEY, {
activeDid: newId.did,
});
this.$router.push({ name: "account" });
(this.$router as Router).push({ name: "account" });
} catch (err) {
console.error("Error saving mnemonic & updating settings:", err);
}
+3 -4
View File
@@ -5,7 +5,7 @@
v-if="checkingInvite"
class="text-lg text-center font-light relative px-7"
>
<font-awesome icon="spinner" class="fa-spin-pulse" />
<fa icon="spinner" class="fa-spin-pulse" />
</div>
<div v-else class="text-center mt-4">
<p>That invitation did not work.</p>
@@ -28,8 +28,8 @@
/>
<br />
<button
class="ml-2 p-2 bg-blue-500 text-white rounded"
@click="() => processInvite(inputJwt, true)"
class="ml-2 p-2 bg-blue-500 text-white rounded"
>
Accept
</button>
@@ -55,7 +55,6 @@ import { generateSaveAndActivateIdentity } from "../libs/util";
@Component({ components: { QuickNav } })
export default class InviteOneAcceptView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
$router!: Router;
activeDid: string = "";
apiServer: string = "";
@@ -124,7 +123,7 @@ export default class InviteOneAcceptView extends Vue {
// That's good enough for an initial check.
// Send them to the contacts page to finish, with inviteJwt in the query string.
this.$router.push({
(this.$router as Router).push({
name: "contacts",
query: { inviteJwt: jwt },
});
+8 -10
View File
@@ -9,7 +9,7 @@
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.back()"
>
<font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
<fa icon="chevron-left" class="fa-fw"></fa>
</h1>
</div>
@@ -40,7 +40,7 @@
class="fixed right-6 top-12 text-center text-4xl leading-none bg-green-600 text-white w-14 py-2.5 rounded-full"
@click="createInvite()"
>
<font-awesome icon="plus" class="fa-fw"></font-awesome>
<fa icon="plus" class="fa-fw"></fa>
</button>
<InviteDialog ref="inviteDialog" />
@@ -72,18 +72,16 @@
!invite.redeemedAt &&
invite.expiresAt > new Date().toISOString()
"
class="text-center text-blue-500 cursor-pointer"
:title="inviteLink(invite.jwt)"
@click="
copyInviteAndNotify(invite.inviteIdentifier, invite.jwt)
"
class="text-center text-blue-500 cursor-pointer"
:title="inviteLink(invite.jwt)"
>
{{ getTruncatedInviteId(invite.inviteIdentifier) }}
</span>
<span
v-else
class="text-center text-slate-500 cursor-pointer"
:title="inviteLink(invite.jwt)"
@click="
showInvite(
invite.inviteIdentifier,
@@ -91,6 +89,8 @@
invite.expiresAt < new Date().toISOString(),
)
"
class="text-center text-slate-500 cursor-pointer"
:title="inviteLink(invite.jwt)"
>
{{ getTruncatedInviteId(invite.inviteIdentifier) }}
</span>
@@ -106,7 +106,7 @@
<br />
{{ getTruncatedRedeemedBy(invite.redeemedBy) }}
<br />
<font-awesome
<fa
v-if="invite.redeemedBy && !contactsRedeemed[invite.redeemedBy]"
icon="plus"
class="bg-green-600 text-white px-1 py-1 rounded-full cursor-pointer"
@@ -114,7 +114,7 @@
/>
</td>
<td>
<font-awesome
<fa
icon="trash-can"
class="text-red-600 text-xl ml-2 mr-2 cursor-pointer"
@click="deleteInvite(invite.inviteIdentifier, invite.notes)"
@@ -132,7 +132,6 @@
import axios from "axios";
import { Component, Vue } from "vue-facing-decorator";
import { useClipboard } from "@vueuse/core";
import { Router } from "vue-router";
import ContactNameDialog from "../components/ContactNameDialog.vue";
import QuickNav from "../components/QuickNav.vue";
@@ -157,7 +156,6 @@ interface Invite {
})
export default class InviteOneView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
$router!: Router;
invites: Invite[] = [];
activeDid: string = "";
+11 -17
View File
@@ -6,10 +6,10 @@
<div id="ViewBreadcrumb" class="mb-8">
<h1 class="text-lg text-center font-light relative px-7">
<!-- Back -->
<font-awesome
<fa
icon="chevron-left"
class="fa-fw text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.back()"
class="fa-fw text-lg text-center px-2 py-1 absolute -left-2 -top-1"
/>
New Activity For You
</h1>
@@ -25,7 +25,7 @@
<span class="text-lg font-medium ml-4"
>New Offer{{ newOffersToUser.length === 1 ? "" : "s" }} To You</span
>
<font-awesome
<fa
v-if="newOffersToUser.length > 0"
:icon="showOffersDetails ? 'chevron-down' : 'chevron-right'"
class="cursor-pointer ml-4 mr-4 text-lg"
@@ -59,15 +59,12 @@
:to="{ path: '/claim/' + encodeURIComponent(offer.jwtId) }"
class="text-blue-500"
>
<font-awesome
icon="file-lines"
class="pl-2 text-blue-500 cursor-pointer"
/>
<fa icon="file-lines" class="pl-2 text-blue-500 cursor-pointer" />
</router-link>
<!-- New line that appears on hover or when the offer is clicked -->
<div
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)"
class="absolute left-0 w-full text-left text-gray-500 text-sm hidden group-hover:flex cursor-pointer items-center"
>
<span class="inline-block w-8 h-px bg-gray-500 mr-2" />
Click to keep all above as new offers
@@ -90,7 +87,7 @@
>New Offer{{ newOffersToUserProjects.length === 1 ? "" : "s" }} To
Your Projects</span
>
<font-awesome
<fa
v-if="newOffersToUserProjects.length > 0"
:icon="
showOffersToUserProjectsDetails ? 'chevron-down' : 'chevron-right'
@@ -128,15 +125,12 @@
:to="{ path: '/claim/' + encodeURIComponent(offer.jwtId) }"
class="text-blue-500"
>
<font-awesome
icon="file-lines"
class="pl-2 text-blue-500 cursor-pointer"
/>
<fa icon="file-lines" class="pl-2 text-blue-500 cursor-pointer" />
</router-link>
<!-- New line that appears on hover -->
<div
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)"
class="absolute left-0 w-full text-left text-gray-500 text-sm hidden group-hover:flex cursor-pointer items-center"
>
<span class="inline-block w-8 h-px bg-gray-500 mr-2" />
Click to keep all above as new offers
@@ -160,13 +154,13 @@ import {
updateAccountSettings,
} from "../db/index";
import { Contact } from "../db/tables/contacts";
import { Router } from "vue-router";
import { OfferSummaryRecord, OfferToPlanSummaryRecord } from "../interfaces";
import {
didInfo,
displayAmount,
getNewOffersToUser,
getNewOffersToUserProjects,
OfferSummaryRecord,
OfferToPlanSummaryRecord,
} from "../libs/endorserServer";
import { retrieveAccountDids } from "../libs/util";
@@ -175,7 +169,7 @@ import { retrieveAccountDids } from "../libs/util";
})
export default class NewActivityView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
$router!: Router;
activeDid = "";
allContacts: Array<Contact> = [];
allMyDids: string[] = [];
+5 -7
View File
@@ -5,20 +5,20 @@
<h1 class="text-lg text-center font-light relative px-7">
<!-- Cancel -->
<button
@click="$router.go(-1)"
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.back()"
>
<font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
<fa icon="chevron-left" class="fa-fw"></fa>
</button>
Edit Identity
</h1>
</div>
<input
v-model="givenName"
type="text"
placeholder="Name"
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
v-model="givenName"
/>
<div class="mt-8">
@@ -54,8 +54,6 @@ import { MASTER_SETTINGS_KEY } from "../db/tables/settings";
components: {},
})
export default class NewEditAccountView extends Vue {
$router!: Router;
givenName = "";
// 'created' hook runs when the Vue instance is first created
@@ -71,11 +69,11 @@ export default class NewEditAccountView extends Vue {
firstName: this.givenName,
lastName: "", // deprecated, pre v 0.1.3
});
this.$router.back();
(this.$router as Router).back();
}
onClickCancel() {
this.$router.back();
(this.$router as Router).back();
}
}
</script>
+28 -36
View File
@@ -8,10 +8,10 @@
<!-- Cancel -->
<!-- Back -->
<button
@click="$router.go(-1)"
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.back()"
>
<font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
<fa icon="chevron-left" class="fa-fw"></fa>
</button>
Edit Project Idea
</h1>
@@ -25,10 +25,10 @@
</div>
<input
v-model="fullClaim.name"
type="text"
placeholder="Idea Name"
class="block w-full rounded border border-slate-400 mb-4 px-3 py-2"
v-model="fullClaim.name"
/>
<div class="flex justify-center mt-4">
@@ -36,14 +36,14 @@
<a :href="imageUrl" target="_blank" class="text-blue-500 ml-4">
<img :src="imageUrl" class="h-24 rounded-xl" />
</a>
<font-awesome
<fa
icon="trash-can"
class="text-red-500 fa-fw ml-8 mt-10"
@click="confirmDeleteImage"
class="text-red-500 fa-fw ml-8 mt-10"
/>
</span>
<span v-else>
<font-awesome
<fa
icon="camera"
class="bg-gradient-to-b from-blue-400 to-blue-700 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-2 py-2 rounded-md"
@click="openImageDialog"
@@ -53,27 +53,27 @@
<ImageMethodDialog ref="imageDialog" />
<input
v-model="agentDid"
type="text"
placeholder="Other Authorized Representative"
class="mt-4 block w-full rounded border border-slate-400 px-3 py-2"
v-model="agentDid"
/>
<div class="mb-4">
<p v-if="activeDid != projectIssuerDid && agentDid != projectIssuerDid">
<span class="text-red-500">Beware!</span>
If you save this, the original project owner will no longer be able to
edit it.
<button class="text-blue-500" @click="agentDid = projectIssuerDid">
<button @click="agentDid = projectIssuerDid" class="text-blue-500">
Click here to make the original owner an authorized representative.
</button>
</p>
</div>
<textarea
v-model="fullClaim.description"
placeholder="Description"
class="block w-full rounded border border-slate-400 px-3 py-2"
rows="5"
v-model="fullClaim.description"
maxlength="5000"
></textarea>
<div class="text-xs text-slate-500 italic">
@@ -102,9 +102,9 @@
class="rounded border border-slate-400 px-3 py-2"
/>
<input
v-model="startTimeInput"
:disabled="!startDateInput"
placeholder="Start Time"
v-model="startTimeInput"
type="time"
class="rounded border border-slate-400 ml-2 px-3 py-2"
/>
@@ -127,9 +127,9 @@
class="ml-2 rounded border border-slate-400 px-3 py-2"
/>
<input
v-model="endTimeInput"
:disabled="!endDateInput"
placeholder="End Time"
v-model="endTimeInput"
type="time"
class="rounded border border-slate-400 ml-2 px-3 py-2"
/>
@@ -140,7 +140,7 @@
class="flex items-center mt-4"
@click="includeLocation = !includeLocation"
>
<input v-model="includeLocation" type="checkbox" class="mr-2" />
<input type="checkbox" class="mr-2" v-model="includeLocation" />
<label for="includeLocation">Include Location</label>
</div>
<div v-if="includeLocation" class="mb-4 aspect-video">
@@ -179,9 +179,9 @@
class="items-center mb-4"
>
<div class="flex" @click="sendToTrustroots = !sendToTrustroots">
<input v-model="sendToTrustroots" type="checkbox" class="mr-2" />
<input type="checkbox" class="mr-2" v-model="sendToTrustroots" />
<label>Send to Trustroots</label>
<font-awesome
<fa
icon="circle-info"
class="text-blue-500 ml-2 cursor-pointer"
@click.stop="showNostrPartnerInfo"
@@ -191,7 +191,7 @@
<div class="flex" @click="sendToTripHopping = !sendToTripHopping">
<input type="checkbox" class="mr-2" v-model="sendToTripHopping" />
<label>Send to TripHopping</label>
<font-awesome icon="circle-info" class="text-blue-500 ml-2 cursor-pointer" @click.stop="showNostrPartnerInfo" />
<fa icon="circle-info" class="text-blue-500 ml-2 cursor-pointer" @click.stop="showNostrPartnerInfo" />
</div>
-->
</div>
@@ -229,11 +229,10 @@
import "leaflet/dist/leaflet.css";
import { AxiosError, AxiosRequestHeaders } from "axios";
import { DateTime } from "luxon";
import { finalizeEvent } from "nostr-tools/lib/esm/index.js";
import {
accountFromExtendedKey,
extendedKeysFromSeedWords,
} from "nostr-tools/lib/esm/nip06.js";
import { finalizeEvent, serializeEvent } from "nostr-tools";
// these core imports could also be included as "import type ..."
import { EventTemplate, UnsignedEvent, VerifiedEvent } from "nostr-tools/core";
import * as nip06 from "nostr-tools/nip06";
import { Component, Vue } from "vue-facing-decorator";
import { LMap, LMarker, LTileLayer } from "@vue-leaflet/vue-leaflet";
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
@@ -246,30 +245,21 @@ import {
NotificationIface,
} from "../constants/app";
import { retrieveSettingsForActiveAccount } from "../db/index";
import { PlanVerifiableCredential } from "../interfaces";
import {
createEndorserJwtVcFromClaim,
getHeaders,
PlanVerifiableCredential,
} from "../libs/endorserServer";
import {
retrieveAccountCount,
retrieveFullyDecryptedAccount,
} from "../libs/util";
import {
EventTemplate,
UnsignedEvent,
VerifiedEvent,
} from "nostr-tools/lib/esm/index.js";
import { serializeEvent } from "nostr-tools/lib/esm/index.js";
@Component({
components: { ImageMethodDialog, LMap, LMarker, LTileLayer, QuickNav },
})
export default class NewEditProjectView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
$route!: RouteLocationNormalizedLoaded;
$router!: Router;
errNote(message: string) {
this.$notify(
{ group: "alert", type: "danger", title: "Error", text: message },
@@ -315,7 +305,8 @@ export default class NewEditProjectView extends Vue {
this.apiServer = settings.apiServer || "";
this.showGeneralAdvanced = !!settings.showGeneralAdvanced;
this.projectId = (this.$route.query["projectId"] as string) || "";
this.projectId =
(this.$route as RouteLocationNormalizedLoaded).query["projectId"] || "";
if (this.projectId) {
if (this.numAccounts === 0) {
@@ -600,7 +591,7 @@ export default class NewEditProjectView extends Vue {
}
}
this.$router.push({ path: "/project/" + projectPath });
(this.$router as Router).push({ path: "/project/" + projectPath });
} else {
console.error(
"Got unexpected 'data' inside response from server",
@@ -677,7 +668,7 @@ export default class NewEditProjectView extends Vue {
// remove any trailing '
const finalDerNumNoApostrophe = finalDerNum?.replace(/'/g, "");
const accountNum = Number(finalDerNumNoApostrophe || 0);
const extPubPri = extendedKeysFromSeedWords(
const extPubPri = nip06.extendedKeysFromSeedWords(
account?.mnemonic as string,
"",
accountNum,
@@ -685,7 +676,7 @@ export default class NewEditProjectView extends Vue {
const publicExtendedKey: string = extPubPri?.publicExtendedKey;
const privateExtendedKey = extPubPri?.privateExtendedKey;
const privateBytes: Uint8Array =
accountFromExtendedKey(privateExtendedKey).privateKey;
nip06.accountFromExtendedKey(privateExtendedKey).privateKey;
// No real content is necessary, we just want something signed,
// so we might as well use nostr libs for nostr functions.
// Besides: someday we may create real content that we can relay.
@@ -719,7 +710,8 @@ export default class NewEditProjectView extends Vue {
const endorserPartnerUrl = partnerServer + "/api/partner/link";
const timeSafariUrl = window.location.origin + "/claim/" + jwtId;
const content = this.fullClaim.name + " - see " + timeSafariUrl;
const publicKeyHex = accountFromExtendedKey(publicExtendedKey).publicKey;
const publicKeyHex =
nip06.accountFromExtendedKey(publicExtendedKey).publicKey;
const unsignedPayload: UnsignedEvent = {
// why doesn't "...signedPayload" work?
kind: signedPayload.kind,
@@ -818,7 +810,7 @@ export default class NewEditProjectView extends Vue {
}
public onCancelClick() {
this.$router.back();
(this.$router as Router).back();
}
public showNostrPartnerInfo() {
+6 -7
View File
@@ -11,7 +11,7 @@
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.back()"
>
<font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
<fa icon="chevron-left" class="fa-fw"></fa>
</h1>
</div>
@@ -25,16 +25,16 @@
<div />
<div v-if="loading">
<span class="text-xl">Creating...&nbsp;</span>
<font-awesome
<fa
icon="spinner"
class="fa-spin fa-spin-pulse"
color="green"
size="128"
></font-awesome>
></fa>
</div>
<div v-else>
<span class="text-xl">Created!</span>
<font-awesome
<fa
icon="burst"
class="fa-beat px-12"
color="green"
@@ -44,7 +44,7 @@
--fa-animation-iteration-count: 1;
--fa-beat-scale: 6;
"
></font-awesome>
></fa>
</div>
<div />
</div>
@@ -62,13 +62,12 @@ import QuickNav from "../components/QuickNav.vue";
@Component({ components: { QuickNav } })
export default class NewIdentifierView extends Vue {
loading = true;
$router!: Router;
async mounted() {
await generateSaveAndActivateIdentity();
this.loading = false;
setTimeout(() => {
this.$router.push({ name: "home" });
(this.$router as Router).push({ name: "home" });
}, 1000);
}
}
+31 -31
View File
@@ -13,7 +13,7 @@
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="cancelBack()"
>
<font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
<fa icon="chevron-left" class="fa-fw"></fa>
</h1>
</div>
@@ -33,9 +33,9 @@
>
</h1>
<textarea
v-model="descriptionOfItem"
class="block w-full rounded border border-slate-400 mb-2 px-3 py-2"
placeholder="What is offered"
v-model="descriptionOfItem"
data-testId="itemDescription"
/>
<div class="flex flex-row justify-center">
@@ -49,19 +49,19 @@
class="border border-r-0 border-slate-400 bg-slate-200 px-4 py-2"
@click="amountInput === '0' ? null : decrement()"
>
<font-awesome icon="chevron-left" />
<fa icon="chevron-left" />
</div>
<input
v-model="amountInput"
type="number"
class="border border-r-0 border-slate-400 px-2 py-2 text-center w-20"
v-model="amountInput"
data-testId="inputOfferAmount"
/>
<div
class="rounded-r border border-slate-400 bg-slate-200 px-4 py-2"
@click="increment()"
>
<font-awesome icon="chevron-right" />
<fa icon="chevron-right" />
</div>
</div>
@@ -72,9 +72,9 @@
Conditions
</span>
<textarea
v-model="descriptionOfCondition"
class="w-full border border-slate-400 px-3 py-2 rounded-r"
placeholder="Prerequisites, other people to include, etc."
v-model="descriptionOfCondition"
/>
</div>
@@ -94,11 +94,11 @@
<div class="h-7 mt-4 flex">
<input
v-if="projectId && !offeredToRecipient"
v-model="offeredToProject"
type="checkbox"
class="h-6 w-6 mr-2"
v-model="offeredToProject"
/>
<font-awesome
<fa
v-else
icon="square"
class="bg-slate-500 text-slate-500 h-5 w-5 px-0.5 py-0.5 mr-2 rounded"
@@ -116,11 +116,11 @@
<div class="h-7 mt-4 flex">
<input
v-if="recipientDid && !offeredToProject"
v-model="offeredToRecipient"
type="checkbox"
class="h-6 w-6 mr-2"
v-model="offeredToRecipient"
/>
<font-awesome
<fa
v-else
icon="square"
class="bg-slate-500 text-slate-500 h-5 w-5 px-0.5 py-0.5 mr-2 rounded"
@@ -151,7 +151,7 @@
<p class="text-center mb-2 mt-6 italic">
Sign & Send to publish to the world
<font-awesome
<fa
icon="circle-info"
class="pl-2 text-blue-500 cursor-pointer"
@click="explainData()"
@@ -176,19 +176,20 @@
<script lang="ts">
import { Component, Vue } from "vue-facing-decorator";
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
import { Router } from "vue-router";
import QuickNav from "../components/QuickNav.vue";
import TopMessage from "../components/TopMessage.vue";
import { NotificationIface } from "../constants/app";
import { db, retrieveSettingsForActiveAccount } from "../db/index";
import { GenericCredWrapper, OfferVerifiableCredential } from "../interfaces";
import {
createAndSubmitOffer,
didInfo,
editAndSubmitOffer,
GenericCredWrapper,
getPlanFromCache,
hydrateOffer,
OfferVerifiableCredential,
} from "../libs/endorserServer";
import * as libsUtil from "../libs/util";
import { retrieveAccountDids } from "../libs/util";
@@ -201,8 +202,6 @@ import { retrieveAccountDids } from "../libs/util";
})
export default class OfferDetailsView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
$route!: RouteLocationNormalizedLoaded;
$router!: Router;
activeDid = "";
apiServer = "";
@@ -230,9 +229,9 @@ export default class OfferDetailsView extends Vue {
async mounted() {
try {
this.prevCredToEdit = (this.$route.query["prevCredToEdit"] as string)
this.prevCredToEdit = (this.$route as Router).query["prevCredToEdit"]
? (JSON.parse(
this.$route.query["prevCredToEdit"] as string,
(this.$route as Router).query["prevCredToEdit"],
) as GenericCredWrapper<OfferVerifiableCredential>)
: undefined;
} catch (error) {
@@ -250,28 +249,28 @@ export default class OfferDetailsView extends Vue {
const prevAmount =
this.prevCredToEdit?.claim?.includesObject?.amountOfThisGood;
this.amountInput =
(this.$route.query["amountInput"] as string) ||
(this.$route as Router).query["amountInput"] ||
(prevAmount ? String(prevAmount) : "") ||
this.amountInput;
this.unitCode = ((this.$route.query["unitCode"] as string) ||
this.unitCode = ((this.$route as Router).query["unitCode"] ||
this.prevCredToEdit?.claim?.includesObject?.unitCode ||
this.unitCode) as string;
this.descriptionOfCondition =
this.prevCredToEdit?.claim?.description || this.descriptionOfCondition;
this.descriptionOfItem =
(this.$route.query["description"] as string) ||
(this.$route as Router).query["description"] ||
this.prevCredToEdit?.claim?.itemOffered?.description ||
this.descriptionOfItem;
this.destinationPathAfter =
(this.$route.query["destinationPathAfter"] as string) || "";
this.offererDid = ((this.$route.query["offererDid"] as string) ||
(this.prevCredToEdit?.claim?.agent as unknown as { identifier: string })
?.identifier ||
this.destinationPathAfter = (this.$route as Router).query[
"destinationPathAfter"
];
this.offererDid = ((this.$route as Router).query["offererDid"] ||
this.prevCredToEdit?.claim?.agent?.identifier ||
this.offererDid) as string;
this.hideBackButton =
(this.$route.query["hideBackButton"] as string) === "true";
this.message = (this.$route.query["message"] as string) || "";
(this.$route as Router).query["hideBackButton"] === "true";
this.message = ((this.$route as Router).query["message"] as string) || "";
// find any project ID
let project;
@@ -281,16 +280,17 @@ export default class OfferDetailsView extends Vue {
) {
project = this.prevCredToEdit?.claim?.itemOffered?.isPartOf;
}
this.projectId = ((this.$route.query["projectId"] as string) ||
this.projectId = ((this.$route as Router).query["projectId"] ||
project?.identifier ||
this.projectId) as string;
this.projectName = ((this.$route.query["projectName"] as string) ||
this.projectName = ((this.$route as Router).query["projectName"] ||
project?.name ||
this.projectName) as string;
this.recipientDid = ((this.$route.query["recipientDid"] as string) ||
this.recipientDid = ((this.$route as Router).query["recipientDid"] ||
this.prevCredToEdit?.claim?.recipient?.identifier) as string;
this.recipientName = (this.$route.query["recipientName"] as string) || "";
this.recipientName =
((this.$route as Router).query["recipientName"] as string) || "";
this.validThroughDateInput =
this.prevCredToEdit?.claim?.validThrough || this.validThroughDateInput;
+6 -7
View File
@@ -10,7 +10,7 @@
<!-- Loading State -->
<div v-if="isLoading" class="flex justify-center items-center py-8">
<font-awesome icon="spinner" class="fa-spin-pulse" />
<fa icon="spinner" class="fa-spin-pulse" />
</div>
<div v-else-if="attendingMeeting">
@@ -22,11 +22,11 @@
<div class="flex justify-between items-center">
<h2 class="text-xl font-medium">{{ attendingMeeting.name }}</h2>
<button
@click.stop="leaveMeeting"
class="text-red-600 hover:text-red-700 p-2"
title="Leave Meeting"
@click.stop="leaveMeeting"
>
<font-awesome icon="right-from-bracket" />
<fa icon="right-from-bracket" />
</button>
</div>
</div>
@@ -65,14 +65,14 @@
/>
<div class="flex justify-end space-x-4">
<button
class="px-4 py-2 bg-gray-200 rounded hover:bg-gray-300"
@click="cancelPasswordDialog"
class="px-4 py-2 bg-gray-200 rounded hover:bg-gray-300"
>
Cancel
</button>
<button
class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
@click="submitPassword"
class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
>
Submit
</button>
@@ -120,7 +120,6 @@ export default class OnboardMeetingListView extends Vue {
},
timeout?: number,
) => void;
$router!: Router;
activeDid = "";
apiServer = "";
@@ -258,7 +257,7 @@ export default class OnboardMeetingListView extends Vue {
if (postResult.data && postResult.data.success) {
// Navigate to members view with password and groupId
this.$router.push({
(this.$router as Router).push({
name: "onboard-meeting-members",
params: {
groupId: this.selectedMeeting.groupId.toString(),
+8 -8
View File
@@ -10,10 +10,10 @@
<!-- Loading Animation -->
<div
v-if="isLoading"
class="mt-16 text-center text-4xl bg-slate-400 text-white w-14 py-2.5 rounded-full mx-auto"
v-if="isLoading"
>
<font-awesome icon="spinner" class="fa-spin-pulse"></font-awesome>
<fa icon="spinner" class="fa-spin-pulse"></fa>
</div>
<!-- Error State -->
@@ -39,7 +39,7 @@
<script lang="ts">
import { Component, Vue } from "vue-facing-decorator";
import { RouteLocationNormalizedLoaded, Router } from "vue-router";
import { RouteLocation } from "vue-router";
import QuickNav from "../components/QuickNav.vue";
import TopMessage from "../components/TopMessage.vue";
@@ -69,17 +69,17 @@ export default class OnboardMeetingMembersView extends Vue {
firstName = "";
isRegistered = false;
isLoading = true;
$route!: RouteLocationNormalizedLoaded;
$router!: Router;
userNameDialog!: InstanceType<typeof UserNameDialog>;
$refs!: {
userNameDialog: InstanceType<typeof UserNameDialog>;
};
get groupId(): string {
return (this.$route.params.groupId as string) || "";
return (this.$route as RouteLocation).params.groupId as string;
}
get password(): string {
return (this.$route.query.password as string) || "";
return (this.$route as RouteLocation).query.password as string;
}
async created() {
+11 -12
View File
@@ -17,24 +17,24 @@
<div class="flex items-center">
<h2 class="text-2xl">Current Meeting</h2>
<button
@click="startEditing"
class="mb-4 text-blue-600 hover:text-blue-800 transition-colors duration-200 ml-2"
title="Edit Meeting"
@click="startEditing"
>
<font-awesome icon="pen" class="fa-fw" />
<fa icon="pen" class="fa-fw" />
<span class="sr-only">{{
isInCreateMode() ? "Create Meeting" : "Edit Meeting"
}}</span>
</button>
</div>
<button
@click="confirmDelete"
class="text-red-600 hover:text-red-800 transition-colors duration-200"
:disabled="isDeleting"
:class="{ 'opacity-50 cursor-not-allowed': isDeleting }"
title="Delete Meeting"
@click="confirmDelete"
>
<font-awesome icon="trash-can" class="fa-fw" />
<fa icon="trash-can" class="fa-fw" />
<span class="sr-only">{{
isDeleting ? "Deleting..." : "Delete Meeting"
}}</span>
@@ -72,14 +72,14 @@
</p>
<div class="flex justify-between space-x-4">
<button
class="px-4 py-2 bg-slate-500 text-white rounded hover:bg-slate-700"
@click="showDeleteConfirm = false"
class="px-4 py-2 bg-slate-500 text-white rounded hover:bg-slate-700"
>
Cancel
</button>
<button
class="px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700"
@click="deleteMeeting"
class="px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700"
>
Delete
</button>
@@ -101,8 +101,8 @@
</h2>
<!-- This is my first form. Not sure if I like it; will see if the browser benefits extend to the native app. -->
<form
class="space-y-4"
@submit.prevent="isInCreateMode() ? createMeeting() : updateMeeting()"
class="space-y-4"
>
<div>
<label
@@ -182,8 +182,8 @@
<button
v-if="isInEditOrCreateMode()"
type="button"
class="w-full bg-slate-500 text-white px-4 py-2 rounded-md hover:bg-slate-600"
@click="cancelEditing"
class="w-full bg-slate-500 text-white px-4 py-2 rounded-md hover:bg-slate-600"
>
Cancel
</button>
@@ -204,21 +204,20 @@
class="inline-block text-blue-600"
target="_blank"
>
&bull; Open shortcut page for members
<font-awesome icon="external-link" />
&bull; Open shortcut page for members <fa icon="external-link" />
</router-link>
<MembersList
:password="currentMeeting.password || ''"
:show-organizer-tools="true"
class="mt-4"
@error="handleMembersError"
class="mt-4"
/>
</div>
<div v-else-if="isLoading">
<div class="flex justify-center items-center h-full">
<font-awesome icon="spinner" class="fa-spin-pulse" />
<fa icon="spinner" class="fa-spin-pulse" />
</div>
</div>
</section>
+272 -338
View File
@@ -10,10 +10,10 @@
<h1 class="text-center text-lg font-light relative px-7">
<!-- Back -->
<button
@click="$router.go(-1)"
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.back()"
>
<font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
<fa icon="chevron-left" class="fa-fw"></fa>
</button>
Project Idea
</h1>
@@ -21,11 +21,11 @@
{{ name }}
<button
v-if="activeDid === issuer || activeDid === agentDid"
@click="onEditClick()"
title="Edit"
data-testId="editClaimButton"
@click="onEditClick()"
>
<font-awesome icon="pen" class="text-sm text-blue-500 ml-2 mb-1" />
<fa icon="pen" class="text-sm text-blue-500 ml-2 mb-1" />
</button>
</h2>
</div>
@@ -37,10 +37,10 @@
<div class="pb-4 flex gap-4">
<div class="pt-1">
<ProjectIcon
:entity-id="projectId"
:icon-size="64"
:image-url="imageUrl"
:link-to-full="true"
:entityId="projectId"
:iconSize="64"
:imageUrl="imageUrl"
:linkToFull="true"
class="block border border-slate-300 rounded-md max-h-16 max-w-16"
/>
</div>
@@ -48,10 +48,7 @@
<div class="overflow-hidden">
<div class="text-sm mb-3">
<div class="truncate">
<font-awesome
icon="user"
class="fa-fw text-slate-400"
></font-awesome>
<fa icon="user" class="fa-fw text-slate-400"></fa>
{{ issuerInfoObject?.displayName }}
<span v-if="!serverUtil.isEmptyOrHiddenDid(issuer)">
<a
@@ -59,14 +56,11 @@
target="_blank"
class="text-blue-500"
>
<font-awesome
icon="arrow-up-right-from-square"
class="fa-fw"
/>
<fa icon="arrow-up-right-from-square" class="fa-fw" />
</a>
</span>
<span v-else-if="serverUtil.isHiddenDid(issuer)">
<font-awesome
<fa
icon="info-circle"
class="fa-fw text-blue-500 cursor-pointer"
@click="openHiddenDidDialog()"
@@ -74,50 +68,35 @@
</span>
</div>
<div v-if="startTime">
<font-awesome
icon="calendar"
class="fa-fw text-slate-400"
></font-awesome>
<fa icon="calendar" class="fa-fw text-slate-400"></fa>
Starts {{ startTime }}
</div>
<div v-if="endTime">
<font-awesome
icon="calendar"
class="fa-fw text-slate-400"
></font-awesome>
<fa icon="calendar" class="fa-fw text-slate-400"></fa>
Ends {{ endTime }}
</div>
<div v-if="latitude || longitude">
<font-awesome
icon="location-dot"
class="fa-fw text-slate-400"
></font-awesome>
<fa icon="location-dot" class="fa-fw text-slate-400"></fa>
<a
:href="getOpenStreetMapUrl()"
target="_blank"
class="underline text-blue-500"
>Map View
<font-awesome
<fa
icon="arrow-up-right-from-square"
class="fa-fw text-blue-500"
/>
</a>
</div>
<div v-if="url">
<font-awesome
icon="globe"
class="fa-fw text-slate-400"
></font-awesome>
<fa icon="globe" class="fa-fw text-slate-400"></fa>
<a
:href="addScheme(url)"
target="_blank"
class="underline text-blue-500"
>
{{ domainForWebsite(url) }}
<font-awesome
icon="arrow-up-right-from-square"
class="fa-fw"
/>
{{ domainForWebsite(this.url) }}
<fa icon="arrow-up-right-from-square" class="fa-fw" />
</a>
</div>
</div>
@@ -129,23 +108,23 @@
{{ truncatedDesc }}
<a
v-if="description.length >= truncateLength"
class="uppercase text-xs font-semibold text-slate-700"
@click="expandText"
class="uppercase text-xs font-semibold text-slate-700"
>... Read More</a
>
</div>
<div v-else>
{{ description }}
<a
class="uppercase text-xs font-semibold text-slate-700"
@click="collapseText"
class="uppercase text-xs font-semibold text-slate-700"
>- Read Less</a
>
</div>
</div>
<a class="cursor-pointer" @click="onClickLoadClaim(projectId)">
<font-awesome icon="file-lines" class="pl-2 pt-1 text-blue-500" />
<a @click="onClickLoadClaim(projectId)" class="cursor-pointer">
<fa icon="file-lines" class="pl-2 pt-1 text-blue-500" />
</a>
</div>
</div>
@@ -163,8 +142,8 @@
<div class="text-center">
<div v-for="plan in fulfillersToThis" :key="plan.handleId">
<button
class="text-blue-500"
@click="onClickLoadProject(plan.handleId)"
class="text-blue-500"
>
{{ plan.name }}
</button>
@@ -184,8 +163,8 @@
<!-- centering because long, wrapped project names didn't left align with blank or "text-left" -->
<div class="text-center">
<button
class="text-blue-500"
@click="onClickLoadProject(fulfilledByThis.handleId)"
class="text-blue-500"
>
{{ fulfilledByThis.name }}
</button>
@@ -202,10 +181,7 @@
class="grid grid-cols-4 sm:grid-cols-5 md:grid-cols-6 gap-x-3 gap-y-5 text-center mb-5 mt-2"
>
<li @click="openGiftDialogToProject({ name: 'you', did: activeDid })">
<font-awesome
icon="hand"
class="fa-fw text-blue-500 text-5xl cursor-pointer"
/>
<fa icon="hand" class="fa-fw text-blue-500 text-5xl cursor-pointer" />
<h3
class="mt-5 text-xs text-blue-500 font-medium text-ellipsis whitespace-nowrap overflow-hidden cursor-pointer"
>
@@ -230,7 +206,7 @@
>
<EntityIcon
:contact="contact"
:icon-size="64"
:iconSize="64"
class="mx-auto border border-blue-300 rounded-md mb-1 cursor-pointer"
/>
<h3
@@ -242,14 +218,14 @@
<li>
<span
v-if="allContacts.length >= 5"
class="flex align-bottom text-xs text-blue-500 mt-12 cursor-pointer"
@click="onClickAllContactsGifting()"
class="flex align-bottom text-xs text-blue-500 mt-12 cursor-pointer"
>
... or someone else...
</span>
</li>
</ul>
<GiftedDialog ref="giveDialogToThis" :to-project-id="projectId" />
<GiftedDialog ref="giveDialogToThis" :toProjectId="this.projectId" />
</div>
<!-- Offers & Gifts to & from this -->
@@ -260,8 +236,8 @@
<div class="text-center">
<button
data-testId="offerButton"
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()"
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"
>
Offer to this (maybe with conditions)...
</button>
@@ -269,15 +245,15 @@
</div>
<OfferDialog
ref="customOfferDialog"
:project-id="projectId"
:project-name="name"
:projectId="this.projectId"
:projectName="this.name"
/>
<h3 class="text-lg font-bold mb-3 mt-4">Offered To This Idea</h3>
<div v-if="offersToThis.length === 0">
(None yet. Wanna
<span class="cursor-pointer text-blue-500" @click="openOfferDialog()"
<span @click="openOfferDialog()" class="cursor-pointer text-blue-500"
>offer something... especially if others join you</span
>?)
</div>
@@ -290,10 +266,7 @@
>
<div class="flex justify-between gap-4">
<span>
<font-awesome
icon="user"
class="fa-fw text-slate-400"
></font-awesome>
<fa icon="user" class="fa-fw text-slate-400"></fa>
{{
serverUtil.didInfo(
offer.offeredByDid,
@@ -304,31 +277,28 @@
}}
</span>
<span v-if="offer.amount" class="whitespace-nowrap">
<font-awesome
<fa
:icon="libsUtil.iconForUnitCode(offer.unit)"
class="fa-fw text-slate-400"
/>{{ offer.amount }}
</span>
</div>
<div v-if="offer.objectDescription" class="text-slate-500">
<font-awesome icon="comment" class="fa-fw text-slate-400" />
<fa icon="comment" class="fa-fw text-slate-400" />
{{ offer.objectDescription }}
</div>
<div class="flex justify-between">
<a
class="cursor-pointer"
@click="onClickLoadClaim(offer.jwtId as string)"
class="cursor-pointer"
>
<font-awesome
icon="file-lines"
class="pl-2 pt-1 text-blue-500"
/>
<fa icon="file-lines" class="pl-2 pt-1 text-blue-500" />
</a>
<a
v-if="checkIsFulfillable(offer)"
@click="onClickFulfillGiveToOffer(offer)"
>
<font-awesome
<fa
icon="hand-holding-heart"
class="text-blue-500 cursor-pointer"
/>
@@ -347,91 +317,147 @@
<div v-if="activeDid && isRegistered">
<div class="text-center">
<button
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()"
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"
>
Given To This...
</button>
</div>
</div>
<h3 class="text-lg font-bold mb-3 mt-4">Given To This Idea</h3>
<h3 class="text-lg font-bold mt-4">Given To This Idea</h3>
<div v-if="givesToThis.length === 0">
<div v-if="givesToThis.length === 0" class="text-sm">
(None yet. If you've seen something, say something by clicking a
contact above.)
</div>
<ul v-else class="text-sm border-t border-slate-300">
<li
v-for="give in givesToThis"
:key="give.id"
class="py-1.5 border-b border-slate-300"
>
<div class="flex justify-between gap-4">
<span>
<font-awesome icon="user" class="fa-fw text-slate-400" />
{{
serverUtil.didInfo(
give.agentDid,
activeDid,
allMyDids,
allContacts,
)
<div v-else class="mt-1 text-sm">
<!-- Totals section -->
<div class="mt-1 flex items-center min-h-[1.5rem]">
<div v-if="loadingTotals" class="flex-1">
<fa icon="spinner" class="fa-spin-pulse text-blue-500" />
</div>
<div v-else-if="givesTotalsByUnit.length > 0" class="flex-1">
<span class="font-semibold mr-2 shrink-0">Totals</span>
<span class="whitespace-nowrap overflow-hidden text-ellipsis">
<a
@click="totalsExpanded = !totalsExpanded"
class="cursor-pointer text-blue-500"
>
<!-- just show the hours, or alternatively whatever is first -->
<span v-if="givenTotalHours() > 0">
{{ givenTotalHours() }} {{ libsUtil.UNIT_SHORT["HUR"] }}
</span>
<span v-else>
{{ givesTotalsByUnit[0].amount }}
{{ libsUtil.UNIT_SHORT[givesTotalsByUnit[0].unit] }}
</span>
<span v-if="givesTotalsByUnit.length > 1">...</span>
<span>
<fa
:icon="totalsExpanded ? 'chevron-up' : 'chevron-right'"
class="fa-fw text-xs ml-1"
/>
</span>
</a>
<!-- show the full list when expanded -->
<div v-if="totalsExpanded">
<div
v-for="total in givesTotalsByUnit"
:key="total.unit"
class="ml-2"
>
<fa
:icon="libsUtil.iconForUnitCode(total.unit)"
class="fa-fw text-slate-400 mr-1"
/>
{{ total.amount }} {{ libsUtil.UNIT_LONG[total.unit] }}
</div>
</div>
</span>
</div>
<div v-else>
<span class="font-semibold mr-2 shrink-0">
{{ givesToThis.length }}{{ givesHitLimit ? "+" : "" }} record{{
givesToThis.length === 1 ? "" : "s"
}}
</span>
<span v-if="give.amount" class="whitespace-nowrap">
<font-awesome
:icon="libsUtil.iconForUnitCode(give.unit)"
class="fa-fw text-slate-400"
/>{{ give.amount }}
</span>
</div>
<div class="text-slate-500">
<font-awesome icon="calendar" class="fa-fw text-slate-400" />
{{ give.issuedAt?.substring(0, 10) }}
</div>
<div v-if="give.description" class="text-slate-500">
<font-awesome icon="comment" class="fa-fw text-slate-400" />
{{ give.description }}
</div>
<div class="flex justify-between">
<a @click="onClickLoadClaim(give.jwtId)">
<font-awesome
icon="file-lines"
class="text-blue-500 cursor-pointer"
/>
</a>
</div>
<a
v-if="
checkIsConfirmable(give) &&
!recentlyCheckedAndUnconfirmableJwts.includes(give.jwtId)
"
@click="deepCheckConfirmable(give)"
>
<font-awesome
icon="circle-check"
class="text-blue-500 cursor-pointer"
/>
</a>
<a v-else-if="checkingConfirmationForJwtId === give.jwtId">
<font-awesome icon="spinner" class="fa-spin-pulse" />
</a>
<a v-else @click="shallowNotifyWhyCannotConfirm(give)">
<font-awesome
icon="circle-check"
class="text-slate-500 cursor-pointer"
/>
</a>
</div>
<div v-if="give.fullClaim.image" class="flex justify-center">
<a :href="give.fullClaim.image" target="_blank">
<img :src="give.fullClaim.image" class="h-24 mt-2 rounded-xl" />
</a>
</div>
</li>
</ul>
<!-- List of gives -->
<ul class="mt-2 text-sm border-t border-slate-300">
<li
v-for="give in givesToThis"
:key="give.id"
class="py-1.5 border-b border-slate-300"
>
<div class="flex justify-between gap-4">
<span>
<fa icon="user" class="fa-fw text-slate-400" />
{{
serverUtil.didInfo(
give.agentDid,
activeDid,
allMyDids,
allContacts,
)
}}
</span>
<span v-if="give.amount" class="whitespace-nowrap">
<fa
:icon="libsUtil.iconForUnitCode(give.unit)"
class="fa-fw text-slate-400"
/>{{ give.amount }}
</span>
</div>
<div class="text-slate-500">
<fa icon="calendar" class="fa-fw text-slate-400" />
{{ give.issuedAt?.substring(0, 10) }}
</div>
<div v-if="give.description" class="text-slate-500">
<fa icon="comment" class="fa-fw text-slate-400" />
{{ give.description }}
</div>
<div class="flex justify-between">
<a @click="onClickLoadClaim(give.jwtId)">
<fa icon="file-lines" class="text-blue-500 cursor-pointer" />
</a>
<a
v-if="
checkIsConfirmable(give) &&
!recentlyCheckedAndUnconfirmableJwts.includes(give.jwtId)
"
@click="deepCheckConfirmable(give)"
>
<fa
icon="circle-check"
class="text-blue-500 cursor-pointer"
/>
</a>
<a v-else-if="checkingConfirmationForJwtId === give.jwtId">
<fa icon="spinner" class="fa-spin-pulse" />
</a>
<a v-else @click="shallowNotifyWhyCannotConfirm(give)">
<fa
icon="circle-check"
class="text-slate-500 cursor-pointer"
/>
</a>
</div>
<div v-if="give.fullClaim.image" class="flex justify-center">
<a :href="give.fullClaim.image" target="_blank">
<img
:src="give.fullClaim.image"
class="h-24 mt-2 rounded-xl"
/>
</a>
</div>
</li>
</ul>
</div>
<div v-if="givesHitLimit" class="text-center text-blue-500">
<button @click="loadGives()">Load More</button>
</div>
@@ -443,14 +469,17 @@
<div v-if="activeDid && isRegistered">
<div class="text-center">
<button
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()"
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"
>
Given By This...
</button>
</div>
</div>
<GiftedDialog ref="giveDialogFromThis" :from-project-id="projectId" />
<GiftedDialog
ref="giveDialogFromThis"
:fromProjectId="this.projectId"
/>
<h3 class="text-lg font-bold mb-3 mt-4">
Benefitted From This Project
@@ -476,26 +505,23 @@
}}
</span>
<span v-if="give.amount" class="whitespace-nowrap">
<font-awesome
<fa
:icon="libsUtil.iconForUnitCode(give.unit)"
class="fa-fw text-slate-400"
/>{{ give.amount }}
</span>
</div>
<div class="text-slate-500">
<font-awesome icon="calendar" class="fa-fw text-slate-400" />
<fa icon="calendar" class="fa-fw text-slate-400" />
{{ give.issuedAt?.substring(0, 10) }}
</div>
<div v-if="give.description" class="text-slate-500">
<font-awesome icon="comment" class="fa-fw text-slate-400" />
<fa icon="comment" class="fa-fw text-slate-400" />
{{ give.description }}
</div>
<div class="flex justify-between">
<a @click="onClickLoadClaim(give.jwtId)">
<font-awesome
icon="file-lines"
class="text-blue-500 cursor-pointer"
/>
<fa icon="file-lines" class="text-blue-500 cursor-pointer" />
</a>
<a
@@ -505,19 +531,13 @@
"
@click="deepCheckConfirmable(give)"
>
<font-awesome
icon="circle-check"
class="text-blue-500 cursor-pointer"
/>
<fa icon="circle-check" class="text-blue-500 cursor-pointer" />
</a>
<a v-else-if="checkingConfirmationForJwtId === give.jwtId">
<font-awesome icon="spinner" class="fa-spin-pulse" />
<fa icon="spinner" class="fa-spin-pulse" />
</a>
<a v-else @click="shallowNotifyWhyCannotConfirm(give)">
<font-awesome
icon="circle-check"
class="text-slate-500 cursor-pointer"
/>
<fa icon="circle-check" class="text-slate-500 cursor-pointer" />
</a>
</div>
<div v-if="give.fullClaim.image" class="flex justify-center">
@@ -541,15 +561,7 @@
import { AxiosError } from "axios";
import { Component, Vue } from "vue-facing-decorator";
import { Router } from "vue-router";
import {
GenericVerifiableCredential,
GenericCredWrapper,
GiveSummaryRecord,
GiveVerifiableCredential,
OfferSummaryRecord,
OfferVerifiableCredential,
PlanSummaryRecord,
} from "../interfaces";
import GiftedDialog from "../components/GiftedDialog.vue";
import OfferDialog from "../components/OfferDialog.vue";
import TopMessage from "../components/TopMessage.vue";
@@ -564,41 +576,18 @@ import {
} from "../db/index";
import { Contact } from "../db/tables/contacts";
import * as libsUtil from "../libs/util";
import {
GenericCredWrapper,
GiveSummaryRecord,
GiveVerifiableCredential,
OfferSummaryRecord,
OfferVerifiableCredential,
PlanSummaryRecord,
} from "../libs/endorserServer";
import * as serverUtil from "../libs/endorserServer";
import { retrieveAccountDids } from "../libs/util";
import HiddenDidDialog from "../components/HiddenDidDialog.vue";
/**
* Project View Component
* @author Matthew Raymer
*
* This component displays detailed project information and manages interactions including:
* - Project metadata (name, description, dates, location)
* - Issuer information and verification
* - Project contributions and fulfillments
* - Offers and gifts tracking
* - Contact interactions
*
* Data Flow:
* 1. Component loads with project ID from route
* 2. Fetches project data, contacts, and account settings
* 3. Loads related data (offers, gifts, fulfillments)
* 4. Updates UI with paginated results
*
* Security Features:
* - DID visibility controls
* - JWT validation for imports
* - Permission checks for actions
*
* State Management:
* - Maintains separate loading states for different data types
* - Handles pagination limits
* - Tracks confirmation states
*
* @see GiftedDialog for gift creation
* @see OfferDialog for offer creation
* @see HiddenDidDialog for DID privacy explanations
*/
@Component({
components: {
EntityIcon,
@@ -611,103 +600,52 @@ import HiddenDidDialog from "../components/HiddenDidDialog.vue";
},
})
export default class ProjectViewView extends Vue {
/** Notification function injected by Vue */
$notify!: (notification: NotificationIface, timeout?: number) => void;
/** Router instance for navigation */
$router!: Router;
// Account and Settings State
/** Currently active DID */
activeDid = "";
/** Project agent DID */
agentDid = "";
/** DIDs that can see the agent DID */
agentDidVisibleToDids: Array<string> = [];
/** All DIDs associated with current account */
allMyDids: Array<string> = [];
/** All known contacts */
allContacts: Array<Contact> = [];
/** API server endpoint */
apiServer = "";
/** Registration status of current user */
isRegistered = false;
// Project Data
/** Project description */
checkingConfirmationForJwtId = "";
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 */
givesToThis: Array<GiveSummaryRecord> = [];
givesHitLimit = false;
givesProvidedByThis: Array<GiveSummaryRecord> = [];
givesProvidedByHitLimit = false;
givesTotalsByUnit: Array<{ unit: string; amount: number }> = [];
imageUrl = "";
/** Project issuer DID */
isRegistered = false;
issuer = "";
/** Cached issuer information */
issuerInfoObject: {
known: boolean;
displayName: string;
profileImageUrl?: string;
} | null = null;
/** DIDs that can see issuer information */
issuerVisibleToDids: Array<string> = [];
/** Project location data */
latitude = 0;
loadingTotals = false;
longitude = 0;
/** Project name */
name = "";
/** Project ID (handle) */
projectId = "";
/** Project start time */
offersToThis: Array<OfferSummaryRecord> = [];
offersHitLimit = false;
projectId = ""; // handle ID
recentlyCheckedAndUnconfirmableJwts: string[] = [];
startTime = "";
/** Project URL */
totalsExpanded = false;
truncatedDesc = "";
truncateLength = 40;
url = "";
// Interaction Data
/** Gifts to this project */
givesToThis: Array<GiveSummaryRecord> = [];
/** Flag for gifts pagination */
givesHitLimit = false;
/** Gifts from this project */
givesProvidedByThis: Array<GiveSummaryRecord> = [];
/** Flag for provided gifts pagination */
givesProvidedByHitLimit = false;
/** Offers to this project */
offersToThis: Array<OfferSummaryRecord> = [];
/** Flag for offers pagination */
offersHitLimit = false;
// UI State
/** JWT being checked for confirmation */
checkingConfirmationForJwtId = "";
/** Recently checked unconfirmable JWTs */
recentlyCheckedAndUnconfirmableJwts: string[] = [];
truncatedDesc = "";
/** Truncation length */
truncateLength = 40;
// Utility References
libsUtil = libsUtil;
serverUtil = serverUtil;
/**
* Component lifecycle hook that initializes the project view
*
* Workflow:
* 1. Loads account settings and contacts
* 2. Retrieves all account DIDs
* 3. Extracts project ID from URL
* 4. Initializes project data loading
*
* @throws Logs errors but continues loading
* @emits Notification on profile loading errors
*/
async created() {
const settings = await retrieveSettingsForActiveAccount();
this.activeDid = settings.activeDid || "";
@@ -739,21 +677,26 @@ export default class ProjectViewView extends Vue {
this.projectId = decodeURIComponent(pathParam);
}
this.loadProject(this.projectId, this.activeDid);
this.loadTotals();
}
onEditClick() {
const route = {
name: "new-edit-project",
query: { projectId: this.projectId },
};
(this.$router as Router).push(route);
}
// Isn't there a better way to make this available to the template?
expandText() {
this.expanded = true;
}
collapseText() {
this.expanded = false;
}
/**
* Loads project data and related information
*
* Workflow:
* 1. Fetches project details from API
* 2. Updates component state with project data
* 3. Initializes related data loading (gifts, offers, fulfillments)
*
* @param projectId Project handle ID
* @param userDid Active user's DID
* @throws Logs errors and notifies user
* @emits Notification on loading errors
*/
async loadProject(projectId: string, userDid: string) {
this.projectId = projectId;
@@ -840,15 +783,6 @@ export default class ProjectViewView extends Vue {
this.loadPlanFulfilledBy();
}
/**
* Loads gifts made to this project
*
* Handles pagination and updates component state with results.
* Uses beforeId for pagination based on last loaded gift.
*
* @throws Logs errors and notifies user
* @emits Notification on loading errors
*/
async loadGives() {
const givesUrl =
this.apiServer +
@@ -896,15 +830,6 @@ export default class ProjectViewView extends Vue {
}
}
/**
* Loads gifts provided by this project
*
* Similar to loadGives but for outgoing gifts.
* Maintains separate pagination state.
*
* @throws Logs errors and notifies user
* @emits Notification on loading errors
*/
async loadGivesProvidedBy() {
const providedByUrl =
this.apiServer +
@@ -955,15 +880,6 @@ export default class ProjectViewView extends Vue {
}
}
/**
* Loads offers made to this project
*
* Handles pagination and filtering of valid offers.
* Updates component state with results.
*
* @throws Logs errors and notifies user
* @emits Notification on loading errors
*/
async loadOffers() {
const offersUrl =
this.apiServer +
@@ -1011,14 +927,6 @@ export default class ProjectViewView extends Vue {
}
}
/**
* Loads projects that fulfill this project
*
* Manages pagination state and updates component with results.
*
* @throws Logs errors and notifies user
* @emits Notification on loading errors
*/
async loadPlanFulfillersTo() {
const fulfillsUrl =
this.apiServer +
@@ -1067,14 +975,6 @@ export default class ProjectViewView extends Vue {
}
}
/**
* Loads project that this project fulfills
*
* Updates fulfilledByThis state with result.
*
* @throws Logs errors and notifies user
* @emits Notification on loading errors
*/
async loadPlanFulfilledBy() {
const fulfilledByUrl =
this.apiServer +
@@ -1114,23 +1014,6 @@ export default class ProjectViewView extends Vue {
}
}
onEditClick() {
const route = {
name: "new-edit-project",
query: { projectId: this.projectId },
};
this.$router.push(route);
}
// Isn't there a better way to make this available to the template?
expandText() {
this.expanded = true;
}
collapseText() {
this.expanded = false;
}
/**
* Handle clicking on a project entry found in the list
* @param id of the project
@@ -1139,7 +1022,7 @@ export default class ProjectViewView extends Vue {
const route = {
path: "/project/" + encodeURIComponent(projectId),
};
this.$router.push(route);
(this.$router as Router).push(route);
this.loadProject(projectId, this.activeDid);
}
@@ -1186,14 +1069,14 @@ export default class ProjectViewView extends Vue {
projectId: this.projectId,
},
};
this.$router.push(route);
(this.$router as Router).push(route);
}
onClickLoadClaim(jwtId: string) {
const route = {
path: "/claim/" + encodeURIComponent(jwtId),
};
this.$router.push(route);
(this.$router as Router).push(route);
}
checkIsFulfillable(offer: OfferSummaryRecord) {
@@ -1342,7 +1225,7 @@ export default class ProjectViewView extends Vue {
),
),
);
const confirmationClaim: GenericVerifiableCredential = {
const confirmationClaim: serverUtil.GenericVerifiableCredential = {
"@context": "https://schema.org",
"@type": "AgreeAction",
object: goodClaim,
@@ -1393,5 +1276,56 @@ export default class ProjectViewView extends Vue {
this.allMyDids,
);
}
async loadTotals() {
this.loadingTotals = true;
const url =
this.apiServer +
"/api/v2/report/givesToPlans?planIds=" +
encodeURIComponent(JSON.stringify([this.projectId]));
const headers = await serverUtil.getHeaders(this.activeDid);
try {
const resp = await this.axios.get(url, { headers });
if (resp.status === 200 && resp.data.data) {
// Calculate totals by unit
const totals: { [key: string]: number } = {};
resp.data.data.forEach((give: GiveSummaryRecord) => {
const amount = give.fullClaim.object?.amountOfThisGood;
const unit = give.fullClaim.object?.unitCode;
if (amount && unit) {
totals[unit] = (totals[unit] || 0) + amount;
}
});
// Convert totals object to array format
this.givesTotalsByUnit = Object.entries(totals).map(
([unit, amount]) => ({
unit,
amount,
}),
);
}
} catch (error) {
console.error("Error loading totals:", error);
this.$notify(
{
group: "alert",
type: "danger",
title: "Error",
text: "Failed to load totals for this project.",
},
5000,
);
} finally {
this.loadingTotals = false;
}
}
givenTotalHours(): number {
return (
this.givesTotalsByUnit.find((total) => total.unit === "HUR")?.amount || 0
);
}
}
</script>
+32 -40
View File
@@ -16,7 +16,6 @@
<li>
<a
href="#"
:class="computedOfferTabClassNames()"
@click="
offers = [];
projects = [];
@@ -24,6 +23,7 @@
showProjects = false;
loadOffers();
"
v-bind:class="computedOfferTabClassNames()"
>
Offers
</a>
@@ -31,7 +31,6 @@
<li>
<a
href="#"
:class="computedProjectTabClassNames()"
@click="
offers = [];
projects = [];
@@ -39,6 +38,7 @@
showProjects = true;
loadProjects();
"
v-bind:class="computedProjectTabClassNames()"
>
Projects
</a>
@@ -57,7 +57,7 @@
<button
class="px-4 rounded-r bg-slate-200 border border-l-0 border-slate-400"
>
<font-awesome icon="magnifying-glass" class="fa-fw"></font-awesome>
<fa icon="magnifying-glass" class="fa-fw"></fa>
</button>
</div>
-->
@@ -68,15 +68,15 @@
class="fixed right-6 top-24 text-center text-4xl leading-none bg-green-600 text-white w-14 py-2.5 rounded-full"
@click="onClickNewProject()"
>
<font-awesome icon="plus" class="fa-fw"></font-awesome>
<fa icon="plus" class="fa-fw"></fa>
</button>
<!-- Loading Animation -->
<div
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"
v-if="isLoading"
>
<font-awesome icon="spinner" class="fa-spin-pulse"></font-awesome>
<fa icon="spinner" class="fa-spin-pulse"></fa>
</div>
<!-- Offer Results List -->
@@ -90,22 +90,22 @@
</div>
<ul id="listOffers" class="border-t border-slate-300">
<li
class="border-b border-slate-300"
v-for="offer in offers"
:key="offer.handleId"
class="border-b border-slate-300"
>
<div class="block py-4 flex gap-4">
<div v-if="offer.fulfillsPlanHandleId" class="flex-none">
<ProjectIcon
:entity-id="offer.fulfillsPlanHandleId"
:icon-size="48"
:entityId="offer.fulfillsPlanHandleId"
:iconSize="48"
class="inline-block align-middle border border-slate-300 rounded-md max-h-12 max-w-12"
/>
</div>
<div v-if="offer.recipientDid" class="flex-none w-12">
<EntityIcon
:entity-id="offer.recipientDid"
:icon-size="48"
:entityId="offer.recipientDid"
:iconSize="48"
class="inline-block align-middle border border-slate-300 rounded-md"
/>
</div>
@@ -130,20 +130,17 @@
<span class="text-sm">
<span v-if="offer.amount">
<font-awesome
<fa
:icon="libsUtil.iconForUnitCode(offer.unit)"
class="fa-fw text-slate-400"
/>
<span v-if="offer.amountGiven >= offer.amount">
<font-awesome
icon="check-circle"
class="fa-fw text-green-500"
/>
<fa icon="check-circle" class="fa-fw text-green-500" />
All {{ offer.amount }} given
</span>
<span v-else>
<font-awesome
<fa
icon="triangle-exclamation"
class="fa-fw text-yellow-500"
/>
@@ -165,7 +162,7 @@
</span>
<span v-else>
<!-- only show icon if there's not already a warning -->
<font-awesome
<fa
v-if="offer.amountGiven >= offer.amount"
icon="triangle-exclamation"
class="fa-fw text-yellow-300"
@@ -179,16 +176,13 @@
<span v-else>
<!-- Non-amount offer -->
<span v-if="offer.nonAmountGivenConfirmed">
<font-awesome
icon="check-circle"
class="fa-fw text-green-500"
/>
<fa icon="check-circle" class="fa-fw text-green-500" />
{{ offer.nonAmountGivenConfirmed }}
{{ offer.nonAmountGivenConfirmed == 1 ? "give" : "gives" }}
are confirmed.
</span>
<span v-else>
<font-awesome
<fa
icon="triangle-exclamation"
class="fa-fw text-yellow-500"
/>
@@ -197,10 +191,10 @@
</span>
<a @click="onClickLoadClaim(offer.jwtId)">
<font-awesome
<fa
icon="file-lines"
class="pl-2 text-blue-500 cursor-pointer"
></font-awesome>
></fa>
</a>
</span>
</div>
@@ -215,7 +209,7 @@
You have not announced any projects.
<div v-if="isRegistered">
Hit the big
<font-awesome
<fa
icon="plus"
class="bg-green-600 text-white px-1.5 py-1 rounded-full"
/>
@@ -223,8 +217,8 @@
</div>
<div v-else>
<button
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()"
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"
>
Get someone to onboard you.
</button>
@@ -233,19 +227,19 @@
</div>
<ul id="listProjects" class="border-t border-slate-300">
<li
class="border-b border-slate-300"
v-for="project in projects"
:key="project.handleId"
class="border-b border-slate-300"
>
<a
class="block py-4 flex gap-4"
@click="onClickLoadProject(project.handleId)"
class="block py-4 flex gap-4"
>
<div class="flex-none">
<ProjectIcon
:entity-id="project.handleId"
:icon-size="48"
:image-url="project.image"
:entityId="project.handleId"
:iconSize="48"
:imageUrl="project.image"
class="inline-block align-middle border border-slate-300 rounded-md max-h-12 max-w-12"
/>
</div>
@@ -301,9 +295,7 @@ import { OnboardPage } from "../libs/util";
})
export default class ProjectsView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
$router!: Router;
errNote(message: string) {
errNote(message) {
this.$notify(
{ group: "alert", type: "danger", title: "Error", text: message },
5000,
@@ -425,7 +417,7 @@ export default class ProjectsView extends Vue {
const route = {
path: "/project/" + encodeURIComponent(id),
};
this.$router.push(route);
(this.$router as Router).push(route);
}
/**
@@ -435,14 +427,14 @@ export default class ProjectsView extends Vue {
const route = {
name: "new-edit-project",
};
this.$router.push(route);
(this.$router as Router).push(route);
}
onClickLoadClaim(jwtId: string) {
const route = {
path: "/claim/" + encodeURIComponent(jwtId),
};
this.$router.push(route);
(this.$router as Router).push(route);
}
/**
@@ -545,10 +537,10 @@ export default class ProjectsView extends Vue {
text: "If so, we'll use those with QR codes to share.",
onCancel: async () => {},
onNo: async () => {
this.$router.push({ name: "share-my-contact-info" });
(this.$router as Router).push({ name: "share-my-contact-info" });
},
onYes: async () => {
this.$router.push({ name: "contact-qr" });
(this.$router as Router).push({ name: "contact-qr" });
},
noText: "we will share another way",
yesText: "we are nearby with cameras",
+7 -7
View File
@@ -10,7 +10,7 @@
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.back()"
>
<font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
<fa icon="chevron-left" class="fa-fw"></fa>
</h1>
</div>
@@ -22,17 +22,17 @@
<div>
<h2 class="text-2xl m-2">You're Here</h2>
<div class="m-2 flex">
<input v-model="attended" type="checkbox" class="h-6 w-6" />
<input type="checkbox" v-model="attended" class="h-6 w-6" />
<span class="pb-2 pl-2 pr-2">Attended</span>
</div>
<div class="m-2 flex">
<input v-model="gaveTime" type="checkbox" class="h-6 w-6" />
<input type="checkbox" v-model="gaveTime" class="h-6 w-6" />
<span class="pb-2 pl-2 pr-2">Spent Time</span>
<span v-if="gaveTime">
<input
v-model="hoursStr"
type="text"
placeholder="How much time"
v-model="hoursStr"
size="1"
class="border border-slate-400 h-6 px-2"
/>
@@ -48,8 +48,8 @@
class="flex justify-center mt-4"
>
<button
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()"
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"
>
Sign & Send
</button>
@@ -90,7 +90,7 @@ import * as libsUtil from "../libs/util";
})
export default class QuickActionBvcBeginView extends Vue {
$notify!: (notification: NotificationIface, timeout?: number) => void;
$router!: Router;
attended = true;
gaveTime = true;
hoursStr = "1";
@@ -201,7 +201,7 @@ export default class QuickActionBvcBeginView extends Vue {
},
3000,
);
this.$router.push({ path: "/quick-action-bvc" });
(this.$router as Router).push({ path: "/quick-action-bvc" });
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
+13 -15
View File
@@ -10,7 +10,7 @@
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.back()"
>
<font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
<fa icon="chevron-left" class="fa-fw"></fa>
</h1>
</div>
@@ -22,16 +22,16 @@
<div>
<h2 class="text-2xl m-2">Confirm</h2>
<div v-if="loadingConfirms" class="flex justify-center">
<font-awesome icon="spinner" class="fa-spin-pulse" />
<fa icon="spinner" class="fa-spin-pulse" />
</div>
<div v-else-if="claimsToConfirm.length === 0">
There are no claims yet today for you to confirm.
</div>
<ul class="border-t border-slate-300 m-2">
<li
class="border-b border-slate-300 py-2"
v-for="record in claimsToConfirm"
:key="record.id"
class="border-b border-slate-300 py-2"
>
<div class="grid grid-cols-12">
<span class="col-span-11 justify-self-start">
@@ -39,7 +39,6 @@
<input
type="checkbox"
:checked="claimsToConfirmSelected.includes(record.id)"
class="mr-2 h-6 w-6"
@click="
claimsToConfirmSelected.includes(record.id)
? claimsToConfirmSelected.splice(
@@ -48,6 +47,7 @@
)
: claimsToConfirmSelected.push(record.id)
"
class="mr-2 h-6 w-6"
/>
</span>
{{
@@ -59,7 +59,7 @@
)
}}
<a @click="onClickLoadClaim(record.id)">
<font-awesome
<fa
icon="file-lines"
class="pl-2 text-blue-500 cursor-pointer"
/>
@@ -78,7 +78,7 @@
}}
so if you expected but do not see details from someone then ask them to
check that their activity is visible to you on their Contacts
<font-awesome icon="users" class="text-slate-500" />
<fa icon="users" class="text-slate-500" />
page.
</span>
</div>
@@ -96,18 +96,18 @@
<div>
<h2 class="text-2xl m-2">Anything else?</h2>
<div class="m-2 flex">
<input v-model="someoneGave" type="checkbox" class="h-6 w-6" />
<input type="checkbox" v-model="someoneGave" class="h-6 w-6" />
<span class="pb-2 pl-2 pr-2">The group provided</span>
<span v-if="someoneGave">
<input
v-model="description"
type="text"
v-model="description"
size="20"
class="border border-slate-400 h-6 px-2"
/>
<br />
(Everyone likes personalized messages! 😁 ... and for a pic:
<input v-model="supplyGiftDetails" type="checkbox" />)
<input type="checkbox" v-model="supplyGiftDetails" />)
</span>
<!-- This is to match input height to avoid shifting when hiding & showing. -->
<span v-else class="h-6">...</span>
@@ -119,8 +119,8 @@
class="flex justify-center mt-4"
>
<button
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()"
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"
>
Sign & Send
</button>
@@ -151,18 +151,16 @@ import {
retrieveSettingsForActiveAccount,
} from "../db/index";
import { Contact } from "../db/tables/contacts";
import {
GenericCredWrapper,
GenericVerifiableCredential,
ErrorResult,
} from "../interfaces";
import {
BVC_MEETUPS_PROJECT_CLAIM_ID,
claimSpecialDescription,
containsHiddenDid,
createAndSubmitConfirmation,
createAndSubmitGive,
GenericCredWrapper,
GenericVerifiableCredential,
getHeaders,
ErrorResult,
} from "../libs/endorserServer";
@Component({
+3 -5
View File
@@ -10,7 +10,7 @@
class="text-lg text-center px-2 py-1 absolute -left-2 -top-1"
@click="$router.back()"
>
<font-awesome icon="chevron-left" class="fa-fw"></font-awesome>
<fa icon="chevron-left" class="fa-fw"></fa>
</h1>
</div>
@@ -41,14 +41,12 @@ import { Component, Vue } from "vue-facing-decorator";
import QuickNav from "../components/QuickNav.vue";
import TopMessage from "../components/TopMessage.vue";
import { Router } from "vue-router";
@Component({
components: {
QuickNav,
TopMessage,
},
})
export default class QuickActionBvcView extends Vue {
$router!: Router;
}
export default class QuickActionBvcView extends Vue {}
</script>

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